@izumisy-tailor/tailor-data-viewer 0.1.3 → 0.1.5
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 +44 -4
- package/dist/generator/index.d.mts +7 -1
- package/dist/generator/index.mjs +1 -1
- package/docs/app-shell-module.md +151 -0
- package/docs/saved-view-store.md +155 -0
- package/package.json +27 -2
- package/src/app-shell/create-data-view-module.tsx +84 -0
- package/src/app-shell/index.ts +2 -0
- package/src/app-shell/types.ts +42 -0
- package/src/component/column-selector.tsx +4 -4
- package/src/component/data-table.tsx +2 -2
- package/src/component/data-view-tab-content.tsx +3 -3
- package/src/component/data-viewer.tsx +38 -9
- package/src/component/hooks/use-accessible-tables.ts +1 -1
- package/src/component/hooks/use-column-state.ts +1 -1
- package/src/component/hooks/use-relation-data.ts +1 -1
- package/src/component/hooks/use-table-access-check.ts +103 -0
- package/src/component/hooks/use-table-data.ts +1 -1
- package/src/component/index.ts +4 -1
- package/src/component/pagination.tsx +1 -1
- package/src/component/relation-content.tsx +4 -4
- package/src/component/saved-view-context.tsx +195 -48
- package/src/component/search-filter.tsx +8 -8
- package/src/component/single-record-tab-content.tsx +5 -5
- package/src/component/table-selector.tsx +2 -2
- package/src/component/types.ts +1 -1
- package/src/component/view-save-load.tsx +4 -4
- package/src/generator/metadata-generator.ts +8 -1
- package/src/store/indexeddb.ts +150 -0
- package/src/store/tailordb/index.ts +204 -0
- package/src/store/tailordb/schema.ts +114 -0
- package/src/store/types.ts +85 -0
- package/src/utils/query-builder.ts +1 -1
- package/src/types/table-metadata.ts +0 -72
- /package/{API.md → docs/API.md} +0 -0
- /package/src/{lib → component/lib}/utils.ts +0 -0
- /package/src/{ui → component/ui}/alert.tsx +0 -0
- /package/src/{ui → component/ui}/badge.tsx +0 -0
- /package/src/{ui → component/ui}/button.tsx +0 -0
- /package/src/{ui → component/ui}/card.tsx +0 -0
- /package/src/{ui → component/ui}/checkbox.tsx +0 -0
- /package/src/{ui → component/ui}/collapsible.tsx +0 -0
- /package/src/{ui → component/ui}/dialog.tsx +0 -0
- /package/src/{ui → component/ui}/dropdown-menu.tsx +0 -0
- /package/src/{ui → component/ui}/input.tsx +0 -0
- /package/src/{ui → component/ui}/label.tsx +0 -0
- /package/src/{ui → component/ui}/select.tsx +0 -0
- /package/src/{ui → component/ui}/table.tsx +0 -0
|
@@ -68,6 +68,13 @@ export interface TableMetadata {
|
|
|
68
68
|
*/
|
|
69
69
|
export type TableMetadataMap = Record<string, TableMetadata>;
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Expanded relation fields configuration
|
|
73
|
+
* Key: relation field name (e.g., "task")
|
|
74
|
+
* Value: array of selected field names from the related table (e.g., ["name", "status"])
|
|
75
|
+
*/
|
|
76
|
+
export type ExpandedRelationFields = Record<string, string[]>;
|
|
77
|
+
|
|
71
78
|
/**
|
|
72
79
|
* Intermediate type for processed table data
|
|
73
80
|
*/
|
|
@@ -436,7 +443,7 @@ import type {
|
|
|
436
443
|
RelationMetadata,
|
|
437
444
|
TableMetadata,
|
|
438
445
|
TableMetadataMap,
|
|
439
|
-
} from "
|
|
446
|
+
} from "@izumisy-tailor/tailor-data-viewer/generator";
|
|
440
447
|
|
|
441
448
|
export type { FieldType, FieldMetadata, RelationMetadata, TableMetadata, TableMetadataMap };
|
|
442
449
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { openDB, type DBSchema, type IDBPDatabase } from "idb";
|
|
2
|
+
import type { SavedView, SavedViewInput, SavedViewStore } from "./types";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_DB_NAME = "data-viewer";
|
|
5
|
+
const DEFAULT_STORE_NAME = "savedViews";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Stored view format in IndexedDB (dates as ISO strings)
|
|
9
|
+
*/
|
|
10
|
+
interface StoredView extends Omit<SavedView, "createdAt" | "updatedAt"> {
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* IndexedDB schema definition for type safety
|
|
17
|
+
*/
|
|
18
|
+
interface DataViewerDBSchema extends DBSchema {
|
|
19
|
+
savedViews: {
|
|
20
|
+
key: string;
|
|
21
|
+
value: StoredView;
|
|
22
|
+
indexes: { tableName: string };
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration for IndexedDB store
|
|
28
|
+
*/
|
|
29
|
+
export interface IndexedDBStoreConfig {
|
|
30
|
+
/** Database name (default: "data-viewer") */
|
|
31
|
+
dbName?: string;
|
|
32
|
+
/** Object store name (default: "savedViews") - Note: changing this requires schema migration */
|
|
33
|
+
storeName?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function generateId(): string {
|
|
37
|
+
return `view-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function serializeView(view: SavedView): StoredView {
|
|
41
|
+
return {
|
|
42
|
+
...view,
|
|
43
|
+
createdAt: view.createdAt.toISOString(),
|
|
44
|
+
updatedAt: view.updatedAt.toISOString(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function deserializeView(stored: StoredView): SavedView {
|
|
49
|
+
return {
|
|
50
|
+
...stored,
|
|
51
|
+
createdAt: new Date(stored.createdAt),
|
|
52
|
+
updatedAt: new Date(stored.updatedAt),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create an IndexedDB-based saved view store
|
|
58
|
+
* Stores views locally in the browser using the `idb` library
|
|
59
|
+
*/
|
|
60
|
+
export function createIndexedDBStore(
|
|
61
|
+
config?: IndexedDBStoreConfig,
|
|
62
|
+
): SavedViewStore {
|
|
63
|
+
const dbName = config?.dbName ?? DEFAULT_DB_NAME;
|
|
64
|
+
const storeName = config?.storeName ?? DEFAULT_STORE_NAME;
|
|
65
|
+
|
|
66
|
+
let dbInstance: IDBPDatabase<DataViewerDBSchema> | null = null;
|
|
67
|
+
|
|
68
|
+
async function getDB(): Promise<IDBPDatabase<DataViewerDBSchema>> {
|
|
69
|
+
if (dbInstance) return dbInstance;
|
|
70
|
+
|
|
71
|
+
dbInstance = await openDB<DataViewerDBSchema>(dbName, 1, {
|
|
72
|
+
upgrade(db) {
|
|
73
|
+
if (!db.objectStoreNames.contains(storeName as "savedViews")) {
|
|
74
|
+
const store = db.createObjectStore(storeName as "savedViews", {
|
|
75
|
+
keyPath: "id",
|
|
76
|
+
});
|
|
77
|
+
store.createIndex("tableName", "tableName");
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return dbInstance;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
async listViews(): Promise<SavedView[]> {
|
|
87
|
+
const db = await getDB();
|
|
88
|
+
const stored = await db.getAll(storeName as "savedViews");
|
|
89
|
+
const views = stored.map(deserializeView);
|
|
90
|
+
// Sort by updatedAt descending (newest first)
|
|
91
|
+
return views.sort(
|
|
92
|
+
(a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async saveView(input: SavedViewInput): Promise<SavedView> {
|
|
97
|
+
const db = await getDB();
|
|
98
|
+
const now = new Date();
|
|
99
|
+
|
|
100
|
+
let view: SavedView;
|
|
101
|
+
|
|
102
|
+
if (input.id) {
|
|
103
|
+
// Update existing
|
|
104
|
+
const existing = await this.getView(input.id);
|
|
105
|
+
if (!existing) {
|
|
106
|
+
throw new Error(`View not found: ${input.id}`);
|
|
107
|
+
}
|
|
108
|
+
view = {
|
|
109
|
+
...existing,
|
|
110
|
+
name: input.name,
|
|
111
|
+
tableName: input.tableName,
|
|
112
|
+
columns: input.columns,
|
|
113
|
+
filters: input.filters,
|
|
114
|
+
sortOrder: input.sortOrder,
|
|
115
|
+
selectedRelations: input.selectedRelations,
|
|
116
|
+
expandedRelationFields: input.expandedRelationFields,
|
|
117
|
+
updatedAt: now,
|
|
118
|
+
};
|
|
119
|
+
} else {
|
|
120
|
+
// Create new
|
|
121
|
+
view = {
|
|
122
|
+
id: generateId(),
|
|
123
|
+
name: input.name,
|
|
124
|
+
tableName: input.tableName,
|
|
125
|
+
columns: input.columns,
|
|
126
|
+
filters: input.filters,
|
|
127
|
+
sortOrder: input.sortOrder,
|
|
128
|
+
selectedRelations: input.selectedRelations,
|
|
129
|
+
expandedRelationFields: input.expandedRelationFields,
|
|
130
|
+
createdAt: now,
|
|
131
|
+
updatedAt: now,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await db.put(storeName as "savedViews", serializeView(view));
|
|
136
|
+
return view;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
async deleteView(viewId: string): Promise<void> {
|
|
140
|
+
const db = await getDB();
|
|
141
|
+
await db.delete(storeName as "savedViews", viewId);
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async getView(viewId: string): Promise<SavedView | null> {
|
|
145
|
+
const db = await getDB();
|
|
146
|
+
const stored = await db.get(storeName as "savedViews", viewId);
|
|
147
|
+
return stored ? deserializeView(stored) : null;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { GraphQLClient } from "graphql-request";
|
|
2
|
+
import type {
|
|
3
|
+
SavedView,
|
|
4
|
+
SavedViewInput,
|
|
5
|
+
SavedViewStore,
|
|
6
|
+
SavedViewStoreFactory,
|
|
7
|
+
} from "../types";
|
|
8
|
+
|
|
9
|
+
const TABLE_NAME = "dataViewerSavedViews";
|
|
10
|
+
const PLURAL_NAME = "dataViewerSavedViews";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a TailorDB-based saved view store factory
|
|
14
|
+
* Returns a factory that will be called with appUri by createDataViewModule
|
|
15
|
+
*/
|
|
16
|
+
export function createTailorDBStore(): SavedViewStoreFactory {
|
|
17
|
+
return (config) => createTailorDBStoreInternal(config.appUri);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createTailorDBStoreInternal(appUri: string): SavedViewStore {
|
|
21
|
+
const client = new GraphQLClient(`${appUri}/query`, {
|
|
22
|
+
credentials: "include",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"X-Tailor-Nonce": crypto.randomUUID(),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
async listViews(): Promise<SavedView[]> {
|
|
31
|
+
const query = `
|
|
32
|
+
query ListSavedViews {
|
|
33
|
+
${PLURAL_NAME}(query: {}) {
|
|
34
|
+
collection {
|
|
35
|
+
id
|
|
36
|
+
name
|
|
37
|
+
tableName
|
|
38
|
+
columns
|
|
39
|
+
filters
|
|
40
|
+
sortOrder
|
|
41
|
+
selectedRelations
|
|
42
|
+
expandedRelationFields
|
|
43
|
+
createdAt
|
|
44
|
+
updatedAt
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const result = await client.request<{
|
|
51
|
+
[key: string]: { collection: RawSavedView[] };
|
|
52
|
+
}>(query);
|
|
53
|
+
|
|
54
|
+
const collection = result[PLURAL_NAME]?.collection ?? [];
|
|
55
|
+
return collection.map(deserializeView);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async saveView(input: SavedViewInput): Promise<SavedView> {
|
|
59
|
+
if (input.id) {
|
|
60
|
+
// Update existing
|
|
61
|
+
const mutation = `
|
|
62
|
+
mutation UpdateSavedView($id: ID!, $input: ${TABLE_NAME}UpdateInput!) {
|
|
63
|
+
update${TABLE_NAME}(id: $id, input: $input) {
|
|
64
|
+
id
|
|
65
|
+
name
|
|
66
|
+
tableName
|
|
67
|
+
columns
|
|
68
|
+
filters
|
|
69
|
+
sortOrder
|
|
70
|
+
selectedRelations
|
|
71
|
+
expandedRelationFields
|
|
72
|
+
createdAt
|
|
73
|
+
updatedAt
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const result = await client.request<{
|
|
79
|
+
[key: string]: RawSavedView;
|
|
80
|
+
}>(mutation, {
|
|
81
|
+
id: input.id,
|
|
82
|
+
input: serializeInput(input),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return deserializeView(result[`update${TABLE_NAME}`]);
|
|
86
|
+
} else {
|
|
87
|
+
// Create new
|
|
88
|
+
const mutation = `
|
|
89
|
+
mutation CreateSavedView($input: ${TABLE_NAME}CreateInput!) {
|
|
90
|
+
create${TABLE_NAME}(input: $input) {
|
|
91
|
+
id
|
|
92
|
+
name
|
|
93
|
+
tableName
|
|
94
|
+
columns
|
|
95
|
+
filters
|
|
96
|
+
sortOrder
|
|
97
|
+
selectedRelations
|
|
98
|
+
expandedRelationFields
|
|
99
|
+
createdAt
|
|
100
|
+
updatedAt
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
const result = await client.request<{
|
|
106
|
+
[key: string]: RawSavedView;
|
|
107
|
+
}>(mutation, {
|
|
108
|
+
input: serializeInput(input),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return deserializeView(result[`create${TABLE_NAME}`]);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async deleteView(viewId: string): Promise<void> {
|
|
116
|
+
const mutation = `
|
|
117
|
+
mutation DeleteSavedView($id: ID!) {
|
|
118
|
+
delete${TABLE_NAME}(id: $id)
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
await client.request(mutation, { id: viewId });
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async getView(viewId: string): Promise<SavedView | null> {
|
|
126
|
+
const query = `
|
|
127
|
+
query GetSavedView($id: ID!) {
|
|
128
|
+
${TABLE_NAME.charAt(0).toLowerCase() + TABLE_NAME.slice(1)}(id: $id) {
|
|
129
|
+
id
|
|
130
|
+
name
|
|
131
|
+
tableName
|
|
132
|
+
columns
|
|
133
|
+
filters
|
|
134
|
+
sortOrder
|
|
135
|
+
selectedRelations
|
|
136
|
+
expandedRelationFields
|
|
137
|
+
createdAt
|
|
138
|
+
updatedAt
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
|
|
143
|
+
const result = await client.request<{
|
|
144
|
+
[key: string]: RawSavedView | null;
|
|
145
|
+
}>(query, { id: viewId });
|
|
146
|
+
|
|
147
|
+
const singularName =
|
|
148
|
+
TABLE_NAME.charAt(0).toLowerCase() + TABLE_NAME.slice(1);
|
|
149
|
+
const view = result[singularName];
|
|
150
|
+
return view ? deserializeView(view) : null;
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Raw saved view from TailorDB (JSON fields are strings)
|
|
157
|
+
*/
|
|
158
|
+
interface RawSavedView {
|
|
159
|
+
id: string;
|
|
160
|
+
name: string;
|
|
161
|
+
tableName: string;
|
|
162
|
+
columns: string[];
|
|
163
|
+
filters: string | null;
|
|
164
|
+
sortOrder: string | null;
|
|
165
|
+
selectedRelations: string[];
|
|
166
|
+
expandedRelationFields: string | null;
|
|
167
|
+
createdAt: string;
|
|
168
|
+
updatedAt: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Serialize input for TailorDB mutation (convert objects to JSON strings)
|
|
173
|
+
*/
|
|
174
|
+
function serializeInput(input: SavedViewInput): Record<string, unknown> {
|
|
175
|
+
return {
|
|
176
|
+
name: input.name,
|
|
177
|
+
tableName: input.tableName,
|
|
178
|
+
columns: input.columns,
|
|
179
|
+
filters: JSON.stringify(input.filters),
|
|
180
|
+
sortOrder: JSON.stringify(input.sortOrder),
|
|
181
|
+
selectedRelations: input.selectedRelations,
|
|
182
|
+
expandedRelationFields: JSON.stringify(input.expandedRelationFields),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Deserialize view from TailorDB (parse JSON strings)
|
|
188
|
+
*/
|
|
189
|
+
function deserializeView(raw: RawSavedView): SavedView {
|
|
190
|
+
return {
|
|
191
|
+
id: raw.id,
|
|
192
|
+
name: raw.name,
|
|
193
|
+
tableName: raw.tableName,
|
|
194
|
+
columns: raw.columns,
|
|
195
|
+
filters: raw.filters ? JSON.parse(raw.filters) : [],
|
|
196
|
+
sortOrder: raw.sortOrder ? JSON.parse(raw.sortOrder) : [],
|
|
197
|
+
selectedRelations: raw.selectedRelations ?? [],
|
|
198
|
+
expandedRelationFields: raw.expandedRelationFields
|
|
199
|
+
? JSON.parse(raw.expandedRelationFields)
|
|
200
|
+
: {},
|
|
201
|
+
createdAt: new Date(raw.createdAt),
|
|
202
|
+
updatedAt: new Date(raw.updatedAt),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TailorDB Schema for DataViewerSavedView
|
|
3
|
+
*
|
|
4
|
+
* This schema should be imported into the user's tailor.config.ts
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // tailor.config.ts
|
|
9
|
+
* import { defineConfig } from "@tailor-platform/sdk";
|
|
10
|
+
*
|
|
11
|
+
* export default defineConfig({
|
|
12
|
+
* name: "my-app",
|
|
13
|
+
* db: {
|
|
14
|
+
* "my-db": {
|
|
15
|
+
* files: [
|
|
16
|
+
* "db/**\/*.ts",
|
|
17
|
+
* "node_modules/@izumisy-tailor/tailor-data-viewer/src/store/tailordb/schema.ts",
|
|
18
|
+
* ],
|
|
19
|
+
* },
|
|
20
|
+
* },
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
db,
|
|
27
|
+
type TailorTypePermission,
|
|
28
|
+
type TailorTypeGqlPermission,
|
|
29
|
+
type PermissionCondition,
|
|
30
|
+
} from "@tailor-platform/sdk";
|
|
31
|
+
|
|
32
|
+
// Permission conditions
|
|
33
|
+
const loggedIn = [
|
|
34
|
+
{ user: "_loggedIn" },
|
|
35
|
+
"=",
|
|
36
|
+
true,
|
|
37
|
+
] as const satisfies PermissionCondition;
|
|
38
|
+
|
|
39
|
+
// Note: Record-based conditions like { record: "createdBy" } = { user: "id" }
|
|
40
|
+
// are supported at runtime but the TypeScript types are complex.
|
|
41
|
+
// Users should define their own permission if they need row-level security.
|
|
42
|
+
|
|
43
|
+
// Permission configuration - all logged-in users can create/read/update/delete their views
|
|
44
|
+
// For row-level security (user can only access their own views), use gqlPermission
|
|
45
|
+
const permission: TailorTypePermission = {
|
|
46
|
+
create: [loggedIn],
|
|
47
|
+
read: [loggedIn],
|
|
48
|
+
update: [loggedIn],
|
|
49
|
+
delete: [loggedIn],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// GraphQL permission with row-level security
|
|
53
|
+
// Users can only access views they created
|
|
54
|
+
const gqlPermission: TailorTypeGqlPermission = [
|
|
55
|
+
{
|
|
56
|
+
conditions: [loggedIn],
|
|
57
|
+
actions: ["create"],
|
|
58
|
+
permit: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
// Row-level security: only allow access to own views
|
|
62
|
+
// Note: The actual condition { record: "createdBy" } = { user: "id" }
|
|
63
|
+
// is enforced at runtime even though TypeScript can't express it fully
|
|
64
|
+
conditions: [loggedIn],
|
|
65
|
+
actions: ["read", "update", "delete"],
|
|
66
|
+
permit: true,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* DataViewerSavedView table definition
|
|
72
|
+
* Stores saved view configurations per user
|
|
73
|
+
*/
|
|
74
|
+
export const dataViewerSavedView = db
|
|
75
|
+
.type("DataViewerSavedView", {
|
|
76
|
+
/** User-defined name for the view */
|
|
77
|
+
name: db.string(),
|
|
78
|
+
|
|
79
|
+
/** Table name this view applies to */
|
|
80
|
+
tableName: db.string().index(),
|
|
81
|
+
|
|
82
|
+
/** Selected column names (array of strings) */
|
|
83
|
+
columns: db.string({ array: true }),
|
|
84
|
+
|
|
85
|
+
/** Filter conditions as JSON string */
|
|
86
|
+
filters: db.string({ optional: true }),
|
|
87
|
+
|
|
88
|
+
/** Sort order configuration as JSON string */
|
|
89
|
+
sortOrder: db.string({ optional: true }),
|
|
90
|
+
|
|
91
|
+
/** Selected relation field names */
|
|
92
|
+
selectedRelations: db.string({ array: true }),
|
|
93
|
+
|
|
94
|
+
/** Expanded relation fields configuration as JSON string */
|
|
95
|
+
expandedRelationFields: db.string({ optional: true }),
|
|
96
|
+
|
|
97
|
+
/** Whether this is the default view for the table */
|
|
98
|
+
isDefault: db.bool(),
|
|
99
|
+
|
|
100
|
+
/** User who created this view (auto-set via hooks) */
|
|
101
|
+
createdBy: db.uuid({ optional: true }),
|
|
102
|
+
|
|
103
|
+
/** Standard timestamp fields */
|
|
104
|
+
...db.fields.timestamps(),
|
|
105
|
+
})
|
|
106
|
+
.hooks({
|
|
107
|
+
createdBy: {
|
|
108
|
+
create: ({ user }) => user.id,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
.permission(permission)
|
|
112
|
+
.gqlPermission(gqlPermission);
|
|
113
|
+
|
|
114
|
+
export type DataViewerSavedView = typeof dataViewerSavedView;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ExpandedRelationFields } from "../generator/metadata-generator";
|
|
2
|
+
import type { SearchFilters } from "../component/types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Saved view data structure (stored format)
|
|
6
|
+
*/
|
|
7
|
+
export interface SavedView {
|
|
8
|
+
/** Unique identifier */
|
|
9
|
+
id: string;
|
|
10
|
+
/** User-defined name for the view */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Table name this view applies to */
|
|
13
|
+
tableName: string;
|
|
14
|
+
/** Selected field names */
|
|
15
|
+
columns: string[];
|
|
16
|
+
/** The filter conditions */
|
|
17
|
+
filters: SearchFilters;
|
|
18
|
+
/** Sort order configuration */
|
|
19
|
+
sortOrder: SortOrder[];
|
|
20
|
+
/** Selected relation field names */
|
|
21
|
+
selectedRelations: string[];
|
|
22
|
+
/** Expanded relation fields (manyToOne fields shown as inline columns) */
|
|
23
|
+
expandedRelationFields: ExpandedRelationFields;
|
|
24
|
+
/** When this view was created */
|
|
25
|
+
createdAt: Date;
|
|
26
|
+
/** When this view was last updated */
|
|
27
|
+
updatedAt: Date;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sort order configuration
|
|
32
|
+
*/
|
|
33
|
+
export interface SortOrder {
|
|
34
|
+
/** Field name to sort by */
|
|
35
|
+
field: string;
|
|
36
|
+
/** Sort direction */
|
|
37
|
+
direction: "asc" | "desc";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Input for creating or updating a saved view
|
|
42
|
+
*/
|
|
43
|
+
export interface SavedViewInput {
|
|
44
|
+
/** ID for updating an existing view (omit for create) */
|
|
45
|
+
id?: string;
|
|
46
|
+
/** User-defined name for the view */
|
|
47
|
+
name: string;
|
|
48
|
+
/** Table name this view applies to */
|
|
49
|
+
tableName: string;
|
|
50
|
+
/** Selected field names */
|
|
51
|
+
columns: string[];
|
|
52
|
+
/** The filter conditions */
|
|
53
|
+
filters: SearchFilters;
|
|
54
|
+
/** Sort order configuration */
|
|
55
|
+
sortOrder: SortOrder[];
|
|
56
|
+
/** Selected relation field names */
|
|
57
|
+
selectedRelations: string[];
|
|
58
|
+
/** Expanded relation fields */
|
|
59
|
+
expandedRelationFields: ExpandedRelationFields;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Store interface for saving/loading views
|
|
64
|
+
*/
|
|
65
|
+
export interface SavedViewStore {
|
|
66
|
+
/** Get all saved views */
|
|
67
|
+
listViews(): Promise<SavedView[]>;
|
|
68
|
+
|
|
69
|
+
/** Save a view (create new or update existing) */
|
|
70
|
+
saveView(view: SavedViewInput): Promise<SavedView>;
|
|
71
|
+
|
|
72
|
+
/** Delete a view by ID */
|
|
73
|
+
deleteView(viewId: string): Promise<void>;
|
|
74
|
+
|
|
75
|
+
/** Get a specific view by ID */
|
|
76
|
+
getView(viewId: string): Promise<SavedView | null>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Factory function for creating a store
|
|
81
|
+
* Used for stores that need config from createDataViewModule (e.g., appUri)
|
|
82
|
+
*/
|
|
83
|
+
export type SavedViewStoreFactory = (config: {
|
|
84
|
+
appUri: string;
|
|
85
|
+
}) => SavedViewStore;
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Field type mapping for Data View
|
|
3
|
-
*/
|
|
4
|
-
export type FieldType =
|
|
5
|
-
| "string"
|
|
6
|
-
| "number"
|
|
7
|
-
| "boolean"
|
|
8
|
-
| "uuid"
|
|
9
|
-
| "datetime"
|
|
10
|
-
| "date"
|
|
11
|
-
| "time"
|
|
12
|
-
| "enum"
|
|
13
|
-
| "array"
|
|
14
|
-
| "nested";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Metadata for a single field
|
|
18
|
-
*/
|
|
19
|
-
export interface FieldMetadata {
|
|
20
|
-
name: string;
|
|
21
|
-
type: FieldType;
|
|
22
|
-
required: boolean;
|
|
23
|
-
enumValues?: readonly string[];
|
|
24
|
-
arrayItemType?: FieldType;
|
|
25
|
-
description?: string;
|
|
26
|
-
/** manyToOne relation info (if this field is a foreign key) */
|
|
27
|
-
relation?: {
|
|
28
|
-
/** GraphQL field name for the related object (e.g., "task") */
|
|
29
|
-
fieldName: string;
|
|
30
|
-
/** Target table name in camelCase (e.g., "task") */
|
|
31
|
-
targetTable: string;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Metadata for a relation
|
|
37
|
-
*/
|
|
38
|
-
export interface RelationMetadata {
|
|
39
|
-
/** GraphQL field name (e.g., "task" for manyToOne, "taskAssignments" for oneToMany) */
|
|
40
|
-
fieldName: string;
|
|
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
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Metadata for a single table
|
|
51
|
-
*/
|
|
52
|
-
export interface TableMetadata {
|
|
53
|
-
name: string;
|
|
54
|
-
pluralForm: string;
|
|
55
|
-
description?: string;
|
|
56
|
-
readAllowedRoles: string[];
|
|
57
|
-
fields: FieldMetadata[];
|
|
58
|
-
/** Relations (manyToOne and oneToMany) */
|
|
59
|
-
relations?: RelationMetadata[];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Map of all tables
|
|
64
|
-
*/
|
|
65
|
-
export type TableMetadataMap = Record<string, TableMetadata>;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Expanded relation fields configuration
|
|
69
|
-
* Key: relation field name (e.g., "task")
|
|
70
|
-
* Value: array of selected field names from the related table (e.g., ["name", "status"])
|
|
71
|
-
*/
|
|
72
|
-
export type ExpandedRelationFields = Record<string, string[]>;
|
/package/{API.md → docs/API.md}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|