Refactor PersonSuggestion and PersonChooseModal for streamlined state management and improved prop handling.

- Replaced `search.suggested` and `search.selected` with dedicated reactive `suggested` and `selected` states.
- Updated `PersonSuggestion.vue` to handle selection via `onUpdateValue` and removed `v-model` usage.
- Simplified `PersonChooseModal.vue` to compute `selectedAndSuggested` directly using props.
- Adjusted `types.ts` to refine `Search.results` and standardize `Suggestion` structure.
This commit is contained in:
2025-10-28 14:32:12 +01:00
parent b0c63fab91
commit 6b208e9962
4 changed files with 61 additions and 52 deletions

View File

@@ -429,7 +429,6 @@ export interface Suggestion {
relevance: number;
result: Entities;
}
export interface SearchPagination {
first: number;
items_per_page: number;
@@ -441,7 +440,7 @@ export interface SearchPagination {
export interface Search {
count: number;
pagination: SearchPagination;
results: Suggestion[];
results: {relevance: number, result: Entities}[];
}
export interface SearchOptions {

View File

@@ -85,7 +85,8 @@ const emit =
}
>();
const personChooseModal = useTemplateRef('personChooseModal');
type PersonChooseModalType = InstanceType<typeof PersonChooseModal>;
const personChooseModal = useTemplateRef<PersonChooseModalType>('personChooseModal');
/**
* Flag to show/hide the modal "choose".
@@ -173,12 +174,15 @@ function updateSelected(payload: {suggestion: Suggestion, isSelected: boolean}):
}
function addSuggestionToSelected(suggestion: Suggestion): void {
console.log("addSuggestionToSelected", suggestion);
console.log("before", selected.value);
if (props.options.uniq) {
selected.value = [suggestion];
} else {
// replace by an array with unique values
selected.value = [...selected.value, suggestion].filter((value, index, array) => array.indexOf(value) === index);
}
console.log("after", selected.value);
}
function removeSuggestionFromSelected(suggestion: Suggestion): void {

View File

@@ -59,10 +59,10 @@
<template #body>
<div class="results">
<person-suggestion
v-for="item in selectedAndSuggested.slice().reverse()"
:key="itemKey(item)"
v-for="item in selectedAndSuggested"
:key="item.key"
:item="item"
:isSelected="isSelected(item)"
:isSelected="item.isSelected"
:type="checkUniq"
@update-selected="(payload) => emit('updateSelected', payload)"
@trigger-add-contact="triggerAddContact"
@@ -116,7 +116,7 @@ import type {
Search,
EntityType,
SearchOptions,
EntitiesOrMe,
EntitiesOrMe, Entities,
} from "ChillPersonAssets/types";
import {ThirdpartyCompany} from "../../../../../../ChillThirdPartyBundle/Resources/public/types";
@@ -155,54 +155,52 @@ const search = reactive({
query: "" as string,
previousQuery: "" as string,
currentSearchQueryController: null as AbortController | null,
suggested: (props.suggested ?? []) as Suggestion[],
selected: (props.selected ?? []) as Suggestion[],
priorSuggestion: {} as Partial<Suggestion>,
});
/**
* Contains the suggested entities from the search results.
*
* In other words, those entities are displayed and selectable by the user
*/
const suggested = ref<Suggestion[]>([]);
const query = computed({
get: () => search.query,
set: (val: string) => setQuery(val),
});
const queryLength = computed(() => search.query.length);
const suggestedCounter = computed(() => search.suggested.length);
const selectedCounter = computed(() => search.selected.length);
const suggestedCounter = computed(() => suggested.value.length);
const selectedCounter = computed(() => props.selected.length);
const checkUniq = computed(() =>
props.options.uniq ? "radio" : "checkbox",
);
const priorSuggestion = computed(() => search.priorSuggestion);
const hasPriorSuggestion = computed(() => !!search.priorSuggestion.key);
const itemKey = (item: Suggestion) => item.result.type + item.result.id;
function addPriorSuggestion() {
if (hasPriorSuggestion.value) {
search.suggested.unshift(priorSuggestion.value as Suggestion);
search.selected.unshift(priorSuggestion.value as Suggestion);
}
}
/**
* Return true if the suggestion is present in the selected
* Return true if the suggestion is present in the selected.
*/
function isSelected(item: Suggestion): boolean {
return search.selected.some((s) => s.key === item.key);
const r = props.selected.some((s: Suggestion) => s.key === item.key);
console.log('isSelected', item.key, r);
return r;
}
const selectedAndSuggested = computed(() => {
addPriorSuggestion();
const uniqBy = (a: Suggestion[], key: (item: Suggestion) => string) => [
...new Map(a.map((x) => [key(x), x])).values(),
];
const union = [
...new Set([
...search.suggested.slice().reverse(),
...search.selected.slice().reverse(),
]),
];
return uniqBy(union, (k: Suggestion) => k.key);
const selectedAndSuggested = computed<(Suggestion & {isSelected: boolean})[]>(() => {
const selectedAndSuggested = [];
// add selected that are not in the search results
for (const selected of props.selected) {
if (!suggested.value.some((s: Suggestion) => s.key === selected.key)) {
selectedAndSuggested.push({...selected, isSelected: false});
}
}
for (const suggestion of suggested.value) {
selectedAndSuggested.push({...suggestion, isSelected: props.selected.some((s: Suggestion) => s.key === suggestion.key)});
}
return selectedAndSuggested;
});
function setQuery(q: string) {
@@ -241,16 +239,20 @@ function setQuery(q: string) {
}, delay);
}
function loadSuggestions(suggestedArr: Suggestion[]) {
search.suggested = suggestedArr;
search.suggested.forEach((item) => {
item.key = itemKey(item);
function loadSuggestions(suggestedArr: {relevance: number, result: Entities}[]): void {
suggested.value = suggestedArr.map((item) => {
return {
key: item.result.type + item.result.id,
relevance: item.relevance,
result: item.result
}
});
}
function resetSuggestion() {
search.query = "";
search.suggested = [];
suggested.value = [];
}
function resetSelection() {
@@ -263,7 +265,7 @@ function resetSearch() {
}
function selectAll() {
search.suggested.forEach((suggestion: Suggestion) => {
suggested.value.forEach((suggestion: Suggestion) => {
emit("updateSelected", {suggestion, isSelected: true})
});
}

View File

@@ -1,14 +1,13 @@
<template>
<div class="list-item" :class="{ checked: isChecked }">
<label>
<div>
<input
:type="type"
:value="item.key"
name="item"
:id="item.key"
v-model="isChecked"
@click="onUpdateValue"
/>
</div>
<suggestion-person
v-if="isSuggestionForPerson(item)"
@@ -69,13 +68,18 @@ const emit = defineEmits<{
(e: "triggerAddContact", payload: { parent: ThirdpartyCompany }): void;
}>();
const isChecked = computed<boolean>({
get: () => props.isSelected,
set: value => emit("updateSelected", {suggestion: props.item, isSelected: props.type === "radio" ? true : value})
})
const isChecked = computed<boolean>(() => props.isSelected)
function setValueByType(value: Suggestion, type: string) {
return type === "radio" ? [value] : value;
const onUpdateValue = (event: Event) => {
const target = event?.target;
if (!(target instanceof HTMLInputElement)) {
console.error("the value of checked is not an HTMLInputElement");
return;
}
console.log("onUpdateValue", target);
console.log("is selected", props.type === "radio" ? true: target.checked);
emit("updateSelected", {suggestion: props.item, isSelected: props.type === "radio" ? true : target.checked});
}
function triggerAddContact(payload: {parent: ThirdpartyCompany}) {