@burdenoff/fluidgrids-fe-sdk 2026.1112.5 → 2026.1112.7
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/dist/{AiStudioPage-BNmXx6LB.js → AiStudioPage-QGTFaVpx.js} +1 -1
- package/dist/ConfirmActionDialog-D9Pir3bc.js +30 -0
- package/dist/DataTablesPage-0U89KNBt.js +2880 -0
- package/dist/{ExecutionDetailPage-B9eEep77.js → ExecutionDetailPage-D4Mdbzb2.js} +2 -1
- package/dist/{ExecutionsListPage-DuuPSVEj.js → ExecutionsListPage-g_SPxAGX.js} +2 -1
- package/dist/RunActionsMenu-DmO14hEE.js +106 -0
- package/dist/RunnersPage-CvAPHdfU.js +673 -0
- package/dist/{TemplatePreview-CBwvi0ga.js → TemplatePreview-DY4GxnfC.js} +1 -1
- package/dist/{WorkflowBuilder-DDgWgnwH.js → WorkflowBuilder-Aslfa2vj.js} +1 -1
- package/dist/{WorkflowBuilder-DmYS5JEY.js → WorkflowBuilder-DY1_KJiV.js} +1 -1
- package/dist/{agentTools-B4Gjf8R7.js → agentTools-B8gnHFVM.js} +1 -1
- package/dist/{canvasNoteTools-Bve4Fo7R.js → canvasNoteTools-Cj7MvVDH.js} +1 -1
- package/dist/executions.js +2 -2
- package/dist/{flow-builder-BPe5YNF6.js → flow-builder-dvqpKs8d.js} +144 -26
- package/dist/{index-u-kM0seP.d.ts → index-DNo9aFUT.d.ts} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -13
- package/dist/{node-detail-view-oAotM2EI.js → node-detail-view-ZnJ3Hvvo.js} +2 -2
- package/dist/{node-editor-tab-body-DIcmar84.js → node-editor-tab-body-BuiIr7VD.js} +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/{popover-DOeSGQOG.js → popover-mexhIFP3.js} +3 -0
- package/dist/{quick-config-panel-Pm5nkZVr.js → quick-config-panel-EV66lJOM.js} +2 -2
- package/dist/{schedule-panel-Dizqz9eA.js → schedule-panel-BeLfDNFh.js} +1 -1
- package/dist/{sidebar-node-editor-BNub2vUq.js → sidebar-node-editor-D85Moj5d.js} +2 -2
- package/dist/templates.js +1 -1
- package/dist/{useDurableParity-Cb9nOdE8.js → useDurableParity-04-5PTk2.js} +3 -101
- package/dist/workflow-builder.js +2 -2
- package/dist/workflow-pages.d.ts +1 -1
- package/dist/workflow-pages.js +7 -7
- package/package.json +1 -1
- package/dist/DataTablesPage-Dm-dzVPD.js +0 -1698
- package/dist/RunnersPage-D9Bgod_d.js +0 -467
|
@@ -0,0 +1,2880 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { t as __exportAll } from "./rolldown-runtime-hDbaFkMy.js";
|
|
3
|
+
import { t as EntityTagPicker } from "./EntityTagPicker-dxfzv8aO.js";
|
|
4
|
+
import { t as TagFilterSelect } from "./TagFilterSelect-CqhxcDjh.js";
|
|
5
|
+
import { _ as throwIfResultError, m as describeOperationError, n as getFluidGridsClient } from "./client-C9XSzSXr.js";
|
|
6
|
+
import { a as useDataTableTagOverlay, n as tagLabel } from "./useEntityTags-D5C0tdLs.js";
|
|
7
|
+
import { r as useTr } from "./TranslationBridge-Da-TPgQm.js";
|
|
8
|
+
import { i as useOpenRequest, n as ACTION_PARAM_VALUES, r as useActionParam } from "./useActionParam-B0ebTZmg.js";
|
|
9
|
+
import { i as SdkPermissionButton, s as useFluidGridsPermission, t as FLUIDGRIDS_PERMISSIONS } from "./constants-dcxg6zG6.js";
|
|
10
|
+
import { t as FluidGridsDocsLink } from "./FluidGridsDocsLink-JxkBpOeb.js";
|
|
11
|
+
import { n as useNavigationV2 } from "./useNavigationV2-C2WeqN7d.js";
|
|
12
|
+
import { t as ConfirmActionDialog } from "./ConfirmActionDialog-D9Pir3bc.js";
|
|
13
|
+
import { n as TechnicalDetails, r as Disclosure, t as RunActionsMenu } from "./RunActionsMenu-DmO14hEE.js";
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
15
|
+
import { useNavigate } from "react-router";
|
|
16
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
import { toast } from "sonner";
|
|
18
|
+
import { gql } from "@apollo/client";
|
|
19
|
+
import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, IllustratedEmptyState, Input, Label, NextSteps, PagePurpose, Switch, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Textarea } from "@burdenoff/fe-libs/ui";
|
|
20
|
+
import { Button as Button$1 } from "@burdenoff/fe-libs/button";
|
|
21
|
+
import { ArrowDownToLine, ArrowLeft, ArrowUpFromLine, ChevronLeft, ChevronRight, Database, Pencil, Plus, RefreshCw, Search, Table2, Trash2 } from "lucide-react";
|
|
22
|
+
import { TagBadge } from "@burdenoff/fe-libs/tag";
|
|
23
|
+
|
|
24
|
+
// src/modules/data-tables/hooks/useDataTables.ts
|
|
25
|
+
const DATA_TABLE_FIELDS = gql`
|
|
26
|
+
fragment DataTableFields on DataTable {
|
|
27
|
+
id
|
|
28
|
+
workspaceId
|
|
29
|
+
name
|
|
30
|
+
description
|
|
31
|
+
columns {
|
|
32
|
+
key
|
|
33
|
+
label
|
|
34
|
+
type
|
|
35
|
+
required
|
|
36
|
+
defaultValue
|
|
37
|
+
}
|
|
38
|
+
rowCount
|
|
39
|
+
sizeBytes
|
|
40
|
+
createdAt
|
|
41
|
+
updatedAt
|
|
42
|
+
createdBy
|
|
43
|
+
updatedBy
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
const DATA_TABLE_ROW_FIELDS = gql`
|
|
47
|
+
fragment DataTableRowFields on DataTableRow {
|
|
48
|
+
id
|
|
49
|
+
tableId
|
|
50
|
+
data
|
|
51
|
+
createdAt
|
|
52
|
+
updatedAt
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
const GET_DATA_TABLES = gql`
|
|
56
|
+
${DATA_TABLE_FIELDS}
|
|
57
|
+
query GetDataTables($search: String, $limit: Int, $offset: Int) {
|
|
58
|
+
dataTables(search: $search, limit: $limit, offset: $offset) {
|
|
59
|
+
tables {
|
|
60
|
+
...DataTableFields
|
|
61
|
+
}
|
|
62
|
+
total
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
const GET_DATA_TABLES_BY_TAG = gql`
|
|
67
|
+
${DATA_TABLE_FIELDS}
|
|
68
|
+
query GetDataTablesByTag(
|
|
69
|
+
$search: String
|
|
70
|
+
$limit: Int
|
|
71
|
+
$offset: Int
|
|
72
|
+
$tagIds: [ID!]
|
|
73
|
+
) {
|
|
74
|
+
dataTables(
|
|
75
|
+
search: $search
|
|
76
|
+
limit: $limit
|
|
77
|
+
offset: $offset
|
|
78
|
+
tagIds: $tagIds
|
|
79
|
+
) {
|
|
80
|
+
tables {
|
|
81
|
+
...DataTableFields
|
|
82
|
+
}
|
|
83
|
+
total
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
const GET_DATA_TABLE_ROWS = gql`
|
|
88
|
+
${DATA_TABLE_ROW_FIELDS}
|
|
89
|
+
query GetDataTableRows($input: DataTableRowsQueryInput!) {
|
|
90
|
+
dataTableRows(input: $input) {
|
|
91
|
+
rows {
|
|
92
|
+
...DataTableRowFields
|
|
93
|
+
}
|
|
94
|
+
total
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
const CREATE_DATA_TABLE = gql`
|
|
99
|
+
${DATA_TABLE_FIELDS}
|
|
100
|
+
mutation CreateDataTable($input: CreateDataTableInput!) {
|
|
101
|
+
createDataTable(input: $input) {
|
|
102
|
+
success
|
|
103
|
+
message
|
|
104
|
+
table {
|
|
105
|
+
...DataTableFields
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
const UPDATE_DATA_TABLE = gql`
|
|
111
|
+
${DATA_TABLE_FIELDS}
|
|
112
|
+
mutation UpdateDataTable($id: ID!, $input: UpdateDataTableInput!) {
|
|
113
|
+
updateDataTable(id: $id, input: $input) {
|
|
114
|
+
success
|
|
115
|
+
message
|
|
116
|
+
table {
|
|
117
|
+
...DataTableFields
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
`;
|
|
122
|
+
const DELETE_DATA_TABLE = gql`
|
|
123
|
+
mutation DeleteDataTable($id: ID!) {
|
|
124
|
+
deleteDataTable(id: $id) {
|
|
125
|
+
success
|
|
126
|
+
message
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
130
|
+
const INSERT_DATA_TABLE_ROW = gql`
|
|
131
|
+
${DATA_TABLE_ROW_FIELDS}
|
|
132
|
+
mutation InsertDataTableRow($tableId: ID!, $data: JSON!) {
|
|
133
|
+
insertDataTableRow(tableId: $tableId, data: $data) {
|
|
134
|
+
success
|
|
135
|
+
message
|
|
136
|
+
row {
|
|
137
|
+
...DataTableRowFields
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
142
|
+
const UPDATE_DATA_TABLE_ROW = gql`
|
|
143
|
+
${DATA_TABLE_ROW_FIELDS}
|
|
144
|
+
mutation UpdateDataTableRow($id: ID!, $data: JSON!) {
|
|
145
|
+
updateDataTableRow(id: $id, data: $data) {
|
|
146
|
+
success
|
|
147
|
+
message
|
|
148
|
+
row {
|
|
149
|
+
...DataTableRowFields
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
const DELETE_DATA_TABLE_ROWS = gql`
|
|
155
|
+
mutation DeleteDataTableRows($tableId: ID!, $ids: [ID!]!) {
|
|
156
|
+
deleteDataTableRows(tableId: $tableId, ids: $ids) {
|
|
157
|
+
success
|
|
158
|
+
message
|
|
159
|
+
affected
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
function toRow(row) {
|
|
164
|
+
return {
|
|
165
|
+
id: row.id,
|
|
166
|
+
tableId: row.tableId,
|
|
167
|
+
data: toRecord(row.data),
|
|
168
|
+
createdAt: row.createdAt,
|
|
169
|
+
updatedAt: row.updatedAt
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function toRecord(value) {
|
|
173
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
174
|
+
}
|
|
175
|
+
function operationError(error, fallback) {
|
|
176
|
+
return new Error(describeOperationError(error, fallback));
|
|
177
|
+
}
|
|
178
|
+
/** A workspace holds at most 100 tables and the service serves at most 100 per request. */
|
|
179
|
+
const DATA_TABLES_LIST_LIMIT = 100;
|
|
180
|
+
/** The workspace's data tables, optionally narrowed by name and by one tag (G47). */
|
|
181
|
+
function useDataTables(search, tagId) {
|
|
182
|
+
const [tables, setTables] = useState([]);
|
|
183
|
+
const [total, setTotal] = useState(0);
|
|
184
|
+
const [loading, setLoading] = useState(true);
|
|
185
|
+
const [error, setError] = useState(null);
|
|
186
|
+
const fetchTables = useCallback(async () => {
|
|
187
|
+
setLoading(true);
|
|
188
|
+
setError(null);
|
|
189
|
+
try {
|
|
190
|
+
const result = throwIfResultError(await getFluidGridsClient().query({
|
|
191
|
+
query: tagId ? GET_DATA_TABLES_BY_TAG : GET_DATA_TABLES,
|
|
192
|
+
variables: {
|
|
193
|
+
search: search?.trim() || void 0,
|
|
194
|
+
limit: 100,
|
|
195
|
+
offset: 0,
|
|
196
|
+
...tagId ? { tagIds: [tagId] } : {}
|
|
197
|
+
},
|
|
198
|
+
fetchPolicy: "network-only"
|
|
199
|
+
}));
|
|
200
|
+
setTables(result.data?.dataTables.tables ?? []);
|
|
201
|
+
setTotal(result.data?.dataTables.total ?? 0);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
setError(operationError(err, "Failed to load data tables"));
|
|
204
|
+
} finally {
|
|
205
|
+
setLoading(false);
|
|
206
|
+
}
|
|
207
|
+
}, [search, tagId]);
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
fetchTables();
|
|
210
|
+
}, [fetchTables]);
|
|
211
|
+
return {
|
|
212
|
+
tables,
|
|
213
|
+
total,
|
|
214
|
+
loading,
|
|
215
|
+
error,
|
|
216
|
+
refetch: fetchTables
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** Largest page the service serves for a single `dataTableRows` request. */
|
|
220
|
+
const DATA_TABLE_MAX_PAGE_SIZE = 5e3;
|
|
221
|
+
async function queryRowsPage(tableId, limit, offset) {
|
|
222
|
+
const response = throwIfResultError(await getFluidGridsClient().query({
|
|
223
|
+
query: GET_DATA_TABLE_ROWS,
|
|
224
|
+
variables: { input: {
|
|
225
|
+
tableId,
|
|
226
|
+
limit,
|
|
227
|
+
offset
|
|
228
|
+
} },
|
|
229
|
+
fetchPolicy: "network-only"
|
|
230
|
+
})).data?.dataTableRows;
|
|
231
|
+
return {
|
|
232
|
+
rows: response?.rows.map(toRow) ?? [],
|
|
233
|
+
total: response?.total ?? 0
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Fetch every row of a table in service-sized batches (used by CSV export so
|
|
238
|
+
* it covers the whole table rather than the page currently on screen). A table
|
|
239
|
+
* is capped at 50k rows server-side, so this is at most ten requests.
|
|
240
|
+
*/
|
|
241
|
+
async function fetchAllDataTableRows(tableId) {
|
|
242
|
+
const collected = [];
|
|
243
|
+
let total = Number.POSITIVE_INFINITY;
|
|
244
|
+
while (collected.length < total) {
|
|
245
|
+
const page = await queryRowsPage(tableId, DATA_TABLE_MAX_PAGE_SIZE, collected.length);
|
|
246
|
+
total = page.total;
|
|
247
|
+
if (page.rows.length === 0) break;
|
|
248
|
+
collected.push(...page.rows);
|
|
249
|
+
}
|
|
250
|
+
return collected;
|
|
251
|
+
}
|
|
252
|
+
function useDataTableRows(tableId, paging) {
|
|
253
|
+
const { page, pageSize } = paging;
|
|
254
|
+
const [rows, setRows] = useState([]);
|
|
255
|
+
const [total, setTotal] = useState(0);
|
|
256
|
+
const [loading, setLoading] = useState(false);
|
|
257
|
+
const [error, setError] = useState(null);
|
|
258
|
+
const fetchRows = useCallback(async () => {
|
|
259
|
+
if (!tableId) {
|
|
260
|
+
setRows([]);
|
|
261
|
+
setTotal(0);
|
|
262
|
+
setLoading(false);
|
|
263
|
+
setError(null);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
setLoading(true);
|
|
267
|
+
setError(null);
|
|
268
|
+
try {
|
|
269
|
+
const response = await queryRowsPage(tableId, pageSize, page * pageSize);
|
|
270
|
+
setRows(response.rows);
|
|
271
|
+
setTotal(response.total);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
setError(operationError(err, "Failed to load data table rows"));
|
|
274
|
+
} finally {
|
|
275
|
+
setLoading(false);
|
|
276
|
+
}
|
|
277
|
+
}, [
|
|
278
|
+
tableId,
|
|
279
|
+
page,
|
|
280
|
+
pageSize
|
|
281
|
+
]);
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
fetchRows();
|
|
284
|
+
}, [fetchRows]);
|
|
285
|
+
return {
|
|
286
|
+
rows,
|
|
287
|
+
total,
|
|
288
|
+
loading,
|
|
289
|
+
error,
|
|
290
|
+
refetch: fetchRows
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function useCreateDataTable() {
|
|
294
|
+
const [loading, setLoading] = useState(false);
|
|
295
|
+
return {
|
|
296
|
+
createDataTable: useCallback(async (input) => {
|
|
297
|
+
setLoading(true);
|
|
298
|
+
try {
|
|
299
|
+
const response = throwIfResultError(await getFluidGridsClient().mutate({
|
|
300
|
+
mutation: CREATE_DATA_TABLE,
|
|
301
|
+
variables: { input }
|
|
302
|
+
})).data?.createDataTable;
|
|
303
|
+
if (!response?.success) throw new Error(response?.message || "Failed to create data table");
|
|
304
|
+
return response.table ?? null;
|
|
305
|
+
} finally {
|
|
306
|
+
setLoading(false);
|
|
307
|
+
}
|
|
308
|
+
}, []),
|
|
309
|
+
loading
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function useUpdateDataTable() {
|
|
313
|
+
const [loading, setLoading] = useState(false);
|
|
314
|
+
return {
|
|
315
|
+
updateDataTable: useCallback(async (id, input) => {
|
|
316
|
+
setLoading(true);
|
|
317
|
+
try {
|
|
318
|
+
const response = throwIfResultError(await getFluidGridsClient().mutate({
|
|
319
|
+
mutation: UPDATE_DATA_TABLE,
|
|
320
|
+
variables: {
|
|
321
|
+
id,
|
|
322
|
+
input
|
|
323
|
+
}
|
|
324
|
+
})).data?.updateDataTable;
|
|
325
|
+
if (!response?.success) throw new Error(response?.message || "Failed to update data table");
|
|
326
|
+
return response.table ?? null;
|
|
327
|
+
} finally {
|
|
328
|
+
setLoading(false);
|
|
329
|
+
}
|
|
330
|
+
}, []),
|
|
331
|
+
loading
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function useDeleteDataTable() {
|
|
335
|
+
const [loading, setLoading] = useState(false);
|
|
336
|
+
return {
|
|
337
|
+
deleteDataTable: useCallback(async (id) => {
|
|
338
|
+
setLoading(true);
|
|
339
|
+
try {
|
|
340
|
+
const response = throwIfResultError(await getFluidGridsClient().mutate({
|
|
341
|
+
mutation: DELETE_DATA_TABLE,
|
|
342
|
+
variables: { id }
|
|
343
|
+
})).data?.deleteDataTable;
|
|
344
|
+
if (!response?.success) throw new Error(response?.message || "Failed to delete data table");
|
|
345
|
+
return true;
|
|
346
|
+
} finally {
|
|
347
|
+
setLoading(false);
|
|
348
|
+
}
|
|
349
|
+
}, []),
|
|
350
|
+
loading
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
function useInsertDataTableRow() {
|
|
354
|
+
const [loading, setLoading] = useState(false);
|
|
355
|
+
return {
|
|
356
|
+
insertDataTableRow: useCallback(async (tableId, data) => {
|
|
357
|
+
setLoading(true);
|
|
358
|
+
try {
|
|
359
|
+
const response = throwIfResultError(await getFluidGridsClient().mutate({
|
|
360
|
+
mutation: INSERT_DATA_TABLE_ROW,
|
|
361
|
+
variables: {
|
|
362
|
+
tableId,
|
|
363
|
+
data
|
|
364
|
+
}
|
|
365
|
+
})).data?.insertDataTableRow;
|
|
366
|
+
if (!response?.success) throw new Error(response?.message || "Failed to insert row");
|
|
367
|
+
return response.row ? toRow(response.row) : null;
|
|
368
|
+
} finally {
|
|
369
|
+
setLoading(false);
|
|
370
|
+
}
|
|
371
|
+
}, []),
|
|
372
|
+
loading
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function useUpdateDataTableRow() {
|
|
376
|
+
const [loading, setLoading] = useState(false);
|
|
377
|
+
return {
|
|
378
|
+
updateDataTableRow: useCallback(async (id, data) => {
|
|
379
|
+
setLoading(true);
|
|
380
|
+
try {
|
|
381
|
+
const response = throwIfResultError(await getFluidGridsClient().mutate({
|
|
382
|
+
mutation: UPDATE_DATA_TABLE_ROW,
|
|
383
|
+
variables: {
|
|
384
|
+
id,
|
|
385
|
+
data
|
|
386
|
+
}
|
|
387
|
+
})).data?.updateDataTableRow;
|
|
388
|
+
if (!response?.success) throw new Error(response?.message || "Failed to update row");
|
|
389
|
+
return response.row ? toRow(response.row) : null;
|
|
390
|
+
} finally {
|
|
391
|
+
setLoading(false);
|
|
392
|
+
}
|
|
393
|
+
}, []),
|
|
394
|
+
loading
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function useDeleteDataTableRows() {
|
|
398
|
+
const [loading, setLoading] = useState(false);
|
|
399
|
+
return {
|
|
400
|
+
deleteDataTableRows: useCallback(async (tableId, ids) => {
|
|
401
|
+
setLoading(true);
|
|
402
|
+
try {
|
|
403
|
+
const response = throwIfResultError(await getFluidGridsClient().mutate({
|
|
404
|
+
mutation: DELETE_DATA_TABLE_ROWS,
|
|
405
|
+
variables: {
|
|
406
|
+
tableId,
|
|
407
|
+
ids
|
|
408
|
+
}
|
|
409
|
+
})).data?.deleteDataTableRows;
|
|
410
|
+
if (!response?.success) throw new Error(response?.message || "Failed to delete rows");
|
|
411
|
+
return response.affected;
|
|
412
|
+
} finally {
|
|
413
|
+
setLoading(false);
|
|
414
|
+
}
|
|
415
|
+
}, []),
|
|
416
|
+
loading
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/modules/data-tables/utils/values.ts
|
|
421
|
+
const TRUE_WORDS = /* @__PURE__ */ new Set([
|
|
422
|
+
"true",
|
|
423
|
+
"yes",
|
|
424
|
+
"1"
|
|
425
|
+
]);
|
|
426
|
+
const FALSE_WORDS = /* @__PURE__ */ new Set([
|
|
427
|
+
"false",
|
|
428
|
+
"no",
|
|
429
|
+
"0"
|
|
430
|
+
]);
|
|
431
|
+
/** "true"/"yes"/"1" and "false"/"no"/"0", any case; `undefined` for anything else. */
|
|
432
|
+
function parseBooleanText(value) {
|
|
433
|
+
const normalized = value.trim().toLowerCase();
|
|
434
|
+
if (TRUE_WORDS.has(normalized)) return true;
|
|
435
|
+
if (FALSE_WORDS.has(normalized)) return false;
|
|
436
|
+
}
|
|
437
|
+
const DATE_ONLY$1 = /^\d{4}-\d{2}-\d{2}$/;
|
|
438
|
+
const HAS_ZONE = /(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
439
|
+
/**
|
|
440
|
+
* A date as the service stores it. A date without a time stays as typed; a
|
|
441
|
+
* date with a time but no zone is read in the viewer's zone and saved with the
|
|
442
|
+
* zone (UTC) so it means the same moment for everyone. Throws on a value that
|
|
443
|
+
* is not a date.
|
|
444
|
+
*/
|
|
445
|
+
function normalizeDateValue(value) {
|
|
446
|
+
const trimmed = value.trim();
|
|
447
|
+
if (DATE_ONLY$1.test(trimmed)) {
|
|
448
|
+
if (Number.isNaN(Date.parse(`${trimmed}T00:00:00Z`))) throw new Error(`Expected a date, got "${value}"`);
|
|
449
|
+
return trimmed;
|
|
450
|
+
}
|
|
451
|
+
const parsed = new Date(trimmed);
|
|
452
|
+
if (Number.isNaN(parsed.getTime())) throw new Error(`Expected a date, got "${value}"`);
|
|
453
|
+
return HAS_ZONE.test(trimmed) ? trimmed : parsed.toISOString();
|
|
454
|
+
}
|
|
455
|
+
function pad(number) {
|
|
456
|
+
return String(number).padStart(2, "0");
|
|
457
|
+
}
|
|
458
|
+
/** A stored date as a `datetime-local` input value (viewer's zone); "" if unreadable. */
|
|
459
|
+
function dateToLocalInput(value) {
|
|
460
|
+
if (typeof value !== "string" || value === "") return "";
|
|
461
|
+
const text = DATE_ONLY$1.test(value) ? `${value}T00:00` : value;
|
|
462
|
+
const parsed = new Date(text);
|
|
463
|
+
if (Number.isNaN(parsed.getTime())) return "";
|
|
464
|
+
return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())}T${pad(parsed.getHours())}:${pad(parsed.getMinutes())}`;
|
|
465
|
+
}
|
|
466
|
+
/** The text a row-form input starts with for a stored value. */
|
|
467
|
+
function asFormValue(value, type) {
|
|
468
|
+
if (value === null || value === void 0) return "";
|
|
469
|
+
if (type === "JSON") return JSON.stringify(value, null, 2);
|
|
470
|
+
if (type === "DATE") return dateToLocalInput(value);
|
|
471
|
+
return String(value);
|
|
472
|
+
}
|
|
473
|
+
/** The value a row-form input saves; throws a plain message when it is not valid. */
|
|
474
|
+
function parseFormValue(value, type) {
|
|
475
|
+
if (type === "STRING") return value;
|
|
476
|
+
if (value.trim() === "") return null;
|
|
477
|
+
if (type === "DATE") return normalizeDateValue(value);
|
|
478
|
+
if (type === "NUMBER") {
|
|
479
|
+
const number = Number(value);
|
|
480
|
+
if (!Number.isFinite(number)) throw new Error(`"${value}" is not a number`);
|
|
481
|
+
return number;
|
|
482
|
+
}
|
|
483
|
+
if (type === "BOOLEAN") {
|
|
484
|
+
const parsed = parseBooleanText(value);
|
|
485
|
+
if (parsed === void 0) throw new Error("Boolean values must be true or false");
|
|
486
|
+
return parsed;
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
return JSON.parse(value);
|
|
490
|
+
} catch {
|
|
491
|
+
throw new Error("JSON values must contain valid JSON");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/modules/data-tables/utils/csv.ts
|
|
496
|
+
/** Parse RFC 4180-style CSV while preserving quoted commas and line breaks. */
|
|
497
|
+
function parseCsv(text) {
|
|
498
|
+
const rows = [];
|
|
499
|
+
let row = [];
|
|
500
|
+
let cell = "";
|
|
501
|
+
let quoted = false;
|
|
502
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
503
|
+
const character = text[index];
|
|
504
|
+
const next = text[index + 1];
|
|
505
|
+
if (quoted) {
|
|
506
|
+
if (character === "\"" && next === "\"") {
|
|
507
|
+
cell += "\"";
|
|
508
|
+
index += 1;
|
|
509
|
+
} else if (character === "\"") quoted = false;
|
|
510
|
+
else cell += character;
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (character === "\"" && cell.length === 0) quoted = true;
|
|
514
|
+
else if (character === ",") {
|
|
515
|
+
row.push(cell);
|
|
516
|
+
cell = "";
|
|
517
|
+
} else if (character === "\n" || character === "\r") {
|
|
518
|
+
if (character === "\r" && next === "\n") index += 1;
|
|
519
|
+
row.push(cell);
|
|
520
|
+
cell = "";
|
|
521
|
+
if (row.some((value) => value.length > 0)) rows.push(row);
|
|
522
|
+
row = [];
|
|
523
|
+
} else cell += character;
|
|
524
|
+
}
|
|
525
|
+
if (quoted) throw new Error("CSV contains an unterminated quoted value");
|
|
526
|
+
if (cell.length > 0 || row.length > 0) {
|
|
527
|
+
row.push(cell);
|
|
528
|
+
if (row.some((value) => value.length > 0)) rows.push(row);
|
|
529
|
+
}
|
|
530
|
+
return rows;
|
|
531
|
+
}
|
|
532
|
+
function parseValue(value, type) {
|
|
533
|
+
if (value === "") return type === "STRING" ? "" : null;
|
|
534
|
+
switch (type) {
|
|
535
|
+
case "NUMBER": {
|
|
536
|
+
const parsed = Number(value);
|
|
537
|
+
if (!Number.isFinite(parsed)) throw new Error(`Expected a number, got "${value}"`);
|
|
538
|
+
return parsed;
|
|
539
|
+
}
|
|
540
|
+
case "BOOLEAN": {
|
|
541
|
+
const parsed = parseBooleanText(value);
|
|
542
|
+
if (parsed === void 0) throw new Error(`Expected true or false, got "${value}"`);
|
|
543
|
+
return parsed;
|
|
544
|
+
}
|
|
545
|
+
case "JSON": try {
|
|
546
|
+
return JSON.parse(value);
|
|
547
|
+
} catch {
|
|
548
|
+
throw new Error(`Expected valid JSON, got "${value}"`);
|
|
549
|
+
}
|
|
550
|
+
case "DATE": return normalizeDateValue(value);
|
|
551
|
+
case "STRING": return value;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
/** Convert a CSV whose first row contains column keys into validated row data. */
|
|
555
|
+
function csvToRows(text, columns) {
|
|
556
|
+
const parsed = parseCsv(text);
|
|
557
|
+
if (parsed.length < 2) return [];
|
|
558
|
+
const headers = parsed[0].map((header) => header.trim());
|
|
559
|
+
const columnByKey = new Map(columns.map((column) => [column.key, column]));
|
|
560
|
+
const columnByLabel = new Map(columns.map((column) => [column.label, column]));
|
|
561
|
+
const seen = /* @__PURE__ */ new Set();
|
|
562
|
+
const headerColumns = headers.map((header) => {
|
|
563
|
+
const column = columnByKey.get(header) ?? columnByLabel.get(header);
|
|
564
|
+
if (!header || column && seen.has(column.key)) throw new Error("CSV must have unique, non-empty column headers");
|
|
565
|
+
if (!column) throw new Error(`CSV contains an unknown column "${header}"`);
|
|
566
|
+
seen.add(column.key);
|
|
567
|
+
return column;
|
|
568
|
+
});
|
|
569
|
+
return parsed.slice(1).map((values, rowIndex) => {
|
|
570
|
+
const row = {};
|
|
571
|
+
headerColumns.forEach((column, columnIndex) => {
|
|
572
|
+
const rawValue = values[columnIndex] ?? "";
|
|
573
|
+
try {
|
|
574
|
+
row[column.key] = parseValue(rawValue, column.type);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
const message = error instanceof Error ? error.message : "Invalid value";
|
|
577
|
+
throw new Error(`Row ${rowIndex + 2}, ${column.label}: ${message}`);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
return row;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function csvCell(value) {
|
|
584
|
+
if (value === null || value === void 0) return "";
|
|
585
|
+
const text = typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
586
|
+
return /[",\r\n]/.test(text) ? `"${text.replaceAll("\"", "\"\"")}"` : text;
|
|
587
|
+
}
|
|
588
|
+
/** Serialize table rows with a stable header matching the table schema. */
|
|
589
|
+
function rowsToCsv(columns, rows) {
|
|
590
|
+
return [columns.map((column) => csvCell(column.key)).join(","), ...rows.map((row) => columns.map((column) => csvCell(row.data[column.key])).join(","))].join("\r\n");
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// src/modules/data-tables/utils/columns.ts
|
|
594
|
+
/** "Contact e-mail" -> "contact_e_mail"; never empty, never starts with a digit. */
|
|
595
|
+
function deriveColumnKey(name) {
|
|
596
|
+
const base = name.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
597
|
+
if (base === "") return "column";
|
|
598
|
+
return /^[0-9]/.test(base) ? `column_${base}` : base;
|
|
599
|
+
}
|
|
600
|
+
/** `key`, or `key_2`, `key_3`… when `taken` already holds it. */
|
|
601
|
+
function uniqueColumnKey(key, taken) {
|
|
602
|
+
if (!taken.has(key)) return key;
|
|
603
|
+
let suffix = 2;
|
|
604
|
+
while (taken.has(`${key}_${suffix}`)) suffix += 1;
|
|
605
|
+
return `${key}_${suffix}`;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* What a save would change that a workflow can notice: the table's name, and
|
|
609
|
+
* columns removed, renamed (key) or given another kind of data. Columns are
|
|
610
|
+
* matched by their position in `originalKeys` (the key each existing column
|
|
611
|
+
* had when the editor opened), so a key edit is not read as remove + add.
|
|
612
|
+
*/
|
|
613
|
+
function describeSchemaChanges(table, next) {
|
|
614
|
+
const changes = [];
|
|
615
|
+
if (next.name.trim() !== table.name) changes.push({
|
|
616
|
+
kind: "renameTable",
|
|
617
|
+
from: table.name,
|
|
618
|
+
to: next.name.trim()
|
|
619
|
+
});
|
|
620
|
+
const kept = /* @__PURE__ */ new Set();
|
|
621
|
+
const beforeByKey = new Map(table.columns.map((c) => [c.key, c]));
|
|
622
|
+
for (const column of next.columns) {
|
|
623
|
+
if (column.originalKey === void 0) continue;
|
|
624
|
+
kept.add(column.originalKey);
|
|
625
|
+
const before = beforeByKey.get(column.originalKey);
|
|
626
|
+
if (!before) continue;
|
|
627
|
+
if (before.key !== column.key.trim()) changes.push({
|
|
628
|
+
kind: "changeKey",
|
|
629
|
+
from: before.key,
|
|
630
|
+
to: column.key.trim(),
|
|
631
|
+
label: before.label
|
|
632
|
+
});
|
|
633
|
+
if (before.type !== column.type) changes.push({
|
|
634
|
+
kind: "changeType",
|
|
635
|
+
key: before.key,
|
|
636
|
+
label: before.label,
|
|
637
|
+
from: before.type,
|
|
638
|
+
to: column.type
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
for (const before of table.columns) if (!kept.has(before.key)) changes.push({
|
|
642
|
+
kind: "removeColumn",
|
|
643
|
+
key: before.key,
|
|
644
|
+
label: before.label
|
|
645
|
+
});
|
|
646
|
+
return changes;
|
|
647
|
+
}
|
|
648
|
+
let columnUid = 0;
|
|
649
|
+
function nextColumnUid() {
|
|
650
|
+
columnUid += 1;
|
|
651
|
+
return `column-${columnUid}`;
|
|
652
|
+
}
|
|
653
|
+
/** The editor's columns for a table (or one blank column for a new table). */
|
|
654
|
+
function toEditableColumns(table, blank) {
|
|
655
|
+
if (!table) return [{
|
|
656
|
+
...blank,
|
|
657
|
+
uid: nextColumnUid()
|
|
658
|
+
}];
|
|
659
|
+
return table.columns.map((column) => ({
|
|
660
|
+
key: column.key,
|
|
661
|
+
label: column.label,
|
|
662
|
+
type: column.type,
|
|
663
|
+
required: column.required,
|
|
664
|
+
...column.defaultValue !== void 0 ? { defaultValue: column.defaultValue } : {},
|
|
665
|
+
uid: nextColumnUid(),
|
|
666
|
+
originalKey: column.key,
|
|
667
|
+
keyEdited: true
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
670
|
+
/** Strip the editor-only fields and trim names before saving. */
|
|
671
|
+
function toColumnInputs(columns) {
|
|
672
|
+
return columns.map((column) => ({
|
|
673
|
+
key: column.key.trim(),
|
|
674
|
+
label: column.label.trim(),
|
|
675
|
+
type: column.type,
|
|
676
|
+
...column.required !== void 0 ? { required: column.required } : {},
|
|
677
|
+
...column.defaultValue !== void 0 ? { defaultValue: column.defaultValue } : {}
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/modules/data-tables/utils/format.ts
|
|
682
|
+
function formatBytes(bytes) {
|
|
683
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
684
|
+
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
685
|
+
return `${(bytes / 1048576).toFixed(1)} MB`;
|
|
686
|
+
}
|
|
687
|
+
function formatValue(value) {
|
|
688
|
+
if (value === null || value === void 0) return "—";
|
|
689
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
690
|
+
return String(value);
|
|
691
|
+
}
|
|
692
|
+
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
693
|
+
const DATE_TIME_FORMAT = new Intl.DateTimeFormat(void 0, {
|
|
694
|
+
dateStyle: "medium",
|
|
695
|
+
timeStyle: "short"
|
|
696
|
+
});
|
|
697
|
+
const DATE_FORMAT = new Intl.DateTimeFormat(void 0, {
|
|
698
|
+
dateStyle: "medium",
|
|
699
|
+
timeZone: "UTC"
|
|
700
|
+
});
|
|
701
|
+
/** A cell as people read it: dates in their own zone, Yes / No as words. */
|
|
702
|
+
function formatCell(value, type, words) {
|
|
703
|
+
if (type === "BOOLEAN" && typeof value === "boolean") return value ? words.yes : words.no;
|
|
704
|
+
if (type === "DATE" && typeof value === "string") {
|
|
705
|
+
const parsed = new Date(value);
|
|
706
|
+
if (Number.isNaN(parsed.getTime())) return value;
|
|
707
|
+
return DATE_ONLY.test(value) ? DATE_FORMAT.format(parsed) : DATE_TIME_FORMAT.format(parsed);
|
|
708
|
+
}
|
|
709
|
+
return formatValue(value);
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Order two cells of one column (`direction` 1 = A to Z, -1 = Z to A). Empty
|
|
713
|
+
* cells sort last either way.
|
|
714
|
+
*/
|
|
715
|
+
function compareCells(a, b, type, direction = 1) {
|
|
716
|
+
const aEmpty = a === null || a === void 0 || a === "";
|
|
717
|
+
const bEmpty = b === null || b === void 0 || b === "";
|
|
718
|
+
if (aEmpty || bEmpty) return Number(aEmpty) - Number(bEmpty);
|
|
719
|
+
return direction * compareFilled(a, b, type);
|
|
720
|
+
}
|
|
721
|
+
function compareFilled(a, b, type) {
|
|
722
|
+
if (type === "NUMBER" && typeof a === "number" && typeof b === "number") return a - b;
|
|
723
|
+
if (type === "BOOLEAN") return Number(a === true) - Number(b === true);
|
|
724
|
+
if (type === "DATE") {
|
|
725
|
+
const diff = Date.parse(String(a)) - Date.parse(String(b));
|
|
726
|
+
if (!Number.isNaN(diff)) return diff;
|
|
727
|
+
}
|
|
728
|
+
return formatValue(a).localeCompare(formatValue(b), void 0, {
|
|
729
|
+
numeric: true,
|
|
730
|
+
sensitivity: "base"
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/modules/data-tables/components/RowFormDialogV2.tsx
|
|
735
|
+
/**
|
|
736
|
+
* Add / Edit row dialog for Navigation v2 (BOFF-11849).
|
|
737
|
+
*
|
|
738
|
+
* The label is just the column name; a Yes / No column is a switch, a Date
|
|
739
|
+
* column a date-and-time picker, and a JSON column sits under "More options"
|
|
740
|
+
* (open from the start when it is required or already has a value). A new row
|
|
741
|
+
* starts from each column's starting value. Every value lives in this dialog's
|
|
742
|
+
* state, so closing "More options" never loses what was typed.
|
|
743
|
+
*/
|
|
744
|
+
/** The values a row form starts with: the row's, or each column's starting value. */
|
|
745
|
+
function initialValues(table, row) {
|
|
746
|
+
const next = {};
|
|
747
|
+
for (const column of table?.columns ?? []) next[column.key] = asFormValue(row ? row.data[column.key] : column.defaultValue, column.type);
|
|
748
|
+
return next;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* The shell only mounts the form while the dialog is open, and the form is
|
|
752
|
+
* keyed by the table and row, so reopening (or editing another row) always
|
|
753
|
+
* starts from fresh values.
|
|
754
|
+
*/
|
|
755
|
+
function RowFormDialogV2({ open, table, row, onOpenChange, onSaved }) {
|
|
756
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
757
|
+
open,
|
|
758
|
+
onOpenChange,
|
|
759
|
+
children: /* @__PURE__ */ jsx(DialogContent, {
|
|
760
|
+
className: "max-h-[90vh] max-w-2xl overflow-y-auto",
|
|
761
|
+
"data-testid": "row-form-dialog",
|
|
762
|
+
children: /* @__PURE__ */ jsx(RowFormBody, {
|
|
763
|
+
table,
|
|
764
|
+
row,
|
|
765
|
+
onOpenChange,
|
|
766
|
+
onSaved
|
|
767
|
+
}, `${table?.id ?? "none"}:${row?.id ?? "new"}`)
|
|
768
|
+
})
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
function RowFormBody({ table, row, onOpenChange, onSaved }) {
|
|
772
|
+
const tr = useTr();
|
|
773
|
+
const { insertDataTableRow, loading: inserting } = useInsertDataTableRow();
|
|
774
|
+
const { updateDataTableRow, loading: updating } = useUpdateDataTableRow();
|
|
775
|
+
const [values, setValues] = useState(() => initialValues(table, row));
|
|
776
|
+
const editing = row !== null;
|
|
777
|
+
const saving = inserting || updating;
|
|
778
|
+
const setValue = useCallback((key, value) => {
|
|
779
|
+
setValues((current) => ({
|
|
780
|
+
...current,
|
|
781
|
+
[key]: value
|
|
782
|
+
}));
|
|
783
|
+
}, []);
|
|
784
|
+
const handleSubmit = useCallback(async () => {
|
|
785
|
+
if (!table) return;
|
|
786
|
+
try {
|
|
787
|
+
const data = {};
|
|
788
|
+
for (const column of table.columns) {
|
|
789
|
+
const value = values[column.key] ?? "";
|
|
790
|
+
if (column.required && value.trim() === "") throw new Error(`${column.label} is required`);
|
|
791
|
+
data[column.key] = parseFormValue(value, column.type);
|
|
792
|
+
}
|
|
793
|
+
if (editing && row) await updateDataTableRow(row.id, data);
|
|
794
|
+
else await insertDataTableRow(table.id, data);
|
|
795
|
+
toast.success(editing ? tr("fluidgrids.dataTables.rowUpdated", "Row updated") : tr("fluidgrids.dataTables.rowCreated", "Row added"));
|
|
796
|
+
onSaved();
|
|
797
|
+
onOpenChange(false);
|
|
798
|
+
} catch (error) {
|
|
799
|
+
toast.error(describeOperationError(error, tr("fluidgrids.dataTables.rowSaveFailed", "Failed to save row")));
|
|
800
|
+
}
|
|
801
|
+
}, [
|
|
802
|
+
editing,
|
|
803
|
+
insertDataTableRow,
|
|
804
|
+
onOpenChange,
|
|
805
|
+
onSaved,
|
|
806
|
+
row,
|
|
807
|
+
table,
|
|
808
|
+
tr,
|
|
809
|
+
updateDataTableRow,
|
|
810
|
+
values
|
|
811
|
+
]);
|
|
812
|
+
const everydayColumns = table?.columns.filter((c) => c.type !== "JSON") ?? [];
|
|
813
|
+
const jsonColumns = table?.columns.filter((c) => c.type === "JSON") ?? [];
|
|
814
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
815
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editRow", "Edit row") : tr("fluidgrids.dataTables.addRow", "Add row") }) }),
|
|
816
|
+
table && /* @__PURE__ */ jsxs("div", {
|
|
817
|
+
className: "space-y-4 py-2",
|
|
818
|
+
children: [everydayColumns.map((column) => {
|
|
819
|
+
const id = `data-table-row-${column.key}`;
|
|
820
|
+
const label = /* @__PURE__ */ jsxs(Label, {
|
|
821
|
+
htmlFor: id,
|
|
822
|
+
children: [column.label, column.required && /* @__PURE__ */ jsx("span", {
|
|
823
|
+
className: "ml-1 text-text-secondary",
|
|
824
|
+
children: "*"
|
|
825
|
+
})]
|
|
826
|
+
});
|
|
827
|
+
if (column.type === "BOOLEAN") return /* @__PURE__ */ jsxs("div", {
|
|
828
|
+
className: "flex items-center justify-between gap-3",
|
|
829
|
+
children: [label, /* @__PURE__ */ jsx(Switch, {
|
|
830
|
+
id,
|
|
831
|
+
checked: values[column.key] === "true",
|
|
832
|
+
onCheckedChange: (checked) => setValue(column.key, checked ? "true" : "false")
|
|
833
|
+
})]
|
|
834
|
+
}, column.key);
|
|
835
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
836
|
+
className: "space-y-1.5",
|
|
837
|
+
children: [label, /* @__PURE__ */ jsx(Input, {
|
|
838
|
+
id,
|
|
839
|
+
type: column.type === "NUMBER" ? "number" : column.type === "DATE" ? "datetime-local" : "text",
|
|
840
|
+
value: values[column.key] ?? "",
|
|
841
|
+
onChange: (event) => setValue(column.key, event.target.value)
|
|
842
|
+
})]
|
|
843
|
+
}, column.key);
|
|
844
|
+
}), jsonColumns.length > 0 && /* @__PURE__ */ jsx(Disclosure, {
|
|
845
|
+
closedLabel: tr("fluidgrids.dataTables.moreOptions", "More options"),
|
|
846
|
+
openLabel: tr("fluidgrids.dataTables.fewerOptions", "Fewer options"),
|
|
847
|
+
defaultOpen: jsonColumns.some((c) => c.required || (row ? row.data[c.key] : c.defaultValue) != null),
|
|
848
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
849
|
+
className: "space-y-4",
|
|
850
|
+
children: jsonColumns.map((column) => /* @__PURE__ */ jsxs("div", {
|
|
851
|
+
className: "space-y-1.5",
|
|
852
|
+
children: [/* @__PURE__ */ jsxs(Label, {
|
|
853
|
+
htmlFor: `data-table-row-${column.key}`,
|
|
854
|
+
children: [column.label, column.required && /* @__PURE__ */ jsx("span", {
|
|
855
|
+
className: "ml-1 text-text-secondary",
|
|
856
|
+
children: "*"
|
|
857
|
+
})]
|
|
858
|
+
}), /* @__PURE__ */ jsx(Textarea, {
|
|
859
|
+
id: `data-table-row-${column.key}`,
|
|
860
|
+
value: values[column.key] ?? "",
|
|
861
|
+
onChange: (event) => setValue(column.key, event.target.value),
|
|
862
|
+
rows: 4,
|
|
863
|
+
placeholder: "{ }"
|
|
864
|
+
})]
|
|
865
|
+
}, column.key))
|
|
866
|
+
})
|
|
867
|
+
})]
|
|
868
|
+
}),
|
|
869
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
|
|
870
|
+
variant: "outline",
|
|
871
|
+
onClick: () => onOpenChange(false),
|
|
872
|
+
children: tr("fluidgrids.dataTables.cancel", "Cancel")
|
|
873
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
874
|
+
onClick: handleSubmit,
|
|
875
|
+
disabled: saving || !table,
|
|
876
|
+
children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.saveRow", "Save row")
|
|
877
|
+
})] })
|
|
878
|
+
] });
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/modules/data-tables/utils/paging.ts
|
|
882
|
+
/** Page sizes offered in the rows pager; the first entry is the default. */
|
|
883
|
+
const ROW_PAGE_SIZES = [
|
|
884
|
+
50,
|
|
885
|
+
100,
|
|
886
|
+
250
|
|
887
|
+
];
|
|
888
|
+
const DEFAULT_ROW_PAGE_SIZE = ROW_PAGE_SIZES[0];
|
|
889
|
+
/** Number of pages needed for `total` rows; an empty table still has one page. */
|
|
890
|
+
function pageCount(total, pageSize) {
|
|
891
|
+
if (pageSize <= 0) return 1;
|
|
892
|
+
return Math.max(1, Math.ceil(total / pageSize));
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* Keep a zero-based page index inside the table. Needed after deleting rows,
|
|
896
|
+
* when the page being viewed may no longer exist.
|
|
897
|
+
*/
|
|
898
|
+
function clampPage(page, total, pageSize) {
|
|
899
|
+
return Math.min(Math.max(page, 0), pageCount(total, pageSize) - 1);
|
|
900
|
+
}
|
|
901
|
+
/** One-based, inclusive `from`–`to` range of the rows shown on a page. */
|
|
902
|
+
function pageRange(page, pageSize, shown) {
|
|
903
|
+
if (shown <= 0) return {
|
|
904
|
+
from: 0,
|
|
905
|
+
to: 0
|
|
906
|
+
};
|
|
907
|
+
const from = page * pageSize + 1;
|
|
908
|
+
return {
|
|
909
|
+
from,
|
|
910
|
+
to: from + shown - 1
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// src/modules/data-tables/components/RowsPanelHeaderV2.tsx
|
|
915
|
+
/**
|
|
916
|
+
* The top of the selected table's panel for Navigation v2 (BOFF-11849).
|
|
917
|
+
*
|
|
918
|
+
* Level 1: the table's name and description, its row count, and one action,
|
|
919
|
+
* "Add row". Level 2: a "⋯" menu (Edit table, Delete table, import and
|
|
920
|
+
* download as a spreadsheet, select several rows), "More options" (tags) and
|
|
921
|
+
* "Show technical details" (table ID with Copy, column count, size, row IDs,
|
|
922
|
+
* rows per page). Menu items a person has no permission for are left out.
|
|
923
|
+
*/
|
|
924
|
+
function RowsPanelHeaderV2({ table, total, importProgress, exporting, deletingRows, deletingTable, selectMode, onSelectModeChange, selectedCount, showRowIds, onShowRowIdsChange, pageSize, onPageSizeChange, onAddRow, onEditTable, onDeleteTable, onImport, onExport, onDeleteSelected, onTagsChanged }) {
|
|
925
|
+
const tr = useTr();
|
|
926
|
+
const canTag = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE).allowed;
|
|
927
|
+
const canEditTable = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_UPDATE).allowed;
|
|
928
|
+
const canDeleteTable = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_DELETE).allowed;
|
|
929
|
+
const canAddRows = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE).allowed;
|
|
930
|
+
const canDeleteRows = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_DELETE).allowed;
|
|
931
|
+
const copyId = useCallback(async () => {
|
|
932
|
+
try {
|
|
933
|
+
await navigator.clipboard.writeText(table.id);
|
|
934
|
+
toast.success(tr("fluidgrids.dataTables.idCopied", "Table ID copied"));
|
|
935
|
+
} catch {
|
|
936
|
+
toast.error(tr("fluidgrids.dataTables.copyFailed", "Could not copy. Select the text and copy it yourself."));
|
|
937
|
+
}
|
|
938
|
+
}, [table.id, tr]);
|
|
939
|
+
const items = [];
|
|
940
|
+
if (canEditTable) items.push({
|
|
941
|
+
id: "edit-table",
|
|
942
|
+
label: tr("fluidgrids.dataTables.editTable", "Edit table"),
|
|
943
|
+
icon: "mdi:pencil-outline",
|
|
944
|
+
onSelect: onEditTable
|
|
945
|
+
});
|
|
946
|
+
if (canAddRows) items.push({
|
|
947
|
+
id: "import",
|
|
948
|
+
label: importProgress ? tr("fluidgrids.dataTables.importing", `Importing ${importProgress.done} / ${importProgress.total}…`) : tr("fluidgrids.dataTables.import", "Import from spreadsheet (CSV)"),
|
|
949
|
+
icon: "mdi:tray-arrow-up",
|
|
950
|
+
onSelect: onImport,
|
|
951
|
+
disabled: importProgress !== null
|
|
952
|
+
});
|
|
953
|
+
items.push({
|
|
954
|
+
id: "export",
|
|
955
|
+
label: exporting ? tr("fluidgrids.dataTables.exporting", "Exporting…") : tr("fluidgrids.dataTables.export", "Download as CSV"),
|
|
956
|
+
icon: "mdi:tray-arrow-down",
|
|
957
|
+
onSelect: onExport,
|
|
958
|
+
disabled: exporting
|
|
959
|
+
});
|
|
960
|
+
if (canDeleteTable) items.push({
|
|
961
|
+
id: "delete-table",
|
|
962
|
+
label: deletingTable ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteTable", "Delete table"),
|
|
963
|
+
icon: "mdi:trash-can-outline",
|
|
964
|
+
onSelect: onDeleteTable,
|
|
965
|
+
disabled: deletingTable,
|
|
966
|
+
destructive: true
|
|
967
|
+
});
|
|
968
|
+
const toggles = canDeleteRows ? [{
|
|
969
|
+
id: "select-rows",
|
|
970
|
+
label: tr("fluidgrids.dataTables.selectSeveral", "Select several rows"),
|
|
971
|
+
checked: selectMode,
|
|
972
|
+
onCheckedChange: onSelectModeChange
|
|
973
|
+
}] : [];
|
|
974
|
+
return /* @__PURE__ */ jsxs(CardHeader, {
|
|
975
|
+
className: "gap-4 border-b border-border-secondary p-4 sm:p-5",
|
|
976
|
+
children: [
|
|
977
|
+
/* @__PURE__ */ jsxs("div", {
|
|
978
|
+
className: "flex flex-wrap items-start justify-between gap-3",
|
|
979
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
980
|
+
className: "min-w-0 flex-1 basis-64",
|
|
981
|
+
children: [/* @__PURE__ */ jsxs(CardTitle, {
|
|
982
|
+
className: "flex items-center gap-2 text-lg",
|
|
983
|
+
children: [/* @__PURE__ */ jsx(Table2, {
|
|
984
|
+
className: "size-5 shrink-0 text-text-secondary",
|
|
985
|
+
"aria-hidden": "true"
|
|
986
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
987
|
+
className: "truncate",
|
|
988
|
+
children: table.name
|
|
989
|
+
})]
|
|
990
|
+
}), /* @__PURE__ */ jsx(CardDescription, {
|
|
991
|
+
className: "mt-1",
|
|
992
|
+
children: table.description || tr("fluidgrids.dataTables.noDescription", "No description")
|
|
993
|
+
})]
|
|
994
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
995
|
+
className: "flex shrink-0 items-center gap-2",
|
|
996
|
+
children: [/* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
997
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE,
|
|
998
|
+
deniedTooltip: tr("fluidgrids.dataTables.addRowDeniedTooltip", "You don't have permission to add rows"),
|
|
999
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
1000
|
+
size: "sm",
|
|
1001
|
+
className: "whitespace-nowrap",
|
|
1002
|
+
onClick: onAddRow,
|
|
1003
|
+
disabled: importProgress !== null,
|
|
1004
|
+
children: [/* @__PURE__ */ jsx(Plus, {
|
|
1005
|
+
className: "mr-1.5 size-4",
|
|
1006
|
+
"aria-hidden": "true"
|
|
1007
|
+
}), tr("fluidgrids.dataTables.addRow", "Add row")]
|
|
1008
|
+
})
|
|
1009
|
+
}), /* @__PURE__ */ jsx(RunActionsMenu, {
|
|
1010
|
+
items,
|
|
1011
|
+
toggles,
|
|
1012
|
+
label: tr("fluidgrids.dataTables.moreActions", "More table actions"),
|
|
1013
|
+
"data-testid": "table-actions-menu"
|
|
1014
|
+
})]
|
|
1015
|
+
})]
|
|
1016
|
+
}),
|
|
1017
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1018
|
+
className: "flex flex-wrap items-center gap-2 text-xs text-text-secondary",
|
|
1019
|
+
children: [/* @__PURE__ */ jsxs(Badge, {
|
|
1020
|
+
variant: "outline",
|
|
1021
|
+
children: [
|
|
1022
|
+
total,
|
|
1023
|
+
" ",
|
|
1024
|
+
tr("fluidgrids.dataTables.rows", "rows")
|
|
1025
|
+
]
|
|
1026
|
+
}), selectMode && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
1027
|
+
"aria-live": "polite",
|
|
1028
|
+
children: tr("fluidgrids.dataTables.selectedCount", "{{count}} selected", { count: selectedCount })
|
|
1029
|
+
}), selectedCount > 0 && canDeleteRows && /* @__PURE__ */ jsxs(Button$1, {
|
|
1030
|
+
variant: "outline",
|
|
1031
|
+
size: "sm",
|
|
1032
|
+
className: "whitespace-nowrap",
|
|
1033
|
+
onClick: onDeleteSelected,
|
|
1034
|
+
disabled: deletingRows,
|
|
1035
|
+
"data-testid": "delete-selected-rows",
|
|
1036
|
+
children: [/* @__PURE__ */ jsx(Trash2, {
|
|
1037
|
+
className: "mr-1.5 size-4",
|
|
1038
|
+
"aria-hidden": "true"
|
|
1039
|
+
}), deletingRows ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteSelected", `Delete ${selectedCount} selected`)]
|
|
1040
|
+
})] })]
|
|
1041
|
+
}),
|
|
1042
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1043
|
+
className: "flex flex-col gap-2",
|
|
1044
|
+
children: [/* @__PURE__ */ jsx(Disclosure, {
|
|
1045
|
+
closedLabel: tr("fluidgrids.dataTables.moreOptions", "More options"),
|
|
1046
|
+
openLabel: tr("fluidgrids.dataTables.fewerOptions", "Fewer options"),
|
|
1047
|
+
"data-testid": "table-more-options",
|
|
1048
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1049
|
+
"data-testid": "data-table-tags",
|
|
1050
|
+
children: /* @__PURE__ */ jsx(EntityTagPicker, {
|
|
1051
|
+
entityType: "FLUIDGRIDS_DATA_TABLE",
|
|
1052
|
+
entityId: table.id,
|
|
1053
|
+
readOnly: !canTag,
|
|
1054
|
+
onChange: onTagsChanged
|
|
1055
|
+
})
|
|
1056
|
+
})
|
|
1057
|
+
}), /* @__PURE__ */ jsx(TechnicalDetails, {
|
|
1058
|
+
"data-testid": "table-technical-details",
|
|
1059
|
+
children: /* @__PURE__ */ jsxs("dl", {
|
|
1060
|
+
className: "grid gap-2 text-xs text-text-secondary sm:grid-cols-[auto_1fr] sm:items-center",
|
|
1061
|
+
children: [
|
|
1062
|
+
/* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.tableId", "Table ID") }),
|
|
1063
|
+
/* @__PURE__ */ jsxs("dd", {
|
|
1064
|
+
className: "flex flex-wrap items-center gap-2",
|
|
1065
|
+
children: [/* @__PURE__ */ jsx("code", {
|
|
1066
|
+
"data-testid": "table-id",
|
|
1067
|
+
children: table.id
|
|
1068
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1069
|
+
variant: "outline",
|
|
1070
|
+
size: "sm",
|
|
1071
|
+
onClick: () => void copyId(),
|
|
1072
|
+
"data-testid": "table-id-copy",
|
|
1073
|
+
children: tr("fluidgrids.dataTables.copyId", "Copy")
|
|
1074
|
+
})]
|
|
1075
|
+
}),
|
|
1076
|
+
/* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.columns", "columns") }),
|
|
1077
|
+
/* @__PURE__ */ jsx("dd", { children: table.columns.length }),
|
|
1078
|
+
/* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.size", "Size") }),
|
|
1079
|
+
/* @__PURE__ */ jsx("dd", { children: formatBytes(table.sizeBytes) }),
|
|
1080
|
+
/* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.rowsPerPage", "Rows per page") }),
|
|
1081
|
+
/* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx("select", {
|
|
1082
|
+
className: "h-8 rounded-md border border-input bg-background px-2 text-xs text-text-primary",
|
|
1083
|
+
value: pageSize,
|
|
1084
|
+
onChange: (event) => onPageSizeChange(Number(event.target.value)),
|
|
1085
|
+
"aria-label": tr("fluidgrids.dataTables.rowsPerPage", "Rows per page"),
|
|
1086
|
+
children: ROW_PAGE_SIZES.map((size) => /* @__PURE__ */ jsx("option", {
|
|
1087
|
+
value: size,
|
|
1088
|
+
children: size
|
|
1089
|
+
}, size))
|
|
1090
|
+
}) }),
|
|
1091
|
+
/* @__PURE__ */ jsx("dt", { children: tr("fluidgrids.dataTables.rowId", "Row") }),
|
|
1092
|
+
/* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsxs("label", {
|
|
1093
|
+
className: "flex items-center gap-2",
|
|
1094
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1095
|
+
type: "checkbox",
|
|
1096
|
+
checked: showRowIds,
|
|
1097
|
+
onChange: (event) => onShowRowIdsChange(event.target.checked)
|
|
1098
|
+
}), tr("fluidgrids.dataTables.showRowIds", "Show row IDs in the table")]
|
|
1099
|
+
}) })
|
|
1100
|
+
]
|
|
1101
|
+
})
|
|
1102
|
+
})]
|
|
1103
|
+
})
|
|
1104
|
+
]
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/modules/data-tables/utils/kinds.ts
|
|
1109
|
+
function kindLabel(tr, type) {
|
|
1110
|
+
switch (type) {
|
|
1111
|
+
case "STRING": return tr("fluidgrids.dataTables.kind.text", "Text");
|
|
1112
|
+
case "NUMBER": return tr("fluidgrids.dataTables.kind.number", "Number");
|
|
1113
|
+
case "BOOLEAN": return tr("fluidgrids.dataTables.kind.yesNo", "Yes / No");
|
|
1114
|
+
case "DATE": return tr("fluidgrids.dataTables.kind.date", "Date");
|
|
1115
|
+
case "JSON": return tr("fluidgrids.dataTables.kind.json", "JSON");
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/modules/data-tables/components/TableNameFields.tsx
|
|
1120
|
+
/** The Name and Description fields shared by both table dialogs. */
|
|
1121
|
+
function TableNameFields({ name, description, onNameChange, onDescriptionChange }) {
|
|
1122
|
+
const tr = useTr();
|
|
1123
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1124
|
+
className: "grid gap-4 sm:grid-cols-2",
|
|
1125
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1126
|
+
className: "space-y-1.5",
|
|
1127
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1128
|
+
htmlFor: "data-table-name",
|
|
1129
|
+
children: tr("fluidgrids.dataTables.name", "Name")
|
|
1130
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
1131
|
+
id: "data-table-name",
|
|
1132
|
+
value: name,
|
|
1133
|
+
onChange: (event) => onNameChange(event.target.value),
|
|
1134
|
+
placeholder: "contacts",
|
|
1135
|
+
autoFocus: true
|
|
1136
|
+
})]
|
|
1137
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1138
|
+
className: "space-y-1.5",
|
|
1139
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1140
|
+
htmlFor: "data-table-description",
|
|
1141
|
+
children: tr("fluidgrids.dataTables.description", "Description")
|
|
1142
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
1143
|
+
id: "data-table-description",
|
|
1144
|
+
value: description,
|
|
1145
|
+
onChange: (event) => onDescriptionChange(event.target.value),
|
|
1146
|
+
placeholder: tr("fluidgrids.dataTables.descriptionPlaceholder", "Optional")
|
|
1147
|
+
})]
|
|
1148
|
+
})]
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/modules/data-tables/components/TableFormDialogV2.tsx
|
|
1153
|
+
/**
|
|
1154
|
+
* New / Edit table dialog for Navigation v2 (BOFF-11849).
|
|
1155
|
+
*
|
|
1156
|
+
* Plain on top: a column has a name and a kind of data (Text, Number, Yes / No,
|
|
1157
|
+
* Date). The name a workflow uses for the column is worked out from its name;
|
|
1158
|
+
* "Must be filled in", the starting value, saving as JSON and the raw name sit
|
|
1159
|
+
* under each column's "More options". Before a save that renames the table,
|
|
1160
|
+
* removes a column or changes a column's name-in-workflows or kind, a warning
|
|
1161
|
+
* says what could stop working.
|
|
1162
|
+
*/
|
|
1163
|
+
/** The kinds people choose from; JSON is offered only to a column that already is JSON. */
|
|
1164
|
+
const EVERYDAY_KINDS = [
|
|
1165
|
+
"STRING",
|
|
1166
|
+
"NUMBER",
|
|
1167
|
+
"BOOLEAN",
|
|
1168
|
+
"DATE"
|
|
1169
|
+
];
|
|
1170
|
+
function blankColumn(index) {
|
|
1171
|
+
return {
|
|
1172
|
+
key: `column_${index}`,
|
|
1173
|
+
label: `Column ${index}`,
|
|
1174
|
+
type: "STRING",
|
|
1175
|
+
required: false
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
function withDefaultText(column) {
|
|
1179
|
+
return {
|
|
1180
|
+
...column,
|
|
1181
|
+
defaultText: column.defaultValue === void 0 ? "" : asFormValue(column.defaultValue, column.type)
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
function changeText(tr, change) {
|
|
1185
|
+
switch (change.kind) {
|
|
1186
|
+
case "renameTable": return tr("fluidgrids.dataTables.change.renameTable", "The table will be renamed from \"{{from}}\" to \"{{to}}\".", {
|
|
1187
|
+
from: change.from,
|
|
1188
|
+
to: change.to
|
|
1189
|
+
});
|
|
1190
|
+
case "removeColumn": return tr("fluidgrids.dataTables.change.removeColumn", "The column \"{{label}}\" will be removed, along with what is in it.", { label: change.label });
|
|
1191
|
+
case "changeKey": return tr("fluidgrids.dataTables.change.changeKey", "\"{{label}}\" will be used in workflows as \"{{to}}\" instead of \"{{from}}\".", {
|
|
1192
|
+
label: change.label,
|
|
1193
|
+
from: change.from,
|
|
1194
|
+
to: change.to
|
|
1195
|
+
});
|
|
1196
|
+
case "changeType": return tr("fluidgrids.dataTables.change.changeType", "\"{{label}}\" will change from {{from}} to {{to}}.", {
|
|
1197
|
+
label: change.label,
|
|
1198
|
+
from: kindLabel(tr, change.from),
|
|
1199
|
+
to: kindLabel(tr, change.to)
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* The shell only mounts the form while the dialog is open, and the form is
|
|
1205
|
+
* keyed by the table being edited, so reopening always starts fresh.
|
|
1206
|
+
*/
|
|
1207
|
+
function TableFormDialogV2({ open, table, onOpenChange, onSaved, workflowCount = null }) {
|
|
1208
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
1209
|
+
open,
|
|
1210
|
+
onOpenChange,
|
|
1211
|
+
children: /* @__PURE__ */ jsx(DialogContent, {
|
|
1212
|
+
className: "max-h-[90vh] max-w-3xl overflow-y-auto",
|
|
1213
|
+
"data-testid": "table-form-dialog",
|
|
1214
|
+
children: /* @__PURE__ */ jsx(TableFormBody, {
|
|
1215
|
+
table,
|
|
1216
|
+
onOpenChange,
|
|
1217
|
+
onSaved,
|
|
1218
|
+
workflowCount
|
|
1219
|
+
}, table?.id ?? "new")
|
|
1220
|
+
})
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
function TableFormBody({ table, onOpenChange, onSaved, workflowCount = null }) {
|
|
1224
|
+
const tr = useTr();
|
|
1225
|
+
const { createDataTable, loading: creating } = useCreateDataTable();
|
|
1226
|
+
const { updateDataTable, loading: updating } = useUpdateDataTable();
|
|
1227
|
+
const [name, setName] = useState(table?.name ?? "");
|
|
1228
|
+
const [description, setDescription] = useState(table?.description ?? "");
|
|
1229
|
+
const [columns, setColumns] = useState(() => toEditableColumns(table, blankColumn(1)).map(withDefaultText));
|
|
1230
|
+
const [pendingChanges, setPendingChanges] = useState(null);
|
|
1231
|
+
const editing = table !== null;
|
|
1232
|
+
const saving = creating || updating;
|
|
1233
|
+
const updateColumn = useCallback((uid, patch) => {
|
|
1234
|
+
setColumns((current) => current.map((column) => column.uid === uid ? {
|
|
1235
|
+
...column,
|
|
1236
|
+
...patch
|
|
1237
|
+
} : column));
|
|
1238
|
+
}, []);
|
|
1239
|
+
/** Typing a column name keeps its workflow name in step until that is edited by hand. */
|
|
1240
|
+
const renameColumn = useCallback((uid, label) => {
|
|
1241
|
+
setColumns((current) => current.map((column) => {
|
|
1242
|
+
if (column.uid !== uid) return column;
|
|
1243
|
+
if (column.keyEdited) return {
|
|
1244
|
+
...column,
|
|
1245
|
+
label
|
|
1246
|
+
};
|
|
1247
|
+
const taken = new Set(current.filter((c) => c.uid !== uid).map((c) => c.key));
|
|
1248
|
+
const derived = label.trim() ? uniqueColumnKey(deriveColumnKey(label), taken) : "";
|
|
1249
|
+
return {
|
|
1250
|
+
...column,
|
|
1251
|
+
label,
|
|
1252
|
+
key: derived
|
|
1253
|
+
};
|
|
1254
|
+
}));
|
|
1255
|
+
}, []);
|
|
1256
|
+
/** The columns as they will be saved, or a plain message why they cannot be. */
|
|
1257
|
+
const buildColumns = useCallback(() => {
|
|
1258
|
+
const resolved = [];
|
|
1259
|
+
const originals = new Map(table?.columns.map((c) => [c.key, c]));
|
|
1260
|
+
for (const column of columns) {
|
|
1261
|
+
const original = column.originalKey === void 0 ? void 0 : originals.get(column.originalKey);
|
|
1262
|
+
const text = (column.defaultText ?? "").trim();
|
|
1263
|
+
const unchanged = original !== void 0 && original.type === column.type && (column.defaultText ?? "") === (original.defaultValue === void 0 ? "" : asFormValue(original.defaultValue, original.type));
|
|
1264
|
+
let next = { ...column };
|
|
1265
|
+
if (!unchanged) {
|
|
1266
|
+
if (text === "") delete next.defaultValue;
|
|
1267
|
+
else try {
|
|
1268
|
+
next = {
|
|
1269
|
+
...next,
|
|
1270
|
+
defaultValue: parseFormValue(text, column.type)
|
|
1271
|
+
};
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
return tr("fluidgrids.dataTables.startingValueInvalid", "The starting value for \"{{label}}\" is not valid: {{reason}}", {
|
|
1274
|
+
label: column.label.trim() || column.key,
|
|
1275
|
+
reason: error instanceof Error ? error.message : ""
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
resolved.push(next);
|
|
1280
|
+
}
|
|
1281
|
+
return resolved;
|
|
1282
|
+
}, [
|
|
1283
|
+
columns,
|
|
1284
|
+
table,
|
|
1285
|
+
tr
|
|
1286
|
+
]);
|
|
1287
|
+
const save = useCallback(async (resolved) => {
|
|
1288
|
+
const input = {
|
|
1289
|
+
name: name.trim(),
|
|
1290
|
+
description: description.trim() || void 0,
|
|
1291
|
+
columns: toColumnInputs(resolved)
|
|
1292
|
+
};
|
|
1293
|
+
try {
|
|
1294
|
+
const saved = editing ? await updateDataTable(table.id, input) : await createDataTable(input);
|
|
1295
|
+
toast.success(editing ? tr("fluidgrids.dataTables.updated", "Data table updated") : tr("fluidgrids.dataTables.created", "Data table created"));
|
|
1296
|
+
setPendingChanges(null);
|
|
1297
|
+
onSaved(saved);
|
|
1298
|
+
onOpenChange(false);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
setPendingChanges(null);
|
|
1301
|
+
toast.error(describeOperationError(error, tr("fluidgrids.dataTables.saveFailed", "Failed to save data table")));
|
|
1302
|
+
}
|
|
1303
|
+
}, [
|
|
1304
|
+
createDataTable,
|
|
1305
|
+
description,
|
|
1306
|
+
editing,
|
|
1307
|
+
name,
|
|
1308
|
+
onOpenChange,
|
|
1309
|
+
onSaved,
|
|
1310
|
+
table,
|
|
1311
|
+
tr,
|
|
1312
|
+
updateDataTable
|
|
1313
|
+
]);
|
|
1314
|
+
const handleSubmit = useCallback(async () => {
|
|
1315
|
+
if (!name.trim()) {
|
|
1316
|
+
toast.error(tr("fluidgrids.dataTables.nameRequired", "Table name is required"));
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
if (columns.length === 0 || columns.some((column) => !column.key.trim() || !column.label.trim())) {
|
|
1320
|
+
toast.error(tr("fluidgrids.dataTables.columnsRequired", "Add at least one column and give every column a name"));
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
if (new Set(columns.map((column) => column.key.trim())).size !== columns.length) {
|
|
1324
|
+
toast.error(tr("fluidgrids.dataTables.uniqueKeys", "Each column needs a different name."));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const resolved = buildColumns();
|
|
1328
|
+
if (typeof resolved === "string") {
|
|
1329
|
+
toast.error(resolved);
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (table) {
|
|
1333
|
+
const changes = describeSchemaChanges(table, {
|
|
1334
|
+
name,
|
|
1335
|
+
columns: resolved
|
|
1336
|
+
});
|
|
1337
|
+
if (changes.length > 0) {
|
|
1338
|
+
setPendingChanges(changes);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
await save(resolved);
|
|
1343
|
+
}, [
|
|
1344
|
+
buildColumns,
|
|
1345
|
+
columns,
|
|
1346
|
+
name,
|
|
1347
|
+
save,
|
|
1348
|
+
table,
|
|
1349
|
+
tr
|
|
1350
|
+
]);
|
|
1351
|
+
const confirmChanges = useCallback(() => {
|
|
1352
|
+
const resolved = buildColumns();
|
|
1353
|
+
if (typeof resolved === "string") {
|
|
1354
|
+
setPendingChanges(null);
|
|
1355
|
+
toast.error(resolved);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
save(resolved);
|
|
1359
|
+
}, [buildColumns, save]);
|
|
1360
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1361
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editTable", "Edit table") : tr("fluidgrids.dataTables.newTable", "New table") }) }),
|
|
1362
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1363
|
+
className: "space-y-5 py-2",
|
|
1364
|
+
children: [/* @__PURE__ */ jsx(TableNameFields, {
|
|
1365
|
+
name,
|
|
1366
|
+
description,
|
|
1367
|
+
onNameChange: setName,
|
|
1368
|
+
onDescriptionChange: setDescription
|
|
1369
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1370
|
+
className: "space-y-3",
|
|
1371
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1372
|
+
className: "flex items-center justify-between gap-3",
|
|
1373
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
1374
|
+
className: "text-sm font-semibold text-text-primary",
|
|
1375
|
+
children: tr("fluidgrids.dataTables.columns", "Columns")
|
|
1376
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1377
|
+
className: "text-xs text-text-secondary",
|
|
1378
|
+
children: tr("fluidgrids.dataTables.columnsHint", "Each column needs a different name.")
|
|
1379
|
+
})] }), /* @__PURE__ */ jsxs(Button$1, {
|
|
1380
|
+
type: "button",
|
|
1381
|
+
variant: "outline",
|
|
1382
|
+
size: "sm",
|
|
1383
|
+
onClick: () => setColumns((current) => {
|
|
1384
|
+
const taken = new Set(current.map((c) => c.key));
|
|
1385
|
+
const blank = blankColumn(current.length + 1);
|
|
1386
|
+
return [...current, withDefaultText({
|
|
1387
|
+
...blank,
|
|
1388
|
+
key: uniqueColumnKey(blank.key, taken),
|
|
1389
|
+
uid: nextColumnUid()
|
|
1390
|
+
})];
|
|
1391
|
+
}),
|
|
1392
|
+
children: [/* @__PURE__ */ jsx(Plus, {
|
|
1393
|
+
className: "mr-1.5 size-4",
|
|
1394
|
+
"aria-hidden": "true"
|
|
1395
|
+
}), tr("fluidgrids.dataTables.addColumn", "Add column")]
|
|
1396
|
+
})]
|
|
1397
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1398
|
+
className: "space-y-2",
|
|
1399
|
+
children: columns.map((column, index) => /* @__PURE__ */ jsx(ColumnEditor, {
|
|
1400
|
+
column,
|
|
1401
|
+
index,
|
|
1402
|
+
canRemove: columns.length > 1,
|
|
1403
|
+
onRename: renameColumn,
|
|
1404
|
+
onChange: updateColumn,
|
|
1405
|
+
onRemove: (uid) => setColumns((current) => current.filter((c) => c.uid !== uid))
|
|
1406
|
+
}, column.uid))
|
|
1407
|
+
})]
|
|
1408
|
+
})]
|
|
1409
|
+
}),
|
|
1410
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
|
|
1411
|
+
variant: "outline",
|
|
1412
|
+
onClick: () => onOpenChange(false),
|
|
1413
|
+
children: tr("fluidgrids.dataTables.cancel", "Cancel")
|
|
1414
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1415
|
+
onClick: handleSubmit,
|
|
1416
|
+
disabled: saving,
|
|
1417
|
+
children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.save", "Save table")
|
|
1418
|
+
})] }),
|
|
1419
|
+
/* @__PURE__ */ jsx(ConfirmActionDialog, {
|
|
1420
|
+
open: pendingChanges !== null,
|
|
1421
|
+
onOpenChange: (next) => {
|
|
1422
|
+
if (!next) setPendingChanges(null);
|
|
1423
|
+
},
|
|
1424
|
+
title: tr("fluidgrids.dataTables.schemaWarningTitle", "Save these changes to the table?"),
|
|
1425
|
+
description: /* @__PURE__ */ jsxs("div", {
|
|
1426
|
+
className: "space-y-2",
|
|
1427
|
+
"data-testid": "schema-change-warning",
|
|
1428
|
+
children: [/* @__PURE__ */ jsx("p", { children: workflowCount !== null ? tr("fluidgrids.dataTables.schemaWarningCount", "{{count}} workflow(s) use this table, and they may stop working after these changes.", { count: workflowCount }) : tr("fluidgrids.dataTables.schemaWarningIntro", "Workflows that use this table may stop working after these changes.") }), /* @__PURE__ */ jsx("ul", {
|
|
1429
|
+
className: "list-disc space-y-1 pl-5",
|
|
1430
|
+
children: (pendingChanges ?? []).map((change) => {
|
|
1431
|
+
const text = changeText(tr, change);
|
|
1432
|
+
return /* @__PURE__ */ jsx("li", { children: text }, text);
|
|
1433
|
+
})
|
|
1434
|
+
})]
|
|
1435
|
+
}),
|
|
1436
|
+
confirmLabel: tr("fluidgrids.dataTables.schemaWarningConfirm", "Save anyway"),
|
|
1437
|
+
cancelLabel: tr("fluidgrids.dataTables.schemaWarningBack", "Go back"),
|
|
1438
|
+
busy: saving,
|
|
1439
|
+
onConfirm: confirmChanges,
|
|
1440
|
+
"data-testid": "schema-change-dialog"
|
|
1441
|
+
})
|
|
1442
|
+
] });
|
|
1443
|
+
}
|
|
1444
|
+
function ColumnEditor({ column, index, canRemove, onRename, onChange, onRemove }) {
|
|
1445
|
+
const tr = useTr();
|
|
1446
|
+
const kinds = column.type === "JSON" ? [...EVERYDAY_KINDS, "JSON"] : EVERYDAY_KINDS;
|
|
1447
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1448
|
+
className: "space-y-2 rounded-md border border-border-secondary p-3",
|
|
1449
|
+
"data-testid": "column-editor",
|
|
1450
|
+
children: [
|
|
1451
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1452
|
+
className: "grid gap-2 sm:grid-cols-[1fr_10rem_auto] sm:items-end",
|
|
1453
|
+
children: [
|
|
1454
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1455
|
+
className: "space-y-1",
|
|
1456
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1457
|
+
htmlFor: `data-table-column-label-${column.uid}`,
|
|
1458
|
+
children: tr("fluidgrids.dataTables.label", "Column name")
|
|
1459
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
1460
|
+
id: `data-table-column-label-${column.uid}`,
|
|
1461
|
+
value: column.label,
|
|
1462
|
+
onChange: (event) => onRename(column.uid, event.target.value)
|
|
1463
|
+
})]
|
|
1464
|
+
}),
|
|
1465
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1466
|
+
className: "space-y-1",
|
|
1467
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1468
|
+
htmlFor: `data-table-column-type-${column.uid}`,
|
|
1469
|
+
children: tr("fluidgrids.dataTables.type", "Kind of data")
|
|
1470
|
+
}), /* @__PURE__ */ jsx("select", {
|
|
1471
|
+
id: `data-table-column-type-${column.uid}`,
|
|
1472
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-text-primary",
|
|
1473
|
+
value: column.type,
|
|
1474
|
+
onChange: (event) => onChange(column.uid, { type: event.target.value }),
|
|
1475
|
+
children: kinds.map((type) => /* @__PURE__ */ jsx("option", {
|
|
1476
|
+
value: type,
|
|
1477
|
+
children: kindLabel(tr, type)
|
|
1478
|
+
}, type))
|
|
1479
|
+
})]
|
|
1480
|
+
}),
|
|
1481
|
+
/* @__PURE__ */ jsx("button", {
|
|
1482
|
+
type: "button",
|
|
1483
|
+
className: "justify-self-end rounded p-2 text-text-secondary hover:bg-background-tertiary hover:text-text-primary disabled:opacity-40",
|
|
1484
|
+
onClick: () => onRemove(column.uid),
|
|
1485
|
+
disabled: !canRemove,
|
|
1486
|
+
title: tr("fluidgrids.dataTables.removeColumn", "Remove column"),
|
|
1487
|
+
"aria-label": `${tr("fluidgrids.dataTables.removeColumn", "Remove column")} ${index + 1}`,
|
|
1488
|
+
children: /* @__PURE__ */ jsx(Trash2, {
|
|
1489
|
+
className: "size-4",
|
|
1490
|
+
"aria-hidden": "true"
|
|
1491
|
+
})
|
|
1492
|
+
})
|
|
1493
|
+
]
|
|
1494
|
+
}),
|
|
1495
|
+
/* @__PURE__ */ jsxs("p", {
|
|
1496
|
+
className: "text-xs text-text-secondary",
|
|
1497
|
+
"data-testid": "column-key-hint",
|
|
1498
|
+
children: [
|
|
1499
|
+
tr("fluidgrids.dataTables.key", "Used in workflows as"),
|
|
1500
|
+
":",
|
|
1501
|
+
" ",
|
|
1502
|
+
/* @__PURE__ */ jsx("code", { children: column.key || "—" })
|
|
1503
|
+
]
|
|
1504
|
+
}),
|
|
1505
|
+
/* @__PURE__ */ jsx(Disclosure, {
|
|
1506
|
+
closedLabel: tr("fluidgrids.dataTables.moreOptions", "More options"),
|
|
1507
|
+
openLabel: tr("fluidgrids.dataTables.fewerOptions", "Fewer options"),
|
|
1508
|
+
defaultOpen: column.required === true || column.type === "JSON" || (column.defaultText ?? "") !== "",
|
|
1509
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1510
|
+
className: "grid gap-3 sm:grid-cols-2",
|
|
1511
|
+
children: [
|
|
1512
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1513
|
+
className: "space-y-1",
|
|
1514
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1515
|
+
htmlFor: `data-table-column-key-${column.uid}`,
|
|
1516
|
+
children: tr("fluidgrids.dataTables.key", "Used in workflows as")
|
|
1517
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
1518
|
+
id: `data-table-column-key-${column.uid}`,
|
|
1519
|
+
value: column.key,
|
|
1520
|
+
onChange: (event) => onChange(column.uid, {
|
|
1521
|
+
key: event.target.value,
|
|
1522
|
+
keyEdited: true
|
|
1523
|
+
})
|
|
1524
|
+
})]
|
|
1525
|
+
}),
|
|
1526
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1527
|
+
className: "space-y-1",
|
|
1528
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1529
|
+
htmlFor: `data-table-column-default-${column.uid}`,
|
|
1530
|
+
children: tr("fluidgrids.dataTables.defaultValue", "Starting value")
|
|
1531
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
1532
|
+
id: `data-table-column-default-${column.uid}`,
|
|
1533
|
+
value: column.defaultText ?? "",
|
|
1534
|
+
onChange: (event) => onChange(column.uid, { defaultText: event.target.value }),
|
|
1535
|
+
placeholder: tr("fluidgrids.dataTables.defaultValueHint", "Filled in when you add a row")
|
|
1536
|
+
})]
|
|
1537
|
+
}),
|
|
1538
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1539
|
+
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
1540
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1541
|
+
type: "checkbox",
|
|
1542
|
+
checked: column.required ?? false,
|
|
1543
|
+
onChange: (event) => onChange(column.uid, { required: event.target.checked })
|
|
1544
|
+
}), tr("fluidgrids.dataTables.required", "Must be filled in")]
|
|
1545
|
+
}),
|
|
1546
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1547
|
+
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
1548
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1549
|
+
type: "checkbox",
|
|
1550
|
+
checked: column.type === "JSON",
|
|
1551
|
+
onChange: (event) => onChange(column.uid, { type: event.target.checked ? "JSON" : "STRING" })
|
|
1552
|
+
}), tr("fluidgrids.dataTables.saveAsJson", "Save values as JSON (for nested data)")]
|
|
1553
|
+
})
|
|
1554
|
+
]
|
|
1555
|
+
})
|
|
1556
|
+
})
|
|
1557
|
+
]
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
// src/modules/data-tables/pages/DataTablesPage.tsx
|
|
1562
|
+
var DataTablesPage_exports = /* @__PURE__ */ __exportAll({ DataTablesPage: () => DataTablesPage });
|
|
1563
|
+
const COLUMN_TYPES = [
|
|
1564
|
+
"STRING",
|
|
1565
|
+
"NUMBER",
|
|
1566
|
+
"BOOLEAN",
|
|
1567
|
+
"DATE",
|
|
1568
|
+
"JSON"
|
|
1569
|
+
];
|
|
1570
|
+
function emptyColumn(index) {
|
|
1571
|
+
return {
|
|
1572
|
+
key: `column_${index}`,
|
|
1573
|
+
label: `Column ${index}`,
|
|
1574
|
+
type: "STRING",
|
|
1575
|
+
required: false
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function columnTypeLabel(type) {
|
|
1579
|
+
return type === "JSON" ? "JSON" : type[0] + type.slice(1).toLowerCase();
|
|
1580
|
+
}
|
|
1581
|
+
function TableFormDialog({ open, table, onOpenChange, onSaved }) {
|
|
1582
|
+
const tr = useTr();
|
|
1583
|
+
const { createDataTable, loading: creating } = useCreateDataTable();
|
|
1584
|
+
const { updateDataTable, loading: updating } = useUpdateDataTable();
|
|
1585
|
+
const [name, setName] = useState("");
|
|
1586
|
+
const [description, setDescription] = useState("");
|
|
1587
|
+
const [columns, setColumns] = useState([]);
|
|
1588
|
+
const editing = table !== null;
|
|
1589
|
+
const saving = creating || updating;
|
|
1590
|
+
useEffect(() => {
|
|
1591
|
+
if (!open) return;
|
|
1592
|
+
setName(table?.name ?? "");
|
|
1593
|
+
setDescription(table?.description ?? "");
|
|
1594
|
+
setColumns(toEditableColumns(table, emptyColumn(1)));
|
|
1595
|
+
}, [open, table]);
|
|
1596
|
+
const updateColumn = useCallback((index, patch) => {
|
|
1597
|
+
setColumns((current) => current.map((column, columnIndex) => columnIndex === index ? {
|
|
1598
|
+
...column,
|
|
1599
|
+
...patch
|
|
1600
|
+
} : column));
|
|
1601
|
+
}, []);
|
|
1602
|
+
const handleSubmit = useCallback(async () => {
|
|
1603
|
+
const trimmedName = name.trim();
|
|
1604
|
+
if (!trimmedName) {
|
|
1605
|
+
toast.error(tr("fluidgrids.dataTables.nameRequired", "Table name is required"));
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
if (columns.length === 0 || columns.some((column) => !column.key.trim() || !column.label.trim())) {
|
|
1609
|
+
toast.error(tr("fluidgrids.dataTables.columnsRequired", "Add at least one column with a key and label"));
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const normalizedColumns = toColumnInputs(columns);
|
|
1613
|
+
if (new Set(normalizedColumns.map((column) => column.key)).size !== normalizedColumns.length) {
|
|
1614
|
+
toast.error(tr("fluidgrids.dataTables.uniqueKeys", "Column keys must be unique"));
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
try {
|
|
1618
|
+
const saved = editing ? await updateDataTable(table.id, {
|
|
1619
|
+
name: trimmedName,
|
|
1620
|
+
description: description.trim() || void 0,
|
|
1621
|
+
columns: normalizedColumns
|
|
1622
|
+
}) : await createDataTable({
|
|
1623
|
+
name: trimmedName,
|
|
1624
|
+
description: description.trim() || void 0,
|
|
1625
|
+
columns: normalizedColumns
|
|
1626
|
+
});
|
|
1627
|
+
toast.success(editing ? tr("fluidgrids.dataTables.updated", "Data table updated") : tr("fluidgrids.dataTables.created", "Data table created"));
|
|
1628
|
+
onSaved(saved);
|
|
1629
|
+
onOpenChange(false);
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
toast.error(describeOperationError(error, tr("fluidgrids.dataTables.saveFailed", "Failed to save data table")));
|
|
1632
|
+
}
|
|
1633
|
+
}, [
|
|
1634
|
+
columns,
|
|
1635
|
+
createDataTable,
|
|
1636
|
+
description,
|
|
1637
|
+
editing,
|
|
1638
|
+
name,
|
|
1639
|
+
onOpenChange,
|
|
1640
|
+
onSaved,
|
|
1641
|
+
table,
|
|
1642
|
+
tr,
|
|
1643
|
+
updateDataTable
|
|
1644
|
+
]);
|
|
1645
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
1646
|
+
open,
|
|
1647
|
+
onOpenChange,
|
|
1648
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
1649
|
+
className: "max-h-[90vh] max-w-3xl overflow-y-auto",
|
|
1650
|
+
children: [
|
|
1651
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editTable", "Edit data table") : tr("fluidgrids.dataTables.newTable", "New data table") }) }),
|
|
1652
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1653
|
+
className: "space-y-5 py-2",
|
|
1654
|
+
children: [/* @__PURE__ */ jsx(TableNameFields, {
|
|
1655
|
+
name,
|
|
1656
|
+
description,
|
|
1657
|
+
onNameChange: setName,
|
|
1658
|
+
onDescriptionChange: setDescription
|
|
1659
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1660
|
+
className: "space-y-3",
|
|
1661
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1662
|
+
className: "flex items-center justify-between gap-3",
|
|
1663
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h3", {
|
|
1664
|
+
className: "text-sm font-semibold text-text-primary",
|
|
1665
|
+
children: tr("fluidgrids.dataTables.columns", "Columns")
|
|
1666
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1667
|
+
className: "text-xs text-text-secondary",
|
|
1668
|
+
children: tr("fluidgrids.dataTables.columnsHint", "Column keys are used by workflow nodes and must be unique.")
|
|
1669
|
+
})] }), /* @__PURE__ */ jsxs(Button$1, {
|
|
1670
|
+
type: "button",
|
|
1671
|
+
variant: "outline",
|
|
1672
|
+
size: "sm",
|
|
1673
|
+
onClick: () => setColumns((current) => [...current, {
|
|
1674
|
+
...emptyColumn(current.length + 1),
|
|
1675
|
+
uid: nextColumnUid()
|
|
1676
|
+
}]),
|
|
1677
|
+
children: [/* @__PURE__ */ jsx(Plus, {
|
|
1678
|
+
className: "mr-1.5 size-4",
|
|
1679
|
+
"aria-hidden": "true"
|
|
1680
|
+
}), tr("fluidgrids.dataTables.addColumn", "Add column")]
|
|
1681
|
+
})]
|
|
1682
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1683
|
+
className: "space-y-2",
|
|
1684
|
+
children: columns.map((column, index) => /* @__PURE__ */ jsxs("div", {
|
|
1685
|
+
className: "grid gap-2 rounded-md border border-border-secondary p-3 sm:grid-cols-[1fr_1fr_8rem_auto] sm:items-end",
|
|
1686
|
+
children: [
|
|
1687
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1688
|
+
className: "space-y-1",
|
|
1689
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1690
|
+
htmlFor: `data-table-column-key-${index}`,
|
|
1691
|
+
children: tr("fluidgrids.dataTables.key", "Key")
|
|
1692
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
1693
|
+
id: `data-table-column-key-${index}`,
|
|
1694
|
+
value: column.key,
|
|
1695
|
+
onChange: (event) => updateColumn(index, { key: event.target.value })
|
|
1696
|
+
})]
|
|
1697
|
+
}),
|
|
1698
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1699
|
+
className: "space-y-1",
|
|
1700
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1701
|
+
htmlFor: `data-table-column-label-${index}`,
|
|
1702
|
+
children: tr("fluidgrids.dataTables.label", "Label")
|
|
1703
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
1704
|
+
id: `data-table-column-label-${index}`,
|
|
1705
|
+
value: column.label,
|
|
1706
|
+
onChange: (event) => updateColumn(index, { label: event.target.value })
|
|
1707
|
+
})]
|
|
1708
|
+
}),
|
|
1709
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1710
|
+
className: "space-y-1",
|
|
1711
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
1712
|
+
htmlFor: `data-table-column-type-${index}`,
|
|
1713
|
+
children: tr("fluidgrids.dataTables.type", "Type")
|
|
1714
|
+
}), /* @__PURE__ */ jsx("select", {
|
|
1715
|
+
id: `data-table-column-type-${index}`,
|
|
1716
|
+
className: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-text-primary",
|
|
1717
|
+
value: column.type,
|
|
1718
|
+
onChange: (event) => updateColumn(index, { type: event.target.value }),
|
|
1719
|
+
children: COLUMN_TYPES.map((type) => /* @__PURE__ */ jsx("option", {
|
|
1720
|
+
value: type,
|
|
1721
|
+
children: columnTypeLabel(type)
|
|
1722
|
+
}, type))
|
|
1723
|
+
})]
|
|
1724
|
+
}),
|
|
1725
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1726
|
+
className: "flex items-center gap-2 pb-2 sm:flex-col sm:items-center sm:gap-1",
|
|
1727
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
1728
|
+
className: "flex items-center gap-2 text-xs text-text-secondary",
|
|
1729
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1730
|
+
type: "checkbox",
|
|
1731
|
+
checked: column.required ?? false,
|
|
1732
|
+
onChange: (event) => updateColumn(index, { required: event.target.checked })
|
|
1733
|
+
}), tr("fluidgrids.dataTables.required", "Required")]
|
|
1734
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1735
|
+
type: "button",
|
|
1736
|
+
className: "rounded p-1 text-text-secondary hover:bg-background-tertiary hover:text-text-primary disabled:opacity-40",
|
|
1737
|
+
onClick: () => setColumns((current) => current.filter((_, columnIndex) => columnIndex !== index)),
|
|
1738
|
+
disabled: columns.length === 1,
|
|
1739
|
+
title: tr("fluidgrids.dataTables.removeColumn", "Remove column"),
|
|
1740
|
+
"aria-label": tr("fluidgrids.dataTables.removeColumn", "Remove column"),
|
|
1741
|
+
children: /* @__PURE__ */ jsx(Trash2, {
|
|
1742
|
+
className: "size-4",
|
|
1743
|
+
"aria-hidden": "true"
|
|
1744
|
+
})
|
|
1745
|
+
})]
|
|
1746
|
+
})
|
|
1747
|
+
]
|
|
1748
|
+
}, column.uid))
|
|
1749
|
+
})]
|
|
1750
|
+
})]
|
|
1751
|
+
}),
|
|
1752
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
|
|
1753
|
+
variant: "outline",
|
|
1754
|
+
onClick: () => onOpenChange(false),
|
|
1755
|
+
children: tr("fluidgrids.dataTables.cancel", "Cancel")
|
|
1756
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1757
|
+
onClick: handleSubmit,
|
|
1758
|
+
disabled: saving,
|
|
1759
|
+
children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.save", "Save table")
|
|
1760
|
+
})] })
|
|
1761
|
+
]
|
|
1762
|
+
})
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
function RowFormDialog({ open, table, row, onOpenChange, onSaved }) {
|
|
1766
|
+
const tr = useTr();
|
|
1767
|
+
const { insertDataTableRow, loading: inserting } = useInsertDataTableRow();
|
|
1768
|
+
const { updateDataTableRow, loading: updating } = useUpdateDataTableRow();
|
|
1769
|
+
const [values, setValues] = useState({});
|
|
1770
|
+
const editing = row !== null;
|
|
1771
|
+
const saving = inserting || updating;
|
|
1772
|
+
useEffect(() => {
|
|
1773
|
+
if (!open || !table) return;
|
|
1774
|
+
const next = {};
|
|
1775
|
+
for (const column of table.columns) next[column.key] = asFormValue(row?.data[column.key], column.type);
|
|
1776
|
+
setValues(next);
|
|
1777
|
+
}, [
|
|
1778
|
+
open,
|
|
1779
|
+
row,
|
|
1780
|
+
table
|
|
1781
|
+
]);
|
|
1782
|
+
const handleSubmit = useCallback(async () => {
|
|
1783
|
+
if (!table) return;
|
|
1784
|
+
try {
|
|
1785
|
+
const data = {};
|
|
1786
|
+
for (const column of table.columns) {
|
|
1787
|
+
const value = values[column.key] ?? "";
|
|
1788
|
+
if (column.required && value.trim() === "") throw new Error(`${column.label} is required`);
|
|
1789
|
+
data[column.key] = parseFormValue(value, column.type);
|
|
1790
|
+
}
|
|
1791
|
+
if (editing && row) await updateDataTableRow(row.id, data);
|
|
1792
|
+
else await insertDataTableRow(table.id, data);
|
|
1793
|
+
toast.success(editing ? tr("fluidgrids.dataTables.rowUpdated", "Row updated") : tr("fluidgrids.dataTables.rowCreated", "Row added"));
|
|
1794
|
+
onSaved();
|
|
1795
|
+
onOpenChange(false);
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
toast.error(describeOperationError(error, tr("fluidgrids.dataTables.rowSaveFailed", "Failed to save row")));
|
|
1798
|
+
}
|
|
1799
|
+
}, [
|
|
1800
|
+
editing,
|
|
1801
|
+
insertDataTableRow,
|
|
1802
|
+
onOpenChange,
|
|
1803
|
+
onSaved,
|
|
1804
|
+
row,
|
|
1805
|
+
table,
|
|
1806
|
+
tr,
|
|
1807
|
+
updateDataTableRow,
|
|
1808
|
+
values
|
|
1809
|
+
]);
|
|
1810
|
+
return /* @__PURE__ */ jsx(Dialog, {
|
|
1811
|
+
open,
|
|
1812
|
+
onOpenChange,
|
|
1813
|
+
children: /* @__PURE__ */ jsxs(DialogContent, {
|
|
1814
|
+
className: "max-h-[90vh] max-w-2xl overflow-y-auto",
|
|
1815
|
+
children: [
|
|
1816
|
+
/* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: editing ? tr("fluidgrids.dataTables.editRow", "Edit row") : tr("fluidgrids.dataTables.addRow", "Add row") }) }),
|
|
1817
|
+
table && /* @__PURE__ */ jsx("div", {
|
|
1818
|
+
className: "space-y-4 py-2",
|
|
1819
|
+
children: table.columns.map((column) => /* @__PURE__ */ jsxs("div", {
|
|
1820
|
+
className: "space-y-1.5",
|
|
1821
|
+
children: [/* @__PURE__ */ jsxs(Label, {
|
|
1822
|
+
htmlFor: `data-table-row-${column.key}`,
|
|
1823
|
+
children: [
|
|
1824
|
+
column.label,
|
|
1825
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1826
|
+
className: "ml-1 text-xs text-text-secondary",
|
|
1827
|
+
children: [
|
|
1828
|
+
"(",
|
|
1829
|
+
column.key,
|
|
1830
|
+
" · ",
|
|
1831
|
+
columnTypeLabel(column.type),
|
|
1832
|
+
")"
|
|
1833
|
+
]
|
|
1834
|
+
}),
|
|
1835
|
+
column.required && /* @__PURE__ */ jsx("span", {
|
|
1836
|
+
className: "ml-1 text-text-secondary",
|
|
1837
|
+
children: "*"
|
|
1838
|
+
})
|
|
1839
|
+
]
|
|
1840
|
+
}), column.type === "JSON" ? /* @__PURE__ */ jsx(Textarea, {
|
|
1841
|
+
id: `data-table-row-${column.key}`,
|
|
1842
|
+
value: values[column.key] ?? "",
|
|
1843
|
+
onChange: (event) => setValues((current) => ({
|
|
1844
|
+
...current,
|
|
1845
|
+
[column.key]: event.target.value
|
|
1846
|
+
})),
|
|
1847
|
+
rows: 4,
|
|
1848
|
+
placeholder: "{ }"
|
|
1849
|
+
}) : /* @__PURE__ */ jsx(Input, {
|
|
1850
|
+
id: `data-table-row-${column.key}`,
|
|
1851
|
+
type: column.type === "NUMBER" ? "number" : column.type === "DATE" ? "datetime-local" : "text",
|
|
1852
|
+
value: values[column.key] ?? "",
|
|
1853
|
+
onChange: (event) => setValues((current) => ({
|
|
1854
|
+
...current,
|
|
1855
|
+
[column.key]: event.target.value
|
|
1856
|
+
}))
|
|
1857
|
+
})]
|
|
1858
|
+
}, column.key))
|
|
1859
|
+
}),
|
|
1860
|
+
/* @__PURE__ */ jsxs(DialogFooter, { children: [/* @__PURE__ */ jsx(Button$1, {
|
|
1861
|
+
variant: "outline",
|
|
1862
|
+
onClick: () => onOpenChange(false),
|
|
1863
|
+
children: tr("fluidgrids.dataTables.cancel", "Cancel")
|
|
1864
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
1865
|
+
onClick: handleSubmit,
|
|
1866
|
+
disabled: saving || !table,
|
|
1867
|
+
children: saving ? tr("fluidgrids.dataTables.saving", "Saving…") : tr("fluidgrids.dataTables.saveRow", "Save row")
|
|
1868
|
+
})] })
|
|
1869
|
+
]
|
|
1870
|
+
})
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
function TableRowsPanel({ table, onTableChanged, onEditTable, onDeleteTable, deletingTable, onTagsChanged }) {
|
|
1874
|
+
const tr = useTr();
|
|
1875
|
+
const v2 = useNavigationV2();
|
|
1876
|
+
const { allowed: canTag } = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.WORKFLOW_UPDATE);
|
|
1877
|
+
const canEditRows = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_UPDATE).allowed;
|
|
1878
|
+
const canDeleteRows = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_DELETE).allowed;
|
|
1879
|
+
const [page, setPage] = useState(0);
|
|
1880
|
+
const [pageSize, setPageSize] = useState(DEFAULT_ROW_PAGE_SIZE);
|
|
1881
|
+
const { rows, total, loading, error, refetch } = useDataTableRows(table.id, {
|
|
1882
|
+
page,
|
|
1883
|
+
pageSize
|
|
1884
|
+
});
|
|
1885
|
+
const { deleteDataTableRows, loading: deleting } = useDeleteDataTableRows();
|
|
1886
|
+
const { insertDataTableRow } = useInsertDataTableRow();
|
|
1887
|
+
const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
1888
|
+
const [rowDialogOpen, setRowDialogOpen] = useState(false);
|
|
1889
|
+
const [editingRow, setEditingRow] = useState(null);
|
|
1890
|
+
const [exporting, setExporting] = useState(false);
|
|
1891
|
+
const [selectMode, setSelectMode] = useState(false);
|
|
1892
|
+
const [showRowIds, setShowRowIds] = useState(false);
|
|
1893
|
+
const [rowSearch, setRowSearch] = useState("");
|
|
1894
|
+
const [sort, setSort] = useState(null);
|
|
1895
|
+
const [pendingRowDelete, setPendingRowDelete] = useState(null);
|
|
1896
|
+
const [importProgress, setImportProgress] = useState(null);
|
|
1897
|
+
const fileInputRef = useRef(null);
|
|
1898
|
+
const pages = pageCount(total, pageSize);
|
|
1899
|
+
const range = pageRange(page, pageSize, rows.length);
|
|
1900
|
+
useEffect(() => {
|
|
1901
|
+
if (loading) return;
|
|
1902
|
+
const clamped = clampPage(page, total, pageSize);
|
|
1903
|
+
if (clamped !== page) setPage(clamped);
|
|
1904
|
+
}, [
|
|
1905
|
+
loading,
|
|
1906
|
+
page,
|
|
1907
|
+
pageSize,
|
|
1908
|
+
total
|
|
1909
|
+
]);
|
|
1910
|
+
useEffect(() => {
|
|
1911
|
+
setSelectedIds((current) => {
|
|
1912
|
+
const visible = new Set(rows.map((row) => row.id));
|
|
1913
|
+
return new Set([...current].filter((id) => visible.has(id)));
|
|
1914
|
+
});
|
|
1915
|
+
}, [rows]);
|
|
1916
|
+
const allVisibleSelected = rows.length > 0 && selectedIds.size === rows.length;
|
|
1917
|
+
const toggleAll = useCallback(() => {
|
|
1918
|
+
setSelectedIds(allVisibleSelected ? /* @__PURE__ */ new Set() : new Set(rows.map((row) => row.id)));
|
|
1919
|
+
}, [allVisibleSelected, rows]);
|
|
1920
|
+
const performDeleteRows = useCallback(async (ids) => {
|
|
1921
|
+
try {
|
|
1922
|
+
const affected = await deleteDataTableRows(table.id, ids);
|
|
1923
|
+
toast.success(tr("fluidgrids.dataTables.rowsDeleted", `${affected} row(s) deleted`));
|
|
1924
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
1925
|
+
await refetch();
|
|
1926
|
+
onTableChanged();
|
|
1927
|
+
} catch (error) {
|
|
1928
|
+
toast.error(describeOperationError(error, tr("fluidgrids.dataTables.deleteRowsFailed", "Failed to delete rows")));
|
|
1929
|
+
}
|
|
1930
|
+
}, [
|
|
1931
|
+
deleteDataTableRows,
|
|
1932
|
+
onTableChanged,
|
|
1933
|
+
refetch,
|
|
1934
|
+
table.id,
|
|
1935
|
+
tr
|
|
1936
|
+
]);
|
|
1937
|
+
const handleDeleteRows = useCallback(async () => {
|
|
1938
|
+
if (selectedIds.size === 0) return;
|
|
1939
|
+
if (v2) {
|
|
1940
|
+
setPendingRowDelete([...selectedIds]);
|
|
1941
|
+
return;
|
|
1942
|
+
}
|
|
1943
|
+
if (!window.confirm(tr("fluidgrids.dataTables.confirmDeleteRows", `Delete ${selectedIds.size} selected row(s)?`))) return;
|
|
1944
|
+
await performDeleteRows([...selectedIds]);
|
|
1945
|
+
}, [
|
|
1946
|
+
performDeleteRows,
|
|
1947
|
+
selectedIds,
|
|
1948
|
+
tr,
|
|
1949
|
+
v2
|
|
1950
|
+
]);
|
|
1951
|
+
const confirmRowDelete = useCallback(async () => {
|
|
1952
|
+
const ids = pendingRowDelete;
|
|
1953
|
+
if (!ids) return;
|
|
1954
|
+
await performDeleteRows(ids);
|
|
1955
|
+
setPendingRowDelete(null);
|
|
1956
|
+
}, [pendingRowDelete, performDeleteRows]);
|
|
1957
|
+
const handleExport = useCallback(async () => {
|
|
1958
|
+
setExporting(true);
|
|
1959
|
+
try {
|
|
1960
|
+
const allRows = await fetchAllDataTableRows(table.id);
|
|
1961
|
+
const csv = rowsToCsv(table.columns, allRows);
|
|
1962
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
|
|
1963
|
+
const url = URL.createObjectURL(blob);
|
|
1964
|
+
const anchor = document.createElement("a");
|
|
1965
|
+
anchor.href = url;
|
|
1966
|
+
anchor.download = `${table.name}.csv`;
|
|
1967
|
+
anchor.click();
|
|
1968
|
+
URL.revokeObjectURL(url);
|
|
1969
|
+
toast.success(tr("fluidgrids.dataTables.exported", `${allRows.length} row(s) exported to CSV`));
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
toast.error(describeOperationError(error, tr("fluidgrids.dataTables.exportFailed", "Failed to export CSV")));
|
|
1972
|
+
} finally {
|
|
1973
|
+
setExporting(false);
|
|
1974
|
+
}
|
|
1975
|
+
}, [
|
|
1976
|
+
table.columns,
|
|
1977
|
+
table.id,
|
|
1978
|
+
table.name,
|
|
1979
|
+
tr
|
|
1980
|
+
]);
|
|
1981
|
+
const handleImport = useCallback(async (event) => {
|
|
1982
|
+
const file = event.target.files?.[0];
|
|
1983
|
+
event.target.value = "";
|
|
1984
|
+
if (!file) return;
|
|
1985
|
+
let inserted = 0;
|
|
1986
|
+
const insertedIds = [];
|
|
1987
|
+
try {
|
|
1988
|
+
const importedRows = csvToRows(await file.text(), table.columns);
|
|
1989
|
+
setImportProgress({
|
|
1990
|
+
done: 0,
|
|
1991
|
+
total: importedRows.length
|
|
1992
|
+
});
|
|
1993
|
+
for (const data of importedRows) {
|
|
1994
|
+
const added = await insertDataTableRow(table.id, data);
|
|
1995
|
+
if (added) insertedIds.push(added.id);
|
|
1996
|
+
inserted += 1;
|
|
1997
|
+
setImportProgress({
|
|
1998
|
+
done: inserted,
|
|
1999
|
+
total: importedRows.length
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
toast.success(tr("fluidgrids.dataTables.imported", `${inserted} row(s) imported`));
|
|
2003
|
+
} catch (error) {
|
|
2004
|
+
const reason = describeOperationError(error, tr("fluidgrids.dataTables.importFailed", "Failed to import CSV"));
|
|
2005
|
+
const undone = inserted > 0 && insertedIds.length === inserted ? await deleteDataTableRows(table.id, insertedIds).then(() => true, () => false) : false;
|
|
2006
|
+
toast.error(inserted === 0 ? reason : undone ? tr("fluidgrids.dataTables.importRolledBack", "Import failed, so none of its rows were added: {{reason}}", { reason }) : tr("fluidgrids.dataTables.importPartial", `Import stopped after ${inserted} row(s): ${reason}`));
|
|
2007
|
+
} finally {
|
|
2008
|
+
setImportProgress(null);
|
|
2009
|
+
setPage(0);
|
|
2010
|
+
await refetch();
|
|
2011
|
+
onTableChanged();
|
|
2012
|
+
}
|
|
2013
|
+
}, [
|
|
2014
|
+
deleteDataTableRows,
|
|
2015
|
+
insertDataTableRow,
|
|
2016
|
+
onTableChanged,
|
|
2017
|
+
refetch,
|
|
2018
|
+
table.columns,
|
|
2019
|
+
table.id,
|
|
2020
|
+
tr
|
|
2021
|
+
]);
|
|
2022
|
+
const bodyColumnCount = table.columns.length + 1 + Number(!v2 || selectMode) + Number(!v2 || showRowIds);
|
|
2023
|
+
const yesNo = {
|
|
2024
|
+
yes: tr("fluidgrids.dataTables.yes", "Yes"),
|
|
2025
|
+
no: tr("fluidgrids.dataTables.no", "No")
|
|
2026
|
+
};
|
|
2027
|
+
const displayRows = useMemo(() => {
|
|
2028
|
+
if (!v2) return rows;
|
|
2029
|
+
const needle = rowSearch.trim().toLowerCase();
|
|
2030
|
+
const matching = needle ? rows.filter((row) => table.columns.some((column) => formatValue(row.data[column.key]).toLowerCase().includes(needle))) : rows;
|
|
2031
|
+
const sortColumn = sort ? table.columns.find((column) => column.key === sort.key) : void 0;
|
|
2032
|
+
if (!sort || !sortColumn) return matching;
|
|
2033
|
+
return [...matching].sort((a, b) => compareCells(a.data[sortColumn.key], b.data[sortColumn.key], sortColumn.type, sort.direction));
|
|
2034
|
+
}, [
|
|
2035
|
+
rowSearch,
|
|
2036
|
+
rows,
|
|
2037
|
+
sort,
|
|
2038
|
+
table.columns,
|
|
2039
|
+
v2
|
|
2040
|
+
]);
|
|
2041
|
+
const toggleSort = useCallback((key) => {
|
|
2042
|
+
setSort((current) => {
|
|
2043
|
+
if (!current || current.key !== key) return {
|
|
2044
|
+
key,
|
|
2045
|
+
direction: 1
|
|
2046
|
+
};
|
|
2047
|
+
return current.direction === 1 ? {
|
|
2048
|
+
key,
|
|
2049
|
+
direction: -1
|
|
2050
|
+
} : null;
|
|
2051
|
+
});
|
|
2052
|
+
}, []);
|
|
2053
|
+
const openNewRow = useCallback(() => {
|
|
2054
|
+
setEditingRow(null);
|
|
2055
|
+
setRowDialogOpen(true);
|
|
2056
|
+
}, []);
|
|
2057
|
+
const openEditRow = useCallback((row) => {
|
|
2058
|
+
setEditingRow(row);
|
|
2059
|
+
setRowDialogOpen(true);
|
|
2060
|
+
}, []);
|
|
2061
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
2062
|
+
className: "min-w-0 border-border-secondary",
|
|
2063
|
+
children: [
|
|
2064
|
+
v2 && /* @__PURE__ */ jsx("input", {
|
|
2065
|
+
ref: fileInputRef,
|
|
2066
|
+
type: "file",
|
|
2067
|
+
accept: ".csv,text/csv",
|
|
2068
|
+
className: "hidden",
|
|
2069
|
+
"aria-label": tr("fluidgrids.dataTables.import", "Import from spreadsheet (CSV)"),
|
|
2070
|
+
onChange: handleImport
|
|
2071
|
+
}),
|
|
2072
|
+
v2 ? /* @__PURE__ */ jsx(RowsPanelHeaderV2, {
|
|
2073
|
+
table,
|
|
2074
|
+
total,
|
|
2075
|
+
importProgress,
|
|
2076
|
+
exporting,
|
|
2077
|
+
deletingRows: deleting,
|
|
2078
|
+
deletingTable,
|
|
2079
|
+
selectMode,
|
|
2080
|
+
onSelectModeChange: (on) => {
|
|
2081
|
+
setSelectMode(on);
|
|
2082
|
+
if (!on) setSelectedIds(/* @__PURE__ */ new Set());
|
|
2083
|
+
},
|
|
2084
|
+
selectedCount: selectedIds.size,
|
|
2085
|
+
showRowIds,
|
|
2086
|
+
onShowRowIdsChange: setShowRowIds,
|
|
2087
|
+
pageSize,
|
|
2088
|
+
onPageSizeChange: (size) => {
|
|
2089
|
+
setPageSize(size);
|
|
2090
|
+
setPage(0);
|
|
2091
|
+
},
|
|
2092
|
+
onAddRow: openNewRow,
|
|
2093
|
+
onEditTable,
|
|
2094
|
+
onDeleteTable,
|
|
2095
|
+
onImport: () => fileInputRef.current?.click(),
|
|
2096
|
+
onExport: handleExport,
|
|
2097
|
+
onDeleteSelected: handleDeleteRows,
|
|
2098
|
+
onTagsChanged
|
|
2099
|
+
}) : /* @__PURE__ */ jsxs(CardHeader, {
|
|
2100
|
+
className: "gap-4 border-b border-border-secondary p-4 sm:p-5",
|
|
2101
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2102
|
+
className: "flex flex-wrap items-start justify-between gap-3",
|
|
2103
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2104
|
+
className: "min-w-0 flex-1 basis-64",
|
|
2105
|
+
children: [
|
|
2106
|
+
/* @__PURE__ */ jsxs(CardTitle, {
|
|
2107
|
+
className: "flex items-center gap-2 text-lg",
|
|
2108
|
+
children: [/* @__PURE__ */ jsx(Table2, {
|
|
2109
|
+
className: "size-5 shrink-0 text-text-secondary",
|
|
2110
|
+
"aria-hidden": "true"
|
|
2111
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2112
|
+
className: "truncate",
|
|
2113
|
+
children: table.name
|
|
2114
|
+
})]
|
|
2115
|
+
}),
|
|
2116
|
+
/* @__PURE__ */ jsx(CardDescription, {
|
|
2117
|
+
className: "mt-1",
|
|
2118
|
+
children: table.description || tr("fluidgrids.dataTables.noDescription", "No description")
|
|
2119
|
+
}),
|
|
2120
|
+
/* @__PURE__ */ jsx("div", {
|
|
2121
|
+
className: "mt-3",
|
|
2122
|
+
"data-testid": "data-table-tags",
|
|
2123
|
+
children: /* @__PURE__ */ jsx(EntityTagPicker, {
|
|
2124
|
+
entityType: "FLUIDGRIDS_DATA_TABLE",
|
|
2125
|
+
entityId: table.id,
|
|
2126
|
+
readOnly: !canTag,
|
|
2127
|
+
onChange: onTagsChanged
|
|
2128
|
+
})
|
|
2129
|
+
})
|
|
2130
|
+
]
|
|
2131
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2132
|
+
className: "flex shrink-0 items-center gap-2",
|
|
2133
|
+
children: [/* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
2134
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_UPDATE,
|
|
2135
|
+
deniedTooltip: tr("fluidgrids.dataTables.editDeniedTooltip", "You don't have permission to edit data tables"),
|
|
2136
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
2137
|
+
variant: "outline",
|
|
2138
|
+
size: "sm",
|
|
2139
|
+
className: "whitespace-nowrap",
|
|
2140
|
+
onClick: onEditTable,
|
|
2141
|
+
children: [/* @__PURE__ */ jsx(Pencil, {
|
|
2142
|
+
className: "mr-1.5 size-4",
|
|
2143
|
+
"aria-hidden": "true"
|
|
2144
|
+
}), tr("fluidgrids.dataTables.editTable", "Edit table")]
|
|
2145
|
+
})
|
|
2146
|
+
}), /* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
2147
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_DELETE,
|
|
2148
|
+
deniedTooltip: tr("fluidgrids.dataTables.deleteDeniedTooltip", "You don't have permission to delete data tables"),
|
|
2149
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
2150
|
+
variant: "outline",
|
|
2151
|
+
size: "sm",
|
|
2152
|
+
className: "whitespace-nowrap",
|
|
2153
|
+
onClick: onDeleteTable,
|
|
2154
|
+
disabled: deletingTable,
|
|
2155
|
+
children: [/* @__PURE__ */ jsx(Trash2, {
|
|
2156
|
+
className: "mr-1.5 size-4",
|
|
2157
|
+
"aria-hidden": "true"
|
|
2158
|
+
}), deletingTable ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteTable", "Delete table")]
|
|
2159
|
+
})
|
|
2160
|
+
})]
|
|
2161
|
+
})]
|
|
2162
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2163
|
+
className: "flex flex-wrap items-center justify-between gap-3",
|
|
2164
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2165
|
+
className: "flex flex-wrap items-center gap-2 text-xs text-text-secondary",
|
|
2166
|
+
children: [
|
|
2167
|
+
/* @__PURE__ */ jsxs(Badge, {
|
|
2168
|
+
variant: "outline",
|
|
2169
|
+
children: [
|
|
2170
|
+
total,
|
|
2171
|
+
" ",
|
|
2172
|
+
tr("fluidgrids.dataTables.rows", "rows")
|
|
2173
|
+
]
|
|
2174
|
+
}),
|
|
2175
|
+
/* @__PURE__ */ jsxs(Badge, {
|
|
2176
|
+
variant: "outline",
|
|
2177
|
+
children: [
|
|
2178
|
+
table.columns.length,
|
|
2179
|
+
" ",
|
|
2180
|
+
tr("fluidgrids.dataTables.columns", "columns")
|
|
2181
|
+
]
|
|
2182
|
+
}),
|
|
2183
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
2184
|
+
variant: "outline",
|
|
2185
|
+
children: formatBytes(table.sizeBytes)
|
|
2186
|
+
}),
|
|
2187
|
+
selectedIds.size > 0 && /* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
2188
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_DELETE,
|
|
2189
|
+
deniedTooltip: tr("fluidgrids.dataTables.deleteRowsDeniedTooltip", "You don't have permission to delete rows"),
|
|
2190
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
2191
|
+
variant: "outline",
|
|
2192
|
+
size: "sm",
|
|
2193
|
+
className: "whitespace-nowrap",
|
|
2194
|
+
onClick: handleDeleteRows,
|
|
2195
|
+
disabled: deleting,
|
|
2196
|
+
children: [/* @__PURE__ */ jsx(Trash2, {
|
|
2197
|
+
className: "mr-1.5 size-4",
|
|
2198
|
+
"aria-hidden": "true"
|
|
2199
|
+
}), deleting ? tr("fluidgrids.dataTables.deleting", "Deleting…") : tr("fluidgrids.dataTables.deleteSelected", `Delete ${selectedIds.size} selected`)]
|
|
2200
|
+
})
|
|
2201
|
+
})
|
|
2202
|
+
]
|
|
2203
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2204
|
+
className: "flex flex-wrap items-center gap-2",
|
|
2205
|
+
children: [
|
|
2206
|
+
/* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
2207
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE,
|
|
2208
|
+
deniedTooltip: tr("fluidgrids.dataTables.addRowDeniedTooltip", "You don't have permission to add rows"),
|
|
2209
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
2210
|
+
size: "sm",
|
|
2211
|
+
className: "whitespace-nowrap",
|
|
2212
|
+
onClick: openNewRow,
|
|
2213
|
+
disabled: importProgress !== null,
|
|
2214
|
+
children: [/* @__PURE__ */ jsx(Plus, {
|
|
2215
|
+
className: "mr-1.5 size-4",
|
|
2216
|
+
"aria-hidden": "true"
|
|
2217
|
+
}), tr("fluidgrids.dataTables.addRow", "Add row")]
|
|
2218
|
+
})
|
|
2219
|
+
}),
|
|
2220
|
+
/* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
2221
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_CREATE,
|
|
2222
|
+
deniedTooltip: tr("fluidgrids.dataTables.importDeniedTooltip", "You don't have permission to import rows"),
|
|
2223
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
2224
|
+
variant: "outline",
|
|
2225
|
+
size: "sm",
|
|
2226
|
+
className: "whitespace-nowrap",
|
|
2227
|
+
onClick: () => fileInputRef.current?.click(),
|
|
2228
|
+
disabled: importProgress !== null,
|
|
2229
|
+
children: [/* @__PURE__ */ jsx(ArrowUpFromLine, {
|
|
2230
|
+
className: "mr-1.5 size-4",
|
|
2231
|
+
"aria-hidden": "true"
|
|
2232
|
+
}), importProgress ? tr("fluidgrids.dataTables.importing", `Importing ${importProgress.done} / ${importProgress.total}…`) : tr("fluidgrids.dataTables.import", "Import CSV")]
|
|
2233
|
+
})
|
|
2234
|
+
}),
|
|
2235
|
+
/* @__PURE__ */ jsx("input", {
|
|
2236
|
+
ref: fileInputRef,
|
|
2237
|
+
type: "file",
|
|
2238
|
+
accept: ".csv,text/csv",
|
|
2239
|
+
className: "hidden",
|
|
2240
|
+
"aria-label": tr("fluidgrids.dataTables.import", "Import CSV"),
|
|
2241
|
+
onChange: handleImport
|
|
2242
|
+
}),
|
|
2243
|
+
/* @__PURE__ */ jsxs(Button$1, {
|
|
2244
|
+
variant: "outline",
|
|
2245
|
+
size: "sm",
|
|
2246
|
+
className: "whitespace-nowrap",
|
|
2247
|
+
onClick: handleExport,
|
|
2248
|
+
disabled: exporting,
|
|
2249
|
+
children: [/* @__PURE__ */ jsx(ArrowDownToLine, {
|
|
2250
|
+
className: "mr-1.5 size-4",
|
|
2251
|
+
"aria-hidden": "true"
|
|
2252
|
+
}), exporting ? tr("fluidgrids.dataTables.exporting", "Exporting…") : tr("fluidgrids.dataTables.export", "Export CSV")]
|
|
2253
|
+
})
|
|
2254
|
+
]
|
|
2255
|
+
})]
|
|
2256
|
+
})]
|
|
2257
|
+
}),
|
|
2258
|
+
/* @__PURE__ */ jsxs(CardContent, {
|
|
2259
|
+
className: "p-0",
|
|
2260
|
+
children: [
|
|
2261
|
+
error && /* @__PURE__ */ jsx("div", {
|
|
2262
|
+
className: "m-4 rounded-md border border-border-secondary bg-background-secondary p-3 text-sm text-text-secondary",
|
|
2263
|
+
children: v2 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", { children: tr("fluidgrids.dataTables.rowsLoadFailed", "We couldn't load the rows. Try again in a moment.") }), /* @__PURE__ */ jsx(TechnicalDetails, {
|
|
2264
|
+
className: "mt-2",
|
|
2265
|
+
children: /* @__PURE__ */ jsx("code", {
|
|
2266
|
+
className: "break-words text-xs",
|
|
2267
|
+
children: error.message
|
|
2268
|
+
})
|
|
2269
|
+
})] }) : error.message
|
|
2270
|
+
}),
|
|
2271
|
+
v2 && (rows.length > 0 || rowSearch !== "") && /* @__PURE__ */ jsxs("div", {
|
|
2272
|
+
className: "relative mx-4 mt-4 max-w-sm",
|
|
2273
|
+
children: [/* @__PURE__ */ jsx(Search, {
|
|
2274
|
+
className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-text-secondary",
|
|
2275
|
+
"aria-hidden": "true"
|
|
2276
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
2277
|
+
className: "pl-8",
|
|
2278
|
+
value: rowSearch,
|
|
2279
|
+
onChange: (event) => setRowSearch(event.target.value),
|
|
2280
|
+
placeholder: tr("fluidgrids.dataTables.searchRows", "Search this page of rows"),
|
|
2281
|
+
"aria-label": tr("fluidgrids.dataTables.searchRows", "Search this page of rows"),
|
|
2282
|
+
"data-testid": "row-search"
|
|
2283
|
+
})]
|
|
2284
|
+
}),
|
|
2285
|
+
/* @__PURE__ */ jsx("div", {
|
|
2286
|
+
className: "overflow-x-auto",
|
|
2287
|
+
children: /* @__PURE__ */ jsxs(Table, { children: [/* @__PURE__ */ jsx(TableHeader, { children: /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
2288
|
+
(!v2 || selectMode) && /* @__PURE__ */ jsx(TableHead, {
|
|
2289
|
+
className: "w-10",
|
|
2290
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
2291
|
+
type: "checkbox",
|
|
2292
|
+
checked: allVisibleSelected,
|
|
2293
|
+
onChange: toggleAll,
|
|
2294
|
+
"aria-label": tr("fluidgrids.dataTables.selectAll", "Select all rows on this page")
|
|
2295
|
+
})
|
|
2296
|
+
}),
|
|
2297
|
+
(!v2 || showRowIds) && /* @__PURE__ */ jsx(TableHead, { children: tr("fluidgrids.dataTables.rowId", "Row") }),
|
|
2298
|
+
table.columns.map((column) => /* @__PURE__ */ jsx(TableHead, {
|
|
2299
|
+
"aria-sort": v2 && sort?.key === column.key ? sort.direction === 1 ? "ascending" : "descending" : void 0,
|
|
2300
|
+
children: v2 ? /* @__PURE__ */ jsxs("button", {
|
|
2301
|
+
type: "button",
|
|
2302
|
+
className: "inline-flex items-center gap-1 font-medium hover:text-text-primary",
|
|
2303
|
+
onClick: () => toggleSort(column.key),
|
|
2304
|
+
"aria-label": tr("fluidgrids.dataTables.sortBy", "Sort by {{column}}", { column: column.label }),
|
|
2305
|
+
children: [column.label, sort?.key === column.key && /* @__PURE__ */ jsx("span", {
|
|
2306
|
+
"aria-hidden": "true",
|
|
2307
|
+
children: sort.direction === 1 ? "▲" : "▼"
|
|
2308
|
+
})]
|
|
2309
|
+
}) : column.label
|
|
2310
|
+
}, column.key)),
|
|
2311
|
+
/* @__PURE__ */ jsx(TableHead, {
|
|
2312
|
+
className: "text-right",
|
|
2313
|
+
children: tr("fluidgrids.dataTables.actions", "Actions")
|
|
2314
|
+
})
|
|
2315
|
+
] }) }), /* @__PURE__ */ jsxs(TableBody, { children: [
|
|
2316
|
+
loading && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
|
|
2317
|
+
colSpan: bodyColumnCount,
|
|
2318
|
+
className: "py-10 text-center text-text-secondary",
|
|
2319
|
+
children: tr("fluidgrids.dataTables.loadingRows", "Loading rows…")
|
|
2320
|
+
}) }),
|
|
2321
|
+
!loading && rows.length === 0 && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
|
|
2322
|
+
colSpan: bodyColumnCount,
|
|
2323
|
+
className: "py-10 text-center text-text-secondary",
|
|
2324
|
+
children: tr("fluidgrids.dataTables.emptyRows", v2 ? "No rows yet. Add a row or import a spreadsheet (CSV) to get started." : "No rows yet. Add a row or import a CSV to get started.")
|
|
2325
|
+
}) }),
|
|
2326
|
+
v2 && !loading && rows.length > 0 && displayRows.length === 0 && /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(TableCell, {
|
|
2327
|
+
colSpan: bodyColumnCount,
|
|
2328
|
+
className: "py-10 text-center text-text-secondary",
|
|
2329
|
+
children: tr("fluidgrids.dataTables.noRowsMatch", "No rows on this page match your search.")
|
|
2330
|
+
}) }),
|
|
2331
|
+
!loading && displayRows.map((row) => /* @__PURE__ */ jsxs(TableRow, { children: [
|
|
2332
|
+
(!v2 || selectMode) && /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("input", {
|
|
2333
|
+
type: "checkbox",
|
|
2334
|
+
checked: selectedIds.has(row.id),
|
|
2335
|
+
onChange: () => setSelectedIds((current) => {
|
|
2336
|
+
const next = new Set(current);
|
|
2337
|
+
if (next.has(row.id)) next.delete(row.id);
|
|
2338
|
+
else next.add(row.id);
|
|
2339
|
+
return next;
|
|
2340
|
+
}),
|
|
2341
|
+
"aria-label": tr("fluidgrids.dataTables.selectRow", "Select row")
|
|
2342
|
+
}) }),
|
|
2343
|
+
(!v2 || showRowIds) && /* @__PURE__ */ jsx(TableCell, { children: /* @__PURE__ */ jsx("code", {
|
|
2344
|
+
className: "text-xs text-text-secondary",
|
|
2345
|
+
children: row.id.slice(0, 8)
|
|
2346
|
+
}) }),
|
|
2347
|
+
table.columns.map((column) => {
|
|
2348
|
+
const text = v2 ? formatCell(row.data[column.key], column.type, yesNo) : formatValue(row.data[column.key]);
|
|
2349
|
+
return /* @__PURE__ */ jsx(TableCell, {
|
|
2350
|
+
className: "max-w-[16rem] truncate text-sm",
|
|
2351
|
+
title: text,
|
|
2352
|
+
children: text
|
|
2353
|
+
}, column.key);
|
|
2354
|
+
}),
|
|
2355
|
+
/* @__PURE__ */ jsx(TableCell, {
|
|
2356
|
+
className: "text-right",
|
|
2357
|
+
children: v2 ? /* @__PURE__ */ jsx(RunActionsMenu, {
|
|
2358
|
+
label: tr("fluidgrids.dataTables.rowActions", "Row actions"),
|
|
2359
|
+
"data-testid": "row-actions-menu",
|
|
2360
|
+
items: [...canEditRows ? [{
|
|
2361
|
+
id: "edit",
|
|
2362
|
+
label: tr("fluidgrids.dataTables.editRow", "Edit row"),
|
|
2363
|
+
icon: "mdi:pencil-outline",
|
|
2364
|
+
onSelect: () => openEditRow(row)
|
|
2365
|
+
}] : [], ...canDeleteRows ? [{
|
|
2366
|
+
id: "delete",
|
|
2367
|
+
label: tr("fluidgrids.dataTables.deleteRow", "Delete row"),
|
|
2368
|
+
icon: "mdi:trash-can-outline",
|
|
2369
|
+
destructive: true,
|
|
2370
|
+
onSelect: () => setPendingRowDelete([row.id])
|
|
2371
|
+
}] : []]
|
|
2372
|
+
}) : /* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
2373
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_ROW_UPDATE,
|
|
2374
|
+
deniedTooltip: tr("fluidgrids.dataTables.editRowDeniedTooltip", "You don't have permission to edit rows"),
|
|
2375
|
+
children: /* @__PURE__ */ jsx(Button$1, {
|
|
2376
|
+
variant: "ghost",
|
|
2377
|
+
size: "icon",
|
|
2378
|
+
onClick: () => openEditRow(row),
|
|
2379
|
+
title: tr("fluidgrids.dataTables.editRow", "Edit row"),
|
|
2380
|
+
"aria-label": tr("fluidgrids.dataTables.editRow", "Edit row"),
|
|
2381
|
+
children: /* @__PURE__ */ jsx(Pencil, {
|
|
2382
|
+
className: "size-4",
|
|
2383
|
+
"aria-hidden": "true"
|
|
2384
|
+
})
|
|
2385
|
+
})
|
|
2386
|
+
})
|
|
2387
|
+
})
|
|
2388
|
+
] }, row.id))
|
|
2389
|
+
] })] })
|
|
2390
|
+
}),
|
|
2391
|
+
total > 0 && /* @__PURE__ */ jsxs("div", {
|
|
2392
|
+
className: "flex flex-wrap items-center justify-between gap-3 border-t border-border-secondary px-4 py-3 text-xs text-text-secondary",
|
|
2393
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2394
|
+
"aria-live": "polite",
|
|
2395
|
+
children: tr("fluidgrids.dataTables.showingRange", `Showing ${range.from}–${range.to} of ${total}`)
|
|
2396
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2397
|
+
className: "flex flex-wrap items-center gap-3",
|
|
2398
|
+
children: [
|
|
2399
|
+
!v2 && /* @__PURE__ */ jsxs("label", {
|
|
2400
|
+
className: "flex items-center gap-2",
|
|
2401
|
+
children: [/* @__PURE__ */ jsx("span", { children: tr("fluidgrids.dataTables.rowsPerPage", "Rows per page") }), /* @__PURE__ */ jsx("select", {
|
|
2402
|
+
className: "h-8 rounded-md border border-input bg-background px-2 text-xs text-text-primary",
|
|
2403
|
+
value: pageSize,
|
|
2404
|
+
onChange: (event) => {
|
|
2405
|
+
setPageSize(Number(event.target.value));
|
|
2406
|
+
setPage(0);
|
|
2407
|
+
},
|
|
2408
|
+
children: ROW_PAGE_SIZES.map((size) => /* @__PURE__ */ jsx("option", {
|
|
2409
|
+
value: size,
|
|
2410
|
+
children: size
|
|
2411
|
+
}, size))
|
|
2412
|
+
})]
|
|
2413
|
+
}),
|
|
2414
|
+
/* @__PURE__ */ jsx("span", { children: tr("fluidgrids.dataTables.pageOf", `Page ${page + 1} of ${pages}`) }),
|
|
2415
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2416
|
+
className: "flex items-center gap-1",
|
|
2417
|
+
children: [/* @__PURE__ */ jsx(Button$1, {
|
|
2418
|
+
variant: "outline",
|
|
2419
|
+
size: "icon",
|
|
2420
|
+
className: "size-8",
|
|
2421
|
+
onClick: () => setPage((current) => Math.max(current - 1, 0)),
|
|
2422
|
+
disabled: page === 0 || loading,
|
|
2423
|
+
title: tr("fluidgrids.dataTables.prevPage", "Previous page"),
|
|
2424
|
+
"aria-label": tr("fluidgrids.dataTables.prevPage", "Previous page"),
|
|
2425
|
+
children: /* @__PURE__ */ jsx(ChevronLeft, {
|
|
2426
|
+
className: "size-4",
|
|
2427
|
+
"aria-hidden": "true"
|
|
2428
|
+
})
|
|
2429
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
2430
|
+
variant: "outline",
|
|
2431
|
+
size: "icon",
|
|
2432
|
+
className: "size-8",
|
|
2433
|
+
onClick: () => setPage((current) => Math.min(current + 1, pages - 1)),
|
|
2434
|
+
disabled: page >= pages - 1 || loading,
|
|
2435
|
+
title: tr("fluidgrids.dataTables.nextPage", "Next page"),
|
|
2436
|
+
"aria-label": tr("fluidgrids.dataTables.nextPage", "Next page"),
|
|
2437
|
+
children: /* @__PURE__ */ jsx(ChevronRight, {
|
|
2438
|
+
className: "size-4",
|
|
2439
|
+
"aria-hidden": "true"
|
|
2440
|
+
})
|
|
2441
|
+
})]
|
|
2442
|
+
})
|
|
2443
|
+
]
|
|
2444
|
+
})]
|
|
2445
|
+
})
|
|
2446
|
+
]
|
|
2447
|
+
}),
|
|
2448
|
+
v2 ? /* @__PURE__ */ jsx(RowFormDialogV2, {
|
|
2449
|
+
open: rowDialogOpen,
|
|
2450
|
+
table,
|
|
2451
|
+
row: editingRow,
|
|
2452
|
+
onOpenChange: setRowDialogOpen,
|
|
2453
|
+
onSaved: async () => {
|
|
2454
|
+
await refetch();
|
|
2455
|
+
onTableChanged();
|
|
2456
|
+
}
|
|
2457
|
+
}) : /* @__PURE__ */ jsx(RowFormDialog, {
|
|
2458
|
+
open: rowDialogOpen,
|
|
2459
|
+
table,
|
|
2460
|
+
row: editingRow,
|
|
2461
|
+
onOpenChange: setRowDialogOpen,
|
|
2462
|
+
onSaved: async () => {
|
|
2463
|
+
await refetch();
|
|
2464
|
+
onTableChanged();
|
|
2465
|
+
}
|
|
2466
|
+
}),
|
|
2467
|
+
v2 && /* @__PURE__ */ jsx(ConfirmActionDialog, {
|
|
2468
|
+
open: pendingRowDelete !== null,
|
|
2469
|
+
onOpenChange: (open) => {
|
|
2470
|
+
if (!open) setPendingRowDelete(null);
|
|
2471
|
+
},
|
|
2472
|
+
title: (pendingRowDelete?.length ?? 0) === 1 ? tr("fluidgrids.dataTables.deleteRowTitle", "Delete this row?") : tr("fluidgrids.dataTables.deleteRowsTitle", "Delete {{count}} rows?", { count: pendingRowDelete?.length ?? 0 }),
|
|
2473
|
+
description: tr("fluidgrids.dataTables.deleteRowsBody", "This permanently removes it from \"{{table}}\". Workflows will no longer find it.", { table: table.name }),
|
|
2474
|
+
confirmLabel: tr("fluidgrids.dataTables.delete", "Delete"),
|
|
2475
|
+
cancelLabel: tr("fluidgrids.dataTables.keep", "Keep"),
|
|
2476
|
+
destructive: true,
|
|
2477
|
+
busy: deleting,
|
|
2478
|
+
onConfirm: () => void confirmRowDelete(),
|
|
2479
|
+
"data-testid": "confirm-delete-rows"
|
|
2480
|
+
})
|
|
2481
|
+
]
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
function DataTablesPage() {
|
|
2485
|
+
const tr = useTr();
|
|
2486
|
+
const v2 = useNavigationV2();
|
|
2487
|
+
const navigate = useNavigate();
|
|
2488
|
+
const createTablePermission = useFluidGridsPermission(FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE);
|
|
2489
|
+
const canCreateTable = createTablePermission.allowed;
|
|
2490
|
+
const [searchInput, setSearchInput] = useState("");
|
|
2491
|
+
const [search, setSearch] = useState("");
|
|
2492
|
+
const [tagFilter, setTagFilter] = useState("");
|
|
2493
|
+
const { tables, total, loading, error, refetch } = useDataTables(search, tagFilter);
|
|
2494
|
+
const { tagsById, refetch: refetchTags } = useDataTableTagOverlay();
|
|
2495
|
+
const { deleteDataTable, loading: deleting } = useDeleteDataTable();
|
|
2496
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
2497
|
+
const [tableDialogOpen, setTableDialogOpen] = useState(false);
|
|
2498
|
+
const [editingTable, setEditingTable] = useState(null);
|
|
2499
|
+
const [confirmingDeleteTable, setConfirmingDeleteTable] = useState(false);
|
|
2500
|
+
const newTableRequest = useActionParam(ACTION_PARAM_VALUES.new, createTablePermission);
|
|
2501
|
+
const openCreateTable = useCallback(() => {
|
|
2502
|
+
setEditingTable(null);
|
|
2503
|
+
setTableDialogOpen(true);
|
|
2504
|
+
}, []);
|
|
2505
|
+
useOpenRequest(newTableRequest.requested, openCreateTable, newTableRequest.handled);
|
|
2506
|
+
const selectedTable = useMemo(() => tables.find((table) => table.id === selectedId) ?? null, [selectedId, tables]);
|
|
2507
|
+
useEffect(() => {
|
|
2508
|
+
if (tables.length === 0) {
|
|
2509
|
+
setSelectedId(null);
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
if (!selectedId || !tables.some((table) => table.id === selectedId)) setSelectedId(tables[0].id);
|
|
2513
|
+
}, [selectedId, tables]);
|
|
2514
|
+
const refresh = useCallback(async () => {
|
|
2515
|
+
await refetch();
|
|
2516
|
+
}, [refetch]);
|
|
2517
|
+
const handleTagsChanged = useCallback(() => {
|
|
2518
|
+
refetchTags();
|
|
2519
|
+
if (tagFilter) refetch();
|
|
2520
|
+
}, [
|
|
2521
|
+
refetch,
|
|
2522
|
+
refetchTags,
|
|
2523
|
+
tagFilter
|
|
2524
|
+
]);
|
|
2525
|
+
const performDeleteTable = useCallback(async () => {
|
|
2526
|
+
if (!selectedTable) return;
|
|
2527
|
+
try {
|
|
2528
|
+
await deleteDataTable(selectedTable.id);
|
|
2529
|
+
toast.success(tr("fluidgrids.dataTables.deleted", "Data table deleted"));
|
|
2530
|
+
setSelectedId(null);
|
|
2531
|
+
await refetch();
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
toast.error(describeOperationError(error, tr("fluidgrids.dataTables.deleteFailed", "Failed to delete data table")));
|
|
2534
|
+
}
|
|
2535
|
+
}, [
|
|
2536
|
+
deleteDataTable,
|
|
2537
|
+
refetch,
|
|
2538
|
+
selectedTable,
|
|
2539
|
+
tr
|
|
2540
|
+
]);
|
|
2541
|
+
const handleDeleteTable = useCallback(async () => {
|
|
2542
|
+
if (!selectedTable) return;
|
|
2543
|
+
if (v2) {
|
|
2544
|
+
setConfirmingDeleteTable(true);
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
if (!window.confirm(tr("fluidgrids.dataTables.confirmDeleteTable", `Delete table "${selectedTable.name}" and all of its rows?`))) return;
|
|
2548
|
+
await performDeleteTable();
|
|
2549
|
+
}, [
|
|
2550
|
+
performDeleteTable,
|
|
2551
|
+
selectedTable,
|
|
2552
|
+
tr,
|
|
2553
|
+
v2
|
|
2554
|
+
]);
|
|
2555
|
+
const confirmDeleteTable = useCallback(async () => {
|
|
2556
|
+
await performDeleteTable();
|
|
2557
|
+
setConfirmingDeleteTable(false);
|
|
2558
|
+
}, [performDeleteTable]);
|
|
2559
|
+
const handleTableSaved = useCallback((table) => {
|
|
2560
|
+
if (table) setSelectedId(table.id);
|
|
2561
|
+
refetch();
|
|
2562
|
+
}, [refetch]);
|
|
2563
|
+
const submitSearch = useCallback((event) => {
|
|
2564
|
+
event.preventDefault();
|
|
2565
|
+
setSearch(searchInput);
|
|
2566
|
+
}, [searchInput]);
|
|
2567
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2568
|
+
className: "min-h-screen bg-background-primary p-4 pb-24 sm:p-6 sm:pb-24",
|
|
2569
|
+
children: [
|
|
2570
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2571
|
+
className: "mx-auto flex max-w-[1500px] flex-col gap-5",
|
|
2572
|
+
children: [
|
|
2573
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2574
|
+
className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
2575
|
+
"data-tour": "fluidgrids-data-tables-header",
|
|
2576
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [
|
|
2577
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2578
|
+
className: "mb-2 flex items-center gap-2 text-sm text-text-secondary",
|
|
2579
|
+
children: [/* @__PURE__ */ jsx(Database, {
|
|
2580
|
+
className: "size-4",
|
|
2581
|
+
"aria-hidden": "true"
|
|
2582
|
+
}), tr("fluidgrids.dataTables.workflowData", "Workflow data")]
|
|
2583
|
+
}),
|
|
2584
|
+
/* @__PURE__ */ jsx("h1", {
|
|
2585
|
+
className: "text-2xl font-bold text-text-primary sm:text-3xl",
|
|
2586
|
+
children: tr("fluidgrids.dataTables.pageTitle", v2 ? "Data tables" : "Data Tables")
|
|
2587
|
+
}),
|
|
2588
|
+
/* @__PURE__ */ jsx(PagePurpose, {
|
|
2589
|
+
icon: /* @__PURE__ */ jsx(Database, {
|
|
2590
|
+
className: "size-4",
|
|
2591
|
+
"aria-hidden": "true"
|
|
2592
|
+
}),
|
|
2593
|
+
children: tr("fluidgrids.dataTables.pageSubtitle", v2 ? "Keep a simple list your workflows can read and add to, like a small spreadsheet every run can use." : "Use Data Tables as lightweight persistent storage for workflow state, lookups, and records shared across runs.")
|
|
2594
|
+
}),
|
|
2595
|
+
/* @__PURE__ */ jsx(FluidGridsDocsLink, {
|
|
2596
|
+
topic: "dataTables",
|
|
2597
|
+
className: "mt-1"
|
|
2598
|
+
})
|
|
2599
|
+
] }), /* @__PURE__ */ jsxs("div", {
|
|
2600
|
+
className: "flex flex-wrap gap-2",
|
|
2601
|
+
children: [/* @__PURE__ */ jsxs(Button$1, {
|
|
2602
|
+
variant: "outline",
|
|
2603
|
+
onClick: () => navigate("../list"),
|
|
2604
|
+
children: [/* @__PURE__ */ jsx(ArrowLeft, {
|
|
2605
|
+
className: "mr-2 size-4",
|
|
2606
|
+
"aria-hidden": "true"
|
|
2607
|
+
}), tr("fluidgrids.dataTables.backToWorkflows", "Back to Workflows")]
|
|
2608
|
+
}), /* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
2609
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE,
|
|
2610
|
+
deniedTooltip: tr("fluidgrids.dataTables.createDeniedTooltip", "You don't have permission to create data tables"),
|
|
2611
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
2612
|
+
onClick: () => {
|
|
2613
|
+
setEditingTable(null);
|
|
2614
|
+
setTableDialogOpen(true);
|
|
2615
|
+
},
|
|
2616
|
+
"data-tour": "fluidgrids-data-tables-create",
|
|
2617
|
+
children: [/* @__PURE__ */ jsx(Plus, {
|
|
2618
|
+
className: "mr-2 size-4",
|
|
2619
|
+
"aria-hidden": "true"
|
|
2620
|
+
}), tr("fluidgrids.dataTables.newTable", v2 ? "New table" : "New data table")]
|
|
2621
|
+
})
|
|
2622
|
+
})]
|
|
2623
|
+
})]
|
|
2624
|
+
}),
|
|
2625
|
+
/* @__PURE__ */ jsx(NextSteps, {
|
|
2626
|
+
storageKey: "data-tables",
|
|
2627
|
+
title: tr("fluidgrids.dataTables.nextStepsTitle", v2 ? "How to use data tables" : "How to use Data Tables"),
|
|
2628
|
+
steps: [
|
|
2629
|
+
{
|
|
2630
|
+
id: "create",
|
|
2631
|
+
label: tr("fluidgrids.dataTables.nextStepCreateLabel", "Create a table"),
|
|
2632
|
+
description: tr("fluidgrids.dataTables.nextStepCreateDescription", v2 ? "Name the columns for the information your workflow needs." : "Define typed columns for the records your workflow needs."),
|
|
2633
|
+
icon: /* @__PURE__ */ jsx(Plus, {
|
|
2634
|
+
className: "size-4",
|
|
2635
|
+
"aria-hidden": "true"
|
|
2636
|
+
}),
|
|
2637
|
+
onClick: canCreateTable ? () => {
|
|
2638
|
+
setEditingTable(null);
|
|
2639
|
+
setTableDialogOpen(true);
|
|
2640
|
+
} : void 0
|
|
2641
|
+
},
|
|
2642
|
+
{
|
|
2643
|
+
id: "connect",
|
|
2644
|
+
label: tr("fluidgrids.dataTables.nextStepConnectLabel", "Connect it to a workflow"),
|
|
2645
|
+
description: tr("fluidgrids.dataTables.nextStepConnectDescription", v2 ? "Add a Table step to a workflow to read or save rows." : "Add a Data Table node to get, insert, update, upsert, or delete rows."),
|
|
2646
|
+
icon: /* @__PURE__ */ jsx(Table2, {
|
|
2647
|
+
className: "size-4",
|
|
2648
|
+
"aria-hidden": "true"
|
|
2649
|
+
}),
|
|
2650
|
+
onClick: () => navigate("../list")
|
|
2651
|
+
},
|
|
2652
|
+
{
|
|
2653
|
+
id: "reuse",
|
|
2654
|
+
label: tr("fluidgrids.dataTables.nextStepReuseLabel", "Reuse data across runs"),
|
|
2655
|
+
description: tr("fluidgrids.dataTables.nextStepReuseDescription", v2 ? "Remember what earlier runs did, for example which emails were already sent." : "Use saved status, cursors, lookups, and deduplication keys on later runs."),
|
|
2656
|
+
icon: /* @__PURE__ */ jsx(RefreshCw, {
|
|
2657
|
+
className: "size-4",
|
|
2658
|
+
"aria-hidden": "true"
|
|
2659
|
+
})
|
|
2660
|
+
}
|
|
2661
|
+
]
|
|
2662
|
+
}),
|
|
2663
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2664
|
+
className: "grid gap-5 xl:grid-cols-[minmax(16rem,22rem)_minmax(0,1fr)]",
|
|
2665
|
+
children: [
|
|
2666
|
+
/* @__PURE__ */ jsxs(Card, {
|
|
2667
|
+
className: "h-fit border-border-secondary",
|
|
2668
|
+
"data-tour": "fluidgrids-data-tables-list",
|
|
2669
|
+
children: [/* @__PURE__ */ jsxs(CardHeader, {
|
|
2670
|
+
className: "gap-3 border-b border-border-secondary p-4",
|
|
2671
|
+
children: [
|
|
2672
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2673
|
+
className: "flex items-center justify-between gap-2",
|
|
2674
|
+
children: [/* @__PURE__ */ jsx(CardTitle, {
|
|
2675
|
+
className: "text-base",
|
|
2676
|
+
children: tr("fluidgrids.dataTables.tables", "Tables")
|
|
2677
|
+
}), /* @__PURE__ */ jsx(Badge, {
|
|
2678
|
+
variant: "outline",
|
|
2679
|
+
children: total
|
|
2680
|
+
})]
|
|
2681
|
+
}),
|
|
2682
|
+
/* @__PURE__ */ jsxs("form", {
|
|
2683
|
+
className: "flex gap-2",
|
|
2684
|
+
onSubmit: submitSearch,
|
|
2685
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2686
|
+
className: "relative min-w-0 flex-1",
|
|
2687
|
+
children: [/* @__PURE__ */ jsx(Search, {
|
|
2688
|
+
className: "pointer-events-none absolute left-2.5 top-2.5 size-4 text-text-secondary",
|
|
2689
|
+
"aria-hidden": "true"
|
|
2690
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
2691
|
+
className: "pl-8",
|
|
2692
|
+
value: searchInput,
|
|
2693
|
+
onChange: (event) => setSearchInput(event.target.value),
|
|
2694
|
+
placeholder: tr("fluidgrids.dataTables.search", "Search tables"),
|
|
2695
|
+
"aria-label": tr("fluidgrids.dataTables.search", "Search tables")
|
|
2696
|
+
})]
|
|
2697
|
+
}), /* @__PURE__ */ jsx(Button$1, {
|
|
2698
|
+
type: "submit",
|
|
2699
|
+
variant: "outline",
|
|
2700
|
+
size: "sm",
|
|
2701
|
+
children: tr("fluidgrids.dataTables.searchAction", "Search")
|
|
2702
|
+
})]
|
|
2703
|
+
}),
|
|
2704
|
+
v2 ? /* @__PURE__ */ jsx(Disclosure, {
|
|
2705
|
+
closedLabel: tr("fluidgrids.dataTables.moreFilters", "More filters"),
|
|
2706
|
+
openLabel: tr("fluidgrids.dataTables.fewerFilters", "Fewer filters"),
|
|
2707
|
+
defaultOpen: tagFilter !== "",
|
|
2708
|
+
"data-testid": "table-more-filters",
|
|
2709
|
+
children: /* @__PURE__ */ jsx(TagFilterSelect, {
|
|
2710
|
+
value: tagFilter,
|
|
2711
|
+
onChange: setTagFilter,
|
|
2712
|
+
className: "w-full"
|
|
2713
|
+
})
|
|
2714
|
+
}) : /* @__PURE__ */ jsx(TagFilterSelect, {
|
|
2715
|
+
value: tagFilter,
|
|
2716
|
+
onChange: setTagFilter,
|
|
2717
|
+
className: "w-full"
|
|
2718
|
+
})
|
|
2719
|
+
]
|
|
2720
|
+
}), /* @__PURE__ */ jsxs(CardContent, {
|
|
2721
|
+
className: "max-h-72 overflow-y-auto p-2 xl:max-h-[36rem]",
|
|
2722
|
+
children: [
|
|
2723
|
+
loading && /* @__PURE__ */ jsx("p", {
|
|
2724
|
+
className: "p-4 text-sm text-text-secondary",
|
|
2725
|
+
children: tr("fluidgrids.dataTables.loading", "Loading tables…")
|
|
2726
|
+
}),
|
|
2727
|
+
!loading && tables.length === 0 && tagFilter && /* @__PURE__ */ jsx("p", {
|
|
2728
|
+
className: "p-4 text-sm text-text-secondary",
|
|
2729
|
+
children: tr("fluidgrids.dataTables.noTablesWithTag", "No tables carry this tag.")
|
|
2730
|
+
}),
|
|
2731
|
+
!loading && tables.length === 0 && !tagFilter && /* @__PURE__ */ jsx(IllustratedEmptyState, {
|
|
2732
|
+
className: "py-8",
|
|
2733
|
+
illustration: "onboarding",
|
|
2734
|
+
title: tr("fluidgrids.dataTables.empty", v2 ? "Create your first table" : "Create your first Data Table"),
|
|
2735
|
+
description: tr("fluidgrids.dataTables.emptyHint", v2 ? "Give your workflows a shared place to keep records between runs." : "Give your workflows a shared place to store typed records across runs."),
|
|
2736
|
+
action: /* @__PURE__ */ jsx(SdkPermissionButton, {
|
|
2737
|
+
permission: FLUIDGRIDS_PERMISSIONS.DATA_TABLE_CREATE,
|
|
2738
|
+
deniedTooltip: tr("fluidgrids.dataTables.createDeniedTooltip", "You don't have permission to create data tables"),
|
|
2739
|
+
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
2740
|
+
size: "sm",
|
|
2741
|
+
onClick: () => {
|
|
2742
|
+
setEditingTable(null);
|
|
2743
|
+
setTableDialogOpen(true);
|
|
2744
|
+
},
|
|
2745
|
+
children: [/* @__PURE__ */ jsx(Plus, {
|
|
2746
|
+
className: "mr-2 size-4",
|
|
2747
|
+
"aria-hidden": "true"
|
|
2748
|
+
}), tr("fluidgrids.dataTables.createFirstTable", "Create a table")]
|
|
2749
|
+
})
|
|
2750
|
+
})
|
|
2751
|
+
}),
|
|
2752
|
+
!loading && tables.map((table) => /* @__PURE__ */ jsxs("button", {
|
|
2753
|
+
type: "button",
|
|
2754
|
+
onClick: () => setSelectedId(table.id),
|
|
2755
|
+
className: `mb-1 flex w-full items-start justify-between gap-3 rounded-md px-3 py-3 text-left transition-colors ${selectedId === table.id ? "bg-background-tertiary text-text-primary" : "text-text-secondary hover:bg-background-secondary hover:text-text-primary"}`,
|
|
2756
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
2757
|
+
className: "min-w-0",
|
|
2758
|
+
children: [
|
|
2759
|
+
/* @__PURE__ */ jsx("span", {
|
|
2760
|
+
className: "block truncate text-sm font-medium",
|
|
2761
|
+
children: table.name
|
|
2762
|
+
}),
|
|
2763
|
+
/* @__PURE__ */ jsxs("span", {
|
|
2764
|
+
className: "mt-1 block text-xs text-text-secondary",
|
|
2765
|
+
children: [
|
|
2766
|
+
table.rowCount,
|
|
2767
|
+
" ",
|
|
2768
|
+
tr("fluidgrids.dataTables.rows", "rows"),
|
|
2769
|
+
" ·",
|
|
2770
|
+
" ",
|
|
2771
|
+
table.columns.length,
|
|
2772
|
+
" ",
|
|
2773
|
+
tr("fluidgrids.dataTables.columns", "columns")
|
|
2774
|
+
]
|
|
2775
|
+
}),
|
|
2776
|
+
(!v2 || tagFilter !== "") && (tagsById[table.id] ?? []).length > 0 && /* @__PURE__ */ jsx("span", {
|
|
2777
|
+
className: "mt-1.5 flex flex-wrap gap-1",
|
|
2778
|
+
children: (tagsById[table.id] ?? []).map((assignment) => /* @__PURE__ */ jsx(TagBadge, {
|
|
2779
|
+
id: assignment.tagId,
|
|
2780
|
+
label: tagLabel(assignment),
|
|
2781
|
+
color: assignment.tagInfo?.color ?? void 0,
|
|
2782
|
+
size: "sm"
|
|
2783
|
+
}, assignment.tagId))
|
|
2784
|
+
})
|
|
2785
|
+
]
|
|
2786
|
+
}), /* @__PURE__ */ jsx(Table2, {
|
|
2787
|
+
className: "mt-0.5 size-4 shrink-0",
|
|
2788
|
+
"aria-hidden": "true"
|
|
2789
|
+
})]
|
|
2790
|
+
}, table.id))
|
|
2791
|
+
]
|
|
2792
|
+
})]
|
|
2793
|
+
}),
|
|
2794
|
+
error && !selectedTable && /* @__PURE__ */ jsx(Card, {
|
|
2795
|
+
className: "border-border-secondary",
|
|
2796
|
+
children: /* @__PURE__ */ jsxs(CardContent, {
|
|
2797
|
+
className: "p-6 text-sm text-text-secondary",
|
|
2798
|
+
children: [v2 ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("p", { children: tr("fluidgrids.dataTables.loadFailed", "We couldn't load your tables. Try again in a moment.") }), /* @__PURE__ */ jsx(TechnicalDetails, {
|
|
2799
|
+
className: "mt-2",
|
|
2800
|
+
children: /* @__PURE__ */ jsx("code", {
|
|
2801
|
+
className: "break-words text-xs",
|
|
2802
|
+
children: error.message
|
|
2803
|
+
})
|
|
2804
|
+
})] }) : /* @__PURE__ */ jsx("p", { children: error.message }), /* @__PURE__ */ jsxs(Button$1, {
|
|
2805
|
+
className: "mt-4",
|
|
2806
|
+
variant: "outline",
|
|
2807
|
+
onClick: () => void refresh(),
|
|
2808
|
+
children: [/* @__PURE__ */ jsx(RefreshCw, {
|
|
2809
|
+
className: "mr-2 size-4",
|
|
2810
|
+
"aria-hidden": "true"
|
|
2811
|
+
}), tr("fluidgrids.dataTables.retry", "Try again")]
|
|
2812
|
+
})]
|
|
2813
|
+
})
|
|
2814
|
+
}),
|
|
2815
|
+
!error && !selectedTable && !loading && !(v2 && tables.length === 0 && tagFilter === "") && /* @__PURE__ */ jsx(Card, {
|
|
2816
|
+
className: "border-border-secondary",
|
|
2817
|
+
children: /* @__PURE__ */ jsxs(CardContent, {
|
|
2818
|
+
className: "flex min-h-[22rem] flex-col items-center justify-center p-6 text-center",
|
|
2819
|
+
children: [
|
|
2820
|
+
/* @__PURE__ */ jsx(Database, {
|
|
2821
|
+
className: "mb-3 size-10 text-text-secondary",
|
|
2822
|
+
"aria-hidden": "true"
|
|
2823
|
+
}),
|
|
2824
|
+
/* @__PURE__ */ jsx("h2", {
|
|
2825
|
+
className: "text-lg font-semibold text-text-primary",
|
|
2826
|
+
children: tr("fluidgrids.dataTables.selectTitle", v2 ? "Select a table" : "Select a data table")
|
|
2827
|
+
}),
|
|
2828
|
+
/* @__PURE__ */ jsx("p", {
|
|
2829
|
+
className: "mt-1 max-w-sm text-sm text-text-secondary",
|
|
2830
|
+
children: tr("fluidgrids.dataTables.selectDescription", "Choose a table to view and edit its rows, or create your first table.")
|
|
2831
|
+
})
|
|
2832
|
+
]
|
|
2833
|
+
})
|
|
2834
|
+
}),
|
|
2835
|
+
selectedTable && /* @__PURE__ */ jsx("div", {
|
|
2836
|
+
className: "min-w-0",
|
|
2837
|
+
children: /* @__PURE__ */ jsx(TableRowsPanel, {
|
|
2838
|
+
table: selectedTable,
|
|
2839
|
+
onTableChanged: refresh,
|
|
2840
|
+
onEditTable: () => {
|
|
2841
|
+
setEditingTable(selectedTable);
|
|
2842
|
+
setTableDialogOpen(true);
|
|
2843
|
+
},
|
|
2844
|
+
onDeleteTable: handleDeleteTable,
|
|
2845
|
+
deletingTable: deleting,
|
|
2846
|
+
onTagsChanged: handleTagsChanged
|
|
2847
|
+
}, selectedTable.id)
|
|
2848
|
+
})
|
|
2849
|
+
]
|
|
2850
|
+
})
|
|
2851
|
+
]
|
|
2852
|
+
}),
|
|
2853
|
+
v2 ? /* @__PURE__ */ jsx(TableFormDialogV2, {
|
|
2854
|
+
open: tableDialogOpen,
|
|
2855
|
+
table: editingTable,
|
|
2856
|
+
onOpenChange: setTableDialogOpen,
|
|
2857
|
+
onSaved: handleTableSaved
|
|
2858
|
+
}) : /* @__PURE__ */ jsx(TableFormDialog, {
|
|
2859
|
+
open: tableDialogOpen,
|
|
2860
|
+
table: editingTable,
|
|
2861
|
+
onOpenChange: setTableDialogOpen,
|
|
2862
|
+
onSaved: handleTableSaved
|
|
2863
|
+
}),
|
|
2864
|
+
v2 && /* @__PURE__ */ jsx(ConfirmActionDialog, {
|
|
2865
|
+
open: confirmingDeleteTable,
|
|
2866
|
+
onOpenChange: setConfirmingDeleteTable,
|
|
2867
|
+
title: tr("fluidgrids.dataTables.deleteTableTitle", "Delete \"{{name}}\"?", { name: selectedTable?.name ?? "" }),
|
|
2868
|
+
description: tr("fluidgrids.dataTables.deleteTableBody", "This permanently deletes the table and the {{count}} row(s) in it. Workflows that use it will stop working until you point them at another table.", { count: selectedTable?.rowCount ?? 0 }),
|
|
2869
|
+
confirmLabel: tr("fluidgrids.dataTables.delete", "Delete"),
|
|
2870
|
+
cancelLabel: tr("fluidgrids.dataTables.keep", "Keep"),
|
|
2871
|
+
destructive: true,
|
|
2872
|
+
busy: deleting,
|
|
2873
|
+
onConfirm: () => void confirmDeleteTable(),
|
|
2874
|
+
"data-testid": "confirm-delete-table"
|
|
2875
|
+
})
|
|
2876
|
+
]
|
|
2877
|
+
});
|
|
2878
|
+
}
|
|
2879
|
+
|
|
2880
|
+
export { rowsToCsv as a, useDataTables as c, useInsertDataTableRow as d, useUpdateDataTable as f, parseCsv as i, useDeleteDataTable as l, DataTablesPage_exports as n, useCreateDataTable as o, useUpdateDataTableRow as p, csvToRows as r, useDataTableRows as s, DataTablesPage as t, useDeleteDataTableRows as u };
|