@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
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ArrowUpDown,
|
|
6
6
|
ChevronRight,
|
|
7
7
|
ChevronDown,
|
|
8
|
+
ChevronLeft,
|
|
8
9
|
X,
|
|
9
10
|
ExternalLink,
|
|
10
11
|
} from "lucide-react";
|
|
@@ -16,33 +17,19 @@ import {
|
|
|
16
17
|
TableHeader,
|
|
17
18
|
TableRow,
|
|
18
19
|
} from "./ui/table";
|
|
20
|
+
import { Button } from "./ui/button";
|
|
19
21
|
import type {
|
|
20
22
|
FieldMetadata,
|
|
21
23
|
TableMetadata,
|
|
22
24
|
RelationMetadata,
|
|
23
|
-
TableMetadataMap,
|
|
24
|
-
ExpandedRelationFields,
|
|
25
25
|
} from "../generator/metadata-generator";
|
|
26
26
|
import { formatFieldValue } from "../graphql/query-builder";
|
|
27
|
-
import type { SortState } from "./hooks/use-table-data";
|
|
28
27
|
import { useRelationData } from "./hooks/use-relation-data";
|
|
29
28
|
import { RelationContent } from "./relation-content";
|
|
29
|
+
import { useDataViewer } from "./contexts";
|
|
30
|
+
import { useTableDataContext } from "./contexts";
|
|
30
31
|
|
|
31
|
-
interface DataTableProps {
|
|
32
|
-
data: Record<string, unknown>[];
|
|
33
|
-
fields: FieldMetadata[];
|
|
34
|
-
selectedFields: string[];
|
|
35
|
-
sortState: SortState | null;
|
|
36
|
-
onSort: (field: string) => void;
|
|
37
|
-
loading?: boolean;
|
|
38
|
-
/** Current table metadata (for relations) */
|
|
39
|
-
tableMetadata?: TableMetadata;
|
|
40
|
-
/** All table metadata (for relation target lookup) */
|
|
41
|
-
tableMetadataMap?: TableMetadataMap;
|
|
42
|
-
/** App URI for GraphQL requests */
|
|
43
|
-
appUri?: string;
|
|
44
|
-
/** Selected relation field names (if not provided, all relations are shown) */
|
|
45
|
-
selectedRelations?: string[];
|
|
32
|
+
export interface DataTableProps {
|
|
46
33
|
/** Callback to open a relation as a new sheet */
|
|
47
34
|
onOpenAsSheet?: (
|
|
48
35
|
targetTableName: string,
|
|
@@ -54,8 +41,6 @@ interface DataTableProps {
|
|
|
54
41
|
targetTableName: string,
|
|
55
42
|
recordId: string,
|
|
56
43
|
) => void;
|
|
57
|
-
/** Expanded relation fields (manyToOne fields shown as inline columns) */
|
|
58
|
-
expandedRelationFields?: ExpandedRelationFields;
|
|
59
44
|
}
|
|
60
45
|
|
|
61
46
|
/**
|
|
@@ -65,22 +50,30 @@ type ExpandedCellKey = string;
|
|
|
65
50
|
|
|
66
51
|
/**
|
|
67
52
|
* Data display table with sortable headers and expandable relation cells
|
|
53
|
+
* Must be used within DataViewer.Root context
|
|
68
54
|
*/
|
|
69
|
-
export function DataTable({
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
55
|
+
export function DataTable(props: DataTableProps = {}) {
|
|
56
|
+
const {
|
|
57
|
+
tableMetadata,
|
|
58
|
+
metadata,
|
|
59
|
+
appUri,
|
|
60
|
+
selectedFields,
|
|
61
|
+
selectedRelations,
|
|
62
|
+
expandedRelationFields,
|
|
63
|
+
} = useDataViewer();
|
|
64
|
+
const {
|
|
65
|
+
data,
|
|
66
|
+
loading,
|
|
67
|
+
sortState,
|
|
68
|
+
setSort,
|
|
69
|
+
pagination,
|
|
70
|
+
hasPreviousPage,
|
|
71
|
+
nextPage,
|
|
72
|
+
previousPage,
|
|
73
|
+
} = useTableDataContext();
|
|
74
|
+
|
|
75
|
+
const fields = tableMetadata?.fields ?? [];
|
|
76
|
+
|
|
84
77
|
// Track expanded cells: Set of "rowId:relationFieldName" keys
|
|
85
78
|
const [expandedCells, setExpandedCells] = useState<Set<ExpandedCellKey>>(
|
|
86
79
|
() => new Set(),
|
|
@@ -89,7 +82,7 @@ export function DataTable({
|
|
|
89
82
|
// Relation data hook
|
|
90
83
|
const { getRelationData, triggerFetch } = useRelationData(
|
|
91
84
|
appUri ?? "",
|
|
92
|
-
|
|
85
|
+
metadata ?? {},
|
|
93
86
|
);
|
|
94
87
|
|
|
95
88
|
// Check if table has relations - filter by selectedRelations if provided
|
|
@@ -101,7 +94,7 @@ export function DataTable({
|
|
|
101
94
|
selectedRelations !== undefined
|
|
102
95
|
? allRelations.filter((r) => selectedRelations.includes(r.fieldName))
|
|
103
96
|
: allRelations;
|
|
104
|
-
const hasRelations = relations.length > 0 && appUri &&
|
|
97
|
+
const hasRelations = relations.length > 0 && appUri && metadata;
|
|
105
98
|
|
|
106
99
|
// Toggle cell expansion and trigger fetch if expanding
|
|
107
100
|
const toggleCell = useCallback(
|
|
@@ -158,7 +151,7 @@ export function DataTable({
|
|
|
158
151
|
|
|
159
152
|
const expandedColumns = useMemo((): ExpandedColumn[] => {
|
|
160
153
|
const columns: ExpandedColumn[] = [];
|
|
161
|
-
if (!
|
|
154
|
+
if (!metadata) return columns;
|
|
162
155
|
|
|
163
156
|
for (const [relationFieldName, targetFieldNames] of Object.entries(
|
|
164
157
|
expandedRelationFields,
|
|
@@ -169,7 +162,7 @@ export function DataTable({
|
|
|
169
162
|
);
|
|
170
163
|
if (!relation) continue;
|
|
171
164
|
|
|
172
|
-
const targetTable =
|
|
165
|
+
const targetTable = metadata[relation.targetTable];
|
|
173
166
|
if (!targetTable) continue;
|
|
174
167
|
|
|
175
168
|
for (const targetFieldName of targetFieldNames) {
|
|
@@ -187,7 +180,7 @@ export function DataTable({
|
|
|
187
180
|
}
|
|
188
181
|
}
|
|
189
182
|
return columns;
|
|
190
|
-
}, [expandedRelationFields, allRelations,
|
|
183
|
+
}, [expandedRelationFields, allRelations, metadata]);
|
|
191
184
|
|
|
192
185
|
// Calculate total column count (for colspan in expanded rows)
|
|
193
186
|
// +1 for the action column (open as sheet button)
|
|
@@ -222,207 +215,244 @@ export function DataTable({
|
|
|
222
215
|
}
|
|
223
216
|
|
|
224
217
|
return (
|
|
225
|
-
|
|
226
|
-
<
|
|
227
|
-
<
|
|
228
|
-
<
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
key={field.name}
|
|
237
|
-
className="hover:bg-muted/50 cursor-pointer select-none"
|
|
238
|
-
onClick={() => onSort(field.name)}
|
|
239
|
-
>
|
|
240
|
-
<div className="flex items-center gap-1">
|
|
241
|
-
<span>{field.name}</span>
|
|
242
|
-
{getSortIcon(field.name)}
|
|
243
|
-
</div>
|
|
244
|
-
</TableHead>
|
|
245
|
-
))}
|
|
246
|
-
{/* Expanded relation field columns (manyToOne inline) */}
|
|
247
|
-
{expandedColumns.map((col) => (
|
|
248
|
-
<TableHead
|
|
249
|
-
key={`${col.relationFieldName}.${col.targetFieldName}`}
|
|
250
|
-
className="bg-muted/20 select-none"
|
|
251
|
-
title={`${col.relationFieldName}.${col.targetFieldName}`}
|
|
252
|
-
>
|
|
253
|
-
<div className="flex items-center gap-1">
|
|
254
|
-
<span className="text-muted-foreground text-xs">
|
|
255
|
-
{col.relationFieldName}.
|
|
256
|
-
</span>
|
|
257
|
-
<span>{col.targetFieldName}</span>
|
|
258
|
-
</div>
|
|
259
|
-
</TableHead>
|
|
260
|
-
))}
|
|
261
|
-
{/* Relation columns */}
|
|
262
|
-
{hasRelations &&
|
|
263
|
-
relations.map((relation) => (
|
|
218
|
+
<>
|
|
219
|
+
<div className="rounded-md border">
|
|
220
|
+
<Table>
|
|
221
|
+
<TableHeader>
|
|
222
|
+
<TableRow>
|
|
223
|
+
{/* Action column (open as sheet) */}
|
|
224
|
+
{props.onOpenSingleRecordAsSheet && tableMetadata && (
|
|
225
|
+
<TableHead className="w-10" />
|
|
226
|
+
)}
|
|
227
|
+
{/* Regular field columns */}
|
|
228
|
+
{visibleFields.map((field) => (
|
|
264
229
|
<TableHead
|
|
265
|
-
key={
|
|
266
|
-
className="
|
|
230
|
+
key={field.name}
|
|
231
|
+
className="hover:bg-muted/50 cursor-pointer select-none"
|
|
232
|
+
onClick={() => setSort(field.name)}
|
|
267
233
|
>
|
|
268
234
|
<div className="flex items-center gap-1">
|
|
269
|
-
<span>{
|
|
270
|
-
|
|
271
|
-
|
|
235
|
+
<span>{field.name}</span>
|
|
236
|
+
{getSortIcon(field.name)}
|
|
237
|
+
</div>
|
|
238
|
+
</TableHead>
|
|
239
|
+
))}
|
|
240
|
+
{/* Expanded relation field columns (manyToOne inline) */}
|
|
241
|
+
{expandedColumns.map((col) => (
|
|
242
|
+
<TableHead
|
|
243
|
+
key={`${col.relationFieldName}.${col.targetFieldName}`}
|
|
244
|
+
className="bg-muted/20 select-none"
|
|
245
|
+
title={`${col.relationFieldName}.${col.targetFieldName}`}
|
|
246
|
+
>
|
|
247
|
+
<div className="flex items-center gap-1">
|
|
248
|
+
<span className="text-muted-foreground text-xs">
|
|
249
|
+
{col.relationFieldName}.
|
|
272
250
|
</span>
|
|
251
|
+
<span>{col.targetFieldName}</span>
|
|
273
252
|
</div>
|
|
274
253
|
</TableHead>
|
|
275
254
|
))}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
255
|
+
{/* Relation columns */}
|
|
256
|
+
{hasRelations &&
|
|
257
|
+
relations.map((relation) => (
|
|
258
|
+
<TableHead
|
|
259
|
+
key={relation.fieldName}
|
|
260
|
+
className="text-muted-foreground select-none"
|
|
261
|
+
>
|
|
262
|
+
<div className="flex items-center gap-1">
|
|
263
|
+
<span>{relation.fieldName}</span>
|
|
264
|
+
<span className="text-xs opacity-50">
|
|
265
|
+
({relation.relationType === "manyToOne" ? "1" : "N"})
|
|
266
|
+
</span>
|
|
267
|
+
</div>
|
|
268
|
+
</TableHead>
|
|
269
|
+
))}
|
|
270
|
+
</TableRow>
|
|
271
|
+
</TableHeader>
|
|
272
|
+
<TableBody>
|
|
273
|
+
{data.map((row, rowIndex) => {
|
|
274
|
+
const rowId = (row.id as string) ?? `row-${rowIndex}`;
|
|
281
275
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
// Check if any cell in this row is expanded
|
|
277
|
+
const expandedRelationsInRow = hasRelations
|
|
278
|
+
? relations.filter((r) => isCellExpanded(rowId, r.fieldName))
|
|
279
|
+
: [];
|
|
280
|
+
const hasExpandedCell = expandedRelationsInRow.length > 0;
|
|
287
281
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
</TableCell>
|
|
317
|
-
)}
|
|
318
|
-
{/* Regular field cells */}
|
|
319
|
-
{visibleFields.map((field) => (
|
|
320
|
-
<TableCell key={field.name}>
|
|
321
|
-
{formatFieldValue(row[field.name], field)}
|
|
322
|
-
</TableCell>
|
|
323
|
-
))}
|
|
324
|
-
{/* Expanded relation field cells (manyToOne inline) */}
|
|
325
|
-
{expandedColumns.map((col) => {
|
|
326
|
-
const relationData = row[col.relationFieldName] as
|
|
327
|
-
| Record<string, unknown>
|
|
328
|
-
| null
|
|
329
|
-
| undefined;
|
|
330
|
-
const value = relationData?.[col.targetFieldName];
|
|
331
|
-
return (
|
|
332
|
-
<TableCell
|
|
333
|
-
key={`${col.relationFieldName}.${col.targetFieldName}`}
|
|
334
|
-
className="bg-muted/10"
|
|
335
|
-
>
|
|
336
|
-
{relationData ? (
|
|
337
|
-
formatFieldValue(value, col.targetField)
|
|
338
|
-
) : (
|
|
339
|
-
<span className="text-muted-foreground">-</span>
|
|
340
|
-
)}
|
|
282
|
+
return (
|
|
283
|
+
<Fragment key={rowId}>
|
|
284
|
+
{/* Main data row */}
|
|
285
|
+
<TableRow
|
|
286
|
+
className={hasExpandedCell ? "border-b-0" : undefined}
|
|
287
|
+
>
|
|
288
|
+
{/* Action cell (open as sheet) */}
|
|
289
|
+
{props.onOpenSingleRecordAsSheet && tableMetadata && (
|
|
290
|
+
<TableCell className="p-1">
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
onClick={() => {
|
|
294
|
+
if (
|
|
295
|
+
typeof rowId === "string" &&
|
|
296
|
+
!rowId.startsWith("row-")
|
|
297
|
+
) {
|
|
298
|
+
props.onOpenSingleRecordAsSheet?.(
|
|
299
|
+
tableMetadata.name,
|
|
300
|
+
rowId,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
}}
|
|
304
|
+
className="hover:bg-muted flex items-center justify-center rounded-md p-1.5 transition-colors"
|
|
305
|
+
aria-label="シートで開く"
|
|
306
|
+
title="シートで開く"
|
|
307
|
+
>
|
|
308
|
+
<ExternalLink className="text-muted-foreground size-4" />
|
|
309
|
+
</button>
|
|
341
310
|
</TableCell>
|
|
342
|
-
)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
|
311
|
+
)}
|
|
312
|
+
{/* Regular field cells */}
|
|
313
|
+
{visibleFields.map((field) => (
|
|
314
|
+
<TableCell key={field.name}>
|
|
315
|
+
{formatFieldValue(row[field.name], field)}
|
|
316
|
+
</TableCell>
|
|
317
|
+
))}
|
|
318
|
+
{/* Expanded relation field cells (manyToOne inline) */}
|
|
319
|
+
{expandedColumns.map((col) => {
|
|
320
|
+
const relationData = row[col.relationFieldName] as
|
|
321
|
+
| Record<string, unknown>
|
|
322
|
+
| null
|
|
354
323
|
| undefined;
|
|
355
|
-
const
|
|
356
|
-
relation.relationType === "oneToMany"
|
|
357
|
-
? relationData?.total
|
|
358
|
-
: undefined;
|
|
324
|
+
const value = relationData?.[col.targetFieldName];
|
|
359
325
|
return (
|
|
360
|
-
<TableCell
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
) : (
|
|
370
|
-
<ChevronRight className="size-4" />
|
|
371
|
-
)}
|
|
372
|
-
<span className="text-muted-foreground text-xs">
|
|
373
|
-
{relation.relationType === "manyToOne"
|
|
374
|
-
? "詳細"
|
|
375
|
-
: total !== undefined
|
|
376
|
-
? `${total}件`
|
|
377
|
-
: "一覧"}
|
|
378
|
-
</span>
|
|
379
|
-
</button>
|
|
326
|
+
<TableCell
|
|
327
|
+
key={`${col.relationFieldName}.${col.targetFieldName}`}
|
|
328
|
+
className="bg-muted/10"
|
|
329
|
+
>
|
|
330
|
+
{relationData ? (
|
|
331
|
+
formatFieldValue(value, col.targetField)
|
|
332
|
+
) : (
|
|
333
|
+
<span className="text-muted-foreground">-</span>
|
|
334
|
+
)}
|
|
380
335
|
</TableCell>
|
|
381
336
|
);
|
|
382
337
|
})}
|
|
383
|
-
|
|
338
|
+
{/* Relation cells with expand toggle */}
|
|
339
|
+
{hasRelations &&
|
|
340
|
+
relations.map((relation) => {
|
|
341
|
+
const isExpanded = isCellExpanded(
|
|
342
|
+
rowId,
|
|
343
|
+
relation.fieldName,
|
|
344
|
+
);
|
|
345
|
+
// Get total count for oneToMany relations from the row data
|
|
346
|
+
const relationData = row[relation.fieldName] as
|
|
347
|
+
| { total?: number }
|
|
348
|
+
| undefined;
|
|
349
|
+
const total =
|
|
350
|
+
relation.relationType === "oneToMany"
|
|
351
|
+
? relationData?.total
|
|
352
|
+
: undefined;
|
|
353
|
+
return (
|
|
354
|
+
<TableCell key={relation.fieldName} className="p-1">
|
|
355
|
+
<button
|
|
356
|
+
type="button"
|
|
357
|
+
onClick={() => toggleCell(rowId, relation, row)}
|
|
358
|
+
className="hover:bg-muted flex w-full items-center justify-center gap-1 rounded-md px-2 py-1 text-sm transition-colors"
|
|
359
|
+
aria-label={
|
|
360
|
+
isExpanded ? "折りたたむ" : "展開する"
|
|
361
|
+
}
|
|
362
|
+
>
|
|
363
|
+
{isExpanded ? (
|
|
364
|
+
<ChevronDown className="size-4" />
|
|
365
|
+
) : (
|
|
366
|
+
<ChevronRight className="size-4" />
|
|
367
|
+
)}
|
|
368
|
+
<span className="text-muted-foreground text-xs">
|
|
369
|
+
{relation.relationType === "manyToOne"
|
|
370
|
+
? "詳細"
|
|
371
|
+
: total !== undefined
|
|
372
|
+
? `${total}件`
|
|
373
|
+
: "一覧"}
|
|
374
|
+
</span>
|
|
375
|
+
</button>
|
|
376
|
+
</TableCell>
|
|
377
|
+
);
|
|
378
|
+
})}
|
|
379
|
+
</TableRow>
|
|
384
380
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
>
|
|
392
|
-
<TableCell
|
|
393
|
-
colSpan={totalColumns}
|
|
394
|
-
className="bg-muted/20 p-4"
|
|
381
|
+
{/* Expanded relation content rows - one per expanded relation */}
|
|
382
|
+
{hasExpandedCell &&
|
|
383
|
+
expandedRelationsInRow.map((relation) => (
|
|
384
|
+
<TableRow
|
|
385
|
+
key={`${rowId}-${relation.fieldName}-expanded`}
|
|
386
|
+
className="hover:bg-transparent"
|
|
395
387
|
>
|
|
396
|
-
<
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
388
|
+
<TableCell
|
|
389
|
+
colSpan={totalColumns}
|
|
390
|
+
className="bg-muted/20 p-4"
|
|
391
|
+
>
|
|
392
|
+
<div className="relative">
|
|
393
|
+
<button
|
|
394
|
+
type="button"
|
|
395
|
+
onClick={() => toggleCell(rowId, relation, row)}
|
|
396
|
+
className="hover:bg-muted absolute top-0 right-0 rounded-md p-1 transition-colors"
|
|
397
|
+
aria-label="閉じる"
|
|
398
|
+
>
|
|
399
|
+
<X className="text-muted-foreground size-4" />
|
|
400
|
+
</button>
|
|
401
|
+
<RelationContent
|
|
402
|
+
relation={relation}
|
|
403
|
+
result={getRelationData(relation, rowId, row)}
|
|
404
|
+
targetTableMetadata={
|
|
405
|
+
metadata?.[relation.targetTable]
|
|
406
|
+
}
|
|
407
|
+
parentRowId={rowId}
|
|
408
|
+
onOpenAsSheet={props.onOpenAsSheet}
|
|
409
|
+
onOpenSingleRecordAsSheet={
|
|
410
|
+
props.onOpenSingleRecordAsSheet
|
|
411
|
+
}
|
|
412
|
+
/>
|
|
413
|
+
</div>
|
|
414
|
+
</TableCell>
|
|
415
|
+
</TableRow>
|
|
416
|
+
))}
|
|
417
|
+
</Fragment>
|
|
418
|
+
);
|
|
419
|
+
})}
|
|
420
|
+
</TableBody>
|
|
421
|
+
</Table>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
{/* Integrated Pagination */}
|
|
425
|
+
<div className="flex items-center justify-between py-2">
|
|
426
|
+
<div className="text-muted-foreground text-sm">
|
|
427
|
+
{data.length > 0 ? (
|
|
428
|
+
<>
|
|
429
|
+
<span className="font-medium">{data.length}</span> 件を表示
|
|
430
|
+
</>
|
|
431
|
+
) : (
|
|
432
|
+
"データなし"
|
|
433
|
+
)}
|
|
434
|
+
</div>
|
|
435
|
+
<div className="flex items-center gap-2">
|
|
436
|
+
<Button
|
|
437
|
+
variant="outline"
|
|
438
|
+
size="sm"
|
|
439
|
+
onClick={previousPage}
|
|
440
|
+
disabled={!hasPreviousPage}
|
|
441
|
+
>
|
|
442
|
+
<ChevronLeft className="size-4" />
|
|
443
|
+
前へ
|
|
444
|
+
</Button>
|
|
445
|
+
<Button
|
|
446
|
+
variant="outline"
|
|
447
|
+
size="sm"
|
|
448
|
+
onClick={nextPage}
|
|
449
|
+
disabled={!pagination.hasNextPage}
|
|
450
|
+
>
|
|
451
|
+
次へ
|
|
452
|
+
<ChevronRight className="size-4" />
|
|
453
|
+
</Button>
|
|
454
|
+
</div>
|
|
455
|
+
</div>
|
|
456
|
+
</>
|
|
427
457
|
);
|
|
428
458
|
}
|