add new component searchable select close #28

This commit is contained in:
Adrian Zürcher
2025-11-17 16:57:13 +01:00
parent 39e5479947
commit 09299c65de
3 changed files with 149 additions and 19 deletions

View File

@@ -1,7 +1,9 @@
<template> <template>
<DialogFrame ref="dialog" :header-title="localTitle"> <DialogFrame ref="dialog" :header-title="localTitle">
<div class="row justify-center"> <div class="row justify-center">
<q-select <SearchableSelect
class="q-mx-xl"
dense
autofocus autofocus
:label="$t('event')" :label="$t('event')"
filled filled
@@ -12,7 +14,7 @@
@keyup.enter="addAttendees" @keyup.enter="addAttendees"
map-options map-options
emit-value emit-value
></q-select> ></SearchableSelect>
</div> </div>
<div class="row justify-center"> <div class="row justify-center">
<q-btn class="q-ma-md" color="primary" no-caps @click="addAttendees">{{ localTitle }}</q-btn> <q-btn class="q-ma-md" color="primary" no-caps @click="addAttendees">{{ localTitle }}</q-btn>
@@ -40,6 +42,7 @@ import type { Members } from 'src/vueLib/models/member';
import EditAllDialog from 'src/components/EventEditAllDialog.vue'; import EditAllDialog from 'src/components/EventEditAllDialog.vue';
import { useAttendeesTable } from 'src/vueLib/tables/attendees/AttendeesTable'; import { useAttendeesTable } from 'src/vueLib/tables/attendees/AttendeesTable';
import { useEventTable } from 'src/vueLib/tables/events/EventsTable'; import { useEventTable } from 'src/vueLib/tables/events/EventsTable';
import SearchableSelect from 'src/vueLib/general/SearchableSelect .vue';
const dialog = ref(); const dialog = ref();
const newEventRef = ref(); const newEventRef = ref();
@@ -105,7 +108,7 @@ async function addAttendees() {
NotifyResponse(err, 'error'); NotifyResponse(err, 'error');
}); });
await updateAttendees(); await updateAttendees(0);
updateEvents(); updateEvents();
} }

View File

@@ -0,0 +1,115 @@
<template>
<q-select
ref="selectRef"
v-model="modelValueLocal"
:options="filteredOptions"
:option-label="optionLabel"
:option-value="optionValue"
:use-input="search"
input-debounce="200"
@filter="filterFn"
@update:model-value="emitValue"
v-bind="$attrs"
><template v-slot:append
><q-btn
size="xs"
flat
dense
round
icon="search"
:color="search ? 'primary' : 'grey'"
@click="searchBox"
>
</q-btn
></template>
</q-select>
</template>
<script setup lang="ts" generic="T extends Record<string, string | number | object>">
import type { PropType } from 'vue';
import { ref, watch } from 'vue';
const search = ref(false);
const selectRef = ref();
defineOptions({ inheritAttrs: false });
const props = defineProps({
modelValue: {
type: [String, Number, Object] as PropType<string | number | object | null>,
default: null,
},
options: {
type: Array as PropType<T[]>,
required: true,
},
optionLabel: {
type: [Function, String] as PropType<((option: T) => string) | string>,
required: true,
},
optionValue: {
type: String,
required: true,
},
});
const emit = defineEmits<{
(e: 'update:modelValue', value: string | number | null): void;
}>();
const optionLabel = props.optionLabel;
const optionValue = props.optionValue;
const modelValueLocal = ref<string | number | object | null | undefined>(props.modelValue);
const filteredOptions = ref<T[]>([...props.options]);
function searchBox() {
if (search.value) {
selectRef.value.updateInputValue('');
}
search.value = !search.value;
}
watch(
() => props.modelValue,
(v) => (modelValueLocal.value = v),
);
watch(
() => props.options,
(o) => (filteredOptions.value = [...o]),
);
function filterFn(val: string, update: (fn: () => void) => void) {
update(() => {
if (!val) {
filteredOptions.value = [...props.options];
return;
}
const needle = val.toLowerCase();
filteredOptions.value = props.options.filter((opt) => {
let field = null;
if (typeof optionLabel === 'function') {
field = optionLabel(opt);
} else {
field = opt[optionLabel];
}
if (typeof field !== 'string' && typeof field !== 'number') return false;
return String(field).toLowerCase().includes(needle);
});
// Remove duplicates by optionValue
const seen = new Set();
filteredOptions.value = filteredOptions.value.filter((opt) => {
const value = opt[optionValue];
if (seen.has(value)) return false;
seen.add(value);
return true;
});
});
}
function emitValue(val: string | number | null) {
emit('update:modelValue', val);
}
</script>

View File

@@ -74,6 +74,7 @@
@click="addMemberTo" @click="addMemberTo"
> >
<q-badge floating transparent color="primary" text-color="primary-text">+</q-badge> <q-badge floating transparent color="primary" text-color="primary-text">+</q-badge>
<q-tooltip>{{ $t('addToEvent') }}</q-tooltip>
</q-btn> </q-btn>
<q-btn v-else flat dense icon="more_vert" @click="openSubmenu = true" /> <q-btn v-else flat dense icon="more_vert" @click="openSubmenu = true" />
@@ -164,7 +165,7 @@
</template> </template>
<template v-slot:body-cell-group="props"> <template v-slot:body-cell-group="props">
<q-td :props="props"> <q-td :props="props">
<q-select <SearchableSelect
v-if="groups.length > 0" v-if="groups.length > 0"
dense dense
:readonly="!user.isPermittedTo('members', 'write')" :readonly="!user.isPermittedTo('members', 'write')"
@@ -175,20 +176,20 @@
option-label="name" option-label="name"
v-model="props.row.group" v-model="props.row.group"
@update:model-value="updateMember(props.row)" @update:model-value="updateMember(props.row)"
></q-select> />
</q-td> </q-td>
</template> </template>
<template v-slot:body-cell-responsiblePerson="props"> <template v-slot:body-cell-responsiblePerson="props">
<q-td :props="props"> <q-td :props="props">
<q-select <SearchableSelect
dense
v-if="responsibles.length > 0" v-if="responsibles.length > 0"
:readonly="!user.isPermittedTo('members', 'write')" :readonly="!user.isPermittedTo('members', 'write')"
:options="responsibles" :options="responsibles"
:option-label="(opt) => opt.firstName + ' ' + opt.lastName" :option-label="(opt) => opt.firstName + ' ' + opt.lastName"
option-value="firstName"
v-model="props.row.responsiblePerson" v-model="props.row.responsiblePerson"
@update:model-value="updateMember(props.row)" @update:model-value="updateMember(props.row)"
></q-select> />
</q-td> </q-td>
</template> </template>
<template v-slot:body-cell-option="props"> <template v-slot:body-cell-option="props">
@@ -274,6 +275,7 @@ import { databaseName } from 'src/vueLib/models/settings';
import { useUserStore } from 'src/vueLib/login/userStore'; import { useUserStore } from 'src/vueLib/login/userStore';
import { i18n } from 'src/boot/lang'; import { i18n } from 'src/boot/lang';
import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage'; import { getLocalPageDefaults, setLocalPageDefaults } from 'src/localstorage/localStorage';
import SearchableSelect from 'src/vueLib/general/SearchableSelect .vue';
const inProps = defineProps({ const inProps = defineProps({
addAttendees: { type: Boolean }, addAttendees: { type: Boolean },
@@ -348,13 +350,13 @@ onMounted(() => {
selectedColumnFilter.value = defaults?.filteredColumn || ''; selectedColumnFilter.value = defaults?.filteredColumn || '';
selectedColumnOptions.value = defaults?.filteredValue ?? []; selectedColumnOptions.value = defaults?.filteredValue ?? [];
// set custom filter
setNewFilter(selectedColumnFilter.value, ...selectedColumnOptions.value); setNewFilter(selectedColumnFilter.value, ...selectedColumnOptions.value);
appApi appApi
.post('database/open', { dbPath: databaseName.value, create: true }) .post('database/open', { dbPath: databaseName.value, create: true })
.then(() => { .then(() => {
updateMembers(inProps.compareMembers, inProps.addResponsible).catch((err) => updateTable().catch((err) => NotifyResponse(err, 'error'));
NotifyResponse(err, 'error'),
);
}) })
.catch((err) => NotifyResponse(err, 'error')) .catch((err) => NotifyResponse(err, 'error'))
@@ -363,6 +365,12 @@ onMounted(() => {
}); });
}); });
async function updateTable() {
await updateMembers(localCompareMembers.value, inProps.addResponsible).catch((err) =>
NotifyResponse(err, 'error'),
);
}
// opens dialog for all member values // opens dialog for all member values
function openSingleValueDialog(label: string, field: string, member: Member) { function openSingleValueDialog(label: string, field: string, member: Member) {
editOneDialog.value?.open(label, field, member); editOneDialog.value?.open(label, field, member);
@@ -414,7 +422,7 @@ function setColumnOptions(columnName: string) {
async function filterMembers(field: string, ...keys: string[]) { async function filterMembers(field: string, ...keys: string[]) {
setNewFilter(field, ...keys); setNewFilter(field, ...keys);
setLocalPageDefaults(page.value, field, keys); setLocalPageDefaults(page.value, field, keys);
await updateMembers(); await updateTable();
} }
//remove member from database //remove member from database
@@ -428,7 +436,7 @@ function removeMember(...removeMembers: Members) {
appApi appApi
.post('members/delete', { ids: memberIds }) .post('members/delete', { ids: memberIds })
.then(() => { .then(() => {
updateMembers().catch((err) => NotifyResponse(err, 'error')); updateTable().catch((err) => NotifyResponse(err, 'error'));
selected.value = []; selected.value = [];
}) })
.catch((err) => NotifyResponse(err, 'error')) .catch((err) => NotifyResponse(err, 'error'))
@@ -443,7 +451,7 @@ function updateMember(member: Member | null) {
.post('/members/edit', [member]) .post('/members/edit', [member])
.then(() => NotifyResponse(i18n.global.t('memberUpdated'))) .then(() => NotifyResponse(i18n.global.t('memberUpdated')))
.catch((err) => NotifyResponse(err, 'error')); .catch((err) => NotifyResponse(err, 'error'));
updateMembers().catch((err) => NotifyResponse(err, 'error')); updateTable().catch((err) => NotifyResponse(err, 'error'));
} }
function addToEvent() { function addToEvent() {
@@ -485,8 +493,10 @@ async function addMemberTo() {
if (inProps.addAttendees) { if (inProps.addAttendees) {
await updateMemberLastVisit(selected.value); await updateMemberLastVisit(selected.value);
} else {
await updateTable();
emit('update-event', filteredMembers.value.length);
} }
emit('update-event');
} }
async function updateMemberLastVisit(members: Members) { async function updateMemberLastVisit(members: Members) {
@@ -514,11 +524,13 @@ async function updateMemberLastVisit(members: Members) {
} }
}) })
.catch((err) => NotifyResponse(err, 'error')); .catch((err) => NotifyResponse(err, 'error'));
await updateMembers(localCompareMembers.value, inProps.addResponsible) localCompareMembers.value?.push(...members);
.then(() => localCompareMembers.value?.push(...members)) await updateTable().catch((err) => NotifyResponse(err, 'error'));
.catch((err) => NotifyResponse(err, 'error'));
emit('update-event'); emit('update-event', filteredMembers.value.length);
} }
defineExpose({ allMembers, filteredMembers });
</script> </script>
<style> <style>