@izumisy-tailor/tailor-data-viewer 0.1.51 → 0.1.53
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 +10 -0
- package/package.json +1 -1
- package/src/component/built-in-renderers.tsx +102 -2
- package/src/component/column-definition.test.ts +151 -0
- package/src/component/column-definition.ts +93 -7
- package/src/component/contexts/table-data-context.tsx +196 -0
- package/src/component/hooks/table-data-store.test.ts +357 -0
- package/src/component/hooks/table-data-store.ts +92 -0
- package/src/component/hooks/use-table-data.ts +18 -0
- package/src/component/index.ts +50 -2
- package/src/component/relation-column.test.ts +180 -0
- package/src/component/relation-column.ts +292 -0
- package/src/component/types.ts +248 -6
- package/src/graphql/query-builder.test.ts +27 -0
- package/src/graphql/query-builder.ts +216 -0
- package/docs/API.md +0 -155
- package/docs/README.md +0 -27
- package/docs/app-shell-module.md +0 -163
- package/docs/columns.md +0 -276
- package/docs/compositional-api.md +0 -418
- package/docs/custom-renderers.md +0 -382
- package/docs/data-table.md +0 -250
- package/docs/fetcher.md +0 -88
- package/docs/file-type.md +0 -249
- package/docs/inline-cell-editing.md +0 -711
- package/docs/labels.md +0 -258
- package/docs/manual-refetch.md +0 -242
- package/docs/saved-view-store.md +0 -155
package/README.md
CHANGED
|
@@ -27,6 +27,16 @@ pnpm add @izumisy-tailor/tailor-data-viewer
|
|
|
27
27
|
yarn add @izumisy-tailor/tailor-data-viewer
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
### For AI Users
|
|
31
|
+
|
|
32
|
+
If you're using AI coding assistants (Claude Code, Cursor, GitHub Copilot, etc.), we strongly recommend installing the Data Viewer skill:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx skills add git@github.com:tailor-sandbox/tailor-data-viewer.git
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This provides AI-optimized documentation for better code generation and assistance.
|
|
39
|
+
|
|
30
40
|
### Peer Dependencies
|
|
31
41
|
|
|
32
42
|
This library requires the following peer dependencies:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback } from "react";
|
|
2
2
|
import { Check, X, Copy, CheckCircle, FileIcon } from "lucide-react";
|
|
3
|
-
import type { CellRenderer } from "./types";
|
|
4
|
-
import { isFileValue, type FileValue } from "./types";
|
|
3
|
+
import type { CellRenderer, RelationFieldMetadata } from "./types";
|
|
4
|
+
import { isFileValue, type FileValue, isRelationFieldMetadata } from "./types";
|
|
5
5
|
import { formatFileSize } from "./lib/format-file-size";
|
|
6
6
|
|
|
7
7
|
// =============================================================================
|
|
@@ -428,6 +428,100 @@ function FileImageThumbnail(
|
|
|
428
428
|
};
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
// =============================================================================
|
|
432
|
+
// Relation Column Renderers (for rel() API)
|
|
433
|
+
// =============================================================================
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Default renderer for relation total (count) columns.
|
|
437
|
+
*/
|
|
438
|
+
const RelationTotal: CellRenderer = ({ value }) => {
|
|
439
|
+
if (value == null) {
|
|
440
|
+
return <span className="text-muted-foreground">-</span>;
|
|
441
|
+
}
|
|
442
|
+
const num = Number(value);
|
|
443
|
+
return <span>{num}</span>;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Default renderer for relation fields with a single field.
|
|
448
|
+
* For hasMany: displays as comma-separated list.
|
|
449
|
+
* For hasOne: displays the single value.
|
|
450
|
+
*/
|
|
451
|
+
const RelationSingleField: CellRenderer = ({ value, field }) => {
|
|
452
|
+
if (value == null) {
|
|
453
|
+
return <span className="text-muted-foreground">-</span>;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Check if this is a relation field with result type info
|
|
457
|
+
if (isRelationFieldMetadata(field)) {
|
|
458
|
+
// For hasMany (array of values)
|
|
459
|
+
if (Array.isArray(value)) {
|
|
460
|
+
const items = value as unknown[];
|
|
461
|
+
if (items.length === 0) {
|
|
462
|
+
return <span className="text-muted-foreground">-</span>;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Single field: items are objects with one key, extract the value
|
|
466
|
+
const strings = items.map((item) => {
|
|
467
|
+
if (typeof item === "object" && item !== null) {
|
|
468
|
+
const vals = Object.values(item as Record<string, unknown>);
|
|
469
|
+
return vals.length === 1
|
|
470
|
+
? String(vals[0] ?? "")
|
|
471
|
+
: JSON.stringify(item);
|
|
472
|
+
}
|
|
473
|
+
return String(item ?? "");
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
return <span>{strings.join(", ")}</span>;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// For hasOne (single object)
|
|
480
|
+
if (typeof value === "object") {
|
|
481
|
+
const vals = Object.values(value as Record<string, unknown>);
|
|
482
|
+
return (
|
|
483
|
+
<span>
|
|
484
|
+
{vals.length === 1 ? String(vals[0] ?? "-") : JSON.stringify(value)}
|
|
485
|
+
</span>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return <span>{String(value)}</span>;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Default renderer for relation fields with multiple fields.
|
|
495
|
+
* Displays as JSON for complex objects.
|
|
496
|
+
*/
|
|
497
|
+
const RelationMultipleFields: CellRenderer = ({ value }) => {
|
|
498
|
+
if (value == null) {
|
|
499
|
+
return <span className="text-muted-foreground">-</span>;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// For arrays (hasMany) or objects (hasOne), display as JSON
|
|
503
|
+
return <span className="font-mono text-xs">{JSON.stringify(value)}</span>;
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Get the appropriate default renderer for a relation column based on its metadata.
|
|
508
|
+
* This is used internally to select the right renderer.
|
|
509
|
+
*/
|
|
510
|
+
export function getDefaultRelationRenderer(
|
|
511
|
+
field: RelationFieldMetadata,
|
|
512
|
+
): CellRenderer {
|
|
513
|
+
if (field.resultType === "total") {
|
|
514
|
+
return RelationTotal;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// For fields, check if single or multiple
|
|
518
|
+
if (field.selectedFields && field.selectedFields.length === 1) {
|
|
519
|
+
return RelationSingleField;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return RelationMultipleFields;
|
|
523
|
+
}
|
|
524
|
+
|
|
431
525
|
// =============================================================================
|
|
432
526
|
// Export
|
|
433
527
|
// =============================================================================
|
|
@@ -450,6 +544,12 @@ export const builtInRenderers = {
|
|
|
450
544
|
FileLink,
|
|
451
545
|
/** Factory: Creates a file thumbnail renderer with size and click options */
|
|
452
546
|
FileImageThumbnail,
|
|
547
|
+
/** Renders relation total (count) as a number */
|
|
548
|
+
RelationTotal,
|
|
549
|
+
/** Renders relation fields (single field) as comma-separated list */
|
|
550
|
+
RelationSingleField,
|
|
551
|
+
/** Renders relation fields (multiple fields) as JSON */
|
|
552
|
+
RelationMultipleFields,
|
|
453
553
|
} as const;
|
|
454
554
|
|
|
455
555
|
export type BuiltInRenderers = typeof builtInRenderers;
|
|
@@ -242,3 +242,154 @@ describe("createColumnDefMap", () => {
|
|
|
242
242
|
expect(map.get("assignee.name")?.label).toBe("担当者");
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
|
+
|
|
246
|
+
describe("relation column support", () => {
|
|
247
|
+
describe("normalizeColumnDef with RelationColumnFn", () => {
|
|
248
|
+
it("should normalize a relation total column function", () => {
|
|
249
|
+
const columnFn = (rel: import("./relation-column").RelationBuilder) =>
|
|
250
|
+
rel("taskAssignments").total;
|
|
251
|
+
const result = normalizeColumnDef(columnFn);
|
|
252
|
+
|
|
253
|
+
expect(result.field).toBe("taskAssignments.total");
|
|
254
|
+
expect(result.isRelationField).toBe(true);
|
|
255
|
+
expect(result.baseField).toBe("taskAssignments");
|
|
256
|
+
expect(result.relationRef).toBeDefined();
|
|
257
|
+
expect(result.relationRef?.__type).toBe("relationTotal");
|
|
258
|
+
expect(result.label).toBe("taskAssignments.total");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("should normalize a relation fields column function", () => {
|
|
262
|
+
const columnFn = (rel: import("./relation-column").RelationBuilder) =>
|
|
263
|
+
rel("taskAssignments").fields(["name", "email"]);
|
|
264
|
+
const result = normalizeColumnDef(columnFn);
|
|
265
|
+
|
|
266
|
+
expect(result.field).toBe("taskAssignments.fields:name,email");
|
|
267
|
+
expect(result.isRelationField).toBe(true);
|
|
268
|
+
expect(result.baseField).toBe("taskAssignments");
|
|
269
|
+
expect(result.relationRef).toBeDefined();
|
|
270
|
+
expect(result.relationRef?.__type).toBe("relationFields");
|
|
271
|
+
expect(result.label).toBe("taskAssignments");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("should normalize a relation column with filters", () => {
|
|
275
|
+
const columnFn = (rel: import("./relation-column").RelationBuilder) =>
|
|
276
|
+
rel("taskAssignments", [
|
|
277
|
+
{ field: "answered", operator: "eq", value: false },
|
|
278
|
+
]).total;
|
|
279
|
+
const result = normalizeColumnDef(columnFn);
|
|
280
|
+
|
|
281
|
+
expect(result.field).toBe("taskAssignments.total:answered=false");
|
|
282
|
+
expect(result.relationRef?.filters).toEqual([
|
|
283
|
+
{ field: "answered", operator: "eq", value: false },
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe("normalizeColumnDef with tuple containing RelationColumnFn", () => {
|
|
289
|
+
it("should normalize [RelationColumnFn, renderer] tuple", () => {
|
|
290
|
+
const mockRenderer: CellRenderer = ({ value }) => String(value);
|
|
291
|
+
const columnFn = (rel: import("./relation-column").RelationBuilder) =>
|
|
292
|
+
rel("taskAssignments").total;
|
|
293
|
+
const result = normalizeColumnDef([columnFn, mockRenderer]);
|
|
294
|
+
|
|
295
|
+
expect(result.field).toBe("taskAssignments.total");
|
|
296
|
+
expect(result.renderer).toBe(mockRenderer);
|
|
297
|
+
expect(result.relationRef?.__type).toBe("relationTotal");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("should normalize [RelationColumnFn, ColumnOptions] tuple", () => {
|
|
301
|
+
const columnFn = (rel: import("./relation-column").RelationBuilder) =>
|
|
302
|
+
rel("taskAssignments").total;
|
|
303
|
+
const result = normalizeColumnDef([
|
|
304
|
+
columnFn,
|
|
305
|
+
{ label: "担当者数", width: 100 },
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
expect(result.field).toBe("taskAssignments.total");
|
|
309
|
+
expect(result.label).toBe("担当者数");
|
|
310
|
+
expect(result.width).toBe(100);
|
|
311
|
+
expect(result.relationRef?.__type).toBe("relationTotal");
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("normalizeColumnDef with serialized RelationRef", () => {
|
|
316
|
+
it("should normalize a serialized RelationTotalRef", () => {
|
|
317
|
+
const serializedRef = {
|
|
318
|
+
__type: "relationTotal" as const,
|
|
319
|
+
id: "taskAssignments.total",
|
|
320
|
+
relationName: "taskAssignments",
|
|
321
|
+
};
|
|
322
|
+
const result = normalizeColumnDef(serializedRef);
|
|
323
|
+
|
|
324
|
+
expect(result.field).toBe("taskAssignments.total");
|
|
325
|
+
expect(result.isRelationField).toBe(true);
|
|
326
|
+
expect(result.relationRef).toEqual(serializedRef);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("should normalize a serialized RelationFieldsRef", () => {
|
|
330
|
+
const serializedRef = {
|
|
331
|
+
__type: "relationFields" as const,
|
|
332
|
+
id: "taskAssignments.fields:name",
|
|
333
|
+
relationName: "taskAssignments",
|
|
334
|
+
fields: ["name"],
|
|
335
|
+
};
|
|
336
|
+
const result = normalizeColumnDef(serializedRef);
|
|
337
|
+
|
|
338
|
+
expect(result.field).toBe("taskAssignments.fields:name");
|
|
339
|
+
expect(result.isRelationField).toBe(true);
|
|
340
|
+
expect(result.relationRef).toEqual(serializedRef);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("should normalize [RelationRef, ColumnOptions] tuple", () => {
|
|
344
|
+
const serializedRef = {
|
|
345
|
+
__type: "relationTotal" as const,
|
|
346
|
+
id: "taskAssignments.total",
|
|
347
|
+
relationName: "taskAssignments",
|
|
348
|
+
};
|
|
349
|
+
const result = normalizeColumnDef([
|
|
350
|
+
serializedRef,
|
|
351
|
+
{ label: "カスタムラベル" },
|
|
352
|
+
]);
|
|
353
|
+
|
|
354
|
+
expect(result.field).toBe("taskAssignments.total");
|
|
355
|
+
expect(result.label).toBe("カスタムラベル");
|
|
356
|
+
expect(result.relationRef).toEqual(serializedRef);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
describe("extractFieldsFromColumns with relationRefs", () => {
|
|
361
|
+
it("should extract relationRefs from normalized columns", () => {
|
|
362
|
+
const columnFn = (rel: import("./relation-column").RelationBuilder) =>
|
|
363
|
+
rel("taskAssignments").total;
|
|
364
|
+
const columns: UntypedColumnDef[] = ["name", "assignee.name", columnFn];
|
|
365
|
+
const normalized = normalizeColumnDefs(columns);
|
|
366
|
+
const result = extractFieldsFromColumns(normalized);
|
|
367
|
+
|
|
368
|
+
expect(result.selectedFields).toEqual(["name"]);
|
|
369
|
+
expect(result.selectedRelations).toEqual(["assignee"]);
|
|
370
|
+
expect(result.expandedRelationFields).toEqual({ assignee: ["name"] });
|
|
371
|
+
expect(result.relationRefs).toHaveLength(1);
|
|
372
|
+
expect(result.relationRefs[0].__type).toBe("relationTotal");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("should extract multiple relationRefs", () => {
|
|
376
|
+
const columns: UntypedColumnDef[] = [
|
|
377
|
+
(rel: import("./relation-column").RelationBuilder) =>
|
|
378
|
+
rel("taskAssignments").total,
|
|
379
|
+
(rel: import("./relation-column").RelationBuilder) =>
|
|
380
|
+
rel("taskAssignments", [
|
|
381
|
+
{ field: "answered", operator: "eq", value: false },
|
|
382
|
+
]).total,
|
|
383
|
+
(rel: import("./relation-column").RelationBuilder) =>
|
|
384
|
+
rel("comments").fields(["text"]),
|
|
385
|
+
];
|
|
386
|
+
const normalized = normalizeColumnDefs(columns);
|
|
387
|
+
const result = extractFieldsFromColumns(normalized);
|
|
388
|
+
|
|
389
|
+
expect(result.relationRefs).toHaveLength(3);
|
|
390
|
+
expect(result.relationRefs[0].relationName).toBe("taskAssignments");
|
|
391
|
+
expect(result.relationRefs[1].filters).toHaveLength(1);
|
|
392
|
+
expect(result.relationRefs[2].__type).toBe("relationFields");
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
});
|
|
@@ -6,6 +6,14 @@ import type {
|
|
|
6
6
|
CellRenderer,
|
|
7
7
|
NormalizedColumnDefinition,
|
|
8
8
|
} from "./types";
|
|
9
|
+
import {
|
|
10
|
+
isRelationRef,
|
|
11
|
+
isRelationColumnFn,
|
|
12
|
+
executeRelationColumnFn,
|
|
13
|
+
getDefaultRelationLabel,
|
|
14
|
+
type RelationRef,
|
|
15
|
+
type RelationColumnFn,
|
|
16
|
+
} from "./relation-column";
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Check if a value is a CellRenderer function
|
|
@@ -23,7 +31,14 @@ function isColumnOptions(value: unknown): value is ColumnOptions {
|
|
|
23
31
|
return false;
|
|
24
32
|
}
|
|
25
33
|
// CellRenderer is a function, ColumnOptions is an object
|
|
26
|
-
|
|
34
|
+
// RelationRef has __type, ColumnOptions does not
|
|
35
|
+
if (isCellRenderer(value)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (isRelationRef(value)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
/**
|
|
@@ -63,12 +78,45 @@ function parseFieldPath(fieldPath: string): {
|
|
|
63
78
|
};
|
|
64
79
|
}
|
|
65
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Normalize a RelationRef to NormalizedColumnDefinition
|
|
83
|
+
*/
|
|
84
|
+
function normalizeRelationRef(
|
|
85
|
+
ref: RelationRef,
|
|
86
|
+
options?: ColumnOptions,
|
|
87
|
+
): NormalizedColumnDefinition {
|
|
88
|
+
return {
|
|
89
|
+
field: ref.id,
|
|
90
|
+
width: options?.width,
|
|
91
|
+
minWidth: options?.minWidth,
|
|
92
|
+
maxWidth: options?.maxWidth,
|
|
93
|
+
formatter: options?.formatter,
|
|
94
|
+
renderer: options?.renderer,
|
|
95
|
+
label: options?.label ?? getDefaultRelationLabel(ref),
|
|
96
|
+
isRelationField: true,
|
|
97
|
+
baseField: ref.relationName,
|
|
98
|
+
nestedField: undefined,
|
|
99
|
+
relationRef: ref,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
66
103
|
/**
|
|
67
104
|
* Normalize a single ColumnDef to NormalizedColumnDefinition
|
|
68
105
|
*/
|
|
69
106
|
export function normalizeColumnDef(
|
|
70
107
|
columnDef: UntypedColumnDef,
|
|
71
108
|
): NormalizedColumnDefinition {
|
|
109
|
+
// Handle RelationColumnFn: (rel) => rel("taskAssignments").total
|
|
110
|
+
if (isRelationColumnFn(columnDef)) {
|
|
111
|
+
const ref = executeRelationColumnFn(columnDef);
|
|
112
|
+
return normalizeRelationRef(ref);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Handle serialized RelationRef (from saved views)
|
|
116
|
+
if (isRelationRef(columnDef)) {
|
|
117
|
+
return normalizeRelationRef(columnDef);
|
|
118
|
+
}
|
|
119
|
+
|
|
72
120
|
// Level 1: Field path only ("email")
|
|
73
121
|
if (typeof columnDef === "string") {
|
|
74
122
|
const { baseField, nestedField, isRelationField } =
|
|
@@ -91,6 +139,7 @@ export function normalizeColumnDef(
|
|
|
91
139
|
width: columnDef.width,
|
|
92
140
|
minWidth: columnDef.minWidth,
|
|
93
141
|
maxWidth: columnDef.maxWidth,
|
|
142
|
+
formatter: columnDef.formatter,
|
|
94
143
|
renderer: columnDef.renderer,
|
|
95
144
|
label: columnDef.label,
|
|
96
145
|
isRelationField,
|
|
@@ -99,9 +148,35 @@ export function normalizeColumnDef(
|
|
|
99
148
|
};
|
|
100
149
|
}
|
|
101
150
|
|
|
102
|
-
// Level 2: Tuple format
|
|
151
|
+
// Level 2: Tuple format
|
|
103
152
|
if (Array.isArray(columnDef) && columnDef.length === 2) {
|
|
104
|
-
const [
|
|
153
|
+
const [first, second] = columnDef;
|
|
154
|
+
|
|
155
|
+
// Handle tuple with RelationColumnFn: [(rel) => ..., options]
|
|
156
|
+
if (isRelationColumnFn(first)) {
|
|
157
|
+
const ref = executeRelationColumnFn(first as RelationColumnFn);
|
|
158
|
+
if (isCellRenderer(second)) {
|
|
159
|
+
return normalizeRelationRef(ref, { renderer: second });
|
|
160
|
+
}
|
|
161
|
+
if (isColumnOptions(second)) {
|
|
162
|
+
return normalizeRelationRef(ref, second);
|
|
163
|
+
}
|
|
164
|
+
return normalizeRelationRef(ref);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Handle tuple with RelationRef: [RelationRef, options]
|
|
168
|
+
if (isRelationRef(first)) {
|
|
169
|
+
if (isCellRenderer(second)) {
|
|
170
|
+
return normalizeRelationRef(first, { renderer: second });
|
|
171
|
+
}
|
|
172
|
+
if (isColumnOptions(second)) {
|
|
173
|
+
return normalizeRelationRef(first, second);
|
|
174
|
+
}
|
|
175
|
+
return normalizeRelationRef(first);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Handle tuple with field path string: ["email", options]
|
|
179
|
+
const fieldPath = first as string;
|
|
105
180
|
const { baseField, nestedField, isRelationField } =
|
|
106
181
|
parseFieldPath(fieldPath);
|
|
107
182
|
|
|
@@ -123,6 +198,7 @@ export function normalizeColumnDef(
|
|
|
123
198
|
width: second.width,
|
|
124
199
|
minWidth: second.minWidth,
|
|
125
200
|
maxWidth: second.maxWidth,
|
|
201
|
+
formatter: second.formatter,
|
|
126
202
|
renderer: second.renderer,
|
|
127
203
|
label: second.label,
|
|
128
204
|
isRelationField,
|
|
@@ -158,14 +234,16 @@ export function normalizeColumnDefs(
|
|
|
158
234
|
export interface ExtractedFields {
|
|
159
235
|
/** List of direct field names (non-relation fields) */
|
|
160
236
|
selectedFields: string[];
|
|
161
|
-
/** List of relation field names */
|
|
237
|
+
/** List of relation field names (manyToOne/hasOne) */
|
|
162
238
|
selectedRelations: string[];
|
|
163
|
-
/** Map of relation field names to their expanded field names */
|
|
239
|
+
/** Map of relation field names to their expanded field names (manyToOne/hasOne) */
|
|
164
240
|
expandedRelationFields: ExpandedRelationFields;
|
|
241
|
+
/** List of relation refs from rel() columns (oneToMany/hasMany) */
|
|
242
|
+
relationRefs: RelationRef[];
|
|
165
243
|
}
|
|
166
244
|
|
|
167
245
|
/**
|
|
168
|
-
* Extract selectedFields, selectedRelations, and
|
|
246
|
+
* Extract selectedFields, selectedRelations, expandedRelationFields, and relationRefs
|
|
169
247
|
* from normalized column definitions
|
|
170
248
|
*/
|
|
171
249
|
export function extractFieldsFromColumns(
|
|
@@ -174,10 +252,17 @@ export function extractFieldsFromColumns(
|
|
|
174
252
|
const selectedFields: string[] = [];
|
|
175
253
|
const selectedRelations: string[] = [];
|
|
176
254
|
const expandedRelationFields: ExpandedRelationFields = {};
|
|
255
|
+
const relationRefs: RelationRef[] = [];
|
|
177
256
|
|
|
178
257
|
for (const column of columns) {
|
|
258
|
+
// Handle rel() columns (relationRef present)
|
|
259
|
+
if (column.relationRef) {
|
|
260
|
+
relationRefs.push(column.relationRef);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
179
264
|
if (column.isRelationField) {
|
|
180
|
-
// Relation field (e.g., "assignee.name")
|
|
265
|
+
// Relation field (e.g., "assignee.name") - manyToOne/hasOne
|
|
181
266
|
if (!selectedRelations.includes(column.baseField)) {
|
|
182
267
|
selectedRelations.push(column.baseField);
|
|
183
268
|
}
|
|
@@ -203,6 +288,7 @@ export function extractFieldsFromColumns(
|
|
|
203
288
|
selectedFields,
|
|
204
289
|
selectedRelations,
|
|
205
290
|
expandedRelationFields,
|
|
291
|
+
relationRefs,
|
|
206
292
|
};
|
|
207
293
|
}
|
|
208
294
|
|