blixify-ui-web-mcp 0.1.25 → 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.
@@ -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="Documents"
271
- description="Three records hold five attachments between them"
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={DEMO_COLLECTION}
275
- model={DEMO_SCHEMA}
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={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 }}
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: 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] },
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 DataTemplate = () => (
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 — self-contained demo
483
+ Data Template — grid card reads raw record
322
484
  </div>
323
485
  <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.
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
- <DataTemplateDemo />
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
- bareListGridComponent: (data) => (
441
- <div className="rounded-lg overflow-hidden border border-gray-200">
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) => [data.category, data.status],
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) => data.status === "new" ? "New" : "",
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) => ReactElement` | Custom card renderer for grid view |
1249
- | `bareListGridColClassName` | `string` | Tailwind class for grid column width |
1250
- | `bareListGridTags` | `(data: any) => string[]` | Tag strings shown on grid cards |
1251
- | `bareListGridRibbonText` | `(data: any) => string` | Ribbon text on grid cards |
1252
- | `bareListGridFallbackImg` | `(data: any) => string \| ReactElement` | Fallback image for grid cards |
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 }[]` | Returns prefill values for the update form |
1263
- | `bareUpdateComponent` | `(data: any) => ReactElement` | Custom component rendered inside the update form |
1264
- | `bareUpdateComponentAbove` | `boolean` | Renders `bareUpdateComponent` above the form fields |
1265
- | `bareUpdateActionComponent` | `(data: any) => ReactElement` | Custom action buttons in the form footer |
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?) => void` | Called on every form field change |
1270
- | `bareUpdateHandlePreComplete` | `(data: any) => Promise<boolean>` | Called before save — return `false` to cancel |
1271
- | `bareUpdateHandleComplete` | `(data: any) => void` | Called after save completes |
1272
- | `bareUpdateHandlePostComplete` | `(data: any) => void` | Called after post-save operations complete |
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` | Replaces a specific form field with a custom component |
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) | ✓ |