mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-12-19 08:35:43 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0f9e953fb | |||
| a49ea2b6b9 | |||
| a30232d3ce | |||
| aae55e6f8c | |||
| c9513f2f6c | |||
| 11d7425883 | |||
| 08897e0981 | |||
| 98cbfed054 | |||
| 9af4d19744 | |||
| c1cf5a8bb2 | |||
|
ba4e445110
|
|||
|
0f1ff9baf4
|
|||
| e4365ad058 | |||
| a16d659f69 | |||
| c4a069ba2e | |||
| 2600c6fa2a | |||
| 09ef95d13e | |||
| c19b2e18ad | |||
| 27853c594d | |||
| 7714b07a9d | |||
|
6e1c9b6f29
|
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
|
||||||
9
.changes/v4.11.0.md
Normal file
9
.changes/v4.11.0.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## v4.11.0 - 2025-12-17
|
||||||
|
### Feature
|
||||||
|
* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations
|
||||||
|
### Fixed
|
||||||
|
* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix
|
||||||
|
|
||||||
|
* Fix translation key/value
|
||||||
|
|
||||||
|
Cannot start with % and should be wrapped in "".
|
||||||
17
CHANGELOG.md
17
CHANGELOG.md
@@ -6,6 +6,23 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||||
|
|
||||||
|
|
||||||
|
## v4.11.0 - 2025-12-17
|
||||||
|
### Feature
|
||||||
|
* ([#478](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/478)) Add filtering to admin lists: social actions, social issues, goals, results, and evaluations
|
||||||
|
### Fixed
|
||||||
|
* ([#466](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/466)) Fix migration query after previous fix
|
||||||
|
|
||||||
|
* Fix translation key/value
|
||||||
|
|
||||||
|
Cannot start with % and should be wrapped in "".
|
||||||
|
|
||||||
|
## 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
|
## v4.10.0 - 2025-12-09
|
||||||
### Feature
|
### 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
|
* [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
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-redis": "*",
|
"ext-redis": "*",
|
||||||
"ext-zlib": "*",
|
"ext-zlib": "*",
|
||||||
"chill-project/chill-zimbra-bundle": "@dev",
|
|
||||||
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
||||||
"champs-libres/wopi-lib": "dev-master@dev",
|
"champs-libres/wopi-lib": "dev-master@dev",
|
||||||
"doctrine/data-fixtures": "^1.8",
|
"doctrine/data-fixtures": "^1.8",
|
||||||
@@ -62,7 +61,6 @@
|
|||||||
"symfony/http-client": "^5.4",
|
"symfony/http-client": "^5.4",
|
||||||
"symfony/http-foundation": "^5.4",
|
"symfony/http-foundation": "^5.4",
|
||||||
"symfony/intl": "^5.4",
|
"symfony/intl": "^5.4",
|
||||||
"symfony/loco-translation-provider": "^6.0",
|
|
||||||
"symfony/mailer": "^5.4",
|
"symfony/mailer": "^5.4",
|
||||||
"symfony/messenger": "^5.4",
|
"symfony/messenger": "^5.4",
|
||||||
"symfony/mime": "^5.4",
|
"symfony/mime": "^5.4",
|
||||||
@@ -119,7 +117,8 @@
|
|||||||
"symfony/runtime": "^5.4",
|
"symfony/runtime": "^5.4",
|
||||||
"symfony/stopwatch": "^5.4",
|
"symfony/stopwatch": "^5.4",
|
||||||
"symfony/var-dumper": "^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": {
|
"conflict": {
|
||||||
"symfony/symfony": "*"
|
"symfony/symfony": "*"
|
||||||
|
|||||||
@@ -37,5 +37,4 @@ return [
|
|||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||||
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::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.
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
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)
|
- [Messages to users](messages-to-users.md)
|
||||||
- [Pagination](pagination.md)
|
- [Pagination](pagination.md)
|
||||||
- [Localisation](localisation.md)
|
- [Localisation](localisation.md)
|
||||||
|
- [Translation directives](translation_directives.md)
|
||||||
|
- [Translation provider](translation_provider.md)
|
||||||
- [Logging](logging.md)
|
- [Logging](logging.md)
|
||||||
- [Database migrations](migrations.md)
|
- [Database migrations](migrations.md)
|
||||||
- [Searching](searching.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
|
|
||||||
@@ -3,7 +3,6 @@ parameters:
|
|||||||
paths:
|
paths:
|
||||||
- src/
|
- src/
|
||||||
- utils/
|
- utils/
|
||||||
- packages/
|
|
||||||
tmpDir: var/cache/phpstan
|
tmpDir: var/cache/phpstan
|
||||||
reportUnmatchedIgnoredErrors: false
|
reportUnmatchedIgnoredErrors: false
|
||||||
excludePaths:
|
excludePaths:
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class ByActivityNumberAggregator implements AggregatorInterface
|
|||||||
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
public function alterQuery(QueryBuilder $qb, $data, \Chill\MainBundle\Export\ExportGenerationContext $exportGenerationContext): void
|
||||||
{
|
{
|
||||||
$qb
|
$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');
|
->addGroupBy('activity_by_number_aggregator');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +66,7 @@ class ByActivityNumberAggregator implements AggregatorInterface
|
|||||||
{
|
{
|
||||||
return static function ($value) {
|
return static function ($value) {
|
||||||
if ('_header' === $value) {
|
if ('_header' === $value) {
|
||||||
return '';
|
return 'Count activities linked to an accompanying period';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null === $value) {
|
if (null === $value) {
|
||||||
|
|||||||
@@ -136,6 +136,8 @@ import {
|
|||||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||||
ACTIVITY_CREATE_NEW_LOCATION,
|
ACTIVITY_CREATE_NEW_LOCATION,
|
||||||
|
ACTIVITY_EDIT_ADDRESS,
|
||||||
|
ACTIVITY_CREATE_ADDRESS,
|
||||||
trans,
|
trans,
|
||||||
} from "translator";
|
} from "translator";
|
||||||
|
|
||||||
@@ -156,6 +158,8 @@ export default {
|
|||||||
ACTIVITY_LOCATION_FIELDS_TYPE,
|
ACTIVITY_LOCATION_FIELDS_TYPE,
|
||||||
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
ACTIVITY_CHOOSE_LOCATION_TYPE,
|
||||||
ACTIVITY_CREATE_NEW_LOCATION,
|
ACTIVITY_CREATE_NEW_LOCATION,
|
||||||
|
ACTIVITY_EDIT_ADDRESS,
|
||||||
|
ACTIVITY_CREATE_ADDRESS,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: ["availableLocations"],
|
props: ["availableLocations"],
|
||||||
@@ -179,14 +183,14 @@ export default {
|
|||||||
options: {
|
options: {
|
||||||
button: {
|
button: {
|
||||||
text: {
|
text: {
|
||||||
create: "activity.create_address",
|
create: ACTIVITY_CREATE_ADDRESS,
|
||||||
edit: "activity.edit_address",
|
edit: ACTIVITY_EDIT_ADDRESS,
|
||||||
},
|
},
|
||||||
size: "btn-sm",
|
size: "btn-sm",
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
create: "activity.create_address",
|
create: ACTIVITY_CREATE_ADDRESS,
|
||||||
edit: "activity.edit_address",
|
edit: ACTIVITY_EDIT_ADDRESS,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
context: {
|
context: {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ final class Version20251118124241 extends AbstractMigration
|
|||||||
$this->addSql("COMMENT ON COLUMN activity_user.by_migration IS 'For backup purpose - can be safely deleted after a while. See migration \\Chill\\Migrations\\Activity\\Version20251118124241'");
|
$this->addSql("COMMENT ON COLUMN activity_user.by_migration IS 'For backup purpose - can be safely deleted after a while. See migration \\Chill\\Migrations\\Activity\\Version20251118124241'");
|
||||||
|
|
||||||
$this->addSql('INSERT INTO activity_user (activity_id, user_id, by_migration)
|
$this->addSql('INSERT INTO activity_user (activity_id, user_id, by_migration)
|
||||||
SELECT id, user_id, true FROM activity
|
SELECT id, user_id, true FROM activity WHERE user_id is not null
|
||||||
ON CONFLICT DO NOTHING');
|
ON CONFLICT DO NOTHING');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -189,14 +189,14 @@ crud:
|
|||||||
title_edit: Rapport "belemmering" bewerken
|
title_edit: Rapport "belemmering" bewerken
|
||||||
title_delete: Belemmering verwijderen
|
title_delete: Belemmering verwijderen
|
||||||
button_delete: Verwijderen
|
button_delete: Verwijderen
|
||||||
confirm_message_delete: %as_string% verwijderen?
|
confirm_message_delete: "%as_string% verwijderen?"
|
||||||
cscv:
|
cscv:
|
||||||
title_new: Nieuw CV voor %person%
|
title_new: Nieuw CV voor %person%
|
||||||
title_view: CV voor %person%
|
title_view: CV voor %person%
|
||||||
title_edit: CV bewerken
|
title_edit: CV bewerken
|
||||||
title_delete: CV verwijderen
|
title_delete: CV verwijderen
|
||||||
button_delete: Verwijderen
|
button_delete: Verwijderen
|
||||||
confirm_message_delete: %as_string% verwijderen?
|
confirm_message_delete: "%as_string% verwijderen?"
|
||||||
no_date: Geen datum aangegeven
|
no_date: Geen datum aangegeven
|
||||||
no_end_date: einddatum onbekend
|
no_end_date: einddatum onbekend
|
||||||
no_start_date: startdatum onbekend
|
no_start_date: startdatum onbekend
|
||||||
@@ -206,7 +206,7 @@ crud:
|
|||||||
title_edit: Immersie bewerken
|
title_edit: Immersie bewerken
|
||||||
title_delete: Immersie verwijderen
|
title_delete: Immersie verwijderen
|
||||||
button_delete: Verwijderen
|
button_delete: Verwijderen
|
||||||
confirm_message_delete: %as_string% verwijderen?
|
confirm_message_delete: "%as_string% verwijderen?"
|
||||||
projet_prof:
|
projet_prof:
|
||||||
title_new: Nieuw professioneel project voor %person%
|
title_new: Nieuw professioneel project voor %person%
|
||||||
title_view: Professioneel project voor %person%
|
title_view: Professioneel project voor %person%
|
||||||
|
|||||||
@@ -13,14 +13,63 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
|||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\EvaluationRepository;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class EvaluationController extends CRUDController
|
class EvaluationController extends CRUDController
|
||||||
{
|
{
|
||||||
|
public function __construct(private readonly EvaluationRepository $repository) {}
|
||||||
|
|
||||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||||
{
|
{
|
||||||
$query->addOrderBy('e.id', 'ASC');
|
$query->addOrderBy('e.id', 'ASC');
|
||||||
|
|
||||||
return parent::orderQuery($action, $query, $request, $paginator);
|
return parent::orderQuery($action, $query, $request, $paginator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getQueryResult(
|
||||||
|
string $action,
|
||||||
|
Request $request,
|
||||||
|
int $totalItems,
|
||||||
|
PaginatorInterface $paginator,
|
||||||
|
?FilterOrderHelper $filterOrder = null,
|
||||||
|
) {
|
||||||
|
if (0 === $totalItems) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryString = $filterOrder->getQueryString();
|
||||||
|
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||||
|
$nb = $this->repository->countFilteredEvaluations($queryString, $activeFilter);
|
||||||
|
|
||||||
|
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||||
|
|
||||||
|
return $this->repository->findFilteredEvaluations($queryString, $activeFilter, $paginator->getCurrentPageFirstItemNumber(), $paginator->getItemsPerPage());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||||
|
{
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::countEntities($action, $request, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repository->countFilteredEvaluations(
|
||||||
|
$filterOrder->getQueryString(),
|
||||||
|
$filterOrder->getCheckboxData('activeFilter')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||||
|
{
|
||||||
|
return $this->getFilterOrderHelperFactory()
|
||||||
|
->create(self::class)
|
||||||
|
->addSearchBox(['label'])
|
||||||
|
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||||
|
->build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
|||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\GoalRepository;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class GoalController extends CRUDController
|
class GoalController extends CRUDController
|
||||||
{
|
{
|
||||||
|
public function __construct(private readonly GoalRepository $repository) {}
|
||||||
|
|
||||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||||
{
|
{
|
||||||
$query->addOrderBy('e.id', 'ASC');
|
$query->addOrderBy('e.id', 'ASC');
|
||||||
|
|
||||||
return parent::orderQuery($action, $query, $request, $paginator);
|
return parent::orderQuery($action, $query, $request, $paginator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getQueryResult(
|
||||||
|
string $action,
|
||||||
|
Request $request,
|
||||||
|
int $totalItems,
|
||||||
|
PaginatorInterface $paginator,
|
||||||
|
?FilterOrderHelper $filterOrder = null,
|
||||||
|
) {
|
||||||
|
if (0 === $totalItems) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryString = $filterOrder->getQueryString();
|
||||||
|
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||||
|
$nb = $this->repository->countFilteredGoals($queryString, $activeFilter);
|
||||||
|
|
||||||
|
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||||
|
|
||||||
|
return $this->repository->findFilteredGoals(
|
||||||
|
$queryString,
|
||||||
|
$activeFilter,
|
||||||
|
$paginator->getCurrentPageFirstItemNumber(),
|
||||||
|
$paginator->getItemsPerPage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||||
|
{
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::countEntities($action, $request, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repository->countFilteredGoals(
|
||||||
|
$filterOrder->getQueryString(),
|
||||||
|
$filterOrder->getCheckboxData('activeFilter')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||||
|
{
|
||||||
|
return $this->getFilterOrderHelperFactory()
|
||||||
|
->create(self::class)
|
||||||
|
->addSearchBox(['label'])
|
||||||
|
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||||
|
->build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
|||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\ResultRepository;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class ResultController extends CRUDController
|
class ResultController extends CRUDController
|
||||||
{
|
{
|
||||||
|
public function __construct(private readonly ResultRepository $repository) {}
|
||||||
|
|
||||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||||
{
|
{
|
||||||
$query->addOrderBy('e.id', 'ASC');
|
$query->addOrderBy('e.id', 'ASC');
|
||||||
|
|
||||||
return parent::orderQuery($action, $query, $request, $paginator);
|
return parent::orderQuery($action, $query, $request, $paginator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getQueryResult(
|
||||||
|
string $action,
|
||||||
|
Request $request,
|
||||||
|
int $totalItems,
|
||||||
|
PaginatorInterface $paginator,
|
||||||
|
?FilterOrderHelper $filterOrder = null,
|
||||||
|
) {
|
||||||
|
if (0 === $totalItems) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryString = $filterOrder->getQueryString();
|
||||||
|
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||||
|
$nb = $this->repository->countFilteredResults($queryString, $activeFilter);
|
||||||
|
|
||||||
|
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||||
|
|
||||||
|
return $this->repository->findFilteredResults(
|
||||||
|
$queryString,
|
||||||
|
$activeFilter,
|
||||||
|
$paginator->getCurrentPageFirstItemNumber(),
|
||||||
|
$paginator->getItemsPerPage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||||
|
{
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::countEntities($action, $request, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repository->countFilteredResults(
|
||||||
|
$filterOrder->getQueryString(),
|
||||||
|
$filterOrder->getCheckboxData('activeFilter')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||||
|
{
|
||||||
|
return $this->getFilterOrderHelperFactory()
|
||||||
|
->create(self::class)
|
||||||
|
->addSearchBox(['label'])
|
||||||
|
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||||
|
->build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,68 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
|||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\SocialActionRepository;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class SocialActionController extends CRUDController
|
class SocialActionController extends CRUDController
|
||||||
{
|
{
|
||||||
|
public function __construct(private readonly SocialActionRepository $repository) {}
|
||||||
|
|
||||||
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
|
||||||
{
|
{
|
||||||
$query->addOrderBy('e.ordering', 'ASC');
|
$query->addOrderBy('e.ordering', 'ASC');
|
||||||
|
|
||||||
return parent::orderQuery($action, $query, $request, $paginator);
|
return parent::orderQuery($action, $query, $request, $paginator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getQueryResult(
|
||||||
|
string $action,
|
||||||
|
Request $request,
|
||||||
|
int $totalItems,
|
||||||
|
PaginatorInterface $paginator,
|
||||||
|
?FilterOrderHelper $filterOrder = null,
|
||||||
|
) {
|
||||||
|
if (0 === $totalItems) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryString = $filterOrder->getQueryString();
|
||||||
|
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||||
|
$nb = $this->repository->countFilteredSocialActions($queryString, $activeFilter);
|
||||||
|
|
||||||
|
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||||
|
|
||||||
|
return $this->repository->findFilteredSocialActions(
|
||||||
|
$queryString,
|
||||||
|
$activeFilter,
|
||||||
|
$paginator->getCurrentPageFirstItemNumber(),
|
||||||
|
$paginator->getItemsPerPage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||||
|
{
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::countEntities($action, $request, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repository->countFilteredSocialActions(
|
||||||
|
$filterOrder->getQueryString(),
|
||||||
|
$filterOrder->getCheckboxData('activeFilter')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||||
|
{
|
||||||
|
return $this->getFilterOrderHelperFactory()
|
||||||
|
->create(self::class)
|
||||||
|
->addSearchBox(['label'])
|
||||||
|
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||||
|
->build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,15 @@ namespace Chill\PersonBundle\Controller\SocialWork;
|
|||||||
|
|
||||||
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
use Chill\MainBundle\CRUD\Controller\CRUDController;
|
||||||
use Chill\MainBundle\Pagination\PaginatorInterface;
|
use Chill\MainBundle\Pagination\PaginatorInterface;
|
||||||
|
use Chill\MainBundle\Templating\Listing\FilterOrderHelper;
|
||||||
|
use Chill\PersonBundle\Repository\SocialWork\SocialIssueRepository;
|
||||||
use Symfony\Component\Form\FormInterface;
|
use Symfony\Component\Form\FormInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
|
||||||
class SocialIssueController extends CRUDController
|
class SocialIssueController extends CRUDController
|
||||||
{
|
{
|
||||||
|
public function __construct(private readonly SocialIssueRepository $repository) {}
|
||||||
|
|
||||||
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
|
protected function createFormFor(string $action, $entity, ?string $formClass = null, array $formOptions = []): FormInterface
|
||||||
{
|
{
|
||||||
if ('new' === $action) {
|
if ('new' === $action) {
|
||||||
@@ -37,4 +41,54 @@ class SocialIssueController extends CRUDController
|
|||||||
|
|
||||||
return parent::orderQuery($action, $query, $request, $paginator);
|
return parent::orderQuery($action, $query, $request, $paginator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getQueryResult(
|
||||||
|
string $action,
|
||||||
|
Request $request,
|
||||||
|
int $totalItems,
|
||||||
|
PaginatorInterface $paginator,
|
||||||
|
?FilterOrderHelper $filterOrder = null,
|
||||||
|
) {
|
||||||
|
if (0 === $totalItems) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::getQueryResult($action, $request, $totalItems, $paginator, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queryString = $filterOrder->getQueryString();
|
||||||
|
$activeFilter = $filterOrder->getCheckboxData('activeFilter');
|
||||||
|
$nb = $this->repository->countFilteredSocialIssues($queryString, $activeFilter);
|
||||||
|
|
||||||
|
$paginator = $this->getPaginatorFactory()->create($nb);
|
||||||
|
|
||||||
|
return $this->repository->findFilteredSocialIssues(
|
||||||
|
$queryString,
|
||||||
|
$activeFilter,
|
||||||
|
$paginator->getCurrentPageFirstItemNumber(),
|
||||||
|
$paginator->getItemsPerPage()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function countEntities(string $action, Request $request, ?FilterOrderHelper $filterOrder = null): int
|
||||||
|
{
|
||||||
|
if (!$filterOrder instanceof FilterOrderHelper) {
|
||||||
|
return parent::countEntities($action, $request, $filterOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->repository->countFilteredSocialIssues(
|
||||||
|
$filterOrder->getQueryString(),
|
||||||
|
$filterOrder->getCheckboxData('activeFilter')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildFilterOrderHelper(string $action, Request $request): ?FilterOrderHelper
|
||||||
|
{
|
||||||
|
return $this->getFilterOrderHelperFactory()
|
||||||
|
->create(self::class)
|
||||||
|
->addSearchBox(['label'])
|
||||||
|
->addCheckbox('activeFilter', [true => 'Active', false => 'Inactive'], ['Active'])
|
||||||
|
->build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ namespace Chill\PersonBundle\Repository\SocialWork;
|
|||||||
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
|
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
final readonly class EvaluationRepository implements EvaluationRepositoryInterface
|
final readonly class EvaluationRepository implements EvaluationRepositoryInterface
|
||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||||
{
|
{
|
||||||
$this->repository = $entityManager->getRepository(Evaluation::class);
|
$this->repository = $entityManager->getRepository(Evaluation::class);
|
||||||
}
|
}
|
||||||
@@ -65,4 +69,86 @@ final readonly class EvaluationRepository implements EvaluationRepositoryInterfa
|
|||||||
{
|
{
|
||||||
return Evaluation::class;
|
return Evaluation::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getLang(): string
|
||||||
|
{
|
||||||
|
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult(
|
||||||
|
QueryBuilder $qb,
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = [],
|
||||||
|
): array {
|
||||||
|
$qb->select('e');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->setFirstResult($start)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
foreach ($orderBy as $field => $direction) {
|
||||||
|
$qb->addOrderBy('e.'.$field, $direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function queryByTitle(string $pattern): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(Evaluation::class, 'e');
|
||||||
|
|
||||||
|
// Extract the current locale's value from the JSON `title` and search on it
|
||||||
|
$qb
|
||||||
|
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(e.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||||
|
->setParameter('pattern', $pattern)
|
||||||
|
->setParameter('lang', $this->getLang());
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||||
|
{
|
||||||
|
if (null !== $queryString) {
|
||||||
|
$qb = $this->queryByTitle($queryString);
|
||||||
|
} else {
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(Evaluation::class, 'e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add condition based on active/inactive status
|
||||||
|
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||||
|
$qb->andWhere('e.active = true');
|
||||||
|
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||||
|
$qb->andWhere('e.active = false');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findFilteredEvaluations(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = ['title' => 'ASC'],
|
||||||
|
): array {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countFilteredEvaluations(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
): int {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $qb
|
||||||
|
->select('COUNT(e)')
|
||||||
|
->getQuery()->getSingleScalarResult();
|
||||||
|
} catch (NoResultException|NonUniqueResultException $e) {
|
||||||
|
throw new \LogicException('a count query should return one result', previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,14 +15,17 @@ use Chill\PersonBundle\Entity\SocialWork\Goal;
|
|||||||
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
final readonly class GoalRepository implements ObjectRepository
|
final readonly class GoalRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||||
{
|
{
|
||||||
$this->repository = $entityManager->getRepository(Goal::class);
|
$this->repository = $entityManager->getRepository(Goal::class);
|
||||||
}
|
}
|
||||||
@@ -101,6 +104,102 @@ final readonly class GoalRepository implements ObjectRepository
|
|||||||
return Goal::class;
|
return Goal::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getLang(): string
|
||||||
|
{
|
||||||
|
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult(
|
||||||
|
QueryBuilder $qb,
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = [],
|
||||||
|
): array {
|
||||||
|
$qb->select('g');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->setFirstResult($start)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
foreach ($orderBy as $field => $direction) {
|
||||||
|
$qb->addOrderBy('g.'.$field, $direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function queryByTitle(string $pattern): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(Goal::class, 'g');
|
||||||
|
|
||||||
|
// search across locales by extracting the localized value
|
||||||
|
$qb
|
||||||
|
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(g.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||||
|
->setParameter('pattern', $pattern)
|
||||||
|
->setParameter('lang', $this->getLang());
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||||
|
{
|
||||||
|
if (null !== $queryString) {
|
||||||
|
$qb = $this->queryByTitle($queryString);
|
||||||
|
} else {
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(Goal::class, 'g');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active when desactivationDate is null or in the future
|
||||||
|
$now = new \DateTime('now');
|
||||||
|
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('g.desactivationDate'),
|
||||||
|
$qb->expr()->gt('g.desactivationDate', ':now')
|
||||||
|
)
|
||||||
|
)->setParameter('now', $now);
|
||||||
|
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->isNotNull('g.desactivationDate'),
|
||||||
|
$qb->expr()->lte('g.desactivationDate', ':now')
|
||||||
|
)
|
||||||
|
)->setParameter('now', $now);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, Goal>
|
||||||
|
*/
|
||||||
|
public function findFilteredGoals(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = ['id' => 'ASC'],
|
||||||
|
): array {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countFilteredGoals(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
): int {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $qb
|
||||||
|
->select('COUNT(g)')
|
||||||
|
->getQuery()->getSingleScalarResult();
|
||||||
|
} catch (NoResultException|NonUniqueResultException $e) {
|
||||||
|
throw new \LogicException('a count query should return one result', previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
|
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
|
||||||
{
|
{
|
||||||
$actions = $action->getDescendantsWithThis();
|
$actions = $action->getDescendantsWithThis();
|
||||||
|
|||||||
@@ -16,14 +16,17 @@ use Chill\PersonBundle\Entity\SocialWork\Result;
|
|||||||
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
final readonly class ResultRepository implements ObjectRepository
|
final readonly class ResultRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||||
{
|
{
|
||||||
$this->repository = $entityManager->getRepository(Result::class);
|
$this->repository = $entityManager->getRepository(Result::class);
|
||||||
}
|
}
|
||||||
@@ -125,6 +128,100 @@ final readonly class ResultRepository implements ObjectRepository
|
|||||||
return Result::class;
|
return Result::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getLang(): string
|
||||||
|
{
|
||||||
|
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult(
|
||||||
|
QueryBuilder $qb,
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = [],
|
||||||
|
): array {
|
||||||
|
$qb->select('r');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->setFirstResult($start)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
foreach ($orderBy as $field => $direction) {
|
||||||
|
$qb->addOrderBy('r.'.$field, $direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function queryByTitle(string $pattern): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(Result::class, 'r');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(r.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||||
|
->setParameter('pattern', $pattern)
|
||||||
|
->setParameter('lang', $this->getLang());
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||||
|
{
|
||||||
|
if (null !== $queryString) {
|
||||||
|
$qb = $this->queryByTitle($queryString);
|
||||||
|
} else {
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(Result::class, 'r');
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = new \DateTime('now');
|
||||||
|
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('r.desactivationDate'),
|
||||||
|
$qb->expr()->gt('r.desactivationDate', ':now')
|
||||||
|
)
|
||||||
|
)->setParameter('now', $now);
|
||||||
|
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->isNotNull('r.desactivationDate'),
|
||||||
|
$qb->expr()->lte('r.desactivationDate', ':now')
|
||||||
|
)
|
||||||
|
)->setParameter('now', $now);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, Result>
|
||||||
|
*/
|
||||||
|
public function findFilteredResults(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = ['id' => 'ASC'],
|
||||||
|
): array {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countFilteredResults(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
): int {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $qb
|
||||||
|
->select('COUNT(r)')
|
||||||
|
->getQuery()->getSingleScalarResult();
|
||||||
|
} catch (NoResultException|NonUniqueResultException $e) {
|
||||||
|
throw new \LogicException('a count query should return one result', previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function buildQueryByGoal(Goal $goal): QueryBuilder
|
private function buildQueryByGoal(Goal $goal): QueryBuilder
|
||||||
{
|
{
|
||||||
$qb = $this->repository->createQueryBuilder('r');
|
$qb = $this->repository->createQueryBuilder('r');
|
||||||
|
|||||||
@@ -14,14 +14,17 @@ namespace Chill\PersonBundle\Repository\SocialWork;
|
|||||||
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
final readonly class SocialActionRepository implements ObjectRepository
|
final readonly class SocialActionRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||||
{
|
{
|
||||||
$this->repository = $entityManager->getRepository(SocialAction::class);
|
$this->repository = $entityManager->getRepository(SocialAction::class);
|
||||||
}
|
}
|
||||||
@@ -84,6 +87,100 @@ final readonly class SocialActionRepository implements ObjectRepository
|
|||||||
return SocialAction::class;
|
return SocialAction::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getLang(): string
|
||||||
|
{
|
||||||
|
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getResult(
|
||||||
|
QueryBuilder $qb,
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = [],
|
||||||
|
): array {
|
||||||
|
$qb->select('sa');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->setFirstResult($start)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
foreach ($orderBy as $field => $direction) {
|
||||||
|
$qb->addOrderBy('sa.'.$field, $direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function queryByTitle(string $pattern): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(SocialAction::class, 'sa');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(sa.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||||
|
->setParameter('pattern', $pattern)
|
||||||
|
->setParameter('lang', $this->getLang());
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||||
|
{
|
||||||
|
if (null !== $queryString) {
|
||||||
|
$qb = $this->queryByTitle($queryString);
|
||||||
|
} else {
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(SocialAction::class, 'sa');
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = new \DateTime('now');
|
||||||
|
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('sa.desactivationDate'),
|
||||||
|
$qb->expr()->gt('sa.desactivationDate', ':now')
|
||||||
|
)
|
||||||
|
)->setParameter('now', $now);
|
||||||
|
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->isNotNull('sa.desactivationDate'),
|
||||||
|
$qb->expr()->lte('sa.desactivationDate', ':now')
|
||||||
|
)
|
||||||
|
)->setParameter('now', $now);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, SocialAction>
|
||||||
|
*/
|
||||||
|
public function findFilteredSocialActions(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = ['ordering' => 'ASC'],
|
||||||
|
): array {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countFilteredSocialActions(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
): int {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $qb
|
||||||
|
->select('COUNT(sa)')
|
||||||
|
->getQuery()->getSingleScalarResult();
|
||||||
|
} catch (NoResultException|NonUniqueResultException $e) {
|
||||||
|
throw new \LogicException('a count query should return one result', previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder
|
private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder
|
||||||
{
|
{
|
||||||
$qb = $this->repository->createQueryBuilder('sa');
|
$qb = $this->repository->createQueryBuilder('sa');
|
||||||
|
|||||||
@@ -14,14 +14,17 @@ namespace Chill\PersonBundle\Repository\SocialWork;
|
|||||||
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\EntityRepository;
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use Doctrine\ORM\NonUniqueResultException;
|
||||||
|
use Doctrine\ORM\NoResultException;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Doctrine\Persistence\ObjectRepository;
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
|
||||||
final readonly class SocialIssueRepository implements ObjectRepository
|
final readonly class SocialIssueRepository implements ObjectRepository
|
||||||
{
|
{
|
||||||
private EntityRepository $repository;
|
private EntityRepository $repository;
|
||||||
|
|
||||||
public function __construct(EntityManagerInterface $entityManager)
|
public function __construct(private EntityManagerInterface $entityManager, private RequestStack $requestStack)
|
||||||
{
|
{
|
||||||
$this->repository = $entityManager->getRepository(SocialIssue::class);
|
$this->repository = $entityManager->getRepository(SocialIssue::class);
|
||||||
}
|
}
|
||||||
@@ -79,6 +82,100 @@ final readonly class SocialIssueRepository implements ObjectRepository
|
|||||||
return SocialIssue::class;
|
return SocialIssue::class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getResult(
|
||||||
|
QueryBuilder $qb,
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = [],
|
||||||
|
): array {
|
||||||
|
$qb->select('si');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->setFirstResult($start)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
foreach ($orderBy as $field => $direction) {
|
||||||
|
$qb->addOrderBy('si.'.$field, $direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLang(): string
|
||||||
|
{
|
||||||
|
return $this->requestStack->getCurrentRequest()?->getLocale() ?? 'fr';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function queryByTitle(string $pattern): QueryBuilder
|
||||||
|
{
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(SocialIssue::class, 'si');
|
||||||
|
|
||||||
|
$qb
|
||||||
|
->where($qb->expr()->like('LOWER(UNACCENT(JSON_EXTRACT(si.title, :lang)))', "CONCAT('%', LOWER(UNACCENT(:pattern)), '%')"))
|
||||||
|
->setParameter('pattern', $pattern)
|
||||||
|
->setParameter('lang', $this->getLang());
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildFilterBaseQuery(?string $queryString, array $isActive): QueryBuilder
|
||||||
|
{
|
||||||
|
if (null !== $queryString) {
|
||||||
|
$qb = $this->queryByTitle($queryString);
|
||||||
|
} else {
|
||||||
|
$qb = $this->entityManager->createQueryBuilder()->from(SocialIssue::class, 'si');
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = new \DateTime('now');
|
||||||
|
if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->orX(
|
||||||
|
$qb->expr()->isNull('si.desactivationDate'),
|
||||||
|
$qb->expr()->gt('si.desactivationDate', ':now')
|
||||||
|
)
|
||||||
|
)->setParameter('now', $now);
|
||||||
|
} elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
|
||||||
|
$qb->andWhere(
|
||||||
|
$qb->expr()->andX(
|
||||||
|
$qb->expr()->isNotNull('si.desactivationDate'),
|
||||||
|
$qb->expr()->lte('si.desactivationDate', ':now')
|
||||||
|
)
|
||||||
|
)->setParameter('now', $now);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, SocialIssue>
|
||||||
|
*/
|
||||||
|
public function findFilteredSocialIssues(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
?int $start = 0,
|
||||||
|
?int $limit = 50,
|
||||||
|
?array $orderBy = ['ordering' => 'ASC'],
|
||||||
|
): array {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
return $this->getResult($qb, $start, $limit, $orderBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function countFilteredSocialIssues(
|
||||||
|
?string $queryString = null,
|
||||||
|
array $isActive = ['active'],
|
||||||
|
): int {
|
||||||
|
$qb = $this->buildFilterBaseQuery($queryString, $isActive);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $qb
|
||||||
|
->select('COUNT(si)')
|
||||||
|
->getQuery()->getSingleScalarResult();
|
||||||
|
} catch (NoResultException|NonUniqueResultException $e) {
|
||||||
|
throw new \LogicException('a count query should return one result', previous: $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder
|
private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder
|
||||||
{
|
{
|
||||||
$qb = $this->repository->createQueryBuilder('si');
|
$qb = $this->repository->createQueryBuilder('si');
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
{% block admin_content %}
|
{% block admin_content %}
|
||||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||||
|
|
||||||
|
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||||
|
|
||||||
{% block table_entities_thead_tr %}
|
{% block table_entities_thead_tr %}
|
||||||
<th>{{ 'Id'|trans }}</th>
|
<th>{{ 'Id'|trans }}</th>
|
||||||
<th>{{ 'Title'|trans }}</th>
|
<th>{{ 'Title'|trans }}</th>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
{% block admin_content %}
|
{% block admin_content %}
|
||||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||||
|
|
||||||
|
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||||
|
|
||||||
{% block table_entities_thead_tr %}
|
{% block table_entities_thead_tr %}
|
||||||
<th>{{ 'Id'|trans }}</th>
|
<th>{{ 'Id'|trans }}</th>
|
||||||
<th>{{ 'Title'|trans }}</th>
|
<th>{{ 'Title'|trans }}</th>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
{% block admin_content %}
|
{% block admin_content %}
|
||||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||||
|
|
||||||
|
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||||
|
|
||||||
{% block table_entities_thead_tr %}
|
{% block table_entities_thead_tr %}
|
||||||
<th>{{ 'Id'|trans }}</th>
|
<th>{{ 'Id'|trans }}</th>
|
||||||
<th>{{ 'Title'|trans }}</th>
|
<th>{{ 'Title'|trans }}</th>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
{% block admin_content %}
|
{% block admin_content %}
|
||||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||||
|
|
||||||
|
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||||
|
|
||||||
{% block table_entities_thead_tr %}
|
{% block table_entities_thead_tr %}
|
||||||
<th>{{ 'Id' }}</th>
|
<th>{{ 'Id' }}</th>
|
||||||
<th>{{ 'Title'|trans }}</th>
|
<th>{{ 'Title'|trans }}</th>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
{% block admin_content %}
|
{% block admin_content %}
|
||||||
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
{% embed '@ChillMain/CRUD/_index.html.twig' %}
|
||||||
|
|
||||||
|
{% block filter_order %}{{ filter_order|chill_render_filter_order_helper }}{% endblock %}
|
||||||
|
|
||||||
{% block table_entities_thead_tr %}
|
{% block table_entities_thead_tr %}
|
||||||
<th>{{ 'Id'|trans }}</th>
|
<th>{{ 'Id'|trans }}</th>
|
||||||
<th>{{ 'Title'|trans }}</th>
|
<th>{{ 'Title'|trans }}</th>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
module.exports = function (encore, entries) {
|
module.exports = function (encore, entries) {
|
||||||
encore.addEntry(
|
encore.addEntry(
|
||||||
"page_wopi_editor",
|
"page_wopi_editor",
|
||||||
__dirname + "/src/Resources/public/page/editor/index.js",
|
__dirname + "/src/Resources/public/page/editor/index.ts",
|
||||||
);
|
);
|
||||||
encore.addEntry(
|
encore.addEntry(
|
||||||
"mod_reload_page",
|
"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": {
|
"champs-libres/wopi-bundle": {
|
||||||
"version": "dev-master"
|
"version": "dev-master"
|
||||||
},
|
},
|
||||||
"chill-project/chill-zimbra-bundle": {
|
|
||||||
"version": "dev-472-zimbra-connector"
|
|
||||||
},
|
|
||||||
"doctrine/annotations": {
|
"doctrine/annotations": {
|
||||||
"version": "1.14",
|
"version": "1.14",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
Reference in New Issue
Block a user