mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-10-01 19:09:45 +00:00
Compare commits
1 Commits
366-pick-u
...
translatio
Author | SHA1 | Date | |
---|---|---|---|
171f7585c2 |
107
docs/source/development/translations.rst
Normal file
107
docs/source/development/translations.rst
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
Translations
|
||||||
|
************
|
||||||
|
|
||||||
|
One source of truth
|
||||||
|
===================
|
||||||
|
|
||||||
|
As of January 2025 we have opted to use one source of truth for translations in our backend as well as our frontend.
|
||||||
|
You will find translations still being present in i18ns files for our vue components, but these will slowly be replaced.
|
||||||
|
The goal is to only use the messages.{locale}.yaml files to create our translations and keys.
|
||||||
|
|
||||||
|
Each time we do `symfony console cache:clear` a javascript and typescript file are generated containing all the keys and the corresponding translations.
|
||||||
|
These can then be imported into our vue components together with the `trans` method, for use in the vue templates.
|
||||||
|
|
||||||
|
Vue import example
|
||||||
|
^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
. code-block:: js
|
||||||
|
import {
|
||||||
|
ACTIVITY_BLOC_PERSONS,
|
||||||
|
ACTIVITY_BLOC_PERSONS_ASSOCIATED,
|
||||||
|
ACTIVITY_BLOC_THIRDPARTY,
|
||||||
|
ACTIVITY_BLOC_USERS,
|
||||||
|
ACTIVITY_ADD_PERSONS,
|
||||||
|
trans,
|
||||||
|
} from "translator";
|
||||||
|
|
||||||
|
Setup
|
||||||
|
=====
|
||||||
|
|
||||||
|
For development purposes we generally make use of the chill-bundles standalone project. Here the new translation setup will work out of the box.
|
||||||
|
|
||||||
|
However when working on a customer chill instance (with a root project and a chill-bundles implementation) it is required to execute the chill translations recipe
|
||||||
|
using the command
|
||||||
|
|
||||||
|
Translation key conventions
|
||||||
|
===========================
|
||||||
|
|
||||||
|
When adding new translation keys we have chosen to adhere to the following conventions as of April 2025.
|
||||||
|
Older translation keys will gradually be adapted to respect these conventions.
|
||||||
|
|
||||||
|
Conventions
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
Entity related messages
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Translation keys will be structured as followed as follows:
|
||||||
|
|
||||||
|
`[bundle].[entity].[page or component].[action / message]`
|
||||||
|
|
||||||
|
. code-block:: yaml
|
||||||
|
person:
|
||||||
|
household:
|
||||||
|
index:
|
||||||
|
edit_comment: "Mettre à jour le commentaire"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
So the key to be used will be `person.household.index.edit_comment` when used in a twig template
|
||||||
|
or
|
||||||
|
`PERSON_HOUSEHOLD_INDEX_EDIT_COMMENT` when used in a vue component.
|
||||||
|
|
||||||
|
|
||||||
|
Export related messages
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Translation keys will be structured as followed as follows:
|
||||||
|
|
||||||
|
[bundle]
|
||||||
|
|
|
||||||
|
|__ export
|
||||||
|
|
|
||||||
|
|__ ['count | list' | 'filter' | 'aggregator']
|
||||||
|
|
|
||||||
|
|__ [export | filter | aggregator - name] OR [ properties (end of hierarchy) ]
|
||||||
|
|
|
||||||
|
|__ [action / message]
|
||||||
|
|
||||||
|
ex. Filter
|
||||||
|
|
||||||
|
. code-block:: yaml
|
||||||
|
activity:
|
||||||
|
export:
|
||||||
|
filter:
|
||||||
|
by_users_job:
|
||||||
|
title: Filtrer les échanges par type
|
||||||
|
'Filtered activity by users job: only %jobs%': 'Filtré par métier d''au moins un utilisateur participant: seulement %jobs%'
|
||||||
|
|
||||||
|
ex. Export type
|
||||||
|
|
||||||
|
. code-block:: yaml
|
||||||
|
activity:
|
||||||
|
export:
|
||||||
|
count:
|
||||||
|
count_persons_on_activity:
|
||||||
|
title: Nombre d'usagers concernés par les échanges
|
||||||
|
list:
|
||||||
|
activities_by_parcours:
|
||||||
|
title: Liste des échanges liés à un parcours
|
||||||
|
|
||||||
|
ex. Export properties shared by a certain export type
|
||||||
|
|
||||||
|
. code-block:: yaml
|
||||||
|
activity:
|
||||||
|
export:
|
||||||
|
list:
|
||||||
|
users ids: Identifiant des utilisateurs
|
@@ -66,11 +66,8 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function denormalizeOne(array|string $item)
|
private function denormalizeOne(array $item)
|
||||||
{
|
{
|
||||||
if ('me' === $item) {
|
|
||||||
return $item;
|
|
||||||
}
|
|
||||||
if (!\array_key_exists('type', $item)) {
|
if (!\array_key_exists('type', $item)) {
|
||||||
throw new TransformationFailedException('the key "type" is missing on element');
|
throw new TransformationFailedException('the key "type" is missing on element');
|
||||||
}
|
}
|
||||||
@@ -101,6 +98,5 @@ class EntityToJsonTransformer implements DataTransformerInterface
|
|||||||
'json',
|
'json',
|
||||||
$context,
|
$context,
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,82 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Chill is a software for social workers
|
|
||||||
*
|
|
||||||
* For the full copyright and license information, please view
|
|
||||||
* the LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Chill\MainBundle\Form\Type;
|
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
|
||||||
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
|
|
||||||
use Symfony\Component\Form\AbstractType;
|
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
|
||||||
use Symfony\Component\Form\FormInterface;
|
|
||||||
use Symfony\Component\Form\FormView;
|
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pick user dymically, using vuejs module "AddPerson".
|
|
||||||
*
|
|
||||||
* Possible options:
|
|
||||||
*
|
|
||||||
* - `multiple`: pick one or more users
|
|
||||||
* - `suggested`: a list of suggested users
|
|
||||||
* - `suggest_myself`: append the current user to the list of suggested
|
|
||||||
* - `as_id`: only the id will be set in the returned data
|
|
||||||
* - `submit_on_adding_new_entity`: the browser will immediately submit the form when new users are checked
|
|
||||||
*/
|
|
||||||
class PickUserOrMeDynamicType extends AbstractType
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly DenormalizerInterface $denormalizer,
|
|
||||||
private readonly SerializerInterface $serializer,
|
|
||||||
private readonly NormalizerInterface $normalizer,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
|
||||||
{
|
|
||||||
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user'));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
|
||||||
{
|
|
||||||
$view->vars['multiple'] = $options['multiple'];
|
|
||||||
$view->vars['types'] = ['user'];
|
|
||||||
$view->vars['uniqid'] = uniqid('pick_user_or_me_dyn');
|
|
||||||
$view->vars['suggested'] = [];
|
|
||||||
$view->vars['as_id'] = true === $options['as_id'] ? '1' : '0';
|
|
||||||
$view->vars['submit_on_adding_new_entity'] = true === $options['submit_on_adding_new_entity'] ? '1' : '0';
|
|
||||||
|
|
||||||
foreach ($options['suggested'] as $user) {
|
|
||||||
$view->vars['suggested'][] = $this->normalizer->normalize($user, 'json', ['groups' => 'read']);
|
|
||||||
}
|
|
||||||
// $user = /* should come from context */ $options['context'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
|
||||||
{
|
|
||||||
$resolver
|
|
||||||
->setDefault('multiple', false)
|
|
||||||
->setAllowedTypes('multiple', ['bool'])
|
|
||||||
->setDefault('compound', false)
|
|
||||||
->setDefault('suggested', [])
|
|
||||||
// if set to true, only the id will be set inside the content. The denormalization will not work.
|
|
||||||
->setDefault('as_id', false)
|
|
||||||
->setAllowedTypes('as_id', ['bool'])
|
|
||||||
->setDefault('submit_on_adding_new_entity', false)
|
|
||||||
->setAllowedTypes('submit_on_adding_new_entity', ['bool']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getBlockPrefix()
|
|
||||||
{
|
|
||||||
return 'pick_entity_dynamic';
|
|
||||||
}
|
|
||||||
}
|
|
@@ -12,11 +12,6 @@ function loadDynamicPicker(element) {
|
|||||||
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
|
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
|
||||||
|
|
||||||
apps.forEach(function (el) {
|
apps.forEach(function (el) {
|
||||||
let suggested;
|
|
||||||
let as_id;
|
|
||||||
let submit_on_adding_new_entity;
|
|
||||||
let label;
|
|
||||||
let isCurrentUserPicker;
|
|
||||||
const isMultiple = parseInt(el.dataset.multiple) === 1,
|
const isMultiple = parseInt(el.dataset.multiple) === 1,
|
||||||
uniqId = el.dataset.uniqid,
|
uniqId = el.dataset.uniqid,
|
||||||
input = element.querySelector(
|
input = element.querySelector(
|
||||||
@@ -27,13 +22,12 @@ function loadDynamicPicker(element) {
|
|||||||
? JSON.parse(input.value)
|
? JSON.parse(input.value)
|
||||||
: input.value === "[]" || input.value === ""
|
: input.value === "[]" || input.value === ""
|
||||||
? null
|
? null
|
||||||
: [JSON.parse(input.value)];
|
: [JSON.parse(input.value)],
|
||||||
suggested = JSON.parse(el.dataset.suggested);
|
suggested = JSON.parse(el.dataset.suggested),
|
||||||
as_id = parseInt(el.dataset.asId) === 1;
|
as_id = parseInt(el.dataset.asId) === 1,
|
||||||
submit_on_adding_new_entity =
|
submit_on_adding_new_entity =
|
||||||
parseInt(el.dataset.submitOnAddingNewEntity) === 1;
|
parseInt(el.dataset.submitOnAddingNewEntity) === 1,
|
||||||
label = el.dataset.label;
|
label = el.dataset.label;
|
||||||
isCurrentUserPicker = uniqId.startsWith("pick_user_or_me_dyn");
|
|
||||||
|
|
||||||
if (!isMultiple) {
|
if (!isMultiple) {
|
||||||
if (input.value === "[]") {
|
if (input.value === "[]") {
|
||||||
@@ -50,7 +44,6 @@ function loadDynamicPicker(element) {
|
|||||||
':uniqid="uniqid" ' +
|
':uniqid="uniqid" ' +
|
||||||
':suggested="notPickedSuggested" ' +
|
':suggested="notPickedSuggested" ' +
|
||||||
':label="label" ' +
|
':label="label" ' +
|
||||||
':isCurrentUserPicker="isCurrentUserPicker" ' +
|
|
||||||
'@addNewEntity="addNewEntity" ' +
|
'@addNewEntity="addNewEntity" ' +
|
||||||
'@removeEntity="removeEntity" ' +
|
'@removeEntity="removeEntity" ' +
|
||||||
'@addNewEntityProcessEnded="addNewEntityProcessEnded"' +
|
'@addNewEntityProcessEnded="addNewEntityProcessEnded"' +
|
||||||
@@ -68,7 +61,6 @@ function loadDynamicPicker(element) {
|
|||||||
as_id,
|
as_id,
|
||||||
submit_on_adding_new_entity,
|
submit_on_adding_new_entity,
|
||||||
label,
|
label,
|
||||||
isCurrentUserPicker,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -97,8 +89,7 @@ function loadDynamicPicker(element) {
|
|||||||
const ids = this.picked.map((el) => el.id);
|
const ids = this.picked.map((el) => el.id);
|
||||||
input.value = ids.join(",");
|
input.value = ids.join(",");
|
||||||
}
|
}
|
||||||
console.log(this.picked);
|
console.log(entity);
|
||||||
// console.log(entity);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
|
@@ -1,25 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<ul :class="listClasses" v-if="picked.length && displayPicked">
|
<ul :class="listClasses" v-if="picked.length && displayPicked">
|
||||||
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type + p.id">
|
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type + p.id">
|
||||||
<span
|
<span class="chill_denomination">{{ p.text }}</span>
|
||||||
v-if="'me' === p"
|
|
||||||
class="chill_denomination current-user updatedBy"
|
|
||||||
>{{ trans(USER_CURRENT_USER) }}</span
|
|
||||||
>
|
|
||||||
<span v-else class="chill_denomination">{{ p.text }}</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
<li v-if="isCurrentUserPicker" class="btn btn-sm btn-misc">
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
ref="itsMeCheckbox"
|
|
||||||
:type="multiple ? 'checkbox' : 'radio'"
|
|
||||||
@change="selectItsMe"
|
|
||||||
/>
|
|
||||||
{{ trans(USER_CURRENT_USER) }}
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
<li class="add-persons">
|
<li class="add-persons">
|
||||||
<add-persons
|
<add-persons
|
||||||
:options="addPersonsOptions"
|
:options="addPersonsOptions"
|
||||||
@@ -39,83 +24,119 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script>
|
||||||
import { ref, computed } from "vue";
|
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
|
||||||
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; // eslint-disable-line
|
|
||||||
import { appMessages } from "./i18n";
|
import { appMessages } from "./i18n";
|
||||||
import { trans, USER_CURRENT_USER } from "translator";
|
|
||||||
|
|
||||||
const props = defineProps({
|
export default {
|
||||||
multiple: Boolean,
|
name: "PickEntity",
|
||||||
types: Array,
|
props: {
|
||||||
picked: Array,
|
multiple: {
|
||||||
uniqid: String,
|
type: Boolean,
|
||||||
removableIfSet: { type: Boolean, default: true },
|
required: true,
|
||||||
displayPicked: { type: Boolean, default: true },
|
},
|
||||||
suggested: { type: Array, default: () => [] },
|
types: {
|
||||||
label: String,
|
type: Array,
|
||||||
isCurrentUserPicker: { type: Boolean, default: false },
|
required: true,
|
||||||
});
|
},
|
||||||
|
picked: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
uniqid: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
removableIfSet: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
displayPicked: {
|
||||||
|
// display picked entities.
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
suggested: {
|
||||||
|
type: Array,
|
||||||
|
default: [],
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emits: ["addNewEntity", "removeEntity", "addNewEntityProcessEnded"],
|
||||||
|
components: {
|
||||||
|
AddPersons,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
key: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
addPersonsOptions() {
|
||||||
|
return {
|
||||||
|
uniq: !this.multiple,
|
||||||
|
type: this.types,
|
||||||
|
priority: null,
|
||||||
|
button: {
|
||||||
|
size: "btn-sm",
|
||||||
|
class: "btn-submit",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
translatedListOfTypes() {
|
||||||
|
if (this.label !== "") {
|
||||||
|
return this.label;
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits([
|
let trans = [];
|
||||||
"addNewEntity",
|
this.types.forEach((t) => {
|
||||||
"removeEntity",
|
if (this.$props.multiple) {
|
||||||
"addNewEntityProcessEnded",
|
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
|
||||||
]);
|
} else {
|
||||||
|
trans.push(
|
||||||
|
appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const itsMeCheckbox = ref(null);
|
if (this.$props.multiple) {
|
||||||
const addPersons = ref(null);
|
return (
|
||||||
|
appMessages.fr.pick_entity.modal_title + trans.join(", ")
|
||||||
const addPersonsOptions = computed(() => ({
|
);
|
||||||
uniq: !props.multiple,
|
} else {
|
||||||
type: props.types,
|
return (
|
||||||
priority: null,
|
appMessages.fr.pick_entity.modal_title_one +
|
||||||
button: { size: "btn-sm", class: "btn-submit" },
|
trans.join(", ")
|
||||||
}));
|
);
|
||||||
|
}
|
||||||
const translatedListOfTypes = computed(() => {
|
},
|
||||||
if (props.label) return props.label;
|
listClasses() {
|
||||||
let trans = props.types.map((t) =>
|
return {
|
||||||
props.multiple
|
"list-suggest": true,
|
||||||
? appMessages.fr.pick_entity[t].toLowerCase()
|
"remove-items": this.$props.removableIfSet,
|
||||||
: appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
|
};
|
||||||
);
|
},
|
||||||
return props.multiple
|
},
|
||||||
? appMessages.fr.pick_entity.modal_title + trans.join(", ")
|
methods: {
|
||||||
: appMessages.fr.pick_entity.modal_title_one + trans.join(", ");
|
addNewSuggested(entity) {
|
||||||
});
|
this.$emit("addNewEntity", { entity: entity });
|
||||||
|
},
|
||||||
const listClasses = computed(() => ({
|
addNewEntity({ selected, modal }) {
|
||||||
"list-suggest": true,
|
selected.forEach((item) => {
|
||||||
"remove-items": props.removableIfSet,
|
this.$emit("addNewEntity", { entity: item.result });
|
||||||
}));
|
}, this);
|
||||||
|
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||||
const selectItsMe = (event) =>
|
modal.showModal = false;
|
||||||
event.target.checked ? addNewSuggested("me") : removeEntity("me");
|
this.$emit("addNewEntityProcessEnded");
|
||||||
|
},
|
||||||
const addNewSuggested = (entity) => {
|
removeEntity(entity) {
|
||||||
emit("addNewEntity", { entity });
|
if (!this.$props.removableIfSet) {
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
const addNewEntity = ({ selected, modal }) => {
|
this.$emit("removeEntity", { entity: entity });
|
||||||
selected.forEach((item) => emit("addNewEntity", { entity: item.result }));
|
},
|
||||||
addPersons.value?.resetSearch();
|
},
|
||||||
modal.showModal = false;
|
|
||||||
emit("addNewEntityProcessEnded");
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeEntity = (entity) => {
|
|
||||||
if (!props.removableIfSet) return;
|
|
||||||
if (entity === "me" && itsMeCheckbox.value) {
|
|
||||||
itsMeCheckbox.value.checked = false;
|
|
||||||
}
|
|
||||||
emit("removeEntity", { entity });
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.current-user {
|
|
||||||
color: var(--bs-body-color);
|
|
||||||
background-color: var(--bs-chill-l-gray) !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@@ -49,7 +49,6 @@ Name: Nom
|
|||||||
Label: Nom
|
Label: Nom
|
||||||
|
|
||||||
user:
|
user:
|
||||||
current_user: Utilisateur courant
|
|
||||||
profile:
|
profile:
|
||||||
title: Mon profil
|
title: Mon profil
|
||||||
Phonenumber successfully updated!: Numéro de téléphone mis à jour!
|
Phonenumber successfully updated!: Numéro de téléphone mis à jour!
|
||||||
|
@@ -13,7 +13,7 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
|||||||
|
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
use Chill\MainBundle\Form\Type\PickUserOrMeDynamicType;
|
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
@@ -66,7 +66,7 @@ class ReferrerFilter implements FilterInterface
|
|||||||
public function buildForm(FormBuilderInterface $builder)
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('accepted_referrers', PickUserOrMeDynamicType::class, [
|
->add('accepted_referrers', PickUserDynamicType::class, [
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
])
|
])
|
||||||
->add('date_calc', PickRollingDateType::class, [
|
->add('date_calc', PickRollingDateType::class, [
|
||||||
|
Reference in New Issue
Block a user