runeforge 0.0.17 → 0.0.19
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/dist/components/crud/GenericCRUD.svelte +346 -311
- package/dist/components/crud/GenericCRUD.svelte.d.ts +13 -1
- package/dist/components/crud/SearchInput.svelte +53 -0
- package/dist/components/crud/SearchInput.svelte.d.ts +7 -0
- package/dist/components/crud/views/List.svelte +379 -221
- package/dist/components/crud/views/List.svelte.d.ts +12 -1
- package/dist/components/table/PaginatedTable.svelte +16 -0
- package/dist/components/table/PaginatedTable.svelte.d.ts +8 -2
- package/dist/components/table/export.d.ts +17 -0
- package/dist/components/table/export.js +31 -0
- package/dist/i18n/en.js +5 -1
- package/dist/i18n/es.js +5 -1
- package/dist/i18n/types.d.ts +4 -0
- package/dist/icons/defaults/Download.svelte +7 -0
- package/dist/icons/defaults/Download.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 +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/types/crud.d.ts +13 -0
- package/package.json +1 -1
|
@@ -1,236 +1,394 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
} from '../../../types/crud.js';
|
|
20
|
+
import type {
|
|
21
|
+
FilterSnapshot,
|
|
22
|
+
ServerPagination,
|
|
23
|
+
SortDirection,
|
|
24
|
+
TableQuery
|
|
25
|
+
} from '../../../types/table.js';
|
|
26
|
+
import { getStrings } from '../../../i18n/context.js';
|
|
27
|
+
|
|
28
|
+
const strings = getStrings();
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
data = [] as T[],
|
|
32
|
+
labelOne = '',
|
|
33
|
+
labelMany = '',
|
|
34
|
+
icon,
|
|
35
|
+
pageSize = 10,
|
|
36
|
+
idKey = '_id',
|
|
37
|
+
creation = {} as ActionConfiguration<T>,
|
|
38
|
+
update = {} as ActionConfiguration<T>,
|
|
39
|
+
read = {} as ActionConfiguration<T>,
|
|
40
|
+
deletion = {} as ActionConfiguration<T>,
|
|
41
|
+
actions = [] as CustomAction<T>[],
|
|
42
|
+
customBulkActions = [] as CustomBulkAction<T>[],
|
|
43
|
+
search = undefined as SearchConfiguration | undefined,
|
|
44
|
+
columns = [] as ColumnDefinition<T>[],
|
|
45
|
+
pagination = undefined as ServerPagination | undefined,
|
|
46
|
+
initialSort = undefined as { column: string; direction: SortDirection } | undefined,
|
|
47
|
+
initialFilters = undefined as Partial<FilterSnapshot> | undefined,
|
|
48
|
+
onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
|
|
49
|
+
enableExport = false,
|
|
50
|
+
onExport = undefined as ((query: TableQuery) => Promise<T[]>) | undefined,
|
|
51
|
+
xlsx = undefined as XlsxModule | undefined,
|
|
52
|
+
onCreate,
|
|
53
|
+
onEdit,
|
|
54
|
+
onView,
|
|
55
|
+
onAction
|
|
56
|
+
}: {
|
|
57
|
+
data?: T[];
|
|
58
|
+
labelOne?: string;
|
|
59
|
+
labelMany?: string;
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
icon?: any;
|
|
62
|
+
pageSize?: number;
|
|
63
|
+
idKey?: string;
|
|
64
|
+
creation?: ActionConfiguration<T>;
|
|
65
|
+
update?: ActionConfiguration<T>;
|
|
66
|
+
read?: ActionConfiguration<T>;
|
|
67
|
+
deletion?: ActionConfiguration<T>;
|
|
68
|
+
actions?: CustomAction<T>[];
|
|
69
|
+
customBulkActions?: CustomBulkAction<T>[];
|
|
70
|
+
search?: SearchConfiguration;
|
|
71
|
+
columns?: ColumnDefinition<T>[];
|
|
72
|
+
pagination?: ServerPagination;
|
|
73
|
+
initialSort?: { column: string; direction: SortDirection };
|
|
74
|
+
initialFilters?: Partial<FilterSnapshot>;
|
|
75
|
+
onPaginationChange?: (query: TableQuery) => void;
|
|
76
|
+
/** Shows an icon-only export button (CSV, and Excel if `xlsx` is provided). */
|
|
77
|
+
enableExport?: boolean;
|
|
78
|
+
/** Server-pagination mode only: fetch all rows matching the current query
|
|
79
|
+
* (unpaginated) for export. Without it, export falls back to the loaded page. */
|
|
80
|
+
onExport?: (query: TableQuery) => Promise<T[]>;
|
|
81
|
+
/** Resolved `xlsx` (SheetJS) module, e.g. `import * as xlsx from 'xlsx'`.
|
|
82
|
+
* Enables the "Export as Excel" option; omit to only offer CSV. */
|
|
83
|
+
xlsx?: XlsxModule;
|
|
84
|
+
onCreate?: () => void;
|
|
85
|
+
onEdit?: (item: T) => void;
|
|
86
|
+
onView?: (item: T) => void;
|
|
87
|
+
onAction?: (action: CustomAction<T>, item: T) => void;
|
|
88
|
+
} = $props();
|
|
89
|
+
|
|
90
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
91
|
+
const entityIcon = $derived(icon ?? icons.folder);
|
|
92
|
+
|
|
93
|
+
const allowRead = $derived(read.enabled ?? true);
|
|
94
|
+
const allowUpdate = $derived(update.enabled ?? true);
|
|
95
|
+
const allowDelete = $derived(deletion.enabled ?? true);
|
|
96
|
+
const deleteLabel = $derived(deletion.label ?? strings.delete);
|
|
97
|
+
const updateLabel = $derived(update.label ?? strings.edit);
|
|
98
|
+
const readLabel = $derived(read.label ?? strings.view);
|
|
99
|
+
const showRowActions = $derived(allowRead || allowUpdate || allowDelete || actions.length > 0);
|
|
100
|
+
const allowSelection = $derived(allowDelete || customBulkActions.length > 0);
|
|
101
|
+
|
|
102
|
+
let selected = new SvelteSet<number>();
|
|
103
|
+
let pendingDeletion = $state<T[] | null>(null);
|
|
104
|
+
let pendingBulkAction = $state<{ action: CustomBulkAction<T>; items: T[] } | null>(null);
|
|
105
|
+
const selectedItems = $derived([...selected].map((i) => data[i]));
|
|
106
|
+
|
|
107
|
+
let visibleRows = $state<T[]>([]);
|
|
108
|
+
let exportQuery = $state<TableQuery | undefined>(undefined);
|
|
109
|
+
let exportPopoverEl: HTMLElement | undefined = $state();
|
|
110
|
+
const exportId = $props.id();
|
|
111
|
+
|
|
112
|
+
async function resolveExportRows(): Promise<T[]> {
|
|
113
|
+
if (!pagination) return visibleRows;
|
|
114
|
+
if (!onExport || !exportQuery) return data;
|
|
115
|
+
return onExport(exportQuery);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function handleExport(format: 'csv' | 'xlsx') {
|
|
119
|
+
exportPopoverEl?.hidePopover();
|
|
120
|
+
const rows = await resolveExportRows();
|
|
121
|
+
const filename = `${labelMany || 'export'}-${new Date().toISOString().slice(0, 10)}`;
|
|
122
|
+
if (format === 'csv') downloadCsv(rows, columns, filename);
|
|
123
|
+
else if (xlsx) downloadXlsx(rows, columns, filename, xlsx);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// `selected` holds indices into `data`; if `data` is swapped for a different
|
|
127
|
+
// slice (page/sort/filter change in server mode) stale indices could point
|
|
128
|
+
// at unrelated rows, so clear on any data reference change.
|
|
129
|
+
let lastData: T[] | undefined;
|
|
130
|
+
$effect(() => {
|
|
131
|
+
if (data !== lastData) {
|
|
132
|
+
selected.clear();
|
|
133
|
+
lastData = data;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
async function runEndpointAction(endpoint: string, items: T[]) {
|
|
138
|
+
await Promise.all(
|
|
139
|
+
items.map((item) => {
|
|
140
|
+
const fd = new FormData();
|
|
141
|
+
fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
|
|
142
|
+
return fetch(endpoint, { method: 'POST', body: fd });
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
await invalidateAll();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function runDeletion(items: T[]) {
|
|
149
|
+
if (deletion.endpoint) {
|
|
150
|
+
await runEndpointAction(deletion.endpoint, items);
|
|
151
|
+
} else {
|
|
152
|
+
await deletion.callback?.(items);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function requestDeletion(items: T[]) {
|
|
157
|
+
if (deletion.confirm) {
|
|
158
|
+
pendingDeletion = items;
|
|
159
|
+
} else {
|
|
160
|
+
runDeletion(items);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function handleCreate() {
|
|
165
|
+
onCreate?.();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function handleDelete() {
|
|
169
|
+
const items = [...selected].map((i) => data[i]);
|
|
170
|
+
selected.clear();
|
|
171
|
+
requestDeletion(items);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function confirmDeletion() {
|
|
175
|
+
if (pendingDeletion) {
|
|
176
|
+
await runDeletion(pendingDeletion);
|
|
177
|
+
pendingDeletion = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function runBulkAction(action: CustomBulkAction<T>, items: T[]) {
|
|
182
|
+
await runEndpointAction(action.endpoint, items);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function requestBulkAction(action: CustomBulkAction<T>, items: T[]) {
|
|
186
|
+
if (action.confirm) {
|
|
187
|
+
pendingBulkAction = { action, items };
|
|
188
|
+
} else {
|
|
189
|
+
runBulkAction(action, items);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function handleBulkAction(action: CustomBulkAction<T>) {
|
|
194
|
+
const items = selectedItems;
|
|
195
|
+
selected.clear();
|
|
196
|
+
requestBulkAction(action, items);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function confirmBulkAction() {
|
|
200
|
+
if (pendingBulkAction) {
|
|
201
|
+
await runBulkAction(pendingBulkAction.action, pendingBulkAction.items);
|
|
202
|
+
pendingBulkAction = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const rowActions = $derived<RowAction<T>[]>([
|
|
207
|
+
...actions.map((action) => ({
|
|
208
|
+
label: action.label,
|
|
209
|
+
icon: action.icon,
|
|
210
|
+
condition: action.condition,
|
|
211
|
+
run: (item: T) => onAction?.(action, item)
|
|
212
|
+
})),
|
|
213
|
+
...(allowRead
|
|
214
|
+
? [{ label: readLabel, icon: icons.view, run: (item: T) => onView?.(item) }]
|
|
215
|
+
: []),
|
|
216
|
+
...(allowUpdate
|
|
217
|
+
? [{ label: updateLabel, icon: icons.edit, run: (item: T) => onEdit?.(item) }]
|
|
218
|
+
: []),
|
|
219
|
+
...(allowDelete
|
|
220
|
+
? [
|
|
221
|
+
{
|
|
222
|
+
label: deleteLabel,
|
|
223
|
+
icon: icons.delete,
|
|
224
|
+
class: 'text-error',
|
|
225
|
+
run: (item: T) => requestDeletion([item])
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
: [])
|
|
229
|
+
]);
|
|
142
230
|
</script>
|
|
143
231
|
|
|
144
232
|
{#snippet actionsCell(item: T)}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
233
|
+
<div class="flex justify-end gap-1">
|
|
234
|
+
{#each rowActions as action (action.label)}
|
|
235
|
+
{#if action.condition?.(item) ?? true}
|
|
236
|
+
{@const Icon = action.icon}
|
|
237
|
+
<Button
|
|
238
|
+
variant="ghost"
|
|
239
|
+
class={['btn-xs', action.class]}
|
|
240
|
+
title={action.label}
|
|
241
|
+
aria-label={action.label}
|
|
242
|
+
onclick={(e) => {
|
|
243
|
+
e.stopPropagation();
|
|
244
|
+
action.run(item);
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
<Icon class="size-4" />
|
|
248
|
+
</Button>
|
|
249
|
+
{/if}
|
|
250
|
+
{/each}
|
|
251
|
+
</div>
|
|
161
252
|
{/snippet}
|
|
162
253
|
|
|
163
254
|
<div class="flex flex-col gap-6">
|
|
255
|
+
<Header
|
|
256
|
+
title={labelMany}
|
|
257
|
+
breadcrumbs={[{ label: labelMany, icon: entityIcon, link: { href: '#' }, prominent: true }]}
|
|
258
|
+
>
|
|
259
|
+
{#snippet buttons()}
|
|
260
|
+
{#if search}
|
|
261
|
+
<SearchInput config={search} />
|
|
262
|
+
{/if}
|
|
164
263
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
264
|
+
{#if enableExport}
|
|
265
|
+
{@const ExportIcon = icons.download}
|
|
266
|
+
<Button
|
|
267
|
+
variant="ghost"
|
|
268
|
+
class="btn-square"
|
|
269
|
+
popovertarget="export-menu-{exportId}"
|
|
270
|
+
style="anchor-name:--export-anchor-{exportId}"
|
|
271
|
+
aria-label={strings.export}
|
|
272
|
+
title={strings.export}
|
|
273
|
+
>
|
|
274
|
+
<ExportIcon class="size-4" />
|
|
275
|
+
</Button>
|
|
276
|
+
<div
|
|
277
|
+
popover="auto"
|
|
278
|
+
id="export-menu-{exportId}"
|
|
279
|
+
style="position-anchor:--export-anchor-{exportId}"
|
|
280
|
+
class="dropdown dropdown-end w-40 rounded-box border border-base-content/10 bg-base-100 p-1 shadow-lg"
|
|
281
|
+
bind:this={exportPopoverEl}
|
|
282
|
+
>
|
|
283
|
+
<Button
|
|
284
|
+
variant="ghost"
|
|
285
|
+
class="btn-sm w-full justify-start"
|
|
286
|
+
onclick={() => handleExport('csv')}
|
|
287
|
+
>
|
|
288
|
+
{strings.exportCsv}
|
|
289
|
+
</Button>
|
|
290
|
+
{#if xlsx}
|
|
291
|
+
<Button
|
|
292
|
+
variant="ghost"
|
|
293
|
+
class="btn-sm w-full justify-start"
|
|
294
|
+
onclick={() => handleExport('xlsx')}
|
|
295
|
+
>
|
|
296
|
+
{strings.exportExcel}
|
|
297
|
+
</Button>
|
|
298
|
+
{/if}
|
|
299
|
+
</div>
|
|
300
|
+
{/if}
|
|
301
|
+
|
|
302
|
+
{#each customBulkActions as bulkAction (bulkAction.label)}
|
|
303
|
+
{#if bulkAction.condition?.(selectedItems) ?? true}
|
|
304
|
+
{@const BulkIcon = bulkAction.icon}
|
|
305
|
+
<Button
|
|
306
|
+
variant={bulkAction.variant ?? 'ghost'}
|
|
307
|
+
class="btn-outline"
|
|
308
|
+
disabled={selected.size === 0}
|
|
309
|
+
onclick={() => handleBulkAction(bulkAction)}
|
|
310
|
+
>
|
|
311
|
+
<BulkIcon class="size-4" />
|
|
312
|
+
{bulkAction.label} ({selected.size})
|
|
313
|
+
</Button>
|
|
314
|
+
{/if}
|
|
315
|
+
{/each}
|
|
316
|
+
|
|
317
|
+
{#if allowDelete}
|
|
318
|
+
{@const DeleteIcon = icons.delete}
|
|
319
|
+
<Button
|
|
320
|
+
variant="error"
|
|
321
|
+
class="btn-outline"
|
|
322
|
+
disabled={selected.size === 0}
|
|
323
|
+
onclick={handleDelete}
|
|
324
|
+
>
|
|
325
|
+
<DeleteIcon class="size-4" />
|
|
326
|
+
{deleteLabel} ({selected.size})
|
|
327
|
+
</Button>
|
|
328
|
+
{/if}
|
|
329
|
+
|
|
330
|
+
{@const CreateIcon = icons.create}
|
|
331
|
+
<Button variant="primary" onclick={handleCreate}>
|
|
332
|
+
<CreateIcon class="size-5" />
|
|
333
|
+
{#if creation.label}
|
|
334
|
+
<span>{creation.label}</span>
|
|
335
|
+
{:else}
|
|
336
|
+
<span>{strings.create}<span class="hidden sm:inline"> {labelOne}</span></span>
|
|
337
|
+
{/if}
|
|
338
|
+
</Button>
|
|
339
|
+
{/snippet}
|
|
340
|
+
</Header>
|
|
341
|
+
|
|
342
|
+
<div class="table-wrapper">
|
|
343
|
+
<PaginatedTable
|
|
344
|
+
{data}
|
|
345
|
+
{columns}
|
|
346
|
+
{pageSize}
|
|
347
|
+
selectable={allowSelection}
|
|
348
|
+
{selected}
|
|
349
|
+
rowActions={showRowActions ? actionsCell : undefined}
|
|
350
|
+
{pagination}
|
|
351
|
+
{initialSort}
|
|
352
|
+
{initialFilters}
|
|
353
|
+
{onPaginationChange}
|
|
354
|
+
bind:visibleRows
|
|
355
|
+
bind:query={exportQuery}
|
|
356
|
+
/>
|
|
357
|
+
</div>
|
|
214
358
|
</div>
|
|
215
359
|
|
|
216
360
|
{#if pendingDeletion !== null}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
361
|
+
<Modal title={deleteLabel} onClose={() => (pendingDeletion = null)}>
|
|
362
|
+
<p>{strings.deleteConfirm(pendingDeletion.length, deleteLabel)}</p>
|
|
363
|
+
<div class="flex justify-end gap-2 mt-4">
|
|
364
|
+
<Button variant="ghost" onclick={() => (pendingDeletion = null)}>
|
|
365
|
+
{strings.cancel}
|
|
366
|
+
</Button>
|
|
367
|
+
<Button variant="error" onclick={confirmDeletion}>
|
|
368
|
+
{strings.confirm}
|
|
369
|
+
</Button>
|
|
370
|
+
</div>
|
|
371
|
+
</Modal>
|
|
372
|
+
{/if}
|
|
373
|
+
|
|
374
|
+
{#if pendingBulkAction !== null}
|
|
375
|
+
<Modal title={pendingBulkAction.action.label} onClose={() => (pendingBulkAction = null)}>
|
|
376
|
+
<p>{strings.deleteConfirm(pendingBulkAction.items.length, pendingBulkAction.action.label)}</p>
|
|
377
|
+
<div class="flex justify-end gap-2 mt-4">
|
|
378
|
+
<Button variant="ghost" onclick={() => (pendingBulkAction = null)}>
|
|
379
|
+
{strings.cancel}
|
|
380
|
+
</Button>
|
|
381
|
+
<Button variant={pendingBulkAction.action.variant ?? 'primary'} onclick={confirmBulkAction}>
|
|
382
|
+
{strings.confirm}
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
385
|
+
</Modal>
|
|
228
386
|
{/if}
|
|
229
387
|
|
|
230
388
|
<style>
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
389
|
+
.table-wrapper {
|
|
390
|
+
width: 100%;
|
|
391
|
+
max-width: var(--runeforge-crud-max-width);
|
|
392
|
+
margin-inline: auto;
|
|
393
|
+
}
|
|
236
394
|
</style>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type XlsxModule } from '../../table/export.js';
|
|
2
|
+
import type { ActionConfiguration, ColumnDefinition, CustomAction, CustomBulkAction, SearchConfiguration } from '../../../types/crud.js';
|
|
2
3
|
import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../../types/table.js';
|
|
3
4
|
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
4
5
|
props: {
|
|
@@ -13,6 +14,8 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
13
14
|
read?: ActionConfiguration<T>;
|
|
14
15
|
deletion?: ActionConfiguration<T>;
|
|
15
16
|
actions?: CustomAction<T>[];
|
|
17
|
+
customBulkActions?: CustomBulkAction<T>[];
|
|
18
|
+
search?: SearchConfiguration;
|
|
16
19
|
columns?: ColumnDefinition<T>[];
|
|
17
20
|
pagination?: ServerPagination;
|
|
18
21
|
initialSort?: {
|
|
@@ -21,6 +24,14 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
21
24
|
};
|
|
22
25
|
initialFilters?: Partial<FilterSnapshot>;
|
|
23
26
|
onPaginationChange?: (query: TableQuery) => void;
|
|
27
|
+
/** Shows an icon-only export button (CSV, and Excel if `xlsx` is provided). */
|
|
28
|
+
enableExport?: boolean;
|
|
29
|
+
/** Server-pagination mode only: fetch all rows matching the current query
|
|
30
|
+
* (unpaginated) for export. Without it, export falls back to the loaded page. */
|
|
31
|
+
onExport?: (query: TableQuery) => Promise<T[]>;
|
|
32
|
+
/** Resolved `xlsx` (SheetJS) module, e.g. `import * as xlsx from 'xlsx'`.
|
|
33
|
+
* Enables the "Export as Excel" option; omit to only offer CSV. */
|
|
34
|
+
xlsx?: XlsxModule;
|
|
24
35
|
onCreate?: () => void;
|
|
25
36
|
onEdit?: (item: T) => void;
|
|
26
37
|
onView?: (item: T) => void;
|