runeforge 0.0.25 → 0.0.26
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 +36 -9
- package/dist/components/crud/utils/embedded.d.ts +1 -0
- package/dist/components/crud/utils/embedded.js +13 -5
- package/dist/components/crud/utils/resolution.d.ts +1 -1
- package/dist/components/table/export.d.ts +4 -0
- package/dist/components/table/export.js +35 -3
- package/dist/types/crud.d.ts +7 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
inferType,
|
|
12
12
|
buildFieldDefinitions
|
|
13
13
|
} from './utils/resolution.js';
|
|
14
|
+
import { defaultItemLabel } from './utils/embedded.js';
|
|
14
15
|
import { isFilterable } from '../table/utils.js';
|
|
15
16
|
import type { XlsxModule } from '../table/export.js';
|
|
16
17
|
import type { AttributeMetadata } from '../../types/attribute.js';
|
|
@@ -150,15 +151,41 @@
|
|
|
150
151
|
(meta
|
|
151
152
|
? (Object.entries(meta) as [string, AttributeMetadata][])
|
|
152
153
|
.filter(([, m]) => !m.excludedFromList)
|
|
153
|
-
.map(([k, m]) =>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
154
|
+
.map(([k, m]) => {
|
|
155
|
+
const embeddedFields =
|
|
156
|
+
m.type === 'embedded' && m.fields
|
|
157
|
+
? buildFieldDefinitions(m.fields, data, 'excludedFromList', new Set())
|
|
158
|
+
: undefined;
|
|
159
|
+
// Arrays of objects have no sensible raw cell value, so an
|
|
160
|
+
// embedded column without an explicit formatter falls back to
|
|
161
|
+
// joining each item's label (itemLabel, or the same
|
|
162
|
+
// sub-field-joining summary the embedded form list uses).
|
|
163
|
+
const formatter =
|
|
164
|
+
resolveFormatter(m, data) ??
|
|
165
|
+
(embeddedFields
|
|
166
|
+
? (value: unknown) =>
|
|
167
|
+
Array.isArray(value)
|
|
168
|
+
? value
|
|
169
|
+
.map((item) =>
|
|
170
|
+
m.itemLabel
|
|
171
|
+
? m.itemLabel(item as Record<string, unknown>)
|
|
172
|
+
: defaultItemLabel(embeddedFields, item as Record<string, unknown>)
|
|
173
|
+
)
|
|
174
|
+
.join(', ')
|
|
175
|
+
: ''
|
|
176
|
+
: undefined);
|
|
177
|
+
return {
|
|
178
|
+
attribute: k as keyof T & string,
|
|
179
|
+
title: m.label ?? k,
|
|
180
|
+
type: m.type,
|
|
181
|
+
formatter,
|
|
182
|
+
component: m.component,
|
|
183
|
+
sortable: m.sortable,
|
|
184
|
+
filterable: m.filterable,
|
|
185
|
+
fields: embeddedFields,
|
|
186
|
+
itemLabel: m.itemLabel
|
|
187
|
+
};
|
|
188
|
+
})
|
|
162
189
|
: entityData.length > 0
|
|
163
190
|
? (Object.keys(entityData[0]) as (keyof T & string)[])
|
|
164
191
|
.filter((k) => !excluded.has(k))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
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
|
+
export declare function formatEmbeddedFieldValue<T extends object = Record<string, unknown>>(f: FieldDefinition<T>, raw: unknown): string;
|
|
4
5
|
export declare function defaultItemLabel<T extends object = Record<string, unknown>>(fields: FieldDefinition<T>[], item: Record<string, unknown>): string;
|
|
@@ -20,6 +20,18 @@ export function seedField(f, raw) {
|
|
|
20
20
|
export function emptyRecord(fields) {
|
|
21
21
|
return Object.fromEntries(fields.map((f) => [f.attribute, seedField(f, f.default)]));
|
|
22
22
|
}
|
|
23
|
+
// Shared by defaultItemLabel and the CSV/XLSX export column expansion: renders
|
|
24
|
+
// one sub-field's raw stored value as display text, resolving a select's
|
|
25
|
+
// option label instead of its stored value. Booleans are left to the caller
|
|
26
|
+
// since the item-label summary and an export cell want different renderings
|
|
27
|
+
// (omit-when-false vs. an explicit true/false column).
|
|
28
|
+
export function formatEmbeddedFieldValue(f, raw) {
|
|
29
|
+
if (raw == null || raw === '')
|
|
30
|
+
return '';
|
|
31
|
+
if (f.type === 'select')
|
|
32
|
+
return f.options?.find((o) => o.value === String(raw))?.label ?? String(raw);
|
|
33
|
+
return String(raw);
|
|
34
|
+
}
|
|
23
35
|
// Default summary shown per item in an embedded list when the field has no
|
|
24
36
|
// `itemLabel`: joins each sub-field's resolved display value so the list is
|
|
25
37
|
// at least readable out of the box.
|
|
@@ -27,13 +39,9 @@ export function defaultItemLabel(fields, item) {
|
|
|
27
39
|
return fields
|
|
28
40
|
.map((f) => {
|
|
29
41
|
const raw = item[f.attribute];
|
|
30
|
-
if (raw == null || raw === '')
|
|
31
|
-
return null;
|
|
32
|
-
if (f.type === 'select')
|
|
33
|
-
return f.options?.find((o) => o.value === String(raw))?.label ?? String(raw);
|
|
34
42
|
if (f.type === 'boolean')
|
|
35
43
|
return raw ? fieldLabel(f) : null;
|
|
36
|
-
return
|
|
44
|
+
return formatEmbeddedFieldValue(f, raw) || null;
|
|
37
45
|
})
|
|
38
46
|
.filter((v) => !!v)
|
|
39
47
|
.join(' · ');
|
|
@@ -4,4 +4,4 @@ export declare function resolveOptions(m: AttributeMetadata, d: unknown): Select
|
|
|
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
6
|
export declare function inferType(key: string, value: unknown): AttributeType;
|
|
7
|
-
export declare function buildFieldDefinitions<T extends object = Record<string, unknown>>(meta: Partial<Record<string, AttributeMetadata>>, data: unknown, excludedFlag: 'excludedFromCreate' | 'excludedFromRead' | 'excludedFromUpdate', excluded: Set<string>): FieldDefinition<T>[];
|
|
7
|
+
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>[];
|
|
@@ -13,5 +13,9 @@ export interface XlsxModule {
|
|
|
13
13
|
};
|
|
14
14
|
writeFile: (workbook: unknown, filename: string) => void;
|
|
15
15
|
}
|
|
16
|
+
export declare function buildTable<T extends object>(rows: T[], columns: ColumnDefinition<T>[]): {
|
|
17
|
+
headers: string[];
|
|
18
|
+
body: string[][];
|
|
19
|
+
};
|
|
16
20
|
export declare function downloadCsv<T extends object>(rows: T[], columns: ColumnDefinition<T>[], filename: string): void;
|
|
17
21
|
export declare function downloadXlsx<T extends object>(rows: T[], columns: ColumnDefinition<T>[], filename: string, xlsx: XlsxModule): void;
|
|
@@ -1,7 +1,39 @@
|
|
|
1
1
|
import { cellRenderedText } from './utils.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { formatEmbeddedFieldValue } from '../crud/utils/embedded.js';
|
|
3
|
+
function isEmbeddedColumn(col) {
|
|
4
|
+
return col.type === 'embedded' && !!col.fields?.length;
|
|
5
|
+
}
|
|
6
|
+
// Embedded columns hold one array of sub-records per row — there's no single
|
|
7
|
+
// plain-text cell for that, so export (unlike the on-screen table, which
|
|
8
|
+
// shows a joined summary) expands each one into its own row per item and one
|
|
9
|
+
// sub-column per field, duplicating the row's other column values across
|
|
10
|
+
// them. Rows are aligned by index across embedded columns rather than
|
|
11
|
+
// cross-joined, since unrelated embedded lists on the same row (e.g. two
|
|
12
|
+
// independent one-to-many relations) have no meaningful pairing.
|
|
13
|
+
export function buildTable(rows, columns) {
|
|
14
|
+
const embeddedColumns = columns.filter(isEmbeddedColumn);
|
|
15
|
+
if (embeddedColumns.length === 0) {
|
|
16
|
+
const headers = columns.map((col) => col.title ?? col.attribute);
|
|
17
|
+
const body = rows.map((row) => columns.map((col) => cellRenderedText(row, col)));
|
|
18
|
+
return { headers, body };
|
|
19
|
+
}
|
|
20
|
+
const headers = columns.flatMap((col) => isEmbeddedColumn(col)
|
|
21
|
+
? col.fields.map((f) => `${col.title ?? col.attribute} - ${f.title ?? f.attribute}`)
|
|
22
|
+
: [col.title ?? col.attribute]);
|
|
23
|
+
const body = [];
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const itemCount = Math.max(1, ...embeddedColumns.map((col) => row[col.attribute]?.length ?? 0));
|
|
26
|
+
for (let i = 0; i < itemCount; i++) {
|
|
27
|
+
const line = columns.flatMap((col) => {
|
|
28
|
+
if (!isEmbeddedColumn(col))
|
|
29
|
+
return [cellRenderedText(row, col)];
|
|
30
|
+
const items = row[col.attribute] ?? [];
|
|
31
|
+
const item = items[i];
|
|
32
|
+
return col.fields.map((f) => (item ? formatEmbeddedFieldValue(f, item[f.attribute]) : ''));
|
|
33
|
+
});
|
|
34
|
+
body.push(line);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
5
37
|
return { headers, body };
|
|
6
38
|
}
|
|
7
39
|
function escapeCsvCell(value) {
|
package/dist/types/crud.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export type ColumnDefinition<T extends object = Record<string, unknown>> = {
|
|
|
11
11
|
formatter?: CellFormatter<T, T[K]>;
|
|
12
12
|
sortable?: boolean;
|
|
13
13
|
filterable?: boolean;
|
|
14
|
+
/** Embedded columns only: sub-field definitions for each item, used to
|
|
15
|
+
* render a default cell summary and to expand the column into one
|
|
16
|
+
* sub-column per field on CSV/XLSX export. */
|
|
17
|
+
fields?: FieldDefinition<Record<string, unknown>>[];
|
|
18
|
+
/** Embedded columns only: short label for an item, reused from the form
|
|
19
|
+
* field's `itemLabel` for the default cell summary. */
|
|
20
|
+
itemLabel?: (item: Record<string, unknown>) => string;
|
|
14
21
|
};
|
|
15
22
|
}[keyof T & string];
|
|
16
23
|
export interface FieldDefinition<T extends object = Record<string, unknown>> {
|