@izumisy-tailor/tailor-data-viewer 0.2.32 → 0.2.33

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.
@@ -1,181 +1,102 @@
1
+ import type { ReactNode } from "react";
1
2
  import type { TableMetadata } from "../generator/metadata-generator";
2
3
  import type {
3
- CellRenderer,
4
- DisplayColumn,
5
- DisplayColumnOptions,
6
- FieldColumn,
7
- FieldColumnOptions,
4
+ Column,
5
+ ColumnOptions,
8
6
  TableFieldName,
9
7
  FilterConfig,
10
8
  MetadataFieldOptions,
11
- MetadataFieldsOptions,
12
9
  SortConfig,
13
10
  } from "./types";
14
11
  import { fieldTypeToFilterConfig, fieldTypeToSortConfig } from "./types";
15
12
 
16
13
  // =============================================================================
17
- // field() helper
14
+ // column() helper
18
15
  // =============================================================================
19
16
 
20
17
  /**
21
- * Define a data field column.
18
+ * Define a column with explicit render and optional sort/filter/accessor.
22
19
  *
23
- * Accesses `row[dataKey]` for the cell value. Supports sort/filter when configured.
24
- *
25
- * @param dataKey - Property name on the row object.
26
- * @param options - Column options (label, width, sort, filter, renderer).
20
+ * @param options - Column options (label, render, sort, filter, accessor, width).
27
21
  *
28
22
  * @example
29
23
  * ```tsx
30
- * field("name", {
24
+ * column({
31
25
  * label: "Name",
32
- * sort: { type: "string" },
33
- * filter: { type: "string" },
26
+ * render: (row) => row.name,
27
+ * sort: { field: "name", type: "string" },
28
+ * filter: { field: "name", type: "string" },
34
29
  * })
35
30
  * ```
36
31
  */
37
- export function field<TRow extends Record<string, unknown>>(
38
- dataKey: keyof TRow & string,
39
- options?: FieldColumnOptions<TRow>,
40
- ): FieldColumn<TRow> {
32
+ export function column<TRow extends Record<string, unknown>>(
33
+ options: ColumnOptions<TRow>,
34
+ ): Column<TRow> {
41
35
  return {
42
- kind: "field",
43
- dataKey,
44
- label: options?.label,
45
- width: options?.width,
46
- sort: options?.sort,
47
- filter: options?.filter,
48
- renderer: options?.renderer,
36
+ label: options.label,
37
+ render: options.render,
38
+ id: options.id,
39
+ width: options.width,
40
+ accessor: options.accessor,
41
+ sort: options.sort,
42
+ filter: options.filter,
49
43
  };
50
44
  }
51
45
 
52
46
  // =============================================================================
53
- // display() helper
47
+ // inferColumns() — metadata-driven column defaults
54
48
  // =============================================================================
55
49
 
56
50
  /**
57
- * Define a display-only column (not backed by a data field).
58
- *
59
- * Sort and filter are always disabled for display columns.
60
- *
61
- * @param id - Unique column identifier.
62
- * @param options - Column options (label, width, render function).
63
- *
64
- * @example
65
- * ```tsx
66
- * display("actions", {
67
- * width: 50,
68
- * render: (row) => <ActionMenu row={row} />,
69
- * })
70
- * ```
51
+ * Default value formatter for cell display.
71
52
  */
72
- export function display<TRow extends Record<string, unknown>>(
73
- id: string,
74
- options: DisplayColumnOptions<TRow>,
75
- ): DisplayColumn<TRow> {
76
- return {
77
- kind: "display",
78
- id,
79
- label: options.label,
80
- width: options.width,
81
- render: options.render,
82
- };
53
+ function formatValue(value: unknown): ReactNode {
54
+ if (value == null) return "";
55
+ if (typeof value === "boolean") return value ? "✓" : "✗";
56
+ if (value instanceof Date) return value.toLocaleDateString();
57
+ if (typeof value === "object") return JSON.stringify(value);
58
+ return String(value);
83
59
  }
84
60
 
85
- // =============================================================================
86
- // createColumnHelper() factory
87
- // =============================================================================
88
-
89
61
  /**
90
- * Create `field`, `display`, and `inferColumns` helpers with TRow bound once.
91
- *
92
- * This avoids repeating the row type parameter on every helper call.
93
- * Use `inferColumns(tableMetadata)` to create metadata-inferred columns
94
- * without specifying TRow again.
62
+ * Return a function that produces `ColumnOptions` from metadata field names.
95
63
  *
96
64
  * @example
97
65
  * ```tsx
98
- * type Order = { id: string; title: string; amount: number };
99
- *
100
- * const { field, display, inferColumns } = createColumnHelper<Order>();
101
- *
102
- * // Manual columns
103
- * const manualColumns = [
104
- * field("title", { label: "Title", sort: { type: "string" } }),
105
- * display("actions", { render: (row) => <ActionMenu row={row} /> }),
66
+ * const infer = inferColumns<OrderRow>(tableMetadata.order);
67
+ * const columns = [
68
+ * column(infer("title")),
69
+ * column({ ...infer("status"), render: (row) => <StatusBadge value={row.status} /> }),
106
70
  * ];
107
- *
108
- * // Metadata-inferred columns (no need to pass TRow again)
109
- * const { column } = inferColumns(tableMetadata.order);
110
- * const inferredColumns = [column("title"), column("amount")];
111
71
  * ```
112
72
  */
113
- export function createColumnHelper<TRow extends Record<string, unknown>>(): {
114
- field: (
115
- dataKey: keyof TRow & string,
116
- options?: FieldColumnOptions<TRow>,
117
- ) => FieldColumn<TRow>;
118
- display: (
119
- id: string,
120
- options: DisplayColumnOptions<TRow>,
121
- ) => DisplayColumn<TRow>;
122
- inferColumns: <const TTable extends TableMetadata>(
123
- tableMetadata: TTable,
124
- ) => {
125
- column: (
126
- dataKey: TableFieldName<TTable>,
127
- options?: MetadataFieldOptions<TRow>,
128
- ) => FieldColumn<TRow>;
129
- columns: (
130
- dataKeys: TableFieldName<TTable>[],
131
- options?: MetadataFieldsOptions,
132
- ) => FieldColumn<TRow>[];
133
- };
134
- } {
135
- return {
136
- field: (dataKey, options) => field<TRow>(dataKey, options),
137
- display: (id, options) => display<TRow>(id, options),
138
- inferColumns: (tableMetadata) => inferColumnHelper<TRow>(tableMetadata),
139
- };
140
- }
141
-
142
- // =============================================================================
143
- // Internal: metadata-based column inference (used by createColumnHelper)
144
- // =============================================================================
145
-
146
- function inferColumnHelper<
73
+ export function inferColumns<
147
74
  TRow extends Record<string, unknown>,
148
75
  const TTable extends TableMetadata = TableMetadata,
149
76
  >(
150
77
  tableMetadata: TTable,
151
- ): {
152
- column: (
153
- dataKey: TableFieldName<TTable>,
154
- options?: MetadataFieldOptions<TRow>,
155
- ) => FieldColumn<TRow>;
156
-
157
- columns: (
158
- dataKeys: TableFieldName<TTable>[],
159
- options?: MetadataFieldsOptions,
160
- ) => FieldColumn<TRow>[];
161
- } {
78
+ ): (
79
+ dataKey: TableFieldName<TTable>,
80
+ options?: MetadataFieldOptions,
81
+ ) => ColumnOptions<TRow> {
162
82
  const fields = tableMetadata.fields;
163
83
 
164
- const column = (
84
+ return (
165
85
  dataKey: TableFieldName<TTable>,
166
- columnOptions?: MetadataFieldOptions<TRow>,
167
- ): FieldColumn<TRow> => {
168
- const fieldMeta = fields.find((f) => f.name === dataKey);
86
+ columnOptions?: MetadataFieldOptions,
87
+ ): ColumnOptions<TRow> => {
88
+ const fieldName = dataKey as string;
89
+ const fieldMeta = fields.find((f) => f.name === fieldName);
169
90
  if (!fieldMeta) {
170
91
  throw new Error(
171
- `Field "${String(dataKey)}" not found in table "${tableMetadata.name}" metadata`,
92
+ `Field "${fieldName}" not found in table "${tableMetadata.name}" metadata`,
172
93
  );
173
94
  }
174
95
 
175
96
  // Auto-detect sort config
176
97
  let sort: SortConfig | undefined;
177
98
  if (columnOptions?.sort !== false) {
178
- sort = fieldTypeToSortConfig(fieldMeta.type);
99
+ sort = fieldTypeToSortConfig(fieldName, fieldMeta.type);
179
100
  }
180
101
  if (columnOptions?.sort === false) {
181
102
  sort = undefined;
@@ -184,37 +105,65 @@ function inferColumnHelper<
184
105
  // Auto-detect filter config
185
106
  let filter: FilterConfig | undefined;
186
107
  if (columnOptions?.filter !== false) {
187
- filter = fieldTypeToFilterConfig(fieldMeta.type, fieldMeta.enumValues);
108
+ filter = fieldTypeToFilterConfig(
109
+ fieldName,
110
+ fieldMeta.type,
111
+ fieldMeta.enumValues,
112
+ );
188
113
  }
189
114
  if (columnOptions?.filter === false) {
190
115
  filter = undefined;
191
116
  }
192
117
 
118
+ const label =
119
+ columnOptions?.label ?? fieldMeta.description ?? fieldMeta.name;
120
+
193
121
  return {
194
- kind: "field",
195
- dataKey: fieldMeta.name,
196
- label: columnOptions?.label ?? fieldMeta.description ?? fieldMeta.name,
122
+ label,
123
+ render: ((row: Record<string, unknown>) =>
124
+ formatValue(row[fieldName])) as (row: TRow) => ReactNode,
125
+ accessor: ((row: Record<string, unknown>) => row[fieldName]) as (
126
+ row: TRow,
127
+ ) => unknown,
197
128
  width: columnOptions?.width,
198
129
  sort,
199
130
  filter,
200
- renderer: columnOptions?.renderer as CellRenderer<TRow> | undefined,
201
131
  };
202
132
  };
133
+ }
203
134
 
204
- const columnsHelper = (
205
- dataKeys: TableFieldName<TTable>[],
206
- options?: MetadataFieldsOptions,
207
- ): FieldColumn<TRow>[] => {
208
- return dataKeys.map((dataKey) => {
209
- const overrides = options?.overrides?.[dataKey as string];
210
- return column(dataKey, {
211
- label: overrides?.label,
212
- width: overrides?.width,
213
- sort: overrides?.sort ?? options?.sort,
214
- filter: overrides?.filter ?? options?.filter,
215
- });
216
- });
217
- };
135
+ // =============================================================================
136
+ // createColumnHelper() — factory with TRow bound once
137
+ // =============================================================================
218
138
 
219
- return { column, columns: columnsHelper };
139
+ /**
140
+ * Factory that captures the row type once and returns `column` and `inferColumns`
141
+ * with `TRow` already bound, avoiding repeated type annotations.
142
+ *
143
+ * @example
144
+ * ```tsx
145
+ * const { column, inferColumns } = createColumnHelper<Order>();
146
+ *
147
+ * const infer = inferColumns(tableMetadata.order);
148
+ * const columns = [
149
+ * column(infer("title")),
150
+ * column({ label: "Actions", render: (row) => <button>Edit {row.name}</button> }),
151
+ * ];
152
+ * ```
153
+ */
154
+ export function createColumnHelper<TRow extends Record<string, unknown>>(): {
155
+ column: (options: ColumnOptions<TRow>) => Column<TRow>;
156
+ inferColumns: <const TTable extends TableMetadata = TableMetadata>(
157
+ tableMetadata: TTable,
158
+ ) => (
159
+ dataKey: TableFieldName<TTable>,
160
+ options?: MetadataFieldOptions,
161
+ ) => ColumnOptions<TRow>;
162
+ } {
163
+ return {
164
+ column: (options: ColumnOptions<TRow>) => column<TRow>(options),
165
+ inferColumns: <const TTable extends TableMetadata = TableMetadata>(
166
+ tableMetadata: TTable,
167
+ ) => inferColumns<TRow, TTable>(tableMetadata),
168
+ };
220
169
  }
@@ -10,12 +10,7 @@ export type {
10
10
  QueryVariables,
11
11
  CollectionResult,
12
12
  NodeType,
13
- CellRendererProps,
14
- CellRenderer,
15
- FieldColumnOptions,
16
- DisplayColumnOptions,
17
- FieldColumn,
18
- DisplayColumn,
13
+ ColumnOptions,
19
14
  Column,
20
15
  ColumnDefinition,
21
16
  UseCollectionOptions,
@@ -12,12 +12,13 @@ import type {
12
12
  /**
13
13
  * Sort configuration for a column.
14
14
  * The `type` determines how values are compared when sorting.
15
+ * The `field` identifies the backend field name used for ordering.
15
16
  */
16
17
  export type SortConfig =
17
- | { type: "string" }
18
- | { type: "number" }
19
- | { type: "date" }
20
- | { type: "boolean" };
18
+ | { field: string; type: "string" }
19
+ | { field: string; type: "number" }
20
+ | { field: string; type: "date" }
21
+ | { field: string; type: "boolean" };
21
22
 
22
23
  /**
23
24
  * Select option for enum/dropdown filters.
@@ -30,14 +31,15 @@ export interface SelectOption {
30
31
  /**
31
32
  * Filter configuration for a column.
32
33
  * The `type` determines which operators are available.
34
+ * The `field` identifies the backend field name used for filtering.
33
35
  */
34
36
  export type FilterConfig =
35
- | { type: "string" }
36
- | { type: "number" }
37
- | { type: "date" }
38
- | { type: "enum"; options: SelectOption[] }
39
- | { type: "boolean" }
40
- | { type: "uuid" };
37
+ | { field: string; type: "string" }
38
+ | { field: string; type: "number" }
39
+ | { field: string; type: "date" }
40
+ | { field: string; type: "enum"; options: SelectOption[] }
41
+ | { field: string; type: "boolean" }
42
+ | { field: string; type: "uuid" };
41
43
 
42
44
  // =============================================================================
43
45
  // Filter Operators (Single Source of Truth)
@@ -265,114 +267,57 @@ export interface CollectionResult<T> {
265
267
  * type OrdersData = ResultOf<typeof GET_ORDERS>;
266
268
  * type Order = NodeType<OrdersData["orders"]>;
267
269
  *
268
- * const { field, display } = createColumnHelper<Order>();
270
+ * const { column } = createColumnHelper<Order>();
269
271
  * ```
270
272
  */
271
273
  export type NodeType<
272
274
  T extends { edges: { node: unknown }[] } | null | undefined,
273
275
  > = NonNullable<T>["edges"][number]["node"];
274
276
 
275
- // =============================================================================
276
- // Cell Renderer
277
- // =============================================================================
278
-
279
- /**
280
- * Props passed to a custom cell renderer.
281
- */
282
- export interface CellRendererProps<
283
- TRow extends Record<string, unknown>,
284
- TKey extends keyof TRow = never,
285
- > {
286
- /** The value of the cell */
287
- value: [TKey] extends [never] ? unknown : TRow[TKey];
288
- /** The entire row data */
289
- row: TRow;
290
- /** The row index (0-based) */
291
- rowIndex: number;
292
- /** The column definition */
293
- column: ColumnDefinition<TRow>;
294
- }
295
-
296
- /**
297
- * Custom cell renderer component.
298
- */
299
- export type CellRenderer<TRow extends Record<string, unknown>> = (
300
- props: CellRendererProps<TRow>,
301
- ) => ReactNode;
302
-
303
277
  // =============================================================================
304
278
  // Column Definitions
305
279
  // =============================================================================
306
280
 
307
281
  /**
308
- * Options for a data field column (used with `field()` helper).
282
+ * Options for defining a column (used with `column()` helper).
309
283
  */
310
- export interface FieldColumnOptions<TRow extends Record<string, unknown>> {
311
- /** Column header label */
284
+ export interface ColumnOptions<TRow extends Record<string, unknown>> {
285
+ /** Column header label (displayed in table header, column selector, filter form) */
312
286
  label?: string;
287
+ /** Render function for cell content (required) */
288
+ render: (row: TRow) => ReactNode;
289
+ /** Optional column identifier for visibility management (defaults to label) */
290
+ id?: string;
313
291
  /** Column width in pixels */
314
292
  width?: number;
293
+ /** Data accessor for CSV export and non-rendering data extraction */
294
+ accessor?: (row: TRow) => unknown;
315
295
  /** Sort configuration. When specified, sorting is enabled for this column. */
316
296
  sort?: SortConfig;
317
297
  /** Filter configuration. When specified, filtering is enabled for this column. */
318
298
  filter?: FilterConfig;
319
- /** Custom cell renderer */
320
- renderer?: CellRenderer<TRow>;
321
299
  }
322
300
 
323
301
  /**
324
- * Options for a display-only column (used with `display()` helper).
302
+ * A column definition (produced by `column()` helper).
325
303
  */
326
- export interface DisplayColumnOptions<TRow extends Record<string, unknown>> {
304
+ export interface Column<TRow extends Record<string, unknown>> {
327
305
  /** Column header label */
328
306
  label?: string;
329
- /** Column width in pixels */
330
- width?: number;
331
- /** Render function for the cell content */
307
+ /** Render function for cell content */
332
308
  render: (row: TRow) => ReactNode;
333
- }
334
-
335
- /**
336
- * A data field column definition (produced by `field()` helper).
337
- */
338
- export interface FieldColumn<TRow extends Record<string, unknown>> {
339
- kind: "field";
340
- /** Data key corresponding to a property on the row object */
341
- dataKey: string;
342
- /** Column header label */
343
- label?: string;
309
+ /** Optional column identifier for visibility management */
310
+ id?: string;
344
311
  /** Column width in pixels */
345
312
  width?: number;
313
+ /** Data accessor for CSV export and non-rendering data extraction */
314
+ accessor?: (row: TRow) => unknown;
346
315
  /** Sort configuration */
347
316
  sort?: SortConfig;
348
317
  /** Filter configuration */
349
318
  filter?: FilterConfig;
350
- /** Custom cell renderer */
351
- renderer?: CellRenderer<TRow>;
352
- }
353
-
354
- /**
355
- * A display-only column definition (produced by `display()` helper).
356
- */
357
- export interface DisplayColumn<TRow extends Record<string, unknown>> {
358
- kind: "display";
359
- /** Column identifier */
360
- id: string;
361
- /** Column header label */
362
- label?: string;
363
- /** Column width in pixels */
364
- width?: number;
365
- /** Render function for the cell content */
366
- render: (row: TRow) => ReactNode;
367
319
  }
368
320
 
369
- /**
370
- * Union of all column types.
371
- */
372
- export type Column<TRow extends Record<string, unknown>> =
373
- | FieldColumn<TRow>
374
- | DisplayColumn<TRow>;
375
-
376
321
  /**
377
322
  * Column definition used by `useDataTable` (same as `Column`).
378
323
  */
@@ -960,19 +905,13 @@ export type MatchingTableName<
960
905
  }[string & keyof TMetadata];
961
906
 
962
907
  /**
963
- * Options for metadata-based single field definition.
964
- *
965
- * @typeParam TRow - The row type for type-safe renderer access.
908
+ * Options for metadata-based single field inference.
966
909
  */
967
- export interface MetadataFieldOptions<
968
- TRow extends Record<string, unknown> = Record<string, unknown>,
969
- > {
910
+ export interface MetadataFieldOptions {
970
911
  /** Label override (defaults to metadata description or field name) */
971
912
  label?: string;
972
913
  /** Column width */
973
914
  width?: number;
974
- /** Custom cell renderer with typed row data */
975
- renderer?: CellRenderer<TRow>;
976
915
  /** Enable/disable sort (default: auto-detected from type) */
977
916
  sort?: boolean;
978
917
  /** Enable/disable filter (default: auto-detected from type) */
@@ -1143,20 +1082,23 @@ export interface PaginationProps {
1143
1082
  * Map metadata field type to SortConfig.
1144
1083
  * Returns undefined for types that don't support sorting.
1145
1084
  */
1146
- export function fieldTypeToSortConfig(type: FieldType): SortConfig | undefined {
1085
+ export function fieldTypeToSortConfig(
1086
+ field: string,
1087
+ type: FieldType,
1088
+ ): SortConfig | undefined {
1147
1089
  switch (type) {
1148
1090
  case "string":
1149
- return { type: "string" };
1091
+ return { field, type: "string" };
1150
1092
  case "number":
1151
- return { type: "number" };
1093
+ return { field, type: "number" };
1152
1094
  case "boolean":
1153
- return { type: "boolean" };
1095
+ return { field, type: "boolean" };
1154
1096
  case "datetime":
1155
1097
  case "date":
1156
1098
  case "time":
1157
- return { type: "date" };
1099
+ return { field, type: "date" };
1158
1100
  case "enum":
1159
- return { type: "string" };
1101
+ return { field, type: "string" };
1160
1102
  default:
1161
1103
  return undefined;
1162
1104
  }
@@ -1167,24 +1109,26 @@ export function fieldTypeToSortConfig(type: FieldType): SortConfig | undefined {
1167
1109
  * Returns undefined for types that don't support filtering.
1168
1110
  */
1169
1111
  export function fieldTypeToFilterConfig(
1112
+ field: string,
1170
1113
  type: FieldType,
1171
1114
  enumValues?: readonly string[],
1172
1115
  ): FilterConfig | undefined {
1173
1116
  switch (type) {
1174
1117
  case "string":
1175
- return { type: "string" };
1118
+ return { field, type: "string" };
1176
1119
  case "number":
1177
- return { type: "number" };
1120
+ return { field, type: "number" };
1178
1121
  case "boolean":
1179
- return { type: "boolean" };
1122
+ return { field, type: "boolean" };
1180
1123
  case "uuid":
1181
- return { type: "uuid" };
1124
+ return { field, type: "uuid" };
1182
1125
  case "datetime":
1183
1126
  case "date":
1184
1127
  case "time":
1185
- return { type: "date" };
1128
+ return { field, type: "date" };
1186
1129
  case "enum":
1187
1130
  return {
1131
+ field,
1188
1132
  type: "enum",
1189
1133
  options: (enumValues ?? []).map((v) => ({ value: v, label: v })),
1190
1134
  };