@izumisy-tailor/tailor-data-viewer 0.1.20 → 0.1.22
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 +55 -2
- package/dist/generator/index.d.mts +54 -31
- package/dist/generator/index.mjs +20 -13
- package/docs/app-shell-module.md +3 -3
- package/docs/compositional-api.md +366 -0
- package/package.json +1 -1
- package/src/app-shell/create-data-view-module.tsx +3 -3
- package/src/app-shell/types.ts +5 -5
- package/src/component/column-selector.test.tsx +143 -103
- package/src/component/column-selector.tsx +121 -156
- package/src/component/contexts/data-viewer-context.test.tsx +191 -0
- package/src/component/contexts/data-viewer-context.tsx +244 -0
- package/src/component/contexts/index.ts +19 -0
- package/src/component/contexts/table-data-context.tsx +114 -0
- package/src/component/contexts/toolbar-context.tsx +62 -0
- package/src/component/csv-button.tsx +79 -0
- package/src/component/data-table-toolbar.test.tsx +127 -72
- package/src/component/data-table-toolbar.tsx +14 -151
- package/src/component/data-table.tsx +255 -225
- package/src/component/data-view-tab-content.tsx +67 -138
- package/src/component/data-viewer.tsx +11 -11
- package/src/component/hooks/use-column-state.ts +2 -2
- package/src/component/index.ts +33 -1
- package/src/component/refresh-button.tsx +20 -0
- package/src/component/search-filter.tsx +19 -24
- package/src/component/single-record-tab-content.test.tsx +10 -10
- package/src/component/single-record-tab-content.tsx +62 -21
- package/src/component/view-save-load.tsx +13 -17
- package/src/generator/metadata-generator.ts +100 -67
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ import { tableMetadata } from "./generated/table-metadata";
|
|
|
52
52
|
export const dataViewModule = createDataViewModule({
|
|
53
53
|
tableMetadata,
|
|
54
54
|
appUri: import.meta.env.VITE_APP_URL,
|
|
55
|
-
|
|
55
|
+
savedViewStore: createIndexedDBStore(),
|
|
56
56
|
});
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -88,7 +88,7 @@ function App() {
|
|
|
88
88
|
return (
|
|
89
89
|
<SavedViewProvider store={store}>
|
|
90
90
|
<DataViewer
|
|
91
|
-
|
|
91
|
+
metadata={tableMetadata}
|
|
92
92
|
appUri="https://your-app.tailor.tech/graphql"
|
|
93
93
|
/>
|
|
94
94
|
</SavedViewProvider>
|
|
@@ -98,6 +98,59 @@ function App() {
|
|
|
98
98
|
|
|
99
99
|
For store options, see [Saved View Store](./docs/saved-view-store.md).
|
|
100
100
|
|
|
101
|
+
### Compositional API
|
|
102
|
+
|
|
103
|
+
For more control over the UI layout and behavior, use the compositional (compound component) API:
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import {
|
|
107
|
+
DataViewerProvider,
|
|
108
|
+
TableDataProvider,
|
|
109
|
+
ToolbarProvider,
|
|
110
|
+
DataTable,
|
|
111
|
+
ColumnSelector,
|
|
112
|
+
SearchFilterForm,
|
|
113
|
+
CsvButton,
|
|
114
|
+
RefreshButton,
|
|
115
|
+
} from "@izumisy-tailor/tailor-data-viewer/component";
|
|
116
|
+
|
|
117
|
+
function CustomDataViewer() {
|
|
118
|
+
return (
|
|
119
|
+
<DataViewerProvider
|
|
120
|
+
tableName="User"
|
|
121
|
+
metadata={tableMetadata}
|
|
122
|
+
appUri="https://your-app.tailor.tech"
|
|
123
|
+
>
|
|
124
|
+
<TableDataProvider>
|
|
125
|
+
<ToolbarProvider>
|
|
126
|
+
<ColumnSelector />
|
|
127
|
+
<SearchFilterForm />
|
|
128
|
+
<CsvButton />
|
|
129
|
+
<RefreshButton />
|
|
130
|
+
</ToolbarProvider>
|
|
131
|
+
<DataTable onRecordClick={(id) => console.log(id)} />
|
|
132
|
+
</TableDataProvider>
|
|
133
|
+
</DataViewerProvider>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
This approach allows you to:
|
|
139
|
+
- Customize the layout and styling
|
|
140
|
+
- Add your own components alongside Data Viewer components
|
|
141
|
+
- Access internal state via hooks (`useDataViewer`, `useTableDataContext`)
|
|
142
|
+
|
|
143
|
+
For detailed documentation, see [Compositional API](./docs/compositional-api.md).
|
|
144
|
+
|
|
145
|
+
### About savedViewStore
|
|
146
|
+
|
|
147
|
+
`savedViewStore` is a storage implementation for persisting views created in Data Viewer (such as filter settings and column visibility settings). Two built-in stores are provided:
|
|
148
|
+
|
|
149
|
+
- **IndexedDB Store** (`createIndexedDBStore`): Stores data in browser's IndexedDB. Ideal for development, personal use, and offline support
|
|
150
|
+
- **TailorDB Store** (`createTailorDBStore`): Stores data on the server side. Ideal for sharing views across teams
|
|
151
|
+
|
|
152
|
+
For more details, see [Saved View Store](./docs/saved-view-store.md).
|
|
153
|
+
|
|
101
154
|
### Generating Table Metadata
|
|
102
155
|
|
|
103
156
|
This library includes a custom generator for [Tailor Platform SDK](https://www.npmjs.com/package/@tailor-platform/sdk) that automatically generates table metadata from your TailorDB schema.
|
|
@@ -7,51 +7,74 @@ type FieldType = "string" | "number" | "boolean" | "uuid" | "datetime" | "date"
|
|
|
7
7
|
* Metadata for a single field
|
|
8
8
|
*/
|
|
9
9
|
interface FieldMetadata {
|
|
10
|
-
name: string;
|
|
11
|
-
type: FieldType;
|
|
12
|
-
required: boolean;
|
|
13
|
-
enumValues?: readonly string[];
|
|
14
|
-
arrayItemType?: FieldType;
|
|
15
|
-
description?: string;
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly type: FieldType;
|
|
12
|
+
readonly required: boolean;
|
|
13
|
+
readonly enumValues?: readonly string[];
|
|
14
|
+
readonly arrayItemType?: FieldType;
|
|
15
|
+
readonly description?: string;
|
|
16
16
|
/** manyToOne relation info (if this field is a foreign key) */
|
|
17
|
-
relation?: {
|
|
18
|
-
/** GraphQL field name for the related object (e.g., "task") */fieldName: string; /** Target table name in camelCase (e.g., "task") */
|
|
19
|
-
targetTable: string;
|
|
17
|
+
readonly relation?: {
|
|
18
|
+
/** GraphQL field name for the related object (e.g., "task") */readonly fieldName: string; /** Target table name in camelCase (e.g., "task") */
|
|
19
|
+
readonly targetTable: string;
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
*
|
|
23
|
+
* Base properties shared by all relation types
|
|
24
24
|
*/
|
|
25
|
-
interface
|
|
26
|
-
/** GraphQL field name (e.g., "task" for manyToOne, "taskAssignments" for oneToMany) */
|
|
27
|
-
fieldName: string;
|
|
25
|
+
interface RelationMetadataBase {
|
|
26
|
+
/** GraphQL field name (e.g., "task" for manyToOne/oneToOne, "taskAssignments" for oneToMany) */
|
|
27
|
+
readonly fieldName: string;
|
|
28
28
|
/** Target table name in camelCase (e.g., "task", "taskAssignment") */
|
|
29
|
-
targetTable: string;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
readonly targetTable: string;
|
|
30
|
+
/** The FK field name (e.g., "taskId") */
|
|
31
|
+
readonly foreignKeyField: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* ManyToOne relation: this table has a FK pointing to another table
|
|
35
|
+
* Generates an inverse oneToMany relation on the target table
|
|
36
|
+
*/
|
|
37
|
+
interface ManyToOneRelation extends RelationMetadataBase {
|
|
38
|
+
readonly relationType: "manyToOne";
|
|
39
|
+
/** The backward field name on the target type (used to generate oneToMany relation) */
|
|
40
|
+
readonly backwardFieldName?: string;
|
|
38
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* OneToOne relation: this table has a FK pointing to another table (1:1)
|
|
44
|
+
* Does NOT generate an inverse oneToMany relation
|
|
45
|
+
*/
|
|
46
|
+
interface OneToOneRelation extends RelationMetadataBase {
|
|
47
|
+
readonly relationType: "oneToOne";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* OneToMany relation: another table has a FK pointing to this table
|
|
51
|
+
* Generated automatically from manyToOne relations
|
|
52
|
+
*/
|
|
53
|
+
interface OneToManyRelation extends RelationMetadataBase {
|
|
54
|
+
readonly relationType: "oneToMany";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Metadata for a relation (discriminated union)
|
|
58
|
+
*/
|
|
59
|
+
type RelationMetadata = ManyToOneRelation | OneToOneRelation | OneToManyRelation;
|
|
39
60
|
/**
|
|
40
61
|
* Metadata for a single table
|
|
41
62
|
*/
|
|
42
63
|
interface TableMetadata {
|
|
43
|
-
name: string;
|
|
44
|
-
pluralForm: string;
|
|
45
|
-
description?: string;
|
|
46
|
-
readAllowedRoles: string[];
|
|
47
|
-
fields: FieldMetadata[];
|
|
48
|
-
/** Relations (manyToOne and oneToMany) */
|
|
49
|
-
relations?: RelationMetadata[];
|
|
64
|
+
readonly name: string;
|
|
65
|
+
readonly pluralForm: string;
|
|
66
|
+
readonly description?: string;
|
|
67
|
+
readonly readAllowedRoles: readonly string[];
|
|
68
|
+
readonly fields: readonly FieldMetadata[];
|
|
69
|
+
/** Relations (manyToOne, oneToOne, and oneToMany) */
|
|
70
|
+
readonly relations?: readonly RelationMetadata[];
|
|
50
71
|
}
|
|
51
72
|
/**
|
|
52
73
|
* Map of all tables
|
|
53
74
|
*/
|
|
54
|
-
type TableMetadataMap =
|
|
75
|
+
type TableMetadataMap = {
|
|
76
|
+
readonly [key: string]: TableMetadata;
|
|
77
|
+
};
|
|
55
78
|
/**
|
|
56
79
|
* Expanded relation fields configuration
|
|
57
80
|
* Key: relation field name (e.g., "task")
|
|
@@ -209,4 +232,4 @@ declare const tableMetadataGenerator: {
|
|
|
209
232
|
}): GeneratorResult;
|
|
210
233
|
};
|
|
211
234
|
//#endregion
|
|
212
|
-
export { DataViewerMetadataGeneratorOptions, ExpandedRelationFields, FieldMetadata, FieldType, RelationMetadata, TableMetadata, TableMetadataMap, dataViewerMetadataGenerator, tableMetadataGenerator as default, tableMetadataGenerator };
|
|
235
|
+
export { DataViewerMetadataGeneratorOptions, ExpandedRelationFields, FieldMetadata, FieldType, ManyToOneRelation, OneToManyRelation, OneToOneRelation, RelationMetadata, TableMetadata, TableMetadataMap, dataViewerMetadataGenerator, tableMetadataGenerator as default, tableMetadataGenerator };
|
package/dist/generator/index.mjs
CHANGED
|
@@ -62,33 +62,41 @@ function dataViewerMetadataGenerator(options = {}) {
|
|
|
62
62
|
for (const [fieldName, field] of Object.entries(type.fields)) {
|
|
63
63
|
const config = field.config;
|
|
64
64
|
const { type: fieldType, arrayItemType } = mapFieldType(config.type, config.array);
|
|
65
|
-
const fieldMetadata = {
|
|
66
|
-
name: fieldName,
|
|
67
|
-
type: fieldType,
|
|
68
|
-
required: config.required ?? false,
|
|
69
|
-
description: config.description
|
|
70
|
-
};
|
|
71
|
-
if (config.allowedValues && config.allowedValues.length > 0) fieldMetadata.enumValues = config.allowedValues.map((v) => typeof v === "string" ? v : v.value);
|
|
72
|
-
if (arrayItemType) fieldMetadata.arrayItemType = arrayItemType;
|
|
73
65
|
const rawRelation = config.rawRelation;
|
|
74
66
|
const isManyToOneRelation = rawRelation && (rawRelation.type === "manyToOne" || rawRelation.type === "n-1" || rawRelation.type === "oneToOne" || rawRelation.type === "1-1") && rawRelation.toward;
|
|
75
67
|
const isOneToOne = rawRelation?.type === "oneToOne" || rawRelation?.type === "1-1";
|
|
68
|
+
let fieldRelation;
|
|
76
69
|
if (isManyToOneRelation && rawRelation.toward) {
|
|
77
70
|
const targetTableName = toCamelCase(rawRelation.toward.type);
|
|
78
71
|
const relationFieldName = rawRelation.toward.as ?? toCamelCase(rawRelation.toward.type);
|
|
79
|
-
|
|
72
|
+
fieldRelation = {
|
|
80
73
|
fieldName: relationFieldName,
|
|
81
74
|
targetTable: targetTableName
|
|
82
75
|
};
|
|
83
|
-
relations.push({
|
|
76
|
+
if (isOneToOne) relations.push({
|
|
77
|
+
fieldName: relationFieldName,
|
|
78
|
+
targetTable: targetTableName,
|
|
79
|
+
relationType: "oneToOne",
|
|
80
|
+
foreignKeyField: fieldName
|
|
81
|
+
});
|
|
82
|
+
else relations.push({
|
|
84
83
|
fieldName: relationFieldName,
|
|
85
84
|
targetTable: targetTableName,
|
|
86
85
|
relationType: "manyToOne",
|
|
87
86
|
foreignKeyField: fieldName,
|
|
88
|
-
backwardFieldName: rawRelation.backward
|
|
89
|
-
isOneToOne
|
|
87
|
+
backwardFieldName: rawRelation.backward
|
|
90
88
|
});
|
|
91
89
|
}
|
|
90
|
+
const enumValues = config.allowedValues && config.allowedValues.length > 0 ? config.allowedValues.map((v) => typeof v === "string" ? v : v.value) : void 0;
|
|
91
|
+
const fieldMetadata = {
|
|
92
|
+
name: fieldName,
|
|
93
|
+
type: fieldType,
|
|
94
|
+
required: config.required ?? false,
|
|
95
|
+
...config.description && { description: config.description },
|
|
96
|
+
...enumValues && { enumValues },
|
|
97
|
+
...arrayItemType && { arrayItemType },
|
|
98
|
+
...fieldRelation && { relation: fieldRelation }
|
|
99
|
+
};
|
|
92
100
|
fields.push(fieldMetadata);
|
|
93
101
|
}
|
|
94
102
|
const readAllowedRoles = extractReadAllowedRoles(type.permissions.gql);
|
|
@@ -116,7 +124,6 @@ function dataViewerMetadataGenerator(options = {}) {
|
|
|
116
124
|
const tableByOriginalName = /* @__PURE__ */ new Map();
|
|
117
125
|
for (const table of allTables) tableByOriginalName.set(table.originalName, table);
|
|
118
126
|
for (const table of allTables) for (const relation of table.relations) if (relation.relationType === "manyToOne") {
|
|
119
|
-
if (relation.isOneToOne) continue;
|
|
120
127
|
const targetTable = tableByOriginalName.get(relation.targetTable.charAt(0).toUpperCase() + relation.targetTable.slice(1));
|
|
121
128
|
if (targetTable) {
|
|
122
129
|
const oneToManyFieldName = relation.backwardFieldName ?? table.pluralForm;
|
package/docs/app-shell-module.md
CHANGED
|
@@ -21,7 +21,7 @@ import { tableMetadata } from "@tailor-platform/my-app/generated/table-metadata"
|
|
|
21
21
|
export const dataViewModule = createDataViewModule({
|
|
22
22
|
tableMetadata,
|
|
23
23
|
appUri: import.meta.env.VITE_APP_URL,
|
|
24
|
-
|
|
24
|
+
savedViewStore: createIndexedDBStore(),
|
|
25
25
|
});
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -36,7 +36,7 @@ import { tableMetadata } from "@tailor-platform/my-app/generated/table-metadata"
|
|
|
36
36
|
export const dataViewModule = createDataViewModule({
|
|
37
37
|
tableMetadata,
|
|
38
38
|
appUri: import.meta.env.VITE_APP_URL,
|
|
39
|
-
|
|
39
|
+
savedViewStore: createTailorDBStore(),
|
|
40
40
|
});
|
|
41
41
|
```
|
|
42
42
|
|
|
@@ -68,7 +68,7 @@ interface DataViewModuleConfig {
|
|
|
68
68
|
appUri: string;
|
|
69
69
|
|
|
70
70
|
/** Store implementation for view persistence (required) */
|
|
71
|
-
|
|
71
|
+
savedViewStore: SavedViewStore | SavedViewStoreFactory;
|
|
72
72
|
|
|
73
73
|
/** Routing configuration (optional) */
|
|
74
74
|
path?: {
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# Compositional Component API
|
|
2
|
+
|
|
3
|
+
Data Viewer provides a compositional (compound component) API that allows you to build custom data viewing interfaces by composing individual components. This approach gives you full control over the layout and behavior of your data viewer.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The compositional API is built around React Context providers that share state between components:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
DataViewerProvider ← Metadata, column state, filters
|
|
11
|
+
└── TableDataProvider ← Data fetching, pagination, sorting
|
|
12
|
+
└── ToolbarProvider ← Toolbar panel state (exclusive open/close)
|
|
13
|
+
└── Components (ColumnSelector, SearchFilterForm, etc.)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import {
|
|
20
|
+
DataViewerProvider,
|
|
21
|
+
TableDataProvider,
|
|
22
|
+
ToolbarProvider,
|
|
23
|
+
DataTable,
|
|
24
|
+
ColumnSelector,
|
|
25
|
+
SearchFilterForm,
|
|
26
|
+
CsvButton,
|
|
27
|
+
RefreshButton,
|
|
28
|
+
} from "@izumisy-tailor/tailor-data-viewer/component";
|
|
29
|
+
import { tableMetadata } from "./generated/table-metadata";
|
|
30
|
+
|
|
31
|
+
function MyDataViewer() {
|
|
32
|
+
return (
|
|
33
|
+
<DataViewerProvider
|
|
34
|
+
tableName="User"
|
|
35
|
+
metadata={tableMetadata}
|
|
36
|
+
appUri="https://your-app.tailor.tech"
|
|
37
|
+
>
|
|
38
|
+
<TableDataProvider>
|
|
39
|
+
{/* Toolbar */}
|
|
40
|
+
<ToolbarProvider>
|
|
41
|
+
<div className="flex gap-2 mb-4">
|
|
42
|
+
<ColumnSelector />
|
|
43
|
+
<SearchFilterForm />
|
|
44
|
+
<CsvButton />
|
|
45
|
+
<RefreshButton />
|
|
46
|
+
</div>
|
|
47
|
+
</ToolbarProvider>
|
|
48
|
+
|
|
49
|
+
{/* Data Table */}
|
|
50
|
+
<DataTable onRecordClick={(id) => console.log("Clicked:", id)} />
|
|
51
|
+
</TableDataProvider>
|
|
52
|
+
</DataViewerProvider>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Providers
|
|
58
|
+
|
|
59
|
+
### DataViewerProvider
|
|
60
|
+
|
|
61
|
+
The root provider that manages metadata, column state, and filters.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
interface DataViewerProviderProps {
|
|
65
|
+
children: ReactNode;
|
|
66
|
+
/** Table name to display */
|
|
67
|
+
tableName: string;
|
|
68
|
+
/** All table metadata (generated from TailorDB schema) */
|
|
69
|
+
metadata: TableMetadataMap;
|
|
70
|
+
/** App URI for GraphQL endpoint */
|
|
71
|
+
appUri: string;
|
|
72
|
+
/** Initial data for filters, selected fields, and relations */
|
|
73
|
+
initialData?: DataViewerInitialData;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface DataViewerInitialData {
|
|
77
|
+
filters?: SearchFilters;
|
|
78
|
+
selectedFields?: string[];
|
|
79
|
+
selectedRelations?: string[];
|
|
80
|
+
expandedRelationFields?: ExpandedRelationFields;
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Example:**
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<DataViewerProvider
|
|
88
|
+
tableName="Task"
|
|
89
|
+
metadata={tableMetadata}
|
|
90
|
+
appUri="https://your-app.tailor.tech"
|
|
91
|
+
initialData={{
|
|
92
|
+
filters: [{ field: "status", fieldType: "enum", value: "active" }],
|
|
93
|
+
selectedFields: ["id", "title", "status", "createdAt"],
|
|
94
|
+
selectedRelations: ["assignee"],
|
|
95
|
+
expandedRelationFields: { assignee: ["name", "email"] },
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</DataViewerProvider>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### TableDataProvider
|
|
103
|
+
|
|
104
|
+
Handles data fetching, pagination, and sorting. Must be a child of `DataViewerProvider`.
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
<DataViewerProvider {...props}>
|
|
108
|
+
<TableDataProvider>
|
|
109
|
+
{/* Components that need data access */}
|
|
110
|
+
</TableDataProvider>
|
|
111
|
+
</DataViewerProvider>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### ToolbarProvider
|
|
115
|
+
|
|
116
|
+
Manages exclusive panel state for toolbar components (only one panel can be open at a time).
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<ToolbarProvider>
|
|
120
|
+
<ColumnSelector /> {/* Opens column selection panel */}
|
|
121
|
+
<SearchFilterForm /> {/* Opens search filter panel */}
|
|
122
|
+
<CsvButton /> {/* No panel, just action */}
|
|
123
|
+
<RefreshButton /> {/* No panel, just action */}
|
|
124
|
+
</ToolbarProvider>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Components
|
|
128
|
+
|
|
129
|
+
### DataTable
|
|
130
|
+
|
|
131
|
+
Displays data in a sortable table with expandable relation cells.
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
interface DataTableProps {
|
|
135
|
+
/** Callback when a record row is clicked */
|
|
136
|
+
onRecordClick?: (recordId: string) => void;
|
|
137
|
+
/** Callback to open a relation as a new sheet */
|
|
138
|
+
onOpenAsSheet?: (tableName: string, filterField: string, filterValue: string) => void;
|
|
139
|
+
/** Callback to open a single record as a new sheet */
|
|
140
|
+
onOpenSingleRecordAsSheet?: (tableName: string, recordId: string) => void;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Example:**
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
<DataTable
|
|
148
|
+
onRecordClick={(id) => navigate(`/records/${id}`)}
|
|
149
|
+
onOpenAsSheet={(table, field, value) => openNewTab(table, field, value)}
|
|
150
|
+
/>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### ColumnSelector
|
|
154
|
+
|
|
155
|
+
Dropdown for selecting visible columns and relation fields.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
<ToolbarProvider>
|
|
159
|
+
<ColumnSelector />
|
|
160
|
+
</ToolbarProvider>
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Features:
|
|
164
|
+
- Select/deselect individual fields
|
|
165
|
+
- Select all / Deselect all
|
|
166
|
+
- ManyToOne relation field expansion (inline columns)
|
|
167
|
+
- OneToMany relation selection (expandable rows)
|
|
168
|
+
|
|
169
|
+
### SearchFilterForm
|
|
170
|
+
|
|
171
|
+
Form for adding search filters with AND logic.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<ToolbarProvider>
|
|
175
|
+
<SearchFilterForm />
|
|
176
|
+
</ToolbarProvider>
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Features:
|
|
180
|
+
- Supports string, number, boolean, enum, and UUID fields
|
|
181
|
+
- Multiple filters with AND logic
|
|
182
|
+
- Remove individual filters
|
|
183
|
+
|
|
184
|
+
### CsvButton
|
|
185
|
+
|
|
186
|
+
Downloads current view data as CSV.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
<TableDataProvider>
|
|
190
|
+
<CsvButton />
|
|
191
|
+
</TableDataProvider>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### RefreshButton
|
|
195
|
+
|
|
196
|
+
Refetches current data.
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
<TableDataProvider>
|
|
200
|
+
<RefreshButton />
|
|
201
|
+
</TableDataProvider>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Can also be used outside TableDataProvider with explicit props:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
<RefreshButton loading={isLoading} refetch={handleRefetch} />
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Hooks
|
|
211
|
+
|
|
212
|
+
### useDataViewer
|
|
213
|
+
|
|
214
|
+
Access the DataViewer context values.
|
|
215
|
+
|
|
216
|
+
```tsx
|
|
217
|
+
const {
|
|
218
|
+
tableMetadata,
|
|
219
|
+
metadata,
|
|
220
|
+
appUri,
|
|
221
|
+
selectedFields,
|
|
222
|
+
toggleField,
|
|
223
|
+
selectAllFields,
|
|
224
|
+
deselectAllFields,
|
|
225
|
+
selectedRelations,
|
|
226
|
+
toggleRelation,
|
|
227
|
+
expandedRelationFields,
|
|
228
|
+
filters,
|
|
229
|
+
setFilters,
|
|
230
|
+
refetch,
|
|
231
|
+
} = useDataViewer();
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### useTableDataContext
|
|
235
|
+
|
|
236
|
+
Access data fetching state and pagination controls.
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
const {
|
|
240
|
+
data,
|
|
241
|
+
loading,
|
|
242
|
+
error,
|
|
243
|
+
sortState,
|
|
244
|
+
setSort,
|
|
245
|
+
pagination,
|
|
246
|
+
hasPreviousPage,
|
|
247
|
+
nextPage,
|
|
248
|
+
previousPage,
|
|
249
|
+
} = useTableDataContext();
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### useToolbar
|
|
253
|
+
|
|
254
|
+
Access toolbar panel state.
|
|
255
|
+
|
|
256
|
+
```tsx
|
|
257
|
+
const {
|
|
258
|
+
activePanel, // "column" | "search" | null
|
|
259
|
+
setActivePanel,
|
|
260
|
+
} = useToolbar();
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Advanced Examples
|
|
264
|
+
|
|
265
|
+
### Custom Toolbar Layout
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
function CustomToolbar() {
|
|
269
|
+
return (
|
|
270
|
+
<ToolbarProvider>
|
|
271
|
+
<div className="flex justify-between items-center p-4 border-b">
|
|
272
|
+
<div className="flex gap-2">
|
|
273
|
+
<ColumnSelector />
|
|
274
|
+
<SearchFilterForm />
|
|
275
|
+
</div>
|
|
276
|
+
<div className="flex gap-2">
|
|
277
|
+
<CsvButton />
|
|
278
|
+
<RefreshButton />
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</ToolbarProvider>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Building a Custom Filter Display
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
function ActiveFilters() {
|
|
290
|
+
const { filters, setFilters } = useDataViewer();
|
|
291
|
+
|
|
292
|
+
const removeFilter = (fieldName: string) => {
|
|
293
|
+
setFilters(filters.filter(f => f.field !== fieldName));
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<div className="flex gap-2">
|
|
298
|
+
{filters.map(filter => (
|
|
299
|
+
<Badge key={filter.field}>
|
|
300
|
+
{filter.field}: {filter.value}
|
|
301
|
+
<button onClick={() => removeFilter(filter.field)}>×</button>
|
|
302
|
+
</Badge>
|
|
303
|
+
))}
|
|
304
|
+
</div>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Custom Data Display
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
function CardView() {
|
|
313
|
+
const { tableMetadata, selectedFields } = useDataViewer();
|
|
314
|
+
const { data, loading } = useTableDataContext();
|
|
315
|
+
|
|
316
|
+
if (loading) return <Spinner />;
|
|
317
|
+
|
|
318
|
+
return (
|
|
319
|
+
<div className="grid grid-cols-3 gap-4">
|
|
320
|
+
{data.map(record => (
|
|
321
|
+
<Card key={record.id as string}>
|
|
322
|
+
{selectedFields.map(field => (
|
|
323
|
+
<div key={field}>
|
|
324
|
+
<strong>{field}:</strong> {String(record[field])}
|
|
325
|
+
</div>
|
|
326
|
+
))}
|
|
327
|
+
</Card>
|
|
328
|
+
))}
|
|
329
|
+
</div>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Combining with External State
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
function DataViewerWithExternalState() {
|
|
338
|
+
const [selectedRecordId, setSelectedRecordId] = useState<string | null>(null);
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div className="flex">
|
|
342
|
+
<div className="w-2/3">
|
|
343
|
+
<DataViewerProvider
|
|
344
|
+
tableName="Task"
|
|
345
|
+
metadata={tableMetadata}
|
|
346
|
+
appUri={appUri}
|
|
347
|
+
>
|
|
348
|
+
<TableDataProvider>
|
|
349
|
+
<ToolbarProvider>
|
|
350
|
+
<ColumnSelector />
|
|
351
|
+
<SearchFilterForm />
|
|
352
|
+
</ToolbarProvider>
|
|
353
|
+
<DataTable onRecordClick={setSelectedRecordId} />
|
|
354
|
+
</TableDataProvider>
|
|
355
|
+
</DataViewerProvider>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
<div className="w-1/3">
|
|
359
|
+
{selectedRecordId && (
|
|
360
|
+
<RecordDetailPanel recordId={selectedRecordId} />
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
```
|
package/package.json
CHANGED
|
@@ -36,7 +36,7 @@ import { Badge } from "../component/ui/badge";
|
|
|
36
36
|
* export const dataViewModule = createDataViewModule({
|
|
37
37
|
* tableMetadata,
|
|
38
38
|
* appUri: import.meta.env.VITE_APP_URL,
|
|
39
|
-
*
|
|
39
|
+
* savedViewStore: createIndexedDBStore(),
|
|
40
40
|
* });
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
@@ -44,7 +44,7 @@ export function createDataViewModule(config: DataViewModuleConfig) {
|
|
|
44
44
|
const {
|
|
45
45
|
tableMetadata,
|
|
46
46
|
appUri,
|
|
47
|
-
|
|
47
|
+
savedViewStore: storeOrFactory,
|
|
48
48
|
path = {},
|
|
49
49
|
meta = {},
|
|
50
50
|
} = config;
|
|
@@ -67,7 +67,7 @@ export function createDataViewModule(config: DataViewModuleConfig) {
|
|
|
67
67
|
return (
|
|
68
68
|
<SavedViewProvider store={store}>
|
|
69
69
|
<DataViewer
|
|
70
|
-
|
|
70
|
+
metadata={tableMetadata}
|
|
71
71
|
appUri={appUri}
|
|
72
72
|
initialViewId={viewId}
|
|
73
73
|
/>
|