blixify-ui-web-mcp 0.1.75 → 0.1.76
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 +2 -1
- package/lib/components/data/dataTemplate/dataTemplate.groupedBoard.stories.tsx +88 -36
- package/lib/components/data/dataTemplate/dataTemplate.readDrawer.stories.tsx +334 -0
- package/lib/components/display/newList/README.md +30 -0
- package/lib/components/display/newTable/README.md +25 -20
- package/lib/components/display/newTable/newTable.stories.tsx +67 -0
- package/package.json +1 -1
|
@@ -480,7 +480,7 @@ Under `bareNewDTW` the grid hides `Grid`'s own page buttons and renders the same
|
|
|
480
480
|
|
|
481
481
|
Set `organise.viewId="grid"` (or add an `extendedTabs` entry with `viewType="grid"`) to render records as cards. Use `bareSettings.bareListGridComponent` to fully customise each card, or use the built-in grid. `viewImage` is optional: point it at an image or multipleImage column to show a picture on each card, or leave it empty for cards with only the title and fields. The Add Tab modal offers **Grid** on every space, and shows the Preview Image picker only when the space has an image column.
|
|
482
482
|
|
|
483
|
-
The list, kanban and grid items show the view's visible columns in column order, with the table's cell renderers. The title column is the item's title, the grid's image column is its picture, and a column with nothing stored is left out. A button, timer or rating is always
|
|
483
|
+
The list, kanban and grid items show the view's visible columns in column order, with the table's cell renderers. Under `bareNewDTW` a list item is a two-line row like a mail inbox (`NewList` `compact`): the first list column as a pill leading the second line, the first date column as a short date at the right, the next two columns that hold something on a grey line after the pill, and every field behind the row's arrow. On a wide screen the row is one line: title, pill, values, date. The grouped column is left off rows under group sections. `bareListDescription` still renders each value, the row's date included, and `bareListComponent` still replaces the row. The title column is the item's title, the grid's image column is its picture, and a column with nothing stored is left out. A button, timer or rating is left off the row's second line and shown behind its arrow; grid and kanban cards always show them. Each item carries the edit pencil (only for users who can update) and the comment bubble (when `edit.activityLog` is on) beside its title. The table also shows a chevron beside each title that opens the read view, for anyone who can see the record when `organise.handleNavigate` is set, since an editable title cell is hard to click open; its pencil and bubble sit in divided columns at the title cell's edge.
|
|
484
484
|
|
|
485
485
|
```tsx
|
|
486
486
|
<DataTemplate
|
|
@@ -766,6 +766,7 @@ Column types with monday.com counterparts. The value maths is in `src/components
|
|
|
766
766
|
| Time tracking | `type: "timeTracking"` | `{ totalSeconds, running, startedAt?, sessions? }` | table, read view and the record form show the total with start / stop, ticking each second inside the cell only; the form draws it as a field box, read-only on a record not created yet or without update rights. Start / stop saves at once, apart from Submit, and a form save never sends the timer. With `organise.onTimeTrackingAction` a click sends only `"start"` / `"stop"` and shows the value the host's server stored, timed on the server's clock (the cell ticks at once and is corrected to the reply, or put back if it fails); without it the cell works the value out and saves that one field. `handleStartTimeTracking` / `handleStopTimeTracking` are idempotent, so a flow can drive the same value. Sorts on `<id>.totalSeconds`; formulas read hours (`handleGetTimeTrackingHours`) |
|
|
767
767
|
| Date range | `type: "dateRange"`, `minDate?`, `maxDate?` | `{ start, end }` | `12 – 18 Nov`; a range picker in the form; in an editable table the whole cell opens it, with the inline date cell's border on hover; filters and sorts on `<id>.start`. A calendar or timeline tab can use `<id>.start` and `<id>.end` as its dates |
|
|
768
768
|
| Rating | `type: "number"`, `ratingMax` | a number | stars in every view; a tap saves the number, tapping the current rating clears it |
|
|
769
|
+
| Lookup | `type: "lookup"`, `referenceFieldId`, `lookupFieldId`, `lookupFieldType?`, `lookupFieldSettings?` | the linked record's `lookupFieldId` value, written by the host's server | every view draws it as `lookupFieldType` (a `multipleList` through a `listReference`) with `lookupFieldSettings`, disabled and optional; `organise.onAddColumns` gets it back as stored, with only its name, width and visibility taken from the table; the column dialog edits it as a lookup |
|
|
769
770
|
|
|
770
771
|
`bareSettings.bareSystemColumns` adds the server-stamped columns without declaring them: Created At / By (`baseCreatedAt`, `baseCreatedBy`), Updated At / By (`baseUpdatedAt`, `baseUpdatedBy`), Record ID (the record's `_id`) and Number (`baseNumber`). They start hidden; showing one is kept per view (`shownSystemColumnIds`) and never written back through `onChangeHide` or `onChangeDrag`. They are read-only, sortable and filterable, empty on a record the server never stamped, and a form save never sends them (nor a timer or a button).
|
|
771
772
|
|
|
@@ -106,6 +106,8 @@ const handleInstallMock = (rows: any[]) => {
|
|
|
106
106
|
inFlight: 0,
|
|
107
107
|
maxInFlight: 0,
|
|
108
108
|
});
|
|
109
|
+
// INFO: e2e reads how many table rows were drawn
|
|
110
|
+
(window as any).__newTableRowDraws = { count: 0 };
|
|
109
111
|
const handleDelay = () => new Promise((resolve) => setTimeout(resolve, 400));
|
|
110
112
|
mock.onPost(`${ENDPOINT}/${COLLECTION}/list`).reply(async (config) => {
|
|
111
113
|
const body = JSON.parse(config.data ?? "{}");
|
|
@@ -155,6 +157,12 @@ const handleInstallMock = (rows: any[]) => {
|
|
|
155
157
|
rows.unshift(row);
|
|
156
158
|
return [200, { data: row._id }];
|
|
157
159
|
});
|
|
160
|
+
mock.onPost(`${ENDPOINT}/${COLLECTION}/update`).reply((config) => {
|
|
161
|
+
const body = JSON.parse(config.data ?? "{}");
|
|
162
|
+
const row = rows.find((each) => each._id === body.id);
|
|
163
|
+
if (row) Object.assign(row, body.data ?? {});
|
|
164
|
+
return [200, { data: body.id }];
|
|
165
|
+
});
|
|
158
166
|
mock.onPost(`${ENDPOINT}/${COLLECTION}/get`).reply((config) => {
|
|
159
167
|
const body = JSON.parse(config.data ?? "{}");
|
|
160
168
|
return [200, { data: rows.find((row) => row._id === body.id) ?? null }];
|
|
@@ -181,8 +189,9 @@ const VIEW_NAMES = [
|
|
|
181
189
|
"1.42 Wang's Lead Board · Drawing",
|
|
182
190
|
];
|
|
183
191
|
|
|
184
|
-
const GroupedBoardDemo = () => {
|
|
192
|
+
const GroupedBoardDemo = (props: { isPhone?: boolean }) => {
|
|
185
193
|
const rows = useMemo(() => handleBuildRows(), []);
|
|
194
|
+
const [openedId, setOpenedId] = useState("");
|
|
186
195
|
useMemo(() => handleInstallMock(rows), [rows]);
|
|
187
196
|
const [sharedViews, setSharedViews] = useState<SharedSavedView[]>(() => [
|
|
188
197
|
{
|
|
@@ -204,40 +213,61 @@ const GroupedBoardDemo = () => {
|
|
|
204
213
|
]);
|
|
205
214
|
|
|
206
215
|
return (
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
216
|
+
<>
|
|
217
|
+
{props.isPhone && (
|
|
218
|
+
<div data-testid="grouped-board-opened" className="mb-2 text-xs">
|
|
219
|
+
{openedId ? `Opened ${openedId}` : ""}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
<DataTemplateImpl
|
|
223
|
+
title="Master Leads"
|
|
224
|
+
description="148,936 leads grouped by state"
|
|
225
|
+
type="list"
|
|
226
|
+
collectionId={COLLECTION}
|
|
227
|
+
model={SCHEMA}
|
|
228
|
+
localURLEndpoint=""
|
|
229
|
+
devSettings={{
|
|
230
|
+
server: "mongoServer",
|
|
231
|
+
dbEndpoint: ENDPOINT,
|
|
232
|
+
dbAPIToken: "",
|
|
233
|
+
dbUserToken: "",
|
|
234
|
+
schemaEndpoint: "",
|
|
235
|
+
imageEndpoint: "",
|
|
236
|
+
}}
|
|
237
|
+
lib={{ axios, Menu, Transition }}
|
|
238
|
+
edit={{ listAddable: true, listEditable: true }}
|
|
239
|
+
limit={50}
|
|
240
|
+
bareSettings={{
|
|
241
|
+
bareNewDTW: true,
|
|
242
|
+
bareFlexFilters: true,
|
|
243
|
+
bareListMobileView: props.isPhone,
|
|
244
|
+
// INFO: rows can be ticked on the phone board
|
|
245
|
+
...(props.isPhone
|
|
246
|
+
? { bareListMultipleSelectAddtionalActions: [] }
|
|
247
|
+
: {}),
|
|
248
|
+
}}
|
|
249
|
+
organise={{
|
|
250
|
+
viewId: "general",
|
|
251
|
+
datasheetDefaults: true,
|
|
252
|
+
showGroupByFeature: true,
|
|
253
|
+
groupBy: "area",
|
|
254
|
+
paginationType: "scroll",
|
|
255
|
+
savedViews: true,
|
|
256
|
+
sharedViews,
|
|
257
|
+
onChangeSharedViews: setSharedViews,
|
|
258
|
+
renderGroupByOptions: () => [
|
|
259
|
+
{ key: "area", label: "State (Area)" },
|
|
260
|
+
{ key: "status", label: "Status" },
|
|
261
|
+
],
|
|
262
|
+
...(props.isPhone
|
|
263
|
+
? {
|
|
264
|
+
handleNavigate: (_type: string, id?: string) =>
|
|
265
|
+
setOpenedId(id ?? ""),
|
|
266
|
+
}
|
|
267
|
+
: {}),
|
|
268
|
+
}}
|
|
269
|
+
/>
|
|
270
|
+
</>
|
|
241
271
|
);
|
|
242
272
|
};
|
|
243
273
|
|
|
@@ -252,8 +282,30 @@ export const GroupedLargeBoard = () => (
|
|
|
252
282
|
Scroll up and down: every band shows its full total from the start, bands
|
|
253
283
|
not read yet draw no "Add item" rows, and bands load one at a time. The 16
|
|
254
284
|
views fold behind "+N more"; the pencil on a view renames it, and "Closed
|
|
255
|
-
only" filters every band.
|
|
285
|
+
only" filters every band. Cells edit in place on hover, and only the rows
|
|
286
|
+
near the screen are drawn.
|
|
256
287
|
</div>
|
|
257
288
|
<GroupedBoardDemo />
|
|
258
289
|
</div>
|
|
259
290
|
);
|
|
291
|
+
|
|
292
|
+
export const GroupedLargeBoardPhone = () => (
|
|
293
|
+
<div className="w-full max-w-sm">
|
|
294
|
+
<div className="mb-1 font-semibold text-gray-900">
|
|
295
|
+
Data Template — the large grouped board on a phone
|
|
296
|
+
</div>
|
|
297
|
+
<div className="mb-4 text-sm text-gray-600">
|
|
298
|
+
The same 148,936 leads in the phone layout. Each row reads like a mail
|
|
299
|
+
inbox: the name and date on top, the status pill under it, and an arrow
|
|
300
|
+
that opens every field. The state is left off the row, as its header
|
|
301
|
+
already says it. Tapping a row opens it, the box ticks it, and each
|
|
302
|
+
state's header folds its rows. Only the rows near the screen are
|
|
303
|
+
drawn.
|
|
304
|
+
</div>
|
|
305
|
+
<GroupedBoardDemo isPhone />
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
GroupedLargeBoardPhone.parameters = {
|
|
310
|
+
viewport: { defaultViewport: "mobile2" },
|
|
311
|
+
};
|
|
@@ -0,0 +1,334 @@
|
|
|
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 { handleApplyStoryQueries } from "../dataWrapper/storyBackend.fixtures";
|
|
9
|
+
import { DataStructure } from "./model";
|
|
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-read-drawer-demo.local/api";
|
|
16
|
+
const DEMO_COLLECTION = "demo-read-drawer-records";
|
|
17
|
+
const COMMENTS_COLLECTION = `${DEMO_COLLECTION}-comments`;
|
|
18
|
+
const LOGS_COLLECTION = `${DEMO_COLLECTION}-logs`;
|
|
19
|
+
const CURRENT_USER = "alice";
|
|
20
|
+
const STAGES = ["New", "In Progress", "Done"];
|
|
21
|
+
const OWNERS = ["Alice", "Ben", "Chitra", "Dan"];
|
|
22
|
+
|
|
23
|
+
const DEMO_SCHEMA: DataStructure[] = [
|
|
24
|
+
{ id: "name", name: "Record", type: "string" },
|
|
25
|
+
{ id: "stage", name: "Stage", type: "list", listSelection: STAGES },
|
|
26
|
+
{ id: "owner", name: "Owner", type: "string" },
|
|
27
|
+
{ id: "summary", name: "Summary", type: "string" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const DRAWER_TOGGLE = [
|
|
31
|
+
{ id: "drawer", name: "Read drawer" },
|
|
32
|
+
{ id: "page", name: "No drawer" },
|
|
33
|
+
];
|
|
34
|
+
const ROLE_TOGGLE = [
|
|
35
|
+
{ id: "editor", name: "Can update" },
|
|
36
|
+
{ id: "viewer", name: "Viewer" },
|
|
37
|
+
];
|
|
38
|
+
const COMMENTS_TOGGLE = [
|
|
39
|
+
{ id: "on", name: "Comments" },
|
|
40
|
+
{ id: "off", name: "No comments" },
|
|
41
|
+
];
|
|
42
|
+
const LAYOUT_TOGGLE = [
|
|
43
|
+
{ id: "desktop", name: "Desktop" },
|
|
44
|
+
{ id: "mobile", name: "Mobile" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const demoDevSettings: any = {
|
|
48
|
+
server: "mongoServer",
|
|
49
|
+
dbEndpoint: DEMO_ENDPOINT,
|
|
50
|
+
dbAPIToken: "",
|
|
51
|
+
dbUserToken: "",
|
|
52
|
+
schemaEndpoint: DEMO_ENDPOINT,
|
|
53
|
+
schemaEndpointToken: "",
|
|
54
|
+
imageEndpoint: "https://placehold.co/600x400/e2e8f0/475569.png?text=",
|
|
55
|
+
uploadEndpoint: "",
|
|
56
|
+
organisationMembers: [
|
|
57
|
+
{ key: "alice", label: "Alice Tan" },
|
|
58
|
+
{ key: "ben", label: "Ben Wong" },
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// INFO: a few records carry comments so the bubble's count shows on some rows
|
|
63
|
+
// and not others
|
|
64
|
+
const COMMENT_COUNTS: { [id: string]: number } = {
|
|
65
|
+
"rec-01": 3,
|
|
66
|
+
"rec-02": 1,
|
|
67
|
+
"rec-04": 12,
|
|
68
|
+
"rec-07": 120,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const renderDemoComments = () =>
|
|
72
|
+
Object.entries(COMMENT_COUNTS).flatMap(([itemId, count]) =>
|
|
73
|
+
Array.from({ length: Math.min(count, 5) }, (_, index) => ({
|
|
74
|
+
_id: `${itemId}-comment-${index + 1}`,
|
|
75
|
+
collectionId: DEMO_COLLECTION,
|
|
76
|
+
itemId,
|
|
77
|
+
userId: index % 2 === 0 ? "ben" : "alice",
|
|
78
|
+
comment: `Comment ${index + 1} on ${itemId}`,
|
|
79
|
+
createdAt: new Date(Date.UTC(2026, 10, 1, 9, index)).toISOString(),
|
|
80
|
+
})),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const renderDemoRecords = () =>
|
|
84
|
+
Array.from({ length: 12 }, (_, index) => {
|
|
85
|
+
const number = String(index + 1).padStart(2, "0");
|
|
86
|
+
return {
|
|
87
|
+
_id: `rec-${number}`,
|
|
88
|
+
name: `Record ${number}`,
|
|
89
|
+
stage: STAGES[index % STAGES.length],
|
|
90
|
+
owner: OWNERS[index % OWNERS.length],
|
|
91
|
+
summary: `Summary for record ${number}`,
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const handleInstallDemoMock = (
|
|
96
|
+
store: any[],
|
|
97
|
+
activity: { [collection: string]: any[] },
|
|
98
|
+
) => {
|
|
99
|
+
const mock = new MockAdapter(axios, {
|
|
100
|
+
onNoMatch: "passthrough",
|
|
101
|
+
delayResponse: 150,
|
|
102
|
+
});
|
|
103
|
+
const renderUrl = (type: string) =>
|
|
104
|
+
`${DEMO_ENDPOINT}/${DEMO_COLLECTION}/${type}`;
|
|
105
|
+
const handleParseBody = (config: any) => {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(config?.data ?? "{}");
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
mock
|
|
114
|
+
.onPost(renderUrl("list"))
|
|
115
|
+
.reply(() => [200, { data: store, count: store.length }]);
|
|
116
|
+
mock.onPost(renderUrl("get")).reply((config) => {
|
|
117
|
+
const body = handleParseBody(config);
|
|
118
|
+
return [200, { data: store.find((each) => each._id === body.id) ?? null }];
|
|
119
|
+
});
|
|
120
|
+
mock.onPost(renderUrl("update")).reply((config) => {
|
|
121
|
+
const body = handleParseBody(config);
|
|
122
|
+
const index = store.findIndex((each) => each._id === body.id);
|
|
123
|
+
if (index >= 0) store[index] = { ...store[index], ...body.data };
|
|
124
|
+
return [200, { data: body.id }];
|
|
125
|
+
});
|
|
126
|
+
mock
|
|
127
|
+
.onPost(
|
|
128
|
+
new RegExp(
|
|
129
|
+
`^${DEMO_ENDPOINT}/(${COMMENTS_COLLECTION}|${LOGS_COLLECTION})/[a-z]+$`,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
.reply((config) => {
|
|
133
|
+
const [collection, action] = (config.url ?? "").split("/").slice(-2);
|
|
134
|
+
const body = handleParseBody(config);
|
|
135
|
+
const rows = activity[collection];
|
|
136
|
+
if (action === "list") {
|
|
137
|
+
const data = handleApplyStoryQueries(rows, body.query).sort(
|
|
138
|
+
(a: any, b: any) =>
|
|
139
|
+
String(b.createdAt ?? "").localeCompare(String(a.createdAt ?? "")),
|
|
140
|
+
);
|
|
141
|
+
const cursor = body.cursor ?? 0;
|
|
142
|
+
return [
|
|
143
|
+
200,
|
|
144
|
+
{
|
|
145
|
+
data: data.slice(cursor, cursor + (body.limit ?? 10)),
|
|
146
|
+
count: data.length,
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
if (action === "get")
|
|
151
|
+
return [200, { data: rows.find((row) => row._id === body.id) }];
|
|
152
|
+
if (action === "create") rows.push(body.data);
|
|
153
|
+
if (action === "update") {
|
|
154
|
+
const index = rows.findIndex((row) => row._id === body.id);
|
|
155
|
+
if (index >= 0) rows[index] = { ...rows[index], ...body.data };
|
|
156
|
+
}
|
|
157
|
+
if (action === "delete") {
|
|
158
|
+
const index = rows.findIndex((row) => row._id === body.id);
|
|
159
|
+
if (index >= 0) rows.splice(index, 1);
|
|
160
|
+
}
|
|
161
|
+
return [200, { success: true }];
|
|
162
|
+
});
|
|
163
|
+
// INFO: the tab bar waits on this lookup
|
|
164
|
+
mock
|
|
165
|
+
.onPost(`${DEMO_ENDPOINT}/spaces/list`)
|
|
166
|
+
.reply(200, { data: [], count: 0 });
|
|
167
|
+
|
|
168
|
+
return mock;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const ReadDrawerDemo = () => {
|
|
172
|
+
const [typeParam, setTypeParam] = useState<"list" | "read" | "update">(
|
|
173
|
+
"list",
|
|
174
|
+
);
|
|
175
|
+
const [dataId, setDataId] = useState<string>("");
|
|
176
|
+
const [drawerMode, setDrawerMode] = useState<"drawer" | "page">("drawer");
|
|
177
|
+
const [role, setRole] = useState<"editor" | "viewer">("editor");
|
|
178
|
+
const [comments, setComments] = useState<"on" | "off">("on");
|
|
179
|
+
const [layout, setLayout] = useState<"desktop" | "mobile">("desktop");
|
|
180
|
+
const [isMockReady, setIsMockReady] = useState(false);
|
|
181
|
+
|
|
182
|
+
const store = useMemo(() => renderDemoRecords(), []);
|
|
183
|
+
const activity = useMemo(
|
|
184
|
+
() => ({
|
|
185
|
+
[COMMENTS_COLLECTION]: renderDemoComments(),
|
|
186
|
+
[LOGS_COLLECTION]: [] as any[],
|
|
187
|
+
}),
|
|
188
|
+
[],
|
|
189
|
+
);
|
|
190
|
+
const hasComments = comments === "on";
|
|
191
|
+
const hasDrawer = drawerMode === "drawer";
|
|
192
|
+
const canUpdate = role === "editor";
|
|
193
|
+
const isMobile = layout === "mobile";
|
|
194
|
+
|
|
195
|
+
// INFO: in an effect so it survives StrictMode's double mount
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
const mock = handleInstallDemoMock(store, activity);
|
|
198
|
+
setIsMockReady(true);
|
|
199
|
+
return () => {
|
|
200
|
+
mock.restore();
|
|
201
|
+
setIsMockReady(false);
|
|
202
|
+
};
|
|
203
|
+
}, [store, activity]);
|
|
204
|
+
|
|
205
|
+
const handleNavigate = (type: string, id?: string) => {
|
|
206
|
+
setTypeParam(type as "list" | "read" | "update");
|
|
207
|
+
setDataId(id ?? "");
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<div className="w-full">
|
|
212
|
+
<div className="mb-4 flex flex-wrap items-center gap-3">
|
|
213
|
+
<ButtonGroup
|
|
214
|
+
selection={DRAWER_TOGGLE}
|
|
215
|
+
selectedId={drawerMode}
|
|
216
|
+
variant="tag"
|
|
217
|
+
size="small"
|
|
218
|
+
onClick={(id) => {
|
|
219
|
+
setDrawerMode(id as "drawer" | "page");
|
|
220
|
+
handleNavigate("list");
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
<ButtonGroup
|
|
224
|
+
selection={ROLE_TOGGLE}
|
|
225
|
+
selectedId={role}
|
|
226
|
+
variant="tag"
|
|
227
|
+
size="small"
|
|
228
|
+
onClick={(id) => setRole(id as "editor" | "viewer")}
|
|
229
|
+
/>
|
|
230
|
+
<ButtonGroup
|
|
231
|
+
selection={COMMENTS_TOGGLE}
|
|
232
|
+
selectedId={comments}
|
|
233
|
+
variant="tag"
|
|
234
|
+
size="small"
|
|
235
|
+
onClick={(id) => setComments(id as "on" | "off")}
|
|
236
|
+
/>
|
|
237
|
+
<ButtonGroup
|
|
238
|
+
selection={LAYOUT_TOGGLE}
|
|
239
|
+
selectedId={layout}
|
|
240
|
+
variant="tag"
|
|
241
|
+
size="small"
|
|
242
|
+
onClick={(id) => setLayout(id as "desktop" | "mobile")}
|
|
243
|
+
/>
|
|
244
|
+
{typeParam !== "list" && !hasDrawer && (
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
className="rounded-md border border-gray-300 bg-white px-3 py-1 text-sm text-gray-700 hover:bg-gray-50"
|
|
248
|
+
onClick={() => handleNavigate("list")}
|
|
249
|
+
>
|
|
250
|
+
← Back to list
|
|
251
|
+
</button>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
254
|
+
<div className={isMobile ? "mx-auto max-w-sm" : "w-full"}>
|
|
255
|
+
{isMockReady && (
|
|
256
|
+
<DataTemplateImpl
|
|
257
|
+
key={`${drawerMode}-${role}-${comments}-${layout}`}
|
|
258
|
+
title="Read Drawer"
|
|
259
|
+
description="Offline records"
|
|
260
|
+
type={typeParam}
|
|
261
|
+
id={dataId}
|
|
262
|
+
collectionId={DEMO_COLLECTION}
|
|
263
|
+
model={DEMO_SCHEMA}
|
|
264
|
+
localURLEndpoint=""
|
|
265
|
+
devSettings={demoDevSettings}
|
|
266
|
+
lib={{ axios, Menu, Transition, papaParser: Papa }}
|
|
267
|
+
organise={{
|
|
268
|
+
handleNavigate,
|
|
269
|
+
// INFO: counted from the live comments, so a comment added in
|
|
270
|
+
// the panel shows on the row once the list reloads
|
|
271
|
+
onFetchCommentCounts: hasComments
|
|
272
|
+
? async (ids: string[]) =>
|
|
273
|
+
Object.fromEntries(
|
|
274
|
+
ids.map((id) => [
|
|
275
|
+
id,
|
|
276
|
+
id in COMMENT_COUNTS && COMMENT_COUNTS[id] > 5
|
|
277
|
+
? COMMENT_COUNTS[id]
|
|
278
|
+
: activity[COMMENTS_COLLECTION].filter(
|
|
279
|
+
(row) => row.itemId === id,
|
|
280
|
+
).length,
|
|
281
|
+
]),
|
|
282
|
+
)
|
|
283
|
+
: undefined,
|
|
284
|
+
}}
|
|
285
|
+
edit={{
|
|
286
|
+
allEditable: canUpdate,
|
|
287
|
+
listEditable: canUpdate,
|
|
288
|
+
disabledUpdate: !canUpdate,
|
|
289
|
+
activityLog: hasComments
|
|
290
|
+
? {
|
|
291
|
+
enableComments: true,
|
|
292
|
+
commentCollectionId: COMMENTS_COLLECTION,
|
|
293
|
+
enableLogs: true,
|
|
294
|
+
logsCollectionId: LOGS_COLLECTION,
|
|
295
|
+
currentUserId: CURRENT_USER,
|
|
296
|
+
}
|
|
297
|
+
: undefined,
|
|
298
|
+
}}
|
|
299
|
+
bareSettings={{
|
|
300
|
+
bareNewDTW: true,
|
|
301
|
+
bareFormDrawer: hasDrawer,
|
|
302
|
+
bareListMobileView: isMobile,
|
|
303
|
+
bareUpdateMobileView: isMobile,
|
|
304
|
+
}}
|
|
305
|
+
viewSettings={
|
|
306
|
+
hasDrawer
|
|
307
|
+
? { readDrawer: { anchor: "responsive", maxWidth: "3xl" } }
|
|
308
|
+
: undefined
|
|
309
|
+
}
|
|
310
|
+
/>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
export const ReadDrawerRowAction = () => (
|
|
318
|
+
<div className="w-full">
|
|
319
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
320
|
+
Data Template — row action beside the title
|
|
321
|
+
</div>
|
|
322
|
+
<div className="text-sm mb-4 text-gray-600">
|
|
323
|
+
Hover a table row to show the chevron beside its title, which opens the
|
|
324
|
+
record's read view for anyone who can see it — in the drawer with a read
|
|
325
|
+
drawer, as a page with No drawer. Then come two divided columns: the
|
|
326
|
+
pencil, which opens the edit form, only for people who can update, and the
|
|
327
|
+
comment bubble, with a count on rows that have any (12 and 120 show the
|
|
328
|
+
two-digit and 99+ badges). Drag the Record column narrower to see its
|
|
329
|
+
floor. Mobile cards open from the card, so they carry the pencil and the
|
|
330
|
+
bubble but no chevron.
|
|
331
|
+
</div>
|
|
332
|
+
<ReadDrawerDemo />
|
|
333
|
+
</div>
|
|
334
|
+
);
|
|
@@ -41,6 +41,36 @@ The legacy `List` renders **every** extra key on an item. `NewList` renders only
|
|
|
41
41
|
<NewList list={tasks} extraKeys={["details", "activity"]} />
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
## Compact rows
|
|
45
|
+
|
|
46
|
+
Set `compact` on an item to draw it as a two-line row, like a mail inbox: the title with a pill and a short date on the first line, one grey line under it, and an arrow that opens the rest. Every compact row is the same height. An item without `compact` keeps the layout above, so existing keys such as an `extraKeys` "details" keep their meaning.
|
|
47
|
+
|
|
48
|
+
| `compact` field | Rendered as |
|
|
49
|
+
| --------------- | ----------------------------------------------------------------------------- |
|
|
50
|
+
| `subtitle` | One grey line under the title, cut to fit |
|
|
51
|
+
| `details` | Shown under the row when its arrow is tapped; the arrow does not open the row |
|
|
52
|
+
| `badge` | A pill leading the second line. Without it, the item's `tags` / `tag` |
|
|
53
|
+
| `trailing` | The right end of the title line. Without it, the item's `date` |
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<NewList
|
|
57
|
+
list={[
|
|
58
|
+
{
|
|
59
|
+
id: "a1",
|
|
60
|
+
title: "Ahmad Solar House",
|
|
61
|
+
compact: {
|
|
62
|
+
badge: <Tag tagText="Done" color="green" size="small" />,
|
|
63
|
+
trailing: "12 Nov",
|
|
64
|
+
subtitle: "Selangor · RM 12,400",
|
|
65
|
+
details: <DetailList title="" list={fields} size="compact" />,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
]}
|
|
69
|
+
/>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`NewListCompactRow` (`newList/compactRow`) is the row on its own; the new table draws its phone rows with it. `handlePickListRowFields` (`newList/listRowFields`) picks a row's fields from a column list in view order: the first date column for `trailing`, the first list column for `badge`, and the next two columns that hold something for `subtitle`. Pictures, files, drawings, long text, nested lists, buttons, timers and star ratings are left off the second line. `handleFormatListRowDate` writes a date as "12 Nov", adding the year outside the current one.
|
|
73
|
+
|
|
44
74
|
## Date
|
|
45
75
|
|
|
46
76
|
| `date` value | Output |
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# NewTable
|
|
2
2
|
|
|
3
|
-
Tabular display with a List-shaped mobile breakpoint.
|
|
3
|
+
Tabular display with a List-shaped mobile breakpoint. With `mobileView=true` each row is a two-line `NewListCompactRow`, like a mail inbox: the title and a short date, then a pill and a grey line of key values, and an arrow that opens every field. Tapping the row opens the record (`onClickRow` / `newTabUrl`), the checkbox ticks it without opening it, and groups draw the same foldable headers, counts and per-group paging as the computer layout. Rows are drawn again only when their own data changes, and a group with 120 or more loaded rows draws only the rows near the screen.
|
|
4
|
+
|
|
5
|
+
Without `mobileTooltip.titleAdjacent`, the view's column order picks a row's fields (`handlePickListRowFields`): the first list column is the pill, the first date column the date, and the next two columns that hold something the grey line.
|
|
4
6
|
|
|
5
7
|
## Row activity
|
|
6
8
|
|
|
7
9
|
Pass `onRowActivity={(id) => ...}` to show a keyboard-accessible chat button at the right of the first visible cell (beside the title on mobile cards). It stops row navigation and does not enter inline editing. When no left pins are configured, the first ungrouped column is pinned automatically; existing pin configurations are preserved. DTW wires this callback automatically when `edit.activityLog` is enabled with `bareNewDTW`.
|
|
8
10
|
|
|
9
|
-
Pass `onRowEdit={(id) => ...}` as well to put an edit pencil before the chat button, at the same size and colour.
|
|
11
|
+
Pass `onRowEdit={(id) => ...}` as well to put an edit pencil before the chat button, at the same size and colour, and `onRowOpen={(id) => ...}` to put a chevron beside the title. The pencil and the chat button each sit in their own divided strip at the title cell's right edge, like columns of their own, with the chevron just left of them. The chevron shows only when the row is hovered or the chevron is focused, and always on a touch screen, which cannot hover. While it shows, the title cell's padding grows to take it, so it never covers the title or its inline editor. All of them sit over the cell's right padding, so the row does not grow. While the title carries these actions, its column cannot be dragged — or set, or saved — narrower than its left padding, the actions' room (with the chevron's) and 48px of title; other columns keep the 80px floor. DTW passes the pencil only when the user can update records and opens the edit form with `handleNavigate("update", id, "general")`. It passes the chevron to anyone who can see the record, when `organise.handleNavigate` is set, and opens the read view with `handleNavigate("read", id, "general")` — in the drawer when `viewSettings.readDrawer` is set, as a page otherwise. An editable title cell is hard to click open, so the chevron is the sure way in. Mobile cards open from the card, so they show the pencil and the chat button but no chevron.
|
|
10
12
|
|
|
11
13
|
## Adding a row
|
|
12
14
|
|
|
@@ -26,18 +28,18 @@ Columns flagged `hideOnMobile` are dropped from the mobile rendering entirely.
|
|
|
26
28
|
|
|
27
29
|
## Mobile tooltip (`mobileTooltip`)
|
|
28
30
|
|
|
29
|
-
With `mobileView`, `mobileTooltip`
|
|
31
|
+
With `mobileView`, `mobileTooltip` picks what the row shows, in place of the column order, and adds a hover card. Through `DataTemplate` it is `bareSettings.bareMobileTooltip`, which applies when `bareNewDTW` and `bareListMobileView` are both on.
|
|
30
32
|
|
|
31
|
-
| Key | Type | Shows
|
|
32
|
-
| --------------------------- | -------------------------- |
|
|
33
|
-
| `titleAdjacent.tagKey` | `string` | That row field's value as a Tag next to the row title
|
|
34
|
-
| `titleAdjacent.tagColor` | `Record<string, TagColor>` | The Tag colour for each value
|
|
35
|
-
| `titleAdjacent.trailingKey` | `string` | That row field's value
|
|
36
|
-
| `titleAdjacent.summaryKeys` | `string[]` |
|
|
37
|
-
| `tooltip.imageKey` | `string` | The image URL field shown in the hover card
|
|
38
|
-
| `tooltip.summaryKeys` | `string[]` | Label + value pairs in the hover card
|
|
39
|
-
| `tooltip.tagKeys` | `string[]` | Tag pills in the hover card
|
|
40
|
-
| `tooltip.tagColors` | `Record<string, TagColor>` | The Tag colour for each value
|
|
33
|
+
| Key | Type | Shows |
|
|
34
|
+
| --------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `titleAdjacent.tagKey` | `string` | That row field's value as a Tag next to the row title |
|
|
36
|
+
| `titleAdjacent.tagColor` | `Record<string, TagColor>` | The Tag colour for each value |
|
|
37
|
+
| `titleAdjacent.trailingKey` | `string` | That row field's value at the right of the title line, e.g. a price. Without it the row shows no date |
|
|
38
|
+
| `titleAdjacent.summaryKeys` | `string[]` | The grey line under the title, as "Label: value" pairs joined with " · ". Labels come from the column titles; a dotted key (`shippingAddress.name`) falls back to its parent column's title. Empty values, hidden columns, the title and the `tagKey` column are skipped. Without it, and with `titleAdjacent` set, the line is left out |
|
|
39
|
+
| `tooltip.imageKey` | `string` | The image URL field shown in the hover card |
|
|
40
|
+
| `tooltip.summaryKeys` | `string[]` | Label + value pairs in the hover card |
|
|
41
|
+
| `tooltip.tagKeys` | `string[]` | Tag pills in the hover card |
|
|
42
|
+
| `tooltip.tagColors` | `Record<string, TagColor>` | The Tag colour for each value |
|
|
41
43
|
|
|
42
44
|
```tsx
|
|
43
45
|
<NewTable
|
|
@@ -117,8 +119,11 @@ Types that need more than a name and a type are completed in the popup:
|
|
|
117
119
|
| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
118
120
|
| `reference`, `listReference`, `referenceSelection` | endpoint and label, from `renderReferenceSpaceOptions` / `renderReferenceLabelOptions` — required, since a reference with no target resolves nothing |
|
|
119
121
|
| `list`, `multipleList` | options, as a comma-separated line |
|
|
122
|
+
| `lookup` | a `reference` or `listReference` column of this table, then a column of the space it links to, from `renderReferenceLabelOptions(spaceId, true)`; `lookup`, `password` and `button` columns are left out. Offered only with `renderReferenceLabelOptions`. Saved as `{ type: "lookup", referenceFieldId, lookupFieldId }`, always optional and read-only |
|
|
120
123
|
| `image`, `file`, `multipleImage`, `multipleFile` | nothing — the consumer fills the storage path from its own settings |
|
|
121
124
|
|
|
125
|
+
A `reference` or `listReference` column has a solid line under its header, and a `lookup` column (or a column drawn from one, carrying `lookupFieldId`) a dashed one, both in the primary colour.
|
|
126
|
+
|
|
122
127
|
A **Required** toggle sets the field's `optional` flag. New columns default to
|
|
123
128
|
optional, since the rows already in the table have no value for one the moment it
|
|
124
129
|
is added — ticking it says so.
|
|
@@ -206,10 +211,10 @@ With `editable.active` most columns edit in the cell (`supportedInlineEditList`)
|
|
|
206
211
|
|
|
207
212
|
`groups` draws rows as coloured, collapsible bands inside the one table. DataTemplate builds them from `organise.groupBy`; a consumer using NewTable on its own passes them directly.
|
|
208
213
|
|
|
209
|
-
| Prop | Type
|
|
210
|
-
| -------------------- |
|
|
211
|
-
| `groups` | `{ id, title, colour?, rowIds, count? }[]`
|
|
212
|
-
| `
|
|
213
|
-
| `
|
|
214
|
-
| `
|
|
215
|
-
|
|
214
|
+
| Prop | Type | Behavior |
|
|
215
|
+
| -------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
216
|
+
| `groups` | `{ id, title, colour?, rowIds, count? }[]` | One band per entry; `rowIds` reference row ids. `count` is the band's real total, shown in its header instead of the rows it holds |
|
|
217
|
+
| `groupColumnKey` | `string` | The column the groups are built from. Phone rows leave it off, since the group header shows it. Group headers show on a phone while their rows load |
|
|
218
|
+
| `onGroupLoadMore` | `(groupId: string) => void` | Scroll paging: fired when the bottom of a band scrolls into view, so that band loads its next rows |
|
|
219
|
+
| `onGroupPageChange` | `(groupId: string, page: number) => void` | Page paging: each band gets the list footer (numbered pages, chevrons, go-to-page) under its rows, scoped to that band. 0-based page |
|
|
220
|
+
| `groupLoadingStatus` | `{ [groupId]: { loadingMore?, exhausted?, page?, size?, fetched? } }` | Per band: a spinner while a page loads; `exhausted` removes the scroll sentinel; `page` and `size` drive the band pager once `fetched` |
|
|
@@ -833,3 +833,70 @@ export const RowLinks = () => {
|
|
|
833
833
|
</StorySection>
|
|
834
834
|
);
|
|
835
835
|
};
|
|
836
|
+
|
|
837
|
+
// INFO: reference columns carry a solid line under their header and lookup
|
|
838
|
+
// columns a dashed one; a lookup is drawn as its source column's type
|
|
839
|
+
export const LinkedColumns = () => {
|
|
840
|
+
const columns: NewTableColumn[] = [
|
|
841
|
+
{ key: "name", title: "Customer", type: "string" },
|
|
842
|
+
{
|
|
843
|
+
key: "project",
|
|
844
|
+
title: "PV Project",
|
|
845
|
+
type: "reference",
|
|
846
|
+
referenceSpaceId: "projects",
|
|
847
|
+
referenceLabel: "code",
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
key: "projectStatus",
|
|
851
|
+
title: "Project Status",
|
|
852
|
+
type: "string",
|
|
853
|
+
referenceFieldId: "project",
|
|
854
|
+
lookupFieldId: "status",
|
|
855
|
+
disabled: true,
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
key: "installer",
|
|
859
|
+
title: "Installer",
|
|
860
|
+
type: "string",
|
|
861
|
+
referenceFieldId: "project",
|
|
862
|
+
lookupFieldId: "installer",
|
|
863
|
+
disabled: true,
|
|
864
|
+
},
|
|
865
|
+
{ key: "notes", title: "Notes", type: "string" },
|
|
866
|
+
];
|
|
867
|
+
const rows = [
|
|
868
|
+
{
|
|
869
|
+
id: "1",
|
|
870
|
+
name: "Andrew Tan",
|
|
871
|
+
project: "PV-1023",
|
|
872
|
+
projectStatus: "Installed",
|
|
873
|
+
installer: "Ahmad",
|
|
874
|
+
notes: "Call before visit",
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
id: "2",
|
|
878
|
+
name: "Celine Wong",
|
|
879
|
+
project: "PV-1031",
|
|
880
|
+
projectStatus: "Scheduled",
|
|
881
|
+
installer: "Kumar",
|
|
882
|
+
notes: "",
|
|
883
|
+
},
|
|
884
|
+
];
|
|
885
|
+
return (
|
|
886
|
+
<StorySection
|
|
887
|
+
title="Linked columns"
|
|
888
|
+
description="A reference column has a solid line under its header; a lookup column, filled from the linked record, has a dashed one."
|
|
889
|
+
>
|
|
890
|
+
<NewTableImpl
|
|
891
|
+
columns={columns}
|
|
892
|
+
data={rows}
|
|
893
|
+
size={rows.length}
|
|
894
|
+
limit={10}
|
|
895
|
+
cursor=""
|
|
896
|
+
loading={false}
|
|
897
|
+
paginationType="page"
|
|
898
|
+
handleUpdatePageIndex={() => {}}
|
|
899
|
+
/>
|
|
900
|
+
</StorySection>
|
|
901
|
+
);
|
|
902
|
+
};
|