blixify-ui-web-mcp 0.1.24 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/data/dataTemplate/README.md +24 -21
- package/lib/components/data/dataTemplate/dataTemplate.savedViews.stories.tsx +142 -0
- package/lib/components/data/dataTemplate/dataTemplate.scrollRestore.stories.tsx +246 -0
- package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +196 -38
- package/lib/components/data/dataWrapper/README.md +92 -49
- package/lib/components/data/dataWrapper/dataWrapper.savedViews.stories.tsx +149 -0
- package/lib/components/data/writeQuery/README.md +22 -0
- package/lib/components/display/newGrid/newGrid.stories.tsx +256 -0
- package/lib/components/display/newKanban/newKanban.stories.tsx +109 -51
- package/lib/components/display/newList/README.md +32 -0
- package/lib/components/display/newList/newList.stories.tsx +40 -0
- package/lib/components/input/datePicker/README.md +1 -0
- package/lib/components/input/select/README.md +1 -0
- package/lib/components/input/textInput/README.md +3 -1
- package/lib/components/input/textInput/textinput.stories.tsx +27 -0
- package/lib/components/navigation/leftNav/README.md +5 -0
- package/lib/components/navigation/leftNav/leftNav.stories.tsx +38 -0
- package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -1
- package/lib/mcp/tools/getBackendComponent.js +4 -0
- package/lib/mcp/tools/getBackendComponent.js.map +1 -1
- package/lib/mcp/tools/getComponent.d.ts.map +1 -1
- package/lib/mcp/tools/getComponent.js +6 -1
- package/lib/mcp/tools/getComponent.js.map +1 -1
- package/package.json +1 -1
|
@@ -266,70 +266,228 @@ const DataTemplateDemo = () => {
|
|
|
266
266
|
phone; without the props, narrowing alone would show a squashed
|
|
267
267
|
desktop table, since Tailwind breakpoints key off the viewport. */}
|
|
268
268
|
<div className={isMobile ? "mx-auto max-w-sm" : "w-full"}>
|
|
269
|
+
<DataTemplateImpl
|
|
270
|
+
title="Documents"
|
|
271
|
+
description="Three records hold five attachments between them"
|
|
272
|
+
type={typeParam}
|
|
273
|
+
id={dataId}
|
|
274
|
+
collectionId={DEMO_COLLECTION}
|
|
275
|
+
model={DEMO_SCHEMA}
|
|
276
|
+
localURLEndpoint=""
|
|
277
|
+
devSettings={demoDevSettings}
|
|
278
|
+
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
279
|
+
limit={4}
|
|
280
|
+
// INFO: allEditable is what gates isEditor, and isEditor is what puts
|
|
281
|
+
// "+ New Tab" and the create button on screen. A read-only demo hides
|
|
282
|
+
// the very controls it is meant to demonstrate.
|
|
283
|
+
edit={{ allEditable: true, deleteBtn: true, createBtn: true }}
|
|
284
|
+
organise={{
|
|
285
|
+
extendedTabs: tabs,
|
|
286
|
+
viewId,
|
|
287
|
+
onChangeTabs: (value: any) => setTabs(value),
|
|
288
|
+
handleNavigate: (type: string, id?: string, view?: string) => {
|
|
289
|
+
setTypeParam(type as any);
|
|
290
|
+
setDataId(id ?? "");
|
|
291
|
+
if (view) setViewId(view);
|
|
292
|
+
},
|
|
293
|
+
}}
|
|
294
|
+
// INFO: mirrors a typical work-order list — mobile gets the card list and
|
|
295
|
+
// the mobile form; desktop pins the first column so the record name
|
|
296
|
+
// stays visible while scrolling wide tables. A pinned column on a
|
|
297
|
+
// phone-width card list would pin nothing useful.
|
|
298
|
+
bareSettings={{
|
|
299
|
+
// INFO: mobileView only reaches NewTable, and NewTable only renders
|
|
300
|
+
// under bareNewDTW — on the legacy table bareListMobileView does
|
|
301
|
+
// nothing and "Mobile" would just squash the desktop table.
|
|
302
|
+
// Apps that use the mobile cards set this unconditionally for the same reason.
|
|
303
|
+
bareNewDTW: true,
|
|
304
|
+
bareListMobileView: isMobile,
|
|
305
|
+
bareUpdateMobileView: isMobile,
|
|
306
|
+
bareListTablePinColumns: isMobile
|
|
307
|
+
? undefined
|
|
308
|
+
: { left: [DEMO_SCHEMA[0].id] },
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
export const DataTemplate = () => (
|
|
317
|
+
// INFO: not wrapped in StorySection — that is w-[90vw], and a data table
|
|
318
|
+
// wants the width it is given.
|
|
319
|
+
<div className="w-full">
|
|
320
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
321
|
+
Data Template — self-contained demo
|
|
322
|
+
</div>
|
|
323
|
+
<div className="text-sm mb-4 text-gray-600">
|
|
324
|
+
Carries its own schema and data, so everything works with no backend:
|
|
325
|
+
create a record, edit inline, delete one, and add a tab through + New Tab.
|
|
326
|
+
Three records hold five attachments and the File view lists FIVE tiles —
|
|
327
|
+
the server unwinds the chosen column, so a row is a file rather than a
|
|
328
|
+
record, and the count and paging follow. The record with no attachments
|
|
329
|
+
contributes nothing. Switch to Scans for the same records unwound on a
|
|
330
|
+
different column, which is why + New Tab asks which column a File view
|
|
331
|
+
should list. Any tab can be set to open by default from that same modal.
|
|
332
|
+
</div>
|
|
333
|
+
<DataTemplateDemo />
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// INFO: a separate, minimal collection for the grid demo. The point is the
|
|
338
|
+
// raw record — assigneeId (a reference id the display row loses to a label) and
|
|
339
|
+
// createdAt (a Date the display row formats to a string) — reaching the grid
|
|
340
|
+
// hooks as the SECOND argument, so a custom card can read the untouched value.
|
|
341
|
+
const GRID_COLLECTION = "demo-grid-items";
|
|
342
|
+
|
|
343
|
+
const GRID_SCHEMA: DataStructure[] = [
|
|
344
|
+
{ id: "name", name: "Project", type: "string" },
|
|
345
|
+
{
|
|
346
|
+
id: "status",
|
|
347
|
+
name: "Status",
|
|
348
|
+
type: "list",
|
|
349
|
+
listSelection: ["new", "active", "done"],
|
|
350
|
+
},
|
|
351
|
+
{ id: "assigneeId", name: "Assignee", type: "string" },
|
|
352
|
+
{ id: "createdAt", name: "Created At", type: "date" },
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
const renderGridRecords = () => [
|
|
356
|
+
{
|
|
357
|
+
_id: "g1",
|
|
358
|
+
name: "Apollo",
|
|
359
|
+
status: "new",
|
|
360
|
+
assigneeId: "usr_1001",
|
|
361
|
+
createdAt: new Date("2026-01-05T09:00:00.000Z"),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
_id: "g2",
|
|
365
|
+
name: "Borealis",
|
|
366
|
+
status: "active",
|
|
367
|
+
assigneeId: "usr_2002",
|
|
368
|
+
createdAt: new Date("2026-02-11T09:00:00.000Z"),
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
_id: "g3",
|
|
372
|
+
name: "Cassini",
|
|
373
|
+
status: "done",
|
|
374
|
+
assigneeId: "usr_3003",
|
|
375
|
+
createdAt: new Date("2026-03-20T09:00:00.000Z"),
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
const handleInstallGridMock = (store: any[]) => {
|
|
380
|
+
const mock = new MockAdapter(axios, {
|
|
381
|
+
onNoMatch: "passthrough",
|
|
382
|
+
delayResponse: 120,
|
|
383
|
+
});
|
|
384
|
+
const renderUrl = (type: string) =>
|
|
385
|
+
`${DEMO_ENDPOINT}/${GRID_COLLECTION}/${type}`;
|
|
386
|
+
const handleParseBody = (config: any) => {
|
|
387
|
+
try {
|
|
388
|
+
return JSON.parse(config?.data ?? "{}");
|
|
389
|
+
} catch (err) {
|
|
390
|
+
return {};
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
mock.onPost(renderUrl("list")).reply((config) => {
|
|
395
|
+
const body = handleParseBody(config);
|
|
396
|
+
const cursor = typeof body.cursor === "number" ? body.cursor : 0;
|
|
397
|
+
const limit = typeof body.limit === "number" ? body.limit : 10;
|
|
398
|
+
return [
|
|
399
|
+
200,
|
|
400
|
+
{ data: store.slice(cursor, cursor + limit), count: store.length },
|
|
401
|
+
];
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
mock.onPost(renderUrl("get")).reply((config) => {
|
|
405
|
+
const body = handleParseBody(config);
|
|
406
|
+
return [200, { data: store.find((each) => each._id === body.id) ?? null }];
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
mock
|
|
410
|
+
.onPost(`${DEMO_ENDPOINT}/spaces/list`)
|
|
411
|
+
.reply(200, { data: [], count: 0 });
|
|
412
|
+
|
|
413
|
+
return mock;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const GridRawDataDemo = () => {
|
|
417
|
+
const store = React.useMemo(() => renderGridRecords(), []);
|
|
418
|
+
React.useMemo(() => handleInstallGridMock(store), [store]);
|
|
419
|
+
|
|
420
|
+
const [typeParam, setTypeParam] = useState<"list" | "read" | "update">(
|
|
421
|
+
"list",
|
|
422
|
+
);
|
|
423
|
+
const [dataId, setDataId] = useState<string>("");
|
|
424
|
+
const [viewId, setViewId] = useState<string | undefined>(undefined);
|
|
425
|
+
// INFO: grid view is driven by an extendedTabs entry with viewType "grid" —
|
|
426
|
+
// the same selector renderTabContent uses. organise.listType does not exist.
|
|
427
|
+
const [tabs] = useState<ExtendedTabs[]>([
|
|
428
|
+
{ name: "Board", type: "list", viewType: "grid", defaultTab: true },
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
// INFO: reads rawData (the untouched fetched record), NOT data (the display
|
|
432
|
+
// row). assigneeId is a raw reference id the display row never carries, and
|
|
433
|
+
// createdAt is a real Date the display row would have formatted to a string.
|
|
434
|
+
const renderGridCard = (data: any, rawData: any) => (
|
|
435
|
+
<div className="rounded-lg border border-gray-200 p-3">
|
|
436
|
+
<p className="font-medium text-sm text-gray-900">{data.title}</p>
|
|
437
|
+
<p className="text-xs text-gray-500">
|
|
438
|
+
raw assigneeId: {rawData?.assigneeId}
|
|
439
|
+
</p>
|
|
440
|
+
<p className="text-xs text-gray-500">
|
|
441
|
+
raw createdAt ISO: {new Date(rawData?.createdAt).toISOString()}
|
|
442
|
+
</p>
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
return (
|
|
447
|
+
<div className="w-full">
|
|
269
448
|
<DataTemplateImpl
|
|
270
|
-
title="
|
|
271
|
-
description="
|
|
449
|
+
title="Grid raw data"
|
|
450
|
+
description="Grid cards read the raw record via the 2nd hook argument"
|
|
272
451
|
type={typeParam}
|
|
273
452
|
id={dataId}
|
|
274
|
-
collectionId={
|
|
275
|
-
model={
|
|
453
|
+
collectionId={GRID_COLLECTION}
|
|
454
|
+
model={GRID_SCHEMA}
|
|
276
455
|
localURLEndpoint=""
|
|
277
456
|
devSettings={demoDevSettings}
|
|
278
457
|
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
279
|
-
limit={
|
|
280
|
-
// INFO: allEditable is what gates isEditor, and isEditor is what puts
|
|
281
|
-
// "+ New Tab" and the create button on screen. A read-only demo hides
|
|
282
|
-
// the very controls it is meant to demonstrate.
|
|
283
|
-
edit={{ allEditable: true, deleteBtn: true, createBtn: true }}
|
|
458
|
+
limit={12}
|
|
284
459
|
organise={{
|
|
285
460
|
extendedTabs: tabs,
|
|
286
461
|
viewId,
|
|
287
|
-
onChangeTabs: (value: any) => setTabs(value),
|
|
288
462
|
handleNavigate: (type: string, id?: string, view?: string) => {
|
|
289
463
|
setTypeParam(type as any);
|
|
290
464
|
setDataId(id ?? "");
|
|
291
465
|
if (view) setViewId(view);
|
|
292
466
|
},
|
|
293
467
|
}}
|
|
294
|
-
// INFO: mirrors a typical work-order list — mobile gets the card list and
|
|
295
|
-
// the mobile form; desktop pins the first column so the record name
|
|
296
|
-
// stays visible while scrolling wide tables. A pinned column on a
|
|
297
|
-
// phone-width card list would pin nothing useful.
|
|
298
468
|
bareSettings={{
|
|
299
|
-
// INFO:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
bareListMobileView: isMobile,
|
|
305
|
-
bareUpdateMobileView: isMobile,
|
|
306
|
-
bareListTablePinColumns: isMobile
|
|
307
|
-
? undefined
|
|
308
|
-
: { left: [DEMO_SCHEMA[0].id] },
|
|
469
|
+
// INFO: 2nd arg (rawData) is the untouched fetched record for the card
|
|
470
|
+
bareListGridComponent: renderGridCard,
|
|
471
|
+
bareListGridRibbonText: (data: any, rawData: any) =>
|
|
472
|
+
rawData?.status === "new" ? "New" : "",
|
|
473
|
+
bareListGridTags: (data: any, rawData: any) => [rawData?.status],
|
|
309
474
|
}}
|
|
310
475
|
/>
|
|
311
|
-
</div>
|
|
312
476
|
</div>
|
|
313
477
|
);
|
|
314
478
|
};
|
|
315
479
|
|
|
316
|
-
export const
|
|
317
|
-
// INFO: not wrapped in StorySection — that is w-[90vw], and a data table
|
|
318
|
-
// wants the width it is given.
|
|
480
|
+
export const GridRawData = () => (
|
|
319
481
|
<div className="w-full">
|
|
320
482
|
<div className="font-semibold mb-1 text-gray-900">
|
|
321
|
-
Data Template —
|
|
483
|
+
Data Template — grid card reads raw record
|
|
322
484
|
</div>
|
|
323
485
|
<div className="text-sm mb-4 text-gray-600">
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
record, and the count and paging follow. The record with no attachments
|
|
329
|
-
contributes nothing. Switch to Scans for the same records unwound on a
|
|
330
|
-
different column, which is why + New Tab asks which column a File view
|
|
331
|
-
should list. Any tab can be set to open by default from that same modal.
|
|
486
|
+
Each grid card renders from the second hook argument (the untouched
|
|
487
|
+
fetched record), so it can show the raw <code>assigneeId</code> reference
|
|
488
|
+
id and the raw <code>createdAt</code> Date — values the formatted display
|
|
489
|
+
row no longer carries.
|
|
332
490
|
</div>
|
|
333
|
-
<
|
|
491
|
+
<GridRawDataDemo />
|
|
334
492
|
</div>
|
|
335
493
|
);
|
|
@@ -436,9 +436,12 @@ Use these together with `viewSettings={{ gridImage: "..." }}` to control how gri
|
|
|
436
436
|
|
|
437
437
|
```tsx
|
|
438
438
|
bareSettings={{
|
|
439
|
-
// Fully replace the default grid card with your own component
|
|
440
|
-
|
|
441
|
-
|
|
439
|
+
// Fully replace the default grid card with your own component.
|
|
440
|
+
// `data` = display row (formatted labels/dates, <img> elements);
|
|
441
|
+
// `rawData` = the untouched fetched record — use it to read raw values
|
|
442
|
+
// like a reference id or an unformatted date the display row has lost.
|
|
443
|
+
bareListGridComponent: (data, rawData) => (
|
|
444
|
+
<div className="rounded-lg overflow-hidden border border-gray-200" data-assignee={rawData?.assigneeId}>
|
|
442
445
|
<img src={data.coverImage} className="w-full h-40 object-cover" />
|
|
443
446
|
<div className="p-3">
|
|
444
447
|
<p className="font-medium text-sm">{data.name}</p>
|
|
@@ -451,10 +454,10 @@ bareSettings={{
|
|
|
451
454
|
bareListGridColClassName: "col-span-1",
|
|
452
455
|
|
|
453
456
|
// Tag badges shown on the default grid card
|
|
454
|
-
bareListGridTags: (data) => [
|
|
457
|
+
bareListGridTags: (data, rawData) => [rawData?.category, rawData?.status],
|
|
455
458
|
|
|
456
459
|
// Ribbon text overlaid on the card image (e.g. "New", "Sold")
|
|
457
|
-
bareListGridRibbonText: (data) =>
|
|
460
|
+
bareListGridRibbonText: (data, rawData) => rawData?.status === "new" ? "New" : "",
|
|
458
461
|
|
|
459
462
|
// Fallback shown when the image column is empty
|
|
460
463
|
bareListGridFallbackImg: () => "/placeholder.png",
|
|
@@ -825,6 +828,46 @@ The modal lists all queued records and lets the user confirm before uploading. R
|
|
|
825
828
|
|
|
826
829
|
---
|
|
827
830
|
|
|
831
|
+
## Saved Views
|
|
832
|
+
|
|
833
|
+
`organise.savedViews` adds a row of view tags under the tab bar. A user saves the current filters (toolbar and flex), sort, rows per page, hidden columns and column order under a name, then reapplies, updates or deletes it. **All** returns to the defaults.
|
|
834
|
+
|
|
835
|
+
- Views belong to the tab they were saved on.
|
|
836
|
+
- They are kept in this browser's `localStorage`, per collection, `persistKey` and user. Pass `devSettings.currentUser` so people sharing a browser keep their own views.
|
|
837
|
+
- `organise.defaultSavedViews` adds views from the app, listed first and never deleted. Give each an `id`. Without `tabId` a view is listed on every tab. A field left out of `state` is reset to its default when the view is applied.
|
|
838
|
+
- Works with or without `persistViewState`.
|
|
839
|
+
|
|
840
|
+
```tsx
|
|
841
|
+
<DataTemplateWrapper
|
|
842
|
+
workspace={workspaceSettings}
|
|
843
|
+
collectionId={taskId}
|
|
844
|
+
schema={TaskSchema}
|
|
845
|
+
devSettings={{ currentUser: { userEmail, userName } }}
|
|
846
|
+
organise={{
|
|
847
|
+
savedViews: true,
|
|
848
|
+
defaultSavedViews: [
|
|
849
|
+
{
|
|
850
|
+
id: "in-progress",
|
|
851
|
+
name: "In progress",
|
|
852
|
+
state: {
|
|
853
|
+
flexFilters: [
|
|
854
|
+
{ id: "f-stage", columnId: "stage", operator: "=", value: "In Progress" },
|
|
855
|
+
],
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
id: "top-priority",
|
|
860
|
+
name: "Top priority",
|
|
861
|
+
tabId: "general",
|
|
862
|
+
state: { sort: { sortId: "priority", type: "desc" } },
|
|
863
|
+
},
|
|
864
|
+
],
|
|
865
|
+
}}
|
|
866
|
+
/>
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
`state` takes any of `filterSearchParams`, `filterSelection`, `filterReference`, `filterReferenceDate`, `flexFilters`, `sort`, `pageLimit`, `userHiddenColumnIds`, `hiddenColumnIds` and `columnOrder`. See the DataTemplate README for the storage key and details.
|
|
870
|
+
|
|
828
871
|
## Union Collections
|
|
829
872
|
|
|
830
873
|
`unionCollections` is passed via `organise` and lets you merge data from multiple MongoDB collections into a single list view. Each entry defines a secondary collection to query, how to filter it, which fields to project, and an optional `source` label to identify which collection each row came from.
|
|
@@ -1233,50 +1276,50 @@ A shorthand that builds the `extendedTabs` and `viewId` config for you. Only one
|
|
|
1233
1276
|
|
|
1234
1277
|
### Bare Settings
|
|
1235
1278
|
|
|
1236
|
-
| Name | Type | Description
|
|
1237
|
-
| ---------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
1238
|
-
| `bareReadComponent` | `(data: any, rawData?: any) => ReactElement` | Custom component rendered in read view. `data` is display data (labels + elements); `rawData` is the untouched DB doc
|
|
1239
|
-
| `bareReadComponentPos` | `"top"` | Renders `bareReadComponent` above the default read content
|
|
1240
|
-
| `bareReadComponentHide` | `boolean` | Hides the default read content
|
|
1241
|
-
| `bareReadDescription` | `(key, value) => any` | Custom renderer for read view field values
|
|
1242
|
-
| `bareReadReferenceView` | `(dataId, collectionId, attributeId) => boolean \| void` | Controls whether a reference field opens a view
|
|
1243
|
-
| `bareReadHandleData` | `(data: any, rawData?: any) => void` | Called when read view data is loaded. `data` is display data (labels + elements); `rawData` is the untouched DB doc
|
|
1244
|
-
| `bareReadListTabs` | `boolean` | Shows list-style tabs in read view
|
|
1245
|
-
| `bareListComponent` | `(data, prettierData, loading, pageIndex) => ReactElement` | Fully replaces the list UI
|
|
1246
|
-
| `bareListActionBtns` | `(data: any) => ReactElement[]` | Extra icon buttons on each list row
|
|
1247
|
-
| `bareListMultipleSelectAddtionalActions` | `{ text: string; className?: string; onClick: (ids, selectAll, query) => void }[]` | Extra bulk-action buttons when rows are selected
|
|
1248
|
-
| `bareListGridComponent` | `(data: any) =>
|
|
1249
|
-
| `bareListGridColClassName` | `string` | Tailwind class for grid column width
|
|
1250
|
-
| `bareListGridTags` | `(data: any) => string[]`
|
|
1251
|
-
| `bareListGridRibbonText` | `(data: any) => string`
|
|
1252
|
-
| `bareListGridFallbackImg` | `(data: any) => string \| ReactElement`
|
|
1253
|
-
| `bareListHandleData` | `(data, prettierData) => void` | Called when list data is loaded
|
|
1254
|
-
| `bareListHandleFilter` | `(search, select, date, reference) => void` | Called when any filter changes
|
|
1255
|
-
| `bareListHandleSelection` | `(selectAll, selectedIds, query?) => void` | Called when row selection changes
|
|
1256
|
-
| `bareListDescription` | `(key, value) => any` | Custom renderer for list view field values
|
|
1257
|
-
| `bareListFilterQuery` | `Query[]` | Additional queries always applied to list fetches
|
|
1258
|
-
| `bareListFilterAddComponent` | `ReactElement[]` | Extra filter components in the filter bar
|
|
1259
|
-
| `bareListFilterAsPopup` | `boolean` | Collapses all `additionalQuery` filters into a funnel icon popup instead of showing them inline
|
|
1260
|
-
| `bareListPipeline` | `PipeLine[]` | Additional MongoDB aggregation pipeline stages
|
|
1261
|
-
| `bareListFilterSelected` | `(filterSearchParams, filterSelection, filterReference, filterReferenceDate) => void` | Called with the current filter state on every change
|
|
1262
|
-
| `bareUpdatePrefill` | `(data: any) => { key: string; value: any }[]`
|
|
1263
|
-
| `bareUpdateComponent` | `(data: any) =>
|
|
1264
|
-
| `bareUpdateComponentAbove` | `boolean` | Renders `bareUpdateComponent` above the form fields
|
|
1265
|
-
| `bareUpdateActionComponent` | `(data: any) =>
|
|
1266
|
-
| `bareUpdateColumnType` | `"one" \| "two" \| "four"` | Form column layout
|
|
1267
|
-
| `bareUpdateMobileView` | `boolean` | Mobile-optimised form layout
|
|
1268
|
-
| `bareUpdateActionBottom` | `boolean` | Moves the read/update action buttons below the content. Right aligned either way. The list's create button is unaffected
|
|
1269
|
-
| `bareUpdateHandleData` | `(data, changedAttributeIds
|
|
1270
|
-
| `bareUpdateHandlePreComplete` | `(data: any) => Promise<boolean>`
|
|
1271
|
-
| `bareUpdateHandleComplete` | `(data: any) =>
|
|
1272
|
-
| `bareUpdateHandlePostComplete` | `(data: any) =>
|
|
1273
|
-
| `bareUpdateTabId` | `(tabId: string) => void` | Called when the active form tab changes
|
|
1274
|
-
| `bareUpdateTabComponent` | `(tabIds: string[]) => { tabId: string; icon?: ReactElement; skip?: boolean }[]` | Customise tab icons or skip specific tabs in the form
|
|
1275
|
-
| `bareUpdateIndividualComponent` | `(columnId, ref, selectedData, setSelectedData) => ReactElement \| null`
|
|
1276
|
-
| `bareReferenceDel` | `{ collectionId, collectionName, columnId, schema }[]` | Blocks deletion when the record is referenced elsewhere
|
|
1277
|
-
| `bareOffline` | `boolean` | Enables offline data caching
|
|
1278
|
-
| `bareCSVImportEndpoint` | `string` | Custom CSV import endpoint
|
|
1279
|
-
| `bareCSVExportEndpoint` | `string` | Custom CSV export endpoint
|
|
1279
|
+
| Name | Type | Description |
|
|
1280
|
+
| ---------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
1281
|
+
| `bareReadComponent` | `(data: any, rawData?: any) => ReactElement` | Custom component rendered in read view. `data` is display data (labels + elements); `rawData` is the untouched DB doc |
|
|
1282
|
+
| `bareReadComponentPos` | `"top"` | Renders `bareReadComponent` above the default read content |
|
|
1283
|
+
| `bareReadComponentHide` | `boolean` | Hides the default read content |
|
|
1284
|
+
| `bareReadDescription` | `(key, value) => any` | Custom renderer for read view field values |
|
|
1285
|
+
| `bareReadReferenceView` | `(dataId, collectionId, attributeId) => boolean \| void` | Controls whether a reference field opens a view |
|
|
1286
|
+
| `bareReadHandleData` | `(data: any, rawData?: any) => void` | Called when read view data is loaded. `data` is display data (labels + elements); `rawData` is the untouched DB doc |
|
|
1287
|
+
| `bareReadListTabs` | `boolean` | Shows list-style tabs in read view |
|
|
1288
|
+
| `bareListComponent` | `(data, prettierData, loading, pageIndex) => ReactElement` | Fully replaces the list UI. `data` is the fetched records (`_id` exposed as `id`), `prettierData` the display rows |
|
|
1289
|
+
| `bareListActionBtns` | `(data: any) => ReactElement[]` | Extra icon buttons on each list row. `data` is the raw fetched record |
|
|
1290
|
+
| `bareListMultipleSelectAddtionalActions` | `{ text: string; className?: string; onClick: (ids, selectAll, query) => void }[]` | Extra bulk-action buttons when rows are selected |
|
|
1291
|
+
| `bareListGridComponent` | `(data: any, rawData?: any) => any` | Custom card renderer for grid/kanban view. `data` is display data; `rawData` is the fetched record for that card (`_id` exposed as `id`) |
|
|
1292
|
+
| `bareListGridColClassName` | `string` | Tailwind class for grid column width |
|
|
1293
|
+
| `bareListGridTags` | `(data: any, rawData?: any) => string[]` | Tag strings shown on grid cards. `rawData` is the fetched record for that card (`_id` exposed as `id`) |
|
|
1294
|
+
| `bareListGridRibbonText` | `(data: any, rawData?: any) => string` | Ribbon text on grid cards. `rawData` is the fetched record for that card (`_id` exposed as `id`) |
|
|
1295
|
+
| `bareListGridFallbackImg` | `(data: any, rawData?: any) => string \| ReactElement` | Fallback image for grid cards. `rawData` is the fetched record for that card (`_id` exposed as `id`) |
|
|
1296
|
+
| `bareListHandleData` | `(data, prettierData) => void` | Called when list data is loaded. `data` is the fetched records (`_id` exposed as `id`), `prettierData` the display rows |
|
|
1297
|
+
| `bareListHandleFilter` | `(search, select, date, reference) => void` | Called when any filter changes |
|
|
1298
|
+
| `bareListHandleSelection` | `(selectAll, selectedIds, query?) => void` | Called when row selection changes |
|
|
1299
|
+
| `bareListDescription` | `(key, value) => any` | Custom renderer for list view field values |
|
|
1300
|
+
| `bareListFilterQuery` | `Query[]` | Additional queries always applied to list fetches |
|
|
1301
|
+
| `bareListFilterAddComponent` | `ReactElement[]` | Extra filter components in the filter bar |
|
|
1302
|
+
| `bareListFilterAsPopup` | `boolean` | Collapses all `additionalQuery` filters into a funnel icon popup instead of showing them inline |
|
|
1303
|
+
| `bareListPipeline` | `PipeLine[]` | Additional MongoDB aggregation pipeline stages |
|
|
1304
|
+
| `bareListFilterSelected` | `(filterSearchParams, filterSelection, filterReference, filterReferenceDate) => void` | Called with the current filter state on every change |
|
|
1305
|
+
| `bareUpdatePrefill` | `(data: any, rawData?: any) => { key: string; value: any }[]` | Returns prefill values for the update form. `rawData` is the stored doc as loaded (undefined for a new record) |
|
|
1306
|
+
| `bareUpdateComponent` | `(data: any, rawData?: any) => any` | Custom component rendered inside the update form. `rawData` is the stored doc as loaded (undefined for a new record) |
|
|
1307
|
+
| `bareUpdateComponentAbove` | `boolean` | Renders `bareUpdateComponent` above the form fields |
|
|
1308
|
+
| `bareUpdateActionComponent` | `(data: any, formRef?: any, rawData?: any) => any` | Custom action buttons in the form footer. `rawData` is the stored doc as loaded (undefined for a new record) |
|
|
1309
|
+
| `bareUpdateColumnType` | `"one" \| "two" \| "four"` | Form column layout |
|
|
1310
|
+
| `bareUpdateMobileView` | `boolean` | Mobile-optimised form layout |
|
|
1311
|
+
| `bareUpdateActionBottom` | `boolean` | Moves the read/update action buttons below the content. Right aligned either way. The list's create button is unaffected |
|
|
1312
|
+
| `bareUpdateHandleData` | `(data: any, changedAttributeIds?: string[], rawData?: any) => void` | Called on every form field change with the current data and changed field IDs. `rawData` is the stored doc as loaded (undefined for a new record) |
|
|
1313
|
+
| `bareUpdateHandlePreComplete` | `(data: any, rawData?: any) => Promise<boolean>` | Called before save — return `false` to cancel. `rawData` is the stored doc as loaded (undefined for a new record) |
|
|
1314
|
+
| `bareUpdateHandleComplete` | `(data: any, rawData?: any) => Promise<any>` | Called after save completes. `rawData` is the stored doc as loaded (undefined for a new record) |
|
|
1315
|
+
| `bareUpdateHandlePostComplete` | `(data: any, rawData?: any) => Promise<any>` | Called after post-save operations complete. `rawData` is the stored doc as loaded (undefined for a new record); undefined for offline uploads |
|
|
1316
|
+
| `bareUpdateTabId` | `(tabId: string) => void` | Called when the active form tab changes |
|
|
1317
|
+
| `bareUpdateTabComponent` | `(tabIds: string[]) => { tabId: string; icon?: ReactElement; skip?: boolean }[]` | Customise tab icons or skip specific tabs in the form |
|
|
1318
|
+
| `bareUpdateIndividualComponent` | `(columnId, ref, selectedData, setSelectedData, rawData?) => ReactElement \| null` | Replaces a specific form field with a custom component. `rawData` is the stored doc as loaded (undefined for a new record) |
|
|
1319
|
+
| `bareReferenceDel` | `{ collectionId, collectionName, columnId, schema }[]` | Blocks deletion when the record is referenced elsewhere |
|
|
1320
|
+
| `bareOffline` | `boolean` | Enables offline data caching |
|
|
1321
|
+
| `bareCSVImportEndpoint` | `string` | Custom CSV import endpoint |
|
|
1322
|
+
| `bareCSVExportEndpoint` | `string` | Custom CSV export endpoint |
|
|
1280
1323
|
|
|
1281
1324
|
---
|
|
1282
1325
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { ButtonGroup } from "../../action/buttonGroup";
|
|
3
|
+
import { DataTemplateWrapper } from ".";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_VIEWS,
|
|
6
|
+
DEMO_ENDPOINT,
|
|
7
|
+
demoDevSettings,
|
|
8
|
+
DEMO_SCHEMA,
|
|
9
|
+
handleInstallDemoMock,
|
|
10
|
+
OWNERS,
|
|
11
|
+
renderDemoRecords,
|
|
12
|
+
TABS,
|
|
13
|
+
} from "../dataTemplate/savedViewsDemo.fixtures";
|
|
14
|
+
import { buildSavedViewsKey } from "../dataTemplate/viewStatePersistence";
|
|
15
|
+
|
|
16
|
+
const meta = { title: "Data/Data Wrapper/Saved Views" };
|
|
17
|
+
export default meta;
|
|
18
|
+
|
|
19
|
+
// INFO: its own collection, so views saved here do not mix with the DataTemplate story's
|
|
20
|
+
const COLLECTION = "demo-saved-view-wrapper-tasks";
|
|
21
|
+
|
|
22
|
+
// INFO: bareFlexFilters replaces the toolbar filters, so the two never show together
|
|
23
|
+
const FILTER_MODES = [
|
|
24
|
+
{ id: "flex", name: "Flex filters" },
|
|
25
|
+
{ id: "toolbar", name: "Toolbar filters" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const workspace = {
|
|
29
|
+
apiEndpointType: "mongoDB" as const,
|
|
30
|
+
apiEndpoint: DEMO_ENDPOINT,
|
|
31
|
+
apiEndpointToken: "",
|
|
32
|
+
apiUserToken: "",
|
|
33
|
+
assetEndpoint: "",
|
|
34
|
+
assetUploadEndpoint: "",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const SavedViewsWrapperDemo = () => {
|
|
38
|
+
const [viewId, setViewId] = useState("general");
|
|
39
|
+
const [isMockReady, setIsMockReady] = useState(false);
|
|
40
|
+
const [resetKey, setResetKey] = useState(0);
|
|
41
|
+
const [filterMode, setFilterMode] = useState<"flex" | "toolbar">("flex");
|
|
42
|
+
|
|
43
|
+
const store = useMemo(() => renderDemoRecords(), []);
|
|
44
|
+
|
|
45
|
+
// INFO: in an effect so it survives StrictMode's double mount
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const mock = handleInstallDemoMock(store, COLLECTION);
|
|
48
|
+
setIsMockReady(true);
|
|
49
|
+
return () => {
|
|
50
|
+
mock.restore();
|
|
51
|
+
setIsMockReady(false);
|
|
52
|
+
};
|
|
53
|
+
}, [store]);
|
|
54
|
+
|
|
55
|
+
const handleClearStoredViews = () => {
|
|
56
|
+
try {
|
|
57
|
+
window.localStorage.removeItem(
|
|
58
|
+
buildSavedViewsKey(
|
|
59
|
+
COLLECTION,
|
|
60
|
+
undefined,
|
|
61
|
+
demoDevSettings.currentUser.userEmail,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// INFO: storage unavailable — nothing to clear
|
|
66
|
+
}
|
|
67
|
+
setResetKey((prev) => prev + 1);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="w-full">
|
|
72
|
+
<div className="mb-4 flex flex-wrap items-center gap-3">
|
|
73
|
+
<ButtonGroup
|
|
74
|
+
selection={FILTER_MODES}
|
|
75
|
+
selectedId={filterMode}
|
|
76
|
+
variant="tag"
|
|
77
|
+
size="small"
|
|
78
|
+
onClick={(id) => setFilterMode(id as "flex" | "toolbar")}
|
|
79
|
+
/>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
className="rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-700 hover:bg-gray-50"
|
|
83
|
+
onClick={handleClearStoredViews}
|
|
84
|
+
>
|
|
85
|
+
Clear my saved views
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
{isMockReady && (
|
|
89
|
+
<DataTemplateWrapper
|
|
90
|
+
key={`${resetKey}-${filterMode}`}
|
|
91
|
+
title="Tasks"
|
|
92
|
+
description="40 offline records"
|
|
93
|
+
type="list"
|
|
94
|
+
collectionId={COLLECTION}
|
|
95
|
+
schema={DEMO_SCHEMA}
|
|
96
|
+
workspace={workspace}
|
|
97
|
+
// INFO: what a workspace cannot carry — the schema endpoint the tab bar
|
|
98
|
+
// reads, and the user whose views these are
|
|
99
|
+
devSettings={{
|
|
100
|
+
schemaEndpoint: demoDevSettings.schemaEndpoint,
|
|
101
|
+
currentUser: demoDevSettings.currentUser,
|
|
102
|
+
}}
|
|
103
|
+
limit={10}
|
|
104
|
+
organise={{
|
|
105
|
+
breadcrumbType: "hidden",
|
|
106
|
+
extendedTabs: TABS,
|
|
107
|
+
viewId,
|
|
108
|
+
savedViews: true,
|
|
109
|
+
defaultSavedViews: DEFAULT_VIEWS,
|
|
110
|
+
filterComponentNewRow: true,
|
|
111
|
+
}}
|
|
112
|
+
additionalQuery={[
|
|
113
|
+
{
|
|
114
|
+
type: "list",
|
|
115
|
+
value: "owner",
|
|
116
|
+
options: OWNERS,
|
|
117
|
+
filterLabel: "Owner",
|
|
118
|
+
},
|
|
119
|
+
{ type: "range", value: "dueDate" },
|
|
120
|
+
]}
|
|
121
|
+
handleNavigate={(type, _id, view) => {
|
|
122
|
+
if (type === "list" && view) setViewId(view);
|
|
123
|
+
}}
|
|
124
|
+
bareSettings={{
|
|
125
|
+
bareNewDTW: true,
|
|
126
|
+
bareFlexFilters: filterMode === "flex",
|
|
127
|
+
}}
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const SavedViews = () => (
|
|
135
|
+
<div className="w-full">
|
|
136
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
137
|
+
DataTemplateWrapper — saved views
|
|
138
|
+
</div>
|
|
139
|
+
<div className="text-sm mb-4 text-gray-600">
|
|
140
|
+
The same saved views as Data/Data Template → Saved Views, through
|
|
141
|
+
DataTemplateWrapper the way apps use it: organise.savedViews and
|
|
142
|
+
organise.defaultSavedViews, with devSettings.currentUser keeping each
|
|
143
|
+
user's views apart. Set a filter, sort or columns, then “+ Save view”;
|
|
144
|
+
click a view to apply it, “All” to go back, × to delete. Views are per tab
|
|
145
|
+
and kept in this browser.
|
|
146
|
+
</div>
|
|
147
|
+
<SavedViewsWrapperDemo />
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
@@ -71,6 +71,27 @@ await query.call("update", {
|
|
|
71
71
|
});
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
+
### Version-checked update — refuse a stale save
|
|
75
|
+
|
|
76
|
+
`sensitive: true` writes the update only if the record has not changed since the version in `data.baseUpdatedAt` was read. Send the version you read plus 1 ms; if someone else saved in between, the server answers `400 { err: "Refresh Sensitive Model" }` and `call` rethrows it.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
try {
|
|
80
|
+
await query.call("update", {
|
|
81
|
+
id: org._id,
|
|
82
|
+
data: {
|
|
83
|
+
members,
|
|
84
|
+
baseUpdatedAt: new Date(new Date(org.baseUpdatedAt).getTime() + 1),
|
|
85
|
+
},
|
|
86
|
+
sensitive: true,
|
|
87
|
+
});
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
if (err?.response?.data?.err === "Refresh Sensitive Model") {
|
|
90
|
+
// someone else saved since this was read: reload and ask the user to try again
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
74
95
|
### Delete a record
|
|
75
96
|
|
|
76
97
|
```ts
|
|
@@ -184,6 +205,7 @@ await query.call("create", {
|
|
|
184
205
|
| `query` | `Query[]` | Filter for `batchDelete` / `batchUpdate` | ✓ |
|
|
185
206
|
| `isOrQuery` | `boolean` | Use OR between query conditions in `batchUpdate` | ✓ |
|
|
186
207
|
| `upsert` | `boolean` | Create doc if not found during `update` | ✓ |
|
|
208
|
+
| `sensitive` | `boolean` | Version-checked `update`: refused with `Refresh Sensitive Model` if the record changed since `data.baseUpdatedAt` was read | ✓ |
|
|
187
209
|
| `unique` | `string` | Field name to enforce uniqueness on `create` | ✓ |
|
|
188
210
|
| `stopRes` | `boolean` | Skip waiting for response body | ✓ |
|
|
189
211
|
| `createLabelsIfMissing` | `boolean` | Auto-create label values if missing (Monday only) | ✓ |
|