Refactor AddPersons and related components for consistent state management and improved handling of selected suggestions.

- Replaced `search.selected` with a dedicated `selected` state and implemented explicit methods for adding, removing, and clearing selections.
- Updated event handling and props (`addNewPersons`, `selected`, `updateSelected`, `cleanSelected`) for better separation of concerns and type safety.
- Introduced `isSelected` utility for streamlined selection checks and replaced deprecated event usages.
- Adjusted modal behaviors in `PersonChooseModal.vue` and `AddPersons.vue` for improved integration and alignment with new state logic.
This commit is contained in:
2025-10-28 12:41:52 +01:00
parent 6d4c4d2c74
commit b0c63fab91
4 changed files with 120 additions and 71 deletions

View File

@@ -322,7 +322,7 @@ export default {
}
});
},
addNewPersons({ selected, modal }) {
addNewPersons({ selected }) {
//console.log('@@@ CLICK button addNewPersons', selected);
this.$store
.dispatch("addRequestor", selected.shift())
@@ -337,7 +337,6 @@ export default {
});
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
},
saveFormOnTheFly(payload) {
console.log(

View File

@@ -10,16 +10,18 @@
<person-choose-modal
v-if="showModalChoose"
ref="personChooseModal"
:modal-title="modalTitle"
:options="options"
:suggested="suggested"
:selected="selected"
:modal-dialog-class="'modal-dialog-scrollable modal-xl'"
:allow-create="props.allowCreate"
@close="closeModalChoose"
@addNewPersons="(payload) => emit('addNewPersons', payload)"
@onPickEntities="onPickEntities"
@onAskForCreate="onAskForCreate"
@triggerAddContact="triggerAddContact"
@updateSelected="updateSelected"
@cleanSelected="emptySelected"
/>
<CreateModal
@@ -48,7 +50,7 @@
</template>
<script setup lang="ts">
import { ref, computed } from "vue";
import {ref, computed, nextTick, useTemplateRef} from "vue";
import PersonChooseModal from "./AddPersons/PersonChooseModal.vue";
import type {
Suggestion,
@@ -64,7 +66,6 @@ import {Thirdparty, ThirdpartyCompany} from "../../../../../ChillThirdPartyBundl
interface AddPersonsConfig {
suggested?: Suggestion[];
selected?: Suggestion[];
buttonTitle: string;
modalTitle: string;
options: SearchOptions;
@@ -74,7 +75,6 @@ interface AddPersonsConfig {
const props = withDefaults(defineProps<AddPersonsConfig>(), {
suggested: () => [],
selected: () => [],
allowCreate: () => true,
types: () => ["person"],
});
@@ -85,11 +85,38 @@ const emit =
}
>();
const personChooseModal = useTemplateRef('personChooseModal');
/**
* Flag to show/hide the modal "choose".
*/
const showModalChoose = ref(false);
/**
* Flag to show/hide the modal "create".
*/
const showModalCreate = ref(false);
/**
* Store the previous search query, stored while going from "search" state to "create"
*/
const query = ref("");
/**
* Temporarily store the thirdparty company when calling "addContact"
*/
const thirdPartyParentAddContact = ref<ThirdpartyCompany|null>(null);
/**
* Contains the selected elements.
*
* If the property option.uniq is true, this will contains only one element.
*
* Suggestion must be added/removed using the @link{addSuggestionToSelected} and @link{removeSuggestionFromSelected}
* methods.
*/
const selected = ref<Suggestion[]>([]);
const getClassButton = computed(() => {
const size = props.options?.button?.size ?? "";
const type = props.options?.button?.type ?? "btn-create";
@@ -113,55 +140,103 @@ const creatableEntityTypes = computed<CreatableEntityType[]>(() => {
);
});
function onAskForCreate(payload: { query: string }) {
function onAskForCreate(payload: { query: string }): void {
query.value = payload.query;
closeModalChoose();
showModalCreate.value = true;
}
function openModalChoose() {
function openModalChoose(): void {
showModalChoose.value = true;
}
function closeModalChoose() {
function closeModalChoose(): void {
showModalChoose.value = false;
}
function closeModalCreate() {
function closeModalCreate(): void {
if (null !== thirdPartyParentAddContact) {
thirdPartyParentAddContact.value = null;
}
showModalCreate.value = false;
}
function triggerAddContact({parent}: {parent: ThirdpartyCompany}) {
console.log("triggerAddContact", parent);
/**
* Called by PersonSuggestion's updateSelection event, when an element is checked/unchecked
*/
function updateSelected(payload: {suggestion: Suggestion, isSelected: boolean}): void {
if (payload.isSelected) {
addSuggestionToSelected(payload.suggestion);
} else {
removeSuggestionFromSelected(payload.suggestion);
}
}
function addSuggestionToSelected(suggestion: Suggestion): void {
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);
}
}
function removeSuggestionFromSelected(suggestion: Suggestion): void {
selected.value = selected.value.filter((s: Suggestion) => s.key !== suggestion.key && s.result.id !== suggestion.result.id);
}
function emptySelected(): void {
selected.value.splice(0, selected.value.length);
}
function onPickEntities(): void {
emit("addNewPersons", { selected: selected.value });
closeModalChoose();
}
function triggerAddContact({parent}: {parent: ThirdpartyCompany}): void {
closeModalChoose();
openModalChoose();
thirdPartyParentAddContact.value = parent;
showModalCreate.value = true;
}
function onPersonCreated(payload: { person: Person }) {
console.log("onPersonCreated", payload);
function onPersonCreated(payload: { person: Person }): void {
showModalCreate.value = false;
const suggestion = {
result: payload.person,
relevance: 999999,
key: "person",
};
emit("addNewPersons", { selected: [suggestion] });
addSuggestionToSelected(suggestion);
if (props.options.uniq) {
emit("addNewPersons", { selected: [suggestion] });
} else {
openModalChoose();
}
}
function onThirdPartyCreated(payload: {thirdParty: Thirdparty}) {
console.log("onThirdPartyCreated", payload);
function onThirdPartyCreated(payload: {thirdParty: Thirdparty}): void {
showModalCreate.value = false;
const suggestion = {
result: payload.thirdParty,
relevance: 999999,
key: "thirdparty",
};
emit("addNewPersons", { selected: [suggestion] });
addSuggestionToSelected(suggestion);
if (props.options.uniq) {
emit("addNewPersons", { selected: [suggestion] });
} else {
openModalChoose();
}
}
function resetSearch(): void {
selected.value = [];
personChooseModal.value?.resetSearch();
}
defineExpose({resetSearch})
</script>
<style lang="scss" scoped>

View File

@@ -62,10 +62,10 @@
v-for="item in selectedAndSuggested.slice().reverse()"
:key="itemKey(item)"
:item="item"
:search="search"
:isSelected="isSelected(item)"
:type="checkUniq"
@update-selected="updateSelected"
@trigger-add-contact="(payload) => emit('triggerAddContact', payload)"
@update-selected="(payload) => emit('updateSelected', payload)"
@trigger-add-contact="triggerAddContact"
/>
<div
@@ -124,29 +124,27 @@ interface Props {
modalTitle: string;
options: SearchOptions;
suggested?: Suggestion[];
selected?: Suggestion[];
selected: Suggestion[];
modalDialogClass?: string;
allowCreate?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
suggested: () => [],
selected: () => [],
modalDialogClass: "modal-dialog-scrollable modal-xl",
allowCreate: () => true,
});
const emit = defineEmits<{
(e: "close"): void;
/** @deprecated use 'onPickEntities' */
(e: "addNewPersons", payload: { selected: Suggestion[] }): void;
(e: "onPickEntities", payload: { selected: EntitiesOrMe[] }): void;
(e: "onPickEntities"): void;
(e: "onAskForCreate", payload: { query: string }): void;
(e: "triggerAddContact", payload: { parent: ThirdpartyCompany }): void;
(e: "updateSelected", payload: {suggestion: Suggestion, isSelected: boolean}): void;
(e: "cleanSelected"): void;
}>();
const searchRef = ref<HTMLInputElement | null>(null);
const onTheFly = ref<InstanceType<typeof OnTheFly> | null>(null);
onMounted(() => {
// give the focus on the search bar
@@ -162,33 +160,16 @@ const search = reactive({
priorSuggestion: {} as Partial<Suggestion>,
});
watch(
() => props.selected,
(newSelected) => {
search.selected = newSelected ? [...newSelected] : [];
},
{ deep: true },
);
watch(
() => props.suggested,
(newSuggested) => {
search.suggested = newSuggested ? [...newSuggested] : [];
},
{ deep: true },
);
const query = computed({
get: () => search.query,
set: (val: string) => setQuery(val),
});
const queryLength = computed(() => search.query.length);
const suggestedCounter = computed(() => search.suggested.length);
const selectedComputed = computed<Suggestion[]>(() => search.selected);
const selectedCounter = computed(() => search.selected.length);
const checkUniq = computed(() =>
props.options.uniq === true ? "radio" : "checkbox",
props.options.uniq ? "radio" : "checkbox",
);
const priorSuggestion = computed(() => search.priorSuggestion);
@@ -203,6 +184,13 @@ function addPriorSuggestion() {
}
}
/**
* 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 selectedAndSuggested = computed(() => {
addPriorSuggestion();
const uniqBy = (a: Suggestion[], key: (item: Suggestion) => string) => [
@@ -260,17 +248,13 @@ function loadSuggestions(suggestedArr: Suggestion[]) {
});
}
function updateSelected(value: Suggestion[]) {
search.selected = value;
}
function resetSuggestion() {
search.query = "";
search.suggested = [];
}
function resetSelection() {
search.selected = [];
emit("cleanSelected");
}
function resetSearch() {
@@ -279,8 +263,8 @@ function resetSearch() {
}
function selectAll() {
search.suggested.forEach((item) => {
search.selected.push(item);
search.suggested.forEach((suggestion: Suggestion) => {
emit("updateSelected", {suggestion, isSelected: true})
});
}
@@ -292,10 +276,7 @@ function triggerAddContact(payload: {parent: ThirdpartyCompany}) {
* Triggered when the user clicks on the "add" button.
*/
function pickEntities(): void {
emit("addNewPersons", { selected: search.selected });
emit("onPickEntities", {
selected: search.selected.map((s: Suggestion) => s.result),
});
emit("onPickEntities", );
search.query = "";
emit("close");
}

View File

@@ -4,10 +4,9 @@
<div>
<input
:type="type"
v-model="selected"
name="item"
:id="item.key"
:value="setValueByType(item, type)"
v-model="isChecked"
/>
</div>
@@ -62,23 +61,18 @@ import {ThirdpartyCompany} from "../../../../../../ChillThirdPartyBundle/Resourc
const props = defineProps<{
item: Suggestion;
search: { selected: Suggestion[] };
type: string;
isSelected: boolean;
type: "radio"|"checkbox";
}>();
const emit = defineEmits<{
(e: "updateSelected", payload: Suggestion[]): void;
(e: "updateSelected", payload: {suggestion: Suggestion, isSelected: boolean}): void;
(e: "triggerAddContact", payload: { parent: ThirdpartyCompany }): void;
}>();
// v-model for selected
const selected = computed({
get: () => props.search.selected,
set: (value) => emit("updateSelected", value),
});
const isChecked = computed(
() => props.search.selected.indexOf(props.item) !== -1,
);
const isChecked = computed<boolean>({
get: () => props.isSelected,
set: value => emit("updateSelected", {suggestion: props.item, isSelected: props.type === "radio" ? true : value})
})
function setValueByType(value: Suggestion, type: string) {
return type === "radio" ? [value] : value;