feat: add loading state and enhance media thumbnail functionality in data tables for improved user experience
This commit is contained in:
parent
f105d2cb3f
commit
765e74d8a9
@ -37,11 +37,13 @@ const props = withDefaults(
|
||||
paginationDisplayedCount?: number;
|
||||
paginationItemLabel?: string;
|
||||
getRowClassName?: (row: TData, index: number) => string | undefined;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
searchPlaceholder: 'Ketikkan sesuatu...',
|
||||
showRowNumber: true,
|
||||
paginationItemLabel: 'data',
|
||||
loading: false,
|
||||
}
|
||||
);
|
||||
|
||||
@ -147,7 +149,15 @@ function resolveRowSpan(row: TData, columnId: string): number | undefined {
|
||||
:search-placeholder="searchPlaceholder" @filter-change="(key, value) => emit('filter-change', key, value)"
|
||||
@filters-reset="emit('filters-reset')" />
|
||||
|
||||
<div class="min-w-0 rounded-md border">
|
||||
<div class="relative min-w-0 rounded-md border">
|
||||
<!-- Loading Overlay -->
|
||||
<div v-if="loading" class="absolute inset-0 z-10 flex items-center justify-center bg-background/50 backdrop-blur-[1px] transition-all duration-300">
|
||||
<div class="flex items-center gap-2 rounded-lg border bg-background px-4 py-2 shadow-md">
|
||||
<span class="size-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<span class="text-sm font-medium text-muted-foreground select-none">Memuat data...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||
|
||||
@ -13,6 +13,7 @@ const props = defineProps<{
|
||||
items: MediaItem[];
|
||||
maxVisible?: number;
|
||||
title?: string;
|
||||
allUrls?: string[];
|
||||
}>();
|
||||
|
||||
const previewOpen = ref(false);
|
||||
@ -119,5 +120,5 @@ function openGallery() {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="items.map(item => item.url)" :title="title" @close="onPreviewClose" />
|
||||
<MediaPreviewDialog v-model:open="previewOpen" v-model:url="previewUrl" :urls="allUrls ?? items.map(item => item.url)" :title="title" @close="onPreviewClose" />
|
||||
</template>
|
||||
|
||||
@ -9,6 +9,7 @@ type UseDataTableQueryOptions = {
|
||||
initial: DataTableQuery;
|
||||
filterKeys?: string[];
|
||||
debounceMs?: number;
|
||||
only?: string[];
|
||||
};
|
||||
|
||||
function buildParams(query: DataTableQuery, filterKeys: string[]): Record<string, string | undefined> {
|
||||
@ -28,12 +29,21 @@ function buildParams(query: DataTableQuery, filterKeys: string[]): Record<string
|
||||
export function useDataTableQuery(options: UseDataTableQueryOptions) {
|
||||
const filterKeys = options.filterKeys ?? [];
|
||||
const query = ref<DataTableQuery>({ ...options.initial });
|
||||
const isLoading = ref(false);
|
||||
|
||||
const reload = useDebounceFn(() => {
|
||||
router.get(options.url, buildParams(query.value, filterKeys), {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
preserveScroll: true,
|
||||
showProgress: false,
|
||||
only: options.only,
|
||||
onBefore: () => {
|
||||
isLoading.value = true;
|
||||
},
|
||||
onFinish: () => {
|
||||
isLoading.value = false;
|
||||
},
|
||||
});
|
||||
}, options.debounceMs ?? 300);
|
||||
|
||||
@ -67,6 +77,7 @@ export function useDataTableQuery(options: UseDataTableQueryOptions) {
|
||||
|
||||
return {
|
||||
query,
|
||||
isLoading,
|
||||
setSearch,
|
||||
setSort,
|
||||
setFilter,
|
||||
|
||||
@ -55,6 +55,10 @@ function rowNumber(index: number): number {
|
||||
return groupedTableRowNumber(props.firstItem, index);
|
||||
}
|
||||
|
||||
function allVariantImageUrls(product: ProductListItem): string[] {
|
||||
return product.variants.flatMap((variant) => (variant.images ?? []).map((img) => img.url));
|
||||
}
|
||||
|
||||
const verificationModalOpen = ref(false);
|
||||
const selectedRequestId = ref<number | null>(null);
|
||||
|
||||
@ -148,7 +152,7 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
{{ variant.name }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="variant.images ?? []" :max-visible="1" :title="`${product.name} - ${variant.name}`" />
|
||||
<MediaThumbnailCell :items="variant.images ?? []" :max-visible="1" :title="`${product.name} - ${variant.name}`" :all-urls="allVariantImageUrls(product)" />
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ variant.stock_formatted }}
|
||||
|
||||
@ -32,7 +32,7 @@ const props = defineProps<{
|
||||
const { can } = useCan();
|
||||
const search = ref(props.filters.search ?? '');
|
||||
|
||||
const { query, setSearch, setFilter, resetFilters, syncFromServer } =
|
||||
const { query, isLoading, setSearch, setFilter, resetFilters, syncFromServer } =
|
||||
useDataTableQuery({
|
||||
url: index.url(),
|
||||
initial: { ...props.filters },
|
||||
@ -129,6 +129,7 @@ watch(
|
||||
:pagination-links="rawMaterials.links"
|
||||
:filter-defs="filterDefs"
|
||||
:filter-values="filterValues"
|
||||
:loading="isLoading"
|
||||
@filter-change="setFilter"
|
||||
@filters-reset="resetFilters"
|
||||
/>
|
||||
|
||||
@ -32,6 +32,7 @@ const props = defineProps<{
|
||||
paginationLinks?: DataTablePaginationLink[];
|
||||
filterDefs?: DataTableFilterDef[];
|
||||
filterValues?: Record<string, string>;
|
||||
loading?: boolean;
|
||||
}>();
|
||||
|
||||
const search = defineModel<string>('search', { default: '' });
|
||||
@ -48,6 +49,10 @@ function rowNumber(index: number): number {
|
||||
return groupedTableRowNumber(props.firstItem, index);
|
||||
}
|
||||
|
||||
function allVariantImageUrls(material: RawMaterialListItem): string[] {
|
||||
return material.prices.flatMap((price) => (price.images ?? []).map((img) => img.url));
|
||||
}
|
||||
|
||||
const verificationModalOpen = ref(false);
|
||||
const selectedRequestId = ref<number | null>(null);
|
||||
|
||||
@ -64,8 +69,17 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
<DataTableToolbar v-model:search="search" :filter-defs="filterDefs" :filter-values="filterValues"
|
||||
@filter-change="(key, value) => emit('filter-change', key, value)" @filters-reset="emit('filters-reset')" />
|
||||
|
||||
<div v-if="materials.length" class="space-y-4">
|
||||
<div v-for="(material, index) in materials" :key="material.id" class="overflow-hidden rounded-md border">
|
||||
<div class="relative min-h-[100px]">
|
||||
<!-- Loading Overlay -->
|
||||
<div v-if="loading" class="absolute inset-0 z-10 flex items-center justify-center bg-background/50 backdrop-blur-[1px] transition-all duration-300">
|
||||
<div class="flex items-center gap-2 rounded-lg border bg-background px-4 py-2 shadow-md">
|
||||
<span class="size-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
<span class="text-sm font-medium text-muted-foreground select-none">Memuat data...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="materials.length" class="space-y-4">
|
||||
<div v-for="(material, index) in materials" :key="material.id" class="overflow-hidden rounded-md border">
|
||||
<div
|
||||
class="flex flex-col gap-3 border-b bg-muted/30 px-4 py-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="flex min-w-0 items-start gap-3">
|
||||
@ -130,7 +144,7 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
{{ price.variant }}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MediaThumbnailCell :items="price.images ?? []" :max-visible="1" :title="`${material.name} - ${price.variant}`" />
|
||||
<MediaThumbnailCell :items="price.images ?? []" :max-visible="1" :title="`${material.name} - ${price.variant}`" :all-urls="allVariantImageUrls(material)" />
|
||||
</TableCell>
|
||||
<TableCell class="tabular-nums">
|
||||
{{ price.stock_formatted }}
|
||||
@ -144,7 +158,8 @@ function openVerificationDetail(requestId: number | undefined) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataTableEmpty v-else />
|
||||
<DataTableEmpty v-else />
|
||||
</div>
|
||||
|
||||
<GroupedTableFooter :summary="paginationSummary" :pagination="pagination" :pagination-links="paginationLinks" />
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user