Update SelectionDialog component

- Remove deprecated BTable in favor of GTable for improved functionality.
- Update field types from FieldEntry to TableField for consistency.
- Enhance sorting and filtering capabilities with new computed properties.
- Refactor event handling for row clicks and sorting changes.
- Improve pagination reset logic when filters are applied.
This commit is contained in:
Alireza Heidari
2026-03-23 15:05:53 +01:00
committed by Alireza Heidari
parent e7272a77d1
commit 5044379dc3
@@ -3,29 +3,30 @@ import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { faCheckSquare, faMinusSquare, faSquare } from "@fortawesome/free-regular-svg-icons";
import { faCaretLeft, faCheck, faFolder, faSpinner, faTimes } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BButton, BLink, BPagination, BTable } from "bootstrap-vue";
import { BAlert, BButton, BLink, BPagination, BSpinner } from "bootstrap-vue";
import { computed, ref, watch } from "vue";
import type { RowClickEvent, TableField } from "@/components/Common/GTable.types";
import { type ItemsProvider, SELECTION_STATES, type SelectionState } from "@/components/SelectionDialog/selectionTypes";
import type Filtering from "@/utils/filtering";
import type { FieldEntry, SelectionItem } from "./selectionTypes";
import type { SelectionItem } from "./selectionTypes";
import GModal from "../BaseComponents/GModal.vue";
import Heading from "../Common/Heading.vue";
import FilterMenu from "@/components/Common/FilterMenu.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
import GTable from "@/components/Common/GTable.vue";
import DataDialogSearch from "@/components/SelectionDialog/DataDialogSearch.vue";
import StatelessTags from "@/components/TagsMultiselect/StatelessTags.vue";
const LABEL_FIELD: FieldEntry = { key: "label", sortable: true };
const SELECT_ICON_FIELD: FieldEntry = { key: "__select_icon__", label: "", sortable: false };
const LABEL_FIELD: TableField = { key: "label", label: "Name", sortable: true };
const SELECT_ICON_FIELD: TableField = { key: "__select_icon__", label: "", sortable: false };
interface Props {
disableOk?: boolean;
errorMessage?: string;
fileMode?: boolean;
fields?: FieldEntry[];
fields?: TableField[];
isBusy?: boolean;
isEncoded?: boolean;
items?: SelectionItem[];
@@ -87,15 +88,22 @@ const currentPage = ref(1);
const perPage = ref(25);
const showAdvancedSearch = ref(false);
const providerRequestId = ref(0);
const providerItems = ref<SelectionItem[]>([]);
const sortBy = ref<string | undefined>(undefined);
const sortDesc = ref<boolean | undefined>(undefined);
const usingProvider = computed(() => Boolean(props.itemsProvider));
const okButtonText = computed(() => {
return props.okButtonText ? props.okButtonText : props.fileMode ? "Select" : "Select this folder";
});
const fieldDetails = computed(() => {
const fields = props.fields.slice().map((x) => {
x.sortable = x.sortable === undefined ? true : x.sortable;
return x;
});
const fieldDetails = computed<TableField[]>(() => {
const fields: TableField[] = props.fields.slice().map((field) => ({
...field,
sortable: field.sortable ?? true,
}));
if (fields.length === 0) {
fields.unshift(LABEL_FIELD);
}
@@ -105,7 +113,44 @@ const fieldDetails = computed(() => {
return fields;
});
function selectionIcon(variant: string) {
const tableItems = computed(() => {
return usingProvider.value ? providerItems.value : props.items;
});
async function loadProviderItems() {
if (!props.itemsProvider || !props.optionsShow) {
return;
}
const requestId = ++providerRequestId.value;
const result = await props.itemsProvider({
apiUrl: props.providerUrl,
currentPage: currentPage.value,
perPage: perPage.value,
filter: filter.value || undefined,
sortBy: sortBy.value,
sortDesc: sortDesc.value,
});
if (requestId === providerRequestId.value) {
providerItems.value = result ?? [];
}
}
function onSortChanged(newSortBy: string, newSortDesc: boolean) {
sortBy.value = newSortBy || undefined;
sortDesc.value = newSortDesc;
}
function onRowClick(event: RowClickEvent<SelectionItem>) {
emit("onClick", event.item);
}
function onOpen(item: SelectionItem) {
emit("onOpen", item);
}
function selectionIcon(variant?: string) {
switch (variant) {
case SELECTION_STATES.SELECTED:
return faCheckSquare;
@@ -116,13 +161,6 @@ function selectionIcon(variant: string) {
}
}
/** Resets pagination when a filter/search word is entered **/
function filtered(items: SelectionItem[]) {
if (props.itemsProvider === undefined) {
resetPagination();
}
}
/** Format time stamp */
function formatTime(value: string) {
if (value) {
@@ -159,6 +197,30 @@ if (props.watchOnPageChanges) {
}
const dialog = ref<InstanceType<typeof GModal> | null>(null);
watch(
[
() => props.itemsProvider,
currentPage,
perPage,
filter,
sortBy,
sortDesc,
() => props.providerUrl,
() => props.optionsShow,
],
() => {
if (props.itemsProvider && props.optionsShow) {
void loadProviderItems();
}
},
{ immediate: true },
);
watch(filter, () => {
resetPagination();
});
watch(
() => dialog.value,
(newValue) => {
@@ -208,19 +270,21 @@ defineExpose({
</BAlert>
<div v-else>
<div v-if="optionsShow" data-description="selection dialog options">
<BTable
small
hover
<GTable
class="selection-dialog-table"
primary-key="id"
:busy="isBusy"
clickable-rows
compact
hover
:current-page="currentPage"
:items="itemsProvider ?? items"
:fields="fieldDetails"
:filter="filter"
:items="tableItems"
:loading="isBusy"
:local-filtering="!usingProvider"
:local-sorting="!usingProvider"
:per-page="perPage"
@filtered="filtered"
@row-clicked="emit('onClick', $event)">
@sort-changed="onSortChanged"
@row-click="onRowClick">
<template v-slot:head(__select_icon__)="">
<FontAwesomeIcon
class="select-checkbox cursor-pointer"
@@ -228,9 +292,11 @@ defineExpose({
:icon="selectionIcon(selectAllVariant)"
@click="$emit('onSelectAll')" />
</template>
<template v-slot:cell(__select_icon__)="data">
<FontAwesomeIcon :icon="selectionIcon(data.item._rowVariant)" />
</template>
<template v-slot:cell(label)="data">
<div style="cursor: pointer">
<pre
@@ -241,34 +307,46 @@ defineExpose({
<i :class="leafIcon" />
<span :title="`label-${data.item.url}`">{{ data.value ? data.value : "-" }}</span>
</div>
<div v-else @click.stop="emit('onOpen', data.item)">
<div
v-else
role="button"
tabindex="0"
@click.stop="onOpen(data.item)"
@keydown.enter.stop="onOpen(data.item)"
@keydown.space.stop.prevent="onOpen(data.item)">
<FontAwesomeIcon :icon="props.folderIcon" />
<BLink :title="`label-${data.item.url}`">{{ data.value ? data.value : "-" }}</BLink>
</div>
</span>
</div>
</template>
<template v-slot:cell(details)="data">
<span :title="`details-${data.item.url}`">{{ data.value ? data.value : "-" }}</span>
</template>
<template v-slot:cell(tags)="data">
<StatelessTags v-if="data.value?.length > 0" :value="data.value" :disabled="true" />
<span v-else>-</span>
</template>
<template v-slot:cell(time)="data">
{{ formatTime(data.value) }}
</template>
<template v-slot:cell(update_time)="data">
{{ formatTime(data.value) }}
</template>
</BTable>
</GTable>
<div v-if="isBusy" class="text-center">
<LoadingSpan />
<BSpinner small type="grow" />
<BSpinner small type="grow" />
<BSpinner small type="grow" />
</div>
<div v-else-if="totalItems === 0">
<div v-if="filter">
No search results found for: <b>{{ filter }}</b
>.
No search results found for: <b> {{ filter }} </b>.
</div>
<div v-else>No entries.</div>
</div>