@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.
- package/README.md +45 -1
- package/dist/generator/index.d.mts +54 -31
- package/dist/generator/index.mjs +20 -13
- package/docs/compositional-api.md +366 -0
- package/package.json +1 -1
- package/src/app-shell/create-data-view-module.tsx +1 -1
- 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 +68 -138
- package/src/component/data-viewer.tsx +11 -11
- package/src/component/hooks/use-column-state.ts +2 -2
- package/src/component/hooks/use-table-data.test.ts +399 -0
- package/src/component/hooks/use-table-data.ts +24 -7
- package/src/component/index.ts +43 -1
- package/src/component/refresh-button.tsx +20 -0
- package/src/component/saved-view-context.tsx +31 -2
- package/src/component/search-filter.test.tsx +612 -0
- package/src/component/search-filter.tsx +168 -33
- package/src/component/single-record-tab-content.test.tsx +10 -10
- package/src/component/single-record-tab-content.tsx +62 -21
- package/src/component/types.ts +78 -0
- package/src/component/view-save-load.tsx +13 -17
- package/src/generator/metadata-generator.ts +100 -67
|
@@ -3,15 +3,18 @@ import { Alert, AlertDescription } from "./ui/alert";
|
|
|
3
3
|
import type {
|
|
4
4
|
TableMetadata,
|
|
5
5
|
TableMetadataMap,
|
|
6
|
-
FieldMetadata,
|
|
7
6
|
} from "../generator/metadata-generator";
|
|
8
|
-
import { formatFieldValue } from "../graphql/query-builder";
|
|
9
7
|
import { TableSelector } from "./table-selector";
|
|
10
8
|
import { DataTable } from "./data-table";
|
|
11
9
|
import { DataTableToolbar } from "./data-table-toolbar";
|
|
12
10
|
import { Pagination } from "./pagination";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
11
|
+
import { ColumnSelector } from "./column-selector";
|
|
12
|
+
import { SearchFilterForm } from "./search-filter";
|
|
13
|
+
import { ViewSave } from "./view-save-load";
|
|
14
|
+
import { CsvButton } from "./csv-button";
|
|
15
|
+
import { RefreshButton } from "./refresh-button";
|
|
16
|
+
import { DataViewerProvider } from "./contexts";
|
|
17
|
+
import { TableDataProvider, useTableDataContext } from "./contexts";
|
|
15
18
|
import type { InitialQuery } from "./data-viewer";
|
|
16
19
|
import type { SearchFilters } from "./types";
|
|
17
20
|
import type { SavedView } from "./saved-view-context";
|
|
@@ -22,8 +25,8 @@ interface DataViewTabContentProps {
|
|
|
22
25
|
initialTable?: TableMetadata;
|
|
23
26
|
onTableConfirm: (table: TableMetadata) => void;
|
|
24
27
|
isTableLocked: boolean;
|
|
25
|
-
/** All table metadata
|
|
26
|
-
|
|
28
|
+
/** All table metadata (generated from TailorDB schema) */
|
|
29
|
+
metadata: TableMetadataMap;
|
|
27
30
|
/** Initial query condition for filtering */
|
|
28
31
|
initialQuery?: InitialQuery;
|
|
29
32
|
/** Initial saved view to load */
|
|
@@ -51,7 +54,7 @@ export function DataViewTabContent({
|
|
|
51
54
|
initialTable,
|
|
52
55
|
onTableConfirm,
|
|
53
56
|
isTableLocked,
|
|
54
|
-
|
|
57
|
+
metadata,
|
|
55
58
|
initialQuery,
|
|
56
59
|
initialView,
|
|
57
60
|
onOpenAsSheet,
|
|
@@ -79,6 +82,7 @@ export function DataViewTabContent({
|
|
|
79
82
|
{
|
|
80
83
|
field: initialQuery.field,
|
|
81
84
|
fieldType: field.type,
|
|
85
|
+
operator: "eq" as const,
|
|
82
86
|
value: initialQuery.value,
|
|
83
87
|
enumValues: field.enumValues,
|
|
84
88
|
},
|
|
@@ -86,94 +90,11 @@ export function DataViewTabContent({
|
|
|
86
90
|
}, [initialQuery, initialTable, initialView]);
|
|
87
91
|
|
|
88
92
|
// Search filters (includes initialQuery as a regular filter that can be edited/removed)
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Memoize fields to avoid creating new array reference on every render when selectedTable is null
|
|
93
|
-
const fields = useMemo(
|
|
94
|
-
() => selectedTable?.fields ?? [],
|
|
95
|
-
[selectedTable?.fields],
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
// Column state (includes relation selection)
|
|
99
|
-
// If initialView is provided, use its column selections as defaults
|
|
100
|
-
const columnState = useColumnState(
|
|
101
|
-
fields,
|
|
102
|
-
selectedTable?.relations,
|
|
103
|
-
initialView?.selectedFields,
|
|
104
|
-
initialView?.selectedRelations,
|
|
105
|
-
initialView?.expandedRelationFields,
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
// Table data fetching (pass selectedRelations for FK field inclusion and search filters)
|
|
109
|
-
const tableData = useTableData(
|
|
110
|
-
appUri,
|
|
111
|
-
selectedTable,
|
|
112
|
-
columnState.selectedFields,
|
|
113
|
-
columnState.selectedRelations,
|
|
114
|
-
searchFilters,
|
|
115
|
-
tableMetadataMap,
|
|
116
|
-
columnState.expandedRelationFields,
|
|
93
|
+
const initialFilters = useMemo(
|
|
94
|
+
() => getInitialFilters(),
|
|
95
|
+
[getInitialFilters],
|
|
117
96
|
);
|
|
118
97
|
|
|
119
|
-
// Handle filter changes - reset pagination when filters change
|
|
120
|
-
const handleFiltersChange = useCallback(
|
|
121
|
-
(newFilters: SearchFilters) => {
|
|
122
|
-
setSearchFilters(newFilters);
|
|
123
|
-
tableData.resetPagination();
|
|
124
|
-
},
|
|
125
|
-
[tableData],
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// CSV Download handler
|
|
129
|
-
const handleDownloadCsv = useCallback(() => {
|
|
130
|
-
if (!selectedTable || tableData.data.length === 0) return;
|
|
131
|
-
|
|
132
|
-
const selectedFieldsMetadata = selectedTable.fields.filter(
|
|
133
|
-
(f: FieldMetadata) => columnState.selectedFields.includes(f.name),
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
// Create CSV header
|
|
137
|
-
const headers = selectedFieldsMetadata.map((f: FieldMetadata) => f.name);
|
|
138
|
-
|
|
139
|
-
// Create CSV rows
|
|
140
|
-
const rows = tableData.data.map((row: Record<string, unknown>) =>
|
|
141
|
-
selectedFieldsMetadata.map((field: FieldMetadata) => {
|
|
142
|
-
const value = row[field.name];
|
|
143
|
-
const formattedValue = formatFieldValue(value, field);
|
|
144
|
-
// Escape double quotes and wrap in quotes if contains comma, newline, or quotes
|
|
145
|
-
const stringValue = String(formattedValue ?? "");
|
|
146
|
-
if (
|
|
147
|
-
stringValue.includes(",") ||
|
|
148
|
-
stringValue.includes("\n") ||
|
|
149
|
-
stringValue.includes('"')
|
|
150
|
-
) {
|
|
151
|
-
return `"${stringValue.replace(/"/g, '""')}"`;
|
|
152
|
-
}
|
|
153
|
-
return stringValue;
|
|
154
|
-
}),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
// Combine headers and rows
|
|
158
|
-
const csvContent = [
|
|
159
|
-
headers.join(","),
|
|
160
|
-
...rows.map((r: string[]) => r.join(",")),
|
|
161
|
-
].join("\n");
|
|
162
|
-
|
|
163
|
-
// Create and download file
|
|
164
|
-
const blob = new Blob(["\uFEFF" + csvContent], {
|
|
165
|
-
type: "text/csv;charset=utf-8;",
|
|
166
|
-
});
|
|
167
|
-
const url = URL.createObjectURL(blob);
|
|
168
|
-
const link = document.createElement("a");
|
|
169
|
-
link.href = url;
|
|
170
|
-
link.download = `${selectedTable.name}_${new Date().toISOString().slice(0, 10)}.csv`;
|
|
171
|
-
document.body.appendChild(link);
|
|
172
|
-
link.click();
|
|
173
|
-
document.body.removeChild(link);
|
|
174
|
-
URL.revokeObjectURL(url);
|
|
175
|
-
}, [selectedTable, tableData.data, columnState.selectedFields]);
|
|
176
|
-
|
|
177
98
|
const handleTableSelect = useCallback(
|
|
178
99
|
(table: TableMetadata) => {
|
|
179
100
|
setSelectedTable(table);
|
|
@@ -204,6 +125,52 @@ export function DataViewTabContent({
|
|
|
204
125
|
);
|
|
205
126
|
}
|
|
206
127
|
|
|
128
|
+
return (
|
|
129
|
+
<DataViewerProvider
|
|
130
|
+
appUri={appUri}
|
|
131
|
+
tableName={selectedTable.name}
|
|
132
|
+
metadata={metadata}
|
|
133
|
+
initialData={{
|
|
134
|
+
filters: initialFilters,
|
|
135
|
+
selectedFields: initialView?.selectedFields,
|
|
136
|
+
selectedRelations: initialView?.selectedRelations,
|
|
137
|
+
expandedRelationFields: initialView?.expandedRelationFields,
|
|
138
|
+
}}
|
|
139
|
+
>
|
|
140
|
+
<TableDataProvider>
|
|
141
|
+
<DataViewTabContentInner
|
|
142
|
+
selectedTable={selectedTable}
|
|
143
|
+
onOpenAsSheet={onOpenAsSheet}
|
|
144
|
+
onOpenSingleRecordAsSheet={onOpenSingleRecordAsSheet}
|
|
145
|
+
/>
|
|
146
|
+
</TableDataProvider>
|
|
147
|
+
</DataViewerProvider>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface DataViewTabContentInnerProps {
|
|
152
|
+
selectedTable: TableMetadata;
|
|
153
|
+
onOpenAsSheet?: (
|
|
154
|
+
targetTableName: string,
|
|
155
|
+
filterField: string,
|
|
156
|
+
filterValue: string,
|
|
157
|
+
) => void;
|
|
158
|
+
onOpenSingleRecordAsSheet?: (
|
|
159
|
+
targetTableName: string,
|
|
160
|
+
recordId: string,
|
|
161
|
+
) => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Inner component that uses context
|
|
166
|
+
*/
|
|
167
|
+
function DataViewTabContentInner({
|
|
168
|
+
selectedTable,
|
|
169
|
+
onOpenAsSheet,
|
|
170
|
+
onOpenSingleRecordAsSheet,
|
|
171
|
+
}: DataViewTabContentInnerProps) {
|
|
172
|
+
const tableData = useTableDataContext();
|
|
173
|
+
|
|
207
174
|
return (
|
|
208
175
|
<div className="space-y-4">
|
|
209
176
|
{/* Table name and description */}
|
|
@@ -217,40 +184,14 @@ export function DataViewTabContent({
|
|
|
217
184
|
</div>
|
|
218
185
|
|
|
219
186
|
{/* Operations row: column selector, search filter, etc. */}
|
|
220
|
-
<DataTableToolbar
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
selectedRelations: columnState.selectedRelations,
|
|
229
|
-
onToggleRelation: columnState.toggleRelation,
|
|
230
|
-
tableMetadataMap: tableMetadataMap,
|
|
231
|
-
expandedRelationFields: columnState.expandedRelationFields,
|
|
232
|
-
onToggleExpandedRelationField:
|
|
233
|
-
columnState.toggleExpandedRelationField,
|
|
234
|
-
isExpandedRelationFieldSelected:
|
|
235
|
-
columnState.isExpandedRelationFieldSelected,
|
|
236
|
-
}}
|
|
237
|
-
searchFilter={{
|
|
238
|
-
fields: selectedTable.fields,
|
|
239
|
-
filters: searchFilters,
|
|
240
|
-
onFiltersChange: handleFiltersChange,
|
|
241
|
-
}}
|
|
242
|
-
viewSave={{
|
|
243
|
-
tableName: selectedTable.name,
|
|
244
|
-
filters: searchFilters,
|
|
245
|
-
selectedFields: columnState.selectedFields,
|
|
246
|
-
selectedRelations: columnState.selectedRelations,
|
|
247
|
-
expandedRelationFields: columnState.expandedRelationFields,
|
|
248
|
-
}}
|
|
249
|
-
onDownloadCsv={handleDownloadCsv}
|
|
250
|
-
onRefresh={() => tableData.refetch()}
|
|
251
|
-
loading={tableData.loading}
|
|
252
|
-
csvDisabled={tableData.data.length === 0}
|
|
253
|
-
/>
|
|
187
|
+
<DataTableToolbar>
|
|
188
|
+
<ColumnSelector />
|
|
189
|
+
<SearchFilterForm />
|
|
190
|
+
<ViewSave />
|
|
191
|
+
<div className="flex-1" />
|
|
192
|
+
<CsvButton />
|
|
193
|
+
<RefreshButton />
|
|
194
|
+
</DataTableToolbar>
|
|
254
195
|
|
|
255
196
|
{/* Error Display */}
|
|
256
197
|
{tableData.error && (
|
|
@@ -263,19 +204,8 @@ export function DataViewTabContent({
|
|
|
263
204
|
|
|
264
205
|
{/* Data Table */}
|
|
265
206
|
<DataTable
|
|
266
|
-
data={tableData.data}
|
|
267
|
-
fields={selectedTable.fields}
|
|
268
|
-
selectedFields={columnState.selectedFields}
|
|
269
|
-
sortState={tableData.sortState}
|
|
270
|
-
onSort={tableData.setSort}
|
|
271
|
-
loading={tableData.loading}
|
|
272
|
-
tableMetadata={selectedTable}
|
|
273
|
-
tableMetadataMap={tableMetadataMap}
|
|
274
|
-
appUri={appUri}
|
|
275
|
-
selectedRelations={columnState.selectedRelations}
|
|
276
207
|
onOpenAsSheet={onOpenAsSheet}
|
|
277
208
|
onOpenSingleRecordAsSheet={onOpenSingleRecordAsSheet}
|
|
278
|
-
expandedRelationFields={columnState.expandedRelationFields}
|
|
279
209
|
/>
|
|
280
210
|
|
|
281
211
|
{/* Pagination */}
|
|
@@ -11,7 +11,7 @@ import { SingleRecordTabContent } from "./single-record-tab-content";
|
|
|
11
11
|
import { useSavedViews, type SavedView } from "./saved-view-context";
|
|
12
12
|
|
|
13
13
|
interface DataViewerProps {
|
|
14
|
-
|
|
14
|
+
metadata: TableMetadataMap;
|
|
15
15
|
appUri: string;
|
|
16
16
|
/** Initial view ID to load on mount */
|
|
17
17
|
initialViewId?: string;
|
|
@@ -52,11 +52,11 @@ function generateTabId(): string {
|
|
|
52
52
|
* Similar to Excel sheets - each tab has its own table view
|
|
53
53
|
*/
|
|
54
54
|
export function DataViewer({
|
|
55
|
-
|
|
55
|
+
metadata,
|
|
56
56
|
appUri,
|
|
57
57
|
initialViewId,
|
|
58
58
|
}: DataViewerProps) {
|
|
59
|
-
const allTables = Object.values(
|
|
59
|
+
const allTables = Object.values(metadata);
|
|
60
60
|
const { getViewById, isLoading } = useSavedViews();
|
|
61
61
|
|
|
62
62
|
// Track if we've already initialized from the saved view
|
|
@@ -84,7 +84,7 @@ export function DataViewer({
|
|
|
84
84
|
|
|
85
85
|
const view = getViewById(initialViewId);
|
|
86
86
|
if (view) {
|
|
87
|
-
const table =
|
|
87
|
+
const table = metadata[view.tableName];
|
|
88
88
|
if (table) {
|
|
89
89
|
initializedFromViewRef.current = true;
|
|
90
90
|
const newTab: Tab = {
|
|
@@ -98,7 +98,7 @@ export function DataViewer({
|
|
|
98
98
|
setActiveTabId(newTab.id);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
|
-
}, [initialViewId, isLoading, getViewById,
|
|
101
|
+
}, [initialViewId, isLoading, getViewById, metadata]);
|
|
102
102
|
|
|
103
103
|
const handleAddTab = useCallback(() => {
|
|
104
104
|
const newTab: Tab = {
|
|
@@ -154,7 +154,7 @@ export function DataViewer({
|
|
|
154
154
|
*/
|
|
155
155
|
const handleOpenAsSheet = useCallback(
|
|
156
156
|
(targetTableName: string, filterField: string, filterValue: string) => {
|
|
157
|
-
const targetTable =
|
|
157
|
+
const targetTable = metadata[targetTableName];
|
|
158
158
|
if (!targetTable) return;
|
|
159
159
|
|
|
160
160
|
const newTab: Tab = {
|
|
@@ -170,7 +170,7 @@ export function DataViewer({
|
|
|
170
170
|
setTabs((prev) => [...prev, newTab]);
|
|
171
171
|
setActiveTabId(newTab.id);
|
|
172
172
|
},
|
|
173
|
-
[
|
|
173
|
+
[metadata],
|
|
174
174
|
);
|
|
175
175
|
|
|
176
176
|
/**
|
|
@@ -178,7 +178,7 @@ export function DataViewer({
|
|
|
178
178
|
*/
|
|
179
179
|
const handleOpenSingleRecordAsSheet = useCallback(
|
|
180
180
|
(targetTableName: string, recordId: string) => {
|
|
181
|
-
const targetTable =
|
|
181
|
+
const targetTable = metadata[targetTableName];
|
|
182
182
|
if (!targetTable) return;
|
|
183
183
|
|
|
184
184
|
const newTab: Tab = {
|
|
@@ -193,7 +193,7 @@ export function DataViewer({
|
|
|
193
193
|
setTabs((prev) => [...prev, newTab]);
|
|
194
194
|
setActiveTabId(newTab.id);
|
|
195
195
|
},
|
|
196
|
-
[
|
|
196
|
+
[metadata],
|
|
197
197
|
);
|
|
198
198
|
|
|
199
199
|
return (
|
|
@@ -250,7 +250,7 @@ export function DataViewer({
|
|
|
250
250
|
{tab.singleRecordMode && tab.table ? (
|
|
251
251
|
<SingleRecordTabContent
|
|
252
252
|
tableMetadata={tab.table}
|
|
253
|
-
|
|
253
|
+
metadata={metadata}
|
|
254
254
|
appUri={appUri}
|
|
255
255
|
recordId={tab.singleRecordMode.recordId}
|
|
256
256
|
onOpenAsSheet={handleOpenAsSheet}
|
|
@@ -263,7 +263,7 @@ export function DataViewer({
|
|
|
263
263
|
initialTable={tab.table ?? undefined}
|
|
264
264
|
onTableConfirm={(table) => handleTableConfirm(tab.id, table)}
|
|
265
265
|
isTableLocked={tab.isLocked}
|
|
266
|
-
|
|
266
|
+
metadata={metadata}
|
|
267
267
|
initialQuery={tab.initialQuery}
|
|
268
268
|
initialView={tab.initialView}
|
|
269
269
|
onOpenAsSheet={handleOpenAsSheet}
|
|
@@ -51,8 +51,8 @@ export interface ColumnState {
|
|
|
51
51
|
* Manage column visibility state (fields and relations)
|
|
52
52
|
*/
|
|
53
53
|
export function useColumnState(
|
|
54
|
-
fields: FieldMetadata[],
|
|
55
|
-
relations?: RelationMetadata[],
|
|
54
|
+
fields: readonly FieldMetadata[],
|
|
55
|
+
relations?: readonly RelationMetadata[],
|
|
56
56
|
defaultFields?: string[],
|
|
57
57
|
defaultRelations?: string[],
|
|
58
58
|
defaultExpandedRelationFields?: ExpandedRelationFields,
|