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.
Files changed (46) hide show
  1. package/README.md +128 -26
  2. package/dist/components/crud/GenericCRUD.svelte +18 -23
  3. package/dist/components/crud/GenericCRUD.svelte.d.ts +6 -9
  4. package/dist/components/crud/utils/embedded.d.ts +1 -0
  5. package/dist/components/crud/utils/embedded.js +14 -0
  6. package/dist/components/crud/utils/formatters.d.ts +0 -1
  7. package/dist/components/crud/utils/formatters.js +0 -6
  8. package/dist/components/crud/utils/reorder.d.ts +4 -0
  9. package/dist/components/crud/utils/reorder.js +13 -0
  10. package/dist/components/crud/utils/resolution.d.ts +1 -0
  11. package/dist/components/crud/utils/resolution.js +11 -0
  12. package/dist/components/crud/views/Create.svelte +2 -1
  13. package/dist/components/crud/views/Update.svelte +2 -2
  14. package/dist/components/crud/views/list/List.svelte +291 -0
  15. package/dist/components/crud/views/{List.svelte.d.ts → list/List.svelte.d.ts} +4 -14
  16. package/dist/components/crud/views/list/Modals.svelte +56 -0
  17. package/dist/components/crud/views/list/Modals.svelte.d.ts +36 -0
  18. package/dist/components/crud/views/list/Table.svelte +176 -0
  19. package/dist/components/crud/views/list/Table.svelte.d.ts +59 -0
  20. package/dist/components/crud/views/list/Toolbar.svelte +341 -0
  21. package/dist/components/crud/views/list/Toolbar.svelte.d.ts +46 -0
  22. package/dist/components/table/PaginatedTable.svelte +361 -16
  23. package/dist/components/table/PaginatedTable.svelte.d.ts +11 -2
  24. package/dist/components/table/TableBody.svelte +83 -3
  25. package/dist/components/table/TableBody.svelte.d.ts +14 -1
  26. package/dist/components/table/TableHeader.svelte +9 -4
  27. package/dist/components/table/TableHeader.svelte.d.ts +1 -0
  28. package/dist/components/table/sortable.d.ts +27 -0
  29. package/dist/components/table/sortable.js +55 -0
  30. package/dist/components/table/utils.d.ts +19 -1
  31. package/dist/components/table/utils.js +40 -0
  32. package/dist/i18n/en.js +2 -0
  33. package/dist/i18n/es.js +2 -0
  34. package/dist/i18n/types.d.ts +2 -0
  35. package/dist/icons/defaults/Grip.svelte +7 -0
  36. package/dist/icons/defaults/Grip.svelte.d.ts +7 -0
  37. package/dist/icons/sets/bootstrap.js +2 -1
  38. package/dist/icons/sets/default.js +2 -0
  39. package/dist/icons/types.d.ts +3 -0
  40. package/dist/index.d.ts +7 -6
  41. package/dist/index.js +5 -4
  42. package/dist/types/attribute.d.ts +8 -0
  43. package/dist/types/crud.d.ts +79 -2
  44. package/dist/types/table.d.ts +56 -0
  45. package/package.json +3 -1
  46. 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` — `CustomAction[]`, extra per-row actions see [Custom row actions](#custom-row-actions)
701
- - `customBulkActions` — `CustomBulkAction[]`, extra actions on the current selection — see [Custom bulk actions](#custom-bulk-actions)
702
- - `search` — `SearchConfiguration`, shows a free-text search box — see [Free-text search](#free-text-search)
703
- - `enableExport`, `onExport`, `xlsx` CSV/Excel export see [Exporting data](#exporting-data-csvxlsx)
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
- search={{ param: 'q', placeholder: 'Search tasks...', debounceMs: 300 }}
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
- ```svelte
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
- ```svelte
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
- `customBulkActions` 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.
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
- customBulkActions={[
791
- { label: 'Complete', icon: CheckIcon, endpoint: '?/complete' },
792
- { label: 'Mark pending', icon: UndoIcon, endpoint: '?/incomplete', variant: 'error', confirm: true },
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
- `enableExport` adds an export button to the header offering CSV (always) and Excel (when an `xlsx` module is supplied). Runeforge never bundles `xlsx` itself — install it separately and pass the resolved module in, so the dependency stays fully optional:
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 ... enableExport {xlsx} />
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 `onExport` to fetch the full, unpaginated result set for the current query — without it, export falls back to just the currently loaded page:
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
- enableExport
822
- onExport={async (query) => {
823
- const params = new URLSearchParams();
824
- if (query.ordering) params.set('ordering', query.ordering);
825
- // ...translate query.filters into your API's params
826
- const res = await fetch(`/api/widgets/export?${params}`);
827
- return res.json();
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 `onExport`) 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`.
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
- SearchConfiguration,
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 = [] as CustomAction<T>[],
48
- customBulkActions = [] as CustomBulkAction<T>[],
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?: CustomAction<T>[];
71
- customBulkActions?: CustomBulkAction<T>[];
72
- search?: SearchConfiguration;
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 formatter =
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
- {customBulkActions}
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, CustomAction, CustomBulkAction, FieldDefinition, SearchConfiguration } from '../../types/crud.js';
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?: CustomAction<T>[];
19
- customBulkActions?: CustomBulkAction<T>[];
20
- search?: SearchConfiguration;
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, '&amp;')
@@ -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
  }