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).
## v3.10.3 - 2025-03-18
### DX
* Eslint fixes
## v3.10.2 - 2025-03-17
### Fixed
* 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:
"vue/multi-word-component-names": "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)) {
throw new TransformationFailedException('the key "type" is missing on element');
}
@ -98,5 +101,6 @@ class EntityToJsonTransformer implements DataTransformerInterface
'json',
$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"]');
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,
uniqId = el.dataset.uniqid,
input = element.querySelector(
@ -22,12 +27,13 @@ function loadDynamicPicker(element) {
? JSON.parse(input.value)
: input.value === "[]" || input.value === ""
? null
: [JSON.parse(input.value)],
suggested = JSON.parse(el.dataset.suggested),
as_id = parseInt(el.dataset.asId) === 1,
submit_on_adding_new_entity =
parseInt(el.dataset.submitOnAddingNewEntity) === 1,
label = el.dataset.label;
: [JSON.parse(input.value)];
suggested = JSON.parse(el.dataset.suggested);
as_id = parseInt(el.dataset.asId) === 1;
submit_on_adding_new_entity =
parseInt(el.dataset.submitOnAddingNewEntity) === 1;
label = el.dataset.label;
isCurrentUserPicker = uniqId.startsWith("pick_user_or_me_dyn");
if (!isMultiple) {
if (input.value === "[]") {
@ -44,6 +50,7 @@ function loadDynamicPicker(element) {
':uniqid="uniqid" ' +
':suggested="notPickedSuggested" ' +
':label="label" ' +
':isCurrentUserPicker="isCurrentUserPicker" ' +
'@addNewEntity="addNewEntity" ' +
'@removeEntity="removeEntity" ' +
'@addNewEntityProcessEnded="addNewEntityProcessEnded"' +
@ -61,6 +68,7 @@ function loadDynamicPicker(element) {
as_id,
submit_on_adding_new_entity,
label,
isCurrentUserPicker,
};
},
computed: {
@ -89,7 +97,8 @@ function loadDynamicPicker(element) {
const ids = this.picked.map((el) => el.id);
input.value = ids.join(",");
}
console.log(entity);
console.log(this.picked);
// console.log(entity);
}
} else {
if (

View File

@ -1,10 +1,25 @@
<template>
<ul :class="listClasses" v-if="picked.length && displayPicked">
<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>
</ul>
<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">
<add-persons
:options="addPersonsOptions"
@ -24,119 +39,83 @@
</ul>
</template>
<script>
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue";
<script setup>
import { ref, computed } from "vue";
import AddPersons from "ChillPersonAssets/vuejs/_components/AddPersons.vue"; // eslint-disable-line
import { appMessages } from "./i18n";
import { trans, USER_CURRENT_USER } from "translator";
export default {
name: "PickEntity",
props: {
multiple: {
type: Boolean,
required: true,
},
types: {
type: Array,
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 props = defineProps({
multiple: Boolean,
types: Array,
picked: Array,
uniqid: String,
removableIfSet: { type: Boolean, default: true },
displayPicked: { type: Boolean, default: true },
suggested: { type: Array, default: () => [] },
label: String,
isCurrentUserPicker: { type: Boolean, default: false },
});
let trans = [];
this.types.forEach((t) => {
if (this.$props.multiple) {
trans.push(appMessages.fr.pick_entity[t].toLowerCase());
} else {
trans.push(
appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
);
}
});
const emit = defineEmits([
"addNewEntity",
"removeEntity",
"addNewEntityProcessEnded",
]);
if (this.$props.multiple) {
return (
appMessages.fr.pick_entity.modal_title + trans.join(", ")
);
} else {
return (
appMessages.fr.pick_entity.modal_title_one +
trans.join(", ")
);
}
},
listClasses() {
return {
"list-suggest": true,
"remove-items": this.$props.removableIfSet,
};
},
},
methods: {
addNewSuggested(entity) {
this.$emit("addNewEntity", { entity: entity });
},
addNewEntity({ selected, modal }) {
selected.forEach((item) => {
this.$emit("addNewEntity", { entity: item.result });
}, this);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
this.$emit("addNewEntityProcessEnded");
},
removeEntity(entity) {
if (!this.$props.removableIfSet) {
return;
}
this.$emit("removeEntity", { entity: entity });
},
},
const itsMeCheckbox = ref(null);
const addPersons = ref(null);
const addPersonsOptions = computed(() => ({
uniq: !props.multiple,
type: props.types,
priority: null,
button: { size: "btn-sm", class: "btn-submit" },
}));
const translatedListOfTypes = computed(() => {
if (props.label) return props.label;
let trans = props.types.map((t) =>
props.multiple
? appMessages.fr.pick_entity[t].toLowerCase()
: appMessages.fr.pick_entity[t + "_one"].toLowerCase(),
);
return props.multiple
? appMessages.fr.pick_entity.modal_title + trans.join(", ")
: appMessages.fr.pick_entity.modal_title_one + trans.join(", ");
});
const listClasses = computed(() => ({
"list-suggest": true,
"remove-items": props.removableIfSet,
}));
const selectItsMe = (event) =>
event.target.checked ? addNewSuggested("me") : removeEntity("me");
const addNewSuggested = (entity) => {
emit("addNewEntity", { 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>
<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
user:
current_user: Utilisateur courant
profile:
title: Mon profil
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\Form\Type\PickRollingDateType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use Chill\MainBundle\Form\Type\PickUserOrMeDynamicType;
use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\PersonBundle\Export\Declarations;
@ -24,6 +24,7 @@ use Symfony\Component\Form\FormBuilderInterface;
final readonly class ReferrerFilter implements FilterInterface
{
use \Chill\MainBundle\Export\ExportDataNormalizerTrait;
private const A = 'acp_referrer_filter_uhistory';
private const P = 'acp_referrer_filter_date';
@ -68,7 +69,7 @@ final readonly class ReferrerFilter implements FilterInterface
public function buildForm(FormBuilderInterface $builder): void
{
$builder
->add('accepted_referrers', PickUserDynamicType::class, [
->add('accepted_referrers', PickUserOrMeDynamicType::class, [
'multiple' => true,
])
->add('date_calc', PickRollingDateType::class, [