blixify-ui-web-mcp 0.1.19 → 0.1.21

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.
Files changed (47) hide show
  1. package/lib/components/action/iconButton/iconButton.stories.tsx +11 -2
  2. package/lib/components/auth/signUp/signUp.stories.tsx +39 -1
  3. package/lib/components/chart/statsCard/README.md +24 -0
  4. package/lib/components/chart/statsCard/statsCard.stories.tsx +68 -0
  5. package/lib/components/chat/chatBase/chatBase.stories.tsx +1 -1
  6. package/lib/components/data/dataGraph/dataGraph.stories.tsx +5 -5
  7. package/lib/components/data/dataTemplate/README.md +204 -85
  8. package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +307 -295
  9. package/lib/components/data/dataTemplate/drive.stories.tsx +197 -0
  10. package/lib/components/data/dataWrapper/README.md +109 -22
  11. package/lib/components/data/dataWrapper/dataWrapper.bareHooks.stories.tsx +951 -0
  12. package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +356 -5
  13. package/lib/components/data/dataWrapper/dataWrapper.view.stories.tsx +79 -5
  14. package/lib/components/display/calendar/calendar.stories.tsx +1 -1
  15. package/lib/components/display/detailList/README.md +11 -10
  16. package/lib/components/display/detailList/detailList.stories.tsx +16 -3
  17. package/lib/components/display/feeds/README.md +3 -3
  18. package/lib/components/display/feeds/feeds.stories.tsx +3 -3
  19. package/lib/components/display/filePreview/filePreview.stories.tsx +111 -0
  20. package/lib/components/display/modal/README.md +3 -0
  21. package/lib/components/display/modal/modal.stories.tsx +50 -1
  22. package/lib/components/display/newCalendar/README.md +0 -6
  23. package/lib/components/display/newCalendar/newCalendar.stories.tsx +1 -1
  24. package/lib/components/display/newFeeds/README.md +1 -5
  25. package/lib/components/display/newFeeds/newFeeds.stories.tsx +3 -3
  26. package/lib/components/display/newKanban/README.md +0 -7
  27. package/lib/components/display/newList/README.md +1 -4
  28. package/lib/components/display/newTable/README.md +29 -9
  29. package/lib/components/display/newTable/newTable.stories.tsx +120 -9
  30. package/lib/components/display/tag/README.md +1 -1
  31. package/lib/components/display/tag/tag.stories.tsx +7 -0
  32. package/lib/components/input/markdown/markdown.stories.tsx +3 -1
  33. package/lib/components/input/uploadInput/uploadInput.stories.tsx +41 -0
  34. package/lib/components/navigation/leftNav/leftNav.stories.tsx +175 -0
  35. package/lib/components/navigation/tabs/tabs.stories.tsx +23 -0
  36. package/lib/components/template/calendarView/calendarView.stories.tsx +4 -4
  37. package/lib/components/template/chatDashboard/chatDashboard.stories.tsx +2 -2
  38. package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -1
  39. package/lib/mcp/tools/getBackendComponent.js +7 -3
  40. package/lib/mcp/tools/getBackendComponent.js.map +1 -1
  41. package/lib/mcp/tools/suggestTemplate.js +3 -3
  42. package/lib/mcp/tools/suggestTemplate.js.map +1 -1
  43. package/package.json +1 -1
  44. package/lib/components/data/dataTemplate/dataModule.stories.tsx +0 -191
  45. package/lib/components/data/dataWrapper/dataWrapper.consumer.stories.tsx +0 -2125
  46. package/lib/components/data/dataWrapper/dataWrapper.utils.stories.tsx +0 -576
  47. package/lib/components/data/flexFilter/flexFilter.stories.tsx +0 -171
@@ -0,0 +1,951 @@
1
+ import axios from "axios";
2
+ import MockAdapter from "axios-mock-adapter";
3
+ import React, { ReactElement, useMemo, useState } from "react";
4
+ import { Button } from "../../action/button";
5
+ import { ButtonGroup } from "../../action/buttonGroup";
6
+ import { Tag } from "../../display/tag";
7
+ import { DataStructure } from "../dataTemplate/model";
8
+ import { DataTemplateWrapper } from "./index";
9
+
10
+ /**
11
+ * INFO: a how-to for DataTemplateWrapper, one story per question someone
12
+ * actually asks — change the list, add under the read view, replace the read
13
+ * view, add under the update form, replace a field, replace the buttons, wire a
14
+ * full page, filter the list, create with fixed values, format a value,
15
+ * validate before save, and merge a second collection into the list.
16
+ *
17
+ * Each story opens straight on the view it is about and mocks its own API, so
18
+ * it works with no server. The descriptions state what each hook DOES, checked
19
+ * against DataTemplate's render code — several of these are easy to guess wrong
20
+ * from the prop name alone (bareUpdateComponent adds, it does not replace;
21
+ * bareUpdateActionComponent replaces, it does not add).
22
+ */
23
+ const meta = {
24
+ title: "Data/Data Wrapper/Bare Hooks",
25
+ parameters: { layout: "fullscreen" },
26
+ decorators: [
27
+ (Story: () => ReactElement) => (
28
+ <div className="bg-gray-100 min-h-screen p-4">
29
+ <Story />
30
+ </div>
31
+ ),
32
+ ],
33
+ };
34
+ export default meta;
35
+
36
+ const ENDPOINT = "https://bare-hooks.blixify.invalid/api/data";
37
+ const COLLECTION = "bare-orders";
38
+
39
+ const ORDER_SCHEMA: DataStructure[] = [
40
+ { id: "orderNo", name: "Order No", type: "string" },
41
+ { id: "customer", name: "Customer", type: "string" },
42
+ {
43
+ id: "status",
44
+ name: "Status",
45
+ type: "list",
46
+ listSelection: ["Draft", "Submitted", "Approved"],
47
+ },
48
+ { id: "amount", name: "Amount (RM)", type: "number" },
49
+ { id: "notes", name: "Notes", type: "textArea", optional: true },
50
+ ];
51
+
52
+ const handleSeedOrders = () => [
53
+ {
54
+ _id: "o1",
55
+ orderNo: "ORD-1001",
56
+ customer: "Northwind Trading",
57
+ status: "Submitted",
58
+ amount: 1250,
59
+ notes: "Deliver before Friday",
60
+ },
61
+ {
62
+ _id: "o2",
63
+ orderNo: "ORD-1002",
64
+ customer: "Contoso Supplies",
65
+ status: "Draft",
66
+ amount: 480,
67
+ },
68
+ {
69
+ _id: "o3",
70
+ orderNo: "ORD-1003",
71
+ customer: "Fabrikam Works",
72
+ status: "Approved",
73
+ amount: 3920,
74
+ },
75
+ {
76
+ _id: "o4",
77
+ orderNo: "ORD-1004",
78
+ customer: "Tailspin Toys",
79
+ status: "Submitted",
80
+ amount: 760,
81
+ },
82
+ ];
83
+
84
+ const RETURN_COLLECTION = "bare-returns";
85
+
86
+ const handleSeedReturns = () => [
87
+ {
88
+ _id: "r1",
89
+ orderNo: "RET-2001",
90
+ customer: "Northwind Trading",
91
+ status: "Submitted",
92
+ amount: 150,
93
+ },
94
+ {
95
+ _id: "r2",
96
+ orderNo: "RET-2002",
97
+ customer: "Fabrikam Works",
98
+ status: "Approved",
99
+ amount: 320,
100
+ },
101
+ ];
102
+
103
+ const handleClone = (value: any) =>
104
+ value === null || value === undefined
105
+ ? value
106
+ : JSON.parse(JSON.stringify(value));
107
+
108
+ // INFO: the two query types these stories send; anything else (the search
109
+ // box's own conditions) is let through rather than guessed at
110
+ const handleMatchQuery = (row: any, query?: any[]) =>
111
+ (query ?? []).every((eachQuery) => {
112
+ if (eachQuery?.type === "=")
113
+ return row[eachQuery.queryId] === eachQuery.value;
114
+ if (eachQuery?.type === "in")
115
+ return (eachQuery.value ?? []).includes(row[eachQuery.queryId]);
116
+ return true;
117
+ });
118
+
119
+ /**
120
+ * INFO: answers every call a story makes against its own in-memory store, so
121
+ * an update really changes what the next read returns. It follows the server
122
+ * where the stories depend on it: the list query filters every collection,
123
+ * and each unionCollections entry adds its rows with its own `source` label.
124
+ */
125
+ const handleInstallMock = (store: any[]) => {
126
+ const mock = new MockAdapter(axios, {
127
+ onNoMatch: "passthrough",
128
+ delayResponse: 50,
129
+ });
130
+ const returns = handleSeedReturns();
131
+ const handleParseBody = (config: any) => {
132
+ try {
133
+ return JSON.parse(config?.data ?? "{}");
134
+ } catch (err) {
135
+ return {};
136
+ }
137
+ };
138
+ const handleListRows = (body: any) => {
139
+ const rows = store.filter((eachRow) =>
140
+ handleMatchQuery(eachRow, body.query),
141
+ );
142
+ const unionRows = (body.unionCollections ?? []).flatMap((eachUnion: any) =>
143
+ eachUnion.collection === RETURN_COLLECTION
144
+ ? returns
145
+ .filter(
146
+ (eachRow) =>
147
+ handleMatchQuery(eachRow, eachUnion.query) &&
148
+ handleMatchQuery(eachRow, body.query),
149
+ )
150
+ .map((eachRow) => ({ ...eachRow, source: eachUnion.source }))
151
+ : [],
152
+ );
153
+ return [...rows, ...unionRows];
154
+ };
155
+ const renderUrl = (type: string) => `${ENDPOINT}/${COLLECTION}/${type}`;
156
+ mock.onPost(renderUrl("list")).reply((config) => {
157
+ const rows = handleListRows(handleParseBody(config));
158
+ return [200, { data: handleClone(rows), count: rows.length }];
159
+ });
160
+ mock.onPost(renderUrl("count")).reply((config) => {
161
+ const rows = handleListRows(handleParseBody(config));
162
+ return [200, { data: rows.length, count: rows.length }];
163
+ });
164
+ mock.onPost(renderUrl("get")).reply((config) => {
165
+ const body = handleParseBody(config);
166
+ return [
167
+ 200,
168
+ {
169
+ data: handleClone(
170
+ store.find((eachRow) => eachRow._id === body.id) ?? null,
171
+ ),
172
+ },
173
+ ];
174
+ });
175
+ mock.onPost(renderUrl("create")).reply((config) => {
176
+ const body = handleParseBody(config);
177
+ store.push({ ...(body.data ?? {}) });
178
+ return [200, { success: true }];
179
+ });
180
+ mock.onPost(renderUrl("update")).reply((config) => {
181
+ const body = handleParseBody(config);
182
+ const index = store.findIndex((eachRow) => eachRow._id === body.id);
183
+ if (index >= 0) store[index] = { ...store[index], ...(body.data ?? {}) };
184
+ return [200, { success: true }];
185
+ });
186
+ mock.onPost(renderUrl("delete")).reply((config) => {
187
+ const body = handleParseBody(config);
188
+ const index = store.findIndex((eachRow) => eachRow._id === body.id);
189
+ if (index >= 0) store.splice(index, 1);
190
+ return [200, { success: true }];
191
+ });
192
+ return mock;
193
+ };
194
+
195
+ const useOrderMock = () => {
196
+ const store = useMemo(() => handleSeedOrders(), []);
197
+ useMemo(() => handleInstallMock(store), [store]);
198
+ return store;
199
+ };
200
+
201
+ const workspace = {
202
+ apiEndpointType: "mongoDB" as const,
203
+ apiEndpoint: ENDPOINT,
204
+ apiEndpointToken: "",
205
+ apiUserToken: "",
206
+ assetEndpoint: "",
207
+ assetUploadEndpoint: "",
208
+ };
209
+
210
+ const renderIntro = (title: string, description: ReactElement) => (
211
+ <div className="mb-4 max-w-3xl">
212
+ <div className="font-semibold text-gray-900">{title}</div>
213
+ <div className="mt-1 text-sm text-gray-600">{description}</div>
214
+ </div>
215
+ );
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Components the hooks return. They live at module level on purpose: a hook
219
+ // runs INSIDE DataTemplate's render, so React hooks written directly in the
220
+ // arrow function would belong to DataTemplate and break the rules of hooks.
221
+ // Return a component instead and its state is its own.
222
+ // ---------------------------------------------------------------------------
223
+
224
+ const OrderCards = (props: { rows: any[]; loading: boolean }) => {
225
+ if (props.loading)
226
+ return <div className="text-sm text-gray-500">Loading…</div>;
227
+ return (
228
+ <div
229
+ id="bare-order-cards"
230
+ className="grid grid-cols-1 gap-3 sm:grid-cols-3"
231
+ >
232
+ {props.rows.map((eachRow: any) => (
233
+ <div
234
+ key={eachRow._id ?? eachRow.orderNo}
235
+ className="rounded-lg border border-gray-200 bg-white p-4"
236
+ >
237
+ <div className="text-xs text-gray-500">{eachRow.orderNo}</div>
238
+ <div className="mt-1 font-medium text-gray-900">
239
+ {eachRow.customer}
240
+ </div>
241
+ <div className="mt-2 text-sm text-gray-700">RM {eachRow.amount}</div>
242
+ </div>
243
+ ))}
244
+ </div>
245
+ );
246
+ };
247
+
248
+ const OrderSummary = (props: { data: any }) => (
249
+ <div className="mt-4 rounded-lg border border-primary-200 bg-primary-50 p-4 text-sm">
250
+ <div className="font-medium text-gray-900">Summary</div>
251
+ <div className="mt-1 text-gray-700">
252
+ {props.data?.customer} owes RM {props.data?.amount} on{" "}
253
+ {props.data?.orderNo}.
254
+ </div>
255
+ </div>
256
+ );
257
+
258
+ const OrderReceipt = (props: { data: any }) => (
259
+ <div className="rounded-lg border border-gray-200 bg-white p-6">
260
+ <div className="text-xs uppercase tracking-wide text-gray-500">Receipt</div>
261
+ <div className="mt-2 text-2xl font-semibold text-gray-900">
262
+ RM {props.data?.amount}
263
+ </div>
264
+ <div className="mt-1 text-sm text-gray-700">
265
+ {props.data?.orderNo} · {props.data?.customer}
266
+ </div>
267
+ <div className="mt-3">
268
+ <Tag size="small" tagText={props.data?.status ?? ""} color="default" />
269
+ </div>
270
+ </div>
271
+ );
272
+
273
+ const LivePanel = (props: { data: any }) => {
274
+ // 1. State
275
+ const [checked, setChecked] = useState(0);
276
+
277
+ // 7. return
278
+ return (
279
+ <div
280
+ id="bare-live-panel"
281
+ className="mt-4 rounded-lg border border-gray-200 bg-white p-4 text-sm"
282
+ >
283
+ <div className="font-medium text-gray-900">Live preview</div>
284
+ <div id="bare-live-values" className="mt-1 text-gray-700">
285
+ {props.data?.customer || "—"} · RM {props.data?.amount ?? 0}
286
+ </div>
287
+ <div className="mt-3 flex items-center gap-3">
288
+ <Button
289
+ text="Mark checked"
290
+ size="small"
291
+ type="light"
292
+ onClick={() => setChecked((current) => current + 1)}
293
+ />
294
+ <span id="bare-live-checked" className="text-gray-600">
295
+ checked {checked}×
296
+ </span>
297
+ </div>
298
+ </div>
299
+ );
300
+ };
301
+
302
+ const StatusPicker = (props: {
303
+ value: string;
304
+ onChange: (value: string) => void;
305
+ }) => (
306
+ <div id="bare-status-picker" className="col-span-12 sm:col-span-6">
307
+ <div className="mb-2 text-sm font-medium text-gray-700">
308
+ Status (custom input)
309
+ </div>
310
+ <ButtonGroup
311
+ selection={["Draft", "Submitted", "Approved"].map((eachStatus) => ({
312
+ id: eachStatus,
313
+ name: eachStatus,
314
+ }))}
315
+ selectedId={props.value}
316
+ variant="tag"
317
+ size="small"
318
+ onClick={props.onChange}
319
+ />
320
+ </div>
321
+ );
322
+
323
+ const STATUS_COLOUR: { [status: string]: "yellow" | "blue" | "green" } = {
324
+ Draft: "yellow",
325
+ Submitted: "blue",
326
+ Approved: "green",
327
+ };
328
+
329
+ const STATUS_SELECTION = ["Draft", "Submitted", "Approved"].map(
330
+ (eachStatus) => ({ id: eachStatus, name: eachStatus }),
331
+ );
332
+
333
+ const handleFormatValue = (key: string, value: any) => {
334
+ if (key === "amount" && value !== undefined && value !== null && value !== "")
335
+ return `RM ${Number(value).toLocaleString("en-MY", {
336
+ minimumFractionDigits: 2,
337
+ })}`;
338
+ if (key === "status" && value)
339
+ return (
340
+ <Tag
341
+ size="small"
342
+ tagText={value}
343
+ color={STATUS_COLOUR[value] ?? "default"}
344
+ />
345
+ );
346
+ return undefined;
347
+ };
348
+
349
+ const UNION_SCHEMA: DataStructure[] = [
350
+ ...ORDER_SCHEMA,
351
+ { id: "source", name: "Source", type: "string", hideForm: true },
352
+ ];
353
+
354
+ // ---------------------------------------------------------------------------
355
+ // Stories
356
+ // ---------------------------------------------------------------------------
357
+
358
+ export const ListOverride = () => {
359
+ useOrderMock();
360
+ return (
361
+ <div>
362
+ {renderIntro(
363
+ "Change the list — bareListComponent",
364
+ <>
365
+ <code>bareListComponent(rows, prettierRows, loading, pageIndex)</code>{" "}
366
+ replaces the list body with your own rendering; the data, search and
367
+ paging stay DataTemplate&apos;s. It only applies inside a list{" "}
368
+ <b>view tab</b> — declare one in <code>organise.extendedTabs</code>{" "}
369
+ and select it with <code>organise.viewId</code> (the tab name,
370
+ lowercased, spaces as underscores). On the default table view it is
371
+ ignored and the normal table renders.
372
+ </>,
373
+ )}
374
+ <DataTemplateWrapper
375
+ title="Orders"
376
+ type="list"
377
+ collectionId={COLLECTION}
378
+ schema={ORDER_SCHEMA}
379
+ workspace={workspace}
380
+ organise={{
381
+ breadcrumbType: "hidden",
382
+ viewId: "cards",
383
+ extendedTabs: [{ name: "Cards", type: "list", viewType: "list" }],
384
+ }}
385
+ bareSettings={{
386
+ bareNewDTW: true,
387
+ bareListComponent: (
388
+ rows: any[],
389
+ _prettierRows: any,
390
+ loading: boolean,
391
+ ) => <OrderCards rows={rows ?? []} loading={loading} />,
392
+ }}
393
+ />
394
+ </div>
395
+ );
396
+ };
397
+
398
+ export const ReadAddBelow = () => {
399
+ useOrderMock();
400
+ const [position, setPosition] = useState<"below" | "top">("below");
401
+ return (
402
+ <div>
403
+ {renderIntro(
404
+ "Add under the read view — bareReadComponent",
405
+ <>
406
+ <code>bareReadComponent(record, rawRecord)</code> renders <b>below</b>{" "}
407
+ the record&apos;s fields by default. Set{" "}
408
+ <code>bareReadComponentPos: &quot;top&quot;</code> to put it above
409
+ them. The default fields stay.
410
+ </>,
411
+ )}
412
+ <div className="mb-3">
413
+ <ButtonGroup
414
+ selection={[
415
+ { id: "below", name: "Below (default)" },
416
+ { id: "top", name: 'bareReadComponentPos: "top"' },
417
+ ]}
418
+ selectedId={position}
419
+ variant="tag"
420
+ size="small"
421
+ onClick={(id) => setPosition(id as "below" | "top")}
422
+ />
423
+ </div>
424
+ <DataTemplateWrapper
425
+ title="Order"
426
+ type="read"
427
+ id="o1"
428
+ collectionId={COLLECTION}
429
+ schema={ORDER_SCHEMA}
430
+ workspace={workspace}
431
+ organise={{ breadcrumbType: "hidden" }}
432
+ bareSettings={{
433
+ bareNewDTW: true,
434
+ bareReadComponent: (record: any) => <OrderSummary data={record} />,
435
+ bareReadComponentPos: position === "top" ? "top" : undefined,
436
+ }}
437
+ />
438
+ </div>
439
+ );
440
+ };
441
+
442
+ export const ReadReplace = () => {
443
+ useOrderMock();
444
+ return (
445
+ <div>
446
+ {renderIntro(
447
+ "Replace the read view — bareReadComponent + bareReadComponentHide",
448
+ <>
449
+ Add <code>bareReadComponentHide: true</code> and the default field
450
+ list is not drawn — only what <code>bareReadComponent</code> returns.
451
+ The action buttons are still DataTemplate&apos;s.
452
+ </>,
453
+ )}
454
+ <DataTemplateWrapper
455
+ title="Order"
456
+ type="read"
457
+ id="o3"
458
+ collectionId={COLLECTION}
459
+ schema={ORDER_SCHEMA}
460
+ workspace={workspace}
461
+ organise={{ breadcrumbType: "hidden" }}
462
+ bareSettings={{
463
+ bareNewDTW: true,
464
+ bareReadComponent: (record: any) => <OrderReceipt data={record} />,
465
+ bareReadComponentHide: true,
466
+ }}
467
+ />
468
+ </div>
469
+ );
470
+ };
471
+
472
+ export const UpdateAddBelow = () => {
473
+ useOrderMock();
474
+ const [above, setAbove] = useState(false);
475
+ return (
476
+ <div>
477
+ {renderIntro(
478
+ "Add under the update form — bareUpdateComponent",
479
+ <>
480
+ <code>bareUpdateComponent(liveData)</code> renders{" "}
481
+ <b>inside the form, below the fields</b> (
482
+ <code>bareUpdateComponentAbove</code> for above). The fields still
483
+ render — it adds, it does not replace. It re-runs with the <b>live</b>{" "}
484
+ values as you type, and the component you return keeps its own state:
485
+ type in Customer, press Mark checked, type again. It gets no setter —
486
+ to write into the form, use <code>bareUpdateIndividualComponent</code>
487
+ .
488
+ </>,
489
+ )}
490
+ <div className="mb-3">
491
+ <ButtonGroup
492
+ selection={[
493
+ { id: "below", name: "Below (default)" },
494
+ { id: "above", name: "bareUpdateComponentAbove" },
495
+ ]}
496
+ selectedId={above ? "above" : "below"}
497
+ variant="tag"
498
+ size="small"
499
+ onClick={(id) => setAbove(id === "above")}
500
+ />
501
+ </div>
502
+ <DataTemplateWrapper
503
+ title="Order"
504
+ type="update"
505
+ id="o1"
506
+ collectionId={COLLECTION}
507
+ schema={ORDER_SCHEMA}
508
+ workspace={workspace}
509
+ edit={{ allEditable: true }}
510
+ organise={{ breadcrumbType: "hidden" }}
511
+ bareSettings={{
512
+ bareNewDTW: true,
513
+ bareUpdateComponent: (liveData: any) => <LivePanel data={liveData} />,
514
+ bareUpdateComponentAbove: above,
515
+ }}
516
+ handleComplete={() => {}}
517
+ />
518
+ </div>
519
+ );
520
+ };
521
+
522
+ export const UpdateReplaceField = () => {
523
+ useOrderMock();
524
+ return (
525
+ <div>
526
+ {renderIntro(
527
+ "Replace one field — bareUpdateIndividualComponent",
528
+ <>
529
+ <code>
530
+ bareUpdateIndividualComponent(columnId, ref, data, setData)
531
+ </code>{" "}
532
+ is asked for every field. Return an element to replace that
533
+ field&apos;s input, or <code>null</code> to keep the default. It is
534
+ the only hook that can write into the form — here the Status list is
535
+ swapped for buttons that call <code>setData</code>. There is no single
536
+ prop that replaces the whole update form.
537
+ </>,
538
+ )}
539
+ <DataTemplateWrapper
540
+ title="Order"
541
+ type="update"
542
+ id="o2"
543
+ collectionId={COLLECTION}
544
+ schema={ORDER_SCHEMA}
545
+ workspace={workspace}
546
+ edit={{ allEditable: true }}
547
+ organise={{ breadcrumbType: "hidden" }}
548
+ bareSettings={{
549
+ bareNewDTW: true,
550
+ bareUpdateIndividualComponent: (
551
+ columnId: string,
552
+ _ref: any,
553
+ data: any,
554
+ setData: (value: any) => void,
555
+ ) =>
556
+ columnId === "status" ? (
557
+ <StatusPicker
558
+ key="status-picker"
559
+ value={data?.status ?? ""}
560
+ onChange={(value) => setData({ ...data, status: value })}
561
+ />
562
+ ) : null,
563
+ }}
564
+ handleComplete={() => {}}
565
+ />
566
+ </div>
567
+ );
568
+ };
569
+
570
+ export const ActionButtons = () => {
571
+ useOrderMock();
572
+ return (
573
+ <div>
574
+ {renderIntro(
575
+ "Replace the action buttons — bareUpdateActionComponent",
576
+ <>
577
+ <code>bareUpdateActionComponent(data, formRef)</code> <b>replaces</b>{" "}
578
+ the default buttons — Save and Submit in the update view, Update in
579
+ the read view. Call <code>formRef.current.handleSubmit()</code> to
580
+ submit. Return an empty fragment to remove the bar altogether. Delete
581
+ is controlled separately.
582
+ </>,
583
+ )}
584
+ <DataTemplateWrapper
585
+ title="Order"
586
+ type="update"
587
+ id="o1"
588
+ collectionId={COLLECTION}
589
+ schema={ORDER_SCHEMA}
590
+ workspace={workspace}
591
+ edit={{ allEditable: true }}
592
+ organise={{ breadcrumbType: "hidden" }}
593
+ bareSettings={{
594
+ bareNewDTW: true,
595
+ bareUpdateActionComponent: (_data: any, formRef: any) => (
596
+ <div className="flex gap-2">
597
+ <Button
598
+ text="Discard"
599
+ size="small"
600
+ type="light"
601
+ onClick={() => {}}
602
+ />
603
+ <Button
604
+ text="Submit order"
605
+ size="small"
606
+ type="normal"
607
+ onClick={() => formRef?.current?.handleSubmit?.()}
608
+ />
609
+ </div>
610
+ ),
611
+ }}
612
+ handleComplete={() => {}}
613
+ />
614
+ </div>
615
+ );
616
+ };
617
+
618
+ export const FullPage = () => {
619
+ // 1. State
620
+ const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
621
+ const [selectedId, setSelectedId] = useState("");
622
+
623
+ // 2. Refs & mocks
624
+ useOrderMock();
625
+
626
+ // 5. handle functions
627
+ const handleNavigate = (type: "list" | "read" | "update", id?: string) => {
628
+ setViewType(type);
629
+ if (id !== undefined) setSelectedId(id);
630
+ };
631
+
632
+ // 7. return
633
+ return (
634
+ <div>
635
+ {renderIntro(
636
+ "Wire a full page — handleNavigate",
637
+ <>
638
+ DataTemplate never switches its own view. It calls{" "}
639
+ <code>handleNavigate(type, id)</code> and you pass the new{" "}
640
+ <code>type</code> and <code>id</code> back in. A row opens{" "}
641
+ <code>(&quot;read&quot;, id)</code>, Update is{" "}
642
+ <code>(&quot;update&quot;, id)</code>, Create is{" "}
643
+ <code>(&quot;update&quot;, &quot;new&quot;)</code>, a save lands on{" "}
644
+ <code>(&quot;read&quot;, id)</code> and a delete on{" "}
645
+ <code>(&quot;list&quot;, &quot;&quot;)</code>. Passing{" "}
646
+ <code>handleComplete</code> replaces the jump to read after a save.
647
+ The wrapper defaults <code>edit</code> to{" "}
648
+ <code>disabledUpdate: true</code>, so pass{" "}
649
+ <code>disabledUpdate: false</code> to get the Update button.
650
+ </>,
651
+ )}
652
+ <div id="bare-nav-state" className="mb-3 font-mono text-xs text-gray-500">
653
+ type={viewType} · id={selectedId || "—"}
654
+ </div>
655
+ <DataTemplateWrapper
656
+ title="Orders"
657
+ type={viewType}
658
+ id={selectedId}
659
+ collectionId={COLLECTION}
660
+ schema={ORDER_SCHEMA}
661
+ workspace={workspace}
662
+ edit={{ allEditable: true, disabledUpdate: false, deleteBtn: true }}
663
+ organise={{ breadcrumbType: "button" }}
664
+ bareSettings={{ bareNewDTW: true }}
665
+ handleNavigate={handleNavigate}
666
+ />
667
+ </div>
668
+ );
669
+ };
670
+
671
+ export const FilterList = () => {
672
+ // 1. State
673
+ const [status, setStatus] = useState("Submitted");
674
+
675
+ // 2. Refs & mocks
676
+ useOrderMock();
677
+
678
+ // 7. return
679
+ return (
680
+ <div>
681
+ {renderIntro(
682
+ "Filter the list — bareListFilterQuery",
683
+ <>
684
+ <code>
685
+ bareListFilterQuery: [{"{"} type: &quot;=&quot;, queryId, value{" "}
686
+ {"}"}]
687
+ </code>{" "}
688
+ is added to every list request, so the list only ever holds matching
689
+ rows and search, tabs and paging work inside them. The user cannot see
690
+ or clear it, and its columns are left out of the flex filter picker.
691
+ If the user filters the same field, their condition replaces yours.
692
+ The list here is keyed on the status so a change refetches.
693
+ </>,
694
+ )}
695
+ <div className="mb-3">
696
+ <ButtonGroup
697
+ selection={STATUS_SELECTION}
698
+ selectedId={status}
699
+ variant="tag"
700
+ size="small"
701
+ onClick={(id) => setStatus(id)}
702
+ />
703
+ </div>
704
+ <DataTemplateWrapper
705
+ key={status}
706
+ title="Orders"
707
+ type="list"
708
+ collectionId={COLLECTION}
709
+ schema={ORDER_SCHEMA}
710
+ workspace={workspace}
711
+ organise={{ breadcrumbType: "hidden" }}
712
+ bareSettings={{
713
+ bareNewDTW: true,
714
+ bareListFilterQuery: [
715
+ { type: "=", queryId: "status", value: status },
716
+ ],
717
+ }}
718
+ />
719
+ </div>
720
+ );
721
+ };
722
+
723
+ export const CreateWithPrefill = () => {
724
+ // 1. State
725
+ const [savedId, setSavedId] = useState("");
726
+
727
+ // 2. Refs & mocks
728
+ const store = useOrderMock();
729
+
730
+ // 3. Computed
731
+ const savedRecord = store.find((eachRow) => eachRow._id === savedId);
732
+
733
+ // 7. return
734
+ return (
735
+ <div>
736
+ {renderIntro(
737
+ "Create with fixed values — bareUpdatePrefill",
738
+ <>
739
+ Open the form on <code>type=&quot;update&quot;</code> and{" "}
740
+ <code>id=&quot;new&quot;</code> to create.{" "}
741
+ <code>bareUpdatePrefill(formData)</code> returns{" "}
742
+ <code>
743
+ [{"{"} key, value {"}"}]
744
+ </code>
745
+ : those fields are <b>taken out of the form</b> and the values are
746
+ written on save, so use it for values the user should not choose — a
747
+ status, an owner, a parent id. It is not a default the user can edit,
748
+ and it applies to every save from the form, not only the first. Status
749
+ is set to Draft here; fill the rest and save.
750
+ </>,
751
+ )}
752
+ <DataTemplateWrapper
753
+ title="New order"
754
+ type="update"
755
+ id="new"
756
+ collectionId={COLLECTION}
757
+ schema={ORDER_SCHEMA}
758
+ workspace={workspace}
759
+ edit={{ allEditable: true }}
760
+ organise={{ breadcrumbType: "hidden" }}
761
+ bareSettings={{
762
+ bareNewDTW: true,
763
+ bareUpdatePrefill: () => [{ key: "status", value: "Draft" }],
764
+ }}
765
+ handleComplete={(id) => setSavedId(id)}
766
+ />
767
+ {savedRecord && (
768
+ <div className="mt-4 rounded-lg border border-gray-200 bg-white p-4 text-sm">
769
+ <div className="font-medium text-gray-900">Saved record</div>
770
+ <pre
771
+ id="bare-saved-record"
772
+ className="mt-2 overflow-x-auto text-xs text-gray-700"
773
+ >
774
+ {JSON.stringify(savedRecord, null, 2)}
775
+ </pre>
776
+ </div>
777
+ )}
778
+ </div>
779
+ );
780
+ };
781
+
782
+ export const FormatValue = () => {
783
+ useOrderMock();
784
+ return (
785
+ <div>
786
+ {renderIntro(
787
+ "Format a value — bareListDescription / bareReadDescription",
788
+ <>
789
+ <code>bareListDescription(fieldId, value)</code> (table cells) and{" "}
790
+ <code>bareReadDescription(fieldId, value)</code> (read view) are asked
791
+ for every value. Return a string or an element to show instead, or{" "}
792
+ <code>undefined</code> to keep the default. Only a truthy return
793
+ replaces it, so an empty string or <code>0</code> falls back. The
794
+ stored value is untouched. The list version needs{" "}
795
+ <code>bareNewDTW</code>.
796
+ </>,
797
+ )}
798
+ <DataTemplateWrapper
799
+ title="Orders"
800
+ type="list"
801
+ collectionId={COLLECTION}
802
+ schema={ORDER_SCHEMA}
803
+ workspace={workspace}
804
+ organise={{ breadcrumbType: "hidden" }}
805
+ bareSettings={{
806
+ bareNewDTW: true,
807
+ bareListDescription: handleFormatValue,
808
+ }}
809
+ />
810
+ <div id="bare-format-read" className="mt-6">
811
+ <DataTemplateWrapper
812
+ title="Order"
813
+ type="read"
814
+ id="o1"
815
+ collectionId={COLLECTION}
816
+ schema={ORDER_SCHEMA}
817
+ workspace={workspace}
818
+ organise={{ breadcrumbType: "hidden" }}
819
+ bareSettings={{
820
+ bareNewDTW: true,
821
+ bareReadDescription: handleFormatValue,
822
+ }}
823
+ />
824
+ </div>
825
+ </div>
826
+ );
827
+ };
828
+
829
+ export const ValidateAndAfterSave = () => {
830
+ // 1. State
831
+ const [error, setError] = useState("");
832
+ const [log, setLog] = useState<string[]>([]);
833
+
834
+ // 2. Refs & mocks
835
+ useOrderMock();
836
+
837
+ // 5. handle functions
838
+ const handleCheckBeforeSave = async (data: any) => {
839
+ if (!(Number(data?.amount) > 0)) {
840
+ setError("Amount must be more than 0 — nothing was saved.");
841
+ return false;
842
+ }
843
+ setError("");
844
+ return true;
845
+ };
846
+
847
+ const handleAfterSave = async (data: any) => {
848
+ setLog((current) => [
849
+ ...current,
850
+ `Saved ${data?.orderNo} at RM ${data?.amount}`,
851
+ ]);
852
+ };
853
+
854
+ // 7. return
855
+ return (
856
+ <div>
857
+ {renderIntro(
858
+ "Check before save, act after — bareUpdateHandlePreComplete / PostComplete",
859
+ <>
860
+ <code>bareUpdateHandlePreComplete(formData)</code> runs before the
861
+ write. Return <code>false</code> to stop it — the saving modal just
862
+ closes, with <b>no message</b>, so show your own (as here). It also
863
+ gates delete. <code>bareUpdateHandlePostComplete(savedData)</code>{" "}
864
+ runs after a successful save, create or delete — use it for emails,
865
+ audit rows or refreshing something else. Set Amount to 0 and save,
866
+ then set it back and save.
867
+ </>,
868
+ )}
869
+ {error && (
870
+ <div
871
+ id="bare-precomplete-error"
872
+ className="mb-3 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-700"
873
+ >
874
+ {error}
875
+ </div>
876
+ )}
877
+ <DataTemplateWrapper
878
+ title="Order"
879
+ type="update"
880
+ id="o2"
881
+ collectionId={COLLECTION}
882
+ schema={ORDER_SCHEMA}
883
+ workspace={workspace}
884
+ edit={{ allEditable: true }}
885
+ organise={{ breadcrumbType: "hidden" }}
886
+ bareSettings={{
887
+ bareNewDTW: true,
888
+ bareUpdateHandlePreComplete: handleCheckBeforeSave,
889
+ bareUpdateHandlePostComplete: handleAfterSave,
890
+ }}
891
+ handleComplete={() => {}}
892
+ />
893
+ <div className="mt-4 rounded-lg border border-gray-200 bg-white p-4 text-sm">
894
+ <div className="font-medium text-gray-900">After-save log</div>
895
+ <ul id="bare-postcomplete-log" className="mt-1 text-gray-700">
896
+ {log.length === 0 ? (
897
+ <li className="text-gray-400">Nothing saved yet</li>
898
+ ) : (
899
+ log.map((eachLine, index) => <li key={index}>{eachLine}</li>)
900
+ )}
901
+ </ul>
902
+ </div>
903
+ </div>
904
+ );
905
+ };
906
+
907
+ export const UnionCollections = () => {
908
+ useOrderMock();
909
+ return (
910
+ <div>
911
+ {renderIntro(
912
+ "Merge another collection into the list — organise.unionCollections",
913
+ <>
914
+ Each <code>unionCollections</code> entry names a second collection,
915
+ its own <code>query</code> and a <code>projection</code>; its rows
916
+ come back in the same list with a <code>source</code> field set to the
917
+ entry&apos;s label (or computed per row with{" "}
918
+ <code>source: &quot;dynamic&quot;</code> + <code>sourceLogic</code>).
919
+ Add a <code>source</code> column with <code>hideForm: true</code> to
920
+ show it. The list&apos;s own search and filters only see projected
921
+ fields. MongoDB workspaces only (<code>mongoServer</code>).
922
+ </>,
923
+ )}
924
+ <DataTemplateWrapper
925
+ title="Orders and returns"
926
+ type="list"
927
+ collectionId={COLLECTION}
928
+ schema={UNION_SCHEMA}
929
+ workspace={workspace}
930
+ organise={{
931
+ breadcrumbType: "hidden",
932
+ unionCollections: [
933
+ {
934
+ collection: RETURN_COLLECTION,
935
+ source: "Return",
936
+ query: [],
937
+ projection: {
938
+ _id: 1,
939
+ orderNo: 1,
940
+ customer: 1,
941
+ status: 1,
942
+ amount: 1,
943
+ },
944
+ },
945
+ ],
946
+ }}
947
+ bareSettings={{ bareNewDTW: true }}
948
+ />
949
+ </div>
950
+ );
951
+ };