@izumisy-tailor/tailor-data-viewer 0.1.21 → 0.1.23

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 (32) hide show
  1. package/README.md +45 -1
  2. package/dist/generator/index.d.mts +54 -31
  3. package/dist/generator/index.mjs +20 -13
  4. package/docs/compositional-api.md +366 -0
  5. package/package.json +1 -1
  6. package/src/app-shell/create-data-view-module.tsx +1 -1
  7. package/src/component/column-selector.test.tsx +143 -103
  8. package/src/component/column-selector.tsx +121 -156
  9. package/src/component/contexts/data-viewer-context.test.tsx +191 -0
  10. package/src/component/contexts/data-viewer-context.tsx +244 -0
  11. package/src/component/contexts/index.ts +19 -0
  12. package/src/component/contexts/table-data-context.tsx +114 -0
  13. package/src/component/contexts/toolbar-context.tsx +62 -0
  14. package/src/component/csv-button.tsx +79 -0
  15. package/src/component/data-table-toolbar.test.tsx +127 -72
  16. package/src/component/data-table-toolbar.tsx +14 -151
  17. package/src/component/data-table.tsx +255 -225
  18. package/src/component/data-view-tab-content.tsx +68 -138
  19. package/src/component/data-viewer.tsx +11 -11
  20. package/src/component/hooks/use-column-state.ts +2 -2
  21. package/src/component/hooks/use-table-data.test.ts +399 -0
  22. package/src/component/hooks/use-table-data.ts +24 -7
  23. package/src/component/index.ts +43 -1
  24. package/src/component/refresh-button.tsx +20 -0
  25. package/src/component/saved-view-context.tsx +31 -2
  26. package/src/component/search-filter.test.tsx +612 -0
  27. package/src/component/search-filter.tsx +168 -33
  28. package/src/component/single-record-tab-content.test.tsx +10 -10
  29. package/src/component/single-record-tab-content.tsx +62 -21
  30. package/src/component/types.ts +78 -0
  31. package/src/component/view-save-load.tsx +13 -17
  32. package/src/generator/metadata-generator.ts +100 -67
@@ -17,56 +17,84 @@ export type FieldType =
17
17
  * Metadata for a single field
18
18
  */
19
19
  export interface FieldMetadata {
20
- name: string;
21
- type: FieldType;
22
- required: boolean;
23
- enumValues?: readonly string[];
24
- arrayItemType?: FieldType;
25
- description?: string;
20
+ readonly name: string;
21
+ readonly type: FieldType;
22
+ readonly required: boolean;
23
+ readonly enumValues?: readonly string[];
24
+ readonly arrayItemType?: FieldType;
25
+ readonly description?: string;
26
26
  /** manyToOne relation info (if this field is a foreign key) */
27
- relation?: {
27
+ readonly relation?: {
28
28
  /** GraphQL field name for the related object (e.g., "task") */
29
- fieldName: string;
29
+ readonly fieldName: string;
30
30
  /** Target table name in camelCase (e.g., "task") */
31
- targetTable: string;
31
+ readonly targetTable: string;
32
32
  };
33
33
  }
34
34
 
35
35
  /**
36
- * Metadata for a relation
36
+ * Base properties shared by all relation types
37
37
  */
38
- export interface RelationMetadata {
39
- /** GraphQL field name (e.g., "task" for manyToOne, "taskAssignments" for oneToMany) */
40
- fieldName: string;
38
+ interface RelationMetadataBase {
39
+ /** GraphQL field name (e.g., "task" for manyToOne/oneToOne, "taskAssignments" for oneToMany) */
40
+ readonly fieldName: string;
41
41
  /** Target table name in camelCase (e.g., "task", "taskAssignment") */
42
- targetTable: string;
43
- /** Relation type */
44
- relationType: "manyToOne" | "oneToMany";
45
- /** For manyToOne: the FK field name (e.g., "taskId"). For oneToMany: the FK field on the child table */
46
- foreignKeyField: string;
47
- /** For manyToOne: the backward field name on the target type (used to generate oneToMany relation) */
48
- backwardFieldName?: string;
49
- /** True if this is a oneToOne relation (no inverse oneToMany should be generated) */
50
- isOneToOne?: boolean;
42
+ readonly targetTable: string;
43
+ /** The FK field name (e.g., "taskId") */
44
+ readonly foreignKeyField: string;
45
+ }
46
+
47
+ /**
48
+ * ManyToOne relation: this table has a FK pointing to another table
49
+ * Generates an inverse oneToMany relation on the target table
50
+ */
51
+ export interface ManyToOneRelation extends RelationMetadataBase {
52
+ readonly relationType: "manyToOne";
53
+ /** The backward field name on the target type (used to generate oneToMany relation) */
54
+ readonly backwardFieldName?: string;
55
+ }
56
+
57
+ /**
58
+ * OneToOne relation: this table has a FK pointing to another table (1:1)
59
+ * Does NOT generate an inverse oneToMany relation
60
+ */
61
+ export interface OneToOneRelation extends RelationMetadataBase {
62
+ readonly relationType: "oneToOne";
63
+ }
64
+
65
+ /**
66
+ * OneToMany relation: another table has a FK pointing to this table
67
+ * Generated automatically from manyToOne relations
68
+ */
69
+ export interface OneToManyRelation extends RelationMetadataBase {
70
+ readonly relationType: "oneToMany";
51
71
  }
52
72
 
73
+ /**
74
+ * Metadata for a relation (discriminated union)
75
+ */
76
+ export type RelationMetadata =
77
+ | ManyToOneRelation
78
+ | OneToOneRelation
79
+ | OneToManyRelation;
80
+
53
81
  /**
54
82
  * Metadata for a single table
55
83
  */
56
84
  export interface TableMetadata {
57
- name: string;
58
- pluralForm: string;
59
- description?: string;
60
- readAllowedRoles: string[];
61
- fields: FieldMetadata[];
62
- /** Relations (manyToOne and oneToMany) */
63
- relations?: RelationMetadata[];
85
+ readonly name: string;
86
+ readonly pluralForm: string;
87
+ readonly description?: string;
88
+ readonly readAllowedRoles: readonly string[];
89
+ readonly fields: readonly FieldMetadata[];
90
+ /** Relations (manyToOne, oneToOne, and oneToMany) */
91
+ readonly relations?: readonly RelationMetadata[];
64
92
  }
65
93
 
66
94
  /**
67
95
  * Map of all tables
68
96
  */
69
- export type TableMetadataMap = Record<string, TableMetadata>;
97
+ export type TableMetadataMap = { readonly [key: string]: TableMetadata };
70
98
 
71
99
  /**
72
100
  * Expanded relation fields configuration
@@ -304,25 +332,6 @@ export function dataViewerMetadataGenerator(
304
332
  config.array,
305
333
  );
306
334
 
307
- const fieldMetadata: FieldMetadata = {
308
- name: fieldName,
309
- type: fieldType,
310
- required: config.required ?? false,
311
- description: config.description,
312
- };
313
-
314
- // Add enum values if present
315
- if (config.allowedValues && config.allowedValues.length > 0) {
316
- fieldMetadata.enumValues = config.allowedValues.map((v) =>
317
- typeof v === "string" ? v : v.value,
318
- );
319
- }
320
-
321
- // Add array item type if it's an array field
322
- if (arrayItemType) {
323
- fieldMetadata.arrayItemType = arrayItemType;
324
- }
325
-
326
335
  // Extract manyToOne relation info from rawRelation
327
336
  // Include: manyToOne, n-1, oneToOne, 1-1 (all are treated as manyToOne for data view purposes)
328
337
  const rawRelation = config.rawRelation;
@@ -338,28 +347,56 @@ export function dataViewerMetadataGenerator(
338
347
  const isOneToOne =
339
348
  rawRelation?.type === "oneToOne" || rawRelation?.type === "1-1";
340
349
 
350
+ // Build relation info for field if applicable
351
+ let fieldRelation: FieldMetadata["relation"] | undefined;
341
352
  if (isManyToOneRelation && rawRelation.toward) {
342
353
  const targetTableName = toCamelCase(rawRelation.toward.type);
343
354
  const relationFieldName =
344
355
  rawRelation.toward.as ?? toCamelCase(rawRelation.toward.type);
345
356
 
346
- // Add relation info to the field
347
- fieldMetadata.relation = {
357
+ fieldRelation = {
348
358
  fieldName: relationFieldName,
349
359
  targetTable: targetTableName,
350
360
  };
351
361
 
352
- // Add to relations array (include backward field name for oneToMany generation)
353
- relations.push({
354
- fieldName: relationFieldName,
355
- targetTable: targetTableName,
356
- relationType: "manyToOne",
357
- foreignKeyField: fieldName,
358
- backwardFieldName: rawRelation.backward,
359
- isOneToOne,
360
- });
362
+ // Add to relations array
363
+ if (isOneToOne) {
364
+ relations.push({
365
+ fieldName: relationFieldName,
366
+ targetTable: targetTableName,
367
+ relationType: "oneToOne",
368
+ foreignKeyField: fieldName,
369
+ });
370
+ } else {
371
+ relations.push({
372
+ fieldName: relationFieldName,
373
+ targetTable: targetTableName,
374
+ relationType: "manyToOne",
375
+ foreignKeyField: fieldName,
376
+ backwardFieldName: rawRelation.backward,
377
+ });
378
+ }
361
379
  }
362
380
 
381
+ // Build enum values if present
382
+ const enumValues =
383
+ config.allowedValues && config.allowedValues.length > 0
384
+ ? config.allowedValues.map((v) =>
385
+ typeof v === "string" ? v : v.value,
386
+ )
387
+ : undefined;
388
+
389
+ // Create field metadata with all properties at once
390
+ const fieldMetadata: FieldMetadata = {
391
+ name: fieldName,
392
+ type: fieldType,
393
+ required: config.required ?? false,
394
+ ...(config.description && { description: config.description }),
395
+ ...(enumValues && { enumValues }),
396
+ ...(arrayItemType && { arrayItemType }),
397
+ ...(fieldRelation && { relation: fieldRelation }),
398
+ };
399
+
363
400
  fields.push(fieldMetadata);
364
401
  }
365
402
 
@@ -406,13 +443,9 @@ export function dataViewerMetadataGenerator(
406
443
  // Second pass: Add oneToMany relations by inverting manyToOne relations
407
444
  for (const table of allTables) {
408
445
  for (const relation of table.relations) {
446
+ // Only manyToOne relations generate inverse oneToMany
447
+ // oneToOne relations don't have a oneToMany inverse
409
448
  if (relation.relationType === "manyToOne") {
410
- // Skip oneToOne relations - they don't have a oneToMany inverse
411
- // (GraphQL generates a single object field, not a connection)
412
- if (relation.isOneToOne) {
413
- continue;
414
- }
415
-
416
449
  // Find the target table and add the inverse oneToMany relation
417
450
  const targetTable = tableByOriginalName.get(
418
451
  // Convert camelCase back to PascalCase for lookup
@@ -442,7 +475,7 @@ export function dataViewerMetadataGenerator(
442
475
  }
443
476
 
444
477
  // Build the metadata map (excluding originalName and backwardFieldName from output)
445
- const metadataMap: TableMetadataMap = {};
478
+ const metadataMap: Record<string, TableMetadata> = {};
446
479
  for (const table of allTables) {
447
480
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
448
481
  const { originalName, ...tableWithoutOriginalName } = table;