blixify-ui-web-mcp 0.1.26 → 0.1.27
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/data/dataTemplate/README.md +23 -3
- package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +33 -28
- package/lib/components/data/dataWrapper/dataWrapper.utils.stories.tsx +852 -0
- package/lib/components/data/dataWrapper/dataWrapper.view.stories.tsx +107 -81
- package/lib/components/data/flexFilter/flexFilter.stories.tsx +223 -0
- package/lib/components/display/modal/modal.stories.tsx +8 -5
- package/package.json +1 -1
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
// INFO: DTW plumbing — flex filters, hidden columns, persisted view state and
|
|
2
|
+
// SSE live updates.
|
|
3
|
+
|
|
4
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
5
|
+
import { Button } from "../../action/button";
|
|
6
|
+
import { Loading } from "../../display/loading";
|
|
7
|
+
import { FlexFilterData } from "../flexFilter/model";
|
|
8
|
+
import { DataTemplateWrapper } from "./index";
|
|
9
|
+
import {
|
|
10
|
+
STORY_LIVE_ENDPOINT,
|
|
11
|
+
STORY_NAMES,
|
|
12
|
+
handleBuildStoryRecords,
|
|
13
|
+
handleEmitStoryEnvelope,
|
|
14
|
+
handleInstallStoryLiveStream,
|
|
15
|
+
storyWorkspace,
|
|
16
|
+
useStoryBackend,
|
|
17
|
+
} from "./storyBackend.fixtures";
|
|
18
|
+
import { Tabs } from "../../navigation/tabs";
|
|
19
|
+
import { Tag } from "../../display/tag";
|
|
20
|
+
|
|
21
|
+
const meta = {
|
|
22
|
+
title: "Data/Data Wrapper/Utils",
|
|
23
|
+
decorators: [
|
|
24
|
+
(Story: () => React.ReactElement) => (
|
|
25
|
+
<div className="bg-white min-h-screen p-4">
|
|
26
|
+
<Story />
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
|
|
34
|
+
// INFO: SSE live updates, served by the story backend's stand-in EventSource.
|
|
35
|
+
// bareSocketName="dtw-test-collection" → list room.
|
|
36
|
+
// bareSocketName="dtw-test-collection-{id}" → document room (wired automatically
|
|
37
|
+
// when the consumer passes the room in bareSettings on the read/update DTW).
|
|
38
|
+
//
|
|
39
|
+
// Expected behaviour:
|
|
40
|
+
// List view — insert prepends a row; update patches in-place; delete removes.
|
|
41
|
+
// Read view — update pushes new data into the form; delete navigates back to list.
|
|
42
|
+
// isLive indicator (green dot) appears when EventSource connects.
|
|
43
|
+
// isLiveOffline indicator (yellow dot) appears when navigator goes offline.
|
|
44
|
+
const DTW_SSE_COLLECTION = "dtw-test-collection";
|
|
45
|
+
|
|
46
|
+
const DTWSseLiveSchema = [
|
|
47
|
+
{ id: "clientName", name: "Client Name", type: "string" as const },
|
|
48
|
+
{
|
|
49
|
+
id: "status",
|
|
50
|
+
name: "Status",
|
|
51
|
+
type: "list" as const,
|
|
52
|
+
listSelection: ["Draft", "Active", "Closed"],
|
|
53
|
+
optional: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "createdAt",
|
|
57
|
+
name: "Created At",
|
|
58
|
+
type: "date" as const,
|
|
59
|
+
optional: true,
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const DTW_SSE_STATUSES = ["Draft", "Active", "Closed"];
|
|
64
|
+
|
|
65
|
+
const handleBuildSseRecord = (index: number) => ({
|
|
66
|
+
_id: `live-${index}`,
|
|
67
|
+
clientName: STORY_NAMES[index % STORY_NAMES.length],
|
|
68
|
+
status: DTW_SSE_STATUSES[index % DTW_SSE_STATUSES.length],
|
|
69
|
+
createdAt: new Date(Date.now() - index * 3_600_000).toISOString(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const DTWSseLive = () => {
|
|
73
|
+
const dataRefreshRef: any = useRef(null);
|
|
74
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
75
|
+
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
76
|
+
const storeRef = useRef<any[]>(
|
|
77
|
+
handleBuildStoryRecords(8, handleBuildSseRecord),
|
|
78
|
+
);
|
|
79
|
+
const nextIndexRef = useRef<number>(9);
|
|
80
|
+
const mount = useStoryBackend({
|
|
81
|
+
collections: { [DTW_SSE_COLLECTION]: storeRef.current },
|
|
82
|
+
live: true,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
useEffect(() => handleInstallStoryLiveStream(), []);
|
|
86
|
+
|
|
87
|
+
// INFO: a change made by "someone else" — written straight to the store and
|
|
88
|
+
// pushed down the live stream, never through this DTW
|
|
89
|
+
const handleSimulateRemoteChange = (type: "insert" | "update" | "delete") => {
|
|
90
|
+
const store = storeRef.current;
|
|
91
|
+
if (type === "insert") {
|
|
92
|
+
const record = handleBuildSseRecord(nextIndexRef.current++);
|
|
93
|
+
record.createdAt = new Date().toISOString();
|
|
94
|
+
store.unshift(record);
|
|
95
|
+
handleEmitStoryEnvelope(DTW_SSE_COLLECTION, "insert", record);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const target = store[0];
|
|
99
|
+
if (!target) return;
|
|
100
|
+
if (type === "update") {
|
|
101
|
+
const status =
|
|
102
|
+
DTW_SSE_STATUSES[
|
|
103
|
+
(DTW_SSE_STATUSES.indexOf(target.status) + 1) %
|
|
104
|
+
DTW_SSE_STATUSES.length
|
|
105
|
+
];
|
|
106
|
+
store[0] = { ...target, status };
|
|
107
|
+
handleEmitStoryEnvelope(DTW_SSE_COLLECTION, "update", store[0]);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
store.splice(0, 1);
|
|
111
|
+
handleEmitStoryEnvelope(DTW_SSE_COLLECTION, "delete", { _id: target._id });
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleNavigate = (type: any, id: any) => {
|
|
115
|
+
setViewType(type);
|
|
116
|
+
setSelectedId(id);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
if (!mount) return <Loading />;
|
|
120
|
+
|
|
121
|
+
// INFO: room name follows SocketWrapper convention —
|
|
122
|
+
// list view → "{collection}"
|
|
123
|
+
// read/update view → "{collection}-{id}"
|
|
124
|
+
const bareSocketName =
|
|
125
|
+
viewType === "list"
|
|
126
|
+
? DTW_SSE_COLLECTION
|
|
127
|
+
: `${DTW_SSE_COLLECTION}-${selectedId}`;
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="w-full">
|
|
131
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
132
|
+
DTW — SSE Live Updates
|
|
133
|
+
</div>
|
|
134
|
+
<div className="text-sm font-light mb-4 text-gray-500">
|
|
135
|
+
<code>bareSocketName</code> + <code>workspace.sseEndpoint</code>. The
|
|
136
|
+
green dot in the table header appears when the stream connects. The
|
|
137
|
+
buttons below make a change as another user would — it arrives without a
|
|
138
|
+
refresh. Edits made here are pushed to the stream too.
|
|
139
|
+
</div>
|
|
140
|
+
<div className="mb-4 flex flex-row gap-x-2">
|
|
141
|
+
<Button
|
|
142
|
+
type="light"
|
|
143
|
+
size="small"
|
|
144
|
+
text="Someone adds a record"
|
|
145
|
+
onClick={() => handleSimulateRemoteChange("insert")}
|
|
146
|
+
/>
|
|
147
|
+
<Button
|
|
148
|
+
type="light"
|
|
149
|
+
size="small"
|
|
150
|
+
text="Someone updates the first"
|
|
151
|
+
onClick={() => handleSimulateRemoteChange("update")}
|
|
152
|
+
/>
|
|
153
|
+
<Button
|
|
154
|
+
type="light"
|
|
155
|
+
size="small"
|
|
156
|
+
text="Someone deletes the first"
|
|
157
|
+
onClick={() => handleSimulateRemoteChange("delete")}
|
|
158
|
+
/>
|
|
159
|
+
</div>
|
|
160
|
+
<DataTemplateWrapper
|
|
161
|
+
ref={dataRefreshRef}
|
|
162
|
+
title="Live Collection"
|
|
163
|
+
type={viewType}
|
|
164
|
+
id={selectedId}
|
|
165
|
+
collectionId={DTW_SSE_COLLECTION}
|
|
166
|
+
schema={DTWSseLiveSchema}
|
|
167
|
+
workspace={{
|
|
168
|
+
...storyWorkspace(),
|
|
169
|
+
// INFO: sseEndpoint flows through parseWorkspaceSettings →
|
|
170
|
+
// devSettings.sseEndpoint → buildSSEUrl → EventSource
|
|
171
|
+
sseEndpoint: STORY_LIVE_ENDPOINT,
|
|
172
|
+
}}
|
|
173
|
+
edit={{
|
|
174
|
+
allEditable: true,
|
|
175
|
+
disabledUpdate: false,
|
|
176
|
+
disableBatchUpdate: false,
|
|
177
|
+
deleteBtn: true,
|
|
178
|
+
}}
|
|
179
|
+
bareSettings={{
|
|
180
|
+
bareCompactMode: true,
|
|
181
|
+
bareNewDTW: true,
|
|
182
|
+
// INFO: room switches between list room and document room depending on view type
|
|
183
|
+
bareSocketName,
|
|
184
|
+
bareUpdateActionComponent:
|
|
185
|
+
viewType === "read"
|
|
186
|
+
? (data) => (
|
|
187
|
+
<Button
|
|
188
|
+
type="normal"
|
|
189
|
+
text="Update"
|
|
190
|
+
onClick={() => {
|
|
191
|
+
setSelectedId(data._id);
|
|
192
|
+
setViewType("update");
|
|
193
|
+
}}
|
|
194
|
+
/>
|
|
195
|
+
)
|
|
196
|
+
: undefined,
|
|
197
|
+
}}
|
|
198
|
+
organise={{
|
|
199
|
+
sort: { sortId: "createdAt", type: "desc" as const },
|
|
200
|
+
breadcrumbType: "button",
|
|
201
|
+
}}
|
|
202
|
+
handleNavigate={handleNavigate}
|
|
203
|
+
limit={10}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
DTWSseLive.storyName = "DTW SSE Live";
|
|
209
|
+
|
|
210
|
+
const DEMO_JOB_COLLECTION = "demo-field-jobs";
|
|
211
|
+
const DEMO_JOB_STATUSES = [
|
|
212
|
+
"Not Completed",
|
|
213
|
+
"In Progress",
|
|
214
|
+
"Completed",
|
|
215
|
+
"Cancelled",
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const DemoJobSchema = [
|
|
219
|
+
{ id: "title", name: "Title", type: "string" as const },
|
|
220
|
+
{ id: "verified", name: "Verified", type: "boolean" as const },
|
|
221
|
+
{
|
|
222
|
+
id: "status",
|
|
223
|
+
name: "Status",
|
|
224
|
+
type: "list" as const,
|
|
225
|
+
listSelection: DEMO_JOB_STATUSES,
|
|
226
|
+
listSelectionColour: ["red", "yellow", "green", "dark"],
|
|
227
|
+
},
|
|
228
|
+
{ id: "startDate", name: "Clock In", type: "date" as const },
|
|
229
|
+
{ id: "endDate", name: "Clock Out", type: "date" as const },
|
|
230
|
+
{ id: "operationDay", name: "Operation Day", type: "number" as const },
|
|
231
|
+
{ id: "crew", name: "Crew Lead", type: "string" as const },
|
|
232
|
+
{ id: "hoursWorked", name: "Hours Worked", type: "number" as const },
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// INFO: incidentData_count stands in for the count the server's bareListPipeline
|
|
236
|
+
// lookup adds to each row
|
|
237
|
+
const handleBuildJobRecord = (index: number) => {
|
|
238
|
+
const start = new Date(Date.now() - index * 86_400_000);
|
|
239
|
+
return {
|
|
240
|
+
_id: `job-${index}`,
|
|
241
|
+
title: `Job ${String(index).padStart(3, "0")}`,
|
|
242
|
+
verified: index % 3 !== 0,
|
|
243
|
+
status: DEMO_JOB_STATUSES[index % DEMO_JOB_STATUSES.length],
|
|
244
|
+
startDate: start.toISOString(),
|
|
245
|
+
endDate: new Date(start.getTime() + 6 * 3_600_000).toISOString(),
|
|
246
|
+
operationDay: (index % 4) + 1,
|
|
247
|
+
crew: STORY_NAMES[index % STORY_NAMES.length],
|
|
248
|
+
hoursWorked: 4 + (index % 5),
|
|
249
|
+
incidentData_count: index % 5 === 0 ? (index % 3) + 1 : 0,
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
export const DTWFlexFilters = () => {
|
|
254
|
+
const mount = useStoryBackend({
|
|
255
|
+
collections: {
|
|
256
|
+
[DEMO_JOB_COLLECTION]: handleBuildStoryRecords(36, handleBuildJobRecord),
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
const dataRef = useRef<any>(null);
|
|
260
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
261
|
+
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
262
|
+
const [listMobileView, setListMobileView] = useState(false);
|
|
263
|
+
// INFO: live applied flex filters, fed by bareOnFlexFiltersChange. Card active
|
|
264
|
+
// state is DERIVED from this rather than click history, so a card highlights
|
|
265
|
+
// only while its filter is actually applied — and de-highlights on its own if
|
|
266
|
+
// the user edits or clears that row in the flex filter panel.
|
|
267
|
+
const [liveFilters, setLiveFilters] = useState<FlexFilterData[]>([]);
|
|
268
|
+
|
|
269
|
+
// INFO: set mobile view when browser width is smaller than 640px
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
const handleResize = () => {
|
|
272
|
+
setListMobileView(window.innerWidth < 640);
|
|
273
|
+
};
|
|
274
|
+
handleResize();
|
|
275
|
+
window.addEventListener("resize", handleResize);
|
|
276
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
const handleNavigate = (type: any, id: any) => {
|
|
280
|
+
setViewType(type);
|
|
281
|
+
setSelectedId(id);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
if (!mount) return <Loading />;
|
|
285
|
+
|
|
286
|
+
// INFO: two mutually-exclusive cards on DIFFERENT columns. Clicking a card
|
|
287
|
+
// applies its filter; clicking a different card switches to it even if the
|
|
288
|
+
// previous card was never unclicked. Since the cards target different columns
|
|
289
|
+
// this switch clears one column and applies another in the SAME tick — the
|
|
290
|
+
// exact clear+apply sequence the ref-backed commit keeps correct within a
|
|
291
|
+
// tick. Requires bareSettings.bareFlexFilters.
|
|
292
|
+
const quickFilterCards: {
|
|
293
|
+
label: string;
|
|
294
|
+
description: string;
|
|
295
|
+
columnIds: string[];
|
|
296
|
+
filters: FlexFilterData[];
|
|
297
|
+
}[] = [
|
|
298
|
+
{
|
|
299
|
+
label: "Completed cycles",
|
|
300
|
+
description: "status = Completed",
|
|
301
|
+
columnIds: ["status"],
|
|
302
|
+
filters: [
|
|
303
|
+
{
|
|
304
|
+
id: "card-status",
|
|
305
|
+
columnId: "status",
|
|
306
|
+
operator: "=",
|
|
307
|
+
value: "Completed",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
label: "Verified only",
|
|
313
|
+
description: "verified = true",
|
|
314
|
+
columnIds: ["verified"],
|
|
315
|
+
filters: [
|
|
316
|
+
{
|
|
317
|
+
id: "card-verified",
|
|
318
|
+
columnId: "verified",
|
|
319
|
+
operator: "=",
|
|
320
|
+
value: "true",
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
// INFO: filters on a bareListPipeline-derived column (incidentData_count).
|
|
326
|
+
// value 0 is falsy, so this also exercises the badge fix — the old
|
|
327
|
+
// truthiness count would have skipped it even though it filters.
|
|
328
|
+
label: "Has incidents",
|
|
329
|
+
description: "incident count > 0",
|
|
330
|
+
columnIds: ["incidentData_count"],
|
|
331
|
+
filters: [
|
|
332
|
+
{
|
|
333
|
+
id: "card-incidents",
|
|
334
|
+
columnId: "incidentData_count",
|
|
335
|
+
operator: ">",
|
|
336
|
+
value: 0,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
];
|
|
341
|
+
|
|
342
|
+
// INFO: a card is active only when every one of its filters is actually
|
|
343
|
+
// present in the live applied filters — matched by column, operator and value
|
|
344
|
+
// (value compared as string since the panel stores e.g. boolean as "true").
|
|
345
|
+
const handleIsCardActive = (card: (typeof quickFilterCards)[number]) =>
|
|
346
|
+
card.filters.every((cardFilter) =>
|
|
347
|
+
liveFilters.some(
|
|
348
|
+
(live) =>
|
|
349
|
+
live.columnId === cardFilter.columnId &&
|
|
350
|
+
live.operator === cardFilter.operator &&
|
|
351
|
+
String(live.value) === String(cardFilter.value),
|
|
352
|
+
),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// INFO: mutually exclusive — clicking the active card clears it; clicking any
|
|
356
|
+
// other card clears every card column then applies the clicked one, so only
|
|
357
|
+
// one card's filter is ever applied even if the previous was never unclicked.
|
|
358
|
+
const handleToggleCardFilter = (card: (typeof quickFilterCards)[number]) => {
|
|
359
|
+
if (handleIsCardActive(card)) {
|
|
360
|
+
dataRef.current?.handleClearFlexFilters(card.columnIds);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const allCardColumnIds = quickFilterCards.flatMap((c) => c.columnIds);
|
|
364
|
+
dataRef.current?.handleClearFlexFilters(allCardColumnIds);
|
|
365
|
+
dataRef.current?.handleApplyFlexFilters(card.filters);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<div className="w-full min-h-screen">
|
|
370
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
371
|
+
DTW - Card-Driven Flex Filters
|
|
372
|
+
</div>
|
|
373
|
+
<div className="text-sm font-light mb-4 text-gray-500">
|
|
374
|
+
Mutually-exclusive cards on different columns. Clicking a card applies
|
|
375
|
+
its filter via <code>ref.handleApplyFlexFilters</code>; clicking another
|
|
376
|
+
switches to it — clearing the first column and applying the second in
|
|
377
|
+
one go — even if the first was never unclicked. Click the active card
|
|
378
|
+
again to clear it. The "Has incidents" card filters a{" "}
|
|
379
|
+
<code>bareListPipeline</code> column with value <code>0</code>, so the
|
|
380
|
+
filter badge counts it correctly. Seeded rows show up in the flex filter
|
|
381
|
+
panel and stay clearable by the user.
|
|
382
|
+
</div>
|
|
383
|
+
<div className="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
|
384
|
+
{quickFilterCards.map((card) => {
|
|
385
|
+
const isActive = handleIsCardActive(card);
|
|
386
|
+
return (
|
|
387
|
+
<button
|
|
388
|
+
key={card.label}
|
|
389
|
+
type="button"
|
|
390
|
+
onClick={() => handleToggleCardFilter(card)}
|
|
391
|
+
className={`rounded-lg border p-4 text-left text-sm transition-colors ${
|
|
392
|
+
isActive
|
|
393
|
+
? "border-primary-500 bg-primary-50"
|
|
394
|
+
: "border-gray-200 hover:bg-primary-50"
|
|
395
|
+
}`}
|
|
396
|
+
>
|
|
397
|
+
<p className="font-medium text-gray-900">{card.label}</p>
|
|
398
|
+
<p className="mt-1 text-xs text-gray-500">{card.description}</p>
|
|
399
|
+
{isActive && (
|
|
400
|
+
<p className="mt-2 text-xs font-medium text-primary-600">
|
|
401
|
+
Applied — click to clear
|
|
402
|
+
</p>
|
|
403
|
+
)}
|
|
404
|
+
</button>
|
|
405
|
+
);
|
|
406
|
+
})}
|
|
407
|
+
</div>
|
|
408
|
+
<DataTemplateWrapper
|
|
409
|
+
ref={dataRef}
|
|
410
|
+
title="Flex Filters"
|
|
411
|
+
description=""
|
|
412
|
+
type={viewType}
|
|
413
|
+
id={selectedId}
|
|
414
|
+
collectionId={DEMO_JOB_COLLECTION}
|
|
415
|
+
schema={DemoJobSchema}
|
|
416
|
+
workspace={storyWorkspace()}
|
|
417
|
+
edit={{
|
|
418
|
+
allEditable: true,
|
|
419
|
+
disabledUpdate: false,
|
|
420
|
+
disableBatchUpdate: false,
|
|
421
|
+
deleteBtn: true,
|
|
422
|
+
}}
|
|
423
|
+
organise={{
|
|
424
|
+
breadcrumbType: "button",
|
|
425
|
+
sort: { sortId: "endDate", type: "desc" },
|
|
426
|
+
}}
|
|
427
|
+
bareSettings={{
|
|
428
|
+
bareCompactMode: true,
|
|
429
|
+
bareNewDTW: true,
|
|
430
|
+
bareListMobileView: listMobileView,
|
|
431
|
+
bareFlexFilters: true,
|
|
432
|
+
bareOnFlexFiltersChange: setLiveFilters,
|
|
433
|
+
bareListMobileFloatingSearch: true,
|
|
434
|
+
bareListTablePinColumns: { left: ["title"] },
|
|
435
|
+
bareListPipeline: [
|
|
436
|
+
{
|
|
437
|
+
spaceReferenceId: "demo-incidents",
|
|
438
|
+
spaceReferenceAs: "incidentData",
|
|
439
|
+
condition: [
|
|
440
|
+
{ type: "=", fromColumnId: "_id", toColumnId: "jobId" },
|
|
441
|
+
],
|
|
442
|
+
referenceSpaceLabel: [],
|
|
443
|
+
referenceQuery: [{ type: "=", queryId: "crashed", value: false }],
|
|
444
|
+
referenceShowCount: true,
|
|
445
|
+
},
|
|
446
|
+
],
|
|
447
|
+
bareListDescription: (key, value) => {
|
|
448
|
+
if (key === "incidentData_count") {
|
|
449
|
+
if (value) {
|
|
450
|
+
return <Tag tagText={value} size="small" color="red" />;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return undefined;
|
|
454
|
+
},
|
|
455
|
+
}}
|
|
456
|
+
handleNavigate={handleNavigate}
|
|
457
|
+
limit={10}
|
|
458
|
+
/>
|
|
459
|
+
</div>
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
DTWFlexFilters.storyName = "DTW Flex Filters";
|
|
463
|
+
|
|
464
|
+
// INFO: Story used by the hide-columns e2e block, which answers its requests
|
|
465
|
+
// with page.route under the dtw-test-collection alias. It adds one
|
|
466
|
+
// developer-hidden column to prove users can never toggle it back on.
|
|
467
|
+
const DataWrapperHideColumnSchema = [
|
|
468
|
+
{
|
|
469
|
+
id: "clientName",
|
|
470
|
+
name: "Client Name",
|
|
471
|
+
type: "string" as const,
|
|
472
|
+
optional: true,
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
id: "clientPhone",
|
|
476
|
+
name: "Client Phone",
|
|
477
|
+
type: "string" as const,
|
|
478
|
+
optional: true,
|
|
479
|
+
},
|
|
480
|
+
{ id: "address", name: "Address", type: "string" as const, optional: true },
|
|
481
|
+
{
|
|
482
|
+
id: "internalRef",
|
|
483
|
+
name: "Internal Ref",
|
|
484
|
+
type: "string" as const,
|
|
485
|
+
optional: true,
|
|
486
|
+
hide: true,
|
|
487
|
+
},
|
|
488
|
+
];
|
|
489
|
+
|
|
490
|
+
// INFO: a deliberately wide schema — the view settings panel has to stay usable
|
|
491
|
+
// on a collection with dozens of fields (Space runs to 24+), so the column list
|
|
492
|
+
// scrolls and the search box earns its place
|
|
493
|
+
const DataWrapperWideSchema = [
|
|
494
|
+
...DataWrapperHideColumnSchema,
|
|
495
|
+
{ id: "email", name: "Email", type: "email" as const, optional: true },
|
|
496
|
+
{ id: "city", name: "City", type: "string" as const, optional: true },
|
|
497
|
+
{ id: "state", name: "State", type: "string" as const, optional: true },
|
|
498
|
+
{ id: "postcode", name: "Postcode", type: "string" as const, optional: true },
|
|
499
|
+
{ id: "country", name: "Country", type: "string" as const, optional: true },
|
|
500
|
+
{
|
|
501
|
+
id: "status",
|
|
502
|
+
name: "Status",
|
|
503
|
+
type: "list" as const,
|
|
504
|
+
listSelection: ["New", "Active", "On Hold", "Closed"],
|
|
505
|
+
listSelectionColour: ["blue", "green", "yellow", "red"],
|
|
506
|
+
optional: true,
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
id: "priority",
|
|
510
|
+
name: "Priority",
|
|
511
|
+
type: "list" as const,
|
|
512
|
+
listSelection: ["Low", "Medium", "High"],
|
|
513
|
+
listSelectionColour: ["green", "yellow", "red"],
|
|
514
|
+
optional: true,
|
|
515
|
+
},
|
|
516
|
+
{ id: "owner", name: "Owner", type: "string" as const, optional: true },
|
|
517
|
+
{ id: "team", name: "Team", type: "string" as const, optional: true },
|
|
518
|
+
{ id: "source", name: "Source", type: "string" as const, optional: true },
|
|
519
|
+
{
|
|
520
|
+
id: "contractValue",
|
|
521
|
+
name: "Contract Value",
|
|
522
|
+
type: "number" as const,
|
|
523
|
+
optional: true,
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
id: "outstanding",
|
|
527
|
+
name: "Outstanding Amount",
|
|
528
|
+
type: "number" as const,
|
|
529
|
+
optional: true,
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
id: "startDate",
|
|
533
|
+
name: "Start Date",
|
|
534
|
+
type: "date" as const,
|
|
535
|
+
optional: true,
|
|
536
|
+
},
|
|
537
|
+
{ id: "endDate", name: "End Date", type: "date" as const, optional: true },
|
|
538
|
+
{
|
|
539
|
+
id: "lastContact",
|
|
540
|
+
name: "Last Contact",
|
|
541
|
+
type: "date" as const,
|
|
542
|
+
optional: true,
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: "followUpDate",
|
|
546
|
+
name: "Follow Up Date",
|
|
547
|
+
type: "date" as const,
|
|
548
|
+
optional: true,
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
id: "verified",
|
|
552
|
+
name: "Verified",
|
|
553
|
+
type: "boolean" as const,
|
|
554
|
+
optional: true,
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
id: "subscribed",
|
|
558
|
+
name: "Subscribed",
|
|
559
|
+
type: "boolean" as const,
|
|
560
|
+
optional: true,
|
|
561
|
+
},
|
|
562
|
+
{ id: "remarks", name: "Remarks", type: "textArea" as const, optional: true },
|
|
563
|
+
{
|
|
564
|
+
id: "internalNotes",
|
|
565
|
+
name: "Internal Notes",
|
|
566
|
+
type: "textArea" as const,
|
|
567
|
+
optional: true,
|
|
568
|
+
},
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
const handleBuildContactRecord = (index: number) => ({
|
|
572
|
+
_id: `contact-${index}`,
|
|
573
|
+
clientName: STORY_NAMES[index % STORY_NAMES.length],
|
|
574
|
+
clientPhone: `01${2000000 + index * 1379}`,
|
|
575
|
+
address: `${index} Jalan Contoh, 50450 Kuala Lumpur`,
|
|
576
|
+
internalRef: `REF-${1000 + index}`,
|
|
577
|
+
email: `contact${index}@example.com`,
|
|
578
|
+
city: ["Kuala Lumpur", "Penang", "Johor Bahru", "Ipoh"][index % 4],
|
|
579
|
+
state: ["Selangor", "Penang", "Johor", "Perak"][index % 4],
|
|
580
|
+
postcode: String(50000 + index * 17),
|
|
581
|
+
country: "Malaysia",
|
|
582
|
+
status: ["New", "Active", "On Hold", "Closed"][index % 4],
|
|
583
|
+
priority: ["Low", "Medium", "High"][index % 3],
|
|
584
|
+
owner: STORY_NAMES[(index + 3) % STORY_NAMES.length],
|
|
585
|
+
team: ["North", "South", "Central"][index % 3],
|
|
586
|
+
source: ["Referral", "Website", "Event"][index % 3],
|
|
587
|
+
contractValue: 5000 + index * 750,
|
|
588
|
+
outstanding: (index % 4) * 1250,
|
|
589
|
+
startDate: new Date(Date.now() - index * 86_400_000 * 7).toISOString(),
|
|
590
|
+
endDate: new Date(Date.now() + index * 86_400_000 * 30).toISOString(),
|
|
591
|
+
lastContact: new Date(Date.now() - index * 86_400_000).toISOString(),
|
|
592
|
+
followUpDate: new Date(Date.now() + index * 86_400_000 * 2).toISOString(),
|
|
593
|
+
verified: index % 2 === 0,
|
|
594
|
+
subscribed: index % 3 === 0,
|
|
595
|
+
remarks: index % 4 === 0 ? "Prefers a call in the morning." : "",
|
|
596
|
+
internalNotes: "",
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
export const DataWrapperHideColumnTest = () => {
|
|
600
|
+
const mount = useStoryBackend({
|
|
601
|
+
collections: {
|
|
602
|
+
"dtw-test-collection": handleBuildStoryRecords(
|
|
603
|
+
24,
|
|
604
|
+
handleBuildContactRecord,
|
|
605
|
+
),
|
|
606
|
+
},
|
|
607
|
+
yieldToE2E: true,
|
|
608
|
+
});
|
|
609
|
+
const dataRefreshRef: any = useRef(null);
|
|
610
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
611
|
+
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
612
|
+
|
|
613
|
+
const handleNavigate = (type: any, id: any) => {
|
|
614
|
+
setViewType(type);
|
|
615
|
+
setSelectedId(id);
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
if (!mount) {
|
|
619
|
+
return <Loading />;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<div className="w-full">
|
|
624
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
625
|
+
DTW Hide Column Test
|
|
626
|
+
</div>
|
|
627
|
+
<div className="text-sm font-light mb-4 text-gray-500">
|
|
628
|
+
Exercises bareSettings.bareViewSettings — the client-only column hiding
|
|
629
|
+
control for bare DTWs. Internal Ref is hidden by the schema and must
|
|
630
|
+
never appear in the toggle list.
|
|
631
|
+
</div>
|
|
632
|
+
<DataTemplateWrapper
|
|
633
|
+
ref={dataRefreshRef}
|
|
634
|
+
title="Checklist"
|
|
635
|
+
type={viewType}
|
|
636
|
+
id={selectedId}
|
|
637
|
+
collectionId={"dtw-test-collection"}
|
|
638
|
+
schema={DataWrapperHideColumnSchema}
|
|
639
|
+
workspace={storyWorkspace()}
|
|
640
|
+
edit={{
|
|
641
|
+
allEditable: true,
|
|
642
|
+
disabledUpdate: false,
|
|
643
|
+
disableBatchUpdate: false,
|
|
644
|
+
deleteBtn: true,
|
|
645
|
+
}}
|
|
646
|
+
bareSettings={{
|
|
647
|
+
bareCompactMode: true,
|
|
648
|
+
bareNewDTW: true,
|
|
649
|
+
bareOffline: false,
|
|
650
|
+
bareViewSettings: true,
|
|
651
|
+
bareFlexFilters: true,
|
|
652
|
+
}}
|
|
653
|
+
organise={{
|
|
654
|
+
breadcrumbType: "button",
|
|
655
|
+
persistViewState: "session",
|
|
656
|
+
}}
|
|
657
|
+
handleNavigate={handleNavigate}
|
|
658
|
+
limit={10}
|
|
659
|
+
/>
|
|
660
|
+
</div>
|
|
661
|
+
);
|
|
662
|
+
};
|
|
663
|
+
DataWrapperHideColumnTest.storyName = "DTW Hide Column Test";
|
|
664
|
+
|
|
665
|
+
export const DTWPersistView = () => {
|
|
666
|
+
const mount = useStoryBackend({
|
|
667
|
+
collections: {
|
|
668
|
+
[DEMO_JOB_COLLECTION]: handleBuildStoryRecords(36, handleBuildJobRecord),
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
const dataRef = useRef<any>(null);
|
|
672
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
673
|
+
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
674
|
+
const [selectedTab, setSelectedTab] = useState("a");
|
|
675
|
+
|
|
676
|
+
const handleNavigate = (type: any, id: any) => {
|
|
677
|
+
setViewType(type);
|
|
678
|
+
setSelectedId(id);
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
if (!mount) return <Loading />;
|
|
682
|
+
|
|
683
|
+
return (
|
|
684
|
+
<div className="w-full min-h-screen">
|
|
685
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
686
|
+
DTW - Persisted View
|
|
687
|
+
</div>
|
|
688
|
+
<p className="text-sm text-gray-500">
|
|
689
|
+
Persist filters, sort, page index, searched value and user hidden
|
|
690
|
+
columns in session storage / local storage
|
|
691
|
+
</p>
|
|
692
|
+
<Tabs
|
|
693
|
+
tabs={[
|
|
694
|
+
{ id: "a", name: "Tab A", href: "#" },
|
|
695
|
+
{ id: "b", name: "Tab B", href: "#" },
|
|
696
|
+
]}
|
|
697
|
+
selectedId={selectedTab}
|
|
698
|
+
onClick={(id) => {
|
|
699
|
+
setSelectedTab(id);
|
|
700
|
+
}}
|
|
701
|
+
/>
|
|
702
|
+
<DataTemplateWrapper
|
|
703
|
+
// INFO: remount per tab so persisted view state hydrates from the
|
|
704
|
+
// matching persistKey — it is only read on mount
|
|
705
|
+
key={selectedTab}
|
|
706
|
+
ref={dataRef}
|
|
707
|
+
title="Persisted View"
|
|
708
|
+
description=""
|
|
709
|
+
type={viewType}
|
|
710
|
+
id={selectedId}
|
|
711
|
+
collectionId={DEMO_JOB_COLLECTION}
|
|
712
|
+
schema={DemoJobSchema}
|
|
713
|
+
workspace={storyWorkspace()}
|
|
714
|
+
edit={{
|
|
715
|
+
allEditable: true,
|
|
716
|
+
disabledUpdate: false,
|
|
717
|
+
disableBatchUpdate: false,
|
|
718
|
+
deleteBtn: true,
|
|
719
|
+
}}
|
|
720
|
+
organise={{
|
|
721
|
+
breadcrumbType: "button",
|
|
722
|
+
persistViewState: "session",
|
|
723
|
+
persistKey: selectedTab,
|
|
724
|
+
}}
|
|
725
|
+
bareSettings={{
|
|
726
|
+
bareCompactMode: true,
|
|
727
|
+
bareNewDTW: true,
|
|
728
|
+
bareViewSettings: true,
|
|
729
|
+
bareFlexFilters: true,
|
|
730
|
+
bareListMobileFloatingSearch: true,
|
|
731
|
+
bareListFilterQuery: [
|
|
732
|
+
{
|
|
733
|
+
type: "=",
|
|
734
|
+
queryId: "verified",
|
|
735
|
+
value: selectedTab === "a" ? true : false,
|
|
736
|
+
},
|
|
737
|
+
],
|
|
738
|
+
}}
|
|
739
|
+
handleNavigate={handleNavigate}
|
|
740
|
+
limit={10}
|
|
741
|
+
/>
|
|
742
|
+
</div>
|
|
743
|
+
);
|
|
744
|
+
};
|
|
745
|
+
DTWPersistView.storyName = "DTW Persisted View";
|
|
746
|
+
|
|
747
|
+
export const DataWrapperViewSettingsWide = () => {
|
|
748
|
+
const mount = useStoryBackend({
|
|
749
|
+
collections: {
|
|
750
|
+
"dtw-test-collection": handleBuildStoryRecords(
|
|
751
|
+
24,
|
|
752
|
+
handleBuildContactRecord,
|
|
753
|
+
),
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
const dataRefreshRef: any = useRef(null);
|
|
757
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
758
|
+
const [selectedId, setSelectedId] = useState<string | undefined>(undefined);
|
|
759
|
+
// INFO: the same panel behaves two ways — per user by default, or schema level
|
|
760
|
+
// once onChangeHide / onChangeDrag are wired to write the shared schema
|
|
761
|
+
const [schemaMode, setSchemaMode] = useState<boolean>(false);
|
|
762
|
+
const [model, setModel] = useState<any[]>(DataWrapperWideSchema);
|
|
763
|
+
const [lastChange, setLastChange] = useState<string>("none");
|
|
764
|
+
|
|
765
|
+
const handleNavigate = (type: any, id: any) => {
|
|
766
|
+
setViewType(type);
|
|
767
|
+
setSelectedId(id);
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const handleChangeHide = (updatedModel: any[]) => {
|
|
771
|
+
setModel(updatedModel);
|
|
772
|
+
setLastChange(
|
|
773
|
+
`hide → ${
|
|
774
|
+
updatedModel
|
|
775
|
+
.filter((eachColumn) => eachColumn.hide)
|
|
776
|
+
.map((eachColumn) => eachColumn.id)
|
|
777
|
+
.join(", ") || "(none)"
|
|
778
|
+
}`,
|
|
779
|
+
);
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const handleChangeDrag = (dragId: string, dropId: string) => {
|
|
783
|
+
setLastChange(`order → ${dragId} before ${dropId}`);
|
|
784
|
+
};
|
|
785
|
+
|
|
786
|
+
const handleToggleMode = () => {
|
|
787
|
+
setModel(DataWrapperWideSchema);
|
|
788
|
+
setLastChange("none");
|
|
789
|
+
setSchemaMode((previous) => !previous);
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
if (!mount) return <Loading />;
|
|
793
|
+
|
|
794
|
+
return (
|
|
795
|
+
<div className="w-full">
|
|
796
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
797
|
+
DTW View Settings — 23 columns
|
|
798
|
+
</div>
|
|
799
|
+
<div className="text-sm font-light mb-3 text-gray-500">
|
|
800
|
+
The shape a Space collection actually has. Open the sliders icon: the
|
|
801
|
+
column list scrolls with a shade on the cut edge, the search box narrows
|
|
802
|
+
it, the handle reorders, and Sort and Rows per page stay pinned below.
|
|
803
|
+
On a phone the same panel opens as a modal.
|
|
804
|
+
</div>
|
|
805
|
+
<div className="mb-4 flex flex-row items-center gap-x-3">
|
|
806
|
+
<Button
|
|
807
|
+
type={schemaMode ? "normal" : "light"}
|
|
808
|
+
size="small"
|
|
809
|
+
text={schemaMode ? "Schema mode" : "Per user mode"}
|
|
810
|
+
onClick={handleToggleMode}
|
|
811
|
+
/>
|
|
812
|
+
<span className="text-xs text-gray-500">
|
|
813
|
+
{schemaMode
|
|
814
|
+
? "onChangeHide / onChangeDrag are wired — the change is handed back to write to the schema for everyone"
|
|
815
|
+
: "no callbacks — the hide and the order stay in this user's persisted view state"}
|
|
816
|
+
</span>
|
|
817
|
+
</div>
|
|
818
|
+
{schemaMode && (
|
|
819
|
+
<div className="mb-4 text-xs text-primary-600">
|
|
820
|
+
last change: {lastChange}
|
|
821
|
+
</div>
|
|
822
|
+
)}
|
|
823
|
+
<DataTemplateWrapper
|
|
824
|
+
// INFO: a fresh instance per mode so the panel re-reads the model
|
|
825
|
+
key={schemaMode ? "schema" : "user"}
|
|
826
|
+
ref={dataRefreshRef}
|
|
827
|
+
title="Wide Collection"
|
|
828
|
+
type={viewType}
|
|
829
|
+
id={selectedId}
|
|
830
|
+
collectionId={"dtw-test-collection"}
|
|
831
|
+
schema={schemaMode ? model : DataWrapperWideSchema}
|
|
832
|
+
workspace={storyWorkspace()}
|
|
833
|
+
edit={{ allEditable: true, deleteBtn: true }}
|
|
834
|
+
bareSettings={{
|
|
835
|
+
bareCompactMode: true,
|
|
836
|
+
bareNewDTW: true,
|
|
837
|
+
bareFlexFilters: true,
|
|
838
|
+
}}
|
|
839
|
+
organise={{
|
|
840
|
+
breadcrumbType: "button",
|
|
841
|
+
persistViewState: "session",
|
|
842
|
+
...(schemaMode
|
|
843
|
+
? { onChangeHide: handleChangeHide, onChangeDrag: handleChangeDrag }
|
|
844
|
+
: {}),
|
|
845
|
+
}}
|
|
846
|
+
handleNavigate={handleNavigate}
|
|
847
|
+
limit={10}
|
|
848
|
+
/>
|
|
849
|
+
</div>
|
|
850
|
+
);
|
|
851
|
+
};
|
|
852
|
+
DataWrapperViewSettingsWide.storyName = "DTW View Settings";
|