blixify-ui-web-mcp 0.1.63 → 0.1.65
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.
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { Menu, Transition } from "@headlessui/react";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import MockAdapter from "axios-mock-adapter";
|
|
4
|
+
import React, { useMemo, useState } from "react";
|
|
5
|
+
import { DataTemplate } from "../dataTemplate";
|
|
6
|
+
import { DataStructure } from "../dataTemplate/model";
|
|
7
|
+
import { ButtonCell } from "./buttonCell";
|
|
8
|
+
import { ColumnTypeContext } from "./context";
|
|
9
|
+
import { RatingStars } from "./ratingCell";
|
|
10
|
+
import { TimeTrackingCell } from "./timeTrackingCell";
|
|
11
|
+
|
|
12
|
+
const meta = { title: "Data/Column Types" };
|
|
13
|
+
export default meta;
|
|
14
|
+
|
|
15
|
+
// INFO: an in-browser endpoint — every read and write below stays in memory
|
|
16
|
+
const DEMO_ENDPOINT = "https://column-types-demo.local/api";
|
|
17
|
+
const JOBS_COLLECTION = "demo-jobs";
|
|
18
|
+
const BOARD_COLLECTION = "demo-board";
|
|
19
|
+
|
|
20
|
+
const MEMBERS = [
|
|
21
|
+
{ key: "u-alice", label: "Alice", image: "" },
|
|
22
|
+
{ key: "u-bob", label: "Bob", image: "" },
|
|
23
|
+
{ key: "u-carol", label: "Carol", image: "" },
|
|
24
|
+
{ key: "u-dan", label: "Dan", image: "" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const demoDevSettings: any = {
|
|
28
|
+
server: "mongoServer",
|
|
29
|
+
dbEndpoint: DEMO_ENDPOINT,
|
|
30
|
+
dbAPIToken: "",
|
|
31
|
+
dbUserToken: "",
|
|
32
|
+
schemaEndpoint: "",
|
|
33
|
+
imageEndpoint: "",
|
|
34
|
+
organisationMembers: MEMBERS,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const JOB_SCHEMA: DataStructure[] = [
|
|
38
|
+
{ id: "name", name: "Job", type: "string" },
|
|
39
|
+
{
|
|
40
|
+
id: "stage",
|
|
41
|
+
name: "Stage",
|
|
42
|
+
type: "list",
|
|
43
|
+
listSelection: ["Survey", "Install", "Handover"],
|
|
44
|
+
listSelectionColour: ["orange", "blue", "green"],
|
|
45
|
+
optional: true,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "generate",
|
|
49
|
+
name: "Quote",
|
|
50
|
+
type: "button",
|
|
51
|
+
buttonLabel: "Generate PDF",
|
|
52
|
+
buttonColour: "primary",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "moveOn",
|
|
56
|
+
name: "Move On",
|
|
57
|
+
type: "button",
|
|
58
|
+
buttonLabel: "Move on",
|
|
59
|
+
buttonColour: "green",
|
|
60
|
+
buttonConfirm: "Move this job to the next stage?",
|
|
61
|
+
},
|
|
62
|
+
{ id: "work", name: "Site Time", type: "timeTracking", optional: true },
|
|
63
|
+
{ id: "timeline", name: "Timeline", type: "dateRange", optional: true },
|
|
64
|
+
{
|
|
65
|
+
id: "score",
|
|
66
|
+
name: "Customer Rating",
|
|
67
|
+
type: "number",
|
|
68
|
+
ratingMax: 5,
|
|
69
|
+
optional: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "daysLeft",
|
|
73
|
+
name: "Days Planned",
|
|
74
|
+
type: "formula",
|
|
75
|
+
formulaExpression: "DAYS({timeline#end}, {timeline#start})",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "hoursText",
|
|
79
|
+
name: "Hours",
|
|
80
|
+
type: "formula",
|
|
81
|
+
formulaExpression: "ROUND({work}, 2)",
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const handleSeedJobs = () => {
|
|
86
|
+
const today = new Date();
|
|
87
|
+
const handleDay = (offset: number) =>
|
|
88
|
+
new Date(
|
|
89
|
+
today.getFullYear(),
|
|
90
|
+
today.getMonth(),
|
|
91
|
+
today.getDate() + offset,
|
|
92
|
+
).toISOString();
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
_id: "job-1",
|
|
96
|
+
name: "Ipoh rooftop",
|
|
97
|
+
stage: "Survey",
|
|
98
|
+
work: { totalSeconds: 5400, running: false },
|
|
99
|
+
timeline: { start: handleDay(2), end: handleDay(8) },
|
|
100
|
+
score: 4,
|
|
101
|
+
baseNumber: 101,
|
|
102
|
+
baseCreatedAt: handleDay(-20),
|
|
103
|
+
baseCreatedBy: "u-alice",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
_id: "job-2",
|
|
107
|
+
name: "Shah Alam warehouse",
|
|
108
|
+
stage: "Install",
|
|
109
|
+
work: {
|
|
110
|
+
totalSeconds: 600,
|
|
111
|
+
running: true,
|
|
112
|
+
startedAt: new Date(Date.now() - 90 * 1000).toISOString(),
|
|
113
|
+
},
|
|
114
|
+
timeline: { start: handleDay(-3), end: handleDay(25) },
|
|
115
|
+
baseNumber: 102,
|
|
116
|
+
baseCreatedAt: handleDay(-10),
|
|
117
|
+
baseCreatedBy: "u-bob",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
_id: "job-3",
|
|
121
|
+
name: "Penang terrace (older record, no stamps)",
|
|
122
|
+
stage: "Archived",
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const handleIsIn = (list: any[], value: any) =>
|
|
128
|
+
list.some((each) =>
|
|
129
|
+
each === null ? value === null || value === undefined : each === value,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const handleMatchQuery = (row: any, query: any[]) =>
|
|
133
|
+
(Array.isArray(query) ? query : []).every((eachQuery) => {
|
|
134
|
+
if (!eachQuery?.queryId) return true;
|
|
135
|
+
const value = eachQuery.queryId
|
|
136
|
+
.split(".")
|
|
137
|
+
.reduce((acc: any, key: string) => acc?.[key], row);
|
|
138
|
+
switch (eachQuery.type) {
|
|
139
|
+
case "=":
|
|
140
|
+
return String(value) === String(eachQuery.value);
|
|
141
|
+
case "in":
|
|
142
|
+
return handleIsIn(eachQuery.value, value);
|
|
143
|
+
case "!in":
|
|
144
|
+
return !handleIsIn(eachQuery.value, value);
|
|
145
|
+
default:
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// INFO: list, get and update over one array per collection, so a timer
|
|
151
|
+
// started or a rating given in the story survives a refresh of the list
|
|
152
|
+
const handleInstallMock = (stores: { [collectionId: string]: any[] }) => {
|
|
153
|
+
const mock = new MockAdapter(axios, {
|
|
154
|
+
onNoMatch: "passthrough",
|
|
155
|
+
delayResponse: 200,
|
|
156
|
+
});
|
|
157
|
+
const handleBody = (config: any) => JSON.parse(config?.data ?? "{}");
|
|
158
|
+
Object.entries(stores).forEach(([collectionId, store]) => {
|
|
159
|
+
const url = (type: string) => `${DEMO_ENDPOINT}/${collectionId}/${type}`;
|
|
160
|
+
mock.onPost(url("list")).reply((config) => {
|
|
161
|
+
const body = handleBody(config);
|
|
162
|
+
const rows = store.filter((each) => handleMatchQuery(each, body.query));
|
|
163
|
+
const cursor = typeof body.cursor === "number" ? body.cursor : 0;
|
|
164
|
+
const limit = typeof body.limit === "number" ? body.limit : rows.length;
|
|
165
|
+
return [
|
|
166
|
+
200,
|
|
167
|
+
{
|
|
168
|
+
data: JSON.parse(JSON.stringify(rows.slice(cursor, cursor + limit))),
|
|
169
|
+
count: rows.length,
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
});
|
|
173
|
+
mock.onPost(url("get")).reply((config) => {
|
|
174
|
+
const body = handleBody(config);
|
|
175
|
+
const row = store.find((each) => each._id === body.id);
|
|
176
|
+
return [200, { data: row ? JSON.parse(JSON.stringify(row)) : null }];
|
|
177
|
+
});
|
|
178
|
+
mock.onPost(url("update")).reply((config) => {
|
|
179
|
+
const body = handleBody(config);
|
|
180
|
+
const row = store.find((each) => each._id === body.id);
|
|
181
|
+
if (row)
|
|
182
|
+
Object.assign(row, body.data, {
|
|
183
|
+
baseUpdatedAt: new Date().toISOString(),
|
|
184
|
+
baseUpdatedBy: "u-alice",
|
|
185
|
+
});
|
|
186
|
+
return [200, { success: true }];
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
return mock;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const MondayParityColumns = () => {
|
|
193
|
+
// 1. State
|
|
194
|
+
const [type, setType] = useState<"list" | "read" | "update">("list");
|
|
195
|
+
const [id, setId] = useState("");
|
|
196
|
+
const [log, setLog] = useState<string[]>([]);
|
|
197
|
+
|
|
198
|
+
// 2. Refs
|
|
199
|
+
const jobs = useMemo(() => handleSeedJobs(), []);
|
|
200
|
+
useMemo(() => handleInstallMock({ [JOBS_COLLECTION]: jobs }), [jobs]);
|
|
201
|
+
|
|
202
|
+
// 5. Handlers
|
|
203
|
+
// INFO: stands in for the host — Space posts the click to its button route
|
|
204
|
+
const handleButtonClick = async (columnId: string, row: any) => {
|
|
205
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
206
|
+
setLog((previous) => [
|
|
207
|
+
`${new Date().toLocaleTimeString()} — ${columnId} on ${row.name}`,
|
|
208
|
+
...previous,
|
|
209
|
+
]);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// 7. Return
|
|
213
|
+
return (
|
|
214
|
+
<div className="w-full space-y-4">
|
|
215
|
+
<div className="text-sm text-gray-600">
|
|
216
|
+
A button column (Generate PDF; Move on asks first), a time tracking
|
|
217
|
+
column that ticks live, a timeline (date range), a 5-star rating and two
|
|
218
|
+
formulas reading them. System columns (Created At / By, Updated At / By,
|
|
219
|
+
Record ID, Number) are offered hidden in the column settings. The third
|
|
220
|
+
record is an older one with no stamps.
|
|
221
|
+
</div>
|
|
222
|
+
<DataTemplate
|
|
223
|
+
title="Jobs"
|
|
224
|
+
description="Monday parity column types"
|
|
225
|
+
type={type}
|
|
226
|
+
id={id}
|
|
227
|
+
collectionId={JOBS_COLLECTION}
|
|
228
|
+
model={JOB_SCHEMA}
|
|
229
|
+
localURLEndpoint=""
|
|
230
|
+
devSettings={demoDevSettings}
|
|
231
|
+
lib={{ axios, Menu, Transition }}
|
|
232
|
+
edit={{ allEditable: true, listEditable: true }}
|
|
233
|
+
bareSettings={{ bareNewDTW: true, bareSystemColumns: true }}
|
|
234
|
+
organise={{
|
|
235
|
+
paginationType: "page",
|
|
236
|
+
showGroupByFeature: true,
|
|
237
|
+
onButtonClick: handleButtonClick,
|
|
238
|
+
handleNavigate: (nextType: string, nextId?: string) => {
|
|
239
|
+
setType(nextType as any);
|
|
240
|
+
setId(nextId ?? "");
|
|
241
|
+
},
|
|
242
|
+
}}
|
|
243
|
+
/>
|
|
244
|
+
<div data-testid="button-click-log" className="text-xs text-gray-500">
|
|
245
|
+
{log.length === 0
|
|
246
|
+
? "Button clicks appear here."
|
|
247
|
+
: log.map((each) => <div key={each}>{each}</div>)}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const BOARD_STATUSES = ["Working on it", "Stuck", "Done"];
|
|
254
|
+
|
|
255
|
+
const handleSeedBoard = () =>
|
|
256
|
+
Array.from({ length: 2300 }, (_, index) => ({
|
|
257
|
+
_id: `board-${index}`,
|
|
258
|
+
name: `Task #${index}`,
|
|
259
|
+
// INFO: every 50th row has no owner and every 70th a member who left, so
|
|
260
|
+
// the No Value and Unknown sections are both there
|
|
261
|
+
owner:
|
|
262
|
+
index % 50 === 49
|
|
263
|
+
? ""
|
|
264
|
+
: index % 70 === 69
|
|
265
|
+
? "u-left"
|
|
266
|
+
: MEMBERS[index % MEMBERS.length].key,
|
|
267
|
+
status:
|
|
268
|
+
index % 45 === 44 ? "" : BOARD_STATUSES[index % BOARD_STATUSES.length],
|
|
269
|
+
done: index % 3 === 0,
|
|
270
|
+
score: index % 6,
|
|
271
|
+
}));
|
|
272
|
+
|
|
273
|
+
const BOARD_SCHEMA: DataStructure[] = [
|
|
274
|
+
{ id: "name", name: "Task", type: "string" },
|
|
275
|
+
{
|
|
276
|
+
id: "status",
|
|
277
|
+
name: "Status",
|
|
278
|
+
type: "list",
|
|
279
|
+
listSelection: BOARD_STATUSES,
|
|
280
|
+
listSelectionColour: ["orange", "red", "green"],
|
|
281
|
+
optional: true,
|
|
282
|
+
},
|
|
283
|
+
{ id: "owner", name: "Owner", type: "person", optional: true },
|
|
284
|
+
{ id: "done", name: "Checked", type: "boolean", optional: true },
|
|
285
|
+
{
|
|
286
|
+
id: "score",
|
|
287
|
+
name: "Priority",
|
|
288
|
+
type: "number",
|
|
289
|
+
ratingMax: 5,
|
|
290
|
+
optional: true,
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
export const GroupedBoard2300Rows = () => {
|
|
295
|
+
// 1. State
|
|
296
|
+
const [groupBy, setGroupBy] = useState("owner");
|
|
297
|
+
|
|
298
|
+
// 2. Refs
|
|
299
|
+
const board = useMemo(() => handleSeedBoard(), []);
|
|
300
|
+
useMemo(() => handleInstallMock({ [BOARD_COLLECTION]: board }), [board]);
|
|
301
|
+
|
|
302
|
+
// 7. Return
|
|
303
|
+
return (
|
|
304
|
+
<div className="w-full space-y-4">
|
|
305
|
+
<div className="text-sm text-gray-600">
|
|
306
|
+
2,300 tasks read in one page and grouped into sections. Grouped by Owner
|
|
307
|
+
(a person column) the sections follow the member order, then Unknown (a
|
|
308
|
+
member who left) and No Value; each section draws 50 rows and a Show
|
|
309
|
+
more. Grouped by Status or Checked each section reads and pages its own
|
|
310
|
+
rows.
|
|
311
|
+
</div>
|
|
312
|
+
<DataTemplate
|
|
313
|
+
key={groupBy}
|
|
314
|
+
title="Board"
|
|
315
|
+
description="Grouped into sections"
|
|
316
|
+
type="list"
|
|
317
|
+
collectionId={BOARD_COLLECTION}
|
|
318
|
+
model={BOARD_SCHEMA}
|
|
319
|
+
localURLEndpoint=""
|
|
320
|
+
devSettings={demoDevSettings}
|
|
321
|
+
lib={{ axios, Menu, Transition }}
|
|
322
|
+
limit={groupBy === "owner" ? 2300 : 25}
|
|
323
|
+
edit={{ allEditable: true }}
|
|
324
|
+
bareSettings={{ bareNewDTW: true }}
|
|
325
|
+
organise={{
|
|
326
|
+
paginationType: groupBy === "owner" ? "page" : "scroll",
|
|
327
|
+
showGroupByFeature: true,
|
|
328
|
+
groupBy,
|
|
329
|
+
onChangeGroupBy: (value: string) => setGroupBy(value || "owner"),
|
|
330
|
+
}}
|
|
331
|
+
/>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// INFO: the cells on their own, outside a DataTemplate — the context is what
|
|
337
|
+
// a DataTemplate provides
|
|
338
|
+
export const Cells = () => {
|
|
339
|
+
// 1. State
|
|
340
|
+
const [score, setScore] = useState(3);
|
|
341
|
+
const [timer, setTimer] = useState<any>({
|
|
342
|
+
totalSeconds: 3725,
|
|
343
|
+
running: false,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// 2. Refs
|
|
347
|
+
const column = (overrides: Partial<DataStructure>): DataStructure => ({
|
|
348
|
+
id: "cell",
|
|
349
|
+
name: "Cell",
|
|
350
|
+
type: "string",
|
|
351
|
+
...overrides,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// 7. Return
|
|
355
|
+
return (
|
|
356
|
+
<ColumnTypeContext.Provider
|
|
357
|
+
value={{
|
|
358
|
+
canUpdate: true,
|
|
359
|
+
onButtonClick: () =>
|
|
360
|
+
new Promise((resolve) => setTimeout(resolve, 1500)),
|
|
361
|
+
onUpdateCell: async (_rowId, _columnId, value) => setTimer(value),
|
|
362
|
+
}}
|
|
363
|
+
>
|
|
364
|
+
<div className="max-w-sm space-y-6">
|
|
365
|
+
<ButtonCell
|
|
366
|
+
column={column({ type: "button", buttonLabel: "Generate document" })}
|
|
367
|
+
row={{ _id: "demo" }}
|
|
368
|
+
/>
|
|
369
|
+
<ButtonCell
|
|
370
|
+
column={column({
|
|
371
|
+
type: "button",
|
|
372
|
+
buttonLabel: "Archive",
|
|
373
|
+
buttonColour: "red",
|
|
374
|
+
buttonConfirm: "Archive this item?",
|
|
375
|
+
})}
|
|
376
|
+
row={{ _id: "demo" }}
|
|
377
|
+
/>
|
|
378
|
+
<TimeTrackingCell
|
|
379
|
+
column={column({ id: "timer", type: "timeTracking" })}
|
|
380
|
+
rowId="demo"
|
|
381
|
+
value={timer}
|
|
382
|
+
/>
|
|
383
|
+
<RatingStars
|
|
384
|
+
id="stars"
|
|
385
|
+
value={score}
|
|
386
|
+
max={5}
|
|
387
|
+
size="form"
|
|
388
|
+
onChange={setScore}
|
|
389
|
+
/>
|
|
390
|
+
</div>
|
|
391
|
+
</ColumnTypeContext.Provider>
|
|
392
|
+
);
|
|
393
|
+
};
|
|
@@ -189,6 +189,12 @@ Custom layout via renderSettings:
|
|
|
189
189
|
| `topN` | `number` | Keep the first N groups; the rest are added up as "Others" unless `showOthers` is `false` (averages never get "Others"). |
|
|
190
190
|
| `showPercent` | `boolean` | Show each group as a percentage of the total (count and sum, unsplit charts). |
|
|
191
191
|
| `cumulative` | `boolean` | Running total over the date buckets. |
|
|
192
|
+
| `datesAcross` | `boolean` | Table grouped by a date interval: the dates become the columns (every bucket in the range, oldest first) and the `segmentBy` values the rows, with a Total column and a Total row for count and sum. With no `segmentBy`, one row named by its measure. |
|
|
193
|
+
| `higherIsBetter` | `boolean` | Dates-across table: colours the Change column (last complete period vs the one before) green for good and red for bad. Unset leaves it grey. |
|
|
194
|
+
| `highlights` | `{ row, colour?, bold? }[]` | Table rows marked by label: tint (`red` `amber` `green` `blue` `purple`) and/or bold. Set by an editor from the highlight bar beside a row label (`onHighlightRow`). |
|
|
195
|
+
| `valueDisplay` | `"count" \| "percent" \| "both"` | Dates-across table split by `segmentBy`: cells as counts, as each period's share of its total, or both. In `percent`, Change is in percentage points. |
|
|
196
|
+
| `recordColumns` / `recordSort` / `recordPageSize` | `string[]` / `Sort` / `number` | What clicking a value opens: the columns in order, the sort and the rows per page. Unset follows the datasheet. ChartView's `renderDrillDown` lets an app render that view itself. |
|
|
197
|
+
| `rangeOverride` | `{ startDate?: string; endDate: string }` | ISO range set by the page the chart sits on; replaces the chart's own range and hides its picker. No `startDate` counts all time, and date buckets then start at the first record's day. Not saved with the chart. |
|
|
192
198
|
|
|
193
199
|
##### WorkspaceDevSettings
|
|
194
200
|
|
|
@@ -744,13 +744,29 @@ A `type: "formula"` column is a monday.com-style formula. It is worked out from
|
|
|
744
744
|
|
|
745
745
|
- **Syntax** follows [monday's formula column](https://support.monday.com/hc/en-us/articles/360001235445-The-Formula-Column): `{Column}` references a column (the builder shows names, the schema stores ids, so renaming a column never breaks a formula), text in quotes, `=` / `<>` compare, `&` joins text (below `+`/`-`, above comparisons, as in monday), `+ - * / ^ %` and `( )`.
|
|
746
746
|
- **Functions** — monday's library, in `src/components/data/formula/functions.ts`: text (`CONCATENATE`, `LEFT`, `LEN`, `REPLACE`, `SEARCH`, `SUBSTITUTE`, `TEXT`, `TRIM` …), logical (`IF`, `SWITCH`, `AND`, `OR`, `XOR`, `NOT`, `EXACT`), numeric (`SUM`, `AVERAGE`, `ROUND`, `ROUNDUP`, `ROUNDDOWN`, `MOD`, `POWER` …), date (`TODAY`, `NOW`, `DAYS`, `WORKDAYS`, `ADD_DAYS`, `FORMAT_DATE`, `WEEKNUM` …) and `PI` / `TRUE` / `FALSE`. Text results are capped at 10,000 characters.
|
|
747
|
-
- **
|
|
747
|
+
- **More functions:** `MID`, `FIND`, `VALUE`, `REGEXEXTRACT` (text), `ISBLANK`, `ISNUMBER` (logical), `INT` (numeric), `WEEKDAY`, `YEARFRAC` (date). `REGEXEXTRACT` returns the first match, or its first group; a pattern over 200 characters, one with a backreference or a nested quantifier such as `(a+)+`, and one that once ran longer than 50 ms are refused with an error instead of run.
|
|
748
|
+
- **Values:** an empty cell is blank text, 0 in arithmetic and equal to 0 in a comparison with a number. Reference and role columns read their label; a math column reads the number it shows. A time tracking column reads in hours; a date range reads as `start - end`, and `{Timeline#Start}` / `{Timeline#End}` read one end as a date. A formula can read another formula; a circular reference shows `Error`. Files, images, addresses, objects, passwords, signatures, canvas and gantt columns cannot be used.
|
|
748
749
|
- **Adding:** with `bareAddColumns`, pick **Formula** as the type in the header + panel. The builder lists columns and functions with an example of each, validates as you type and previews the first row.
|
|
749
750
|
- **Sort and filter** use the computed value, as monday does: header / view settings sort, and the flex filters. Either makes the list read the matching rows — at most `FORMULA_VIEW_MAX_ROWS` (5,000), in a fixed order, with a notice when there are more — and filter, order and page them locally. The cap also applies to the calendar, map, timeline and legacy grouped views, which otherwise read every row. On Firebase and union lists formula columns are not offered for sorting or filtering. CSV export sends the rows a formula filter kept, by id; "Select all pages" is off in a formula view, and the selection callbacks' query does not carry formula conditions.
|
|
750
751
|
- **Earlier library versions** have no case for `formula`, so they show its cell blank. Apps that build schemas with `parseSpaceToDataStructure` need this version to receive `formulaExpression`; a consumer with its own `DataType` list (blixify-space-next `models/Data.ts`) adds `"formula"` to it.
|
|
751
752
|
|
|
752
753
|
---
|
|
753
754
|
|
|
755
|
+
### Button, time tracking, date range, rating and system columns
|
|
756
|
+
|
|
757
|
+
Column types with monday.com counterparts. The value maths is in `src/components/data/columnTypes/`, as pure functions a server can import.
|
|
758
|
+
|
|
759
|
+
| Column | Schema | Stored value | Where it shows |
|
|
760
|
+
| --- | --- | --- | --- |
|
|
761
|
+
| Button | `type: "button"`, `buttonLabel?`, `buttonColour?`, `buttonConfirm?` | nothing | table, list, kanban and read view; never in a form. A tap calls `organise.onButtonClick(columnId, row)` and the button stays busy until the returned promise settles, ignoring taps meanwhile. `buttonConfirm` asks first. Disabled without `onButtonClick` or update rights |
|
|
762
|
+
| Time tracking | `type: "timeTracking"` | `{ totalSeconds, running, startedAt?, sessions? }` | table and read view show the total with start / stop, ticking each second inside the cell only; a form shows the total read-only. Start / stop saves that one field. `handleStartTimeTracking` / `handleStopTimeTracking` are idempotent, so a flow can drive the same value. Sorts on `<id>.totalSeconds`; formulas read hours (`handleGetTimeTrackingHours`) |
|
|
763
|
+
| Date range | `type: "dateRange"`, `minDate?`, `maxDate?` | `{ start, end }` | `12 – 18 Nov`; a range picker in the form; filters and sorts on `<id>.start`. A calendar or timeline tab can use `<id>.start` and `<id>.end` as its dates |
|
|
764
|
+
| Rating | `type: "number"`, `ratingMax` | a number | stars in every view; a tap saves the number, tapping the current rating clears it |
|
|
765
|
+
|
|
766
|
+
`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).
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
754
770
|
### With activity log (comments + audit trail)
|
|
755
771
|
|
|
756
772
|
Enable `edit.activityLog` to open the activity panel from list and read views, in a drawer on the right (a bottom sheet on phones). In the new table each record also gets a chat bubble beside its title, for anyone who can see the row. It uses the new feeds and pagination UI, an inline comment composer, editable comments, confirmed deletion, and before/after audit values. It follows `organise.darkMode`, including when opened from a legacy table.
|
|
@@ -1112,10 +1128,16 @@ path should not render it.
|
|
|
1112
1128
|
Which field types can be edited from the list table, and how. Types not listed under
|
|
1113
1129
|
"Never rendered" appear as read-only cells when they are not editable.
|
|
1114
1130
|
|
|
1115
|
-
| Edited **in the cell**
|
|
1116
|
-
|
|
|
1117
|
-
| `string`, `contact`, `email`, `textArea`, `number
|
|
1131
|
+
| Edited **in the cell** | Edited **in a modal** (cell click opens it) | Read-only in list | Never rendered as a list column |
|
|
1132
|
+
| --- | --- | --- | --- |
|
|
1133
|
+
| `string`, `contact`, `email`, `textArea`, `number` (stars when `ratingMax` is set), `boolean` (checkbox and radio commit on select), `date`, `dateRange` (the range picker opens in the cell), `list`, `multipleList`, `person`, `listPerson`, `roles`, `reference`, `listReference`, `referenceSelection`, `timeTracking` (start / stop), `button` (click) | `image`, `multipleImage`, `address`, `object`, `markdown`, `signature`, `file`, `multipleFile` | `math` (computed), `formula` (computed), `password` | `listObject`, `listAddress`, `canvas`, `ganttchart` |
|
|
1118
1134
|
|
|
1135
|
+
- **What a modal cell shows:** one line — an image thumbnail, a file's name or
|
|
1136
|
+
`N files`, the first words of a markdown body without its markup, a signature
|
|
1137
|
+
thumbnail — or `Add` when empty, so no row grows taller than its neighbours.
|
|
1138
|
+
- **The `+` Add Column types** leave out `listAddress`, `canvas` and
|
|
1139
|
+
`ganttchart` (and `listObject`, `object` and `math`): a column of them would
|
|
1140
|
+
never be drawn in the table it was added from.
|
|
1119
1141
|
- **Why some types edit in a modal:** their editor is bigger than one line. An
|
|
1120
1142
|
`address` is a Places autocomplete plus a map with a draggable pin plus
|
|
1121
1143
|
country/state/district fields; `image` / `multipleImage` are an upload area
|
|
@@ -1193,7 +1215,8 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
1193
1215
|
| `defaultSavedViewId` | `string` | The view everyone opens on, marked ★, stored by the app. The list opens on it when no `initialSavedViewId` is given — also when it arrives after mount — unless `persistViewState` restored a state or a view was already picked. A view that no longer exists leaves the list on All. Opening on it does not call `onChangeSort` |
|
|
1194
1216
|
| `onChangeDefaultSavedViewId` | `(id: string \| null) => void` | Gives each view a ☆ that makes it the default; the ★ on the default takes it off (`null`, the list opens on All). Deleting the default view also sends `null` |
|
|
1195
1217
|
| `initialSavedViewId` | `string` | A view the list opens on, once, in place of the ★ default — e.g. a page embedding a datasheet opens it on a chosen view. A view that no longer exists, or is on another tab, leaves the list on All. Opening on it does not call `onChangeSort` |
|
|
1196
|
-
| `groupBy` | `string` | Column ID to group list results by. With `bareNewDTW` the groups are bands in one table. Grouped by a `list` column, every option plus "Unknown
|
|
1218
|
+
| `groupBy` | `string` | Column ID to group list results by. With `bareNewDTW` the groups are bands in one table. Grouped by a `list` or `boolean` column, every option (in option order, with its colour) plus "Unknown" (a value outside the options) and "No Value" (missing or blank) is a band with its real total that reads its own rows under the list's filters, search and sort: in scroll mode each band loads more as it scrolls, in page mode each band has its own pager and the list-wide pager is hidden. Grouped by any other column (a `person` column's sections follow the member order), page mode groups the rows of the current page and scroll mode keeps the normal scroll; a section built from loaded rows draws 50 rows and a "Show more" at a time. A search, filter, sort or regroup starts every band over. Firebase and `bareListMobileView` keep the normal pagination. Saved views store and restore it |
|
|
1219
|
+
| `onButtonClick` | `(columnId: string, row: any) => Promise<any> \| void` | Called when a `button` column is tapped, with the stored record. The button is busy until the promise settles. See Button, time tracking … columns |
|
|
1197
1220
|
| `onChangeGroupBy` | `(groupBy: string) => void` | Called when the user picks another grouping in the group-by selector or by applying a saved view (`""` for none), so the app can store it with the tab. Not called when the list opens on a view |
|
|
1198
1221
|
| `groupIdCollapsible` | `{ [key: string]: boolean }` | Initial collapsed/expanded state per group ID |
|
|
1199
1222
|
| `sortGroupByAlphabetically` | `boolean` | Sorts group-by values alphabetically |
|
|
@@ -1309,6 +1332,7 @@ Injects custom React components and callbacks into the list, read, and update vi
|
|
|
1309
1332
|
| `bareListMobileFloatingSearch` | `boolean` | Floating search bar at the bottom on phones; the list reserves room under its last row for it |
|
|
1310
1333
|
| `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 |
|
|
1311
1334
|
| `bareListMobileTooltip` | `NewListMobileTooltip` | Passed to NewList when `bareNewDTW` is on: `imageKey`, `summaryKeys`, `tagKeys` and `tagColors` for the hover card on list items |
|
|
1335
|
+
| `bareSystemColumns` | `boolean` | Offers Created At / By, Updated At / By, Record ID and Number in the column settings, sort, filters and saved views without declaring them. See Button, time tracking … columns |
|
|
1312
1336
|
| `bareCalendarOnClickEvent` | `(event: NewCalendarEvent, rawData?: any) => void` | Replaces the calendar's open-in-read-view on tap when `bareNewDTW` is on; `rawData` is the tapped event's unformatted row. See Calendar view |
|
|
1313
1337
|
| `bareCalendarMapEvents` | `(events: NewCalendarEvent[]) => NewCalendarEvent[]` | Reshapes the `bareNewDTW` calendar's events after each fetch — expand one row into several, or restyle; each event's row is in `meta.rawData`. See Calendar view |
|
|
1314
1338
|
| `bareCalendarOnChangeRange` | `(start: Date, end: Date, view) => void` | Fired when the user pages the `bareNewDTW` calendar or switches its view, with the range now on screen. Never fires on render. See Calendar view |
|
|
@@ -1160,7 +1160,7 @@ const GROUPBY_SCHEMA: DataStructure[] = [
|
|
|
1160
1160
|
|
|
1161
1161
|
// INFO: 2400 rows in owner blocks (~400 each), so the first page holds only
|
|
1162
1162
|
// Alice — every other band has to load on its own. Every 40th row has no owner
|
|
1163
|
-
// and lands in
|
|
1163
|
+
// and lands in the No Value section.
|
|
1164
1164
|
const renderGroupByRecords = () => {
|
|
1165
1165
|
const rows: any[] = [];
|
|
1166
1166
|
const total = 2400;
|
|
@@ -1206,14 +1206,20 @@ const handleInstallGroupByMock = (store: any[]) => {
|
|
|
1206
1206
|
}
|
|
1207
1207
|
};
|
|
1208
1208
|
|
|
1209
|
-
// INFO: applies every "=" and "!in" condition — a band's own query
|
|
1210
|
-
// any filter the list carries — so a filter narrows each band's total
|
|
1209
|
+
// INFO: applies every "=", "in" and "!in" condition — a band's own query
|
|
1210
|
+
// and any filter the list carries — so a filter narrows each band's total.
|
|
1211
|
+
// A null in a list matches a missing field too, as Mongo's $in does.
|
|
1212
|
+
const handleIsIn = (list: any[], value: any) =>
|
|
1213
|
+
list.some((each) =>
|
|
1214
|
+
each === null ? value === null || value === undefined : each === value,
|
|
1215
|
+
);
|
|
1211
1216
|
const handleMatchQuery = (row: any, query: any[]) =>
|
|
1212
1217
|
(Array.isArray(query) ? query : []).every((eachQuery) => {
|
|
1213
1218
|
if (!eachQuery?.queryId) return true;
|
|
1214
1219
|
const value = row[eachQuery.queryId];
|
|
1215
1220
|
if (eachQuery.type === "=") return value === eachQuery.value;
|
|
1216
|
-
if (eachQuery.type === "
|
|
1221
|
+
if (eachQuery.type === "in") return handleIsIn(eachQuery.value, value);
|
|
1222
|
+
if (eachQuery.type === "!in") return !handleIsIn(eachQuery.value, value);
|
|
1217
1223
|
return true;
|
|
1218
1224
|
});
|
|
1219
1225
|
|
|
@@ -1337,7 +1343,7 @@ export const GroupByScrollPagination = () => (
|
|
|
1337
1343
|
<div className="text-sm mb-4 text-gray-600">
|
|
1338
1344
|
2,400 items in owner blocks (~400 each, every 40th with no owner), all
|
|
1339
1345
|
from local mock data. The board opens grouped by <code>Owner</code>; the
|
|
1340
|
-
first page holds only Alice, yet every owner and
|
|
1346
|
+
first page holds only Alice, yet every owner, Unknown and No Value is a
|
|
1341
1347
|
band from the start and loads its own rows. To demo: keep the{" "}
|
|
1342
1348
|
<strong>Group</strong> picker on Owner (or switch it to Status), then
|
|
1343
1349
|
scroll down inside a band — that group fetches its next page on its own,
|