@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
package/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# @tailor-platform/data-viewer
|
|
2
|
+
|
|
3
|
+
A React component library for building data exploration interfaces with GraphQL backend support. Provides a tab-based spreadsheet-like UI for viewing and managing table data with relation navigation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Tab-based Interface**: Excel-like sheet management with multiple tabs
|
|
8
|
+
- **Table Selection**: Role-based access control for tables
|
|
9
|
+
- **Column Selection**: Dynamic column visibility with manyToOne relation expansion
|
|
10
|
+
- **Relation Navigation**: Expandable inline relations with "Open as Sheet" functionality
|
|
11
|
+
- **Search & Filter**: AND-based filtering on string, number, boolean, and enum fields
|
|
12
|
+
- **View Persistence**: Save and load views with filter and column configurations
|
|
13
|
+
- **Sorting & Pagination**: Full cursor-based pagination support
|
|
14
|
+
- **CSV Export**: Download current view as CSV
|
|
15
|
+
- **Single Record View**: Detailed single record view with all relations
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @tailor-platform/data-viewer
|
|
21
|
+
# or
|
|
22
|
+
pnpm add @tailor-platform/data-viewer
|
|
23
|
+
# or
|
|
24
|
+
yarn add @tailor-platform/data-viewer
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Peer Dependencies
|
|
28
|
+
|
|
29
|
+
This library requires the following peer dependencies:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install react react-dom
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Basic Setup
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { DataViewer, SavedViewProvider } from "@tailor-platform/data-viewer/component";
|
|
41
|
+
import type { TableMetadataMap } from "@tailor-platform/data-viewer/generator";
|
|
42
|
+
import "@tailor-platform/data-viewer/styles/theme.css";
|
|
43
|
+
|
|
44
|
+
// Your table metadata (generated from Tailor Platform schema)
|
|
45
|
+
const tableMetadata: TableMetadataMap = {
|
|
46
|
+
user: {
|
|
47
|
+
name: "user",
|
|
48
|
+
pluralForm: "users",
|
|
49
|
+
fields: [
|
|
50
|
+
{ name: "id", type: "uuid", nullable: false },
|
|
51
|
+
{ name: "name", type: "string", nullable: false },
|
|
52
|
+
{ name: "email", type: "string", nullable: false },
|
|
53
|
+
],
|
|
54
|
+
allowedRoles: ["admin", "viewer"],
|
|
55
|
+
},
|
|
56
|
+
// ... more tables
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function App() {
|
|
60
|
+
return (
|
|
61
|
+
<SavedViewProvider>
|
|
62
|
+
<DataViewer
|
|
63
|
+
tableMetadata={tableMetadata}
|
|
64
|
+
userRoles={["admin"]}
|
|
65
|
+
appUri="https://your-app.tailor.tech/graphql"
|
|
66
|
+
/>
|
|
67
|
+
</SavedViewProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Using Individual Components
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import {
|
|
76
|
+
DataTable,
|
|
77
|
+
TableSelector,
|
|
78
|
+
ColumnSelector,
|
|
79
|
+
Pagination,
|
|
80
|
+
SearchFilterForm,
|
|
81
|
+
useTableData,
|
|
82
|
+
useColumnState,
|
|
83
|
+
} from "@tailor-platform/data-viewer/component";
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Generating Table Metadata
|
|
87
|
+
|
|
88
|
+
Use the metadata generator to create type-safe metadata from your Tailor Platform schema:
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import { generateTableMetadata } from "@tailor-platform/data-viewer/generator";
|
|
92
|
+
|
|
93
|
+
// From your GraphQL schema types
|
|
94
|
+
const metadata = generateTableMetadata(schemaTypes);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API Reference
|
|
98
|
+
|
|
99
|
+
### `<DataViewer />`
|
|
100
|
+
|
|
101
|
+
Main component with tab-based interface.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
interface DataViewerProps {
|
|
105
|
+
/** Map of table name to metadata */
|
|
106
|
+
tableMetadata: TableMetadataMap;
|
|
107
|
+
/** User's roles for access control */
|
|
108
|
+
userRoles: string[];
|
|
109
|
+
/** GraphQL endpoint URI */
|
|
110
|
+
appUri: string;
|
|
111
|
+
/** Optional initial view ID to load */
|
|
112
|
+
initialViewId?: string;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### `<SavedViewProvider />`
|
|
117
|
+
|
|
118
|
+
Context provider for view persistence (localStorage-based).
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
<SavedViewProvider>
|
|
122
|
+
<DataViewer {...props} />
|
|
123
|
+
</SavedViewProvider>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `<DataTable />`
|
|
127
|
+
|
|
128
|
+
Core data table component with sortable columns and expandable relations.
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
interface DataTableProps {
|
|
132
|
+
data: Record<string, unknown>[];
|
|
133
|
+
fields: FieldMetadata[];
|
|
134
|
+
selectedFields: string[];
|
|
135
|
+
sortState: SortState | null;
|
|
136
|
+
onSort: (field: string) => void;
|
|
137
|
+
loading?: boolean;
|
|
138
|
+
tableMetadata?: TableMetadata;
|
|
139
|
+
tableMetadataMap?: TableMetadataMap;
|
|
140
|
+
appUri?: string;
|
|
141
|
+
selectedRelations?: string[];
|
|
142
|
+
onOpenAsSheet?: (tableName: string, filterField: string, filterValue: string) => void;
|
|
143
|
+
onOpenSingleRecordAsSheet?: (tableName: string, recordId: string) => void;
|
|
144
|
+
expandedRelationFields?: ExpandedRelationFields;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Hooks
|
|
149
|
+
|
|
150
|
+
#### `useTableData`
|
|
151
|
+
|
|
152
|
+
Manages table data fetching with pagination, sorting, and filtering.
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
const {
|
|
156
|
+
data,
|
|
157
|
+
loading,
|
|
158
|
+
error,
|
|
159
|
+
pagination,
|
|
160
|
+
sortState,
|
|
161
|
+
setSort,
|
|
162
|
+
nextPage,
|
|
163
|
+
previousPage,
|
|
164
|
+
resetPagination,
|
|
165
|
+
refetch,
|
|
166
|
+
} = useTableData(appUri, table, selectedFields, selectedRelations, filters, metadataMap, expandedFields);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `useColumnState`
|
|
170
|
+
|
|
171
|
+
Manages column visibility and relation field expansion.
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
const {
|
|
175
|
+
selectedFields,
|
|
176
|
+
selectedRelations,
|
|
177
|
+
expandedRelationFields,
|
|
178
|
+
toggleField,
|
|
179
|
+
toggleRelation,
|
|
180
|
+
toggleExpandedRelationField,
|
|
181
|
+
selectAll,
|
|
182
|
+
deselectAll,
|
|
183
|
+
} = useColumnState(fields, relations);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### `useAccessibleTables`
|
|
187
|
+
|
|
188
|
+
Filters tables based on user roles.
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
const tables = useAccessibleTables(tableMetadataMap, userRoles);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Types
|
|
195
|
+
|
|
196
|
+
### TableMetadata
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
interface TableMetadata {
|
|
200
|
+
name: string;
|
|
201
|
+
pluralForm: string;
|
|
202
|
+
description?: string;
|
|
203
|
+
fields: FieldMetadata[];
|
|
204
|
+
relations?: RelationMetadata[];
|
|
205
|
+
allowedRoles: string[];
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### FieldMetadata
|
|
210
|
+
|
|
211
|
+
```tsx
|
|
212
|
+
interface FieldMetadata {
|
|
213
|
+
name: string;
|
|
214
|
+
type: FieldType;
|
|
215
|
+
nullable: boolean;
|
|
216
|
+
enumValues?: string[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
type FieldType = "string" | "number" | "boolean" | "uuid" | "datetime" | "date" | "time" | "json" | "enum" | "nested" | "array";
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### RelationMetadata
|
|
223
|
+
|
|
224
|
+
```tsx
|
|
225
|
+
interface RelationMetadata {
|
|
226
|
+
fieldName: string;
|
|
227
|
+
targetTable: string;
|
|
228
|
+
relationType: "manyToOne" | "oneToMany";
|
|
229
|
+
foreignKeyField: string;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Styling
|
|
234
|
+
|
|
235
|
+
The library uses Tailwind CSS classes. Import the base theme or provide your own CSS variables:
|
|
236
|
+
|
|
237
|
+
```css
|
|
238
|
+
/* Option 1: Import included theme */
|
|
239
|
+
@import "@tailor-platform/data-viewer/styles/theme.css";
|
|
240
|
+
|
|
241
|
+
/* Option 2: Use with Tailor app-shell */
|
|
242
|
+
@import "@tailor-platform/app-shell/theme.css";
|
|
243
|
+
|
|
244
|
+
/* Option 3: Define your own CSS variables */
|
|
245
|
+
:root {
|
|
246
|
+
--background: 0 0% 100%;
|
|
247
|
+
--foreground: 222.2 84% 4.9%;
|
|
248
|
+
--primary: 222.2 47.4% 11.2%;
|
|
249
|
+
/* ... see theme.css for full list */
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
## License
|
|
254
|
+
|
|
255
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@izumisy-tailor/tailor-data-viewer",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Flexible data viewer component for Tailor Platform",
|
|
6
|
+
"files": [
|
|
7
|
+
"src"
|
|
8
|
+
],
|
|
9
|
+
"exports": {
|
|
10
|
+
"./component": {
|
|
11
|
+
"default": "./src/component/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./generator": {
|
|
14
|
+
"default": "./src/generator/metadata-generator.ts"
|
|
15
|
+
},
|
|
16
|
+
"./styles": {
|
|
17
|
+
"default": "./src/styles/theme.css"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"graphql-request": "^6.1.0",
|
|
22
|
+
"lucide-react": "^0.468.0",
|
|
23
|
+
"@radix-ui/react-checkbox": "^1.1.4",
|
|
24
|
+
"@radix-ui/react-collapsible": "^1.1.3",
|
|
25
|
+
"@radix-ui/react-dialog": "^1.1.6",
|
|
26
|
+
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
|
27
|
+
"@radix-ui/react-label": "^2.1.2",
|
|
28
|
+
"@radix-ui/react-select": "^2.1.6",
|
|
29
|
+
"@radix-ui/react-slot": "^1.1.2",
|
|
30
|
+
"class-variance-authority": "^0.7.1",
|
|
31
|
+
"clsx": "^2.1.1",
|
|
32
|
+
"tailwind-merge": "^2.6.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
36
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"react": "^19.0.0",
|
|
40
|
+
"react-dom": "^19.0.0",
|
|
41
|
+
"@types/react": "^19.0.0",
|
|
42
|
+
"@types/react-dom": "^19.0.0"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"type-check": "tsc -b"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { Columns3 } from "lucide-react";
|
|
2
|
+
import { Checkbox } from "../ui/checkbox";
|
|
3
|
+
import { Button } from "../ui/button";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuTrigger,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuSub,
|
|
9
|
+
DropdownMenuSubTrigger,
|
|
10
|
+
DropdownMenuSubContent,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuLabel,
|
|
13
|
+
} from "../ui/dropdown-menu";
|
|
14
|
+
import type {
|
|
15
|
+
FieldMetadata,
|
|
16
|
+
RelationMetadata,
|
|
17
|
+
TableMetadataMap,
|
|
18
|
+
ExpandedRelationFields,
|
|
19
|
+
} from "../types/table-metadata";
|
|
20
|
+
|
|
21
|
+
interface ColumnSelectorProps {
|
|
22
|
+
fields: FieldMetadata[];
|
|
23
|
+
selectedFields: string[];
|
|
24
|
+
onToggle: (fieldName: string) => void;
|
|
25
|
+
onSelectAll: () => void;
|
|
26
|
+
onDeselectAll: () => void;
|
|
27
|
+
/** Relations for the current table */
|
|
28
|
+
relations?: RelationMetadata[];
|
|
29
|
+
/** Currently selected relation field names */
|
|
30
|
+
selectedRelations?: string[];
|
|
31
|
+
/** Toggle relation visibility */
|
|
32
|
+
onToggleRelation?: (fieldName: string) => void;
|
|
33
|
+
/** Table metadata map for relation field lookups */
|
|
34
|
+
tableMetadataMap?: TableMetadataMap;
|
|
35
|
+
/** Expanded relation fields (manyToOne fields shown as inline columns) */
|
|
36
|
+
expandedRelationFields?: ExpandedRelationFields;
|
|
37
|
+
/** Toggle a field within an expanded relation */
|
|
38
|
+
onToggleExpandedRelationField?: (
|
|
39
|
+
relationFieldName: string,
|
|
40
|
+
fieldName: string,
|
|
41
|
+
) => void;
|
|
42
|
+
/** Check if a field is selected within an expanded relation */
|
|
43
|
+
isExpandedRelationFieldSelected?: (
|
|
44
|
+
relationFieldName: string,
|
|
45
|
+
fieldName: string,
|
|
46
|
+
) => boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Column visibility selector with checkboxes
|
|
51
|
+
*/
|
|
52
|
+
export function ColumnSelector({
|
|
53
|
+
fields,
|
|
54
|
+
selectedFields,
|
|
55
|
+
onToggle,
|
|
56
|
+
onSelectAll,
|
|
57
|
+
onDeselectAll,
|
|
58
|
+
relations = [],
|
|
59
|
+
selectedRelations = [],
|
|
60
|
+
onToggleRelation,
|
|
61
|
+
tableMetadataMap,
|
|
62
|
+
expandedRelationFields = {},
|
|
63
|
+
onToggleExpandedRelationField,
|
|
64
|
+
isExpandedRelationFieldSelected,
|
|
65
|
+
}: ColumnSelectorProps) {
|
|
66
|
+
// Filter out nested fields as they're not directly selectable
|
|
67
|
+
const selectableFields = fields.filter((field) => field.type !== "nested");
|
|
68
|
+
|
|
69
|
+
// Separate manyToOne and oneToMany relations
|
|
70
|
+
const manyToOneRelations = relations.filter(
|
|
71
|
+
(r) => r.relationType === "manyToOne",
|
|
72
|
+
);
|
|
73
|
+
const oneToManyRelations = relations.filter(
|
|
74
|
+
(r) => r.relationType === "oneToMany",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Count expanded relation fields
|
|
78
|
+
const expandedFieldCount = Object.values(expandedRelationFields).reduce(
|
|
79
|
+
(sum, fields) => sum + fields.length,
|
|
80
|
+
0,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const totalSelectable =
|
|
84
|
+
selectableFields.length + relations.length + expandedFieldCount;
|
|
85
|
+
const totalSelected =
|
|
86
|
+
selectedFields.length + selectedRelations.length + expandedFieldCount;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<DropdownMenu>
|
|
90
|
+
<DropdownMenuTrigger asChild>
|
|
91
|
+
<Button variant="outline" size="sm" className="gap-1">
|
|
92
|
+
<Columns3 className="size-4" />
|
|
93
|
+
カラム選択 ({totalSelected}/{totalSelectable})
|
|
94
|
+
</Button>
|
|
95
|
+
</DropdownMenuTrigger>
|
|
96
|
+
|
|
97
|
+
<DropdownMenuContent align="start" className="w-64">
|
|
98
|
+
<div className="flex gap-1 border-b px-2 py-1.5">
|
|
99
|
+
<Button variant="ghost" size="sm" onClick={onSelectAll}>
|
|
100
|
+
全選択
|
|
101
|
+
</Button>
|
|
102
|
+
<Button variant="ghost" size="sm" onClick={onDeselectAll}>
|
|
103
|
+
全解除
|
|
104
|
+
</Button>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="max-h-80 overflow-auto p-2">
|
|
107
|
+
{/* Regular fields */}
|
|
108
|
+
<div className="space-y-1">
|
|
109
|
+
{selectableFields.map((field) => (
|
|
110
|
+
<label
|
|
111
|
+
key={field.name}
|
|
112
|
+
className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-sm"
|
|
113
|
+
>
|
|
114
|
+
<Checkbox
|
|
115
|
+
checked={selectedFields.includes(field.name)}
|
|
116
|
+
onCheckedChange={() => onToggle(field.name)}
|
|
117
|
+
/>
|
|
118
|
+
<span
|
|
119
|
+
className="truncate"
|
|
120
|
+
title={field.description ?? field.name}
|
|
121
|
+
>
|
|
122
|
+
{field.name}
|
|
123
|
+
</span>
|
|
124
|
+
{field.required && (
|
|
125
|
+
<span className="text-destructive text-xs">*</span>
|
|
126
|
+
)}
|
|
127
|
+
</label>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{/* ManyToOne Relations with submenu for field expansion */}
|
|
132
|
+
{manyToOneRelations.length > 0 &&
|
|
133
|
+
onToggleRelation &&
|
|
134
|
+
tableMetadataMap && (
|
|
135
|
+
<>
|
|
136
|
+
<DropdownMenuSeparator />
|
|
137
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
138
|
+
リレーション (1対1)
|
|
139
|
+
</DropdownMenuLabel>
|
|
140
|
+
<div className="space-y-1">
|
|
141
|
+
{manyToOneRelations.map((relation) => {
|
|
142
|
+
const targetTable = tableMetadataMap[relation.targetTable];
|
|
143
|
+
const targetFields =
|
|
144
|
+
targetTable?.fields.filter(
|
|
145
|
+
(f) => f.type !== "nested" && f.name !== "id",
|
|
146
|
+
) ?? [];
|
|
147
|
+
const selectedExpandedFields =
|
|
148
|
+
expandedRelationFields[relation.fieldName] ?? [];
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div
|
|
152
|
+
key={relation.fieldName}
|
|
153
|
+
className="flex items-center gap-1"
|
|
154
|
+
>
|
|
155
|
+
{/* Inline toggle checkbox */}
|
|
156
|
+
<label className="hover:bg-accent flex flex-1 cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-sm">
|
|
157
|
+
<Checkbox
|
|
158
|
+
checked={selectedRelations.includes(
|
|
159
|
+
relation.fieldName,
|
|
160
|
+
)}
|
|
161
|
+
onCheckedChange={() =>
|
|
162
|
+
onToggleRelation(relation.fieldName)
|
|
163
|
+
}
|
|
164
|
+
/>
|
|
165
|
+
<span className="truncate" title={relation.fieldName}>
|
|
166
|
+
{relation.fieldName}
|
|
167
|
+
</span>
|
|
168
|
+
<span className="text-muted-foreground text-xs">
|
|
169
|
+
(1)
|
|
170
|
+
</span>
|
|
171
|
+
</label>
|
|
172
|
+
|
|
173
|
+
{/* Submenu for field expansion */}
|
|
174
|
+
{targetTable && onToggleExpandedRelationField && (
|
|
175
|
+
<DropdownMenuSub>
|
|
176
|
+
<DropdownMenuSubTrigger className="h-7 px-2">
|
|
177
|
+
<Columns3 className="size-3" />
|
|
178
|
+
{selectedExpandedFields.length > 0 && (
|
|
179
|
+
<span className="text-muted-foreground ml-1 text-xs">
|
|
180
|
+
({selectedExpandedFields.length})
|
|
181
|
+
</span>
|
|
182
|
+
)}
|
|
183
|
+
</DropdownMenuSubTrigger>
|
|
184
|
+
<DropdownMenuSubContent className="w-56">
|
|
185
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
186
|
+
{relation.targetTable}{" "}
|
|
187
|
+
のフィールドを列として展開
|
|
188
|
+
</DropdownMenuLabel>
|
|
189
|
+
<DropdownMenuSeparator />
|
|
190
|
+
<div className="max-h-60 overflow-auto p-1">
|
|
191
|
+
<div className="space-y-1">
|
|
192
|
+
{targetFields.map((field) => (
|
|
193
|
+
<label
|
|
194
|
+
key={field.name}
|
|
195
|
+
className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-sm"
|
|
196
|
+
>
|
|
197
|
+
<Checkbox
|
|
198
|
+
checked={
|
|
199
|
+
isExpandedRelationFieldSelected?.(
|
|
200
|
+
relation.fieldName,
|
|
201
|
+
field.name,
|
|
202
|
+
) ?? false
|
|
203
|
+
}
|
|
204
|
+
onCheckedChange={() =>
|
|
205
|
+
onToggleExpandedRelationField(
|
|
206
|
+
relation.fieldName,
|
|
207
|
+
field.name,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
className="size-3.5"
|
|
211
|
+
/>
|
|
212
|
+
<span
|
|
213
|
+
className="truncate"
|
|
214
|
+
title={field.description ?? field.name}
|
|
215
|
+
>
|
|
216
|
+
{field.name}
|
|
217
|
+
</span>
|
|
218
|
+
</label>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</DropdownMenuSubContent>
|
|
223
|
+
</DropdownMenuSub>
|
|
224
|
+
)}
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
</>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* OneToMany Relations (inline toggle only) */}
|
|
233
|
+
{oneToManyRelations.length > 0 && onToggleRelation && (
|
|
234
|
+
<>
|
|
235
|
+
<DropdownMenuSeparator />
|
|
236
|
+
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
|
237
|
+
リレーション (1対多)
|
|
238
|
+
</DropdownMenuLabel>
|
|
239
|
+
<div className="space-y-1">
|
|
240
|
+
{oneToManyRelations.map((relation) => (
|
|
241
|
+
<label
|
|
242
|
+
key={relation.fieldName}
|
|
243
|
+
className="hover:bg-accent flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1 text-sm"
|
|
244
|
+
>
|
|
245
|
+
<Checkbox
|
|
246
|
+
checked={selectedRelations.includes(relation.fieldName)}
|
|
247
|
+
onCheckedChange={() =>
|
|
248
|
+
onToggleRelation(relation.fieldName)
|
|
249
|
+
}
|
|
250
|
+
/>
|
|
251
|
+
<span className="truncate" title={relation.fieldName}>
|
|
252
|
+
{relation.fieldName}
|
|
253
|
+
</span>
|
|
254
|
+
<span className="text-muted-foreground text-xs">(N)</span>
|
|
255
|
+
</label>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
</>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
</DropdownMenuContent>
|
|
262
|
+
</DropdownMenu>
|
|
263
|
+
);
|
|
264
|
+
}
|