runeforge 0.0.11 → 0.0.13
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 +102 -1
- package/dist/components/crud/views/List.svelte +25 -1
- package/dist/components/crud/views/List.svelte.d.ts +8 -0
- package/dist/components/table/ColumnFilter.svelte +3 -1
- package/dist/components/table/PaginatedTable.svelte +84 -17
- package/dist/components/table/PaginatedTable.svelte.d.ts +11 -0
- package/dist/components/table/state.svelte.d.ts +7 -1
- package/dist/components/table/state.svelte.js +22 -0
- package/dist/i18n/en.js +1 -1
- package/dist/i18n/es.js +1 -1
- package/dist/i18n/types.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/types/table.d.ts +25 -0
- package/package.json +78 -80
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import Update from './views/Update.svelte';
|
|
8
8
|
import { AUTO_EXCLUDED } from './utils/constants.js';
|
|
9
9
|
import { resolveOptions, resolveFormatter, inferType } from './utils/resolution.js';
|
|
10
|
+
import { isFilterable } from '../table/utils.js';
|
|
10
11
|
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
11
12
|
import type {
|
|
12
13
|
ActionConfiguration,
|
|
@@ -14,6 +15,13 @@
|
|
|
14
15
|
CustomAction,
|
|
15
16
|
FieldDefinition,
|
|
16
17
|
} from '../../types/crud.js';
|
|
18
|
+
import type {
|
|
19
|
+
FilterSnapshot,
|
|
20
|
+
PaginatedEnvelope,
|
|
21
|
+
ServerPagination,
|
|
22
|
+
SortDirection,
|
|
23
|
+
TableQuery,
|
|
24
|
+
} from '../../types/table.js';
|
|
17
25
|
|
|
18
26
|
let {
|
|
19
27
|
data = undefined as Record<string, unknown> | undefined,
|
|
@@ -52,8 +60,31 @@
|
|
|
52
60
|
form?: { error?: string } | null;
|
|
53
61
|
} = $props();
|
|
54
62
|
|
|
63
|
+
function isEnvelope(value: unknown): value is PaginatedEnvelope<T> {
|
|
64
|
+
return (
|
|
65
|
+
!!value &&
|
|
66
|
+
typeof value === 'object' &&
|
|
67
|
+
!Array.isArray(value) &&
|
|
68
|
+
Array.isArray((value as Record<string, unknown>).results) &&
|
|
69
|
+
typeof (value as Record<string, unknown>).count === 'number'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const rawSlice = $derived(data && dataKey ? data[dataKey] : undefined);
|
|
74
|
+
const envelope = $derived(isEnvelope(rawSlice) ? rawSlice : undefined);
|
|
55
75
|
const entityData = $derived<T[]>(
|
|
56
|
-
|
|
76
|
+
envelope ? envelope.results : (Array.isArray(rawSlice) ? (rawSlice as T[]) : [])
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const serverPagination = $derived<ServerPagination | undefined>(
|
|
80
|
+
envelope
|
|
81
|
+
? {
|
|
82
|
+
page: envelope.page,
|
|
83
|
+
pageSize: envelope.pageSize,
|
|
84
|
+
totalPages: Math.max(1, Math.ceil(envelope.count / envelope.pageSize)),
|
|
85
|
+
total: envelope.count,
|
|
86
|
+
}
|
|
87
|
+
: undefined
|
|
57
88
|
);
|
|
58
89
|
|
|
59
90
|
const viewParam = $derived(page.url.searchParams.get('view'));
|
|
@@ -102,6 +133,72 @@
|
|
|
102
133
|
: [])
|
|
103
134
|
);
|
|
104
135
|
|
|
136
|
+
// Server-pagination mode only: GenericCRUD owns `page`/`ordering`/per-column
|
|
137
|
+
// filter query params the same way it already owns `view`/`id`, so pagination
|
|
138
|
+
// state survives reloads/direct links and `+page.svelte` never has to change.
|
|
139
|
+
const orderingParam = $derived(page.url.searchParams.get('ordering'));
|
|
140
|
+
const initialSort = $derived(
|
|
141
|
+
orderingParam
|
|
142
|
+
? {
|
|
143
|
+
column: orderingParam.startsWith('-') ? orderingParam.slice(1) : orderingParam,
|
|
144
|
+
direction: (orderingParam.startsWith('-') ? 'desc' : 'asc') as SortDirection,
|
|
145
|
+
}
|
|
146
|
+
: undefined
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const initialFilters = $derived.by<Partial<FilterSnapshot> | undefined>(() => {
|
|
150
|
+
if (!envelope) return undefined;
|
|
151
|
+
const text: Record<string, string> = {};
|
|
152
|
+
const values: Record<string, string[]> = {};
|
|
153
|
+
const dateRanges: Record<string, { from: string; to: string }> = {};
|
|
154
|
+
for (const col of resolvedColumns) {
|
|
155
|
+
if (!isFilterable(col)) continue;
|
|
156
|
+
const raw = page.url.searchParams.get(col.attribute);
|
|
157
|
+
if (raw != null) {
|
|
158
|
+
if (col.type === 'boolean') values[col.attribute] = raw.split(',').filter(Boolean);
|
|
159
|
+
else text[col.attribute] = raw;
|
|
160
|
+
}
|
|
161
|
+
const from = page.url.searchParams.get(`${col.attribute}_from`);
|
|
162
|
+
const to = page.url.searchParams.get(`${col.attribute}_to`);
|
|
163
|
+
if (from || to) dateRanges[col.attribute] = { from: from ?? '', to: to ?? '' };
|
|
164
|
+
}
|
|
165
|
+
return { text, values, dateRanges };
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
async function handlePaginationChange(query: TableQuery) {
|
|
169
|
+
// Local, synchronous query-string builder consumed immediately by goto();
|
|
170
|
+
// not rendered/reactive state, so plain URLSearchParams is correct here.
|
|
171
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
172
|
+
const params = new URLSearchParams(page.url.searchParams);
|
|
173
|
+
params.delete('view');
|
|
174
|
+
params.delete('id');
|
|
175
|
+
|
|
176
|
+
if (query.page > 1) params.set('page', String(query.page));
|
|
177
|
+
else params.delete('page');
|
|
178
|
+
|
|
179
|
+
if (query.ordering) params.set('ordering', query.ordering);
|
|
180
|
+
else params.delete('ordering');
|
|
181
|
+
|
|
182
|
+
for (const col of resolvedColumns) {
|
|
183
|
+
params.delete(col.attribute);
|
|
184
|
+
params.delete(`${col.attribute}_from`);
|
|
185
|
+
params.delete(`${col.attribute}_to`);
|
|
186
|
+
}
|
|
187
|
+
for (const [k, v] of Object.entries(query.filters.text)) {
|
|
188
|
+
if (v) params.set(k, v);
|
|
189
|
+
}
|
|
190
|
+
for (const [k, vs] of Object.entries(query.filters.values)) {
|
|
191
|
+
if (vs.length) params.set(k, vs.join(','));
|
|
192
|
+
}
|
|
193
|
+
for (const [k, r] of Object.entries(query.filters.dateRanges)) {
|
|
194
|
+
if (r.from) params.set(`${k}_from`, r.from);
|
|
195
|
+
if (r.to) params.set(`${k}_to`, r.to);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const qs = params.toString();
|
|
199
|
+
await goto(qs ? `?${qs}` : '?', { keepFocus: true, noScroll: true });
|
|
200
|
+
}
|
|
201
|
+
|
|
105
202
|
const resolvedFields: FieldDefinition<T>[] = $derived(
|
|
106
203
|
fields ?? (meta
|
|
107
204
|
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
@@ -221,6 +318,10 @@
|
|
|
221
318
|
{deletion}
|
|
222
319
|
{actions}
|
|
223
320
|
columns={resolvedColumns}
|
|
321
|
+
pagination={serverPagination}
|
|
322
|
+
{initialSort}
|
|
323
|
+
{initialFilters}
|
|
324
|
+
onPaginationChange={handlePaginationChange}
|
|
224
325
|
onCreate={navCreate}
|
|
225
326
|
onEdit={navEdit}
|
|
226
327
|
onView={navRead}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
CustomAction,
|
|
14
14
|
RowAction
|
|
15
15
|
} from '../../../types/crud.js';
|
|
16
|
+
import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../../types/table.js';
|
|
16
17
|
import { getStrings } from '../../../i18n/context.js';
|
|
17
18
|
|
|
18
19
|
const strings = getStrings();
|
|
@@ -30,6 +31,10 @@
|
|
|
30
31
|
deletion = {} as ActionConfiguration<T>,
|
|
31
32
|
actions = [] as CustomAction<T>[],
|
|
32
33
|
columns = [] as ColumnDefinition<T>[],
|
|
34
|
+
pagination = undefined as ServerPagination | undefined,
|
|
35
|
+
initialSort = undefined as { column: string; direction: SortDirection } | undefined,
|
|
36
|
+
initialFilters = undefined as Partial<FilterSnapshot> | undefined,
|
|
37
|
+
onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
|
|
33
38
|
onCreate,
|
|
34
39
|
onEdit,
|
|
35
40
|
onView,
|
|
@@ -48,6 +53,10 @@
|
|
|
48
53
|
deletion?: ActionConfiguration<T>;
|
|
49
54
|
actions?: CustomAction<T>[];
|
|
50
55
|
columns?: ColumnDefinition<T>[];
|
|
56
|
+
pagination?: ServerPagination;
|
|
57
|
+
initialSort?: { column: string; direction: SortDirection };
|
|
58
|
+
initialFilters?: Partial<FilterSnapshot>;
|
|
59
|
+
onPaginationChange?: (query: TableQuery) => void;
|
|
51
60
|
onCreate?: () => void;
|
|
52
61
|
onEdit?: (item: T) => void;
|
|
53
62
|
onView?: (item: T) => void;
|
|
@@ -68,6 +77,17 @@
|
|
|
68
77
|
let selected = new SvelteSet<number>();
|
|
69
78
|
let pendingDeletion = $state<T[] | null>(null);
|
|
70
79
|
|
|
80
|
+
// `selected` holds indices into `data`; if `data` is swapped for a different
|
|
81
|
+
// slice (page/sort/filter change in server mode) stale indices could point
|
|
82
|
+
// at unrelated rows, so clear on any data reference change.
|
|
83
|
+
let lastData: T[] | undefined;
|
|
84
|
+
$effect(() => {
|
|
85
|
+
if (data !== lastData) {
|
|
86
|
+
selected.clear();
|
|
87
|
+
lastData = data;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
71
91
|
async function runEndpointAction(endpoint: string, items: T[]) {
|
|
72
92
|
await Promise.all(items.map((item) => {
|
|
73
93
|
const fd = new FormData();
|
|
@@ -185,13 +205,17 @@
|
|
|
185
205
|
selectable={allowDelete}
|
|
186
206
|
{selected}
|
|
187
207
|
rowActions={showRowActions ? actionsCell : undefined}
|
|
208
|
+
{pagination}
|
|
209
|
+
{initialSort}
|
|
210
|
+
{initialFilters}
|
|
211
|
+
{onPaginationChange}
|
|
188
212
|
/>
|
|
189
213
|
</div>
|
|
190
214
|
</div>
|
|
191
215
|
|
|
192
216
|
{#if pendingDeletion !== null}
|
|
193
217
|
<Modal title={deleteLabel} onClose={() => (pendingDeletion = null)}>
|
|
194
|
-
<p>{strings.deleteConfirm(pendingDeletion.length)}</p>
|
|
218
|
+
<p>{strings.deleteConfirm(pendingDeletion.length, deleteLabel)}</p>
|
|
195
219
|
<div class="flex justify-end gap-2 mt-4">
|
|
196
220
|
<Button variant="ghost" onclick={() => (pendingDeletion = null)}>
|
|
197
221
|
{strings.cancel}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ActionConfiguration, ColumnDefinition, CustomAction } from '../../../types/crud.js';
|
|
2
|
+
import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../../types/table.js';
|
|
2
3
|
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
3
4
|
props: {
|
|
4
5
|
data?: T[];
|
|
@@ -13,6 +14,13 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
13
14
|
deletion?: ActionConfiguration<T>;
|
|
14
15
|
actions?: CustomAction<T>[];
|
|
15
16
|
columns?: ColumnDefinition<T>[];
|
|
17
|
+
pagination?: ServerPagination;
|
|
18
|
+
initialSort?: {
|
|
19
|
+
column: string;
|
|
20
|
+
direction: SortDirection;
|
|
21
|
+
};
|
|
22
|
+
initialFilters?: Partial<FilterSnapshot>;
|
|
23
|
+
onPaginationChange?: (query: TableQuery) => void;
|
|
16
24
|
onCreate?: () => void;
|
|
17
25
|
onEdit?: (item: T) => void;
|
|
18
26
|
onView?: (item: T) => void;
|
|
@@ -48,9 +48,11 @@
|
|
|
48
48
|
return () => calendarEl?.removeEventListener('change', handler);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
let debounceTimer: ReturnType<typeof setTimeout>;
|
|
51
52
|
function setText(value: string) {
|
|
52
53
|
filter.setText(column.attribute, value);
|
|
53
|
-
|
|
54
|
+
clearTimeout(debounceTimer);
|
|
55
|
+
debounceTimer = setTimeout(() => onchange?.(), 300);
|
|
54
56
|
}
|
|
55
57
|
function toggle(value: string) {
|
|
56
58
|
filter.toggleValue(column.attribute, value);
|
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
import TableBody from './TableBody.svelte';
|
|
4
4
|
import Paginator from './Paginator.svelte';
|
|
5
5
|
import TableHeader from './TableHeader.svelte';
|
|
6
|
-
import { SortState, FilterState } from './state.svelte.js';
|
|
7
|
-
import { distinctEntries } from './utils.js';
|
|
8
|
-
import type {
|
|
6
|
+
import { SortState, FilterState, snapshotFilter } from './state.svelte.js';
|
|
7
|
+
import { distinctEntries, isFilterable } from './utils.js';
|
|
8
|
+
import type {
|
|
9
|
+
FilterSnapshot,
|
|
10
|
+
IndexedRow,
|
|
11
|
+
ServerPagination,
|
|
12
|
+
SortDirection,
|
|
13
|
+
TableQuery,
|
|
14
|
+
} from '../../types/table.js';
|
|
9
15
|
import type { Snippet } from 'svelte';
|
|
10
16
|
import type { ColumnDefinition } from '../../types/crud.js';
|
|
11
17
|
import { getStrings } from '../../i18n/context.js';
|
|
@@ -20,6 +26,10 @@
|
|
|
20
26
|
selected = $bindable(new SvelteSet<number>()),
|
|
21
27
|
rowActions = undefined as Snippet<[T]> | undefined,
|
|
22
28
|
actionsLabel = strings.actions,
|
|
29
|
+
pagination = undefined as ServerPagination | undefined,
|
|
30
|
+
initialSort = undefined as { column: string; direction: SortDirection } | undefined,
|
|
31
|
+
initialFilters = undefined as Partial<FilterSnapshot> | undefined,
|
|
32
|
+
onPaginationChange = undefined as ((query: TableQuery) => void) | undefined,
|
|
23
33
|
}: {
|
|
24
34
|
data?: T[];
|
|
25
35
|
columns?: ColumnDefinition<T>[];
|
|
@@ -28,31 +38,88 @@
|
|
|
28
38
|
selected?: SvelteSet<number>;
|
|
29
39
|
rowActions?: Snippet<[T]>;
|
|
30
40
|
actionsLabel?: string;
|
|
41
|
+
/** When provided, the table trusts `data` is already the requested page and
|
|
42
|
+
* defers pagination/sort/filter to `onPaginationChange` instead of computing
|
|
43
|
+
* them locally. Omit for the original fully-client-side behavior. */
|
|
44
|
+
pagination?: ServerPagination;
|
|
45
|
+
initialSort?: { column: string; direction: SortDirection };
|
|
46
|
+
initialFilters?: Partial<FilterSnapshot>;
|
|
47
|
+
onPaginationChange?: (query: TableQuery) => void;
|
|
31
48
|
} = $props();
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
// Intentional one-time hydration of local state from the initial prop
|
|
51
|
+
// values (not a live binding) — `svelte-check`'s state_referenced_locally
|
|
52
|
+
// warning is a false positive here.
|
|
53
|
+
const sort = new SortState(initialSort ?? null);
|
|
54
|
+
const filter = new FilterState(initialFilters ?? null);
|
|
35
55
|
|
|
36
|
-
let currentPage = $state(1);
|
|
56
|
+
let currentPage = $state(pagination?.page ?? 1);
|
|
57
|
+
let lastKnownPage = pagination?.page ?? 1;
|
|
37
58
|
|
|
38
|
-
const distinctValues = $derived(
|
|
59
|
+
const distinctValues = $derived(
|
|
60
|
+
pagination
|
|
61
|
+
? Object.fromEntries(
|
|
62
|
+
columns
|
|
63
|
+
.filter((c) => isFilterable(c) && c.type === 'boolean')
|
|
64
|
+
.map((c) => [
|
|
65
|
+
c.attribute,
|
|
66
|
+
[
|
|
67
|
+
{ key: 'true', row: {} as T },
|
|
68
|
+
{ key: 'false', row: {} as T },
|
|
69
|
+
],
|
|
70
|
+
]),
|
|
71
|
+
)
|
|
72
|
+
: distinctEntries(data, columns)
|
|
73
|
+
);
|
|
39
74
|
|
|
40
75
|
const indexed = $derived(data.map((row, index): IndexedRow<T> => ({ row, index })));
|
|
41
|
-
const filtered = $derived(indexed.filter(({ row }) => filter.matches(row, columns)));
|
|
42
|
-
const sorted = $derived(sort.apply(filtered, columns));
|
|
76
|
+
const filtered = $derived(pagination ? indexed : indexed.filter(({ row }) => filter.matches(row, columns)));
|
|
77
|
+
const sorted = $derived(pagination ? filtered : sort.apply(filtered, columns));
|
|
43
78
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
79
|
+
const effectivePageSize = $derived(pagination?.pageSize ?? pageSize);
|
|
80
|
+
const totalPages = $derived(pagination?.totalPages ?? Math.ceil(sorted.length / effectivePageSize));
|
|
81
|
+
const displayPage = $derived(pagination?.page ?? currentPage);
|
|
82
|
+
const pageStart = $derived((displayPage - 1) * effectivePageSize);
|
|
83
|
+
const pageData = $derived(pagination ? sorted : sorted.slice(pageStart, pageStart + effectivePageSize));
|
|
84
|
+
const totalCount = $derived(pagination?.total ?? sorted.length);
|
|
47
85
|
const allChecked = $derived(pageData.length > 0 && pageData.every((e) => selected.has(e.index)));
|
|
48
86
|
const someChecked = $derived(pageData.some((e) => selected.has(e.index)));
|
|
49
87
|
|
|
88
|
+
// Client mode only: server mode's totalPages is externally owned, clamping
|
|
89
|
+
// here would fight with URL-driven navigation while a page reload is pending.
|
|
50
90
|
$effect(() => {
|
|
51
|
-
if (currentPage > totalPages && totalPages > 0) currentPage = totalPages;
|
|
91
|
+
if (!pagination && currentPage > totalPages && totalPages > 0) currentPage = totalPages;
|
|
52
92
|
});
|
|
53
93
|
|
|
54
|
-
|
|
94
|
+
// Server mode: external (URL/reload) page changes -> sync local state.
|
|
95
|
+
$effect(() => {
|
|
96
|
+
if (pagination && pagination.page !== lastKnownPage) {
|
|
97
|
+
currentPage = pagination.page;
|
|
98
|
+
lastKnownPage = pagination.page;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Server mode: local (Paginator click) page changes -> notify caller.
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (pagination && currentPage !== lastKnownPage) {
|
|
105
|
+
lastKnownPage = currentPage;
|
|
106
|
+
onPaginationChange?.(currentQuery(currentPage));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
function currentQuery(page: number): TableQuery {
|
|
111
|
+
return {
|
|
112
|
+
page,
|
|
113
|
+
ordering: sort.column ? (sort.direction === 'asc' ? sort.column : `-${sort.column}`) : null,
|
|
114
|
+
filters: snapshotFilter(filter),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleHeaderChange() {
|
|
55
119
|
currentPage = 1;
|
|
120
|
+
if (!pagination) return;
|
|
121
|
+
lastKnownPage = 1;
|
|
122
|
+
onPaginationChange?.(currentQuery(1));
|
|
56
123
|
}
|
|
57
124
|
|
|
58
125
|
function toggleAll() {
|
|
@@ -82,7 +149,7 @@
|
|
|
82
149
|
{distinctValues}
|
|
83
150
|
hasRowActions={!!rowActions}
|
|
84
151
|
{actionsLabel}
|
|
85
|
-
onchange={
|
|
152
|
+
onchange={handleHeaderChange}
|
|
86
153
|
/>
|
|
87
154
|
<TableBody
|
|
88
155
|
{columns}
|
|
@@ -100,7 +167,7 @@
|
|
|
100
167
|
bind:page={currentPage}
|
|
101
168
|
{totalPages}
|
|
102
169
|
{pageStart}
|
|
103
|
-
{
|
|
104
|
-
total={
|
|
170
|
+
pageSize={effectivePageSize}
|
|
171
|
+
total={totalCount}
|
|
105
172
|
/>
|
|
106
173
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SvelteSet } from 'svelte/reactivity';
|
|
2
|
+
import type { FilterSnapshot, ServerPagination, SortDirection, TableQuery } from '../../types/table.js';
|
|
2
3
|
import type { Snippet } from 'svelte';
|
|
3
4
|
import type { ColumnDefinition } from '../../types/crud.js';
|
|
4
5
|
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
@@ -10,6 +11,16 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
10
11
|
selected?: SvelteSet<number>;
|
|
11
12
|
rowActions?: Snippet<[T]>;
|
|
12
13
|
actionsLabel?: string;
|
|
14
|
+
/** When provided, the table trusts `data` is already the requested page and
|
|
15
|
+
* defers pagination/sort/filter to `onPaginationChange` instead of computing
|
|
16
|
+
* them locally. Omit for the original fully-client-side behavior. */
|
|
17
|
+
pagination?: ServerPagination;
|
|
18
|
+
initialSort?: {
|
|
19
|
+
column: string;
|
|
20
|
+
direction: SortDirection;
|
|
21
|
+
};
|
|
22
|
+
initialFilters?: Partial<FilterSnapshot>;
|
|
23
|
+
onPaginationChange?: (query: TableQuery) => void;
|
|
13
24
|
};
|
|
14
25
|
exports: {};
|
|
15
26
|
bindings: "selected";
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
2
2
|
import type { ColumnDefinition } from '../../types/crud.js';
|
|
3
|
-
import type { IndexedRow, SortDirection } from '../../types/table.js';
|
|
3
|
+
import type { FilterSnapshot, IndexedRow, SortDirection } from '../../types/table.js';
|
|
4
4
|
export declare class SortState {
|
|
5
5
|
column: string | null;
|
|
6
6
|
direction: SortDirection | null;
|
|
7
|
+
constructor(initial?: {
|
|
8
|
+
column: string | null;
|
|
9
|
+
direction: SortDirection | null;
|
|
10
|
+
} | null);
|
|
7
11
|
directionFor(attribute: string): SortDirection | null;
|
|
8
12
|
cycle(attribute: string): void;
|
|
9
13
|
apply<T extends object>(rows: IndexedRow<T>[], columns: ColumnDefinition<T>[]): IndexedRow<T>[];
|
|
@@ -15,6 +19,7 @@ export declare class FilterState {
|
|
|
15
19
|
from: string;
|
|
16
20
|
to: string;
|
|
17
21
|
}>;
|
|
22
|
+
constructor(initial?: Partial<FilterSnapshot> | null);
|
|
18
23
|
textFor(attribute: string): string;
|
|
19
24
|
dateRangeFor(attribute: string): {
|
|
20
25
|
from: string;
|
|
@@ -28,3 +33,4 @@ export declare class FilterState {
|
|
|
28
33
|
clear(attribute: string): void;
|
|
29
34
|
matches<T extends object>(row: T, columns: ColumnDefinition<T>[]): boolean;
|
|
30
35
|
}
|
|
36
|
+
export declare function snapshotFilter(filter: FilterState): FilterSnapshot;
|
|
@@ -3,6 +3,12 @@ import { cellRenderedText, compare, isFilterable } from './utils.js';
|
|
|
3
3
|
export class SortState {
|
|
4
4
|
column = $state(null);
|
|
5
5
|
direction = $state(null);
|
|
6
|
+
constructor(initial) {
|
|
7
|
+
if (initial?.column) {
|
|
8
|
+
this.column = initial.column;
|
|
9
|
+
this.direction = initial.direction ?? 'desc';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
6
12
|
directionFor(attribute) {
|
|
7
13
|
return this.column === attribute ? this.direction : null;
|
|
8
14
|
}
|
|
@@ -36,6 +42,15 @@ export class FilterState {
|
|
|
36
42
|
text = new SvelteMap();
|
|
37
43
|
values = new SvelteMap();
|
|
38
44
|
dateRanges = new SvelteMap();
|
|
45
|
+
constructor(initial) {
|
|
46
|
+
for (const [k, v] of Object.entries(initial?.text ?? {}))
|
|
47
|
+
this.text.set(k, v);
|
|
48
|
+
for (const [k, v] of Object.entries(initial?.values ?? {})) {
|
|
49
|
+
this.values.set(k, new SvelteSet(v));
|
|
50
|
+
}
|
|
51
|
+
for (const [k, v] of Object.entries(initial?.dateRanges ?? {}))
|
|
52
|
+
this.dateRanges.set(k, v);
|
|
53
|
+
}
|
|
39
54
|
textFor(attribute) {
|
|
40
55
|
return this.text.get(attribute) ?? '';
|
|
41
56
|
}
|
|
@@ -107,3 +122,10 @@ export class FilterState {
|
|
|
107
122
|
});
|
|
108
123
|
}
|
|
109
124
|
}
|
|
125
|
+
export function snapshotFilter(filter) {
|
|
126
|
+
return {
|
|
127
|
+
text: Object.fromEntries(filter.text),
|
|
128
|
+
values: Object.fromEntries([...filter.values].map(([k, set]) => [k, [...set]])),
|
|
129
|
+
dateRanges: Object.fromEntries(filter.dateRanges),
|
|
130
|
+
};
|
|
131
|
+
}
|
package/dist/i18n/en.js
CHANGED
|
@@ -20,7 +20,7 @@ export const en = {
|
|
|
20
20
|
cancel: 'Cancel',
|
|
21
21
|
back: 'Back',
|
|
22
22
|
confirm: 'Confirm',
|
|
23
|
-
deleteConfirm: (count) => `Are you sure you want to
|
|
23
|
+
deleteConfirm: (count, actionLabel) => `Are you sure you want to ${String(actionLabel).toLowerCase()} ${count} item${count === 1 ? '' : 's'}?`,
|
|
24
24
|
required: (field) => `${field} is required`,
|
|
25
25
|
serverError: 'Unexpected server error.',
|
|
26
26
|
};
|
package/dist/i18n/es.js
CHANGED
|
@@ -20,7 +20,7 @@ export const es = {
|
|
|
20
20
|
cancel: 'Cancelar',
|
|
21
21
|
back: 'Volver',
|
|
22
22
|
confirm: 'Confirmar',
|
|
23
|
-
deleteConfirm: (count) => `¿Seguro que querés
|
|
23
|
+
deleteConfirm: (count, actionLabel) => `¿Seguro que querés ${String(actionLabel).toLowerCase()} ${count} elemento${count === 1 ? '' : 's'}?`,
|
|
24
24
|
required: (field) => `${field} es requerido`,
|
|
25
25
|
serverError: 'Error inesperado del servidor.',
|
|
26
26
|
};
|
package/dist/i18n/types.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export interface RuneforgeStrings {
|
|
|
20
20
|
cancel: string;
|
|
21
21
|
back: string;
|
|
22
22
|
confirm: string;
|
|
23
|
-
deleteConfirm: (count: number) => string;
|
|
23
|
+
deleteConfirm: (count: number, actionLabel: string) => string;
|
|
24
24
|
required: (field: string) => string;
|
|
25
25
|
serverError: string;
|
|
26
26
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type { BreadcrumbItem } from './types/breadcrumb.js';
|
|
2
2
|
export { AttributeType } from './types/attribute.js';
|
|
3
3
|
export type { AttributeMetadata, InterfaceMetadata, SelectOption, OptionsResolver, FormatterResolver } from './types/attribute.js';
|
|
4
|
-
export type { CellProps, CellComponent, CellFormatter, SortDirection, IndexedRow, DistinctEntry } from './types/table.js';
|
|
4
|
+
export type { CellProps, CellComponent, CellFormatter, SortDirection, IndexedRow, DistinctEntry, PaginatedEnvelope, ServerPagination, FilterSnapshot, TableQuery } from './types/table.js';
|
|
5
5
|
export type { ColumnDefinition, FieldDefinition, ActionConfiguration, CustomAction, RowAction } from './types/crud.js';
|
|
6
6
|
export type { RuneforgeConfig } from './config/context.js';
|
|
7
7
|
export { setConfig, getConfig } from './config/context.js';
|
|
@@ -28,7 +28,7 @@ export { default as TableBody } from './components/table/TableBody.svelte';
|
|
|
28
28
|
export { default as SortHeader } from './components/table/SortHeader.svelte';
|
|
29
29
|
export { default as Paginator } from './components/table/Paginator.svelte';
|
|
30
30
|
export { default as ColumnFilter } from './components/table/ColumnFilter.svelte';
|
|
31
|
-
export { SortState, FilterState } from './components/table/state.svelte.js';
|
|
31
|
+
export { SortState, FilterState, snapshotFilter } from './components/table/state.svelte.js';
|
|
32
32
|
export { cellRenderedText, isSortable, isFilterable, compare, distinctEntries } from './components/table/utils.js';
|
|
33
33
|
export { default as GenericCRUD } from './components/crud/GenericCRUD.svelte';
|
|
34
34
|
export { default as Field } from './components/crud/Field.svelte';
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export { default as TableBody } from './components/table/TableBody.svelte';
|
|
|
24
24
|
export { default as SortHeader } from './components/table/SortHeader.svelte';
|
|
25
25
|
export { default as Paginator } from './components/table/Paginator.svelte';
|
|
26
26
|
export { default as ColumnFilter } from './components/table/ColumnFilter.svelte';
|
|
27
|
-
export { SortState, FilterState } from './components/table/state.svelte.js';
|
|
27
|
+
export { SortState, FilterState, snapshotFilter } from './components/table/state.svelte.js';
|
|
28
28
|
export { cellRenderedText, isSortable, isFilterable, compare, distinctEntries } from './components/table/utils.js';
|
|
29
29
|
// CRUD components
|
|
30
30
|
export { default as GenericCRUD } from './components/crud/GenericCRUD.svelte';
|
package/dist/types/table.d.ts
CHANGED
|
@@ -14,3 +14,28 @@ export interface CellProps<T extends object = Record<string, unknown>, V = unkno
|
|
|
14
14
|
}
|
|
15
15
|
export type CellComponent<T extends object = Record<string, unknown>, V = unknown> = Component<CellProps<T, V>>;
|
|
16
16
|
export type CellFormatter<T extends object = Record<string, unknown>, V = unknown> = (value: CellProps<T, V>['value'], row: CellProps<T, V>['row']) => string;
|
|
17
|
+
export interface PaginatedEnvelope<T> {
|
|
18
|
+
results: T[];
|
|
19
|
+
count: number;
|
|
20
|
+
page: number;
|
|
21
|
+
pageSize: number;
|
|
22
|
+
}
|
|
23
|
+
export interface ServerPagination {
|
|
24
|
+
page: number;
|
|
25
|
+
totalPages: number;
|
|
26
|
+
total: number;
|
|
27
|
+
pageSize: number;
|
|
28
|
+
}
|
|
29
|
+
export interface FilterSnapshot {
|
|
30
|
+
text: Record<string, string>;
|
|
31
|
+
values: Record<string, string[]>;
|
|
32
|
+
dateRanges: Record<string, {
|
|
33
|
+
from: string;
|
|
34
|
+
to: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
export interface TableQuery {
|
|
38
|
+
page: number;
|
|
39
|
+
ordering: string | null;
|
|
40
|
+
filters: FilterSnapshot;
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,81 +1,79 @@
|
|
|
1
1
|
{
|
|
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
|
-
}
|
|
2
|
+
"name": "runeforge",
|
|
3
|
+
"version": "0.0.13",
|
|
4
|
+
"description": "SvelteKit toolkit for building metadata-driven CRUD interfaces with tables, forms, and actions",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ezequiel Puerta",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"svelte",
|
|
9
|
+
"sveltekit",
|
|
10
|
+
"crud",
|
|
11
|
+
"table",
|
|
12
|
+
"form",
|
|
13
|
+
"daisyui",
|
|
14
|
+
"tailwindcss",
|
|
15
|
+
"ui"
|
|
16
|
+
],
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"!dist/**/*.test.*",
|
|
20
|
+
"!dist/**/*.spec.*"
|
|
21
|
+
],
|
|
22
|
+
"sideEffects": [
|
|
23
|
+
"**/*.css"
|
|
24
|
+
],
|
|
25
|
+
"svelte": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"type": "module",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"svelte": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@sveltejs/kit": "^2.0.0",
|
|
36
|
+
"daisyui": "^5.0.0",
|
|
37
|
+
"svelte": "^5.0.0",
|
|
38
|
+
"tailwindcss": "^4.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@eslint/js": "^10.0.1",
|
|
42
|
+
"@playwright/test": "^1.60.0",
|
|
43
|
+
"@sveltejs/adapter-auto": "^7.0.1",
|
|
44
|
+
"@sveltejs/kit": "^2.63.0",
|
|
45
|
+
"@sveltejs/package": "^2.5.8",
|
|
46
|
+
"@sveltejs/vite-plugin-svelte": "^7.1.2",
|
|
47
|
+
"@tailwindcss/vite": "^4.3.1",
|
|
48
|
+
"@types/node": "^22",
|
|
49
|
+
"daisyui": "^5.5.23",
|
|
50
|
+
"eslint": "^10.4.1",
|
|
51
|
+
"eslint-config-prettier": "^10.1.8",
|
|
52
|
+
"eslint-plugin-svelte": "^3.19.0",
|
|
53
|
+
"globals": "^17.6.0",
|
|
54
|
+
"prettier": "^3.8.3",
|
|
55
|
+
"prettier-plugin-svelte": "^4.1.0",
|
|
56
|
+
"publint": "^0.3.21",
|
|
57
|
+
"svelte": "^5.56.1",
|
|
58
|
+
"svelte-bootstrap-icons": "^3.3.0",
|
|
59
|
+
"svelte-check": "^4.6.0",
|
|
60
|
+
"tailwindcss": "^4.3.1",
|
|
61
|
+
"typescript": "^6.0.3",
|
|
62
|
+
"typescript-eslint": "^8.60.1",
|
|
63
|
+
"vite": "^8.0.16",
|
|
64
|
+
"vitest": "^4.1.8"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"dev": "vite dev",
|
|
68
|
+
"build": "vite build && npm run prepack",
|
|
69
|
+
"preview": "vite preview",
|
|
70
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
71
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
72
|
+
"test": "pnpm run test:unit && pnpm run test:e2e",
|
|
73
|
+
"test:unit": "vitest run",
|
|
74
|
+
"test:unit:watch": "vitest",
|
|
75
|
+
"test:e2e": "playwright test",
|
|
76
|
+
"lint": "prettier --check . && eslint .",
|
|
77
|
+
"format": "prettier --write ."
|
|
78
|
+
}
|
|
79
|
+
}
|