runeforge 0.0.52 → 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 +18 -23
- package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
- package/dist/components/crud/utils/embedded.d.ts +1 -0
- package/dist/components/crud/utils/embedded.js +14 -0
- package/dist/components/crud/utils/formatters.d.ts +0 -1
- package/dist/components/crud/utils/formatters.js +0 -6
- package/dist/components/crud/utils/reorder.d.ts +4 -0
- package/dist/components/crud/utils/reorder.js +13 -0
- package/dist/components/crud/utils/resolution.d.ts +1 -0
- package/dist/components/crud/utils/resolution.js +11 -0
- package/dist/components/crud/views/Create.svelte +2 -1
- package/dist/components/crud/views/Update.svelte +2 -2
- 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 +7 -6
- package/dist/index.js +5 -4
- package/dist/types/attribute.d.ts +8 -0
- package/dist/types/crud.d.ts +79 -2
- package/dist/types/table.d.ts +56 -0
- package/package.json +3 -1
- package/dist/components/crud/views/List.svelte +0 -584
|
@@ -0,0 +1,291 @@
|
|
|
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 Toolbar from './Toolbar.svelte';
|
|
5
|
+
import Table from './Table.svelte';
|
|
6
|
+
import Modals from './Modals.svelte';
|
|
7
|
+
import { computeReorderChanges } from '../../utils/reorder.js';
|
|
8
|
+
import type {
|
|
9
|
+
ActionConfiguration,
|
|
10
|
+
ColumnDefinition,
|
|
11
|
+
CustomAction,
|
|
12
|
+
CustomBulkAction,
|
|
13
|
+
ListActions,
|
|
14
|
+
ListConfig,
|
|
15
|
+
ViewBasedCustomBulkAction
|
|
16
|
+
} from '../../../../types/crud.js';
|
|
17
|
+
import type {
|
|
18
|
+
FilterSnapshot,
|
|
19
|
+
ServerPagination,
|
|
20
|
+
SortDirection,
|
|
21
|
+
TableQuery
|
|
22
|
+
} from '../../../../types/table.js';
|
|
23
|
+
import { getStrings } from '../../../../i18n/context.js';
|
|
24
|
+
|
|
25
|
+
const strings = getStrings();
|
|
26
|
+
|
|
27
|
+
function isViewBasedBulkAction<U extends object>(
|
|
28
|
+
action: CustomBulkAction<U>
|
|
29
|
+
): action is ViewBasedCustomBulkAction<U> {
|
|
30
|
+
return action.kind === 'view';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
data = [] as T[],
|
|
35
|
+
labelOne = '',
|
|
36
|
+
labelMany = '',
|
|
37
|
+
icon,
|
|
38
|
+
pageSize = 10,
|
|
39
|
+
idKey = '_id',
|
|
40
|
+
creation = {} as ActionConfiguration<T>,
|
|
41
|
+
update = {} as ActionConfiguration<T>,
|
|
42
|
+
read = {} as ActionConfiguration<T>,
|
|
43
|
+
deletion = {} as ActionConfiguration<T>,
|
|
44
|
+
actions = {} as ListActions<T>,
|
|
45
|
+
config = {} as ListConfig<T>,
|
|
46
|
+
columns = [] as ColumnDefinition<T>[],
|
|
47
|
+
pagination = undefined as ServerPagination | undefined,
|
|
48
|
+
initialSort = undefined as { column: string; direction: SortDirection } | undefined,
|
|
49
|
+
initialFilters = undefined as Partial<FilterSnapshot> | undefined,
|
|
50
|
+
onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
|
|
51
|
+
onCreate,
|
|
52
|
+
onEdit,
|
|
53
|
+
onView,
|
|
54
|
+
onAction,
|
|
55
|
+
onBulkAction
|
|
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?: ListActions<T>;
|
|
69
|
+
config?: ListConfig<T>;
|
|
70
|
+
columns?: ColumnDefinition<T>[];
|
|
71
|
+
pagination?: ServerPagination;
|
|
72
|
+
initialSort?: { column: string; direction: SortDirection };
|
|
73
|
+
initialFilters?: Partial<FilterSnapshot>;
|
|
74
|
+
onPaginationChange?: (query: TableQuery) => void;
|
|
75
|
+
onCreate?: () => void;
|
|
76
|
+
onEdit?: (item: T) => void;
|
|
77
|
+
onView?: (item: T) => void;
|
|
78
|
+
onAction?: (action: CustomAction<T>, item: T) => void;
|
|
79
|
+
onBulkAction?: (action: ViewBasedCustomBulkAction<T>, items: T[]) => void;
|
|
80
|
+
} = $props();
|
|
81
|
+
|
|
82
|
+
const customActions = $derived(actions.custom ?? []);
|
|
83
|
+
const bulkActions = $derived(actions.bulk ?? []);
|
|
84
|
+
const searchConfig = $derived(config.search);
|
|
85
|
+
const exportConfig = $derived(config.export);
|
|
86
|
+
const reorderConfig = $derived(config.reorder);
|
|
87
|
+
|
|
88
|
+
const allowRead = $derived(read.enabled ?? true);
|
|
89
|
+
const allowUpdate = $derived(update.enabled ?? true);
|
|
90
|
+
const allowDelete = $derived(deletion.enabled ?? true);
|
|
91
|
+
const allowCreate = $derived(creation.enabled ?? true);
|
|
92
|
+
const deleteLabel = $derived(deletion.label ?? strings.delete);
|
|
93
|
+
const updateLabel = $derived(update.label ?? strings.edit);
|
|
94
|
+
const readLabel = $derived(read.label ?? strings.view);
|
|
95
|
+
const allowSelection = $derived(
|
|
96
|
+
allowDelete || bulkActions.some((action) => !isViewBasedBulkAction(action))
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// SvelteSet is already reactive on its own; passing it through Table's
|
|
100
|
+
// (and PaginatedTable's) own `bind:selected` makes svelte-check's
|
|
101
|
+
// non_reactive_update warning fire here too — a false positive, per the
|
|
102
|
+
// same reasoning as the "intentional one-time hydration" notes elsewhere
|
|
103
|
+
// in this codebase.
|
|
104
|
+
let selected = new SvelteSet<number>();
|
|
105
|
+
let pendingDeletion = $state<T[] | null>(null);
|
|
106
|
+
let pendingBulkAction = $state<{ action: CustomBulkAction<T>; items: T[] } | null>(null);
|
|
107
|
+
const selectedItems = $derived([...selected].map((i) => data[i]));
|
|
108
|
+
|
|
109
|
+
let visibleRows = $state<T[]>([]);
|
|
110
|
+
let exportQuery = $state<TableQuery | undefined>(undefined);
|
|
111
|
+
|
|
112
|
+
// `selected` holds indices into `data`; if `data` is swapped for a different
|
|
113
|
+
// slice (page/sort/filter change in server mode) stale indices could point
|
|
114
|
+
// at unrelated rows, so clear on any data reference change.
|
|
115
|
+
let lastData: T[] | undefined;
|
|
116
|
+
$effect(() => {
|
|
117
|
+
if (data !== lastData) {
|
|
118
|
+
selected.clear();
|
|
119
|
+
lastData = data;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
async function runEndpointAction(
|
|
124
|
+
endpoint: string,
|
|
125
|
+
items: T[],
|
|
126
|
+
extraFields?: (item: T) => Record<string, string>
|
|
127
|
+
) {
|
|
128
|
+
await Promise.all(
|
|
129
|
+
items.map((item) => {
|
|
130
|
+
const fd = new FormData();
|
|
131
|
+
fd.set('id', String((item as Record<string, unknown>)[idKey] ?? ''));
|
|
132
|
+
for (const [k, v] of Object.entries(extraFields?.(item) ?? {})) fd.set(k, v);
|
|
133
|
+
return fetch(endpoint, { method: 'POST', body: fd });
|
|
134
|
+
})
|
|
135
|
+
);
|
|
136
|
+
await invalidateAll();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** One POST for every row that moved, instead of `runEndpointAction`'s one
|
|
140
|
+
* request per row — a drag that shifts N rows only settles once (on
|
|
141
|
+
* release), but N rows changing means N *parallel* requests fire at that
|
|
142
|
+
* moment, which reads a lot like "one per row crossed" from the network
|
|
143
|
+
* tab. FormData field `changes` carries a JSON array of `{id, value}`
|
|
144
|
+
* pairs, one per row whose `attribute` changed. */
|
|
145
|
+
async function runBatchEndpointAction(endpoint: string, items: T[], attribute: keyof T & string) {
|
|
146
|
+
const fd = new FormData();
|
|
147
|
+
fd.set(
|
|
148
|
+
'changes',
|
|
149
|
+
JSON.stringify(
|
|
150
|
+
items.map((item) => ({
|
|
151
|
+
id: String((item as Record<string, unknown>)[idKey] ?? ''),
|
|
152
|
+
value: (item as Record<string, unknown>)[attribute]
|
|
153
|
+
}))
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
await fetch(endpoint, { method: 'POST', body: fd });
|
|
157
|
+
await invalidateAll();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function runDeletion(items: T[]) {
|
|
161
|
+
if (deletion.endpoint) {
|
|
162
|
+
await runEndpointAction(deletion.endpoint, items);
|
|
163
|
+
} else {
|
|
164
|
+
await deletion.callback?.(items);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function requestDeletion(items: T[]) {
|
|
169
|
+
if (deletion.confirm) {
|
|
170
|
+
pendingDeletion = items;
|
|
171
|
+
} else {
|
|
172
|
+
runDeletion(items);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function handleDeleteSelected() {
|
|
177
|
+
const items = [...selected].map((i) => data[i]);
|
|
178
|
+
selected.clear();
|
|
179
|
+
requestDeletion(items);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function confirmDeletion() {
|
|
183
|
+
if (pendingDeletion) {
|
|
184
|
+
await runDeletion(pendingDeletion);
|
|
185
|
+
pendingDeletion = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function runBulkAction(action: CustomBulkAction<T>, items: T[]) {
|
|
190
|
+
if (isViewBasedBulkAction(action)) return;
|
|
191
|
+
await runEndpointAction(action.endpoint, items);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function requestBulkAction(action: CustomBulkAction<T>, items: T[]) {
|
|
195
|
+
if (!isViewBasedBulkAction(action) && action.confirm) {
|
|
196
|
+
pendingBulkAction = { action, items };
|
|
197
|
+
} else {
|
|
198
|
+
runBulkAction(action, items);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function handleBulkAction(action: CustomBulkAction<T>) {
|
|
203
|
+
const items = selectedItems;
|
|
204
|
+
if (isViewBasedBulkAction(action)) {
|
|
205
|
+
onBulkAction?.(action, items);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
selected.clear();
|
|
209
|
+
requestBulkAction(action, items);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function confirmBulkAction() {
|
|
213
|
+
if (pendingBulkAction) {
|
|
214
|
+
await runBulkAction(pendingBulkAction.action, pendingBulkAction.items);
|
|
215
|
+
pendingBulkAction = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function handleReorder(rows: T[]) {
|
|
220
|
+
const cfg = reorderConfig;
|
|
221
|
+
if (!cfg) return;
|
|
222
|
+
const changed = computeReorderChanges(rows, cfg.attribute);
|
|
223
|
+
if (changed.length === 0) return;
|
|
224
|
+
if (cfg.endpoint) {
|
|
225
|
+
await runBatchEndpointAction(cfg.endpoint, changed, cfg.attribute);
|
|
226
|
+
} else {
|
|
227
|
+
await cfg.callback?.(changed);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<div class="flex flex-col gap-6">
|
|
233
|
+
<Toolbar
|
|
234
|
+
{labelOne}
|
|
235
|
+
{labelMany}
|
|
236
|
+
{icon}
|
|
237
|
+
{creation}
|
|
238
|
+
{allowCreate}
|
|
239
|
+
{allowDelete}
|
|
240
|
+
{deleteLabel}
|
|
241
|
+
search={searchConfig}
|
|
242
|
+
{exportConfig}
|
|
243
|
+
{bulkActions}
|
|
244
|
+
{columns}
|
|
245
|
+
{pagination}
|
|
246
|
+
{visibleRows}
|
|
247
|
+
{exportQuery}
|
|
248
|
+
selectedCount={selected.size}
|
|
249
|
+
{selectedItems}
|
|
250
|
+
{onCreate}
|
|
251
|
+
onDeleteSelected={handleDeleteSelected}
|
|
252
|
+
onBulkAction={handleBulkAction}
|
|
253
|
+
/>
|
|
254
|
+
|
|
255
|
+
<Table
|
|
256
|
+
{data}
|
|
257
|
+
{columns}
|
|
258
|
+
{pageSize}
|
|
259
|
+
selectable={allowSelection}
|
|
260
|
+
bind:selected
|
|
261
|
+
{pagination}
|
|
262
|
+
{initialSort}
|
|
263
|
+
{initialFilters}
|
|
264
|
+
{onPaginationChange}
|
|
265
|
+
bind:visibleRows
|
|
266
|
+
bind:query={exportQuery}
|
|
267
|
+
{customActions}
|
|
268
|
+
{allowRead}
|
|
269
|
+
{allowUpdate}
|
|
270
|
+
{allowDelete}
|
|
271
|
+
{readLabel}
|
|
272
|
+
{updateLabel}
|
|
273
|
+
{deleteLabel}
|
|
274
|
+
reorder={reorderConfig}
|
|
275
|
+
{onView}
|
|
276
|
+
{onEdit}
|
|
277
|
+
{onAction}
|
|
278
|
+
onRequestDeletion={(item) => requestDeletion([item])}
|
|
279
|
+
onReorder={handleReorder}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<Modals
|
|
284
|
+
{deleteLabel}
|
|
285
|
+
{pendingDeletion}
|
|
286
|
+
{pendingBulkAction}
|
|
287
|
+
onCancelDeletion={() => (pendingDeletion = null)}
|
|
288
|
+
onConfirmDeletion={confirmDeletion}
|
|
289
|
+
onCancelBulkAction={() => (pendingBulkAction = null)}
|
|
290
|
+
onConfirmBulkAction={confirmBulkAction}
|
|
291
|
+
/>
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../../types/table.js';
|
|
1
|
+
import type { ActionConfiguration, ColumnDefinition, CustomAction, ListActions, ListConfig, ViewBasedCustomBulkAction } from '../../../../types/crud.js';
|
|
2
|
+
import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../../../types/table.js';
|
|
4
3
|
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
5
4
|
props: {
|
|
6
5
|
data?: T[];
|
|
@@ -13,9 +12,8 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
13
12
|
update?: ActionConfiguration<T>;
|
|
14
13
|
read?: ActionConfiguration<T>;
|
|
15
14
|
deletion?: ActionConfiguration<T>;
|
|
16
|
-
actions?:
|
|
17
|
-
|
|
18
|
-
search?: SearchConfiguration;
|
|
15
|
+
actions?: ListActions<T>;
|
|
16
|
+
config?: ListConfig<T>;
|
|
19
17
|
columns?: ColumnDefinition<T>[];
|
|
20
18
|
pagination?: ServerPagination;
|
|
21
19
|
initialSort?: {
|
|
@@ -24,14 +22,6 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
24
22
|
};
|
|
25
23
|
initialFilters?: Partial<FilterSnapshot>;
|
|
26
24
|
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;
|
|
35
25
|
onCreate?: () => void;
|
|
36
26
|
onEdit?: (item: T) => void;
|
|
37
27
|
onView?: (item: T) => void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import Button from '../../../form/Button.svelte';
|
|
3
|
+
import Modal from '../../../Modal.svelte';
|
|
4
|
+
import type { CustomBulkAction } from '../../../../types/crud.js';
|
|
5
|
+
import { getStrings } from '../../../../i18n/context.js';
|
|
6
|
+
|
|
7
|
+
const strings = getStrings();
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
deleteLabel = '',
|
|
11
|
+
pendingDeletion = null as T[] | null,
|
|
12
|
+
pendingBulkAction = null as { action: CustomBulkAction<T>; items: T[] } | null,
|
|
13
|
+
onCancelDeletion,
|
|
14
|
+
onConfirmDeletion,
|
|
15
|
+
onCancelBulkAction,
|
|
16
|
+
onConfirmBulkAction
|
|
17
|
+
}: {
|
|
18
|
+
deleteLabel?: string;
|
|
19
|
+
pendingDeletion?: T[] | null;
|
|
20
|
+
pendingBulkAction?: { action: CustomBulkAction<T>; items: T[] } | null;
|
|
21
|
+
onCancelDeletion?: () => void;
|
|
22
|
+
onConfirmDeletion?: () => void;
|
|
23
|
+
onCancelBulkAction?: () => void;
|
|
24
|
+
onConfirmBulkAction?: () => void;
|
|
25
|
+
} = $props();
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
{#if pendingDeletion !== null}
|
|
29
|
+
<Modal title={deleteLabel} onClose={onCancelDeletion}>
|
|
30
|
+
<p>{strings.deleteConfirm(pendingDeletion.length, deleteLabel)}</p>
|
|
31
|
+
<div class="flex justify-end gap-2 mt-4">
|
|
32
|
+
<Button variant="ghost" onclick={onCancelDeletion}>
|
|
33
|
+
{strings.cancel}
|
|
34
|
+
</Button>
|
|
35
|
+
<Button variant="error" onclick={onConfirmDeletion}>
|
|
36
|
+
{strings.confirm}
|
|
37
|
+
</Button>
|
|
38
|
+
</div>
|
|
39
|
+
</Modal>
|
|
40
|
+
{/if}
|
|
41
|
+
|
|
42
|
+
{#if pendingBulkAction !== null}
|
|
43
|
+
{@const pendingActionLabel =
|
|
44
|
+
pendingBulkAction.action.label ?? pendingBulkAction.action.tooltip ?? strings.actions}
|
|
45
|
+
<Modal title={pendingActionLabel} onClose={onCancelBulkAction}>
|
|
46
|
+
<p>{strings.deleteConfirm(pendingBulkAction.items.length, pendingActionLabel)}</p>
|
|
47
|
+
<div class="flex justify-end gap-2 mt-4">
|
|
48
|
+
<Button variant="ghost" onclick={onCancelBulkAction}>
|
|
49
|
+
{strings.cancel}
|
|
50
|
+
</Button>
|
|
51
|
+
<Button variant={pendingBulkAction.action.variant ?? 'primary'} onclick={onConfirmBulkAction}>
|
|
52
|
+
{strings.confirm}
|
|
53
|
+
</Button>
|
|
54
|
+
</div>
|
|
55
|
+
</Modal>
|
|
56
|
+
{/if}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { CustomBulkAction } from '../../../../types/crud.js';
|
|
2
|
+
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
3
|
+
props: {
|
|
4
|
+
deleteLabel?: string;
|
|
5
|
+
pendingDeletion?: T[] | null;
|
|
6
|
+
pendingBulkAction?: {
|
|
7
|
+
action: CustomBulkAction<T>;
|
|
8
|
+
items: T[];
|
|
9
|
+
} | null;
|
|
10
|
+
onCancelDeletion?: () => void;
|
|
11
|
+
onConfirmDeletion?: () => void;
|
|
12
|
+
onCancelBulkAction?: () => void;
|
|
13
|
+
onConfirmBulkAction?: () => void;
|
|
14
|
+
};
|
|
15
|
+
exports: {};
|
|
16
|
+
bindings: "";
|
|
17
|
+
slots: {};
|
|
18
|
+
events: {};
|
|
19
|
+
};
|
|
20
|
+
declare class __sveltets_Render<T extends object = Record<string, unknown>> {
|
|
21
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
22
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
23
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
24
|
+
bindings(): "";
|
|
25
|
+
exports(): {};
|
|
26
|
+
}
|
|
27
|
+
interface $$IsomorphicComponent {
|
|
28
|
+
new <T extends object = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
29
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
30
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
31
|
+
<T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
32
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
33
|
+
}
|
|
34
|
+
declare const Modals: $$IsomorphicComponent;
|
|
35
|
+
type Modals<T extends object = Record<string, unknown>> = InstanceType<typeof Modals<T>>;
|
|
36
|
+
export default Modals;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
3
|
+
import Button from '../../../form/Button.svelte';
|
|
4
|
+
import PaginatedTable from '../../../table/PaginatedTable.svelte';
|
|
5
|
+
import { getIconSet } from '../../../../icons/context.js';
|
|
6
|
+
import { defaultIconSet } from '../../../../icons/sets/default.js';
|
|
7
|
+
import type {
|
|
8
|
+
ColumnDefinition,
|
|
9
|
+
CustomAction,
|
|
10
|
+
ReorderConfiguration,
|
|
11
|
+
RowAction
|
|
12
|
+
} from '../../../../types/crud.js';
|
|
13
|
+
import type {
|
|
14
|
+
FilterSnapshot,
|
|
15
|
+
ServerPagination,
|
|
16
|
+
SortDirection,
|
|
17
|
+
TableQuery
|
|
18
|
+
} from '../../../../types/table.js';
|
|
19
|
+
|
|
20
|
+
const icons = $derived(getIconSet() ?? defaultIconSet);
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
data = [] as T[],
|
|
24
|
+
columns = [] as ColumnDefinition<T>[],
|
|
25
|
+
pageSize = 10,
|
|
26
|
+
selectable = true,
|
|
27
|
+
selected = $bindable(new SvelteSet<number>()),
|
|
28
|
+
pagination = undefined as ServerPagination | undefined,
|
|
29
|
+
initialSort = undefined as { column: string; direction: SortDirection } | undefined,
|
|
30
|
+
initialFilters = undefined as Partial<FilterSnapshot> | undefined,
|
|
31
|
+
onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
|
|
32
|
+
visibleRows = $bindable<T[]>([]),
|
|
33
|
+
query = $bindable<TableQuery | undefined>(undefined),
|
|
34
|
+
customActions = [] as CustomAction<T>[],
|
|
35
|
+
allowRead = true,
|
|
36
|
+
allowUpdate = true,
|
|
37
|
+
allowDelete = true,
|
|
38
|
+
readLabel = '',
|
|
39
|
+
updateLabel = '',
|
|
40
|
+
deleteLabel = '',
|
|
41
|
+
reorder = undefined as ReorderConfiguration<T> | undefined,
|
|
42
|
+
onView,
|
|
43
|
+
onEdit,
|
|
44
|
+
onAction,
|
|
45
|
+
onRequestDeletion,
|
|
46
|
+
onReorder
|
|
47
|
+
}: {
|
|
48
|
+
data?: T[];
|
|
49
|
+
columns?: ColumnDefinition<T>[];
|
|
50
|
+
pageSize?: number;
|
|
51
|
+
selectable?: boolean;
|
|
52
|
+
selected?: SvelteSet<number>;
|
|
53
|
+
pagination?: ServerPagination;
|
|
54
|
+
initialSort?: { column: string; direction: SortDirection };
|
|
55
|
+
initialFilters?: Partial<FilterSnapshot>;
|
|
56
|
+
onPaginationChange?: (query: TableQuery) => void;
|
|
57
|
+
visibleRows?: T[];
|
|
58
|
+
query?: TableQuery;
|
|
59
|
+
customActions?: CustomAction<T>[];
|
|
60
|
+
allowRead?: boolean;
|
|
61
|
+
allowUpdate?: boolean;
|
|
62
|
+
allowDelete?: boolean;
|
|
63
|
+
readLabel?: string;
|
|
64
|
+
updateLabel?: string;
|
|
65
|
+
deleteLabel?: string;
|
|
66
|
+
/** Drag-to-reorder configuration — see `ReorderConfiguration`. Ignored
|
|
67
|
+
* in server-pagination mode (`pagination` set). */
|
|
68
|
+
reorder?: ReorderConfiguration<T>;
|
|
69
|
+
onView?: (item: T) => void;
|
|
70
|
+
onEdit?: (item: T) => void;
|
|
71
|
+
onAction?: (action: CustomAction<T>, item: T) => void;
|
|
72
|
+
onRequestDeletion?: (item: T) => void;
|
|
73
|
+
/** Fires with the complete reordered row list after a drag; the caller
|
|
74
|
+
* (the list orchestrator) owns persisting it. */
|
|
75
|
+
onReorder?: (rows: T[]) => void;
|
|
76
|
+
} = $props();
|
|
77
|
+
|
|
78
|
+
const reorderActive = $derived(!!reorder && reorder.enabled !== false && !pagination);
|
|
79
|
+
|
|
80
|
+
const showRowActions = $derived(
|
|
81
|
+
allowRead || allowUpdate || allowDelete || customActions.length > 0
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const rowActions = $derived<RowAction<T>[]>([
|
|
85
|
+
...customActions.map((action) => ({
|
|
86
|
+
label: action.label,
|
|
87
|
+
icon: action.icon,
|
|
88
|
+
toggle: action.toggle,
|
|
89
|
+
class: action.class,
|
|
90
|
+
condition: action.condition,
|
|
91
|
+
run: (item: T) => onAction?.(action, item)
|
|
92
|
+
})),
|
|
93
|
+
...(allowRead
|
|
94
|
+
? [{ label: readLabel, icon: icons.view, run: (item: T) => onView?.(item) }]
|
|
95
|
+
: []),
|
|
96
|
+
...(allowUpdate
|
|
97
|
+
? [{ label: updateLabel, icon: icons.edit, run: (item: T) => onEdit?.(item) }]
|
|
98
|
+
: []),
|
|
99
|
+
...(allowDelete
|
|
100
|
+
? [
|
|
101
|
+
{
|
|
102
|
+
label: deleteLabel,
|
|
103
|
+
icon: icons.delete,
|
|
104
|
+
class: 'text-error',
|
|
105
|
+
run: (item: T) => onRequestDeletion?.(item)
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
: [])
|
|
109
|
+
]);
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
{#snippet actionsCell(item: T)}
|
|
113
|
+
<div class="flex justify-end items-center gap-1">
|
|
114
|
+
{#each rowActions as action (action.label)}
|
|
115
|
+
{#if action.condition?.(item) ?? true}
|
|
116
|
+
{@const resolvedClass =
|
|
117
|
+
typeof action.class === 'function' ? action.class(item) : action.class}
|
|
118
|
+
{#if action.toggle}
|
|
119
|
+
<input
|
|
120
|
+
type="checkbox"
|
|
121
|
+
class={['toggle toggle-sm', resolvedClass]}
|
|
122
|
+
title={action.label}
|
|
123
|
+
aria-label={action.label}
|
|
124
|
+
checked={action.toggle(item)}
|
|
125
|
+
onclick={(e) => {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
e.stopPropagation();
|
|
128
|
+
action.run(item);
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
{:else}
|
|
132
|
+
{@const Icon = action.icon}
|
|
133
|
+
<Button
|
|
134
|
+
variant="ghost"
|
|
135
|
+
class={['btn-xs', resolvedClass]}
|
|
136
|
+
title={action.label}
|
|
137
|
+
aria-label={action.label}
|
|
138
|
+
onclick={(e) => {
|
|
139
|
+
e.stopPropagation();
|
|
140
|
+
action.run(item);
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<Icon class="size-4" />
|
|
144
|
+
</Button>
|
|
145
|
+
{/if}
|
|
146
|
+
{/if}
|
|
147
|
+
{/each}
|
|
148
|
+
</div>
|
|
149
|
+
{/snippet}
|
|
150
|
+
|
|
151
|
+
<div class="table-wrapper">
|
|
152
|
+
<PaginatedTable
|
|
153
|
+
{data}
|
|
154
|
+
{columns}
|
|
155
|
+
{pageSize}
|
|
156
|
+
{selectable}
|
|
157
|
+
bind:selected
|
|
158
|
+
rowActions={showRowActions ? actionsCell : undefined}
|
|
159
|
+
{pagination}
|
|
160
|
+
{initialSort}
|
|
161
|
+
{initialFilters}
|
|
162
|
+
{onPaginationChange}
|
|
163
|
+
bind:visibleRows
|
|
164
|
+
bind:query
|
|
165
|
+
reorder={reorderActive ? reorder : undefined}
|
|
166
|
+
{onReorder}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<style>
|
|
171
|
+
.table-wrapper {
|
|
172
|
+
width: 100%;
|
|
173
|
+
max-width: var(--runeforge-crud-max-width);
|
|
174
|
+
margin-inline: auto;
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
2
|
+
import type { ColumnDefinition, CustomAction, ReorderConfiguration } from '../../../../types/crud.js';
|
|
3
|
+
import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../../../types/table.js';
|
|
4
|
+
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
5
|
+
props: {
|
|
6
|
+
data?: T[];
|
|
7
|
+
columns?: ColumnDefinition<T>[];
|
|
8
|
+
pageSize?: number;
|
|
9
|
+
selectable?: boolean;
|
|
10
|
+
selected?: SvelteSet<number>;
|
|
11
|
+
pagination?: ServerPagination;
|
|
12
|
+
initialSort?: {
|
|
13
|
+
column: string;
|
|
14
|
+
direction: SortDirection;
|
|
15
|
+
};
|
|
16
|
+
initialFilters?: Partial<FilterSnapshot>;
|
|
17
|
+
onPaginationChange?: (query: TableQuery) => void;
|
|
18
|
+
visibleRows?: T[];
|
|
19
|
+
query?: TableQuery;
|
|
20
|
+
customActions?: CustomAction<T>[];
|
|
21
|
+
allowRead?: boolean;
|
|
22
|
+
allowUpdate?: boolean;
|
|
23
|
+
allowDelete?: boolean;
|
|
24
|
+
readLabel?: string;
|
|
25
|
+
updateLabel?: string;
|
|
26
|
+
deleteLabel?: string;
|
|
27
|
+
/** Drag-to-reorder configuration — see `ReorderConfiguration`. Ignored
|
|
28
|
+
* in server-pagination mode (`pagination` set). */
|
|
29
|
+
reorder?: ReorderConfiguration<T>;
|
|
30
|
+
onView?: (item: T) => void;
|
|
31
|
+
onEdit?: (item: T) => void;
|
|
32
|
+
onAction?: (action: CustomAction<T>, item: T) => void;
|
|
33
|
+
onRequestDeletion?: (item: T) => void;
|
|
34
|
+
/** Fires with the complete reordered row list after a drag; the caller
|
|
35
|
+
* (the list orchestrator) owns persisting it. */
|
|
36
|
+
onReorder?: (rows: T[]) => void;
|
|
37
|
+
};
|
|
38
|
+
exports: {};
|
|
39
|
+
bindings: "selected" | "visibleRows" | "query";
|
|
40
|
+
slots: {};
|
|
41
|
+
events: {};
|
|
42
|
+
};
|
|
43
|
+
declare class __sveltets_Render<T extends object = Record<string, unknown>> {
|
|
44
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
45
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
46
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
47
|
+
bindings(): "selected" | "visibleRows" | "query";
|
|
48
|
+
exports(): {};
|
|
49
|
+
}
|
|
50
|
+
interface $$IsomorphicComponent {
|
|
51
|
+
new <T extends object = Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
52
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
53
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
54
|
+
<T extends object = Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
55
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
56
|
+
}
|
|
57
|
+
declare const Table: $$IsomorphicComponent;
|
|
58
|
+
type Table<T extends object = Record<string, unknown>> = InstanceType<typeof Table<T>>;
|
|
59
|
+
export default Table;
|