blixify-ui-web-mcp 0.1.26 → 0.1.28

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.
@@ -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";