@izumisy-tailor/tailor-data-viewer 0.1.0
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 +255 -0
- package/package.json +47 -0
- package/src/component/column-selector.tsx +264 -0
- package/src/component/data-table.tsx +428 -0
- package/src/component/data-view-tab-content.tsx +324 -0
- package/src/component/data-viewer.tsx +280 -0
- package/src/component/hooks/use-accessible-tables.ts +22 -0
- package/src/component/hooks/use-column-state.ts +281 -0
- package/src/component/hooks/use-relation-data.ts +387 -0
- package/src/component/hooks/use-table-data.ts +317 -0
- package/src/component/index.ts +15 -0
- package/src/component/pagination.tsx +56 -0
- package/src/component/relation-content.tsx +250 -0
- package/src/component/saved-view-context.tsx +145 -0
- package/src/component/search-filter.tsx +319 -0
- package/src/component/single-record-tab-content.tsx +676 -0
- package/src/component/table-selector.tsx +102 -0
- package/src/component/types.ts +20 -0
- package/src/component/view-save-load.tsx +112 -0
- package/src/generator/metadata-generator.ts +461 -0
- package/src/lib/utils.ts +6 -0
- package/src/providers/graphql-client.ts +31 -0
- package/src/styles/theme.css +105 -0
- package/src/types/table-metadata.ts +73 -0
- package/src/ui/alert.tsx +66 -0
- package/src/ui/badge.tsx +46 -0
- package/src/ui/button.tsx +62 -0
- package/src/ui/card.tsx +92 -0
- package/src/ui/checkbox.tsx +30 -0
- package/src/ui/collapsible.tsx +31 -0
- package/src/ui/dialog.tsx +143 -0
- package/src/ui/dropdown-menu.tsx +255 -0
- package/src/ui/input.tsx +21 -0
- package/src/ui/label.tsx +24 -0
- package/src/ui/select.tsx +188 -0
- package/src/ui/table.tsx +116 -0
- package/src/utils/query-builder.ts +190 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TableMetadata,
|
|
3
|
+
FieldMetadata,
|
|
4
|
+
RelationMetadata,
|
|
5
|
+
} from "../types/table-metadata";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Convert camelCase to PascalCase
|
|
9
|
+
*/
|
|
10
|
+
function toPascalCase(str: string): string {
|
|
11
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Relation total info to fetch (for oneToMany relations)
|
|
16
|
+
*/
|
|
17
|
+
export interface RelationTotalInfo {
|
|
18
|
+
/** The oneToMany relation metadata */
|
|
19
|
+
relation: RelationMetadata;
|
|
20
|
+
/** The plural form of the target table (e.g., "taskAssignments") */
|
|
21
|
+
targetPluralForm: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Expanded relation info for manyToOne relations to be fetched inline
|
|
26
|
+
*/
|
|
27
|
+
export interface ExpandedRelationInfo {
|
|
28
|
+
/** The manyToOne relation metadata */
|
|
29
|
+
relation: RelationMetadata;
|
|
30
|
+
/** Selected fields from the target table */
|
|
31
|
+
selectedFields: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Options for building a list query
|
|
36
|
+
*/
|
|
37
|
+
export interface QueryOptions {
|
|
38
|
+
tableName: string;
|
|
39
|
+
pluralForm: string;
|
|
40
|
+
selectedFields: string[];
|
|
41
|
+
orderBy?: { field: string; direction: "Asc" | "Desc" };
|
|
42
|
+
first?: number;
|
|
43
|
+
after?: string;
|
|
44
|
+
/** oneToMany relations to fetch total count for */
|
|
45
|
+
oneToManyRelationTotals?: RelationTotalInfo[];
|
|
46
|
+
/** manyToOne relations to fetch inline with selected fields */
|
|
47
|
+
expandedManyToOneRelations?: ExpandedRelationInfo[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Build a GraphQL list query dynamically based on table metadata
|
|
52
|
+
*/
|
|
53
|
+
export function buildListQuery(options: QueryOptions): string {
|
|
54
|
+
const {
|
|
55
|
+
tableName,
|
|
56
|
+
pluralForm,
|
|
57
|
+
selectedFields,
|
|
58
|
+
orderBy,
|
|
59
|
+
first,
|
|
60
|
+
after,
|
|
61
|
+
oneToManyRelationTotals,
|
|
62
|
+
expandedManyToOneRelations,
|
|
63
|
+
} = options;
|
|
64
|
+
|
|
65
|
+
// GraphQL type names need PascalCase
|
|
66
|
+
const typeName = toPascalCase(tableName);
|
|
67
|
+
|
|
68
|
+
// Build order clause if specified
|
|
69
|
+
const orderArg = orderBy
|
|
70
|
+
? `, order: [{ field: ${typeName}OrderFields.${orderBy.field}, direction: OrderDirection.${orderBy.direction} }]`
|
|
71
|
+
: "";
|
|
72
|
+
|
|
73
|
+
// Build pagination arguments
|
|
74
|
+
const firstArg = first !== undefined ? `, first: ${first}` : "";
|
|
75
|
+
const afterArg = after !== undefined ? `, after: "${after}"` : "";
|
|
76
|
+
|
|
77
|
+
// Build oneToMany relation total fields (fetching only total for display before expansion)
|
|
78
|
+
// These are nested connection queries that get total count for each parent record
|
|
79
|
+
const relationTotalFields =
|
|
80
|
+
oneToManyRelationTotals
|
|
81
|
+
?.map((info) => {
|
|
82
|
+
return `${info.relation.fieldName}(first: 0) {
|
|
83
|
+
total
|
|
84
|
+
}`;
|
|
85
|
+
})
|
|
86
|
+
.join("\n ") ?? "";
|
|
87
|
+
|
|
88
|
+
// Build expanded manyToOne relation fields (inline object fetching)
|
|
89
|
+
const expandedRelationFields =
|
|
90
|
+
expandedManyToOneRelations
|
|
91
|
+
?.filter((info) => info.selectedFields.length > 0)
|
|
92
|
+
.map((info) => {
|
|
93
|
+
const nestedFields = info.selectedFields.join("\n ");
|
|
94
|
+
return `${info.relation.fieldName} {
|
|
95
|
+
${nestedFields}
|
|
96
|
+
}`;
|
|
97
|
+
})
|
|
98
|
+
.join("\n ") ?? "";
|
|
99
|
+
|
|
100
|
+
// Format fields with proper indentation
|
|
101
|
+
const fieldsStr = selectedFields.join("\n ");
|
|
102
|
+
let allFieldsStr = fieldsStr;
|
|
103
|
+
if (relationTotalFields) {
|
|
104
|
+
allFieldsStr = `${allFieldsStr}\n ${relationTotalFields}`;
|
|
105
|
+
}
|
|
106
|
+
if (expandedRelationFields) {
|
|
107
|
+
allFieldsStr = `${allFieldsStr}\n ${expandedRelationFields}`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `
|
|
111
|
+
query ${typeName}List($query: ${typeName}QueryInput) {
|
|
112
|
+
${pluralForm}(query: $query${orderArg}${firstArg}${afterArg}) {
|
|
113
|
+
edges {
|
|
114
|
+
node {
|
|
115
|
+
${allFieldsStr}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
pageInfo {
|
|
119
|
+
hasNextPage
|
|
120
|
+
endCursor
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
`.trim();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get default selected fields for a table
|
|
129
|
+
* Excludes nested, array, and id fields by default
|
|
130
|
+
*/
|
|
131
|
+
export function getDefaultSelectedFields(table: TableMetadata): string[] {
|
|
132
|
+
return table.fields
|
|
133
|
+
.filter(
|
|
134
|
+
(field) =>
|
|
135
|
+
field.type !== "nested" &&
|
|
136
|
+
field.type !== "array" &&
|
|
137
|
+
// Exclude id fields (both 'id' and relation ids like 'taskId')
|
|
138
|
+
field.name !== "id" &&
|
|
139
|
+
!field.name.endsWith("Id"),
|
|
140
|
+
)
|
|
141
|
+
.map((field) => field.name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get all selectable fields (excluding nested types)
|
|
146
|
+
*/
|
|
147
|
+
export function getSelectableFields(table: TableMetadata): FieldMetadata[] {
|
|
148
|
+
return table.fields.filter((field) => field.type !== "nested");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Format a value based on its field type for display
|
|
153
|
+
*/
|
|
154
|
+
export function formatFieldValue(value: unknown, field: FieldMetadata): string {
|
|
155
|
+
if (value === null || value === undefined) {
|
|
156
|
+
return "-";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Helper to safely convert unknown to string
|
|
160
|
+
const toStr = (v: unknown): string =>
|
|
161
|
+
typeof v === "string"
|
|
162
|
+
? v
|
|
163
|
+
: typeof v === "number" || typeof v === "boolean"
|
|
164
|
+
? v.toString()
|
|
165
|
+
: JSON.stringify(v);
|
|
166
|
+
|
|
167
|
+
switch (field.type) {
|
|
168
|
+
case "datetime":
|
|
169
|
+
return new Date(toStr(value)).toLocaleString("ja-JP");
|
|
170
|
+
case "date":
|
|
171
|
+
return new Date(toStr(value)).toLocaleDateString("ja-JP");
|
|
172
|
+
case "time":
|
|
173
|
+
return toStr(value);
|
|
174
|
+
case "boolean":
|
|
175
|
+
return value ? "はい" : "いいえ";
|
|
176
|
+
case "number":
|
|
177
|
+
return typeof value === "number"
|
|
178
|
+
? value.toLocaleString("ja-JP")
|
|
179
|
+
: toStr(value);
|
|
180
|
+
case "array":
|
|
181
|
+
return Array.isArray(value)
|
|
182
|
+
? value.map((v) => toStr(v)).join(", ")
|
|
183
|
+
: toStr(value);
|
|
184
|
+
case "uuid":
|
|
185
|
+
// Show truncated UUID for better readability
|
|
186
|
+
return toStr(value).substring(0, 8) + "...";
|
|
187
|
+
default:
|
|
188
|
+
return toStr(value);
|
|
189
|
+
}
|
|
190
|
+
}
|