blixify-ui-web-mcp 0.1.19 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/action/iconButton/iconButton.stories.tsx +11 -2
- package/lib/components/auth/signUp/signUp.stories.tsx +39 -1
- package/lib/components/chart/statsCard/README.md +24 -0
- package/lib/components/chart/statsCard/statsCard.stories.tsx +68 -0
- package/lib/components/chat/chatBase/chatBase.stories.tsx +1 -1
- package/lib/components/data/dataGraph/dataGraph.stories.tsx +5 -5
- package/lib/components/data/dataTemplate/README.md +204 -85
- package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +307 -295
- package/lib/components/data/dataTemplate/drive.stories.tsx +197 -0
- package/lib/components/data/dataWrapper/README.md +109 -22
- package/lib/components/data/dataWrapper/dataWrapper.bareHooks.stories.tsx +951 -0
- package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +356 -5
- package/lib/components/data/dataWrapper/dataWrapper.view.stories.tsx +79 -5
- package/lib/components/display/calendar/calendar.stories.tsx +1 -1
- package/lib/components/display/detailList/README.md +11 -10
- package/lib/components/display/detailList/detailList.stories.tsx +16 -3
- package/lib/components/display/feeds/README.md +3 -3
- package/lib/components/display/feeds/feeds.stories.tsx +3 -3
- package/lib/components/display/filePreview/filePreview.stories.tsx +111 -0
- package/lib/components/display/modal/README.md +3 -0
- package/lib/components/display/modal/modal.stories.tsx +50 -1
- package/lib/components/display/newCalendar/README.md +0 -6
- package/lib/components/display/newCalendar/newCalendar.stories.tsx +1 -1
- package/lib/components/display/newFeeds/README.md +1 -5
- package/lib/components/display/newFeeds/newFeeds.stories.tsx +3 -3
- package/lib/components/display/newKanban/README.md +0 -7
- package/lib/components/display/newList/README.md +1 -4
- package/lib/components/display/newTable/README.md +29 -9
- package/lib/components/display/newTable/newTable.stories.tsx +120 -9
- package/lib/components/display/tag/README.md +1 -1
- package/lib/components/display/tag/tag.stories.tsx +7 -0
- package/lib/components/input/markdown/markdown.stories.tsx +3 -1
- package/lib/components/input/uploadInput/uploadInput.stories.tsx +41 -0
- package/lib/components/navigation/leftNav/leftNav.stories.tsx +175 -0
- package/lib/components/navigation/tabs/tabs.stories.tsx +23 -0
- package/lib/components/template/calendarView/calendarView.stories.tsx +4 -4
- package/lib/components/template/chatDashboard/chatDashboard.stories.tsx +2 -2
- package/lib/mcp/tools/getBackendComponent.d.ts.map +1 -1
- package/lib/mcp/tools/getBackendComponent.js +7 -3
- package/lib/mcp/tools/getBackendComponent.js.map +1 -1
- package/lib/mcp/tools/suggestTemplate.js +3 -3
- package/lib/mcp/tools/suggestTemplate.js.map +1 -1
- package/package.json +1 -1
- package/lib/components/data/dataTemplate/dataModule.stories.tsx +0 -191
- package/lib/components/data/dataWrapper/dataWrapper.consumer.stories.tsx +0 -2125
- package/lib/components/data/dataWrapper/dataWrapper.utils.stories.tsx +0 -576
- package/lib/components/data/flexFilter/flexFilter.stories.tsx +0 -171
|
@@ -1,323 +1,335 @@
|
|
|
1
1
|
import { Menu, Transition } from "@headlessui/react";
|
|
2
|
-
import { TagIcon } from "@heroicons/react/24/outline";
|
|
3
2
|
import axios from "axios";
|
|
3
|
+
import MockAdapter from "axios-mock-adapter";
|
|
4
4
|
import Papa from "papaparse";
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useState } from "react";
|
|
6
|
+
import { ButtonGroup } from "../../action/buttonGroup";
|
|
6
7
|
import { DataTemplate as DataTemplateImpl } from ".";
|
|
7
|
-
import { Button } from "../../action/button";
|
|
8
|
-
import { Loading } from "../../display/loading";
|
|
9
|
-
import { StorySection } from "../../storyUtils";
|
|
10
|
-
import { Sort } from "../readQuery";
|
|
11
|
-
import {
|
|
12
|
-
dbAPIToken,
|
|
13
|
-
defaultDBEndpoint,
|
|
14
|
-
defaultImageEndpoint,
|
|
15
|
-
defaultUploadEndpoint,
|
|
16
|
-
getAllSpacesList,
|
|
17
|
-
getSpaceData,
|
|
18
|
-
getWorkspaceData,
|
|
19
|
-
schemaUrl,
|
|
20
|
-
storyRenderReferenceLabelOptions,
|
|
21
|
-
storyRenderReferenceSpaceOptions,
|
|
22
|
-
} from "../stories";
|
|
23
|
-
import { blixifyDevCollection, blixifyDevWorkspace } from "../testData";
|
|
24
|
-
import { parseSpaceToDataStructure, parseWorkspaceSettings } from "../utils";
|
|
25
8
|
import { DataStructure } from "./model";
|
|
26
|
-
import {
|
|
27
|
-
import { Workspace } from "./workspaceModel";
|
|
9
|
+
import { ExtendedTabs } from "./spaceModel";
|
|
28
10
|
|
|
29
11
|
const meta = { title: "Data/Data Template" };
|
|
30
12
|
export default meta;
|
|
31
13
|
|
|
32
|
-
|
|
33
|
-
const [selectedSpace, setSelectedSpace] = useState<Space | null>(null);
|
|
34
|
-
const [allWorkspaceSpace, setAllWorkspaceSpace] = useState<Space[]>([]);
|
|
35
|
-
const [selectedWorkspace, setSelectedWorkspace] = useState<Workspace | null>(
|
|
36
|
-
null,
|
|
37
|
-
);
|
|
38
|
-
const [mount, setMount] = useState<boolean>(false);
|
|
39
|
-
const [sort, setSort] = useState<Sort | undefined>({
|
|
40
|
-
sortId: "baseUpdatedAt",
|
|
41
|
-
type: "desc",
|
|
42
|
-
});
|
|
43
|
-
const [tabs, setTabs] = useState<ExtendedTabs[]>([]);
|
|
44
|
-
const [queries, setQueries] = useState<AdditionalQuery[]>([]);
|
|
45
|
-
const [collectionParam, setCollectionParams] = useState(blixifyDevCollection);
|
|
46
|
-
const workspaceParam = blixifyDevWorkspace;
|
|
14
|
+
const DEMO_ENDPOINT = "https://dt-demo.local/api";
|
|
47
15
|
|
|
48
|
-
|
|
16
|
+
const LAYOUT_TOGGLE = [
|
|
17
|
+
{ id: "desktop", name: "Desktop" },
|
|
18
|
+
{ id: "mobile", name: "Mobile" },
|
|
19
|
+
];
|
|
20
|
+
const DEMO_COLLECTION = "demo-documents";
|
|
49
21
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
22
|
+
// INFO: two file columns on purpose. A view unwinds ONE of them, chosen when
|
|
23
|
+
// the tab is made — which is why the column cannot be inferred and the Add Tab
|
|
24
|
+
// picker has to ask.
|
|
25
|
+
// INFO: a single image column holds the download TOKEN, not a url — the
|
|
26
|
+
// file's name lives on the column (imageFileName) and its folder on
|
|
27
|
+
// imageCollectionName + the row id. That is what a real datasheet stores, and
|
|
28
|
+
// it is the shape the file view had to learn to render.
|
|
29
|
+
const DEMO_SCHEMA: DataStructure[] = [
|
|
30
|
+
{ id: "name", name: "Document", type: "string" },
|
|
31
|
+
{ id: "owner", name: "Owner", type: "string" },
|
|
32
|
+
{ id: "files", name: "Attachments", type: "multipleFile" },
|
|
33
|
+
{ id: "scans", name: "Scans", type: "multipleFile" },
|
|
34
|
+
// INFO: a SINGLE file field stores only the token — its name comes from the
|
|
35
|
+
// schema (imageFileName), which is fixed config rather than the user's file.
|
|
36
|
+
// That is why the read view shows the field's LABEL here and the entry's own
|
|
37
|
+
// name for a multiple field.
|
|
38
|
+
{
|
|
39
|
+
id: "contract",
|
|
40
|
+
name: "Signed Contract",
|
|
41
|
+
type: "file",
|
|
42
|
+
imageFileName: "contract.pdf",
|
|
43
|
+
imageCollectionName: DEMO_COLLECTION,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "photo",
|
|
47
|
+
name: "Cover Photo",
|
|
48
|
+
type: "image",
|
|
49
|
+
imageFileName: "photo.png",
|
|
50
|
+
imageCollectionName: DEMO_COLLECTION,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
const renderDemoRecords = () => [
|
|
55
|
+
{
|
|
56
|
+
_id: "r1",
|
|
57
|
+
name: "Handbook",
|
|
58
|
+
owner: "alice",
|
|
59
|
+
files: [
|
|
60
|
+
{
|
|
61
|
+
fileName: "https://cdn.local/1757238400000_Onboarding.pdf",
|
|
62
|
+
token: "a",
|
|
63
|
+
},
|
|
64
|
+
{ fileName: "https://cdn.local/1757238400001_Policies.pdf", token: "b" },
|
|
65
|
+
{
|
|
66
|
+
fileName:
|
|
67
|
+
"https://images.pexels.com/photos/2338407/pexels-photo-2338407.jpeg?auto=compress&cs=tinysrgb&w=600",
|
|
68
|
+
token: "c",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
scans: [
|
|
72
|
+
{ fileName: "https://cdn.local/scan_front.png", token: "s1" },
|
|
73
|
+
// INFO: a multiple entry as storage names it — <uuid>-<name> so entries
|
|
74
|
+
// never collide. The tile must show "Site Plan.pdf", not the uuid.
|
|
75
|
+
{
|
|
76
|
+
fileName: "3f2b1c6a-1111-4222-8333-444455556666-Site Plan.pdf",
|
|
77
|
+
token: "s2",
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
photo: "token-r1",
|
|
81
|
+
contract: "token-c1",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
_id: "r2",
|
|
85
|
+
name: "Proposals",
|
|
86
|
+
owner: "bob",
|
|
87
|
+
files: [
|
|
88
|
+
{
|
|
89
|
+
fileName: "https://cdn.local/1757238400002_Q3%20Budget.xlsx",
|
|
90
|
+
token: "d",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
fileName: "https://cdn.local/1757238400003_Walkthrough.mp4",
|
|
94
|
+
token: "e",
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
scans: [],
|
|
98
|
+
photo: "token-r2",
|
|
99
|
+
contract: "token-c2",
|
|
100
|
+
},
|
|
101
|
+
// INFO: no attachments — contributes NO tiles. A blank tile per empty record
|
|
102
|
+
// would bury the ones that matter, which is why the unwind does not preserve
|
|
103
|
+
// empty arrays.
|
|
104
|
+
{ _id: "r3", name: "Archive", owner: "carol", files: [], scans: [] },
|
|
105
|
+
];
|
|
57
106
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
setTabs(space.extendedTabs ?? []);
|
|
77
|
-
setQueries(space?.additionalQuery ?? []);
|
|
78
|
-
if (space?.sortBy?.sortId !== "") {
|
|
79
|
-
setSort(space.sortBy ?? undefined);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (workspaceParam) {
|
|
84
|
-
const workspace: any = await getWorkspaceData(workspaceParam);
|
|
85
|
-
if (workspace) {
|
|
86
|
-
setSelectedWorkspace(workspace);
|
|
87
|
-
const allSpaces = await getAllSpacesList(workspaceParam);
|
|
88
|
-
if (allSpaces) setAllWorkspaceSpace(allSpaces);
|
|
89
|
-
}
|
|
107
|
+
/**
|
|
108
|
+
* INFO: the mock implements the SERVER's unwind rather than working around it —
|
|
109
|
+
* one row per element, and a count taken after the unwind and before paging. A
|
|
110
|
+
* mock that returned whole records would make the story look right while
|
|
111
|
+
* proving nothing about the thing on screen.
|
|
112
|
+
*/
|
|
113
|
+
const handleInstallDemoMock = (store: any[]) => {
|
|
114
|
+
const mock = new MockAdapter(axios, {
|
|
115
|
+
onNoMatch: "passthrough",
|
|
116
|
+
delayResponse: 120,
|
|
117
|
+
});
|
|
118
|
+
const renderUrl = (type: string) =>
|
|
119
|
+
`${DEMO_ENDPOINT}/${DEMO_COLLECTION}/${type}`;
|
|
120
|
+
const handleParseBody = (config: any) => {
|
|
121
|
+
try {
|
|
122
|
+
return JSON.parse(config?.data ?? "{}");
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return {};
|
|
90
125
|
}
|
|
91
|
-
setMount(true);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const handleSortData = (sortValue: Sort) => {
|
|
95
|
-
setSort(sortValue.sortId !== "" ? sortValue : undefined);
|
|
96
126
|
};
|
|
97
127
|
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
)
|
|
128
|
+
mock.onPost(renderUrl("list")).reply((config) => {
|
|
129
|
+
const body = handleParseBody(config);
|
|
130
|
+
let rows: any[] = store;
|
|
131
|
+
if (body.unwind) {
|
|
132
|
+
// INFO: like the server's $unwind — an array becomes one row per
|
|
133
|
+
// element, a single value passes through as one row, and a record
|
|
134
|
+
// without the field contributes nothing
|
|
135
|
+
rows = store.flatMap((eachRecord: any) => {
|
|
136
|
+
const value = eachRecord[body.unwind];
|
|
137
|
+
const entries = Array.isArray(value) ? value : value ? [value] : [];
|
|
138
|
+
return entries.map((eachFile: any) => ({
|
|
139
|
+
...eachRecord,
|
|
140
|
+
[body.unwind]: eachFile,
|
|
141
|
+
}));
|
|
142
|
+
});
|
|
106
143
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
144
|
+
const cursor = typeof body.cursor === "number" ? body.cursor : 0;
|
|
145
|
+
const limit = typeof body.limit === "number" ? body.limit : 10;
|
|
146
|
+
return [
|
|
147
|
+
200,
|
|
148
|
+
{ data: rows.slice(cursor, cursor + limit), count: rows.length },
|
|
149
|
+
];
|
|
150
|
+
});
|
|
110
151
|
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const selectedIndex = selectedDS.findIndex((item) => item.id === dragId);
|
|
116
|
-
const [selectedItem] = selectedDS.splice(selectedIndex, 1);
|
|
117
|
-
const placingIndex = selectedDS.findIndex((item) => item.id === dropId);
|
|
118
|
-
selectedDS.splice(placingIndex, 0, selectedItem);
|
|
119
|
-
const clonedSpace = JSON.parse(JSON.stringify(selectedSpace));
|
|
120
|
-
if (clonedSpace) clonedSpace["structure"] = selectedDS;
|
|
121
|
-
setSelectedSpace(clonedSpace);
|
|
122
|
-
};
|
|
152
|
+
mock.onPost(renderUrl("get")).reply((config) => {
|
|
153
|
+
const body = handleParseBody(config);
|
|
154
|
+
return [200, { data: store.find((each) => each._id === body.id) ?? null }];
|
|
155
|
+
});
|
|
123
156
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
});
|
|
135
|
-
setSelectedSpace(clonedSpace);
|
|
136
|
-
};
|
|
157
|
+
// INFO: the store is mutated in place so a create or an edit persists for the
|
|
158
|
+
// life of the story. Replying from a static array would make every write look
|
|
159
|
+
// like it silently did nothing.
|
|
160
|
+
// INFO: with a schemaEndpoint set, DataTemplate looks up sibling datasheets
|
|
161
|
+
// to offer as reference targets. Nothing in this demo references another
|
|
162
|
+
// space, so an empty list is the honest answer — but the call has to succeed
|
|
163
|
+
// or the tab bar never finishes rendering.
|
|
164
|
+
mock
|
|
165
|
+
.onPost(`${DEMO_ENDPOINT}/spaces/list`)
|
|
166
|
+
.reply(200, { data: [], count: 0 });
|
|
137
167
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
168
|
+
mock.onPost(renderUrl("create")).reply((config) => {
|
|
169
|
+
const body = handleParseBody(config);
|
|
170
|
+
const record = {
|
|
171
|
+
_id: `demo-${Date.now()}`,
|
|
172
|
+
files: [],
|
|
173
|
+
scans: [],
|
|
174
|
+
...body.data,
|
|
175
|
+
};
|
|
176
|
+
store.push(record);
|
|
177
|
+
return [200, { data: record }];
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
mock.onPost(renderUrl("update")).reply((config) => {
|
|
181
|
+
const body = handleParseBody(config);
|
|
182
|
+
const index = store.findIndex((each) => each._id === body.id);
|
|
183
|
+
if (index >= 0) store[index] = { ...store[index], ...body.data };
|
|
184
|
+
return [200, { data: store[index] ?? null }];
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
mock.onPost(renderUrl("delete")).reply((config) => {
|
|
188
|
+
const body = handleParseBody(config);
|
|
189
|
+
const index = store.findIndex((each) => each._id === body.id);
|
|
190
|
+
if (index >= 0) store.splice(index, 1);
|
|
191
|
+
return [200, { data: true }];
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return mock;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const demoDevSettings: any = {
|
|
198
|
+
server: "mongoServer",
|
|
199
|
+
dbEndpoint: DEMO_ENDPOINT,
|
|
200
|
+
dbAPIToken: "",
|
|
201
|
+
dbUserToken: "",
|
|
202
|
+
// INFO: schemaPresent is `devSettings.schemaEndpoint ? true : false`, and it
|
|
203
|
+
// gates the ENTIRE tab bar — an empty string here hides tabs and + New Tab
|
|
204
|
+
// with no other symptom. It points at the demo endpoint, which answers the
|
|
205
|
+
// spaces lookup below.
|
|
206
|
+
schemaEndpoint: DEMO_ENDPOINT,
|
|
207
|
+
schemaEndpointToken: "",
|
|
208
|
+
// INFO: a stored token is turned into <imageEndpoint><folder>%2F<row>%2F<name>
|
|
209
|
+
// ?alt=media&token=… — the same shape as our storage. A placeholder host
|
|
210
|
+
// that draws whatever path it is given stands in for the bucket, so the
|
|
211
|
+
// tile shows a real image rather than a broken one.
|
|
212
|
+
imageEndpoint: "https://placehold.co/600x400/e2e8f0/475569.png?text=",
|
|
213
|
+
uploadEndpoint: "",
|
|
214
|
+
};
|
|
141
215
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
216
|
+
const DataTemplateDemo = () => {
|
|
217
|
+
const store = React.useMemo(() => renderDemoRecords(), []);
|
|
218
|
+
React.useMemo(() => handleInstallDemoMock(store), [store]);
|
|
145
219
|
|
|
146
|
-
|
|
220
|
+
// INFO: a real consumer keeps type and id in its own state and hands them
|
|
221
|
+
// back — that is what makes create and read views open at all. The story does
|
|
222
|
+
// the same rather than intercepting the navigation, which is why an alert
|
|
223
|
+
// stood in for a create form before.
|
|
224
|
+
const [typeParam, setTypeParam] = useState<"list" | "read" | "update">(
|
|
225
|
+
"list",
|
|
226
|
+
);
|
|
227
|
+
const [dataId, setDataId] = useState<string>("");
|
|
228
|
+
const [layout, setLayout] = useState<"desktop" | "mobile">("desktop");
|
|
229
|
+
const isMobile = layout === "mobile";
|
|
230
|
+
const [viewId, setViewId] = useState<string | undefined>(undefined);
|
|
231
|
+
const [tabs, setTabs] = useState<ExtendedTabs[]>([
|
|
147
232
|
{
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
233
|
+
name: "Documents",
|
|
234
|
+
type: "list",
|
|
235
|
+
viewType: "file",
|
|
236
|
+
defaultTab: true,
|
|
237
|
+
fileSettings: { columnId: "files" },
|
|
151
238
|
},
|
|
152
|
-
|
|
153
|
-
|
|
239
|
+
{
|
|
240
|
+
name: "Scans",
|
|
241
|
+
type: "list",
|
|
242
|
+
viewType: "file",
|
|
243
|
+
fileSettings: { columnId: "scans" },
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "Photos",
|
|
247
|
+
type: "list",
|
|
248
|
+
viewType: "file",
|
|
249
|
+
fileSettings: { columnId: "photo" },
|
|
250
|
+
},
|
|
251
|
+
]);
|
|
154
252
|
|
|
155
253
|
return (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
>
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
deleteBtn: true,
|
|
216
|
-
disabledUpdate: false,
|
|
217
|
-
disableBatchUpdate: false,
|
|
218
|
-
activityLog: {
|
|
219
|
-
enableComments: true,
|
|
220
|
-
commentCollectionId: collectionParam + "-comments",
|
|
221
|
-
enableLogs: true,
|
|
222
|
-
logsCollectionId: collectionParam + "-logs",
|
|
223
|
-
userAPIEndpoint: "http://localhost:3000/api/data/users",
|
|
224
|
-
currentUserId: "3P3il3L8uScSkASU2MdAoqGs1M82",
|
|
225
|
-
},
|
|
226
|
-
}}
|
|
227
|
-
organise={{
|
|
228
|
-
sort: sort ?? undefined,
|
|
229
|
-
viewId: viewParam,
|
|
230
|
-
extendedTabs: tabs,
|
|
231
|
-
additionalQuery: queries,
|
|
232
|
-
splitView: false,
|
|
233
|
-
showGroupByFeature: true,
|
|
234
|
-
showReadFloatingNav: true,
|
|
235
|
-
disableMulti: false,
|
|
236
|
-
exportSettings: {
|
|
237
|
-
enableExportReadPDF: true,
|
|
238
|
-
exportReadPDFDimensions: {
|
|
239
|
-
forceDesktopView: true,
|
|
240
|
-
captureWidth: 1440,
|
|
241
|
-
captureHeight: 900,
|
|
242
|
-
pageSize: "a4",
|
|
243
|
-
scale: 2,
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
filterComponentNewRow: true,
|
|
247
|
-
addColumnEnabled: true,
|
|
248
|
-
onChangeSort: handleSortData,
|
|
249
|
-
onChangeTabs: handleTab,
|
|
250
|
-
onChangeDrag: handleDrag,
|
|
251
|
-
onAddColumns: handleAddColumn,
|
|
252
|
-
onChangeHide: handleHide,
|
|
253
|
-
handleNavigate: (type, selectedId, viewId) => {
|
|
254
|
-
setTypeParam(type);
|
|
255
|
-
if (selectedId) setDataParam(selectedId);
|
|
256
|
-
if (viewId) setViewParam(viewId);
|
|
257
|
-
},
|
|
258
|
-
renderReferenceLabelOptions: storyRenderReferenceLabelOptions(
|
|
259
|
-
allWorkspaceSpace,
|
|
260
|
-
selectedSpace,
|
|
261
|
-
),
|
|
262
|
-
renderReferenceSpaceOptions: storyRenderReferenceSpaceOptions(
|
|
263
|
-
allWorkspaceSpace,
|
|
264
|
-
dataParam,
|
|
265
|
-
),
|
|
266
|
-
}}
|
|
267
|
-
lib={{
|
|
268
|
-
axios,
|
|
269
|
-
Menu,
|
|
270
|
-
Transition,
|
|
271
|
-
navigate: null,
|
|
272
|
-
papaParser: Papa,
|
|
273
|
-
}}
|
|
274
|
-
bareSettings={{
|
|
275
|
-
// Custom action buttons shown on each list row
|
|
276
|
-
bareListActionBtns: () => [
|
|
277
|
-
<div
|
|
278
|
-
key="tag"
|
|
279
|
-
onClick={(e) => {
|
|
280
|
-
e.stopPropagation();
|
|
281
|
-
e.preventDefault();
|
|
282
|
-
window.alert("Custom row action clicked!");
|
|
283
|
-
}}
|
|
284
|
-
>
|
|
285
|
-
<TagIcon className="w-5 h-5 text-primary-500" />
|
|
286
|
-
</div>,
|
|
287
|
-
],
|
|
288
|
-
// Extra bulk-action buttons shown when rows are selected
|
|
289
|
-
bareListMultipleSelectAddtionalActions: [
|
|
290
|
-
{
|
|
291
|
-
text: "Custom Bulk Action",
|
|
292
|
-
onClick: (ids: string[]) =>
|
|
293
|
-
window.alert(
|
|
294
|
-
`Bulk action on ${ids.length} selected item(s)`,
|
|
295
|
-
),
|
|
296
|
-
},
|
|
297
|
-
],
|
|
298
|
-
}}
|
|
299
|
-
devSettings={{
|
|
300
|
-
...devSettings,
|
|
301
|
-
uploadEndpoint: defaultUploadEndpoint,
|
|
302
|
-
imageEndpoint: defaultImageEndpoint,
|
|
303
|
-
schemaEndpoint: schemaUrl,
|
|
304
|
-
dbAPIToken: dbAPIToken,
|
|
305
|
-
organisationMembers: [
|
|
306
|
-
{ key: "3P3il3L8uScSkASU2MdAoqGs1M82", label: "Crystal" },
|
|
307
|
-
{ key: "a2vG49kXjFg7hzzEYS5ZfJJJ43C3", label: "Chan Yi" },
|
|
308
|
-
{ key: "qFZVckKT3tMIkxvohlVS27IWuhn1", label: "Chris" },
|
|
309
|
-
],
|
|
310
|
-
currentUser: {
|
|
311
|
-
userEmail: "chanyi@blixify.co",
|
|
312
|
-
userName: "Chan Yi",
|
|
313
|
-
},
|
|
314
|
-
functionEndpoint:
|
|
315
|
-
"http://127.0.0.1:5001/blixify-space/asia-southeast1",
|
|
316
|
-
}}
|
|
317
|
-
/>
|
|
318
|
-
</div>
|
|
319
|
-
)}
|
|
320
|
-
</StorySection>
|
|
321
|
-
</>
|
|
254
|
+
<div className="w-full">
|
|
255
|
+
<div className="mb-4">
|
|
256
|
+
<ButtonGroup
|
|
257
|
+
selection={LAYOUT_TOGGLE}
|
|
258
|
+
selectedId={layout}
|
|
259
|
+
variant="tag"
|
|
260
|
+
size="small"
|
|
261
|
+
onClick={(id) => setLayout(id as "desktop" | "mobile")}
|
|
262
|
+
/>
|
|
263
|
+
</div>
|
|
264
|
+
{/* INFO: the PROPS force the mobile layout — NewTable's mobileView renders
|
|
265
|
+
cards regardless of viewport. The width only makes it read as a
|
|
266
|
+
phone; without the props, narrowing alone would show a squashed
|
|
267
|
+
desktop table, since Tailwind breakpoints key off the viewport. */}
|
|
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>
|
|
322
313
|
);
|
|
323
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
|
+
);
|