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
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ A SvelteKit toolkit that forges forms, tables, actions, and CRUD workflows from
|
|
|
46
46
|
- [Custom row actions](#custom-row-actions)
|
|
47
47
|
- [Custom bulk actions](#custom-bulk-actions)
|
|
48
48
|
- [Exporting data (CSV/XLSX)](#exporting-data-csvxlsx)
|
|
49
|
+
- [Reordering rows](#reordering-rows)
|
|
49
50
|
- [Server-side pagination, sorting & filtering](#server-side-pagination-sorting--filtering)
|
|
50
51
|
- [PaginatedTable](#paginatedtable)
|
|
51
52
|
- [Form Components](#form-components)
|
|
@@ -86,6 +87,7 @@ Runeforge provides a set of composable, metadata-driven components for building
|
|
|
86
87
|
- Tailwind CSS 4
|
|
87
88
|
- DaisyUI 5
|
|
88
89
|
- Cally
|
|
90
|
+
- `sortablejs` (optional, only if you enable [drag-to-reorder](#reordering-rows))
|
|
89
91
|
- `xlsx` (optional, only if you enable Excel export)
|
|
90
92
|
|
|
91
93
|
---
|
|
@@ -102,6 +104,7 @@ Runeforge provides a set of composable, metadata-driven components for building
|
|
|
102
104
|
- **Embedded fields** — model one-to-many sub-documents (e.g. line items, adjustments) as an in-form add/edit list backed by a single JSON field.
|
|
103
105
|
- **Custom row & bulk actions** — add entity-specific actions (in a panel or via redirect) alongside the built-in view/edit/delete, and bulk actions that operate on the current selection.
|
|
104
106
|
- **CSV/XLSX export** — one-click export of the current table view, with optional Excel support via the `xlsx` package.
|
|
107
|
+
- **Drag-to-reorder** — an opt-in row-reordering layer that persists a sequential order attribute after each drag.
|
|
105
108
|
- **Server-side pagination, sorting & filtering** — point `GenericCRUD`/`PaginatedTable` at a paginated envelope and it drives page/sort/filter state through the URL for you.
|
|
106
109
|
- **Pluggable icon system** — swap the default icon set or use the included Bootstrap Icons alternative via `setIconSet`.
|
|
107
110
|
- **Standalone components** — table, form, and navigation components can be used independently without the full CRUD orchestrator.
|
|
@@ -697,19 +700,22 @@ Key props:
|
|
|
697
700
|
- `columns` — `ColumnDefinition[]` for the table view
|
|
698
701
|
- `fields` — `FieldDefinition[]` for form views
|
|
699
702
|
- `creation`, `update`, `read`, `deletion` — `ActionConfiguration` objects that define handlers and permissions for each operation. Set `confirm: true` on `deletion` to show a confirmation dialog before any delete (single row or batch)
|
|
700
|
-
- `actions` — `
|
|
701
|
-
- `
|
|
702
|
-
- `
|
|
703
|
-
- `
|
|
703
|
+
- `actions` — a `ListActions` object grouping the list view's extra actions:
|
|
704
|
+
- `actions.custom` — `CustomAction[]`, extra per-row actions — see [Custom row actions](#custom-row-actions)
|
|
705
|
+
- `actions.bulk` — `CustomBulkAction[]`, extra actions on the current selection — see [Custom bulk actions](#custom-bulk-actions)
|
|
706
|
+
- `config` — a `ListConfig` object grouping the list view's opt-in behaviors:
|
|
707
|
+
- `config.search` — `SearchConfiguration`, shows a free-text search box — see [Free-text search](#free-text-search)
|
|
708
|
+
- `config.export` — `ExportConfiguration`, enables CSV/Excel export — see [Exporting data](#exporting-data-csvxlsx)
|
|
709
|
+
- `config.reorder` — `ReorderConfiguration`, enables drag-to-reorder rows — see [Reordering rows](#reordering-rows)
|
|
704
710
|
|
|
705
711
|
#### Free-text search
|
|
706
712
|
|
|
707
|
-
Passing `search` renders a debounced search box in the header. Typing updates a URL search param (`?search=...` by default), resets pagination and any open create/read/edit view, and leaves interpreting the term entirely to your `load` function — it's the same mechanism server-side pagination uses, so it composes naturally with it.
|
|
713
|
+
Passing `config.search` renders a debounced search box in the header. Typing updates a URL search param (`?search=...` by default), resets pagination and any open create/read/edit view, and leaves interpreting the term entirely to your `load` function — it's the same mechanism server-side pagination uses, so it composes naturally with it.
|
|
708
714
|
|
|
709
715
|
```ts
|
|
710
716
|
<GenericCRUD
|
|
711
717
|
...
|
|
712
|
-
|
|
718
|
+
config={{ search: { param: 'q', placeholder: 'Search tasks...', debounceMs: 300 } }}
|
|
713
719
|
/>
|
|
714
720
|
```
|
|
715
721
|
|
|
@@ -721,7 +727,7 @@ Passing `search` renders a debounced search box in the header. Typing updates a
|
|
|
721
727
|
|
|
722
728
|
#### Custom row actions
|
|
723
729
|
|
|
724
|
-
`actions` adds entries to the per-row action menu, alongside the built-in view/edit/delete. Each `CustomAction` resolves in one of two ways — provide exactly one of `view` or `href`:
|
|
730
|
+
`actions.custom` adds entries to the per-row action menu, alongside the built-in view/edit/delete. Each `CustomAction` resolves in one of two ways — provide exactly one of `view` or `href`:
|
|
725
731
|
|
|
726
732
|
- `href(item)` — plain navigation, e.g. deep-linking into another CRUD's filtered list.
|
|
727
733
|
- `view` — a Svelte component of your own that `GenericCRUD` mounts directly (no wrapper) when the action runs. Since you own the whole component, you decide how it presents itself — typically as a modal built on the exported `Modal` component, sized however that action needs via `Modal`'s `class`/`width`/`maxWidth`/`height`/`maxHeight` props (see [Shared Components](#shared-components)).
|
|
@@ -746,13 +752,13 @@ const actions: CustomAction<IWidget>[] = [
|
|
|
746
752
|
];
|
|
747
753
|
```
|
|
748
754
|
|
|
749
|
-
```
|
|
750
|
-
<GenericCRUD ... {actions} />
|
|
755
|
+
```ts
|
|
756
|
+
<GenericCRUD ... actions={{ custom: actions }} />
|
|
751
757
|
```
|
|
752
758
|
|
|
753
759
|
A `view` component receives `instance`, `label`, `endpoint`, `serverError`, `onCancel`, and `onSuccess` — the same shape Create/Update use internally — so it can reuse `enhance`-based form submission while rendering as a parametrized modal:
|
|
754
760
|
|
|
755
|
-
```
|
|
761
|
+
```ts
|
|
756
762
|
<!-- ArchiveForm.svelte -->
|
|
757
763
|
<script lang="ts">
|
|
758
764
|
import { enhance } from '$app/forms';
|
|
@@ -782,15 +788,17 @@ A `view` component receives `instance`, `label`, `endpoint`, `serverError`, `onC
|
|
|
782
788
|
|
|
783
789
|
#### Custom bulk actions
|
|
784
790
|
|
|
785
|
-
`
|
|
791
|
+
`actions.bulk` adds buttons next to the built-in Delete button in the header, operating on the current row selection. Each one is disabled until at least one row is selected, and (like deletion) can require confirmation.
|
|
786
792
|
|
|
787
793
|
```ts
|
|
788
794
|
<GenericCRUD
|
|
789
795
|
...
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
796
|
+
actions={{
|
|
797
|
+
bulk: [
|
|
798
|
+
{ kind: 'endpoint', label: 'Complete', icon: CheckIcon, endpoint: '?/complete' },
|
|
799
|
+
{ kind: 'endpoint', label: 'Mark pending', icon: UndoIcon, endpoint: '?/incomplete', variant: 'error', confirm: true },
|
|
800
|
+
],
|
|
801
|
+
}}
|
|
794
802
|
/>
|
|
795
803
|
```
|
|
796
804
|
|
|
@@ -798,7 +806,7 @@ A `view` component receives `instance`, `label`, `endpoint`, `serverError`, `onC
|
|
|
798
806
|
|
|
799
807
|
#### Exporting data (CSV/XLSX)
|
|
800
808
|
|
|
801
|
-
`
|
|
809
|
+
`config.export` adds an export button to the header offering CSV (always) and Excel (when a `xlsx` module is supplied). Its mere presence enables the button — pass `{}` for CSV-only export. Runeforge never bundles `xlsx` itself — install it separately and pass the resolved module in, so the dependency stays fully optional:
|
|
802
810
|
|
|
803
811
|
```bash
|
|
804
812
|
pnpm add xlsx
|
|
@@ -810,25 +818,116 @@ pnpm add xlsx
|
|
|
810
818
|
import * as xlsx from 'xlsx';
|
|
811
819
|
</script>
|
|
812
820
|
|
|
813
|
-
<GenericCRUD ...
|
|
821
|
+
<GenericCRUD ... config={{ export: { xlsx } }} />
|
|
814
822
|
```
|
|
815
823
|
|
|
816
|
-
In client-pagination mode, export includes every row currently matching the table's filters (not just the visible page). In [server-pagination mode](#server-side-pagination-sorting--filtering), pass `
|
|
824
|
+
In client-pagination mode, export includes every row currently matching the table's filters (not just the visible page). In [server-pagination mode](#server-side-pagination-sorting--filtering), pass `config.export.callback` to fetch the full, unpaginated result set for the current query — without it, export falls back to just the currently loaded page:
|
|
817
825
|
|
|
818
826
|
```ts
|
|
819
827
|
<GenericCRUD
|
|
820
828
|
...
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
829
|
+
config={{
|
|
830
|
+
export: {
|
|
831
|
+
callback: async (query) => {
|
|
832
|
+
const params = new URLSearchParams();
|
|
833
|
+
if (query.ordering) params.set('ordering', query.ordering);
|
|
834
|
+
// ...translate query.filters into your API's params
|
|
835
|
+
const res = await fetch(`/api/widgets/export?${params}`);
|
|
836
|
+
return res.json();
|
|
837
|
+
},
|
|
838
|
+
},
|
|
828
839
|
}}
|
|
829
840
|
/>
|
|
830
841
|
```
|
|
831
842
|
|
|
843
|
+
#### Reordering rows
|
|
844
|
+
|
|
845
|
+
`config.reorder` turns on drag-to-reorder: each row gets a drag handle (⋮⋮ by default — pass `icon` for something else, e.g. a hamburger or a grab-hand icon) with a thicker left border, and dragging a row persists a new sequential value for whichever attribute you point it at. Off by default; only appears once `config.reorder` is set.
|
|
846
|
+
|
|
847
|
+
Runeforge never imports `sortablejs` itself — install it separately and pass the resolved default export in via `sortable`, the same way `xlsx` works for export, so the dependency stays fully optional:
|
|
848
|
+
|
|
849
|
+
```bash
|
|
850
|
+
pnpm add sortablejs
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
<script>
|
|
855
|
+
import { GenericCRUD } from 'runeforge';
|
|
856
|
+
import Sortable from 'sortablejs';
|
|
857
|
+
</script>
|
|
858
|
+
|
|
859
|
+
<GenericCRUD
|
|
860
|
+
...
|
|
861
|
+
config={{
|
|
862
|
+
reorder: { attribute: 'order', sortable: Sortable, endpoint: '?/reorder' },
|
|
863
|
+
}}
|
|
864
|
+
/>
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
```ts
|
|
868
|
+
// +page.server.ts
|
|
869
|
+
export const actions: Actions = {
|
|
870
|
+
reorder: async ({ request }) => {
|
|
871
|
+
const data = await request.formData();
|
|
872
|
+
const id = String(data.get('id') ?? '');
|
|
873
|
+
const order = Number(data.get('order') ?? 0);
|
|
874
|
+
await setWidgetOrder(id, order);
|
|
875
|
+
return { success: true };
|
|
876
|
+
},
|
|
877
|
+
};
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
`endpoint` is `POST`ed once per row whose `attribute` value actually changed (FormData: `id` plus the attribute, e.g. `order`) after a drag, then the list refreshes — the same convention `deletion`/bulk actions use. Pass `callback` instead to handle the changed rows yourself (each one already carries its new attribute value):
|
|
881
|
+
|
|
882
|
+
```ts
|
|
883
|
+
config={{
|
|
884
|
+
reorder: {
|
|
885
|
+
attribute: 'order',
|
|
886
|
+
sortable: Sortable,
|
|
887
|
+
callback: async (items) => {
|
|
888
|
+
await Promise.all(items.map((item) => api.updateWidget(item._id, { order: item.order })));
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
}}
|
|
892
|
+
```
|
|
893
|
+
|
|
894
|
+
A few things worth knowing about how reorder mode behaves:
|
|
895
|
+
|
|
896
|
+
- **The list is ordered by `attribute` ascending (or `compare`, see below) the whole time reorder is active** — not by `_id`, not by whatever order the backend/array happens to return, and not by clicking a column header either: column-header sorting is unavailable while `config.reorder` is set, since row order needs a single, unambiguous source of truth for dragging to mean anything. This holds even if `attribute` is `excludedFromList` and has no column of its own — ordering reads the raw row value regardless of what's rendered. Per-column filters are unavailable for the same reason (a filtered-out row's position would become undefined).
|
|
897
|
+
- **Pagination stays on.** To move a row across a page boundary, drag it to the narrow zone at either edge of the table and hold — after `pageFlipThresholdMs` (default `2000`) it flips to the previous/next page; keep hovering (without letting go) to flip again. Drop once you're on the right page.
|
|
898
|
+
- It's **not supported in [server-pagination mode](#server-side-pagination-sorting--filtering)** (a `PaginatedEnvelope` `data`) — cross-page drag positions aren't meaningful without a lot more server-side machinery, so `config.reorder` is ignored whenever server pagination is active.
|
|
899
|
+
- Set `enabled: false` to keep the configuration in place (attribute, endpoint, icon) but temporarily turn dragging off, without having to remove the whole object.
|
|
900
|
+
|
|
901
|
+
**Composite orders.** Sometimes the attribute that stores a row's position isn't unique on its own — e.g. an indicator's `order` only makes sense *within* its parent chapter, so two indicators in different chapters can share the same `order` value, and the true display order is really (chapter's order, indicator's own order). Pass `compare` to take full control of that ordering instead of the plain `attribute`-ascending default:
|
|
902
|
+
|
|
903
|
+
```ts
|
|
904
|
+
config={{
|
|
905
|
+
reorder: {
|
|
906
|
+
attribute: 'order',
|
|
907
|
+
sortable: Sortable,
|
|
908
|
+
compare: (a, b) => a.chapter.order - b.chapter.order || a.order - b.order,
|
|
909
|
+
endpoint: '?/reorder',
|
|
910
|
+
},
|
|
911
|
+
}}
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
`attribute` is still what gets written back after a drag (as a plain sequential 0-based index across the whole reordered list) — `compare` only decides how rows are displayed and dragged. If a naive global renumbering doesn't fit your data model (as in the chapter example — you don't want every indicator in every chapter renumbered whenever one indicator moves within its own chapter), use `callback` instead of `endpoint` and remap the incoming rows' indices to whatever scoped scheme your backend actually expects before saving.
|
|
915
|
+
|
|
916
|
+
**Multi-select drag.** Set `multiDrag: true` to let dragging one row of the current checkbox selection carry the whole selection along with it, via SortableJS's `MultiDrag` plugin — mount it on the module you pass in:
|
|
917
|
+
|
|
918
|
+
```ts
|
|
919
|
+
import Sortable, { MultiDrag } from 'sortablejs';
|
|
920
|
+
Sortable.mount(new MultiDrag());
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
```ts
|
|
924
|
+
config={{
|
|
925
|
+
reorder: { attribute: 'order', sortable: Sortable, multiDrag: true, endpoint: '?/reorder' },
|
|
926
|
+
}}
|
|
927
|
+
```
|
|
928
|
+
|
|
929
|
+
The existing row-selection checkboxes *are* the multi-drag selection — there's no separate ctrl/cmd-click UI to learn. Note that `multiDrag` uses native HTML5 drag-and-drop rather than the mouse-simulated dragging the rest of reorder mode uses (SortableJS's `MultiDrag` plugin needs it to track a multi-row drag correctly) — runeforge switches automatically, but it's worth knowing if you're scripting drags for tests.
|
|
930
|
+
|
|
832
931
|
#### Server-side pagination, sorting & filtering
|
|
833
932
|
|
|
834
933
|
By default, `GenericCRUD` and `PaginatedTable` paginate, sort, and filter the full `data` array in the browser. For large datasets, return a `PaginatedEnvelope<T>` from your `load` function instead — `{ results, count, page, pageSize }` — and Runeforge switches to server mode automatically: it drives `page`, `ordering`, and per-column filter values through the URL, and expects your `load` function to read them back.
|
|
@@ -862,7 +961,7 @@ export const load: PageServerLoad = ({ url }) => {
|
|
|
862
961
|
/>
|
|
863
962
|
```
|
|
864
963
|
|
|
865
|
-
No other prop changes are needed — column sorting/filtering UI, the paginator, and (with `
|
|
964
|
+
No other prop changes are needed — column sorting/filtering UI, the paginator, and (with `config.export.callback`) export all keep working the same way, just backed by the server instead of the in-memory array. Boolean-column filters send comma-separated values (`?active=true,false`); date-range filters send `<attribute>_from`/`<attribute>_to`.
|
|
866
965
|
|
|
867
966
|
### PaginatedTable
|
|
868
967
|
|
|
@@ -878,6 +977,8 @@ A standalone table component with built-in sort, filter, and pagination — the
|
|
|
878
977
|
|
|
879
978
|
Sort and filter state can be managed externally via the exported `SortState` and `FilterState` classes. Pass a `pagination` prop (`ServerPagination`) plus `onPaginationChange` to opt into the same [server-driven mode](#server-side-pagination-sorting--filtering) `GenericCRUD` uses. `bind:visibleRows` and `bind:query` expose the currently filtered/sorted rows and query snapshot, useful for building your own export UI on top of the raw table.
|
|
880
979
|
|
|
980
|
+
Pass a `reorder` prop (`{ attribute, sortable, compare?, icon?, multiDrag?, pageFlipThresholdMs? }` — same shape as `GenericCRUD`'s [`config.reorder`](#reordering-rows), minus `endpoint`/`callback`) plus `onReorder` to get the same drag-to-reorder behavior without the CRUD-level persistence wiring — `onReorder` fires with the complete reordered row list after each drag, and it's on you to decide what to do with it. Ignored whenever `pagination` is also set.
|
|
981
|
+
|
|
881
982
|
### Form Components
|
|
882
983
|
|
|
883
984
|
Individual form primitives styled with DaisyUI:
|
|
@@ -1118,6 +1219,7 @@ All UI strings default to **Spanish** (Argentina). To switch to another language
|
|
|
1118
1219
|
| `delete` | `string` | `Eliminar` |
|
|
1119
1220
|
| `create` | `string` | `Crear` |
|
|
1120
1221
|
| `searchPlaceholder` | `string` | `Buscar...` |
|
|
1222
|
+
| `reorder` | `string` | `Arrastrar para reordenar` |
|
|
1121
1223
|
| `export` | `string` | `Exportar` |
|
|
1122
1224
|
| `exportCsv` | `string` | `Exportar a CSV` |
|
|
1123
1225
|
| `exportExcel` | `string` | `Exportar a Excel` |
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends object = Record<string, unknown>">
|
|
2
2
|
import { page } from '$app/state';
|
|
3
3
|
import { goto } from '$app/navigation';
|
|
4
|
-
import List from './views/List.svelte';
|
|
4
|
+
import List from './views/list/List.svelte';
|
|
5
5
|
import Read from './views/Read.svelte';
|
|
6
6
|
import Create from './views/Create.svelte';
|
|
7
7
|
import Update from './views/Update.svelte';
|
|
8
8
|
import { AUTO_EXCLUDED } from './utils/constants.js';
|
|
9
9
|
import {
|
|
10
10
|
resolveFormatter,
|
|
11
|
+
truncateFormatter,
|
|
11
12
|
inferType,
|
|
12
13
|
buildFieldDefinitions
|
|
13
14
|
} from './utils/resolution.js';
|
|
14
15
|
import { defaultItemLabel } from './utils/embedded.js';
|
|
15
16
|
import { isFilterable } from '../table/utils.js';
|
|
16
|
-
import type { XlsxModule } from '../table/export.js';
|
|
17
17
|
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
18
18
|
import type {
|
|
19
19
|
ActionConfiguration,
|
|
20
20
|
ColumnDefinition,
|
|
21
21
|
CustomAction,
|
|
22
|
-
CustomBulkAction,
|
|
23
22
|
FieldDefinition,
|
|
24
|
-
|
|
23
|
+
ListActions,
|
|
24
|
+
ListConfig,
|
|
25
25
|
ViewBasedCustomBulkAction
|
|
26
26
|
} from '../../types/crud.js';
|
|
27
27
|
import type {
|
|
@@ -44,16 +44,12 @@
|
|
|
44
44
|
update = {} as ActionConfiguration<T>,
|
|
45
45
|
read = {} as ActionConfiguration<T>,
|
|
46
46
|
deletion = {} as ActionConfiguration<T>,
|
|
47
|
-
actions =
|
|
48
|
-
|
|
49
|
-
search = undefined as SearchConfiguration | undefined,
|
|
47
|
+
actions = {} as ListActions<T>,
|
|
48
|
+
config = {} as ListConfig<T>,
|
|
50
49
|
columns = undefined as ColumnDefinition<T>[] | undefined,
|
|
51
50
|
fields = undefined as FieldDefinition<T>[] | undefined,
|
|
52
51
|
meta = undefined as Partial<Record<string, AttributeMetadata>> | undefined,
|
|
53
|
-
form = null as { error?: string } | null
|
|
54
|
-
enableExport = false,
|
|
55
|
-
onExport = undefined as ((query: TableQuery) => Promise<T[]>) | undefined,
|
|
56
|
-
xlsx = undefined as XlsxModule | undefined
|
|
52
|
+
form = null as { error?: string } | null
|
|
57
53
|
}: {
|
|
58
54
|
data?: Record<string, unknown>;
|
|
59
55
|
dataKey?: string;
|
|
@@ -67,16 +63,15 @@
|
|
|
67
63
|
update?: ActionConfiguration<T>;
|
|
68
64
|
read?: ActionConfiguration<T>;
|
|
69
65
|
deletion?: ActionConfiguration<T>;
|
|
70
|
-
actions
|
|
71
|
-
|
|
72
|
-
search
|
|
66
|
+
/** Extra per-row (`custom`) and per-selection (`bulk`) actions. */
|
|
67
|
+
actions?: ListActions<T>;
|
|
68
|
+
/** Opt-in list behaviors: free-text search, CSV/Excel export, and
|
|
69
|
+
* drag-to-reorder. */
|
|
70
|
+
config?: ListConfig<T>;
|
|
73
71
|
columns?: ColumnDefinition<T>[];
|
|
74
72
|
fields?: FieldDefinition<T>[];
|
|
75
73
|
meta?: Partial<Record<string, AttributeMetadata>>;
|
|
76
74
|
form?: { error?: string } | null;
|
|
77
|
-
enableExport?: boolean;
|
|
78
|
-
onExport?: (query: TableQuery) => Promise<T[]>;
|
|
79
|
-
xlsx?: XlsxModule;
|
|
80
75
|
} = $props();
|
|
81
76
|
|
|
82
77
|
function isEnvelope(value: unknown): value is PaginatedEnvelope<T> {
|
|
@@ -198,7 +193,7 @@
|
|
|
198
193
|
// embedded column without an explicit formatter falls back to
|
|
199
194
|
// joining each item's label (itemLabel, or the same
|
|
200
195
|
// sub-field-joining summary the embedded form list uses).
|
|
201
|
-
const
|
|
196
|
+
const resolvedFormatter =
|
|
202
197
|
resolveFormatter(m, data) ??
|
|
203
198
|
(embeddedFields
|
|
204
199
|
? (value: unknown) =>
|
|
@@ -212,6 +207,10 @@
|
|
|
212
207
|
.join(', ')
|
|
213
208
|
: ''
|
|
214
209
|
: undefined);
|
|
210
|
+
const formatter =
|
|
211
|
+
m.truncateUpTo != null
|
|
212
|
+
? truncateFormatter(resolvedFormatter, m.truncateUpTo)
|
|
213
|
+
: resolvedFormatter;
|
|
215
214
|
return {
|
|
216
215
|
attribute: k as keyof T & string,
|
|
217
216
|
title: m.label ?? k,
|
|
@@ -388,16 +387,12 @@
|
|
|
388
387
|
{read}
|
|
389
388
|
{deletion}
|
|
390
389
|
{actions}
|
|
391
|
-
{
|
|
392
|
-
{search}
|
|
390
|
+
{config}
|
|
393
391
|
columns={resolvedColumns}
|
|
394
392
|
pagination={serverPagination}
|
|
395
393
|
{initialSort}
|
|
396
394
|
{initialFilters}
|
|
397
395
|
onPaginationChange={handlePaginationChange}
|
|
398
|
-
{enableExport}
|
|
399
|
-
{onExport}
|
|
400
|
-
{xlsx}
|
|
401
396
|
onCreate={navCreate}
|
|
402
397
|
onEdit={navEdit}
|
|
403
398
|
onView={navRead}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import type { XlsxModule } from '../table/export.js';
|
|
2
1
|
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
3
|
-
import type { ActionConfiguration, ColumnDefinition,
|
|
4
|
-
import type { TableQuery } from '../../types/table.js';
|
|
2
|
+
import type { ActionConfiguration, ColumnDefinition, FieldDefinition, ListActions, ListConfig } from '../../types/crud.js';
|
|
5
3
|
declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
6
4
|
props: {
|
|
7
5
|
data?: Record<string, unknown>;
|
|
@@ -15,18 +13,17 @@ declare function $$render<T extends object = Record<string, unknown>>(): {
|
|
|
15
13
|
update?: ActionConfiguration<T>;
|
|
16
14
|
read?: ActionConfiguration<T>;
|
|
17
15
|
deletion?: ActionConfiguration<T>;
|
|
18
|
-
actions
|
|
19
|
-
|
|
20
|
-
search
|
|
16
|
+
/** Extra per-row (`custom`) and per-selection (`bulk`) actions. */
|
|
17
|
+
actions?: ListActions<T>;
|
|
18
|
+
/** Opt-in list behaviors: free-text search, CSV/Excel export, and
|
|
19
|
+
* drag-to-reorder. */
|
|
20
|
+
config?: ListConfig<T>;
|
|
21
21
|
columns?: ColumnDefinition<T>[];
|
|
22
22
|
fields?: FieldDefinition<T>[];
|
|
23
23
|
meta?: Partial<Record<string, AttributeMetadata>>;
|
|
24
24
|
form?: {
|
|
25
25
|
error?: string;
|
|
26
26
|
} | null;
|
|
27
|
-
enableExport?: boolean;
|
|
28
|
-
onExport?: (query: TableQuery) => Promise<T[]>;
|
|
29
|
-
xlsx?: XlsxModule;
|
|
30
27
|
};
|
|
31
28
|
exports: {};
|
|
32
29
|
bindings: "";
|
|
@@ -2,5 +2,6 @@ import type { FieldDefinition } from '../../../types/crud.js';
|
|
|
2
2
|
export declare function seedField<T extends object = Record<string, unknown>>(f: FieldDefinition<T>, raw: unknown): unknown;
|
|
3
3
|
export declare function emptyRecord<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[]): Record<string, unknown>;
|
|
4
4
|
export declare function seedRecord<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], source: Record<string, unknown>): Record<string, unknown>;
|
|
5
|
+
export declare function applyDuplicateOmit<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], record: Record<string, unknown>, omit?: string[]): Record<string, unknown>;
|
|
5
6
|
export declare function formatEmbeddedFieldValue<T extends object = Record<string, unknown>>(f: FieldDefinition<T>, raw: unknown): string;
|
|
6
7
|
export declare function defaultItemLabel<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], item: Record<string, unknown>): string;
|
|
@@ -39,6 +39,20 @@ export function seedRecord(fields, source) {
|
|
|
39
39
|
}
|
|
40
40
|
return seeded;
|
|
41
41
|
}
|
|
42
|
+
// Shared by Update.svelte and Create.svelte's "Duplicate" handling: returns a
|
|
43
|
+
// copy of the just-saved record with `omit`-listed attributes reset to their
|
|
44
|
+
// field default, so the new draft doesn't inherit values that only made sense
|
|
45
|
+
// for the original instance (e.g. a publication date).
|
|
46
|
+
export function applyDuplicateOmit(fields, record, omit = []) {
|
|
47
|
+
if (!omit.length)
|
|
48
|
+
return record;
|
|
49
|
+
const result = { ...record };
|
|
50
|
+
for (const f of fields) {
|
|
51
|
+
if (omit.includes(f.attribute))
|
|
52
|
+
result[f.attribute] = seedField(f, f.default);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
42
56
|
// Shared by defaultItemLabel and the CSV/XLSX export column expansion: renders
|
|
43
57
|
// one sub-field's raw stored value as display text, resolving a select's
|
|
44
58
|
// option label instead of its stored value. Booleans are left to the caller
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export declare const formatBoolean: (trueLabel?: string, falseLabel?: string) => () => (value: boolean) => string;
|
|
2
2
|
export declare const formatDatetime: (format?: string, timeZone?: string) => (() => (value: Date) => string);
|
|
3
|
-
export declare function formatTruncateTextUpTo(maxLength: number): () => (value: string) => string;
|
|
4
3
|
export declare function formatInstance<T extends Record<string, unknown>>(attribute: keyof T & string, instances: T[], urlPath: string, idKey?: keyof T & string): (value: unknown) => string;
|
|
@@ -52,12 +52,6 @@ export const formatDatetime = (format = 'dd/mm/YYYY HH:MM', timeZone) => {
|
|
|
52
52
|
};
|
|
53
53
|
return () => fmt;
|
|
54
54
|
};
|
|
55
|
-
export function formatTruncateTextUpTo(maxLength) {
|
|
56
|
-
return () => (value) => {
|
|
57
|
-
const str = String(value ?? '');
|
|
58
|
-
return str.length > maxLength ? str.slice(0, maxLength) + '…' : str;
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
55
|
function escapeHtml(str) {
|
|
62
56
|
return str
|
|
63
57
|
.replace(/&/g, '&')
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Given the full row list in its new (post-drag) order, returns copies of
|
|
2
|
+
* only the rows whose `attribute` no longer matches their new sequential
|
|
3
|
+
* (0-based) position — the minimal set worth persisting. */
|
|
4
|
+
export declare function computeReorderChanges<T extends object>(rows: T[], attribute: keyof T & string): T[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Given the full row list in its new (post-drag) order, returns copies of
|
|
2
|
+
* only the rows whose `attribute` no longer matches their new sequential
|
|
3
|
+
* (0-based) position — the minimal set worth persisting. */
|
|
4
|
+
export function computeReorderChanges(rows, attribute) {
|
|
5
|
+
const changed = [];
|
|
6
|
+
rows.forEach((row, index) => {
|
|
7
|
+
const current = Number(row[attribute]);
|
|
8
|
+
if (current !== index) {
|
|
9
|
+
changed.push({ ...row, [attribute]: index });
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
return changed;
|
|
13
|
+
}
|
|
@@ -3,5 +3,6 @@ import type { FieldDefinition } from '../../../types/crud.js';
|
|
|
3
3
|
export declare function resolveOptions(m: AttributeMetadata, d: unknown): SelectOption[] | undefined;
|
|
4
4
|
export declare function resolveDefault(m: AttributeMetadata, d: unknown): unknown;
|
|
5
5
|
export declare function resolveFormatter(m: AttributeMetadata, d: unknown): import("../../../index.ts").CellFormatter<any, any> | undefined;
|
|
6
|
+
export declare function truncateFormatter<T extends object = Record<string, unknown>, V = unknown>(formatter: ((value: V, row: T) => string) | undefined, maxLength: number): (value: V, row: T) => string;
|
|
6
7
|
export declare function inferType(key: string, value: unknown): AttributeType;
|
|
7
8
|
export declare function buildFieldDefinitions<T extends object = Record<string, unknown>>(meta: Partial<Record<string, AttributeMetadata>>, data: unknown, excludedFlag: 'excludedFromCreate' | 'excludedFromRead' | 'excludedFromUpdate' | 'excludedFromList', excluded: Set<string>): FieldDefinition<T>[];
|
|
@@ -9,6 +9,17 @@ export function resolveDefault(m, d) {
|
|
|
9
9
|
export function resolveFormatter(m, d) {
|
|
10
10
|
return m.formatter?.(d);
|
|
11
11
|
}
|
|
12
|
+
// List column only (GenericCRUD's resolvedColumns): wraps `formatter` (or,
|
|
13
|
+
// absent one, the same raw String(value) TableBody falls back to) so a column
|
|
14
|
+
// that needs to stay narrow can truncate its cell without that truncation
|
|
15
|
+
// leaking into the Read view or the Create/Update forms, which call
|
|
16
|
+
// `formatter` directly and never go through this wrapper.
|
|
17
|
+
export function truncateFormatter(formatter, maxLength) {
|
|
18
|
+
return (value, row) => {
|
|
19
|
+
const str = formatter ? formatter(value, row) : String(value ?? '');
|
|
20
|
+
return str.length > maxLength ? str.slice(0, maxLength) + '…' : str;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
12
23
|
export function inferType(key, value) {
|
|
13
24
|
if (typeof value === 'boolean')
|
|
14
25
|
return 'boolean';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
9
9
|
import { validateAll } from '../utils/validation.js';
|
|
10
10
|
import { groupFields } from '../utils/grouping.js';
|
|
11
|
-
import { emptyRecord, seedRecord } from '../utils/embedded.js';
|
|
11
|
+
import { applyDuplicateOmit, emptyRecord, seedRecord } from '../utils/embedded.js';
|
|
12
12
|
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
13
13
|
import { getStrings } from '../../../i18n/context.js';
|
|
14
14
|
|
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
fieldErrors = {};
|
|
137
137
|
internalError = '';
|
|
138
138
|
flashSuccess();
|
|
139
|
+
record = applyDuplicateOmit(fields, record, creation.duplication?.omit);
|
|
139
140
|
} else {
|
|
140
141
|
onSuccess?.();
|
|
141
142
|
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { defaultIconSet } from '../../../icons/sets/default.js';
|
|
9
9
|
import { validateAll } from '../utils/validation.js';
|
|
10
10
|
import { groupFields } from '../utils/grouping.js';
|
|
11
|
-
import { seedRecord } from '../utils/embedded.js';
|
|
11
|
+
import { applyDuplicateOmit, seedRecord } from '../utils/embedded.js';
|
|
12
12
|
import type { ActionConfiguration, FieldDefinition } from '../../../types/crud.js';
|
|
13
13
|
import { getStrings } from '../../../i18n/context.js';
|
|
14
14
|
|
|
@@ -123,7 +123,7 @@
|
|
|
123
123
|
onContinue?.();
|
|
124
124
|
} else if (duplicating) {
|
|
125
125
|
duplicating = false;
|
|
126
|
-
onDuplicate?.(record);
|
|
126
|
+
onDuplicate?.(applyDuplicateOmit(fields, record, update.duplication?.omit));
|
|
127
127
|
} else {
|
|
128
128
|
onSuccess?.();
|
|
129
129
|
}
|