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
|
@@ -454,11 +454,12 @@ Set `organise.viewId="grid"` (or add an `extendedTabs` entry with `viewType="gri
|
|
|
454
454
|
<p className="text-sm text-gray-500">{rowData.price}</p>
|
|
455
455
|
</div>
|
|
456
456
|
),
|
|
457
|
-
// Tags shown on the default grid card
|
|
458
|
-
|
|
457
|
+
// Tags shown on the default grid card. The second argument is the fetched
|
|
458
|
+
// record, for values the display row has formatted away
|
|
459
|
+
bareListGridTags: (rowData, rawData) => [rawData?.category, rawData?.status],
|
|
459
460
|
// Ribbon text on the default grid card
|
|
460
|
-
bareListGridRibbonText: (rowData) =>
|
|
461
|
-
|
|
461
|
+
bareListGridRibbonText: (rowData, rawData) =>
|
|
462
|
+
rawData?.status === "new" ? "New" : "",
|
|
462
463
|
// Fallback when the image column is empty
|
|
463
464
|
bareListGridFallbackImg: () => "/placeholder.png",
|
|
464
465
|
}}
|
|
@@ -983,8 +984,10 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
983
984
|
| `fixedPagination` | `boolean` | Fixes the pagination bar to the bottom of the viewport |
|
|
984
985
|
| `filterComponentNewRow` | `boolean` | Renders each filter component on its own row |
|
|
985
986
|
| `addColumnEnabled` | `boolean` | Shows an add-column button in the table header |
|
|
986
|
-
| `persistViewState` | `"persistent" \| "session"` | Remembers the list's sort, filters, hidden columns, page
|
|
987
|
+
| `persistViewState` | `"persistent" \| "session"` | Remembers the list's sort, filters, hidden columns, page, rows per page, search and reading position per collection, so a Back navigation lands where the user left. `"persistent"` uses `localStorage`; `"session"` writes `sessionStorage` and reads it first, falling back to `localStorage`. The reading position is restored on mount and on returning from a record, not on a tab switch; with several lists on one page, only the list a record was last opened from restores (tracked in `blixify-dtw-persist-last-drill`). Covers table, list, grid, kanban and feeds; not map. List view only |
|
|
987
988
|
| `persistKey` | `string` | Keeps a separate saved view state for this instance: the storage key becomes `blixify-dtw-persist-<collectionId>-<persistKey>`. Saved state is read once, on mount, so give the component a React `key` that changes with `persistKey` |
|
|
989
|
+
| `savedViews` | `boolean` | Shows a row of view tags under the tab bar: save the current filters, flex filters, sort, rows per page and columns under a name, reapply it, update it or delete it. Views belong to the tab they were saved on and are kept in `localStorage` under `blixify-dtw-views-<collectionId>[-<persistKey>][-<currentUser.userEmail>]`. Sort is held by the template while it is on, as with `persistViewState` |
|
|
990
|
+
| `defaultSavedViews` | `DefaultSavedView[]` | Views provided by the app (`{ id?, name, tabId?, state }`), listed before the user's own and never deleted. 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 |
|
|
988
991
|
| `groupBy` | `string` | Column ID to group list results by |
|
|
989
992
|
| `groupIdCollapsible` | `{ [key: string]: boolean }` | Initial collapsed/expanded state per group ID |
|
|
990
993
|
| `sortGroupByAlphabetically` | `boolean` | Sorts group-by values alphabetically |
|
|
@@ -1068,15 +1071,15 @@ Injects custom React components and callbacks into the list, read, and update vi
|
|
|
1068
1071
|
| `bareReadDescription` | `(key: string, value: any) => any` | Custom renderer for read view field values |
|
|
1069
1072
|
| `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 |
|
|
1070
1073
|
| `bareReadListTabs` | `boolean` | Shows list-style tabs in read view |
|
|
1071
|
-
| `bareListComponent` | `(data, prettierData, loading, pageIndex) => any` | Fully replaces the list UI
|
|
1072
|
-
| `bareListActionBtns` | `(data: any) => ReactElement[]` | Extra icon buttons
|
|
1074
|
+
| `bareListComponent` | `(data, prettierData, loading, pageIndex) => any` | Fully replaces the list UI. `data` is the fetched records (`_id` exposed as `id`), `prettierData` the display rows |
|
|
1075
|
+
| `bareListActionBtns` | `(data: any) => ReactElement[]` | Extra icon buttons on each list row. `data` is the raw fetched record |
|
|
1073
1076
|
| `bareListMultipleSelectAddtionalActions` | `{ text: string; className?: string; onClick: (ids, selectAll, query) => void }[]` | Extra bulk-action buttons shown when rows are selected |
|
|
1074
|
-
| `bareListGridComponent` | `(data: any) => any`
|
|
1077
|
+
| `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`) |
|
|
1075
1078
|
| `bareListGridColClassName` | `string` | Tailwind class for grid column width |
|
|
1076
|
-
| `bareListGridTags` | `(data: any) => string[]`
|
|
1077
|
-
| `bareListGridFallbackImg` | `(data: any) => string \| ReactElement`
|
|
1078
|
-
| `bareListGridRibbonText` | `(data: any) => string`
|
|
1079
|
-
| `bareListHandleData` | `(data, prettierData) => void` | Called when list data is loaded
|
|
1079
|
+
| `bareListGridTags` | `(data: any, rawData?: any) => string[]` | Tag strings shown on grid cards. `rawData` is the fetched record for that card (`_id` exposed as `id`) |
|
|
1080
|
+
| `bareListGridFallbackImg` | `(data: any, rawData?: any) => string \| ReactElement` | Fallback image for grid cards. `rawData` is the fetched record for that card (`_id` exposed as `id`) |
|
|
1081
|
+
| `bareListGridRibbonText` | `(data: any, rawData?: any) => string` | Ribbon text on grid cards. `rawData` is the fetched record for that card (`_id` exposed as `id`) |
|
|
1082
|
+
| `bareListHandleData` | `(data, prettierData) => void` | Called when list data is loaded. `data` is the fetched records (`_id` exposed as `id`), `prettierData` the display rows |
|
|
1080
1083
|
| `bareListHandleSelection` | `(selectAll, selectedIds, query?) => void` | Called when row selection changes |
|
|
1081
1084
|
| `bareListHandleFilter` | `(search, select, date, reference) => void` | Called when any filter changes |
|
|
1082
1085
|
| `bareListFilterSelected` | `(filterSearchParams, filterSelection, filterReference, filterReferenceDate) => void` | Called with the current filter state whenever it changes |
|
|
@@ -1084,13 +1087,13 @@ Injects custom React components and callbacks into the list, read, and update vi
|
|
|
1084
1087
|
| `bareListFilterQuery` | `Query[]` | Additional queries always applied to list fetches |
|
|
1085
1088
|
| `bareListFilterAddComponent` | `ReactElement[]` | Extra filter components rendered in the filter bar |
|
|
1086
1089
|
| `bareListPipeline` | `PipeLine[]` | Additional pipeline stages appended to list queries |
|
|
1087
|
-
| `bareListObjectHandleData` | `(type: "read" \| "update" \| "new", data: any) => void`
|
|
1088
|
-
| `bareUpdatePrefill` | `(data: any) => { key: string; value: any }[]`
|
|
1089
|
-
| `bareUpdateComponent` | `(data: any) => any`
|
|
1090
|
+
| `bareListObjectHandleData` | `(type: "read" \| "update" \| "new", data: any, rawData?: any) => void` | Called when a list-object row is opened. `rawData` is that row as stored (undefined for a new row) |
|
|
1091
|
+
| `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) |
|
|
1092
|
+
| `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) |
|
|
1090
1093
|
| `bareUpdateComponentAbove` | `boolean` | Renders `bareUpdateComponent` above the default form fields |
|
|
1091
1094
|
| `bareSaveOnTabChange` | `boolean` | Auto-save: every Next / Previous on a tabbed update form writes a draft first, silently (no toast). Pair with `edit.enableUpdateDraft` |
|
|
1092
1095
|
| `bareUpsertPartialUpdate` | `boolean` | Upsert by `props.id` and send only the fields that changed since load; a save with no changes skips the write. Makes auto-save safe across devices |
|
|
1093
|
-
| `bareUpdateActionComponent` | `(data: any, formRef?: any) => any`
|
|
1096
|
+
| `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) |
|
|
1094
1097
|
| `bareFlexFilters` | `boolean` | Funnel icon opens the Filters builder (column · condition · value, values from the schema) — replaces hand-rolled page filters; new table only |
|
|
1095
1098
|
| `bareOnFlexFiltersChange` | `(flexFilters: FlexFilterData[]) => void` | Fires when the applied (debounced) flex filters change — from user panel edits or programmatic `handleApplyFlexFilters` / `handleClearFlexFilters`. Lets a parent observe live filter state. Needs `bareFlexFilters` |
|
|
1096
1099
|
| `bareListFilterAsPopup` | `boolean` | Shows the list's filter inputs in a popup instead of the header row |
|
|
@@ -1100,15 +1103,15 @@ Injects custom React components and callbacks into the list, read, and update vi
|
|
|
1100
1103
|
| `bareMobileTooltip` | `MobileViewTooltip` | Passed to NewTable as `mobileTooltip` when `bareNewDTW` and `bareListMobileView` are on: a Tag, trailing text and summary line on the collapsed row, plus a hover card. See the NewTable README → Mobile tooltip |
|
|
1101
1104
|
| `bareListMobileTooltip` | `NewListMobileTooltip` | Passed to NewList when `bareNewDTW` is on: `imageKey`, `summaryKeys`, `tagKeys` and `tagColors` for the hover card on list items |
|
|
1102
1105
|
| `bareListMaxSelectedCount` | `number` | Caps how many rows the checkbox column may select at once |
|
|
1103
|
-
| `bareUpdateHandleData` | `(data: any, changedAttributeIds?: string[]) => void`
|
|
1104
|
-
| `bareUpdateHandlePreComplete` | `(data: any) => Promise<boolean>`
|
|
1105
|
-
| `bareUpdateHandleComplete` | `(data: any) => Promise<any>`
|
|
1106
|
-
| `bareUpdateHandlePostComplete` | `(data: any) => Promise<any>`
|
|
1106
|
+
| `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) |
|
|
1107
|
+
| `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) |
|
|
1108
|
+
| `bareUpdateHandleComplete` | `(data: any, rawData?: any) => Promise<any>` | Called after save completes. `rawData` is the stored doc as loaded (undefined for a new record) |
|
|
1109
|
+
| `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 |
|
|
1107
1110
|
| `bareUpdateColumnType` | `"one" \| "two" \| "four"` | Form column layout: `"one"` = 2-col grid, `"two"` = 4-col grid (default), `"four"` = 4-col grid |
|
|
1108
1111
|
| `bareUpdateTabComponent` | `(tabIds: string[]) => { tabId: string; icon?: ReactElement; skip?: boolean }[]` | Custom icons/skip flags for form tabs |
|
|
1109
1112
|
| `bareUpdateActionBottom` | `boolean` | Plain right-aligned action row instead of the floating pill — for short forms inside a `Modal` |
|
|
1110
1113
|
| `bareUpdateTabId` | `(tabId: string) => void` | Called when the active form tab changes |
|
|
1111
|
-
| `bareUpdateIndividualComponent` | `(columnId, ref, selectedData, setSelectedData) => ReactElement \| null`
|
|
1114
|
+
| `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) |
|
|
1112
1115
|
| `bareUpdateMobileView` | `boolean` | Renders the update form in mobile-optimised layout |
|
|
1113
1116
|
| `bareOffline` | `boolean` | Enables offline data caching via IndexedDB |
|
|
1114
1117
|
| `bareOfflineNetworkListener` | `boolean` | With `bareOffline`: follow `navigator.onLine` — saves go to the server while online and to IndexedDB only when the network is down. Without it every save goes to IndexedDB until `handleUploadOfflineData()` |
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Menu, Transition } from "@headlessui/react";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import Papa from "papaparse";
|
|
4
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { ButtonGroup } from "../../action/buttonGroup";
|
|
6
|
+
import { DataTemplate as DataTemplateImpl } from ".";
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_VIEWS,
|
|
9
|
+
DEMO_COLLECTION,
|
|
10
|
+
demoDevSettings,
|
|
11
|
+
DEMO_SCHEMA,
|
|
12
|
+
handleInstallDemoMock,
|
|
13
|
+
OWNERS,
|
|
14
|
+
renderDemoRecords,
|
|
15
|
+
TABS,
|
|
16
|
+
} from "./savedViewsDemo.fixtures";
|
|
17
|
+
import { buildSavedViewsKey } from "./viewStatePersistence";
|
|
18
|
+
|
|
19
|
+
const meta = { title: "Data/Data Template" };
|
|
20
|
+
export default meta;
|
|
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 SavedViewsDemo = () => {
|
|
29
|
+
const [viewId, setViewId] = useState("general");
|
|
30
|
+
const [isMockReady, setIsMockReady] = useState(false);
|
|
31
|
+
// INFO: remounts the template after the stored views are cleared
|
|
32
|
+
const [resetKey, setResetKey] = useState(0);
|
|
33
|
+
const [filterMode, setFilterMode] = useState<"flex" | "toolbar">("flex");
|
|
34
|
+
|
|
35
|
+
const store = useMemo(() => renderDemoRecords(), []);
|
|
36
|
+
const storageKey = buildSavedViewsKey(
|
|
37
|
+
DEMO_COLLECTION,
|
|
38
|
+
undefined,
|
|
39
|
+
demoDevSettings.currentUser.userEmail,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// INFO: in an effect so it survives StrictMode's double mount
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
const mock = handleInstallDemoMock(store);
|
|
45
|
+
setIsMockReady(true);
|
|
46
|
+
return () => {
|
|
47
|
+
mock.restore();
|
|
48
|
+
setIsMockReady(false);
|
|
49
|
+
};
|
|
50
|
+
}, [store]);
|
|
51
|
+
|
|
52
|
+
const handleClearStoredViews = () => {
|
|
53
|
+
try {
|
|
54
|
+
window.localStorage.removeItem(storageKey);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
// INFO: storage unavailable — nothing to clear
|
|
57
|
+
}
|
|
58
|
+
setResetKey((prev) => prev + 1);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className="w-full">
|
|
63
|
+
<div className="mb-4 flex flex-wrap items-center gap-3">
|
|
64
|
+
<ButtonGroup
|
|
65
|
+
selection={FILTER_MODES}
|
|
66
|
+
selectedId={filterMode}
|
|
67
|
+
variant="tag"
|
|
68
|
+
size="small"
|
|
69
|
+
onClick={(id) => setFilterMode(id as "flex" | "toolbar")}
|
|
70
|
+
/>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
className="rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-700 hover:bg-gray-50"
|
|
74
|
+
onClick={handleClearStoredViews}
|
|
75
|
+
>
|
|
76
|
+
Clear my saved views
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
79
|
+
{isMockReady && (
|
|
80
|
+
<DataTemplateImpl
|
|
81
|
+
key={`${resetKey}-${filterMode}`}
|
|
82
|
+
title="Tasks"
|
|
83
|
+
description="40 offline records"
|
|
84
|
+
type="list"
|
|
85
|
+
collectionId={DEMO_COLLECTION}
|
|
86
|
+
model={DEMO_SCHEMA}
|
|
87
|
+
localURLEndpoint=""
|
|
88
|
+
devSettings={demoDevSettings}
|
|
89
|
+
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
90
|
+
limit={10}
|
|
91
|
+
organise={{
|
|
92
|
+
extendedTabs: TABS,
|
|
93
|
+
viewId,
|
|
94
|
+
savedViews: true,
|
|
95
|
+
// INFO: toolbar filters on their own full-width row, instead of
|
|
96
|
+
// sharing the title row with the action buttons
|
|
97
|
+
filterComponentNewRow: true,
|
|
98
|
+
// INFO: the toolbar filters outside the flex filter popup — normal
|
|
99
|
+
// size, and captured by saved views like any other filter
|
|
100
|
+
additionalQuery: [
|
|
101
|
+
{
|
|
102
|
+
type: "list",
|
|
103
|
+
value: "owner",
|
|
104
|
+
options: OWNERS,
|
|
105
|
+
filterLabel: "Owner",
|
|
106
|
+
},
|
|
107
|
+
{ type: "range", value: "dueDate" },
|
|
108
|
+
],
|
|
109
|
+
defaultSavedViews: DEFAULT_VIEWS,
|
|
110
|
+
handleNavigate: (type: string, _id?: string, view?: string) => {
|
|
111
|
+
if (type === "list" && view) setViewId(view);
|
|
112
|
+
},
|
|
113
|
+
}}
|
|
114
|
+
bareSettings={{
|
|
115
|
+
bareNewDTW: true,
|
|
116
|
+
bareFlexFilters: filterMode === "flex",
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const SavedViews = () => (
|
|
125
|
+
<div className="w-full">
|
|
126
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
127
|
+
Data Template — saved views
|
|
128
|
+
</div>
|
|
129
|
+
<div className="text-sm mb-4 text-gray-600">
|
|
130
|
+
Offline, with 40 tasks. “Flex filters” uses the compact filter popup;
|
|
131
|
+
“Toolbar filters” shows the normal Owner and Due filters above the list
|
|
132
|
+
instead. Set a filter, a sort or hide a column, then “+ Save view” and
|
|
133
|
+
name it. Click a view to apply it, “All” to go back, and × to delete one.
|
|
134
|
+
Change something while a view is active and it shows “•” with an Update
|
|
135
|
+
action. Views belong to the tab they were saved on — the Board tab keeps
|
|
136
|
+
its own list. “In progress” and “Top priority” are provided by the app and
|
|
137
|
+
cannot be deleted. Views are kept in this browser, so they survive a
|
|
138
|
+
reload.
|
|
139
|
+
</div>
|
|
140
|
+
<SavedViewsDemo />
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { Menu, Transition } from "@headlessui/react";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import MockAdapter from "axios-mock-adapter";
|
|
4
|
+
import Papa from "papaparse";
|
|
5
|
+
import React, { useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { ButtonGroup } from "../../action/buttonGroup";
|
|
7
|
+
import { DataTemplate as DataTemplateImpl } from ".";
|
|
8
|
+
import { DataStructure } from "./model";
|
|
9
|
+
import { ExtendedTabs } from "./spaceModel";
|
|
10
|
+
|
|
11
|
+
const meta = { title: "Data/Data Template" };
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
// INFO: its own endpoint, so the mock never answers for another story
|
|
15
|
+
const DEMO_ENDPOINT = "https://dtw-scroll-demo.local/api";
|
|
16
|
+
const DEMO_COLLECTION = "demo-scroll-records";
|
|
17
|
+
const RECORD_COUNT = 60;
|
|
18
|
+
const STAGES = ["New", "In Progress", "Done"];
|
|
19
|
+
const OWNERS = ["Alice", "Ben", "Chitra", "Dan"];
|
|
20
|
+
|
|
21
|
+
const DEMO_SCHEMA: DataStructure[] = [
|
|
22
|
+
{ id: "name", name: "Record", type: "string" },
|
|
23
|
+
{ id: "stage", name: "Stage", type: "list", listSelection: STAGES },
|
|
24
|
+
{ id: "owner", name: "Owner", type: "string" },
|
|
25
|
+
{ id: "summary", name: "Summary", type: "string" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const TABS: ExtendedTabs[] = [
|
|
29
|
+
{ name: "List", type: "list", viewType: "list" },
|
|
30
|
+
{ name: "Grid", type: "list", viewType: "grid" },
|
|
31
|
+
{
|
|
32
|
+
name: "Kanban",
|
|
33
|
+
type: "list",
|
|
34
|
+
viewType: "kanban",
|
|
35
|
+
kanbanSettings: { title: "stage" },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "Feeds",
|
|
39
|
+
type: "list",
|
|
40
|
+
viewType: "feeds",
|
|
41
|
+
feedsSettings: { title: "name", content: "stage", desc: "summary" },
|
|
42
|
+
},
|
|
43
|
+
] as ExtendedTabs[];
|
|
44
|
+
|
|
45
|
+
const DTW_TOGGLE = [
|
|
46
|
+
{ id: "new", name: "New DTW" },
|
|
47
|
+
{ id: "legacy", name: "Legacy" },
|
|
48
|
+
];
|
|
49
|
+
const LAYOUT_TOGGLE = [
|
|
50
|
+
{ id: "desktop", name: "Desktop" },
|
|
51
|
+
{ id: "mobile", name: "Mobile" },
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const demoDevSettings: any = {
|
|
55
|
+
server: "mongoServer",
|
|
56
|
+
dbEndpoint: DEMO_ENDPOINT,
|
|
57
|
+
dbAPIToken: "",
|
|
58
|
+
dbUserToken: "",
|
|
59
|
+
schemaEndpoint: DEMO_ENDPOINT,
|
|
60
|
+
schemaEndpointToken: "",
|
|
61
|
+
imageEndpoint: "https://placehold.co/600x400/e2e8f0/475569.png?text=",
|
|
62
|
+
uploadEndpoint: "",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const renderDemoRecords = () =>
|
|
66
|
+
Array.from({ length: RECORD_COUNT }, (_, index) => {
|
|
67
|
+
const number = String(index + 1).padStart(2, "0");
|
|
68
|
+
return {
|
|
69
|
+
_id: `rec-${number}`,
|
|
70
|
+
name: `Record ${number}`,
|
|
71
|
+
stage: STAGES[index % STAGES.length],
|
|
72
|
+
owner: OWNERS[index % OWNERS.length],
|
|
73
|
+
summary: `Summary for record ${number}`,
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// INFO: Kanban queries per column — unfiltered, every card would appear in every column
|
|
78
|
+
const handleApplyQuery = (rows: any[], query: any[] = []) =>
|
|
79
|
+
query.reduce((filtered: any[], eachQuery: any) => {
|
|
80
|
+
if (!eachQuery?.queryId) return filtered;
|
|
81
|
+
if (eachQuery.type === "=" || eachQuery.type === "==")
|
|
82
|
+
return filtered.filter(
|
|
83
|
+
(eachRow) => eachRow[eachQuery.queryId] === eachQuery.value,
|
|
84
|
+
);
|
|
85
|
+
if (eachQuery.type === "in" && Array.isArray(eachQuery.value))
|
|
86
|
+
return filtered.filter((eachRow) =>
|
|
87
|
+
eachQuery.value.includes(eachRow[eachQuery.queryId]),
|
|
88
|
+
);
|
|
89
|
+
return filtered;
|
|
90
|
+
}, rows);
|
|
91
|
+
|
|
92
|
+
const handleInstallDemoMock = (store: any[]) => {
|
|
93
|
+
const mock = new MockAdapter(axios, {
|
|
94
|
+
onNoMatch: "passthrough",
|
|
95
|
+
delayResponse: 150,
|
|
96
|
+
});
|
|
97
|
+
const renderUrl = (type: string) =>
|
|
98
|
+
`${DEMO_ENDPOINT}/${DEMO_COLLECTION}/${type}`;
|
|
99
|
+
const handleParseBody = (config: any) => {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(config?.data ?? "{}");
|
|
102
|
+
} catch (err) {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
mock.onPost(renderUrl("list")).reply((config) => {
|
|
108
|
+
const body = handleParseBody(config);
|
|
109
|
+
const rows = handleApplyQuery(store, body.query);
|
|
110
|
+
const cursor = typeof body.cursor === "number" ? body.cursor : 0;
|
|
111
|
+
const limit = typeof body.limit === "number" ? body.limit : RECORD_COUNT;
|
|
112
|
+
return [
|
|
113
|
+
200,
|
|
114
|
+
{ data: rows.slice(cursor, cursor + limit), count: rows.length },
|
|
115
|
+
];
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
mock.onPost(renderUrl("get")).reply((config) => {
|
|
119
|
+
const body = handleParseBody(config);
|
|
120
|
+
return [200, { data: store.find((each) => each._id === body.id) ?? null }];
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// INFO: the tab bar waits on this lookup
|
|
124
|
+
mock
|
|
125
|
+
.onPost(`${DEMO_ENDPOINT}/spaces/list`)
|
|
126
|
+
.reply(200, { data: [], count: 0 });
|
|
127
|
+
|
|
128
|
+
return mock;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const ScrollRestoreDemo = () => {
|
|
132
|
+
const [typeParam, setTypeParam] = useState<"list" | "read" | "update">(
|
|
133
|
+
"list",
|
|
134
|
+
);
|
|
135
|
+
const [dataId, setDataId] = useState<string>("");
|
|
136
|
+
// INFO: kept apart from viewId — opening a record navigates with "general"
|
|
137
|
+
const [listViewId, setListViewId] = useState<string>("general");
|
|
138
|
+
const [dtwMode, setDtwMode] = useState<"new" | "legacy">("new");
|
|
139
|
+
const [layout, setLayout] = useState<"desktop" | "mobile">("desktop");
|
|
140
|
+
const [isMockReady, setIsMockReady] = useState(false);
|
|
141
|
+
|
|
142
|
+
const store = useMemo(() => renderDemoRecords(), []);
|
|
143
|
+
const isMobile = layout === "mobile";
|
|
144
|
+
const isNewDTW = dtwMode === "new";
|
|
145
|
+
|
|
146
|
+
// INFO: in an effect so it survives StrictMode's double mount
|
|
147
|
+
useEffect(() => {
|
|
148
|
+
const mock = handleInstallDemoMock(store);
|
|
149
|
+
setIsMockReady(true);
|
|
150
|
+
return () => {
|
|
151
|
+
mock.restore();
|
|
152
|
+
setIsMockReady(false);
|
|
153
|
+
};
|
|
154
|
+
}, [store]);
|
|
155
|
+
|
|
156
|
+
const handleNavigate = (type: string, id?: string, view?: string) => {
|
|
157
|
+
if (type === "list" && view) setListViewId(view);
|
|
158
|
+
setTypeParam(type as "list" | "read" | "update");
|
|
159
|
+
setDataId(id ?? "");
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const handleBackToList = () => handleNavigate("list", "", listViewId);
|
|
163
|
+
|
|
164
|
+
const renderToggles = () => (
|
|
165
|
+
<div className="mb-4 flex flex-wrap gap-3">
|
|
166
|
+
<ButtonGroup
|
|
167
|
+
selection={DTW_TOGGLE}
|
|
168
|
+
selectedId={dtwMode}
|
|
169
|
+
variant="tag"
|
|
170
|
+
size="small"
|
|
171
|
+
onClick={(id) => setDtwMode(id as "new" | "legacy")}
|
|
172
|
+
/>
|
|
173
|
+
<ButtonGroup
|
|
174
|
+
selection={LAYOUT_TOGGLE}
|
|
175
|
+
selectedId={layout}
|
|
176
|
+
variant="tag"
|
|
177
|
+
size="small"
|
|
178
|
+
onClick={(id) => setLayout(id as "desktop" | "mobile")}
|
|
179
|
+
/>
|
|
180
|
+
{typeParam !== "list" && (
|
|
181
|
+
<button
|
|
182
|
+
id="scroll-restore-back"
|
|
183
|
+
type="button"
|
|
184
|
+
className="rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-700 hover:bg-gray-50"
|
|
185
|
+
onClick={handleBackToList}
|
|
186
|
+
>
|
|
187
|
+
← Back to list
|
|
188
|
+
</button>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<div className="w-full">
|
|
195
|
+
{renderToggles()}
|
|
196
|
+
<div className={isMobile ? "mx-auto max-w-sm" : "w-full"}>
|
|
197
|
+
{/* INFO: keyed on the mode and layout so switching either one starts a
|
|
198
|
+
fresh DataTemplate instead of reusing state from the other set of
|
|
199
|
+
components */}
|
|
200
|
+
{isMockReady && (
|
|
201
|
+
<DataTemplateImpl
|
|
202
|
+
key={`${dtwMode}-${layout}`}
|
|
203
|
+
title="Scroll Restore"
|
|
204
|
+
description={`${RECORD_COUNT} offline records`}
|
|
205
|
+
type={typeParam}
|
|
206
|
+
id={dataId}
|
|
207
|
+
collectionId={DEMO_COLLECTION}
|
|
208
|
+
model={DEMO_SCHEMA}
|
|
209
|
+
localURLEndpoint=""
|
|
210
|
+
devSettings={demoDevSettings}
|
|
211
|
+
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
212
|
+
limit={RECORD_COUNT}
|
|
213
|
+
organise={{
|
|
214
|
+
extendedTabs: TABS,
|
|
215
|
+
viewId: typeParam === "list" ? listViewId : "general",
|
|
216
|
+
persistViewState: "session",
|
|
217
|
+
persistKey: listViewId,
|
|
218
|
+
handleNavigate,
|
|
219
|
+
}}
|
|
220
|
+
bareSettings={{
|
|
221
|
+
bareNewDTW: isNewDTW,
|
|
222
|
+
bareListMobileView: isMobile,
|
|
223
|
+
bareUpdateMobileView: isMobile,
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export const ScrollRestore = () => (
|
|
233
|
+
<div className="w-full">
|
|
234
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
235
|
+
Data Template — scroll position restore
|
|
236
|
+
</div>
|
|
237
|
+
<div className="text-sm mb-4 text-gray-600">
|
|
238
|
+
Offline, with {RECORD_COUNT} records. Pick a view, scroll well down, open
|
|
239
|
+
a record, then go back to the list. You should land on the record you
|
|
240
|
+
opened, not the top. Each view keeps its own position. Switch between New
|
|
241
|
+
DTW and Legacy to cover both sets of list components, and Mobile for the
|
|
242
|
+
mobile table cards and kanban tabs.
|
|
243
|
+
</div>
|
|
244
|
+
<ScrollRestoreDemo />
|
|
245
|
+
</div>
|
|
246
|
+
);
|