mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-12-12 21:33:13 +00:00
Merge branch 'fork/boriswa/1849-1848-1920-1921-fix-bugs' into ticket-app-master
This commit is contained in:
6
.changes/v4.10.0.md
Normal file
6
.changes/v4.10.0.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## v4.10.0 - 2025-12-09
|
||||
### Feature
|
||||
* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results
|
||||
### Fixed
|
||||
* Remove dependency to package @symfony/ux-translator
|
||||
|
||||
6
.changes/v4.10.1.md
Normal file
6
.changes/v4.10.1.md
Normal file
@@ -0,0 +1,6 @@
|
||||
## v4.10.1 - 2025-12-11
|
||||
### Fixed
|
||||
* Fix missing translation variable in NewLocation component
|
||||
* ([#476](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/476)) Fix display of header for ByActivityNumberAggregator
|
||||
* Fix use of ByActivityNumberAggregator in combination with activity count exports
|
||||
* ([#483](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/483)) Tentatively fix usage of CTRL+C in collabora editor with chrome / edge browser
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -6,6 +6,20 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||
|
||||
|
||||
## v4.10.1 - 2025-12-11
|
||||
### Fixed
|
||||
* Fix missing translation variable in NewLocation component
|
||||
* ([#476](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/476)) Fix display of header for ByActivityNumberAggregator
|
||||
* Fix use of ByActivityNumberAggregator in combination with activity count exports
|
||||
* ([#483](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/483)) Tentatively fix usage of CTRL+C in collabora editor with chrome / edge browser
|
||||
|
||||
## v4.10.0 - 2025-12-09
|
||||
### Feature
|
||||
* [MR 928](https://gitlab.com/Chill-Projet/chill-bundles/-/merge_requests/928) [#462](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/462) Add the future appointments for the person in search results
|
||||
### Fixed
|
||||
* Remove dependency to package @symfony/ux-translator
|
||||
|
||||
|
||||
## v4.9.0 - 2025-12-05
|
||||
### Feature
|
||||
* ([#459](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/459)) Add a counter for invitations awaiting reply
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"ext-openssl": "*",
|
||||
"ext-redis": "*",
|
||||
"ext-zlib": "*",
|
||||
"chill-project/chill-zimbra-bundle": "@dev",
|
||||
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
||||
"champs-libres/wopi-lib": "dev-master@dev",
|
||||
"doctrine/data-fixtures": "^1.8",
|
||||
@@ -62,7 +61,6 @@
|
||||
"symfony/http-client": "^5.4",
|
||||
"symfony/http-foundation": "^5.4",
|
||||
"symfony/intl": "^5.4",
|
||||
"symfony/loco-translation-provider": "^6.0",
|
||||
"symfony/mailer": "^5.4",
|
||||
"symfony/messenger": "^5.4",
|
||||
"symfony/mime": "^5.4",
|
||||
@@ -119,7 +117,8 @@
|
||||
"symfony/runtime": "^5.4",
|
||||
"symfony/stopwatch": "^5.4",
|
||||
"symfony/var-dumper": "^5.4",
|
||||
"symfony/web-profiler-bundle": "^5.4"
|
||||
"symfony/web-profiler-bundle": "^5.4",
|
||||
"symfony/loco-translation-provider": "^6.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
|
||||
@@ -38,5 +38,4 @@ return [
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
|
||||
Chill\ZimbraBundle\ChillZimbraBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -11,3 +11,6 @@ services:
|
||||
autowire: true # Automatically injects dependencies in your services.
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
|
||||
when@dev:
|
||||
services:
|
||||
ChampsLibres\WopiLib\Contract\Service\ProofValidatorInterface: '@Chill\WopiBundle\Service\Wopi\NullProofValidator'
|
||||
|
||||
@@ -12,6 +12,8 @@ As Chill relies on the [symfony ](http://symfony.com) framework, reading the fra
|
||||
- [Messages to users](messages-to-users.md)
|
||||
- [Pagination](pagination.md)
|
||||
- [Localisation](localisation.md)
|
||||
- [Translation directives](translation_directives.md)
|
||||
- [Translation provider](translation_provider.md)
|
||||
- [Logging](logging.md)
|
||||
- [Database migrations](migrations.md)
|
||||
- [Searching](searching.md)
|
||||
|
||||
376
docs/source/development/translation_directives.md
Normal file
376
docs/source/development/translation_directives.md
Normal file
@@ -0,0 +1,376 @@
|
||||
# Translation Key Directives
|
||||
|
||||
These directives are meant to ensure better consistency across bundles, avoid duplication, and make keys more predictable.
|
||||
|
||||
## General Principles
|
||||
|
||||
1. **Use lowercase snake_case for all keys**
|
||||
|
||||
2. **Use dot-separated namespaces**
|
||||
The dot is used to reflect:
|
||||
- bundle
|
||||
- feature
|
||||
- sub-feature
|
||||
- key type
|
||||
|
||||
3. **Do not use spaces in keys**
|
||||
|
||||
4. **Avoid duplicating the same text in multiple places**
|
||||
When a translation is needed, try a search for the translation value first and see if it exists elsewhere
|
||||
|
||||
5. **If a key is used across multiple bundles, it must live in ChillMainBundle.**
|
||||
|
||||
6. **If a key is used across multiple bundles and is a generic term, it must be placed in the `common` namespace.**
|
||||
|
||||
## Key Structure
|
||||
|
||||
We use the following structure:
|
||||
|
||||
```
|
||||
<scope>.<feature>.<sub-feature>.<key-type>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
- `<scope>` identifies the bundle or shared context
|
||||
- `<feature>` identifies the part of the module using the translation
|
||||
- `<element>` describes the text purpose
|
||||
- `<subelement>` for a multi-level element (e.g., activity.export.person.count.description)
|
||||
|
||||
### Examples of scopes
|
||||
|
||||
- `activity` — ChillActivityBundle
|
||||
- `person` — ChillPersonBundle
|
||||
- `common` — neutral shared translation values
|
||||
|
||||
## Naming Scopes
|
||||
|
||||
### 1. Bundle-specific keys
|
||||
|
||||
For most things inside a bundle:
|
||||
|
||||
```
|
||||
activity.<feature>.<element>
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
activity.form.save
|
||||
activity.list.title
|
||||
activity.entity.type
|
||||
activity.menu.activities
|
||||
activity.controller.success_created
|
||||
```
|
||||
|
||||
### 2. Shared UI elements (buttons, labels, generic text)
|
||||
|
||||
These belong in the `common` namespace in ChillMainBundle:
|
||||
|
||||
```
|
||||
common.save
|
||||
common.delete
|
||||
common.edit
|
||||
common.filter
|
||||
common.duration_time
|
||||
```
|
||||
|
||||
## Translation Workflow
|
||||
|
||||
Use the following workflow when deciding where a key belongs:
|
||||
|
||||
1. **Is this text used in more than one bundle?**
|
||||
→ Place in `main` or `common`
|
||||
|
||||
2. **Is this text generic UI (button, label, pagination, yes/no)?**
|
||||
→ Place in `common`
|
||||
|
||||
3. **Is this text specific to one bundle and one feature?**
|
||||
→ Place in `<bundle>.feature.<element>`
|
||||
|
||||
4. **Is this text related to an entity or value object?**
|
||||
→ Place in `<bundle>.entity.<entityname>.<field>`
|
||||
|
||||
5. **Is this text used in forms?**
|
||||
→ `<bundle>.form.<field>` or `<bundle>.form.<action>`
|
||||
|
||||
6. **Is this text related to exports?**
|
||||
→ `<bundle>.export.<feature>.<column>`
|
||||
|
||||
7. **Is it related to filtering, searching or parameters?**
|
||||
→ `<bundle>.filter.<name>` or
|
||||
→ `<bundle>.filter.<feature>.<field>` for nested filters
|
||||
|
||||
## Examples Based on Translations Within ChillActivityBundle
|
||||
|
||||
Below are concrete examples from `ChillActivityBundle`, refactored according to the guidelines.
|
||||
|
||||
### General activity keys
|
||||
|
||||
Instead of scattered keys like:
|
||||
|
||||
```
|
||||
Show the activity
|
||||
Edit the activity
|
||||
Activity
|
||||
Duration time
|
||||
...
|
||||
```
|
||||
|
||||
We use:
|
||||
|
||||
```
|
||||
activity.general.show
|
||||
activity.general.edit
|
||||
activity.general.title
|
||||
activity.general.duration
|
||||
activity.general.travel_time
|
||||
activity.general.attendee
|
||||
activity.general.remark
|
||||
activity.general.no_comments
|
||||
```
|
||||
|
||||
### Forms
|
||||
|
||||
Instead of keys like:
|
||||
|
||||
```
|
||||
Activity creation
|
||||
Save activity
|
||||
Reset form
|
||||
Choose a type
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```
|
||||
activity.form.title_create
|
||||
activity.form.save
|
||||
activity.form.reset
|
||||
activity.form.choose_type
|
||||
activity.form.choose_duration
|
||||
```
|
||||
|
||||
Long lists (like durations) should be grouped:
|
||||
|
||||
```
|
||||
activity.form.duration.5min
|
||||
activity.form.duration.10min
|
||||
activity.form.duration.15min
|
||||
activity.form.duration.1h
|
||||
activity.form.duration.1h30
|
||||
activity.form.duration.2h
|
||||
...
|
||||
```
|
||||
|
||||
### Entities
|
||||
|
||||
Entity fields should follow:
|
||||
|
||||
```
|
||||
activity.entity.activity.date
|
||||
activity.entity.activity.comment
|
||||
activity.entity.activity.deleted
|
||||
activity.entity.location.name
|
||||
activity.entity.location.type
|
||||
```
|
||||
|
||||
### Controller messages
|
||||
|
||||
Instead of strings as keys:
|
||||
|
||||
```
|
||||
'Success : activity created!'
|
||||
'The form is not valid. The activity has not been created !'
|
||||
```
|
||||
|
||||
Use:
|
||||
|
||||
```
|
||||
activity.controller.success_created
|
||||
activity.controller.error_invalid_create
|
||||
activity.controller.success_updated
|
||||
activity.controller.error_invalid_update
|
||||
```
|
||||
|
||||
### Roles
|
||||
|
||||
Access control keys should be:
|
||||
|
||||
```
|
||||
activity.role.create
|
||||
activity.role.update
|
||||
activity.role.see
|
||||
activity.role.see_details
|
||||
activity.role.delete
|
||||
activity.role.stats
|
||||
activity.role.list
|
||||
```
|
||||
|
||||
### Admin
|
||||
|
||||
```
|
||||
activity.admin.configuration
|
||||
activity.admin.types
|
||||
activity.admin.reasons
|
||||
activity.admin.reason_category
|
||||
activity.admin.presence
|
||||
```
|
||||
|
||||
### CRUD
|
||||
|
||||
```
|
||||
activity.crud.type.title_new
|
||||
activity.crud.type.title_edit
|
||||
activity.crud.presence.title_new
|
||||
```
|
||||
|
||||
### Activity Reason
|
||||
|
||||
```
|
||||
activity.reason.list
|
||||
activity.reason.create
|
||||
activity.reason.active
|
||||
activity.reason.category
|
||||
activity.reason.entity_title
|
||||
```
|
||||
|
||||
### Exports
|
||||
|
||||
Group them logically:
|
||||
|
||||
```
|
||||
activity.export.person.count.title
|
||||
activity.export.person.count.description
|
||||
activity.export.person.count.header
|
||||
|
||||
activity.export.period.sum_duration.title
|
||||
activity.export.period.sum_duration.description
|
||||
activity.export.period.sum_duration.header
|
||||
```
|
||||
|
||||
### Filters
|
||||
|
||||
Use hierarchical filters:
|
||||
|
||||
```
|
||||
activity.filter.by_reason
|
||||
activity.filter.by_type
|
||||
activity.filter.by_date
|
||||
activity.filter.by_location
|
||||
activity.filter.by_sent_received
|
||||
activity.filter.by_user
|
||||
```
|
||||
|
||||
### Aggregators
|
||||
|
||||
```
|
||||
activity.aggregator.reason.by_category
|
||||
activity.aggregator.reason.level
|
||||
activity.aggregator.user.by_scope
|
||||
activity.aggregator.user.by_job
|
||||
```
|
||||
|
||||
## Global/Shared Keys
|
||||
|
||||
Keys like the following **must not be redeclared** in each bundle:
|
||||
|
||||
- First name
|
||||
- Last name
|
||||
- Username
|
||||
- ID
|
||||
- Type
|
||||
- Duration
|
||||
- Comment
|
||||
- Date
|
||||
- Location
|
||||
- Present / Not present
|
||||
- Add / Edit / Delete / Save / Update
|
||||
|
||||
These belong in `common` or `main`:
|
||||
|
||||
```
|
||||
common.firstname
|
||||
common.lastname
|
||||
common.username
|
||||
common.id
|
||||
common.type
|
||||
common.comment
|
||||
common.date
|
||||
common.location
|
||||
common.present
|
||||
common.absent
|
||||
common.add
|
||||
common.edit
|
||||
common.delete
|
||||
common.save
|
||||
common.update
|
||||
```
|
||||
|
||||
## Naming Directives Summary
|
||||
|
||||
- **snake_case**
|
||||
- **namespaced with dots**
|
||||
- **bundle prefix for bundle-specific concepts**
|
||||
- **common or main for shared concepts**
|
||||
- **avoid free-floating keys (without namespace)**
|
||||
- **reuse common keys wherever possible**
|
||||
|
||||
## Migration Strategy (Optional)
|
||||
|
||||
To apply this structure progressively:
|
||||
|
||||
1. New keys must follow these guidelines.
|
||||
2. Existing keys may remain as-is until refactored.
|
||||
3. When refactoring:
|
||||
- Move cross-bundle keys to ChillMainBundle and possible `common` namespace.
|
||||
- Replace duplicated keys with shared ones.
|
||||
|
||||
---
|
||||
|
||||
# Avoiding Duplicate Translations
|
||||
|
||||
## 1. Use Shared Namespaces
|
||||
|
||||
Two namespaces must be used for shared translations:
|
||||
|
||||
- `common.*` — generic UI concepts (save, delete, date, name, etc.)
|
||||
|
||||
If a translation may be reused in multiple bundles, it must be placed in the `common` namespace or in ChillMainBundle.
|
||||
|
||||
## 2. Bundle-Specific Keys
|
||||
|
||||
Keys belonging only to one bundle or one feature are namespaced inside that bundle:
|
||||
|
||||
```
|
||||
activity.<feature>.<element>
|
||||
person.<feature>.<element>
|
||||
```
|
||||
|
||||
## 3. Search Before Creating
|
||||
|
||||
Before adding a new translation key, developers must:
|
||||
|
||||
1. For common translations like "enregistrer/opslaan" look in the `common` namespace.
|
||||
2. Search in Loco or translations for existing values.
|
||||
|
||||
If a suitable key exists, reuse it.
|
||||
|
||||
## 4. Only Create a New Key When Necessary
|
||||
|
||||
Create a new key only when the text is:
|
||||
|
||||
- specific to the bundle
|
||||
- specific to the feature
|
||||
- not reusable elsewhere
|
||||
|
||||
## 5. Progressive Cleanup
|
||||
|
||||
Old duplicates may remain temporarily. When updating code in an area, clean duplicate values by moving them into `common` or `main`.
|
||||
|
||||
## General Workflow
|
||||
|
||||
- **Reuse shared keys** within `common` namespace.
|
||||
- **Search before creating** new keys.
|
||||
- **Namespace bundle-specific keys** under their bundle.
|
||||
- **Refactor progressively** when touching old code.
|
||||
@@ -1,419 +0,0 @@
|
||||
============================================
|
||||
Directives for creating new translation keys
|
||||
============================================
|
||||
|
||||
These directives are meant to ensure better consistency across bundles, avoid duplication, and make keys more predictable.
|
||||
|
||||
|
||||
General Principles
|
||||
==================
|
||||
|
||||
1. **Use lowercase snake_case for all keys**
|
||||
|
||||
2. **Use dot-separated namespaces**
|
||||
The dot is used to reflect:
|
||||
- bundle
|
||||
- feature
|
||||
- sub-feature
|
||||
- key type
|
||||
|
||||
3. **Do not use spaces in keys**
|
||||
|
||||
4. **Avoid duplicating the same text in multiple places**
|
||||
When a translation is needed, try a search for the translation value first and see if it exists elsewhere
|
||||
|
||||
5. **If a key is used across multiple bundles, it must live in ChillMainBundle.**
|
||||
|
||||
6. **If a key is used across multiple bundles and is a generic term, it must be placed in the ``common`` namespace.**
|
||||
|
||||
|
||||
Key Structure
|
||||
=============
|
||||
|
||||
We use the following structure:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
<scope>.<feature>.<sub-feature>.<key-type>
|
||||
|
||||
Where:
|
||||
|
||||
* ``<>`` identifies the bundle or shared context
|
||||
* ``<feature>`` identifies the part of the module using the translation
|
||||
* ``<element>`` describes the text purpose
|
||||
* ``<subelement>`` for a multi-level element ( eg. activity.export.person.count.description)
|
||||
|
||||
Examples of scopes
|
||||
------------------
|
||||
|
||||
* ``activity`` — ChillActivityBundle
|
||||
* ``person`` — ChillPersonBundle
|
||||
* ``common`` — neutral shared translation values
|
||||
|
||||
|
||||
Naming Scopes
|
||||
=============
|
||||
|
||||
1. **Bundle-specific keys**
|
||||
|
||||
For most things inside a bundle:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.<feature>.<element>
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.form.save
|
||||
activity.list.title
|
||||
activity.entity.type
|
||||
activity.menu.activities
|
||||
activity.controller.success_created
|
||||
|
||||
2. **Shared UI elements (buttons, labels, generic text)**
|
||||
|
||||
These belong in the ``common`` namespace in ChillMainBundle:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
common.save
|
||||
common.delete
|
||||
common.edit
|
||||
common.filter
|
||||
common.duration_time
|
||||
|
||||
Translation workflow
|
||||
====================
|
||||
|
||||
Use the following workflow when deciding where a key belongs:
|
||||
|
||||
1. **Is this text used in more than one bundle?**
|
||||
→ Place in ``main`` or ``common``
|
||||
|
||||
2. **Is this text generic UI (button, label, pagination, yes/no)?**
|
||||
→ Place in ``common``
|
||||
|
||||
3. **Is this text specific to one bundle and one feature?**
|
||||
→ Place in ``<bundle>.feature.<element>``
|
||||
|
||||
4. **Is this text related to an entity or value object?**
|
||||
→ Place in ``<bundle>.entity.<entityname>.<field>``
|
||||
|
||||
5. **Is this text used in forms?**
|
||||
→ ``<bundle>.form.<field>`` or ``<bundle>.form.<action>``
|
||||
|
||||
6. **Is this text related to exports?**
|
||||
→ ``<bundle>.export.<feature>.<column>``
|
||||
|
||||
7. **Is it related to filtering, searching or parameters?**
|
||||
→ ``<bundle>.filter.<name>`` or
|
||||
→ ``<bundle>.filter.<feature>.<field>`` for nested filters
|
||||
|
||||
|
||||
Examples based on translations within ChillActivityBundle
|
||||
=========================================================
|
||||
|
||||
Below are concrete examples from ``ChillActivityBundle``,
|
||||
refactored according to the guidelines.
|
||||
|
||||
|
||||
General activity keys
|
||||
---------------------
|
||||
|
||||
Instead of scattered keys like::
|
||||
|
||||
Show the activity
|
||||
Edit the activity
|
||||
Activity
|
||||
Duration time
|
||||
...
|
||||
|
||||
We use:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.general.show
|
||||
activity.general.edit
|
||||
activity.general.title
|
||||
activity.general.duration
|
||||
activity.general.travel_time
|
||||
activity.general.attendee
|
||||
activity.general.remark
|
||||
activity.general.no_comments
|
||||
|
||||
|
||||
Forms
|
||||
-----
|
||||
|
||||
Instead of keys like::
|
||||
|
||||
Activity creation
|
||||
Save activity
|
||||
Reset form
|
||||
Choose a type
|
||||
|
||||
Use:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.form.title_create
|
||||
activity.form.save
|
||||
activity.form.reset
|
||||
activity.form.choose_type
|
||||
activity.form.choose_duration
|
||||
|
||||
Long lists (like durations) should be grouped:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.form.duration.5min
|
||||
activity.form.duration.10min
|
||||
activity.form.duration.15min
|
||||
activity.form.duration.1h
|
||||
activity.form.duration.1h30
|
||||
activity.form.duration.2h
|
||||
...
|
||||
|
||||
Entities
|
||||
--------
|
||||
|
||||
Entity fields should follow:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.entity.activity.date
|
||||
activity.entity.activity.comment
|
||||
activity.entity.activity.deleted
|
||||
activity.entity.location.name
|
||||
activity.entity.location.type
|
||||
|
||||
|
||||
Controller messages
|
||||
-------------------
|
||||
|
||||
Instead of strings as keys::
|
||||
|
||||
'Success : activity created!'
|
||||
'The form is not valid. The activity has not been created !'
|
||||
|
||||
Use:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.controller.success_created
|
||||
activity.controller.error_invalid_create
|
||||
activity.controller.success_updated
|
||||
activity.controller.error_invalid_update
|
||||
|
||||
|
||||
Roles
|
||||
-----
|
||||
|
||||
Access control keys should be:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.role.create
|
||||
activity.role.update
|
||||
activity.role.see
|
||||
activity.role.see_details
|
||||
activity.role.delete
|
||||
activity.role.stats
|
||||
activity.role.list
|
||||
|
||||
|
||||
Admin
|
||||
-----
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.admin.configuration
|
||||
activity.admin.types
|
||||
activity.admin.reasons
|
||||
activity.admin.reason_category
|
||||
activity.admin.presence
|
||||
|
||||
|
||||
CRUD
|
||||
----
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.crud.type.title_new
|
||||
activity.crud.type.title_edit
|
||||
activity.crud.presence.title_new
|
||||
|
||||
|
||||
Activity Reason
|
||||
---------------
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.reason.list
|
||||
activity.reason.create
|
||||
activity.reason.active
|
||||
activity.reason.category
|
||||
activity.reason.entity_title
|
||||
|
||||
|
||||
Exports
|
||||
-------
|
||||
|
||||
Group them logically:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.export.person.count.title
|
||||
activity.export.person.count.description
|
||||
activity.export.person.count.header
|
||||
|
||||
activity.export.period.sum_duration.title
|
||||
activity.export.period.sum_duration.description
|
||||
activity.export.period.sum_duration.header
|
||||
|
||||
|
||||
Filters
|
||||
-------
|
||||
|
||||
Use hierarchical filters:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.filter.by_reason
|
||||
activity.filter.by_type
|
||||
activity.filter.by_date
|
||||
activity.filter.by_location
|
||||
activity.filter.by_sent_received
|
||||
activity.filter.by_user
|
||||
|
||||
|
||||
Aggregators
|
||||
-----------
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.aggregator.reason.by_category
|
||||
activity.aggregator.reason.level
|
||||
activity.aggregator.user.by_scope
|
||||
activity.aggregator.user.by_job
|
||||
|
||||
|
||||
Global/Shared Keys
|
||||
==================
|
||||
|
||||
Keys like the following **must not be redeclared** in each bundle:
|
||||
|
||||
- First name
|
||||
- Last name
|
||||
- Username
|
||||
- ID
|
||||
- Type
|
||||
- Duration
|
||||
- Comment
|
||||
- Date
|
||||
- Location
|
||||
- Present / Not present
|
||||
- Add / Edit / Delete / Save / Update
|
||||
|
||||
These belong in ``common`` or ``main``:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
common.firstname
|
||||
common.lastname
|
||||
common.username
|
||||
common.id
|
||||
common.type
|
||||
common.comment
|
||||
common.date
|
||||
common.location
|
||||
common.present
|
||||
common.absent
|
||||
common.add
|
||||
common.edit
|
||||
common.delete
|
||||
common.save
|
||||
common.update
|
||||
|
||||
|
||||
Naming directives summary
|
||||
==========================
|
||||
|
||||
* **snake_case**
|
||||
* **namespaced with dots**
|
||||
* **bundle prefix for bundle-specific concepts**
|
||||
* **common or main for shared concepts**
|
||||
* **avoid free-floating keys (without namespace)**
|
||||
* **reuse common keys wherever possible**
|
||||
|
||||
|
||||
Migration Strategy (Optional)
|
||||
=============================
|
||||
|
||||
To apply this structure progressively:
|
||||
|
||||
1. New keys must follow these guidelines.
|
||||
2. Existing keys may remain as-is until refactored.
|
||||
3. When refactoring:
|
||||
- Move cross-bundle keys to ChillMainBundle and possible `common` namespace.
|
||||
- Replace duplicated keys with shared ones.
|
||||
|
||||
===========================================
|
||||
Avoiding duplicate translations
|
||||
===========================================
|
||||
|
||||
1. Use Shared Namespaces
|
||||
========================
|
||||
|
||||
Two namespaces must be used for shared translations:
|
||||
|
||||
* ``common.*`` — generic UI concepts (save, delete, date, name, etc.)
|
||||
|
||||
If a translation may be reused in multiple bundles, it must be placed
|
||||
in the ``common`` namespace or in ChillMainBundle.
|
||||
|
||||
2. Bundle-Specific Keys
|
||||
=======================
|
||||
|
||||
Keys belonging only to one bundle or one feature are namespaced inside that
|
||||
bundle:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
activity.<feature>.<element>
|
||||
person.<feature>.<element>
|
||||
|
||||
3. Search Before Creating
|
||||
=========================
|
||||
|
||||
Before adding a new translation key, developers must:
|
||||
|
||||
1. For common translations like: "enregistrer/opslaan" look in the `common` namespace.
|
||||
3. Search in Loco or translations for existing values.
|
||||
|
||||
If a suitable key exists, reuse it.
|
||||
|
||||
4. Only Create a New Key When Necessary
|
||||
=======================================
|
||||
|
||||
Create a new key only when the text is:
|
||||
|
||||
* specific to the bundle
|
||||
* specific to the feature
|
||||
* not reusable elsewhere
|
||||
|
||||
6. Progressive Cleanup
|
||||
======================
|
||||
|
||||
Old duplicates may remain temporarily. When updating code in an area, clean
|
||||
duplicate values by moving them into ``common`` or ``main``.
|
||||
|
||||
General workflow
|
||||
================
|
||||
|
||||
* **Reuse shared keys** within ``common`` namespace.
|
||||
* **Search before creating** new keys.
|
||||
* **Namespace bundle-specific keys** under their bundle.
|
||||
* **Refactor progressively** when touching old code.
|
||||
139
docs/source/development/translation_provider.md
Normal file
139
docs/source/development/translation_provider.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Managing Translations Within CHILL Using Loco as a Translation Provider
|
||||
|
||||
Within CHILL we make use of Symfony's translation component together with *Loco* as an external translation provider. Using this setup centralises translations in a single online location (Loco), while still allowing developers to create and update translation keys locally in the project (YAML files).
|
||||
|
||||
## Workflow
|
||||
|
||||
We use the following workflow:
|
||||
|
||||
- Developers create translation keys in YAML files inside each bundle.
|
||||
- Keys are written in **English**.
|
||||
- Application UI defaults to **French**, with **Dutch** as an additional locale (other languages can be added in the future).
|
||||
- Loco acts as the central translation memory and synchronisation source.
|
||||
- Loco Symfony package was installed so that built-in translation commands can be used to push/pull content between Loco and the local project.
|
||||
|
||||
## Translation Directory Structure
|
||||
|
||||
Each bundle contains its own `translations` directory, for example:
|
||||
|
||||
```
|
||||
chill-bundles/
|
||||
ChillCoreBundle/
|
||||
translations/
|
||||
messages.fr.yml
|
||||
messages.nl.yml
|
||||
ChillPersonBundle/
|
||||
translations/
|
||||
messages.fr.yml
|
||||
messages.nl.yml
|
||||
...
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The translation configuration is defined in `config/packages/translation.yaml`:
|
||||
|
||||
```yaml
|
||||
framework:
|
||||
default_locale: '%env(resolve:LOCALE)%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- '%env(resolve:LOCALE)%'
|
||||
- 'en'
|
||||
providers:
|
||||
loco:
|
||||
dsn: '%env(LOCO_DSN)%'
|
||||
domains: [ 'messages' ]
|
||||
locales: [ 'fr', 'nl' ]
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
- `en` is the **source locale** in Loco.
|
||||
- `fr` and `nl` are the **application locales**.
|
||||
- `domains: [messages]` means only `messages.*.yml` files are pushed.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
In `.env`:
|
||||
|
||||
```
|
||||
LOCALE=fr
|
||||
```
|
||||
|
||||
In `.env.local`:
|
||||
|
||||
```
|
||||
LOCO_DSN="loco://API_KEY@default"
|
||||
```
|
||||
|
||||
Replace `API_KEY` with the key provided by Loco.
|
||||
|
||||
## Working with Loco
|
||||
|
||||
Loco shows all translation keys under three languages:
|
||||
|
||||
- **English (source)** — keys are listed but remain "untranslated"
|
||||
- **French** — translated strings for French users
|
||||
- **Dutch** — translated strings for Dutch users
|
||||
|
||||
Note: Don't add translations directly in the English column. This column simply represents the *key*.
|
||||
|
||||
## Pushing Translations to Loco
|
||||
|
||||
You can push local translations to Loco using:
|
||||
|
||||
```bash
|
||||
symfony console translation:push loco --locales=fr --locales=nl --force
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Upload all French and Dutch translation values from `*.fr.yml` and `*.nl.yml` files
|
||||
- Ensure Loco stays in sync with local YAML files
|
||||
- Create any missing keys in Loco
|
||||
|
||||
## Pulling Translations from Loco
|
||||
|
||||
When translators update strings in Loco, developers can fetch updates with:
|
||||
|
||||
```bash
|
||||
symfony console translation:pull loco --locales=fr --locales=nl --force
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Download the latest French and Dutch translations
|
||||
- Overwrite the local YAML files with Loco's content
|
||||
- Keep everything consistent across the team
|
||||
|
||||
## Adding New Translation Keys (Developer Workflow)
|
||||
|
||||
1. Add a new key directly in the appropriate YAML file, for example:
|
||||
|
||||
```
|
||||
chill-bundles/ChillPersonBundle/translations/messages.fr.yml
|
||||
```
|
||||
|
||||
Example key:
|
||||
|
||||
```yaml
|
||||
person.form.submit: "Envoyer"
|
||||
```
|
||||
|
||||
2. Add Dutch translation as well if you can (otherwise leave empty to be translated within Loco later):
|
||||
|
||||
```yaml
|
||||
person.form.submit: "Verzenden"
|
||||
```
|
||||
|
||||
3. Run a push to send the new key to Loco:
|
||||
|
||||
```bash
|
||||
symfony console translation:push loco --locales=fr --locales=nl --force
|
||||
```
|
||||
|
||||
4. The key will now appear in Loco for translation management.
|
||||
|
||||
Note: English appears as "untranslated", because it is merely the source language.
|
||||
@@ -1,148 +0,0 @@
|
||||
=======================================================================
|
||||
Managing translations within CHILL using Loco as a translation provider
|
||||
=======================================================================
|
||||
|
||||
Within CHILL we make use of Symfony's translation component together with *Loco* as an external
|
||||
translation provider. Using this setup centralise translations in a single online
|
||||
location (Loco), while still allowing developers to create and update
|
||||
translation keys locally in the project (YAML files).
|
||||
|
||||
Workflow
|
||||
========
|
||||
|
||||
We use the following workflow:
|
||||
|
||||
* Developers create translation keys in YAML files inside each bundle.
|
||||
* Keys are written in **English**.
|
||||
* Application UI defaults to **French**, with **Dutch** as an additional locale (other languages can be added in the future).
|
||||
* Loco acts as the central translation memory and synchronisation source.
|
||||
* Loco Symfony package was installed so that built-in translation commands can be used to push/pull content
|
||||
between Loco and the local project.
|
||||
|
||||
|
||||
Translation directory structure
|
||||
===============================
|
||||
|
||||
Each bundle contains its own ``translations`` directory, for example::
|
||||
|
||||
chill-bundles/
|
||||
ChillCoreBundle/
|
||||
translations/
|
||||
messages.fr.yml
|
||||
messages.nl.yml
|
||||
ChillPersonBundle/
|
||||
translations/
|
||||
messages.fr.yml
|
||||
messages.nl.yml
|
||||
...
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The translation configuration is defined in
|
||||
``config/packages/translation.yaml``::
|
||||
|
||||
framework:
|
||||
default_locale: '%env(resolve:LOCALE)%'
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks:
|
||||
- '%env(resolve:LOCALE)%'
|
||||
- 'en'
|
||||
providers:
|
||||
loco:
|
||||
dsn: '%env(LOCO_DSN)%'
|
||||
domains: [ 'messages' ]
|
||||
locales: [ 'fr', 'nl' ]
|
||||
|
||||
Note:
|
||||
|
||||
* ``en`` is the **source locale** in Loco.
|
||||
* ``fr`` and ``nl`` are the **application locales**.
|
||||
* ``domains: [messages]`` means only ``messages.*.yml`` files are pushed.
|
||||
|
||||
|
||||
Environment variables
|
||||
---------------------
|
||||
|
||||
In ``.env``::
|
||||
|
||||
LOCALE=fr
|
||||
|
||||
In ``.env.local``::
|
||||
|
||||
LOCO_DSN="loco://API_KEY@default"
|
||||
|
||||
Replace ``API_KEY`` with the key provided by Loco.
|
||||
|
||||
|
||||
Working with Loco
|
||||
=================
|
||||
|
||||
Loco shows all translation keys under three languages:
|
||||
|
||||
* **English (source)** — keys are listed but remain “untranslated”
|
||||
* **French** — translated strings for French users
|
||||
* **Dutch** — translated strings for Dutch users
|
||||
|
||||
Note: Don't add translations directly in the English column.
|
||||
This column simply represents the *key*.
|
||||
|
||||
|
||||
Pushing translations to Loco
|
||||
============================
|
||||
|
||||
You can push local translations to Loco using:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony console translation:push loco --locales=fr --locales=nl --force
|
||||
|
||||
This will:
|
||||
|
||||
* Upload all French and Dutch translation values from ``*.fr.yml`` and
|
||||
``*.nl.yml`` files
|
||||
* Ensures Loco stays in sync with local YAML files
|
||||
* Creates any missing keys in Loco
|
||||
|
||||
|
||||
Pulling translations from Loco
|
||||
==============================
|
||||
|
||||
When translators update strings in Loco, developers can fetch updates with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony console translation:pull loco --locales=fr --locales=nl --force
|
||||
|
||||
This will:
|
||||
|
||||
* Download the latest French and Dutch translations
|
||||
* Overwrite the local YAML files with Loco’s content
|
||||
* Keep everything consistent across the team
|
||||
|
||||
|
||||
Adding new translation keys (Developer workflow)
|
||||
================================================
|
||||
|
||||
1. Add a new key directly in the appropriate YAML file, for example::
|
||||
|
||||
chill-bundles/ChillPersonBundle/translations/messages.fr.yml
|
||||
|
||||
Example key::
|
||||
|
||||
person.form.submit: "Envoyer"
|
||||
|
||||
2. Add Dutch translation as well if you can (otherwise leave empty to be translated within Loco later)::
|
||||
|
||||
person.form.submit: "Verzenden"
|
||||
|
||||
3. Run a push to send the new key to Loco:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
symfony console translation:push loco --locales=fr --locales=nl --force
|
||||
|
||||
4. The key will now appear in Loco for translation management.
|
||||
|
||||
Note: English appears as “untranslated”, because it is merely the source language
|
||||
@@ -9,7 +9,7 @@
|
||||
"social worker"
|
||||
],
|
||||
"require": {
|
||||
"chill-project/chill-bundles": "dev-master as v4.6.1",
|
||||
"chill-project/chill-bundles": "^4.9.0",
|
||||
"zimbra-api/soap-api": "^3.2.2",
|
||||
"psr/http-client": "^1.0",
|
||||
"nyholm/psr7": "^1.0"
|
||||
|
||||
@@ -3,7 +3,6 @@ parameters:
|
||||
paths:
|
||||
- src/
|
||||
- utils/
|
||||
- packages/
|
||||
tmpDir: var/cache/phpstan
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
excludePaths:
|
||||
|
||||
@@ -27,7 +27,8 @@ class ByActivityNumberAggregator implements AggregatorInterface
|
||||
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
||||
{
|
||||
$qb
|
||||
->addSelect('(SELECT COUNT(activity.id) FROM '.Activity::class.' activity WHERE activity.accompanyingPeriod = acp) AS activity_by_number_aggregator')
|
||||
// Use a distinct alias inside the subquery to avoid colliding with the root alias "activity"
|
||||
->addSelect('(SELECT COUNT(agg_activity.id) FROM '.Activity::class.' agg_activity WHERE agg_activity.accompanyingPeriod = acp) AS activity_by_number_aggregator')
|
||||
->addGroupBy('activity_by_number_aggregator');
|
||||
}
|
||||
|
||||
@@ -65,7 +66,7 @@ class ByActivityNumberAggregator implements AggregatorInterface
|
||||
{
|
||||
return static function ($value) {
|
||||
if ('_header' === $value) {
|
||||
return '';
|
||||
return 'Count activities linked to an accompanying period';
|
||||
}
|
||||
|
||||
if (null === $value) {
|
||||
|
||||
@@ -1,114 +1,123 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-sm btn-create" @click="openModal">
|
||||
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-sm btn-create" @click="openModal">
|
||||
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<teleport to="body">
|
||||
<modal
|
||||
v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
@close="modal.showModal = false"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="modal-title">
|
||||
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||
</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<form>
|
||||
<div class="alert alert-warning" v-if="errors.length">
|
||||
<ul>
|
||||
<li v-for="(e, i) in errors" :key="i">
|
||||
{{ e }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<teleport to="body">
|
||||
<modal
|
||||
v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
@close="modal.showModal = false"
|
||||
>
|
||||
<template #header>
|
||||
<h3 class="modal-title">
|
||||
{{ trans(ACTIVITY_CREATE_NEW_LOCATION) }}
|
||||
</h3>
|
||||
</template>
|
||||
<template #body>
|
||||
<form>
|
||||
<div class="alert alert-warning" v-if="errors.length">
|
||||
<ul>
|
||||
<li v-for="(e, i) in errors" :key="i">
|
||||
{{ e }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<select
|
||||
class="form-select form-select-lg"
|
||||
id="type"
|
||||
required
|
||||
v-model="selectType"
|
||||
>
|
||||
<option selected disabled value="">
|
||||
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
|
||||
</option>
|
||||
<option v-for="t in locationTypes" :value="t" :key="t.id">
|
||||
{{ localizeString(t.title) }}
|
||||
</option>
|
||||
</select>
|
||||
<label>{{ trans(ACTIVITY_LOCATION_FIELDS_TYPE) }}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<select
|
||||
class="form-select form-select-lg"
|
||||
id="type"
|
||||
required
|
||||
v-model="selectType"
|
||||
>
|
||||
<option selected disabled value="">
|
||||
{{ trans(ACTIVITY_CHOOSE_LOCATION_TYPE) }}
|
||||
</option>
|
||||
<option
|
||||
v-for="t in locationTypes"
|
||||
:value="t"
|
||||
:key="t.id"
|
||||
>
|
||||
{{ localizeString(t.title) }}
|
||||
</option>
|
||||
</select>
|
||||
<label>{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_TYPE)
|
||||
}}</label>
|
||||
</div>
|
||||
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="name"
|
||||
v-model="inputName"
|
||||
placeholder
|
||||
/>
|
||||
<label for="name">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_NAME)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="name"
|
||||
v-model="inputName"
|
||||
placeholder
|
||||
/>
|
||||
<label for="name">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_NAME)
|
||||
}}</label>
|
||||
</div>
|
||||
|
||||
<add-address
|
||||
:context="addAddress.context"
|
||||
:options="addAddress.options"
|
||||
:addressChangedCallback="submitNewAddress"
|
||||
v-if="showAddAddress"
|
||||
ref="addAddress"
|
||||
/>
|
||||
<add-address
|
||||
:context="addAddress.context"
|
||||
:options="addAddress.options"
|
||||
:addressChangedCallback="submitNewAddress"
|
||||
v-if="showAddAddress"
|
||||
ref="addAddress"
|
||||
/>
|
||||
|
||||
<div class="form-floating mb-3" v-if="showContactData">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="phonenumber1"
|
||||
v-model="inputPhonenumber1"
|
||||
placeholder
|
||||
/>
|
||||
<label for="phonenumber1">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="hasPhonenumber1">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="phonenumber2"
|
||||
v-model="inputPhonenumber2"
|
||||
placeholder
|
||||
/>
|
||||
<label for="phonenumber2">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="showContactData">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="email"
|
||||
v-model="inputEmail"
|
||||
placeholder
|
||||
/>
|
||||
<label for="email">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_EMAIL)
|
||||
}}</label>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="btn btn-save" @click.prevent="saveNewLocation">
|
||||
{{ trans(SAVE) }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="showContactData">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="phonenumber1"
|
||||
v-model="inputPhonenumber1"
|
||||
placeholder
|
||||
/>
|
||||
<label for="phonenumber1">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER1)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="hasPhonenumber1">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="phonenumber2"
|
||||
v-model="inputPhonenumber2"
|
||||
placeholder
|
||||
/>
|
||||
<label for="phonenumber2">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_PHONENUMBER2)
|
||||
}}</label>
|
||||
</div>
|
||||
<div class="form-floating mb-3" v-if="showContactData">
|
||||
<input
|
||||
class="form-control form-control-lg"
|
||||
id="email"
|
||||
v-model="inputEmail"
|
||||
placeholder
|
||||
/>
|
||||
<label for="email">{{
|
||||
trans(ACTIVITY_LOCATION_FIELDS_EMAIL)
|
||||
}}</label>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button
|
||||
class="btn btn-save"
|
||||
@click.prevent="saveNewLocation"
|
||||
>
|
||||
{{ trans(SAVE) }}
|
||||
</button>
|
||||
</template>
|
||||
</modal>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -119,236 +128,241 @@ import { getLocationTypes } from "../../api";
|
||||
import { makeFetch } from "ChillMainAssets/lib/api/apiMethods";
|
||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
|
||||
import {
|
||||
SAVE,
|
||||
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||
ACTIVITY_CREATE_NEW_LOCATION,
|
||||
trans,
|
||||
SAVE,
|
||||
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||
ACTIVITY_CREATE_NEW_LOCATION,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
trans,
|
||||
} from "translator";
|
||||
|
||||
export default {
|
||||
name: "NewLocation",
|
||||
components: {
|
||||
Modal,
|
||||
AddAddress,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
SAVE,
|
||||
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||
ACTIVITY_CREATE_NEW_LOCATION,
|
||||
};
|
||||
},
|
||||
props: ["availableLocations"],
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
selected: {
|
||||
type: null,
|
||||
name: null,
|
||||
addressId: null,
|
||||
phonenumber1: null,
|
||||
phonenumber2: null,
|
||||
email: null,
|
||||
},
|
||||
locationTypes: [],
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
||||
},
|
||||
addAddress: {
|
||||
options: {
|
||||
button: {
|
||||
text: {
|
||||
create: "activity.create_address",
|
||||
edit: "activity.edit_address",
|
||||
},
|
||||
size: "btn-sm",
|
||||
},
|
||||
title: {
|
||||
create: "activity.create_address",
|
||||
edit: "activity.edit_address",
|
||||
},
|
||||
},
|
||||
context: {
|
||||
target: {
|
||||
//name, id
|
||||
},
|
||||
edit: false,
|
||||
addressId: null,
|
||||
defaults: window.addaddress,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activity"]),
|
||||
selectType: {
|
||||
get() {
|
||||
return this.selected.type;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.type = value;
|
||||
},
|
||||
name: "NewLocation",
|
||||
components: {
|
||||
Modal,
|
||||
AddAddress,
|
||||
},
|
||||
inputName: {
|
||||
get() {
|
||||
return this.selected.name;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.name = value;
|
||||
},
|
||||
},
|
||||
inputEmail: {
|
||||
get() {
|
||||
return this.selected.email;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.email = value;
|
||||
},
|
||||
},
|
||||
inputPhonenumber1: {
|
||||
get() {
|
||||
return this.selected.phonenumber1;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.phonenumber1 = value;
|
||||
},
|
||||
},
|
||||
inputPhonenumber2: {
|
||||
get() {
|
||||
return this.selected.phonenumber2;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.phonenumber2 = value;
|
||||
},
|
||||
},
|
||||
hasPhonenumber1() {
|
||||
return (
|
||||
this.selected.phonenumber1 !== null && this.selected.phonenumber1 !== ""
|
||||
);
|
||||
},
|
||||
showAddAddress() {
|
||||
let cond = false;
|
||||
if (this.selected.type) {
|
||||
if (this.selected.type.addressRequired !== "never") {
|
||||
cond = true;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
showContactData() {
|
||||
let cond = false;
|
||||
if (this.selected.type) {
|
||||
if (this.selected.type.contactData !== "never") {
|
||||
cond = true;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.getLocationTypesList();
|
||||
},
|
||||
methods: {
|
||||
localizeString,
|
||||
checkForm() {
|
||||
let cond = true;
|
||||
this.errors = [];
|
||||
if (!this.selected.type) {
|
||||
this.errors.push("Type de localisation requis");
|
||||
cond = false;
|
||||
} else {
|
||||
if (
|
||||
this.selected.type.addressRequired === "required" &&
|
||||
!this.selected.addressId
|
||||
) {
|
||||
this.errors.push("Adresse requise");
|
||||
cond = false;
|
||||
}
|
||||
if (
|
||||
this.selected.type.contactData === "required" &&
|
||||
!this.selected.phonenumber1
|
||||
) {
|
||||
this.errors.push("Numéro de téléphone requis");
|
||||
cond = false;
|
||||
}
|
||||
if (
|
||||
this.selected.type.contactData === "required" &&
|
||||
!this.selected.email
|
||||
) {
|
||||
this.errors.push("Adresse email requise");
|
||||
cond = false;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
getLocationTypesList() {
|
||||
getLocationTypes().then((results) => {
|
||||
this.locationTypes = results.filter(
|
||||
(t) => t.availableForUsers === true,
|
||||
);
|
||||
});
|
||||
},
|
||||
openModal() {
|
||||
this.modal.showModal = true;
|
||||
},
|
||||
saveNewLocation() {
|
||||
if (this.checkForm()) {
|
||||
let body = {
|
||||
type: "location",
|
||||
name: this.selected.name,
|
||||
locationType: {
|
||||
id: this.selected.type.id,
|
||||
type: "location-type",
|
||||
},
|
||||
phonenumber1: this.selected.phonenumber1,
|
||||
phonenumber2: this.selected.phonenumber2,
|
||||
email: this.selected.email,
|
||||
setup() {
|
||||
return {
|
||||
trans,
|
||||
SAVE,
|
||||
ACTIVITY_LOCATION_FIELDS_EMAIL,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER1,
|
||||
ACTIVITY_LOCATION_FIELDS_PHONENUMBER2,
|
||||
ACTIVITY_LOCATION_FIELDS_NAME,
|
||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||
ACTIVITY_CREATE_NEW_LOCATION,
|
||||
ACTIVITY_EDIT_ADDRESS,
|
||||
ACTIVITY_CREATE_ADDRESS,
|
||||
};
|
||||
if (this.selected.addressId) {
|
||||
body = Object.assign(body, {
|
||||
address: {
|
||||
id: this.selected.addressId,
|
||||
},
|
||||
props: ["availableLocations"],
|
||||
data() {
|
||||
return {
|
||||
errors: [],
|
||||
selected: {
|
||||
type: null,
|
||||
name: null,
|
||||
addressId: null,
|
||||
phonenumber1: null,
|
||||
phonenumber2: null,
|
||||
email: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
makeFetch("POST", "/api/1.0/main/location.json", body)
|
||||
.then((response) => {
|
||||
this.$store.dispatch("addAvailableLocationGroup", {
|
||||
locationGroup: "Localisations nouvellement créées",
|
||||
locations: [response],
|
||||
});
|
||||
this.$store.dispatch("updateLocation", response);
|
||||
this.modal.showModal = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name === "ValidationException") {
|
||||
for (let v of error.violations) {
|
||||
this.errors.push(v);
|
||||
}
|
||||
} else {
|
||||
this.errors.push("An error occurred");
|
||||
locationTypes: [],
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-dialog-scrollable modal-xl",
|
||||
},
|
||||
addAddress: {
|
||||
options: {
|
||||
button: {
|
||||
text: {
|
||||
create: ACTIVITY_CREATE_ADDRESS,
|
||||
edit: ACTIVITY_EDIT_ADDRESS,
|
||||
},
|
||||
size: "btn-sm",
|
||||
},
|
||||
title: {
|
||||
create: ACTIVITY_CREATE_ADDRESS,
|
||||
edit: ACTIVITY_EDIT_ADDRESS,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
target: {
|
||||
//name, id
|
||||
},
|
||||
edit: false,
|
||||
addressId: null,
|
||||
defaults: window.addaddress,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(["activity"]),
|
||||
selectType: {
|
||||
get() {
|
||||
return this.selected.type;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.type = value;
|
||||
},
|
||||
},
|
||||
inputName: {
|
||||
get() {
|
||||
return this.selected.name;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.name = value;
|
||||
},
|
||||
},
|
||||
inputEmail: {
|
||||
get() {
|
||||
return this.selected.email;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.email = value;
|
||||
},
|
||||
},
|
||||
inputPhonenumber1: {
|
||||
get() {
|
||||
return this.selected.phonenumber1;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.phonenumber1 = value;
|
||||
},
|
||||
},
|
||||
inputPhonenumber2: {
|
||||
get() {
|
||||
return this.selected.phonenumber2;
|
||||
},
|
||||
set(value) {
|
||||
this.selected.phonenumber2 = value;
|
||||
},
|
||||
},
|
||||
hasPhonenumber1() {
|
||||
return (
|
||||
this.selected.phonenumber1 !== null &&
|
||||
this.selected.phonenumber1 !== ""
|
||||
);
|
||||
},
|
||||
showAddAddress() {
|
||||
let cond = false;
|
||||
if (this.selected.type) {
|
||||
if (this.selected.type.addressRequired !== "never") {
|
||||
cond = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
showContactData() {
|
||||
let cond = false;
|
||||
if (this.selected.type) {
|
||||
if (this.selected.type.contactData !== "never") {
|
||||
cond = true;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
},
|
||||
submitNewAddress(payload) {
|
||||
this.selected.addressId = payload.addressId;
|
||||
this.addAddress.context.addressId = payload.addressId;
|
||||
this.addAddress.context.edit = true;
|
||||
mounted() {
|
||||
this.getLocationTypesList();
|
||||
},
|
||||
methods: {
|
||||
localizeString,
|
||||
checkForm() {
|
||||
let cond = true;
|
||||
this.errors = [];
|
||||
if (!this.selected.type) {
|
||||
this.errors.push("Type de localisation requis");
|
||||
cond = false;
|
||||
} else {
|
||||
if (
|
||||
this.selected.type.addressRequired === "required" &&
|
||||
!this.selected.addressId
|
||||
) {
|
||||
this.errors.push("Adresse requise");
|
||||
cond = false;
|
||||
}
|
||||
if (
|
||||
this.selected.type.contactData === "required" &&
|
||||
!this.selected.phonenumber1
|
||||
) {
|
||||
this.errors.push("Numéro de téléphone requis");
|
||||
cond = false;
|
||||
}
|
||||
if (
|
||||
this.selected.type.contactData === "required" &&
|
||||
!this.selected.email
|
||||
) {
|
||||
this.errors.push("Adresse email requise");
|
||||
cond = false;
|
||||
}
|
||||
}
|
||||
return cond;
|
||||
},
|
||||
getLocationTypesList() {
|
||||
getLocationTypes().then((results) => {
|
||||
this.locationTypes = results.filter(
|
||||
(t) => t.availableForUsers === true,
|
||||
);
|
||||
});
|
||||
},
|
||||
openModal() {
|
||||
this.modal.showModal = true;
|
||||
},
|
||||
saveNewLocation() {
|
||||
if (this.checkForm()) {
|
||||
let body = {
|
||||
type: "location",
|
||||
name: this.selected.name,
|
||||
locationType: {
|
||||
id: this.selected.type.id,
|
||||
type: "location-type",
|
||||
},
|
||||
phonenumber1: this.selected.phonenumber1,
|
||||
phonenumber2: this.selected.phonenumber2,
|
||||
email: this.selected.email,
|
||||
};
|
||||
if (this.selected.addressId) {
|
||||
body = Object.assign(body, {
|
||||
address: {
|
||||
id: this.selected.addressId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
makeFetch("POST", "/api/1.0/main/location.json", body)
|
||||
.then((response) => {
|
||||
this.$store.dispatch("addAvailableLocationGroup", {
|
||||
locationGroup: "Localisations nouvellement créées",
|
||||
locations: [response],
|
||||
});
|
||||
this.$store.dispatch("updateLocation", response);
|
||||
this.modal.showModal = false;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.name === "ValidationException") {
|
||||
for (let v of error.violations) {
|
||||
this.errors.push(v);
|
||||
}
|
||||
} else {
|
||||
this.errors.push("An error occurred");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
submitNewAddress(payload) {
|
||||
this.selected.addressId = payload.addressId;
|
||||
this.addAddress.context.addressId = payload.addressId;
|
||||
this.addAddress.context.edit = true;
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -43,6 +43,7 @@ use DateTime;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\Common\Collections\ReadableCollection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -140,6 +141,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
#[ORM\ManyToMany(targetEntity: Calendar::class, mappedBy: 'persons')]
|
||||
private Collection $calendars;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Calendar>&Selectable<int, Calendar>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'person', targetEntity: Calendar::class)]
|
||||
private Collection $directCalendars;
|
||||
|
||||
/**
|
||||
* The person's center.
|
||||
*
|
||||
@@ -409,6 +416,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
public function __construct()
|
||||
{
|
||||
$this->calendars = new ArrayCollection();
|
||||
$this->directCalendars = new ArrayCollection();
|
||||
$this->accompanyingPeriodParticipations = new ArrayCollection();
|
||||
$this->spokenLanguages = new ArrayCollection();
|
||||
$this->addresses = new ArrayCollection();
|
||||
@@ -869,6 +877,30 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
return $this->calendars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next calendars for this person (calendars with start date after today).
|
||||
* Only returns calendars that are directly linked to this person via the person property,
|
||||
* not those linked through AccompanyingPeriods.
|
||||
*
|
||||
* @param int|null $limit Optional limit for the number of results
|
||||
*
|
||||
* @return array<Calendar>
|
||||
*/
|
||||
public function getNextCalendarsForPerson(?int $limit = null): array
|
||||
{
|
||||
$today = new \DateTimeImmutable('today');
|
||||
|
||||
$criteria = Criteria::create()
|
||||
->where(Criteria::expr()->gte('startDate', $today))
|
||||
->orderBy(['startDate' => Order::Ascending]);
|
||||
|
||||
if (null !== $limit) {
|
||||
$criteria->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $this->directCalendars->matching($criteria)->toArray();
|
||||
}
|
||||
|
||||
public function getCenter(): ?Center
|
||||
{
|
||||
if (null !== $this->centerCurrent) {
|
||||
@@ -1122,7 +1154,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
->where(
|
||||
$expr->eq('shareHousehold', false)
|
||||
)
|
||||
->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending]);
|
||||
->orderBy(['startDate' => Order::Descending]);
|
||||
|
||||
return $this->getHouseholdParticipations()
|
||||
->matching($criteria);
|
||||
@@ -1144,7 +1176,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
|
||||
->where(
|
||||
$expr->eq('shareHousehold', true)
|
||||
)
|
||||
->orderBy(['startDate' => \Doctrine\Common\Collections\Order::Descending, 'id' => \Doctrine\Common\Collections\Order::Descending]);
|
||||
->orderBy(['startDate' => Order::Descending, 'id' => Order::Descending]);
|
||||
|
||||
return $this->getHouseholdParticipations()
|
||||
->matching($criteria);
|
||||
|
||||
@@ -212,4 +212,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
|
||||
|
||||
@@ -301,6 +301,47 @@
|
||||
'customButtons': { 'after': _self.button_person_after(person), 'before': _self.button_person_before((person)) }
|
||||
}) }}
|
||||
|
||||
{% set calendars = [] %}
|
||||
{% for c in person.getNextCalendarsForPerson(10) %}
|
||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', c) %}
|
||||
{% set calendars = calendars|merge([c]) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if calendars|length > 0 %}
|
||||
<div class="wrap-list periods-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title">
|
||||
<h3>{{ 'chill_calendar.Next calendars'|trans }}</h3>
|
||||
</div>
|
||||
<div class="wl-col list">
|
||||
<div class="calendar-list">
|
||||
<ul class="calendar-list">
|
||||
{% for c in calendars %}
|
||||
<li>
|
||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', { id: c.id }) }}">
|
||||
<span class="badge bg-secondary">
|
||||
{{ c.startDate|format_datetime('long', 'short') }}
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">
|
||||
{{ c.startDate|format_datetime('long', 'short') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', person) %}
|
||||
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_list_by_person', {'id': person.id}) }}" class="calendar-list__global"><i class="fa fa-list"></i></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{#- 'acps' is for AcCompanyingPeriodS #}
|
||||
{%- set acps = [] %}
|
||||
{%- set acpsClosed = [] %}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
module.exports = function (encore, entries) {
|
||||
encore.addEntry(
|
||||
"page_wopi_editor",
|
||||
__dirname + "/src/Resources/public/page/editor/index.js",
|
||||
__dirname + "/src/Resources/public/page/editor/index.ts",
|
||||
);
|
||||
encore.addEntry(
|
||||
"mod_reload_page",
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
require("./index.scss");
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
let frameholder = document.getElementById("frameholder");
|
||||
let office_frame = document.createElement("iframe");
|
||||
office_frame.name = "office_frame";
|
||||
office_frame.id = "office_frame";
|
||||
|
||||
// The title should be set for accessibility
|
||||
office_frame.title = "Office Frame";
|
||||
|
||||
// This attribute allows true fullscreen mode in slideshow view
|
||||
// when using PowerPoint's 'view' action.
|
||||
office_frame.setAttribute("allowfullscreen", "true");
|
||||
|
||||
// The sandbox attribute is needed to allow automatic redirection to the O365 sign-in page in the business user flow
|
||||
office_frame.setAttribute(
|
||||
"sandbox",
|
||||
"allow-downloads allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-top-navigation allow-popups-to-escape-sandbox",
|
||||
);
|
||||
frameholder.appendChild(office_frame);
|
||||
|
||||
document.getElementById("office_form").submit();
|
||||
|
||||
const url = new URL(editor_url);
|
||||
const editor_domain = url.origin;
|
||||
|
||||
window.addEventListener("message", function (message) {
|
||||
if (message.origin !== editor_domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data = JSON.parse(message.data);
|
||||
|
||||
if ("UI_Close" === data.MessageId) {
|
||||
closeEditor();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function closeEditor() {
|
||||
let params = new URLSearchParams(window.location.search),
|
||||
returnPath = params.get("returnPath");
|
||||
|
||||
window.location.assign(returnPath);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import "./index.scss";
|
||||
|
||||
// Provided by the server-side template
|
||||
declare const editor_url: string;
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function () {
|
||||
const frameholder = document.getElementById("frameholder");
|
||||
const office_frame = document.createElement("iframe");
|
||||
office_frame.name = "office_frame";
|
||||
office_frame.id = "office_frame";
|
||||
|
||||
// The title should be set for accessibility
|
||||
office_frame.title = "Office Frame";
|
||||
|
||||
// This attribute allows true fullscreen mode in slideshow view
|
||||
// when using PowerPoint's 'view' action.
|
||||
office_frame.setAttribute("allowfullscreen", "true");
|
||||
|
||||
// The sandbox attribute is needed to allow automatic redirection to the O365 sign-in page in the business user flow
|
||||
office_frame.setAttribute(
|
||||
"sandbox",
|
||||
"allow-downloads allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-top-navigation allow-popups-to-escape-sandbox",
|
||||
);
|
||||
|
||||
office_frame.setAttribute(
|
||||
"allow",
|
||||
"clipboard-read *; clipboard-write *; fullscreen *",
|
||||
);
|
||||
if (frameholder) {
|
||||
frameholder.appendChild(office_frame);
|
||||
}
|
||||
|
||||
const officeForm = document.getElementById(
|
||||
"office_form",
|
||||
) as HTMLFormElement | null;
|
||||
officeForm?.submit();
|
||||
|
||||
const url = new URL(editor_url);
|
||||
const editor_domain = url.origin;
|
||||
|
||||
window.addEventListener("message", function (message: MessageEvent) {
|
||||
if (message.origin !== editor_domain) {
|
||||
return;
|
||||
}
|
||||
|
||||
let data: { MessageId: "UI_Close" | null; data: string };
|
||||
try {
|
||||
data =
|
||||
typeof message.data === "string"
|
||||
? JSON.parse(message.data)
|
||||
: message.data;
|
||||
} catch (e: unknown) {
|
||||
console.error("error while parsing data from message UI_CLOSE", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if ("UI_Close" === data.MessageId) {
|
||||
closeEditor();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function closeEditor(): void {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const returnPath = params.get("returnPath") ?? "/";
|
||||
|
||||
window.location.assign(returnPath);
|
||||
}
|
||||
@@ -2,9 +2,6 @@
|
||||
"champs-libres/wopi-bundle": {
|
||||
"version": "dev-master"
|
||||
},
|
||||
"chill-project/chill-zimbra-bundle": {
|
||||
"version": "dev-472-zimbra-connector"
|
||||
},
|
||||
"doctrine/annotations": {
|
||||
"version": "1.14",
|
||||
"recipe": {
|
||||
|
||||
Reference in New Issue
Block a user