runeforge 0.0.53 → 0.0.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -26
- package/dist/components/crud/GenericCRUD.svelte +12 -22
- package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
- package/dist/components/crud/utils/reorder.d.ts +4 -0
- package/dist/components/crud/utils/reorder.js +13 -0
- package/dist/components/crud/views/list/List.svelte +291 -0
- package/dist/components/crud/views/{List.svelte.d.ts → list/List.svelte.d.ts} +4 -14
- package/dist/components/crud/views/list/Modals.svelte +56 -0
- package/dist/components/crud/views/list/Modals.svelte.d.ts +36 -0
- package/dist/components/crud/views/list/Table.svelte +176 -0
- package/dist/components/crud/views/list/Table.svelte.d.ts +59 -0
- package/dist/components/crud/views/list/Toolbar.svelte +341 -0
- package/dist/components/crud/views/list/Toolbar.svelte.d.ts +46 -0
- package/dist/components/table/PaginatedTable.svelte +361 -16
- package/dist/components/table/PaginatedTable.svelte.d.ts +11 -2
- package/dist/components/table/TableBody.svelte +83 -3
- package/dist/components/table/TableBody.svelte.d.ts +14 -1
- package/dist/components/table/TableHeader.svelte +9 -4
- package/dist/components/table/TableHeader.svelte.d.ts +1 -0
- package/dist/components/table/sortable.d.ts +27 -0
- package/dist/components/table/sortable.js +55 -0
- package/dist/components/table/utils.d.ts +19 -1
- package/dist/components/table/utils.js +40 -0
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/es.js +2 -0
- package/dist/i18n/types.d.ts +2 -0
- package/dist/icons/defaults/Grip.svelte +7 -0
- package/dist/icons/defaults/Grip.svelte.d.ts +7 -0
- package/dist/icons/sets/bootstrap.js +2 -1
- package/dist/icons/sets/default.js +2 -0
- package/dist/icons/types.d.ts +3 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/types/crud.d.ts +70 -1
- package/dist/types/table.d.ts +56 -0
- package/package.json +3 -1
- package/dist/components/crud/views/List.svelte +0 -584
|
@@ -1,584 +0,0 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
-
import { SvelteSet } from 'svelte/reactivity';
|
|
3
|
-
import { invalidateAll } from '$app/navigation';
|
|
4
|
-
import Button from '../../form/Button.svelte';
|
|
5
|
-
import Header from '../../common/Header.svelte';
|
|
6
|
-
import Modal from '../../Modal.svelte';
|
|
7
|
-
import PaginatedTable from '../../table/PaginatedTable.svelte';
|
|
8
|
-
import SearchInput from '../SearchInput.svelte';
|
|
9
|
-
import { downloadCsv, downloadXlsx, type XlsxModule } from '../../table/export.js';
|
|
10
|
-
import { getIconSet } from '../../../icons/context.js';
|
|
11
|
-
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
12
|
-
import type {
|
|
13
|
-
ActionConfiguration,
|
|
14
|
-
ColumnDefinition,
|
|
15
|
-
CustomAction,
|
|
16
|
-
CustomBulkAction,
|
|
17
|
-
RowAction,
|
|
18
|
-
SearchConfiguration,
|
|
19
|
-
ViewBasedCustomBulkAction
|
|
20
|
-
} from '../../../types/crud.js';
|
|
21
|
-
import type {
|
|
22
|
-
FilterSnapshot,
|
|
23
|
-
ServerPagination,
|
|
24
|
-
SortDirection,
|
|
25
|
-
TableQuery
|
|
26
|
-
} from '../../../types/table.js';
|
|
27
|
-
import { getStrings } from '../../../i18n/context.js';
|
|
28
|
-
|
|
29
|
-
const strings = getStrings();
|
|
30
|
-
|
|
31
|
-
function isViewBasedBulkAction(
|
|
32
|
-
action: CustomBulkAction<T>
|
|
33
|
-
): action is ViewBasedCustomBulkAction<T> {
|
|
34
|
-
return action.kind === 'view';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
let {
|
|
38
|
-
data = [] as T[],
|
|
39
|
-
labelOne = '',
|
|
40
|
-
labelMany = '',
|
|
41
|
-
icon,
|
|
42
|
-
pageSize = 10,
|
|
43
|
-
idKey = '_id',
|
|
44
|
-
creation = {} as ActionConfiguration<T>,
|
|
45
|
-
update = {} as ActionConfiguration<T>,
|
|
46
|
-
read = {} as ActionConfiguration<T>,
|
|
47
|
-
deletion = {} as ActionConfiguration<T>,
|
|
48
|
-
actions = [] as CustomAction<T>[],
|
|
49
|
-
customBulkActions = [] as CustomBulkAction<T>[],
|
|
50
|
-
search = undefined as SearchConfiguration | undefined,
|
|
51
|
-
columns = [] as ColumnDefinition<T>[],
|
|
52
|
-
pagination = undefined as ServerPagination | undefined,
|
|
53
|
-
initialSort = undefined as { column: string; direction: SortDirection } | undefined,
|
|
54
|
-
initialFilters = undefined as Partial<FilterSnapshot> | undefined,
|
|
55
|
-
onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
|
|
56
|
-
enableExport = false,
|
|
57
|
-
onExport = undefined as ((query: TableQuery) => Promise<T[]>) | undefined,
|
|
58
|
-
xlsx = undefined as XlsxModule | undefined,
|
|
59
|
-
onCreate,
|
|
60
|
-
onEdit,
|
|
61
|
-
onView,
|
|
62
|
-
onAction,
|
|
63
|
-
onBulkAction
|
|
64
|
-
}: {
|
|
65
|
-
data?: T[];
|
|
66
|
-
labelOne?: string;
|
|
67
|
-
labelMany?: string;
|
|
68
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
-
icon?: any;
|
|
70
|
-
pageSize?: number;
|
|
71
|
-
idKey?: string;
|
|
72
|
-
creation?: ActionConfiguration<T>;
|
|
73
|
-
update?: ActionConfiguration<T>;
|
|
74
|
-
read?: ActionConfiguration<T>;
|
|
75
|
-
deletion?: ActionConfiguration<T>;
|
|
76
|
-
actions?: CustomAction<T>[];
|
|
77
|
-
customBulkActions?: CustomBulkAction<T>[];
|
|
78
|
-
search?: SearchConfiguration;
|
|
79
|
-
columns?: ColumnDefinition<T>[];
|
|
80
|
-
pagination?: ServerPagination;
|
|
81
|
-
initialSort?: { column: string; direction: SortDirection };
|
|
82
|
-
initialFilters?: Partial<FilterSnapshot>;
|
|
83
|
-
onPaginationChange?: (query: TableQuery) => void;
|
|
84
|
-
/** Shows an icon-only export button (CSV, and Excel if `xlsx` is provided). */
|
|
85
|
-
enableExport?: boolean;
|
|
86
|
-
/** Server-pagination mode only: fetch all rows matching the current query
|
|
87
|
-
* (unpaginated) for export. Without it, export falls back to the loaded page. */
|
|
88
|
-
onExport?: (query: TableQuery) => Promise<T[]>;
|
|
89
|
-
/** Resolved `xlsx` (SheetJS) module, e.g. `import * as xlsx from 'xlsx'`.
|
|
90
|
-
* Enables the "Export as Excel" option; omit to only offer CSV. */
|
|
91
|
-
xlsx?: XlsxModule;
|
|
92
|
-
onCreate?: () => void;
|
|
93
|
-
onEdit?: (item: T) => void;
|
|
94
|
-
onView?: (item: T) => void;
|
|
95
|
-
onAction?: (action: CustomAction<T>, item: T) => void;
|
|
96
|
-
onBulkAction?: (action: ViewBasedCustomBulkAction<T>, items: T[]) => void;
|
|
97
|
-
} = $props();
|
|
98
|
-
|
|
99
|
-
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
100
|
-
const entityIcon = $derived(icon ?? icons.folder);
|
|
101
|
-
|
|
102
|
-
const allowRead = $derived(read.enabled ?? true);
|
|
103
|
-
const allowUpdate = $derived(update.enabled ?? true);
|
|
104
|
-
const allowDelete = $derived(deletion.enabled ?? true);
|
|
105
|
-
const allowCreate = $derived(creation.enabled ?? true);
|
|
106
|
-
const deleteLabel = $derived(deletion.label ?? strings.delete);
|
|
107
|
-
const updateLabel = $derived(update.label ?? strings.edit);
|
|
108
|
-
const readLabel = $derived(read.label ?? strings.view);
|
|
109
|
-
const showRowActions = $derived(allowRead || allowUpdate || allowDelete || actions.length > 0);
|
|
110
|
-
const allowSelection = $derived(
|
|
111
|
-
allowDelete || customBulkActions.some((action) => !isViewBasedBulkAction(action))
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
let selected = new SvelteSet<number>();
|
|
115
|
-
let pendingDeletion = $state<T[] | null>(null);
|
|
116
|
-
let pendingBulkAction = $state<{ action: CustomBulkAction<T>; items: T[] } | null>(null);
|
|
117
|
-
const selectedItems = $derived([...selected].map((i) => data[i]));
|
|
118
|
-
|
|
119
|
-
let visibleRows = $state<T[]>([]);
|
|
120
|
-
let exportQuery = $state<TableQuery | undefined>(undefined);
|
|
121
|
-
let exportPopoverEl: HTMLElement | undefined = $state();
|
|
122
|
-
let actionsPopoverEl: HTMLElement | undefined = $state();
|
|
123
|
-
const exportId = $props.id();
|
|
124
|
-
const actionsMenuId = `${exportId}-actions`;
|
|
125
|
-
|
|
126
|
-
// Collapses export/bulk-actions/delete into a single "Acciones" dropdown
|
|
127
|
-
// once the toolbar no longer fits its available width (small monitor, a
|
|
128
|
-
// browser window not maximized, many customBulkActions, ...) — measured
|
|
129
|
-
// against an identical but off-flow, always-uncollapsed clone rather than
|
|
130
|
-
// the visible row itself, since the visible row's own width changes
|
|
131
|
-
// depending on whether it's currently collapsed.
|
|
132
|
-
let toolbarEl: HTMLElement | undefined = $state();
|
|
133
|
-
let toolbarMeasureEl: HTMLElement | undefined = $state();
|
|
134
|
-
let toolbarCollapsed = $state(false);
|
|
135
|
-
|
|
136
|
-
$effect(() => {
|
|
137
|
-
if (!toolbarEl || !toolbarMeasureEl) return;
|
|
138
|
-
const el = toolbarEl;
|
|
139
|
-
const measureEl = toolbarMeasureEl;
|
|
140
|
-
const check = () => {
|
|
141
|
-
toolbarCollapsed = measureEl.scrollWidth > el.clientWidth + 1;
|
|
142
|
-
};
|
|
143
|
-
const observer = new ResizeObserver(check);
|
|
144
|
-
observer.observe(el);
|
|
145
|
-
observer.observe(measureEl);
|
|
146
|
-
check();
|
|
147
|
-
return () => observer.disconnect();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
async function resolveExportRows(): Promise<T[]> {
|
|
151
|
-
if (!pagination) return visibleRows;
|
|
152
|
-
if (!onExport || !exportQuery) return data;
|
|
153
|
-
return onExport(exportQuery);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
async function handleExport(format: 'csv' | 'xlsx') {
|
|
157
|
-
exportPopoverEl?.hidePopover();
|
|
158
|
-
const rows = await resolveExportRows();
|
|
159
|
-
const filename = `${labelMany || 'export'}-${new Date().toISOString().slice(0, 10)}`;
|
|
160
|
-
if (format === 'csv') downloadCsv(rows, columns, filename);
|
|
161
|
-
else if (xlsx) downloadXlsx(rows, columns, filename, xlsx);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// `selected` holds indices into `data`; if `data` is swapped for a different
|
|
165
|
-
// slice (page/sort/filter change in server mode) stale indices could point
|
|
166
|
-
// at unrelated rows, so clear on any data reference change.
|
|
167
|
-
let lastData: T[] | undefined;
|
|
168
|
-
$effect(() => {
|
|
169
|
-
if (data !== lastData) {
|
|
170
|
-
selected.clear();
|
|
171
|
-
lastData = data;
|
|
172
|
-
}
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
async function runEndpointAction(endpoint: string, items: T[]) {
|
|
176
|
-
await Promise.all(
|
|
177
|
-
items.map((item) => {
|
|
178
|
-
const fd = new FormData();
|
|
179
|
-
fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
|
|
180
|
-
return fetch(endpoint, { method: 'POST', body: fd });
|
|
181
|
-
})
|
|
182
|
-
);
|
|
183
|
-
await invalidateAll();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function runDeletion(items: T[]) {
|
|
187
|
-
if (deletion.endpoint) {
|
|
188
|
-
await runEndpointAction(deletion.endpoint, items);
|
|
189
|
-
} else {
|
|
190
|
-
await deletion.callback?.(items);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function requestDeletion(items: T[]) {
|
|
195
|
-
if (deletion.confirm) {
|
|
196
|
-
pendingDeletion = items;
|
|
197
|
-
} else {
|
|
198
|
-
runDeletion(items);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function handleCreate() {
|
|
203
|
-
onCreate?.();
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function handleDelete() {
|
|
207
|
-
const items = [...selected].map((i) => data[i]);
|
|
208
|
-
selected.clear();
|
|
209
|
-
requestDeletion(items);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function confirmDeletion() {
|
|
213
|
-
if (pendingDeletion) {
|
|
214
|
-
await runDeletion(pendingDeletion);
|
|
215
|
-
pendingDeletion = null;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function runBulkAction(action: CustomBulkAction<T>, items: T[]) {
|
|
220
|
-
if (isViewBasedBulkAction(action)) return;
|
|
221
|
-
await runEndpointAction(action.endpoint, items);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function requestBulkAction(action: CustomBulkAction<T>, items: T[]) {
|
|
225
|
-
if (!isViewBasedBulkAction(action) && action.confirm) {
|
|
226
|
-
pendingBulkAction = { action, items };
|
|
227
|
-
} else {
|
|
228
|
-
runBulkAction(action, items);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function handleBulkAction(action: CustomBulkAction<T>) {
|
|
233
|
-
const items = selectedItems;
|
|
234
|
-
if (isViewBasedBulkAction(action)) {
|
|
235
|
-
onBulkAction?.(action, items);
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
selected.clear();
|
|
239
|
-
requestBulkAction(action, items);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
async function confirmBulkAction() {
|
|
243
|
-
if (pendingBulkAction) {
|
|
244
|
-
await runBulkAction(pendingBulkAction.action, pendingBulkAction.items);
|
|
245
|
-
pendingBulkAction = null;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const rowActions = $derived<RowAction<T>[]>([
|
|
250
|
-
...actions.map((action) => ({
|
|
251
|
-
label: action.label,
|
|
252
|
-
icon: action.icon,
|
|
253
|
-
toggle: action.toggle,
|
|
254
|
-
class: action.class,
|
|
255
|
-
condition: action.condition,
|
|
256
|
-
run: (item: T) => onAction?.(action, item)
|
|
257
|
-
})),
|
|
258
|
-
...(allowRead
|
|
259
|
-
? [{ label: readLabel, icon: icons.view, run: (item: T) => onView?.(item) }]
|
|
260
|
-
: []),
|
|
261
|
-
...(allowUpdate
|
|
262
|
-
? [{ label: updateLabel, icon: icons.edit, run: (item: T) => onEdit?.(item) }]
|
|
263
|
-
: []),
|
|
264
|
-
...(allowDelete
|
|
265
|
-
? [
|
|
266
|
-
{
|
|
267
|
-
label: deleteLabel,
|
|
268
|
-
icon: icons.delete,
|
|
269
|
-
class: 'text-error',
|
|
270
|
-
run: (item: T) => requestDeletion([item])
|
|
271
|
-
}
|
|
272
|
-
]
|
|
273
|
-
: [])
|
|
274
|
-
]);
|
|
275
|
-
</script>
|
|
276
|
-
|
|
277
|
-
{#snippet toolbarButtons(measuring: boolean)}
|
|
278
|
-
{#if search}
|
|
279
|
-
<SearchInput config={search} />
|
|
280
|
-
{/if}
|
|
281
|
-
|
|
282
|
-
{#if enableExport}
|
|
283
|
-
{@const ExportIcon = icons.download}
|
|
284
|
-
<Button
|
|
285
|
-
variant="ghost"
|
|
286
|
-
class="btn-square"
|
|
287
|
-
popovertarget={measuring ? undefined : `export-menu-${exportId}`}
|
|
288
|
-
style={measuring ? undefined : `anchor-name:--export-anchor-${exportId}`}
|
|
289
|
-
aria-label={strings.export}
|
|
290
|
-
title={strings.export}
|
|
291
|
-
>
|
|
292
|
-
<ExportIcon class="size-4" />
|
|
293
|
-
</Button>
|
|
294
|
-
{#if !measuring}
|
|
295
|
-
<div
|
|
296
|
-
popover="auto"
|
|
297
|
-
id="export-menu-{exportId}"
|
|
298
|
-
style="position-anchor:--export-anchor-{exportId}"
|
|
299
|
-
class="dropdown dropdown-end w-40 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
|
|
300
|
-
bind:this={exportPopoverEl}
|
|
301
|
-
>
|
|
302
|
-
<Button
|
|
303
|
-
variant="ghost"
|
|
304
|
-
class="btn-sm w-full justify-start"
|
|
305
|
-
onclick={() => handleExport('csv')}
|
|
306
|
-
>
|
|
307
|
-
{strings.exportCsv}
|
|
308
|
-
</Button>
|
|
309
|
-
{#if xlsx}
|
|
310
|
-
<Button
|
|
311
|
-
variant="ghost"
|
|
312
|
-
class="btn-sm w-full justify-start"
|
|
313
|
-
onclick={() => handleExport('xlsx')}
|
|
314
|
-
>
|
|
315
|
-
{strings.exportExcel}
|
|
316
|
-
</Button>
|
|
317
|
-
{/if}
|
|
318
|
-
</div>
|
|
319
|
-
{/if}
|
|
320
|
-
{/if}
|
|
321
|
-
|
|
322
|
-
{#each customBulkActions as bulkAction, i (i)}
|
|
323
|
-
{#if bulkAction.condition?.(selectedItems) ?? true}
|
|
324
|
-
{@const BulkIcon = bulkAction.icon}
|
|
325
|
-
{@const isView = isViewBasedBulkAction(bulkAction)}
|
|
326
|
-
<Button
|
|
327
|
-
variant={bulkAction.variant ?? 'ghost'}
|
|
328
|
-
class="btn-outline"
|
|
329
|
-
disabled={!isView && selected.size === 0}
|
|
330
|
-
title={bulkAction.tooltip ?? bulkAction.label}
|
|
331
|
-
aria-label={bulkAction.label ? undefined : bulkAction.tooltip}
|
|
332
|
-
onclick={measuring ? undefined : () => handleBulkAction(bulkAction)}
|
|
333
|
-
>
|
|
334
|
-
<BulkIcon class="size-4" />
|
|
335
|
-
{#if bulkAction.label}
|
|
336
|
-
{bulkAction.label}{#if !isView}
|
|
337
|
-
({selected.size})
|
|
338
|
-
{/if}
|
|
339
|
-
{/if}
|
|
340
|
-
</Button>
|
|
341
|
-
{/if}
|
|
342
|
-
{/each}
|
|
343
|
-
|
|
344
|
-
{#if allowDelete}
|
|
345
|
-
{@const DeleteIcon = icons.delete}
|
|
346
|
-
<Button
|
|
347
|
-
variant="error"
|
|
348
|
-
class="btn-outline"
|
|
349
|
-
disabled={selected.size === 0}
|
|
350
|
-
onclick={measuring ? undefined : handleDelete}
|
|
351
|
-
>
|
|
352
|
-
<DeleteIcon class="size-4" />
|
|
353
|
-
{deleteLabel} ({selected.size})
|
|
354
|
-
</Button>
|
|
355
|
-
{/if}
|
|
356
|
-
|
|
357
|
-
{#if allowCreate}
|
|
358
|
-
{@const CreateIcon = icons.create}
|
|
359
|
-
<Button variant="primary" onclick={measuring ? undefined : handleCreate}>
|
|
360
|
-
<CreateIcon class="size-5" />
|
|
361
|
-
{#if creation.label}
|
|
362
|
-
<span>{creation.label}</span>
|
|
363
|
-
{:else}
|
|
364
|
-
<span>{strings.create}<span class="hidden sm:inline"> {labelOne}</span></span>
|
|
365
|
-
{/if}
|
|
366
|
-
</Button>
|
|
367
|
-
{/if}
|
|
368
|
-
{/snippet}
|
|
369
|
-
|
|
370
|
-
{#snippet actionsCell(item: T)}
|
|
371
|
-
<div class="flex justify-end items-center gap-1">
|
|
372
|
-
{#each rowActions as action (action.label)}
|
|
373
|
-
{#if action.condition?.(item) ?? true}
|
|
374
|
-
{@const resolvedClass =
|
|
375
|
-
typeof action.class === 'function' ? action.class(item) : action.class}
|
|
376
|
-
{#if action.toggle}
|
|
377
|
-
<input
|
|
378
|
-
type="checkbox"
|
|
379
|
-
class={['toggle toggle-sm', resolvedClass]}
|
|
380
|
-
title={action.label}
|
|
381
|
-
aria-label={action.label}
|
|
382
|
-
checked={action.toggle(item)}
|
|
383
|
-
onclick={(e) => {
|
|
384
|
-
e.preventDefault();
|
|
385
|
-
e.stopPropagation();
|
|
386
|
-
action.run(item);
|
|
387
|
-
}}
|
|
388
|
-
/>
|
|
389
|
-
{:else}
|
|
390
|
-
{@const Icon = action.icon}
|
|
391
|
-
<Button
|
|
392
|
-
variant="ghost"
|
|
393
|
-
class={['btn-xs', resolvedClass]}
|
|
394
|
-
title={action.label}
|
|
395
|
-
aria-label={action.label}
|
|
396
|
-
onclick={(e) => {
|
|
397
|
-
e.stopPropagation();
|
|
398
|
-
action.run(item);
|
|
399
|
-
}}
|
|
400
|
-
>
|
|
401
|
-
<Icon class="size-4" />
|
|
402
|
-
</Button>
|
|
403
|
-
{/if}
|
|
404
|
-
{/if}
|
|
405
|
-
{/each}
|
|
406
|
-
</div>
|
|
407
|
-
{/snippet}
|
|
408
|
-
|
|
409
|
-
<div class="flex flex-col gap-6">
|
|
410
|
-
<Header
|
|
411
|
-
title={labelMany}
|
|
412
|
-
breadcrumbs={[{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true }]}
|
|
413
|
-
>
|
|
414
|
-
{#snippet buttons()}
|
|
415
|
-
<div bind:this={toolbarEl} class="flex min-w-0 flex-wrap items-center gap-2 overflow-hidden">
|
|
416
|
-
{#if toolbarCollapsed}
|
|
417
|
-
{#if search}
|
|
418
|
-
<SearchInput config={search} />
|
|
419
|
-
{/if}
|
|
420
|
-
|
|
421
|
-
<Button
|
|
422
|
-
variant="ghost"
|
|
423
|
-
class="btn-square"
|
|
424
|
-
popovertarget="actions-menu-{actionsMenuId}"
|
|
425
|
-
style="anchor-name:--actions-anchor-{actionsMenuId}"
|
|
426
|
-
aria-label={strings.actions}
|
|
427
|
-
title={strings.actions}
|
|
428
|
-
>
|
|
429
|
-
<span class="text-lg leading-none" aria-hidden="true">⋯</span>
|
|
430
|
-
</Button>
|
|
431
|
-
<div
|
|
432
|
-
popover="auto"
|
|
433
|
-
id="actions-menu-{actionsMenuId}"
|
|
434
|
-
style="position-anchor:--actions-anchor-{actionsMenuId}"
|
|
435
|
-
class="dropdown dropdown-end w-56 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
|
|
436
|
-
bind:this={actionsPopoverEl}
|
|
437
|
-
>
|
|
438
|
-
{#if allowCreate}
|
|
439
|
-
{@const CreateIcon = icons.create}
|
|
440
|
-
<Button
|
|
441
|
-
variant="ghost"
|
|
442
|
-
class="btn-sm w-full justify-start"
|
|
443
|
-
onclick={() => {
|
|
444
|
-
actionsPopoverEl?.hidePopover();
|
|
445
|
-
handleCreate();
|
|
446
|
-
}}
|
|
447
|
-
>
|
|
448
|
-
<CreateIcon class="size-4" />
|
|
449
|
-
{creation.label || `${strings.create} ${labelOne}`}
|
|
450
|
-
</Button>
|
|
451
|
-
{/if}
|
|
452
|
-
|
|
453
|
-
{#if allowDelete}
|
|
454
|
-
<Button
|
|
455
|
-
variant="ghost"
|
|
456
|
-
class="btn-sm w-full justify-start text-error"
|
|
457
|
-
disabled={selected.size === 0}
|
|
458
|
-
onclick={() => {
|
|
459
|
-
actionsPopoverEl?.hidePopover();
|
|
460
|
-
handleDelete();
|
|
461
|
-
}}
|
|
462
|
-
>
|
|
463
|
-
{deleteLabel} ({selected.size})
|
|
464
|
-
</Button>
|
|
465
|
-
{/if}
|
|
466
|
-
|
|
467
|
-
{#if enableExport}
|
|
468
|
-
<Button
|
|
469
|
-
variant="ghost"
|
|
470
|
-
class="btn-sm w-full justify-start"
|
|
471
|
-
onclick={() => {
|
|
472
|
-
actionsPopoverEl?.hidePopover();
|
|
473
|
-
handleExport('csv');
|
|
474
|
-
}}
|
|
475
|
-
>
|
|
476
|
-
{strings.exportCsv}
|
|
477
|
-
</Button>
|
|
478
|
-
{#if xlsx}
|
|
479
|
-
<Button
|
|
480
|
-
variant="ghost"
|
|
481
|
-
class="btn-sm w-full justify-start"
|
|
482
|
-
onclick={() => {
|
|
483
|
-
actionsPopoverEl?.hidePopover();
|
|
484
|
-
handleExport('xlsx');
|
|
485
|
-
}}
|
|
486
|
-
>
|
|
487
|
-
{strings.exportExcel}
|
|
488
|
-
</Button>
|
|
489
|
-
{/if}
|
|
490
|
-
{/if}
|
|
491
|
-
|
|
492
|
-
{#each customBulkActions as bulkAction, i (i)}
|
|
493
|
-
{#if bulkAction.condition?.(selectedItems) ?? true}
|
|
494
|
-
{@const BulkIcon = bulkAction.icon}
|
|
495
|
-
{@const isView = isViewBasedBulkAction(bulkAction)}
|
|
496
|
-
<Button
|
|
497
|
-
variant="ghost"
|
|
498
|
-
class="btn-sm w-full justify-start"
|
|
499
|
-
disabled={!isView && selected.size === 0}
|
|
500
|
-
title={bulkAction.tooltip}
|
|
501
|
-
onclick={() => {
|
|
502
|
-
actionsPopoverEl?.hidePopover();
|
|
503
|
-
handleBulkAction(bulkAction);
|
|
504
|
-
}}
|
|
505
|
-
>
|
|
506
|
-
<BulkIcon class="size-4" />
|
|
507
|
-
{bulkAction.label ?? bulkAction.tooltip}{#if !isView}
|
|
508
|
-
({selected.size})
|
|
509
|
-
{/if}
|
|
510
|
-
</Button>
|
|
511
|
-
{/if}
|
|
512
|
-
{/each}
|
|
513
|
-
</div>
|
|
514
|
-
{:else}
|
|
515
|
-
{@render toolbarButtons(false)}
|
|
516
|
-
{/if}
|
|
517
|
-
</div>
|
|
518
|
-
|
|
519
|
-
<div
|
|
520
|
-
bind:this={toolbarMeasureEl}
|
|
521
|
-
class="pointer-events-none invisible fixed flex items-center gap-2 whitespace-nowrap"
|
|
522
|
-
style="left:-9999px; top:-9999px; width:max-content;"
|
|
523
|
-
aria-hidden="true"
|
|
524
|
-
>
|
|
525
|
-
{@render toolbarButtons(true)}
|
|
526
|
-
</div>
|
|
527
|
-
{/snippet}
|
|
528
|
-
</Header>
|
|
529
|
-
|
|
530
|
-
<div class="table-wrapper">
|
|
531
|
-
<PaginatedTable
|
|
532
|
-
{data}
|
|
533
|
-
{columns}
|
|
534
|
-
{pageSize}
|
|
535
|
-
selectable={allowSelection}
|
|
536
|
-
{selected}
|
|
537
|
-
rowActions={showRowActions ? actionsCell : undefined}
|
|
538
|
-
{pagination}
|
|
539
|
-
{initialSort}
|
|
540
|
-
{initialFilters}
|
|
541
|
-
{onPaginationChange}
|
|
542
|
-
bind:visibleRows
|
|
543
|
-
bind:query={exportQuery}
|
|
544
|
-
/>
|
|
545
|
-
</div>
|
|
546
|
-
</div>
|
|
547
|
-
|
|
548
|
-
{#if pendingDeletion !== null}
|
|
549
|
-
<Modal title={deleteLabel} onClose={() => (pendingDeletion = null)}>
|
|
550
|
-
<p>{strings.deleteConfirm(pendingDeletion.length, deleteLabel)}</p>
|
|
551
|
-
<div class="flex justify-end gap-2 mt-4">
|
|
552
|
-
<Button variant="ghost" onclick={() => (pendingDeletion = null)}>
|
|
553
|
-
{strings.cancel}
|
|
554
|
-
</Button>
|
|
555
|
-
<Button variant="error" onclick={confirmDeletion}>
|
|
556
|
-
{strings.confirm}
|
|
557
|
-
</Button>
|
|
558
|
-
</div>
|
|
559
|
-
</Modal>
|
|
560
|
-
{/if}
|
|
561
|
-
|
|
562
|
-
{#if pendingBulkAction !== null}
|
|
563
|
-
{@const pendingActionLabel =
|
|
564
|
-
pendingBulkAction.action.label ?? pendingBulkAction.action.tooltip ?? strings.actions}
|
|
565
|
-
<Modal title={pendingActionLabel} onClose={() => (pendingBulkAction = null)}>
|
|
566
|
-
<p>{strings.deleteConfirm(pendingBulkAction.items.length, pendingActionLabel)}</p>
|
|
567
|
-
<div class="flex justify-end gap-2 mt-4">
|
|
568
|
-
<Button variant="ghost" onclick={() => (pendingBulkAction = null)}>
|
|
569
|
-
{strings.cancel}
|
|
570
|
-
</Button>
|
|
571
|
-
<Button variant={pendingBulkAction.action.variant ?? 'primary'} onclick={confirmBulkAction}>
|
|
572
|
-
{strings.confirm}
|
|
573
|
-
</Button>
|
|
574
|
-
</div>
|
|
575
|
-
</Modal>
|
|
576
|
-
{/if}
|
|
577
|
-
|
|
578
|
-
<style>
|
|
579
|
-
.table-wrapper {
|
|
580
|
-
width: 100%;
|
|
581
|
-
max-width: var(--runeforge-crud-max-width);
|
|
582
|
-
margin-inline: auto;
|
|
583
|
-
}
|
|
584
|
-
</style>
|