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.
- package/lib/components/action/iconButton/iconButton.stories.tsx +11 -2
- package/lib/components/auth/signUp/signUp.stories.tsx +39 -1
- package/lib/components/chart/statsCard/README.md +24 -0
- package/lib/components/chart/statsCard/statsCard.stories.tsx +68 -0
- package/lib/components/chat/chatBase/chatBase.stories.tsx +1 -1
- package/lib/components/data/dataGraph/dataGraph.stories.tsx +5 -5
- package/lib/components/data/dataTemplate/README.md +204 -85
- package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +307 -295
- package/lib/components/data/dataTemplate/drive.stories.tsx +197 -0
- package/lib/components/data/dataWrapper/README.md +109 -22
- package/lib/components/data/dataWrapper/dataWrapper.bareHooks.stories.tsx +951 -0
- package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +356 -5
- package/lib/components/data/dataWrapper/dataWrapper.view.stories.tsx +79 -5
- package/lib/components/display/calendar/calendar.stories.tsx +1 -1
- package/lib/components/display/detailList/README.md +11 -10
- package/lib/components/display/detailList/detailList.stories.tsx +16 -3
- package/lib/components/display/feeds/README.md +3 -3
- package/lib/components/display/feeds/feeds.stories.tsx +3 -3
- package/lib/components/display/filePreview/filePreview.stories.tsx +111 -0
- package/lib/components/display/modal/README.md +3 -0
- package/lib/components/display/modal/modal.stories.tsx +50 -1
- package/lib/components/display/newCalendar/README.md +0 -6
- package/lib/components/display/newCalendar/newCalendar.stories.tsx +1 -1
- package/lib/components/display/newFeeds/README.md +1 -5
- package/lib/components/display/newFeeds/newFeeds.stories.tsx +3 -3
- package/lib/components/display/newKanban/README.md +0 -7
- package/lib/components/display/newList/README.md +1 -4
- package/lib/components/display/newTable/README.md +29 -9
- package/lib/components/display/newTable/newTable.stories.tsx +120 -9
- package/lib/components/display/tag/README.md +1 -1
- package/lib/components/display/tag/tag.stories.tsx +7 -0
- package/lib/components/input/markdown/markdown.stories.tsx +3 -1
- package/lib/components/input/uploadInput/uploadInput.stories.tsx +41 -0
- package/lib/components/navigation/leftNav/leftNav.stories.tsx +175 -0
- package/lib/components/navigation/tabs/tabs.stories.tsx +23 -0
- package/lib/components/template/calendarView/calendarView.stories.tsx +4 -4
- package/lib/components/template/chatDashboard/chatDashboard.stories.tsx +2 -2
- package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -1
- package/lib/mcp/tools/getBackendComponent.js +7 -3
- package/lib/mcp/tools/getBackendComponent.js.map +1 -1
- package/lib/mcp/tools/suggestTemplate.js +3 -3
- package/lib/mcp/tools/suggestTemplate.js.map +1 -1
- package/package.json +1 -1
- package/lib/components/data/dataTemplate/dataModule.stories.tsx +0 -191
- package/lib/components/data/dataWrapper/dataWrapper.consumer.stories.tsx +0 -2125
- package/lib/components/data/dataWrapper/dataWrapper.utils.stories.tsx +0 -576
- 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'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's fields by default. Set{" "}
|
|
408
|
+
<code>bareReadComponentPos: "top"</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'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'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>("read", id)</code>, Update is{" "}
|
|
642
|
+
<code>("update", id)</code>, Create is{" "}
|
|
643
|
+
<code>("update", "new")</code>, a save lands on{" "}
|
|
644
|
+
<code>("read", id)</code> and a delete on{" "}
|
|
645
|
+
<code>("list", "")</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: "=", 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="update"</code> and{" "}
|
|
740
|
+
<code>id="new"</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's label (or computed per row with{" "}
|
|
918
|
+
<code>source: "dynamic"</code> + <code>sourceLogic</code>).
|
|
919
|
+
Add a <code>source</code> column with <code>hideForm: true</code> to
|
|
920
|
+
show it. The list'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
|
+
};
|