runeforge 0.0.55 → 0.0.57
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/LICENSE +21 -21
- package/README.md +1355 -1342
- package/dist/components/Avatar.svelte +31 -31
- package/dist/components/IconRenderer.svelte +73 -22
- package/dist/components/IconRenderer.svelte.d.ts +2 -0
- package/dist/components/Modal.svelte +75 -75
- package/dist/components/common/Header.svelte +40 -40
- package/dist/components/crud/EmbeddedField.svelte +175 -175
- package/dist/components/crud/Field.svelte +469 -376
- package/dist/components/crud/GenericCRUD.svelte +426 -426
- package/dist/components/crud/SearchInput.svelte +53 -53
- package/dist/components/crud/columns/Avatar.svelte +15 -15
- package/dist/components/crud/columns/Icon.svelte +8 -8
- package/dist/components/crud/views/Create.svelte +232 -232
- package/dist/components/crud/views/Read.svelte +124 -124
- package/dist/components/crud/views/Update.svelte +208 -208
- package/dist/components/crud/views/list/List.svelte +291 -291
- package/dist/components/crud/views/list/Modals.svelte +56 -56
- package/dist/components/crud/views/list/Table.svelte +176 -176
- package/dist/components/crud/views/list/Toolbar.svelte +341 -341
- package/dist/components/form/Button.svelte +27 -27
- package/dist/components/form/Label.svelte +37 -37
- package/dist/components/form/MultiSelect.svelte +248 -248
- package/dist/components/form/PasswordInput.svelte +68 -68
- package/dist/components/form/Required.svelte +1 -1
- package/dist/components/form/Select.svelte +209 -209
- package/dist/components/form/Tree.svelte +62 -62
- package/dist/components/form/TreeNode.svelte +66 -66
- package/dist/components/navigation/Breadcrumbs.svelte +111 -111
- package/dist/components/table/ColumnFilter.svelte +168 -168
- package/dist/components/table/PaginatedTable.svelte +536 -536
- package/dist/components/table/Paginator.svelte +113 -113
- package/dist/components/table/SortHeader.svelte +43 -43
- package/dist/components/table/TableBody.svelte +150 -150
- package/dist/components/table/TableHeader.svelte +88 -88
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/es.js +1 -0
- package/dist/i18n/types.d.ts +1 -0
- package/dist/icons/context.d.ts +5 -0
- package/dist/icons/context.js +10 -0
- package/dist/icons/defaults/Clear.svelte +6 -6
- package/dist/icons/defaults/Create.svelte +6 -6
- package/dist/icons/defaults/Delete.svelte +6 -6
- package/dist/icons/defaults/Download.svelte +7 -7
- package/dist/icons/defaults/Edit.svelte +7 -7
- package/dist/icons/defaults/Filter.svelte +6 -6
- package/dist/icons/defaults/FilterActive.svelte +6 -6
- package/dist/icons/defaults/Folder.svelte +6 -6
- package/dist/icons/defaults/Grip.svelte +7 -7
- package/dist/icons/defaults/Home.svelte +6 -6
- package/dist/icons/defaults/PasswordHide.svelte +9 -9
- package/dist/icons/defaults/PasswordShow.svelte +7 -7
- package/dist/icons/defaults/SortAsc.svelte +6 -6
- package/dist/icons/defaults/SortDesc.svelte +6 -6
- package/dist/icons/defaults/SortNone.svelte +6 -6
- package/dist/icons/defaults/View.svelte +7 -7
- package/dist/icons/sets/bootstrap.js +1 -4
- package/dist/icons/types.d.ts +0 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -1,341 +1,341 @@
|
|
|
1
|
-
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
-
import Button from '../../../form/Button.svelte';
|
|
3
|
-
import Header from '../../../common/Header.svelte';
|
|
4
|
-
import { downloadCsv, downloadXlsx } from '../../../table/export.js';
|
|
5
|
-
import { getIconSet } from '../../../../icons/context.js';
|
|
6
|
-
import { defaultIconSet } from '../../../../icons/sets/default.js';
|
|
7
|
-
import type {
|
|
8
|
-
ActionConfiguration,
|
|
9
|
-
ColumnDefinition,
|
|
10
|
-
CustomBulkAction,
|
|
11
|
-
ExportConfiguration,
|
|
12
|
-
SearchConfiguration,
|
|
13
|
-
ViewBasedCustomBulkAction
|
|
14
|
-
} from '../../../../types/crud.js';
|
|
15
|
-
import type { ServerPagination, TableQuery } from '../../../../types/table.js';
|
|
16
|
-
import { getStrings } from '../../../../i18n/context.js';
|
|
17
|
-
import SearchInput from '../../SearchInput.svelte';
|
|
18
|
-
|
|
19
|
-
const strings = getStrings();
|
|
20
|
-
|
|
21
|
-
function isViewBasedBulkAction<U extends object>(
|
|
22
|
-
action: CustomBulkAction<U>
|
|
23
|
-
): action is ViewBasedCustomBulkAction<U> {
|
|
24
|
-
return action.kind === 'view';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let {
|
|
28
|
-
labelOne = '',
|
|
29
|
-
labelMany = '',
|
|
30
|
-
icon,
|
|
31
|
-
creation = {} as ActionConfiguration<T>,
|
|
32
|
-
allowCreate,
|
|
33
|
-
allowDelete,
|
|
34
|
-
deleteLabel,
|
|
35
|
-
search = undefined as SearchConfiguration | undefined,
|
|
36
|
-
exportConfig = undefined as ExportConfiguration<T> | undefined,
|
|
37
|
-
bulkActions = [] as CustomBulkAction<T>[],
|
|
38
|
-
columns = [] as ColumnDefinition<T>[],
|
|
39
|
-
pagination = undefined as ServerPagination | undefined,
|
|
40
|
-
visibleRows = [] as T[],
|
|
41
|
-
exportQuery = undefined as TableQuery | undefined,
|
|
42
|
-
selectedCount = 0,
|
|
43
|
-
selectedItems = [] as T[],
|
|
44
|
-
onCreate,
|
|
45
|
-
onDeleteSelected,
|
|
46
|
-
onBulkAction
|
|
47
|
-
}: {
|
|
48
|
-
labelOne?: string;
|
|
49
|
-
labelMany?: string;
|
|
50
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
-
icon?: any;
|
|
52
|
-
creation?: ActionConfiguration<T>;
|
|
53
|
-
allowCreate: boolean;
|
|
54
|
-
allowDelete: boolean;
|
|
55
|
-
deleteLabel: string;
|
|
56
|
-
search?: SearchConfiguration;
|
|
57
|
-
exportConfig?: ExportConfiguration<T>;
|
|
58
|
-
bulkActions?: CustomBulkAction<T>[];
|
|
59
|
-
columns?: ColumnDefinition<T>[];
|
|
60
|
-
pagination?: ServerPagination;
|
|
61
|
-
visibleRows?: T[];
|
|
62
|
-
exportQuery?: TableQuery;
|
|
63
|
-
selectedCount?: number;
|
|
64
|
-
selectedItems?: T[];
|
|
65
|
-
onCreate?: () => void;
|
|
66
|
-
onDeleteSelected?: () => void;
|
|
67
|
-
onBulkAction?: (action: CustomBulkAction<T>) => void;
|
|
68
|
-
} = $props();
|
|
69
|
-
|
|
70
|
-
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
71
|
-
const entityIcon = $derived(icon ?? icons.folder);
|
|
72
|
-
const enableExport = $derived(!!exportConfig);
|
|
73
|
-
|
|
74
|
-
let exportPopoverEl: HTMLElement | undefined = $state();
|
|
75
|
-
let actionsPopoverEl: HTMLElement | undefined = $state();
|
|
76
|
-
const exportId = $props.id();
|
|
77
|
-
const actionsMenuId = `${exportId}-actions`;
|
|
78
|
-
|
|
79
|
-
// Collapses export/bulk-actions/delete into a single "Acciones" dropdown
|
|
80
|
-
// once the toolbar no longer fits its available width (small monitor, a
|
|
81
|
-
// browser window not maximized, many bulkActions, ...) — measured against
|
|
82
|
-
// an identical but off-flow, always-uncollapsed clone rather than the
|
|
83
|
-
// visible row itself, since the visible row's own width changes depending
|
|
84
|
-
// on whether it's currently collapsed.
|
|
85
|
-
let toolbarEl: HTMLElement | undefined = $state();
|
|
86
|
-
let toolbarMeasureEl: HTMLElement | undefined = $state();
|
|
87
|
-
let toolbarCollapsed = $state(false);
|
|
88
|
-
|
|
89
|
-
$effect(() => {
|
|
90
|
-
if (!toolbarEl || !toolbarMeasureEl) return;
|
|
91
|
-
const el = toolbarEl;
|
|
92
|
-
const measureEl = toolbarMeasureEl;
|
|
93
|
-
const check = () => {
|
|
94
|
-
toolbarCollapsed = measureEl.scrollWidth > el.clientWidth + 1;
|
|
95
|
-
};
|
|
96
|
-
const observer = new ResizeObserver(check);
|
|
97
|
-
observer.observe(el);
|
|
98
|
-
observer.observe(measureEl);
|
|
99
|
-
check();
|
|
100
|
-
return () => observer.disconnect();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
async function resolveExportRows(): Promise<T[]> {
|
|
104
|
-
if (!pagination) return visibleRows;
|
|
105
|
-
if (!exportConfig?.callback || !exportQuery) return visibleRows;
|
|
106
|
-
return exportConfig.callback(exportQuery);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async function handleExport(format: 'csv' | 'xlsx') {
|
|
110
|
-
exportPopoverEl?.hidePopover();
|
|
111
|
-
const rows = await resolveExportRows();
|
|
112
|
-
const filename = `${labelMany || 'export'}-${new Date().toISOString().slice(0, 10)}`;
|
|
113
|
-
if (format === 'csv') downloadCsv(rows, columns, filename);
|
|
114
|
-
else if (exportConfig?.xlsx) downloadXlsx(rows, columns, filename, exportConfig.xlsx);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function handleCreate() {
|
|
118
|
-
onCreate?.();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function handleDelete() {
|
|
122
|
-
onDeleteSelected?.();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function handleBulkAction(action: CustomBulkAction<T>) {
|
|
126
|
-
onBulkAction?.(action);
|
|
127
|
-
}
|
|
128
|
-
</script>
|
|
129
|
-
|
|
130
|
-
{#snippet toolbarButtons(measuring: boolean)}
|
|
131
|
-
{#if search}
|
|
132
|
-
<SearchInput config={search} />
|
|
133
|
-
{/if}
|
|
134
|
-
|
|
135
|
-
{#if enableExport}
|
|
136
|
-
{@const ExportIcon = icons.download}
|
|
137
|
-
<Button
|
|
138
|
-
variant="ghost"
|
|
139
|
-
class="btn-square"
|
|
140
|
-
popovertarget={measuring ? undefined : `export-menu-${exportId}`}
|
|
141
|
-
style={measuring ? undefined : `anchor-name:--export-anchor-${exportId}`}
|
|
142
|
-
aria-label={strings.export}
|
|
143
|
-
title={strings.export}
|
|
144
|
-
>
|
|
145
|
-
<ExportIcon class="size-4" />
|
|
146
|
-
</Button>
|
|
147
|
-
{#if !measuring}
|
|
148
|
-
<div
|
|
149
|
-
popover="auto"
|
|
150
|
-
id="export-menu-{exportId}"
|
|
151
|
-
style="position-anchor:--export-anchor-{exportId}"
|
|
152
|
-
class="dropdown dropdown-end w-40 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
|
|
153
|
-
bind:this={exportPopoverEl}
|
|
154
|
-
>
|
|
155
|
-
<Button
|
|
156
|
-
variant="ghost"
|
|
157
|
-
class="btn-sm w-full justify-start"
|
|
158
|
-
onclick={() => handleExport('csv')}
|
|
159
|
-
>
|
|
160
|
-
{strings.exportCsv}
|
|
161
|
-
</Button>
|
|
162
|
-
{#if exportConfig?.xlsx}
|
|
163
|
-
<Button
|
|
164
|
-
variant="ghost"
|
|
165
|
-
class="btn-sm w-full justify-start"
|
|
166
|
-
onclick={() => handleExport('xlsx')}
|
|
167
|
-
>
|
|
168
|
-
{strings.exportExcel}
|
|
169
|
-
</Button>
|
|
170
|
-
{/if}
|
|
171
|
-
</div>
|
|
172
|
-
{/if}
|
|
173
|
-
{/if}
|
|
174
|
-
|
|
175
|
-
{#each bulkActions as bulkAction, i (i)}
|
|
176
|
-
{#if bulkAction.condition?.(selectedItems) ?? true}
|
|
177
|
-
{@const BulkIcon = bulkAction.icon}
|
|
178
|
-
{@const isView = isViewBasedBulkAction(bulkAction)}
|
|
179
|
-
<Button
|
|
180
|
-
variant={bulkAction.variant ?? 'ghost'}
|
|
181
|
-
class="btn-outline"
|
|
182
|
-
disabled={!isView && selectedCount === 0}
|
|
183
|
-
title={bulkAction.tooltip ?? bulkAction.label}
|
|
184
|
-
aria-label={bulkAction.label ? undefined : bulkAction.tooltip}
|
|
185
|
-
onclick={measuring ? undefined : () => handleBulkAction(bulkAction)}
|
|
186
|
-
>
|
|
187
|
-
<BulkIcon class="size-4" />
|
|
188
|
-
{#if bulkAction.label}
|
|
189
|
-
{bulkAction.label}{#if !isView}
|
|
190
|
-
({selectedCount})
|
|
191
|
-
{/if}
|
|
192
|
-
{/if}
|
|
193
|
-
</Button>
|
|
194
|
-
{/if}
|
|
195
|
-
{/each}
|
|
196
|
-
|
|
197
|
-
{#if allowDelete}
|
|
198
|
-
{@const DeleteIcon = icons.delete}
|
|
199
|
-
<Button
|
|
200
|
-
variant="error"
|
|
201
|
-
class="btn-outline"
|
|
202
|
-
disabled={selectedCount === 0}
|
|
203
|
-
onclick={measuring ? undefined : handleDelete}
|
|
204
|
-
>
|
|
205
|
-
<DeleteIcon class="size-4" />
|
|
206
|
-
{deleteLabel} ({selectedCount})
|
|
207
|
-
</Button>
|
|
208
|
-
{/if}
|
|
209
|
-
|
|
210
|
-
{#if allowCreate}
|
|
211
|
-
{@const CreateIcon = icons.create}
|
|
212
|
-
<Button variant="primary" onclick={measuring ? undefined : handleCreate}>
|
|
213
|
-
<CreateIcon class="size-5" />
|
|
214
|
-
{#if creation.label}
|
|
215
|
-
<span>{creation.label}</span>
|
|
216
|
-
{:else}
|
|
217
|
-
<span>{strings.create}<span class="hidden sm:inline"> {labelOne}</span></span>
|
|
218
|
-
{/if}
|
|
219
|
-
</Button>
|
|
220
|
-
{/if}
|
|
221
|
-
{/snippet}
|
|
222
|
-
|
|
223
|
-
<Header
|
|
224
|
-
title={labelMany}
|
|
225
|
-
breadcrumbs={[{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true }]}
|
|
226
|
-
>
|
|
227
|
-
{#snippet buttons()}
|
|
228
|
-
<div bind:this={toolbarEl} class="flex min-w-0 flex-wrap items-center gap-2 overflow-hidden">
|
|
229
|
-
{#if toolbarCollapsed}
|
|
230
|
-
{#if search}
|
|
231
|
-
<SearchInput config={search} />
|
|
232
|
-
{/if}
|
|
233
|
-
|
|
234
|
-
<Button
|
|
235
|
-
variant="ghost"
|
|
236
|
-
class="btn-square"
|
|
237
|
-
popovertarget="actions-menu-{actionsMenuId}"
|
|
238
|
-
style="anchor-name:--actions-anchor-{actionsMenuId}"
|
|
239
|
-
aria-label={strings.actions}
|
|
240
|
-
title={strings.actions}
|
|
241
|
-
>
|
|
242
|
-
<span class="text-lg leading-none" aria-hidden="true">⋯</span>
|
|
243
|
-
</Button>
|
|
244
|
-
<div
|
|
245
|
-
popover="auto"
|
|
246
|
-
id="actions-menu-{actionsMenuId}"
|
|
247
|
-
style="position-anchor:--actions-anchor-{actionsMenuId}"
|
|
248
|
-
class="dropdown dropdown-end w-56 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
|
|
249
|
-
bind:this={actionsPopoverEl}
|
|
250
|
-
>
|
|
251
|
-
{#if allowCreate}
|
|
252
|
-
{@const CreateIcon = icons.create}
|
|
253
|
-
<Button
|
|
254
|
-
variant="ghost"
|
|
255
|
-
class="btn-sm w-full justify-start"
|
|
256
|
-
onclick={() => {
|
|
257
|
-
actionsPopoverEl?.hidePopover();
|
|
258
|
-
handleCreate();
|
|
259
|
-
}}
|
|
260
|
-
>
|
|
261
|
-
<CreateIcon class="size-4" />
|
|
262
|
-
{creation.label || `${strings.create} ${labelOne}`}
|
|
263
|
-
</Button>
|
|
264
|
-
{/if}
|
|
265
|
-
|
|
266
|
-
{#if allowDelete}
|
|
267
|
-
<Button
|
|
268
|
-
variant="ghost"
|
|
269
|
-
class="btn-sm w-full justify-start text-error"
|
|
270
|
-
disabled={selectedCount === 0}
|
|
271
|
-
onclick={() => {
|
|
272
|
-
actionsPopoverEl?.hidePopover();
|
|
273
|
-
handleDelete();
|
|
274
|
-
}}
|
|
275
|
-
>
|
|
276
|
-
{deleteLabel} ({selectedCount})
|
|
277
|
-
</Button>
|
|
278
|
-
{/if}
|
|
279
|
-
|
|
280
|
-
{#if enableExport}
|
|
281
|
-
<Button
|
|
282
|
-
variant="ghost"
|
|
283
|
-
class="btn-sm w-full justify-start"
|
|
284
|
-
onclick={() => {
|
|
285
|
-
actionsPopoverEl?.hidePopover();
|
|
286
|
-
handleExport('csv');
|
|
287
|
-
}}
|
|
288
|
-
>
|
|
289
|
-
{strings.exportCsv}
|
|
290
|
-
</Button>
|
|
291
|
-
{#if exportConfig?.xlsx}
|
|
292
|
-
<Button
|
|
293
|
-
variant="ghost"
|
|
294
|
-
class="btn-sm w-full justify-start"
|
|
295
|
-
onclick={() => {
|
|
296
|
-
actionsPopoverEl?.hidePopover();
|
|
297
|
-
handleExport('xlsx');
|
|
298
|
-
}}
|
|
299
|
-
>
|
|
300
|
-
{strings.exportExcel}
|
|
301
|
-
</Button>
|
|
302
|
-
{/if}
|
|
303
|
-
{/if}
|
|
304
|
-
|
|
305
|
-
{#each bulkActions as bulkAction, i (i)}
|
|
306
|
-
{#if bulkAction.condition?.(selectedItems) ?? true}
|
|
307
|
-
{@const BulkIcon = bulkAction.icon}
|
|
308
|
-
{@const isView = isViewBasedBulkAction(bulkAction)}
|
|
309
|
-
<Button
|
|
310
|
-
variant="ghost"
|
|
311
|
-
class="btn-sm w-full justify-start"
|
|
312
|
-
disabled={!isView && selectedCount === 0}
|
|
313
|
-
title={bulkAction.tooltip}
|
|
314
|
-
onclick={() => {
|
|
315
|
-
actionsPopoverEl?.hidePopover();
|
|
316
|
-
handleBulkAction(bulkAction);
|
|
317
|
-
}}
|
|
318
|
-
>
|
|
319
|
-
<BulkIcon class="size-4" />
|
|
320
|
-
{bulkAction.label ?? bulkAction.tooltip}{#if !isView}
|
|
321
|
-
({selectedCount})
|
|
322
|
-
{/if}
|
|
323
|
-
</Button>
|
|
324
|
-
{/if}
|
|
325
|
-
{/each}
|
|
326
|
-
</div>
|
|
327
|
-
{:else}
|
|
328
|
-
{@render toolbarButtons(false)}
|
|
329
|
-
{/if}
|
|
330
|
-
</div>
|
|
331
|
-
|
|
332
|
-
<div
|
|
333
|
-
bind:this={toolbarMeasureEl}
|
|
334
|
-
class="pointer-events-none invisible fixed flex items-center gap-2 whitespace-nowrap"
|
|
335
|
-
style="left:-9999px; top:-9999px; width:max-content;"
|
|
336
|
-
aria-hidden="true"
|
|
337
|
-
>
|
|
338
|
-
{@render toolbarButtons(true)}
|
|
339
|
-
</div>
|
|
340
|
-
{/snippet}
|
|
341
|
-
</Header>
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import Button from '../../../form/Button.svelte';
|
|
3
|
+
import Header from '../../../common/Header.svelte';
|
|
4
|
+
import { downloadCsv, downloadXlsx } from '../../../table/export.js';
|
|
5
|
+
import { getIconSet } from '../../../../icons/context.js';
|
|
6
|
+
import { defaultIconSet } from '../../../../icons/sets/default.js';
|
|
7
|
+
import type {
|
|
8
|
+
ActionConfiguration,
|
|
9
|
+
ColumnDefinition,
|
|
10
|
+
CustomBulkAction,
|
|
11
|
+
ExportConfiguration,
|
|
12
|
+
SearchConfiguration,
|
|
13
|
+
ViewBasedCustomBulkAction
|
|
14
|
+
} from '../../../../types/crud.js';
|
|
15
|
+
import type { ServerPagination, TableQuery } from '../../../../types/table.js';
|
|
16
|
+
import { getStrings } from '../../../../i18n/context.js';
|
|
17
|
+
import SearchInput from '../../SearchInput.svelte';
|
|
18
|
+
|
|
19
|
+
const strings = getStrings();
|
|
20
|
+
|
|
21
|
+
function isViewBasedBulkAction<U extends object>(
|
|
22
|
+
action: CustomBulkAction<U>
|
|
23
|
+
): action is ViewBasedCustomBulkAction<U> {
|
|
24
|
+
return action.kind === 'view';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let {
|
|
28
|
+
labelOne = '',
|
|
29
|
+
labelMany = '',
|
|
30
|
+
icon,
|
|
31
|
+
creation = {} as ActionConfiguration<T>,
|
|
32
|
+
allowCreate,
|
|
33
|
+
allowDelete,
|
|
34
|
+
deleteLabel,
|
|
35
|
+
search = undefined as SearchConfiguration | undefined,
|
|
36
|
+
exportConfig = undefined as ExportConfiguration<T> | undefined,
|
|
37
|
+
bulkActions = [] as CustomBulkAction<T>[],
|
|
38
|
+
columns = [] as ColumnDefinition<T>[],
|
|
39
|
+
pagination = undefined as ServerPagination | undefined,
|
|
40
|
+
visibleRows = [] as T[],
|
|
41
|
+
exportQuery = undefined as TableQuery | undefined,
|
|
42
|
+
selectedCount = 0,
|
|
43
|
+
selectedItems = [] as T[],
|
|
44
|
+
onCreate,
|
|
45
|
+
onDeleteSelected,
|
|
46
|
+
onBulkAction
|
|
47
|
+
}: {
|
|
48
|
+
labelOne?: string;
|
|
49
|
+
labelMany?: string;
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
icon?: any;
|
|
52
|
+
creation?: ActionConfiguration<T>;
|
|
53
|
+
allowCreate: boolean;
|
|
54
|
+
allowDelete: boolean;
|
|
55
|
+
deleteLabel: string;
|
|
56
|
+
search?: SearchConfiguration;
|
|
57
|
+
exportConfig?: ExportConfiguration<T>;
|
|
58
|
+
bulkActions?: CustomBulkAction<T>[];
|
|
59
|
+
columns?: ColumnDefinition<T>[];
|
|
60
|
+
pagination?: ServerPagination;
|
|
61
|
+
visibleRows?: T[];
|
|
62
|
+
exportQuery?: TableQuery;
|
|
63
|
+
selectedCount?: number;
|
|
64
|
+
selectedItems?: T[];
|
|
65
|
+
onCreate?: () => void;
|
|
66
|
+
onDeleteSelected?: () => void;
|
|
67
|
+
onBulkAction?: (action: CustomBulkAction<T>) => void;
|
|
68
|
+
} = $props();
|
|
69
|
+
|
|
70
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
71
|
+
const entityIcon = $derived(icon ?? icons.folder);
|
|
72
|
+
const enableExport = $derived(!!exportConfig);
|
|
73
|
+
|
|
74
|
+
let exportPopoverEl: HTMLElement | undefined = $state();
|
|
75
|
+
let actionsPopoverEl: HTMLElement | undefined = $state();
|
|
76
|
+
const exportId = $props.id();
|
|
77
|
+
const actionsMenuId = `${exportId}-actions`;
|
|
78
|
+
|
|
79
|
+
// Collapses export/bulk-actions/delete into a single "Acciones" dropdown
|
|
80
|
+
// once the toolbar no longer fits its available width (small monitor, a
|
|
81
|
+
// browser window not maximized, many bulkActions, ...) — measured against
|
|
82
|
+
// an identical but off-flow, always-uncollapsed clone rather than the
|
|
83
|
+
// visible row itself, since the visible row's own width changes depending
|
|
84
|
+
// on whether it's currently collapsed.
|
|
85
|
+
let toolbarEl: HTMLElement | undefined = $state();
|
|
86
|
+
let toolbarMeasureEl: HTMLElement | undefined = $state();
|
|
87
|
+
let toolbarCollapsed = $state(false);
|
|
88
|
+
|
|
89
|
+
$effect(() => {
|
|
90
|
+
if (!toolbarEl || !toolbarMeasureEl) return;
|
|
91
|
+
const el = toolbarEl;
|
|
92
|
+
const measureEl = toolbarMeasureEl;
|
|
93
|
+
const check = () => {
|
|
94
|
+
toolbarCollapsed = measureEl.scrollWidth > el.clientWidth + 1;
|
|
95
|
+
};
|
|
96
|
+
const observer = new ResizeObserver(check);
|
|
97
|
+
observer.observe(el);
|
|
98
|
+
observer.observe(measureEl);
|
|
99
|
+
check();
|
|
100
|
+
return () => observer.disconnect();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
async function resolveExportRows(): Promise<T[]> {
|
|
104
|
+
if (!pagination) return visibleRows;
|
|
105
|
+
if (!exportConfig?.callback || !exportQuery) return visibleRows;
|
|
106
|
+
return exportConfig.callback(exportQuery);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function handleExport(format: 'csv' | 'xlsx') {
|
|
110
|
+
exportPopoverEl?.hidePopover();
|
|
111
|
+
const rows = await resolveExportRows();
|
|
112
|
+
const filename = `${labelMany || 'export'}-${new Date().toISOString().slice(0, 10)}`;
|
|
113
|
+
if (format === 'csv') downloadCsv(rows, columns, filename);
|
|
114
|
+
else if (exportConfig?.xlsx) downloadXlsx(rows, columns, filename, exportConfig.xlsx);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function handleCreate() {
|
|
118
|
+
onCreate?.();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleDelete() {
|
|
122
|
+
onDeleteSelected?.();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function handleBulkAction(action: CustomBulkAction<T>) {
|
|
126
|
+
onBulkAction?.(action);
|
|
127
|
+
}
|
|
128
|
+
</script>
|
|
129
|
+
|
|
130
|
+
{#snippet toolbarButtons(measuring: boolean)}
|
|
131
|
+
{#if search}
|
|
132
|
+
<SearchInput config={search} />
|
|
133
|
+
{/if}
|
|
134
|
+
|
|
135
|
+
{#if enableExport}
|
|
136
|
+
{@const ExportIcon = icons.download}
|
|
137
|
+
<Button
|
|
138
|
+
variant="ghost"
|
|
139
|
+
class="btn-square"
|
|
140
|
+
popovertarget={measuring ? undefined : `export-menu-${exportId}`}
|
|
141
|
+
style={measuring ? undefined : `anchor-name:--export-anchor-${exportId}`}
|
|
142
|
+
aria-label={strings.export}
|
|
143
|
+
title={strings.export}
|
|
144
|
+
>
|
|
145
|
+
<ExportIcon class="size-4" />
|
|
146
|
+
</Button>
|
|
147
|
+
{#if !measuring}
|
|
148
|
+
<div
|
|
149
|
+
popover="auto"
|
|
150
|
+
id="export-menu-{exportId}"
|
|
151
|
+
style="position-anchor:--export-anchor-{exportId}"
|
|
152
|
+
class="dropdown dropdown-end w-40 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
|
|
153
|
+
bind:this={exportPopoverEl}
|
|
154
|
+
>
|
|
155
|
+
<Button
|
|
156
|
+
variant="ghost"
|
|
157
|
+
class="btn-sm w-full justify-start"
|
|
158
|
+
onclick={() => handleExport('csv')}
|
|
159
|
+
>
|
|
160
|
+
{strings.exportCsv}
|
|
161
|
+
</Button>
|
|
162
|
+
{#if exportConfig?.xlsx}
|
|
163
|
+
<Button
|
|
164
|
+
variant="ghost"
|
|
165
|
+
class="btn-sm w-full justify-start"
|
|
166
|
+
onclick={() => handleExport('xlsx')}
|
|
167
|
+
>
|
|
168
|
+
{strings.exportExcel}
|
|
169
|
+
</Button>
|
|
170
|
+
{/if}
|
|
171
|
+
</div>
|
|
172
|
+
{/if}
|
|
173
|
+
{/if}
|
|
174
|
+
|
|
175
|
+
{#each bulkActions as bulkAction, i (i)}
|
|
176
|
+
{#if bulkAction.condition?.(selectedItems) ?? true}
|
|
177
|
+
{@const BulkIcon = bulkAction.icon}
|
|
178
|
+
{@const isView = isViewBasedBulkAction(bulkAction)}
|
|
179
|
+
<Button
|
|
180
|
+
variant={bulkAction.variant ?? 'ghost'}
|
|
181
|
+
class="btn-outline"
|
|
182
|
+
disabled={!isView && selectedCount === 0}
|
|
183
|
+
title={bulkAction.tooltip ?? bulkAction.label}
|
|
184
|
+
aria-label={bulkAction.label ? undefined : bulkAction.tooltip}
|
|
185
|
+
onclick={measuring ? undefined : () => handleBulkAction(bulkAction)}
|
|
186
|
+
>
|
|
187
|
+
<BulkIcon class="size-4" />
|
|
188
|
+
{#if bulkAction.label}
|
|
189
|
+
{bulkAction.label}{#if !isView}
|
|
190
|
+
({selectedCount})
|
|
191
|
+
{/if}
|
|
192
|
+
{/if}
|
|
193
|
+
</Button>
|
|
194
|
+
{/if}
|
|
195
|
+
{/each}
|
|
196
|
+
|
|
197
|
+
{#if allowDelete}
|
|
198
|
+
{@const DeleteIcon = icons.delete}
|
|
199
|
+
<Button
|
|
200
|
+
variant="error"
|
|
201
|
+
class="btn-outline"
|
|
202
|
+
disabled={selectedCount === 0}
|
|
203
|
+
onclick={measuring ? undefined : handleDelete}
|
|
204
|
+
>
|
|
205
|
+
<DeleteIcon class="size-4" />
|
|
206
|
+
{deleteLabel} ({selectedCount})
|
|
207
|
+
</Button>
|
|
208
|
+
{/if}
|
|
209
|
+
|
|
210
|
+
{#if allowCreate}
|
|
211
|
+
{@const CreateIcon = icons.create}
|
|
212
|
+
<Button variant="primary" onclick={measuring ? undefined : handleCreate}>
|
|
213
|
+
<CreateIcon class="size-5" />
|
|
214
|
+
{#if creation.label}
|
|
215
|
+
<span>{creation.label}</span>
|
|
216
|
+
{:else}
|
|
217
|
+
<span>{strings.create}<span class="hidden sm:inline"> {labelOne}</span></span>
|
|
218
|
+
{/if}
|
|
219
|
+
</Button>
|
|
220
|
+
{/if}
|
|
221
|
+
{/snippet}
|
|
222
|
+
|
|
223
|
+
<Header
|
|
224
|
+
title={labelMany}
|
|
225
|
+
breadcrumbs={[{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true }]}
|
|
226
|
+
>
|
|
227
|
+
{#snippet buttons()}
|
|
228
|
+
<div bind:this={toolbarEl} class="flex min-w-0 flex-wrap items-center gap-2 overflow-hidden">
|
|
229
|
+
{#if toolbarCollapsed}
|
|
230
|
+
{#if search}
|
|
231
|
+
<SearchInput config={search} />
|
|
232
|
+
{/if}
|
|
233
|
+
|
|
234
|
+
<Button
|
|
235
|
+
variant="ghost"
|
|
236
|
+
class="btn-square"
|
|
237
|
+
popovertarget="actions-menu-{actionsMenuId}"
|
|
238
|
+
style="anchor-name:--actions-anchor-{actionsMenuId}"
|
|
239
|
+
aria-label={strings.actions}
|
|
240
|
+
title={strings.actions}
|
|
241
|
+
>
|
|
242
|
+
<span class="text-lg leading-none" aria-hidden="true">⋯</span>
|
|
243
|
+
</Button>
|
|
244
|
+
<div
|
|
245
|
+
popover="auto"
|
|
246
|
+
id="actions-menu-{actionsMenuId}"
|
|
247
|
+
style="position-anchor:--actions-anchor-{actionsMenuId}"
|
|
248
|
+
class="dropdown dropdown-end w-56 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
|
|
249
|
+
bind:this={actionsPopoverEl}
|
|
250
|
+
>
|
|
251
|
+
{#if allowCreate}
|
|
252
|
+
{@const CreateIcon = icons.create}
|
|
253
|
+
<Button
|
|
254
|
+
variant="ghost"
|
|
255
|
+
class="btn-sm w-full justify-start"
|
|
256
|
+
onclick={() => {
|
|
257
|
+
actionsPopoverEl?.hidePopover();
|
|
258
|
+
handleCreate();
|
|
259
|
+
}}
|
|
260
|
+
>
|
|
261
|
+
<CreateIcon class="size-4" />
|
|
262
|
+
{creation.label || `${strings.create} ${labelOne}`}
|
|
263
|
+
</Button>
|
|
264
|
+
{/if}
|
|
265
|
+
|
|
266
|
+
{#if allowDelete}
|
|
267
|
+
<Button
|
|
268
|
+
variant="ghost"
|
|
269
|
+
class="btn-sm w-full justify-start text-error"
|
|
270
|
+
disabled={selectedCount === 0}
|
|
271
|
+
onclick={() => {
|
|
272
|
+
actionsPopoverEl?.hidePopover();
|
|
273
|
+
handleDelete();
|
|
274
|
+
}}
|
|
275
|
+
>
|
|
276
|
+
{deleteLabel} ({selectedCount})
|
|
277
|
+
</Button>
|
|
278
|
+
{/if}
|
|
279
|
+
|
|
280
|
+
{#if enableExport}
|
|
281
|
+
<Button
|
|
282
|
+
variant="ghost"
|
|
283
|
+
class="btn-sm w-full justify-start"
|
|
284
|
+
onclick={() => {
|
|
285
|
+
actionsPopoverEl?.hidePopover();
|
|
286
|
+
handleExport('csv');
|
|
287
|
+
}}
|
|
288
|
+
>
|
|
289
|
+
{strings.exportCsv}
|
|
290
|
+
</Button>
|
|
291
|
+
{#if exportConfig?.xlsx}
|
|
292
|
+
<Button
|
|
293
|
+
variant="ghost"
|
|
294
|
+
class="btn-sm w-full justify-start"
|
|
295
|
+
onclick={() => {
|
|
296
|
+
actionsPopoverEl?.hidePopover();
|
|
297
|
+
handleExport('xlsx');
|
|
298
|
+
}}
|
|
299
|
+
>
|
|
300
|
+
{strings.exportExcel}
|
|
301
|
+
</Button>
|
|
302
|
+
{/if}
|
|
303
|
+
{/if}
|
|
304
|
+
|
|
305
|
+
{#each bulkActions as bulkAction, i (i)}
|
|
306
|
+
{#if bulkAction.condition?.(selectedItems) ?? true}
|
|
307
|
+
{@const BulkIcon = bulkAction.icon}
|
|
308
|
+
{@const isView = isViewBasedBulkAction(bulkAction)}
|
|
309
|
+
<Button
|
|
310
|
+
variant="ghost"
|
|
311
|
+
class="btn-sm w-full justify-start"
|
|
312
|
+
disabled={!isView && selectedCount === 0}
|
|
313
|
+
title={bulkAction.tooltip}
|
|
314
|
+
onclick={() => {
|
|
315
|
+
actionsPopoverEl?.hidePopover();
|
|
316
|
+
handleBulkAction(bulkAction);
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<BulkIcon class="size-4" />
|
|
320
|
+
{bulkAction.label ?? bulkAction.tooltip}{#if !isView}
|
|
321
|
+
({selectedCount})
|
|
322
|
+
{/if}
|
|
323
|
+
</Button>
|
|
324
|
+
{/if}
|
|
325
|
+
{/each}
|
|
326
|
+
</div>
|
|
327
|
+
{:else}
|
|
328
|
+
{@render toolbarButtons(false)}
|
|
329
|
+
{/if}
|
|
330
|
+
</div>
|
|
331
|
+
|
|
332
|
+
<div
|
|
333
|
+
bind:this={toolbarMeasureEl}
|
|
334
|
+
class="pointer-events-none invisible fixed flex items-center gap-2 whitespace-nowrap"
|
|
335
|
+
style="left:-9999px; top:-9999px; width:max-content;"
|
|
336
|
+
aria-hidden="true"
|
|
337
|
+
>
|
|
338
|
+
{@render toolbarButtons(true)}
|
|
339
|
+
</div>
|
|
340
|
+
{/snippet}
|
|
341
|
+
</Header>
|