blixify-ui-web-mcp 0.1.72 → 0.1.76
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/lib/components/chart/barChart/README.md +2 -0
- package/lib/components/chart/barChart/barChart.stories.tsx +86 -3
- package/lib/components/chart/pieChart/README.md +3 -1
- package/lib/components/chart/pieChart/pieChart.stories.tsx +18 -1
- package/lib/components/data/dataGraph/README.md +18 -1
- package/lib/components/data/dataTemplate/README.md +6 -3
- package/lib/components/data/dataTemplate/dataTemplate.groupedBoard.stories.tsx +311 -0
- package/lib/components/data/dataTemplate/dataTemplate.readDrawer.stories.tsx +334 -0
- package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +162 -0
- package/lib/components/data/readQuery/README.md +8 -8
- package/lib/components/display/modal/README.md +1 -0
- package/lib/components/display/newList/README.md +30 -0
- package/lib/components/display/newTable/README.md +25 -20
- package/lib/components/display/newTable/newTable.stories.tsx +67 -0
- package/lib/components/navigation/leftNav/README.md +1 -1
- package/lib/components/navigation/leftNav/leftNav.stories.tsx +61 -0
- package/package.json +1 -1
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { Menu, Transition } from "@headlessui/react";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import MockAdapter from "axios-mock-adapter";
|
|
4
|
+
import Papa from "papaparse";
|
|
5
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { ButtonGroup } from "../../action/buttonGroup";
|
|
7
|
+
import { DataTemplate as DataTemplateImpl } from ".";
|
|
8
|
+
import { handleApplyStoryQueries } from "../dataWrapper/storyBackend.fixtures";
|
|
9
|
+
import { DataStructure } from "./model";
|
|
10
|
+
|
|
11
|
+
const meta = { title: "Data/Data Template" };
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
// INFO: its own endpoint, so the mock never answers for another story
|
|
15
|
+
const DEMO_ENDPOINT = "https://dtw-read-drawer-demo.local/api";
|
|
16
|
+
const DEMO_COLLECTION = "demo-read-drawer-records";
|
|
17
|
+
const COMMENTS_COLLECTION = `${DEMO_COLLECTION}-comments`;
|
|
18
|
+
const LOGS_COLLECTION = `${DEMO_COLLECTION}-logs`;
|
|
19
|
+
const CURRENT_USER = "alice";
|
|
20
|
+
const STAGES = ["New", "In Progress", "Done"];
|
|
21
|
+
const OWNERS = ["Alice", "Ben", "Chitra", "Dan"];
|
|
22
|
+
|
|
23
|
+
const DEMO_SCHEMA: DataStructure[] = [
|
|
24
|
+
{ id: "name", name: "Record", type: "string" },
|
|
25
|
+
{ id: "stage", name: "Stage", type: "list", listSelection: STAGES },
|
|
26
|
+
{ id: "owner", name: "Owner", type: "string" },
|
|
27
|
+
{ id: "summary", name: "Summary", type: "string" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const DRAWER_TOGGLE = [
|
|
31
|
+
{ id: "drawer", name: "Read drawer" },
|
|
32
|
+
{ id: "page", name: "No drawer" },
|
|
33
|
+
];
|
|
34
|
+
const ROLE_TOGGLE = [
|
|
35
|
+
{ id: "editor", name: "Can update" },
|
|
36
|
+
{ id: "viewer", name: "Viewer" },
|
|
37
|
+
];
|
|
38
|
+
const COMMENTS_TOGGLE = [
|
|
39
|
+
{ id: "on", name: "Comments" },
|
|
40
|
+
{ id: "off", name: "No comments" },
|
|
41
|
+
];
|
|
42
|
+
const LAYOUT_TOGGLE = [
|
|
43
|
+
{ id: "desktop", name: "Desktop" },
|
|
44
|
+
{ id: "mobile", name: "Mobile" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const demoDevSettings: any = {
|
|
48
|
+
server: "mongoServer",
|
|
49
|
+
dbEndpoint: DEMO_ENDPOINT,
|
|
50
|
+
dbAPIToken: "",
|
|
51
|
+
dbUserToken: "",
|
|
52
|
+
schemaEndpoint: DEMO_ENDPOINT,
|
|
53
|
+
schemaEndpointToken: "",
|
|
54
|
+
imageEndpoint: "https://placehold.co/600x400/e2e8f0/475569.png?text=",
|
|
55
|
+
uploadEndpoint: "",
|
|
56
|
+
organisationMembers: [
|
|
57
|
+
{ key: "alice", label: "Alice Tan" },
|
|
58
|
+
{ key: "ben", label: "Ben Wong" },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// INFO: a few records carry comments so the bubble's count shows on some rows
|
|
63
|
+
// and not others
|
|
64
|
+
const COMMENT_COUNTS: { [id: string]: number } = {
|
|
65
|
+
"rec-01": 3,
|
|
66
|
+
"rec-02": 1,
|
|
67
|
+
"rec-04": 12,
|
|
68
|
+
"rec-07": 120,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const renderDemoComments = () =>
|
|
72
|
+
Object.entries(COMMENT_COUNTS).flatMap(([itemId, count]) =>
|
|
73
|
+
Array.from({ length: Math.min(count, 5) }, (_, index) => ({
|
|
74
|
+
_id: `${itemId}-comment-${index + 1}`,
|
|
75
|
+
collectionId: DEMO_COLLECTION,
|
|
76
|
+
itemId,
|
|
77
|
+
userId: index % 2 === 0 ? "ben" : "alice",
|
|
78
|
+
comment: `Comment ${index + 1} on ${itemId}`,
|
|
79
|
+
createdAt: new Date(Date.UTC(2026, 10, 1, 9, index)).toISOString(),
|
|
80
|
+
})),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const renderDemoRecords = () =>
|
|
84
|
+
Array.from({ length: 12 }, (_, index) => {
|
|
85
|
+
const number = String(index + 1).padStart(2, "0");
|
|
86
|
+
return {
|
|
87
|
+
_id: `rec-${number}`,
|
|
88
|
+
name: `Record ${number}`,
|
|
89
|
+
stage: STAGES[index % STAGES.length],
|
|
90
|
+
owner: OWNERS[index % OWNERS.length],
|
|
91
|
+
summary: `Summary for record ${number}`,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const handleInstallDemoMock = (
|
|
96
|
+
store: any[],
|
|
97
|
+
activity: { [collection: string]: any[] },
|
|
98
|
+
) => {
|
|
99
|
+
const mock = new MockAdapter(axios, {
|
|
100
|
+
onNoMatch: "passthrough",
|
|
101
|
+
delayResponse: 150,
|
|
102
|
+
});
|
|
103
|
+
const renderUrl = (type: string) =>
|
|
104
|
+
`${DEMO_ENDPOINT}/${DEMO_COLLECTION}/${type}`;
|
|
105
|
+
const handleParseBody = (config: any) => {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(config?.data ?? "{}");
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
mock
|
|
114
|
+
.onPost(renderUrl("list"))
|
|
115
|
+
.reply(() => [200, { data: store, count: store.length }]);
|
|
116
|
+
mock.onPost(renderUrl("get")).reply((config) => {
|
|
117
|
+
const body = handleParseBody(config);
|
|
118
|
+
return [200, { data: store.find((each) => each._id === body.id) ?? null }];
|
|
119
|
+
});
|
|
120
|
+
mock.onPost(renderUrl("update")).reply((config) => {
|
|
121
|
+
const body = handleParseBody(config);
|
|
122
|
+
const index = store.findIndex((each) => each._id === body.id);
|
|
123
|
+
if (index >= 0) store[index] = { ...store[index], ...body.data };
|
|
124
|
+
return [200, { data: body.id }];
|
|
125
|
+
});
|
|
126
|
+
mock
|
|
127
|
+
.onPost(
|
|
128
|
+
new RegExp(
|
|
129
|
+
`^${DEMO_ENDPOINT}/(${COMMENTS_COLLECTION}|${LOGS_COLLECTION})/[a-z]+$`,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
.reply((config) => {
|
|
133
|
+
const [collection, action] = (config.url ?? "").split("/").slice(-2);
|
|
134
|
+
const body = handleParseBody(config);
|
|
135
|
+
const rows = activity[collection];
|
|
136
|
+
if (action === "list") {
|
|
137
|
+
const data = handleApplyStoryQueries(rows, body.query).sort(
|
|
138
|
+
(a: any, b: any) =>
|
|
139
|
+
String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? "")),
|
|
140
|
+
);
|
|
141
|
+
const cursor = body.cursor ?? 0;
|
|
142
|
+
return [
|
|
143
|
+
200,
|
|
144
|
+
{
|
|
145
|
+
data: data.slice(cursor, cursor + (body.limit ?? 10)),
|
|
146
|
+
count: data.length,
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
if (action === "get")
|
|
151
|
+
return [200, { data: rows.find((row) => row._id === body.id) }];
|
|
152
|
+
if (action === "create") rows.push(body.data);
|
|
153
|
+
if (action === "update") {
|
|
154
|
+
const index = rows.findIndex((row) => row._id === body.id);
|
|
155
|
+
if (index >= 0) rows[index] = { ...rows[index], ...body.data };
|
|
156
|
+
}
|
|
157
|
+
if (action === "delete") {
|
|
158
|
+
const index = rows.findIndex((row) => row._id === body.id);
|
|
159
|
+
if (index >= 0) rows.splice(index, 1);
|
|
160
|
+
}
|
|
161
|
+
return [200, { success: true }];
|
|
162
|
+
});
|
|
163
|
+
// INFO: the tab bar waits on this lookup
|
|
164
|
+
mock
|
|
165
|
+
.onPost(`${DEMO_ENDPOINT}/spaces/list`)
|
|
166
|
+
.reply(200, { data: [], count: 0 });
|
|
167
|
+
|
|
168
|
+
return mock;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const ReadDrawerDemo = () => {
|
|
172
|
+
const [typeParam, setTypeParam] = useState<"list" | "read" | "update">(
|
|
173
|
+
"list",
|
|
174
|
+
);
|
|
175
|
+
const [dataId, setDataId] = useState<string>("");
|
|
176
|
+
const [drawerMode, setDrawerMode] = useState<"drawer" | "page">("drawer");
|
|
177
|
+
const [role, setRole] = useState<"editor" | "viewer">("editor");
|
|
178
|
+
const [comments, setComments] = useState<"on" | "off">("on");
|
|
179
|
+
const [layout, setLayout] = useState<"desktop" | "mobile">("desktop");
|
|
180
|
+
const [isMockReady, setIsMockReady] = useState(false);
|
|
181
|
+
|
|
182
|
+
const store = useMemo(() => renderDemoRecords(), []);
|
|
183
|
+
const activity = useMemo(
|
|
184
|
+
() => ({
|
|
185
|
+
[COMMENTS_COLLECTION]: renderDemoComments(),
|
|
186
|
+
[LOGS_COLLECTION]: [] as any[],
|
|
187
|
+
}),
|
|
188
|
+
[],
|
|
189
|
+
);
|
|
190
|
+
const hasComments = comments === "on";
|
|
191
|
+
const hasDrawer = drawerMode === "drawer";
|
|
192
|
+
const canUpdate = role === "editor";
|
|
193
|
+
const isMobile = layout === "mobile";
|
|
194
|
+
|
|
195
|
+
// INFO: in an effect so it survives StrictMode's double mount
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
const mock = handleInstallDemoMock(store, activity);
|
|
198
|
+
setIsMockReady(true);
|
|
199
|
+
return () => {
|
|
200
|
+
mock.restore();
|
|
201
|
+
setIsMockReady(false);
|
|
202
|
+
};
|
|
203
|
+
}, [store, activity]);
|
|
204
|
+
|
|
205
|
+
const handleNavigate = (type: string, id?: string) => {
|
|
206
|
+
setTypeParam(type as "list" | "read" | "update");
|
|
207
|
+
setDataId(id ?? "");
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div className="w-full">
|
|
212
|
+
<div className="mb-4 flex flex-wrap items-center gap-3">
|
|
213
|
+
<ButtonGroup
|
|
214
|
+
selection={DRAWER_TOGGLE}
|
|
215
|
+
selectedId={drawerMode}
|
|
216
|
+
variant="tag"
|
|
217
|
+
size="small"
|
|
218
|
+
onClick={(id) => {
|
|
219
|
+
setDrawerMode(id as "drawer" | "page");
|
|
220
|
+
handleNavigate("list");
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
<ButtonGroup
|
|
224
|
+
selection={ROLE_TOGGLE}
|
|
225
|
+
selectedId={role}
|
|
226
|
+
variant="tag"
|
|
227
|
+
size="small"
|
|
228
|
+
onClick={(id) => setRole(id as "editor" | "viewer")}
|
|
229
|
+
/>
|
|
230
|
+
<ButtonGroup
|
|
231
|
+
selection={COMMENTS_TOGGLE}
|
|
232
|
+
selectedId={comments}
|
|
233
|
+
variant="tag"
|
|
234
|
+
size="small"
|
|
235
|
+
onClick={(id) => setComments(id as "on" | "off")}
|
|
236
|
+
/>
|
|
237
|
+
<ButtonGroup
|
|
238
|
+
selection={LAYOUT_TOGGLE}
|
|
239
|
+
selectedId={layout}
|
|
240
|
+
variant="tag"
|
|
241
|
+
size="small"
|
|
242
|
+
onClick={(id) => setLayout(id as "desktop" | "mobile")}
|
|
243
|
+
/>
|
|
244
|
+
{typeParam !== "list" && !hasDrawer && (
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
className="rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-700 hover:bg-gray-50"
|
|
248
|
+
onClick={() => handleNavigate("list")}
|
|
249
|
+
>
|
|
250
|
+
← Back to list
|
|
251
|
+
</button>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
<div className={isMobile ? "mx-auto max-w-sm" : "w-full"}>
|
|
255
|
+
{isMockReady && (
|
|
256
|
+
<DataTemplateImpl
|
|
257
|
+
key={`${drawerMode}-${role}-${comments}-${layout}`}
|
|
258
|
+
title="Read Drawer"
|
|
259
|
+
description="Offline records"
|
|
260
|
+
type={typeParam}
|
|
261
|
+
id={dataId}
|
|
262
|
+
collectionId={DEMO_COLLECTION}
|
|
263
|
+
model={DEMO_SCHEMA}
|
|
264
|
+
localURLEndpoint=""
|
|
265
|
+
devSettings={demoDevSettings}
|
|
266
|
+
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
267
|
+
organise={{
|
|
268
|
+
handleNavigate,
|
|
269
|
+
// INFO: counted from the live comments, so a comment added in
|
|
270
|
+
// the panel shows on the row once the list reloads
|
|
271
|
+
onFetchCommentCounts: hasComments
|
|
272
|
+
? async (ids: string[]) =>
|
|
273
|
+
Object.fromEntries(
|
|
274
|
+
ids.map((id) => [
|
|
275
|
+
id,
|
|
276
|
+
id in COMMENT_COUNTS && COMMENT_COUNTS[id] > 5
|
|
277
|
+
? COMMENT_COUNTS[id]
|
|
278
|
+
: activity[COMMENTS_COLLECTION].filter(
|
|
279
|
+
(row) => row.itemId === id,
|
|
280
|
+
).length,
|
|
281
|
+
]),
|
|
282
|
+
)
|
|
283
|
+
: undefined,
|
|
284
|
+
}}
|
|
285
|
+
edit={{
|
|
286
|
+
allEditable: canUpdate,
|
|
287
|
+
listEditable: canUpdate,
|
|
288
|
+
disabledUpdate: !canUpdate,
|
|
289
|
+
activityLog: hasComments
|
|
290
|
+
? {
|
|
291
|
+
enableComments: true,
|
|
292
|
+
commentCollectionId: COMMENTS_COLLECTION,
|
|
293
|
+
enableLogs: true,
|
|
294
|
+
logsCollectionId: LOGS_COLLECTION,
|
|
295
|
+
currentUserId: CURRENT_USER,
|
|
296
|
+
}
|
|
297
|
+
: undefined,
|
|
298
|
+
}}
|
|
299
|
+
bareSettings={{
|
|
300
|
+
bareNewDTW: true,
|
|
301
|
+
bareFormDrawer: hasDrawer,
|
|
302
|
+
bareListMobileView: isMobile,
|
|
303
|
+
bareUpdateMobileView: isMobile,
|
|
304
|
+
}}
|
|
305
|
+
viewSettings={
|
|
306
|
+
hasDrawer
|
|
307
|
+
? { readDrawer: { anchor: "responsive", maxWidth: "3xl" } }
|
|
308
|
+
: undefined
|
|
309
|
+
}
|
|
310
|
+
/>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const ReadDrawerRowAction = () => (
|
|
318
|
+
<div className="w-full">
|
|
319
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
320
|
+
Data Template — row action beside the title
|
|
321
|
+
</div>
|
|
322
|
+
<div className="text-sm mb-4 text-gray-600">
|
|
323
|
+
Hover a table row to show the chevron beside its title, which opens the
|
|
324
|
+
record's read view for anyone who can see it — in the drawer with a read
|
|
325
|
+
drawer, as a page with No drawer. Then come two divided columns: the
|
|
326
|
+
pencil, which opens the edit form, only for people who can update, and the
|
|
327
|
+
comment bubble, with a count on rows that have any (12 and 120 show the
|
|
328
|
+
two-digit and 99+ badges). Drag the Record column narrower to see its
|
|
329
|
+
floor. Mobile cards open from the card, so they carry the pencil and the
|
|
330
|
+
bubble but no chevron.
|
|
331
|
+
</div>
|
|
332
|
+
<ReadDrawerDemo />
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
@@ -1368,3 +1368,165 @@ export const GroupByPagePagination = () => (
|
|
|
1368
1368
|
<GroupByScrollPaginationDemo paginationType="page" />
|
|
1369
1369
|
</div>
|
|
1370
1370
|
);
|
|
1371
|
+
|
|
1372
|
+
const DEFAULTS_COLLECTION = "demo-workspace-defaults";
|
|
1373
|
+
const DEFAULTS_MEMBERS = [
|
|
1374
|
+
{ key: "u1", label: "Aisha Rahman" },
|
|
1375
|
+
{ key: "u2", label: "Ben Tan" },
|
|
1376
|
+
];
|
|
1377
|
+
const DEFAULTS_SCHEMA: DataStructure[] = [
|
|
1378
|
+
{ id: "name", name: "Name", type: "string" },
|
|
1379
|
+
{
|
|
1380
|
+
id: "stage",
|
|
1381
|
+
name: "Stage",
|
|
1382
|
+
type: "list",
|
|
1383
|
+
listSelection: ["New", "Quoted", "Won"],
|
|
1384
|
+
optional: true,
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
id: "status",
|
|
1388
|
+
name: "Status",
|
|
1389
|
+
type: "list",
|
|
1390
|
+
listSelection: ["Open", "Waiting", "Closed"],
|
|
1391
|
+
optional: true,
|
|
1392
|
+
},
|
|
1393
|
+
{ id: "owner", name: "Owner", type: "person", optional: true },
|
|
1394
|
+
{ id: "amount", name: "Amount", type: "number", optional: true },
|
|
1395
|
+
{ id: "due", name: "Due", type: "date", optional: true },
|
|
1396
|
+
];
|
|
1397
|
+
|
|
1398
|
+
const handleInstallDefaultsMock = (store: any[]) => {
|
|
1399
|
+
const mock = new MockAdapter(axios, { onNoMatch: "passthrough" });
|
|
1400
|
+
const renderUrl = (type: string) =>
|
|
1401
|
+
`${DEMO_ENDPOINT}/${DEFAULTS_COLLECTION}/${type}`;
|
|
1402
|
+
const handleParse = (config: any) => JSON.parse(config?.data ?? "{}");
|
|
1403
|
+
const handleMatch = (row: any, query: any[]) =>
|
|
1404
|
+
(query ?? []).every((eachQuery: any) => {
|
|
1405
|
+
const value = row[eachQuery.queryId];
|
|
1406
|
+
const handleIn = (list: any[]) =>
|
|
1407
|
+
list.some((each) => (each === null ? value == null : each === value));
|
|
1408
|
+
if (eachQuery.type === "=") return value === eachQuery.value;
|
|
1409
|
+
if (eachQuery.type === "in") return handleIn(eachQuery.value);
|
|
1410
|
+
if (eachQuery.type === "!in") return !handleIn(eachQuery.value);
|
|
1411
|
+
return true;
|
|
1412
|
+
});
|
|
1413
|
+
mock.onPost(renderUrl("list")).reply((config) => {
|
|
1414
|
+
const body = handleParse(config);
|
|
1415
|
+
const rows = store.filter((each) => handleMatch(each, body.query));
|
|
1416
|
+
// INFO: a grouped sum, in the shape the list endpoint answers one with
|
|
1417
|
+
if (body.aggregate?.type === "sum") {
|
|
1418
|
+
const totals: { [key: string]: any } = {};
|
|
1419
|
+
rows.forEach((each) => {
|
|
1420
|
+
const key = each[body.aggregate.groupId] ?? null;
|
|
1421
|
+
const total = (totals[String(key)] = totals[String(key)] ?? {
|
|
1422
|
+
_id: key,
|
|
1423
|
+
value: 0,
|
|
1424
|
+
});
|
|
1425
|
+
total.value += Number(each[body.aggregate.queryId]) || 0;
|
|
1426
|
+
});
|
|
1427
|
+
return [200, { data: [{ _id: "range", value: Object.values(totals) }] }];
|
|
1428
|
+
}
|
|
1429
|
+
return [200, { data: rows, count: rows.length }];
|
|
1430
|
+
});
|
|
1431
|
+
mock.onPost(renderUrl("create")).reply((config) => {
|
|
1432
|
+
const row = { ...handleParse(config).data, _id: `r${store.length + 1}` };
|
|
1433
|
+
store.push(row);
|
|
1434
|
+
(window as any).__defaultsWrites = [
|
|
1435
|
+
...((window as any).__defaultsWrites ?? []),
|
|
1436
|
+
{ type: "create", data: row },
|
|
1437
|
+
];
|
|
1438
|
+
return [200, { success: true, data: row }];
|
|
1439
|
+
});
|
|
1440
|
+
mock.onPost(renderUrl("update")).reply((config) => {
|
|
1441
|
+
const body = handleParse(config);
|
|
1442
|
+
const row = store.find((each) => each._id === body.id);
|
|
1443
|
+
if (row) Object.assign(row, body.data);
|
|
1444
|
+
(window as any).__defaultsWrites = [
|
|
1445
|
+
...((window as any).__defaultsWrites ?? []),
|
|
1446
|
+
{ type: "update", id: body.id, data: body.data },
|
|
1447
|
+
];
|
|
1448
|
+
return [200, { success: true }];
|
|
1449
|
+
});
|
|
1450
|
+
mock
|
|
1451
|
+
.onPost(`${DEMO_ENDPOINT}/spaces/list`)
|
|
1452
|
+
.reply(200, { data: [], count: 0 });
|
|
1453
|
+
return mock;
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
const WorkspaceDefaultsDemo = () => {
|
|
1457
|
+
const store = React.useMemo(
|
|
1458
|
+
() => [
|
|
1459
|
+
{
|
|
1460
|
+
_id: "r1",
|
|
1461
|
+
name: "Rooftop survey",
|
|
1462
|
+
stage: "New",
|
|
1463
|
+
status: "Open",
|
|
1464
|
+
owner: "u1",
|
|
1465
|
+
amount: 1200,
|
|
1466
|
+
due: new Date(Date.now() - 2 * 86400000).toISOString(),
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
_id: "r2",
|
|
1470
|
+
name: "Inverter quote",
|
|
1471
|
+
stage: "Quoted",
|
|
1472
|
+
status: "Waiting",
|
|
1473
|
+
owner: "u2",
|
|
1474
|
+
amount: 5400.5,
|
|
1475
|
+
due: new Date().toISOString(),
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
_id: "r3",
|
|
1479
|
+
name: "Panel install",
|
|
1480
|
+
stage: "Won",
|
|
1481
|
+
status: "Closed",
|
|
1482
|
+
owner: "u1",
|
|
1483
|
+
amount: 18000,
|
|
1484
|
+
},
|
|
1485
|
+
],
|
|
1486
|
+
[],
|
|
1487
|
+
);
|
|
1488
|
+
React.useMemo(() => handleInstallDefaultsMock(store), [store]);
|
|
1489
|
+
|
|
1490
|
+
return (
|
|
1491
|
+
<DataTemplateImpl
|
|
1492
|
+
title="Workspace defaults"
|
|
1493
|
+
description=""
|
|
1494
|
+
type="list"
|
|
1495
|
+
collectionId={DEFAULTS_COLLECTION}
|
|
1496
|
+
model={DEFAULTS_SCHEMA}
|
|
1497
|
+
localURLEndpoint=""
|
|
1498
|
+
devSettings={{ ...demoDevSettings, organisationMembers: DEFAULTS_MEMBERS }}
|
|
1499
|
+
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
1500
|
+
edit={{
|
|
1501
|
+
allEditable: true,
|
|
1502
|
+
listAddable: true,
|
|
1503
|
+
activityLog: {
|
|
1504
|
+
enableLogs: true,
|
|
1505
|
+
logsCollectionId: `${DEFAULTS_COLLECTION}-logs`,
|
|
1506
|
+
currentUserId: "u1",
|
|
1507
|
+
},
|
|
1508
|
+
}}
|
|
1509
|
+
bareSettings={{ bareNewDTW: true }}
|
|
1510
|
+
organise={{
|
|
1511
|
+
groupBy: "stage",
|
|
1512
|
+
showGroupByFeature: true,
|
|
1513
|
+
dateWarnings: true,
|
|
1514
|
+
datasheetDefaults: true,
|
|
1515
|
+
renderGroupByOptions: () => [
|
|
1516
|
+
{ key: "stage", label: "Stage" },
|
|
1517
|
+
{ key: "owner", label: "Owner" },
|
|
1518
|
+
],
|
|
1519
|
+
onFetchCommentCounts: async () => ({ r1: 3 }),
|
|
1520
|
+
}}
|
|
1521
|
+
/>
|
|
1522
|
+
);
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
export const WorkspaceDefaults = () => (
|
|
1526
|
+
<div className="w-full">
|
|
1527
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
1528
|
+
Data Template — workspace defaults
|
|
1529
|
+
</div>
|
|
1530
|
+
<WorkspaceDefaultsDemo />
|
|
1531
|
+
</div>
|
|
1532
|
+
);
|
|
@@ -242,14 +242,14 @@ const res = await query.call({
|
|
|
242
242
|
|
|
243
243
|
## Query
|
|
244
244
|
|
|
245
|
-
| Param | Type | Description
|
|
246
|
-
| ---------------- | ----------------------------------------------------------------------------------------- |
|
|
247
|
-
| `type` | `search \| = \| != \| > \| < \| >< \| in \| !in \| exists \| elemMatch \| >size \| <size` | Operator
|
|
248
|
-
| `value` | `any` | Filter value (array for `><` and `in`)
|
|
249
|
-
| `queryId` | `string` | Field name
|
|
250
|
-
| `searchIds` | `string[]` | Fields to search across
|
|
251
|
-
| `orQuery` | `boolean` | Use OR instead of AND
|
|
252
|
-
| `orQueryGroupId` | `string` |
|
|
245
|
+
| Param | Type | Description | Optional |
|
|
246
|
+
| ---------------- | ----------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------ |
|
|
247
|
+
| `type` | `search \| = \| != \| > \| < \| >< \| in \| !in \| exists \| elemMatch \| >size \| <size` | Operator | |
|
|
248
|
+
| `value` | `any` | Filter value (array for `><` and `in`) | |
|
|
249
|
+
| `queryId` | `string` | Field name | Required except `search` |
|
|
250
|
+
| `searchIds` | `string[]` | Fields to search across | Required for `search` |
|
|
251
|
+
| `orQuery` | `boolean` | Use OR instead of AND | ✓ |
|
|
252
|
+
| `orQueryGroupId` | `string` | AND inside a block, OR between blocks | ✓ |
|
|
253
253
|
|
|
254
254
|
## Aggregate
|
|
255
255
|
|
|
@@ -44,3 +44,4 @@ import { Overlay } from "blixify-ui-web/components/display/overlay";
|
|
|
44
44
|
| `bgColor` | `string` | `bg-white` | Tailwind background class for the panel in light mode |
|
|
45
45
|
| `darkMode` | `boolean` | `false` | Switches the panel, text and icons to the dark palette |
|
|
46
46
|
| `darkModeBg` | `string` | `bg-gray-900` | Tailwind background class for the panel when `darkMode` is on |
|
|
47
|
+
| `zIndex` | `string` | `z-40` | Tailwind z-index class for the dialog. Pass a higher one for a modal opened from a layer drawn above z-40, such as the activity drawer (z-50) |
|
|
@@ -41,6 +41,36 @@ The legacy `List` renders **every** extra key on an item. `NewList` renders only
|
|
|
41
41
|
<NewList list={tasks} extraKeys={["details", "activity"]} />
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
## Compact rows
|
|
45
|
+
|
|
46
|
+
Set `compact` on an item to draw it as a two-line row, like a mail inbox: the title with a pill and a short date on the first line, one grey line under it, and an arrow that opens the rest. Every compact row is the same height. An item without `compact` keeps the layout above, so existing keys such as an `extraKeys` "details" keep their meaning.
|
|
47
|
+
|
|
48
|
+
| `compact` field | Rendered as |
|
|
49
|
+
| --------------- | ----------------------------------------------------------------------------- |
|
|
50
|
+
| `subtitle` | One grey line under the title, cut to fit |
|
|
51
|
+
| `details` | Shown under the row when its arrow is tapped; the arrow does not open the row |
|
|
52
|
+
| `badge` | A pill leading the second line. Without it, the item's `tags` / `tag` |
|
|
53
|
+
| `trailing` | The right end of the title line. Without it, the item's `date` |
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<NewList
|
|
57
|
+
list={[
|
|
58
|
+
{
|
|
59
|
+
id: "a1",
|
|
60
|
+
title: "Ahmad Solar House",
|
|
61
|
+
compact: {
|
|
62
|
+
badge: <Tag tagText="Done" color="green" size="small" />,
|
|
63
|
+
trailing: "12 Nov",
|
|
64
|
+
subtitle: "Selangor · RM 12,400",
|
|
65
|
+
details: <DetailList title="" list={fields} size="compact" />,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]}
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`NewListCompactRow` (`newList/compactRow`) is the row on its own; the new table draws its phone rows with it. `handlePickListRowFields` (`newList/listRowFields`) picks a row's fields from a column list in view order: the first date column for `trailing`, the first list column for `badge`, and the next two columns that hold something for `subtitle`. Pictures, files, drawings, long text, nested lists, buttons, timers and star ratings are left off the second line. `handleFormatListRowDate` writes a date as "12 Nov", adding the year outside the current one.
|
|
73
|
+
|
|
44
74
|
## Date
|
|
45
75
|
|
|
46
76
|
| `date` value | Output |
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# NewTable
|
|
2
2
|
|
|
3
|
-
Tabular display with a List-shaped mobile breakpoint.
|
|
3
|
+
Tabular display with a List-shaped mobile breakpoint. With `mobileView=true` each row is a two-line `NewListCompactRow`, like a mail inbox: the title and a short date, then a pill and a grey line of key values, and an arrow that opens every field. Tapping the row opens the record (`onClickRow` / `newTabUrl`), the checkbox ticks it without opening it, and groups draw the same foldable headers, counts and per-group paging as the computer layout. Rows are drawn again only when their own data changes, and a group with 120 or more loaded rows draws only the rows near the screen.
|
|
4
|
+
|
|
5
|
+
Without `mobileTooltip.titleAdjacent`, the view's column order picks a row's fields (`handlePickListRowFields`): the first list column is the pill, the first date column the date, and the next two columns that hold something the grey line.
|
|
4
6
|
|
|
5
7
|
## Row activity
|
|
6
8
|
|
|
7
9
|
Pass `onRowActivity={(id) => ...}` to show a keyboard-accessible chat button at the right of the first visible cell (beside the title on mobile cards). It stops row navigation and does not enter inline editing. When no left pins are configured, the first ungrouped column is pinned automatically; existing pin configurations are preserved. DTW wires this callback automatically when `edit.activityLog` is enabled with `bareNewDTW`.
|
|
8
10
|
|
|
9
|
-
Pass `onRowEdit={(id) => ...}` as well to put an edit pencil before the chat button, at the same size and colour.
|
|
11
|
+
Pass `onRowEdit={(id) => ...}` as well to put an edit pencil before the chat button, at the same size and colour, and `onRowOpen={(id) => ...}` to put a chevron beside the title. The pencil and the chat button each sit in their own divided strip at the title cell's right edge, like columns of their own, with the chevron just left of them. The chevron shows only when the row is hovered or the chevron is focused, and always on a touch screen, which cannot hover. While it shows, the title cell's padding grows to take it, so it never covers the title or its inline editor. All of them sit over the cell's right padding, so the row does not grow. While the title carries these actions, its column cannot be dragged — or set, or saved — narrower than its left padding, the actions' room (with the chevron's) and 48px of title; other columns keep the 80px floor. DTW passes the pencil only when the user can update records and opens the edit form with `handleNavigate("update", id, "general")`. It passes the chevron to anyone who can see the record, when `organise.handleNavigate` is set, and opens the read view with `handleNavigate("read", id, "general")` — in the drawer when `viewSettings.readDrawer` is set, as a page otherwise. An editable title cell is hard to click open, so the chevron is the sure way in. Mobile cards open from the card, so they show the pencil and the chat button but no chevron.
|
|
10
12
|
|
|
11
13
|
## Adding a row
|
|
12
14
|
|
|
@@ -26,18 +28,18 @@ Columns flagged `hideOnMobile` are dropped from the mobile rendering entirely.
|
|
|
26
28
|
|
|
27
29
|
## Mobile tooltip (`mobileTooltip`)
|
|
28
30
|
|
|
29
|
-
With `mobileView`, `mobileTooltip`
|
|
31
|
+
With `mobileView`, `mobileTooltip` picks what the row shows, in place of the column order, and adds a hover card. Through `DataTemplate` it is `bareSettings.bareMobileTooltip`, which applies when `bareNewDTW` and `bareListMobileView` are both on.
|
|
30
32
|
|
|
31
|
-
| Key | Type | Shows
|
|
32
|
-
| --------------------------- | -------------------------- |
|
|
33
|
-
| `titleAdjacent.tagKey` | `string` | That row field's value as a Tag next to the row title
|
|
34
|
-
| `titleAdjacent.tagColor` | `Record<string, TagColor>` | The Tag colour for each value
|
|
35
|
-
| `titleAdjacent.trailingKey` | `string` | That row field's value
|
|
36
|
-
| `titleAdjacent.summaryKeys` | `string[]` |
|
|
37
|
-
| `tooltip.imageKey` | `string` | The image URL field shown in the hover card
|
|
38
|
-
| `tooltip.summaryKeys` | `string[]` | Label + value pairs in the hover card
|
|
39
|
-
| `tooltip.tagKeys` | `string[]` | Tag pills in the hover card
|
|
40
|
-
| `tooltip.tagColors` | `Record<string, TagColor>` | The Tag colour for each value
|
|
33
|
+
| Key | Type | Shows |
|
|
34
|
+
| --------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `titleAdjacent.tagKey` | `string` | That row field's value as a Tag next to the row title |
|
|
36
|
+
| `titleAdjacent.tagColor` | `Record<string, TagColor>` | The Tag colour for each value |
|
|
37
|
+
| `titleAdjacent.trailingKey` | `string` | That row field's value at the right of the title line, e.g. a price. Without it the row shows no date |
|
|
38
|
+
| `titleAdjacent.summaryKeys` | `string[]` | The grey line under the title, as "Label: value" pairs joined with " · ". Labels come from the column titles; a dotted key (`shippingAddress.name`) falls back to its parent column's title. Empty values, hidden columns, the title and the `tagKey` column are skipped. Without it, and with `titleAdjacent` set, the line is left out |
|
|
39
|
+
| `tooltip.imageKey` | `string` | The image URL field shown in the hover card |
|
|
40
|
+
| `tooltip.summaryKeys` | `string[]` | Label + value pairs in the hover card |
|
|
41
|
+
| `tooltip.tagKeys` | `string[]` | Tag pills in the hover card |
|
|
42
|
+
| `tooltip.tagColors` | `Record<string, TagColor>` | The Tag colour for each value |
|
|
41
43
|
|
|
42
44
|
```tsx
|
|
43
45
|
<NewTable
|
|
@@ -117,8 +119,11 @@ Types that need more than a name and a type are completed in the popup:
|
|
|
117
119
|
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
118
120
|
| `reference`, `listReference`, `referenceSelection` | endpoint and label, from `renderReferenceSpaceOptions` / `renderReferenceLabelOptions` — required, since a reference with no target resolves nothing |
|
|
119
121
|
| `list`, `multipleList` | options, as a comma-separated line |
|
|
122
|
+
| `lookup` | a `reference` or `listReference` column of this table, then a column of the space it links to, from `renderReferenceLabelOptions(spaceId, true)`; `lookup`, `password` and `button` columns are left out. Offered only with `renderReferenceLabelOptions`. Saved as `{ type: "lookup", referenceFieldId, lookupFieldId }`, always optional and read-only |
|
|
120
123
|
| `image`, `file`, `multipleImage`, `multipleFile` | nothing — the consumer fills the storage path from its own settings |
|
|
121
124
|
|
|
125
|
+
A `reference` or `listReference` column has a solid line under its header, and a `lookup` column (or a column drawn from one, carrying `lookupFieldId`) a dashed one, both in the primary colour.
|
|
126
|
+
|
|
122
127
|
A **Required** toggle sets the field's `optional` flag. New columns default to
|
|
123
128
|
optional, since the rows already in the table have no value for one the moment it
|
|
124
129
|
is added — ticking it says so.
|
|
@@ -206,10 +211,10 @@ With `editable.active` most columns edit in the cell (`supportedInlineEditList`)
|
|
|
206
211
|
|
|
207
212
|
`groups` draws rows as coloured, collapsible bands inside the one table. DataTemplate builds them from `organise.groupBy`; a consumer using NewTable on its own passes them directly.
|
|
208
213
|
|
|
209
|
-
| Prop | Type
|
|
210
|
-
| -------------------- |
|
|
211
|
-
| `groups` | `{ id, title, colour?, rowIds, count? }[]`
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
215
|
-
|
|
214
|
+
| Prop | Type | Behavior |
|
|
215
|
+
| -------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
216
|
+
| `groups` | `{ id, title, colour?, rowIds, count? }[]` | One band per entry; `rowIds` reference row ids. `count` is the band's real total, shown in its header instead of the rows it holds |
|
|
217
|
+
| `groupColumnKey` | `string` | The column the groups are built from. Phone rows leave it off, since the group header shows it. Group headers show on a phone while their rows load |
|
|
218
|
+
| `onGroupLoadMore` | `(groupId: string) => void` | Scroll paging: fired when the bottom of a band scrolls into view, so that band loads its next rows |
|
|
219
|
+
| `onGroupPageChange` | `(groupId: string, page: number) => void` | Page paging: each band gets the list footer (numbered pages, chevrons, go-to-page) under its rows, scoped to that band. 0-based page |
|
|
220
|
+
| `groupLoadingStatus` | `{ [groupId]: { loadingMore?, exhausted?, page?, size?, fetched? } }` | Per band: a spinner while a page loads; `exhausted` removes the scroll sentinel; `page` and `size` drive the band pager once `fetched` |
|