Merge remote-tracking branch 'origin/366-pick-user-or-me' into 339-partage-d'export-enregistré

This commit is contained in:
Julien Fastré 2025-04-24 14:24:48 +02:00
commit 8c59cbc6a0
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
9 changed files with 205 additions and 121 deletions

3
.changes/v3.10.3.md Normal file
View File

@ -0,0 +1,3 @@
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes

View File

@ -6,6 +6,10 @@ 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).
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes
## v3.10.2 - 2025-03-17 ## v3.10.2 - 2025-03-17
### Fixed ### Fixed
* Replace a ts-expect-error with a ts-ignore * Replace a ts-expect-error with a ts-ignore

View File

@ -34,6 +34,7 @@ export default ts.config(
// override/add rules settings here, such as: // override/add rules settings here, such as:
"vue/multi-word-component-names": "off", "vue/multi-word-component-names": "off",
"@typescript-eslint/no-require-imports": "off", "@typescript-eslint/no-require-imports": "off",
"@typescript-eslint/ban-ts-comment": "off"
}, },
}, },
); );

View File

@ -66,8 +66,11 @@ class EntityToJsonTransformer implements DataTransformerInterface
]); ]);
} }
private function denormalizeOne(array $item) private function denormalizeOne(array|string $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');
} }
@ -98,5 +101,6 @@ class EntityToJsonTransformer implements DataTransformerInterface
'json', 'json',
$context, $context,
); );
} }
} }

View File

@ -0,0 +1,82 @@
<?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';
}
}

View File

@ -12,6 +12,11 @@ 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(
@ -22,12 +27,13 @@ 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 === "[]") {
@ -44,6 +50,7 @@ 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"' +
@ -61,6 +68,7 @@ function loadDynamicPicker(element) {
as_id, as_id,
submit_on_adding_new_entity, submit_on_adding_new_entity,
label, label,
isCurrentUserPicker,
}; };
}, },
computed: { computed: {
@ -89,7 +97,8 @@ 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(entity); console.log(this.picked);
// console.log(entity);
} }
} else { } else {
if ( if (

View File

@ -1,10 +1,25 @@
<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 class="chill_denomination">{{ p.text }}</span> <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"
@ -24,119 +39,83 @@
</ul> </ul>
</template> </template>
<script> <script setup>
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; import { ref, computed } from "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";
export default { const props = defineProps({
name: "PickEntity", multiple: Boolean,
props: { types: Array,
multiple: { picked: Array,
type: Boolean, uniqid: String,
required: true, removableIfSet: { type: Boolean, default: true },
}, displayPicked: { type: Boolean, default: true },
types: { suggested: { type: Array, default: () => [] },
type: Array, label: String,
required: true, isCurrentUserPicker: { type: Boolean, default: false },
}, });
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;
}
let trans = []; const emit = defineEmits([
this.types.forEach((t) => { "addNewEntity",
if (this.$props.multiple) { "removeEntity",
trans.push(appMessages.fr.pick_entity[t].toLowerCase()); "addNewEntityProcessEnded",
} else { ]);
trans.push(
appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
);
}
});
if (this.$props.multiple) { const itsMeCheckbox = ref(null);
return ( const addPersons = ref(null);
appMessages.fr.pick_entity.modal_title + trans.join(", ")
); const addPersonsOptions = computed(() => ({
} else { uniq: !props.multiple,
return ( type: props.types,
appMessages.fr.pick_entity.modal_title_one + priority: null,
trans.join(", ") button: { size: "btn-sm", class: "btn-submit" },
); }));
}
}, const translatedListOfTypes = computed(() => {
listClasses() { if (props.label) return props.label;
return { let trans = props.types.map((t) =>
"list-suggest": true, props.multiple
"remove-items": this.$props.removableIfSet, ? appMessages.fr.pick_entity[t].toLowerCase()
}; : appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
}, );
}, return props.multiple
methods: { ? appMessages.fr.pick_entity.modal_title + trans.join(", ")
addNewSuggested(entity) { : appMessages.fr.pick_entity.modal_title_one + trans.join(", ");
this.$emit("addNewEntity", { entity: entity }); });
},
addNewEntity({ selected, modal }) { const listClasses = computed(() => ({
selected.forEach((item) => { "list-suggest": true,
this.$emit("addNewEntity", { entity: item.result }); "remove-items": props.removableIfSet,
}, this); }));
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false; const selectItsMe = (event) =>
this.$emit("addNewEntityProcessEnded"); event.target.checked ? addNewSuggested("me") : removeEntity("me");
},
removeEntity(entity) { const addNewSuggested = (entity) => {
if (!this.$props.removableIfSet) { emit("addNewEntity", { entity });
return; };
}
this.$emit("removeEntity", { entity: entity }); const addNewEntity = ({ selected, modal }) => {
}, 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>

View File

@ -49,6 +49,7 @@ 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!

View File

@ -13,8 +13,8 @@ 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\PickUserDynamicType;
use Chill\MainBundle\Repository\UserRepositoryInterface; use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Form\Type\PickUserOrMeDynamicType;
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;
@ -24,6 +24,7 @@ use Symfony\Component\Form\FormBuilderInterface;
final readonly class ReferrerFilter implements FilterInterface final readonly class ReferrerFilter implements FilterInterface
{ {
use \Chill\MainBundle\Export\ExportDataNormalizerTrait; use \Chill\MainBundle\Export\ExportDataNormalizerTrait;
private const A = 'acp_referrer_filter_uhistory'; private const A = 'acp_referrer_filter_uhistory';
private const P = 'acp_referrer_filter_date'; private const P = 'acp_referrer_filter_date';
@ -68,7 +69,7 @@ final readonly class ReferrerFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder): void public function buildForm(FormBuilderInterface $builder): void
{ {
$builder $builder
->add('accepted_referrers', PickUserDynamicType::class, [ ->add('accepted_referrers', PickUserOrMeDynamicType::class, [
'multiple' => true, 'multiple' => true,
]) ])
->add('date_calc', PickRollingDateType::class, [ ->add('date_calc', PickRollingDateType::class, [