blixify-ui-web-mcp 0.1.71 → 0.1.75
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/chart/barChart/README.md +2 -0
- package/lib/components/chart/barChart/barChart.stories.tsx +86 -3
- package/lib/components/chart/pieChart/README.md +3 -1
- package/lib/components/chart/pieChart/pieChart.stories.tsx +18 -1
- package/lib/components/data/dataGraph/README.md +18 -1
- package/lib/components/data/dataTemplate/README.md +72 -4
- package/lib/components/data/dataTemplate/activityModal.stories.tsx +52 -0
- package/lib/components/data/dataTemplate/dataTemplate.groupedBoard.stories.tsx +259 -0
- package/lib/components/data/dataTemplate/dataTemplate.stories.tsx +162 -0
- package/lib/components/data/dataWrapper/README.md +45 -3
- package/lib/components/data/flexFilter/flexFilterRelated.stories.tsx +182 -0
- package/lib/components/data/readQuery/README.md +8 -8
- package/lib/components/display/drawer/README.md +1 -1
- package/lib/components/display/modal/README.md +1 -0
- package/lib/components/input/textInput/README.md +30 -30
- package/lib/components/input/textInput/textinput.stories.tsx +30 -0
- package/lib/components/navigation/leftNav/README.md +2 -1
- package/lib/components/navigation/leftNav/leftNav.stories.tsx +61 -0
- package/package.json +1 -1
|
@@ -103,6 +103,7 @@ Custom styled with annotation labels:
|
|
|
103
103
|
| `stackedVertical` | `boolean` | `false` | Stack series on top of each other (requires `stackedBarSeries`) |
|
|
104
104
|
| `distributed` | `boolean` | `false` | Give each bar in a single series a distinct colour |
|
|
105
105
|
| `showDataLabel` | `boolean` | `true` | Show value labels on bars |
|
|
106
|
+
| `dataLabelPosition` | `"inside" \| "outside"` | — | Where each bar's value is printed: inside the bar, or outside past its end (above an upright bar, beside a horizontal one) in dark text. Unset, it is outside on a chart with `benchmarkLines` and inside otherwise. A stacked chart always keeps its values inside |
|
|
106
107
|
| `showToolbar` | `boolean` | `false` | Show the ApexCharts toolbar |
|
|
107
108
|
| `borderColor` | `string` | `"#e7e7e7"` | Grid line colour. Pass `""` to remove grid lines |
|
|
108
109
|
| `newTabUrl` | `string` | `""` | Makes the chart title a link that opens in a new tab |
|
|
@@ -119,6 +120,7 @@ Custom styled with annotation labels:
|
|
|
119
120
|
| `roundedBars` | `boolean` | `false` | Apply rounded corners to bar tops |
|
|
120
121
|
| `disableZoom` | `boolean` | `false` | Disable zoom interaction |
|
|
121
122
|
| `disableTooltip` | `boolean` | `false` | Disable hover tooltips |
|
|
123
|
+
| `benchmarkLines` | `{ value, label?, color? }[]` | — | Dashed benchmark lines across the chart, each with its label (or its value, for one without a label) and colour (default purple); hovering a label shows both. Placed like monday.com: just above the line at its left end on an upright bar, and along the top just right of the line on a horizontal one (left of it at the right edge). Close labels move apart, and none covers a bar value: with benchmark lines the bars show their values past their ends. Past 3 rows, a horizontal label shows as "…". The value axis grows to show a benchmark above every bar |
|
|
122
124
|
| `customAnimation` | `AnimationConfig` | — | Override default animation settings |
|
|
123
125
|
| `xAxisLabels` | `AxisLabelConfig` | — | Customise X-axis label rotation, style, and formatter |
|
|
124
126
|
| `yAxisLabels` | `AxisLabelConfig` | — | Customise Y-axis label rotation, style, and formatter |
|
|
@@ -8,13 +8,20 @@ import { StorySection } from "../../storyUtils";
|
|
|
8
8
|
const meta = { title: "Chart/Bar Chart" };
|
|
9
9
|
export default meta;
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
const stackedSeries = [
|
|
13
12
|
{ name: "Company A", data: [400, 430, 448] },
|
|
14
13
|
{ name: "Company B", data: [430, 460, 478] },
|
|
15
14
|
{ name: "Company C", data: [448, 470, 490] },
|
|
16
15
|
];
|
|
17
16
|
|
|
17
|
+
const benchmarkLines = [
|
|
18
|
+
{ value: 15, label: "April SF Target", color: "#8B5CF6" },
|
|
19
|
+
{ value: 30, label: "May SF Target", color: "#8B5CF6" },
|
|
20
|
+
{ value: 45, label: "June SF Target", color: "#8B5CF6" },
|
|
21
|
+
{ value: 3, label: "April LF Target", color: "#EF4444" },
|
|
22
|
+
{ value: 6, label: "June LF Target", color: "#EF4444" },
|
|
23
|
+
];
|
|
24
|
+
|
|
18
25
|
const rawData = [
|
|
19
26
|
{ label: "Apple", value: 10, daysAgo: 7 },
|
|
20
27
|
{ label: "Mango", value: 20, daysAgo: 7 },
|
|
@@ -50,7 +57,7 @@ export const BarChart = () => {
|
|
|
50
57
|
.map((d) => ({ x: d.label, y: d.value }));
|
|
51
58
|
setSeriesData(points);
|
|
52
59
|
},
|
|
53
|
-
[chartDummyData]
|
|
60
|
+
[chartDummyData],
|
|
54
61
|
);
|
|
55
62
|
|
|
56
63
|
useEffect(() => {
|
|
@@ -95,6 +102,82 @@ export const BarChart = () => {
|
|
|
95
102
|
/>
|
|
96
103
|
</StorySection>
|
|
97
104
|
|
|
105
|
+
<StorySection
|
|
106
|
+
title="Values Outside The Bars"
|
|
107
|
+
description={
|
|
108
|
+
<>
|
|
109
|
+
<code>dataLabelPosition="outside"</code> prints each bar's value
|
|
110
|
+
past its end: above an upright bar, beside a horizontal one. Charts
|
|
111
|
+
with <code>benchmarkLines</code> do this on their own.
|
|
112
|
+
</>
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
<BarChartImpl
|
|
116
|
+
title="Videos Done by Tier"
|
|
117
|
+
seriesName="Videos"
|
|
118
|
+
orientation="col"
|
|
119
|
+
distributed
|
|
120
|
+
dataLabelPosition="outside"
|
|
121
|
+
categories={["Tier 3", "Tier 2", "Tier 1"]}
|
|
122
|
+
seriesData={[
|
|
123
|
+
{ x: "Tier 3", y: 8 },
|
|
124
|
+
{ x: "Tier 2", y: 12 },
|
|
125
|
+
{ x: "Tier 1", y: 33 },
|
|
126
|
+
]}
|
|
127
|
+
/>
|
|
128
|
+
</StorySection>
|
|
129
|
+
|
|
130
|
+
<StorySection
|
|
131
|
+
title="Benchmark Lines"
|
|
132
|
+
description={
|
|
133
|
+
<>
|
|
134
|
+
<code>benchmarkLines</code> draws a dashed line across the chart at
|
|
135
|
+
each goal, with its label, in its <code>color</code>. The value axis
|
|
136
|
+
grows to show a goal above every bar, and a horizontal bar draws its
|
|
137
|
+
goals upright.
|
|
138
|
+
</>
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<BarChartImpl
|
|
142
|
+
title="Videos Done by Type"
|
|
143
|
+
seriesName="Videos"
|
|
144
|
+
orientation="col"
|
|
145
|
+
distributed
|
|
146
|
+
categories={[
|
|
147
|
+
"Poster (Static)",
|
|
148
|
+
"Photo",
|
|
149
|
+
"Video (Long)",
|
|
150
|
+
"Video (short)",
|
|
151
|
+
]}
|
|
152
|
+
seriesData={[
|
|
153
|
+
{ x: "Poster (Static)", y: 3 },
|
|
154
|
+
{ x: "Photo", y: 4 },
|
|
155
|
+
{ x: "Video (Long)", y: 17 },
|
|
156
|
+
{ x: "Video (short)", y: 57 },
|
|
157
|
+
]}
|
|
158
|
+
benchmarkLines={benchmarkLines}
|
|
159
|
+
/>
|
|
160
|
+
<div className="mt-6">
|
|
161
|
+
<BarChartImpl
|
|
162
|
+
title="Videos Done by Type (horizontal, close goals stack)"
|
|
163
|
+
seriesName="Videos"
|
|
164
|
+
orientation="row"
|
|
165
|
+
categories={["Poster (Static)", "Photo", "Video (Long)"]}
|
|
166
|
+
seriesData={[
|
|
167
|
+
{ x: "Poster (Static)", y: 3 },
|
|
168
|
+
{ x: "Photo", y: 4 },
|
|
169
|
+
{ x: "Video (Long)", y: 17 },
|
|
170
|
+
]}
|
|
171
|
+
benchmarkLines={[
|
|
172
|
+
{ value: 30, label: "May SF Target", color: "#8B5CF6" },
|
|
173
|
+
{ value: 4, label: "April LF Target", color: "#EF4444" },
|
|
174
|
+
{ value: 5, label: "May LF Target", color: "#EF4444" },
|
|
175
|
+
{ value: 6, label: "June LF Target", color: "#10B981" },
|
|
176
|
+
]}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
</StorySection>
|
|
180
|
+
|
|
98
181
|
<StorySection
|
|
99
182
|
title="Stacked Vertical Bar Chart"
|
|
100
183
|
description={
|
|
@@ -309,4 +392,4 @@ export const BarChart = () => {
|
|
|
309
392
|
</StorySection>
|
|
310
393
|
</div>
|
|
311
394
|
);
|
|
312
|
-
}
|
|
395
|
+
};
|
|
@@ -83,8 +83,10 @@ Chart only (no header or date range):
|
|
|
83
83
|
| `type` | `"pie" \| "donut"` | `"donut"` | Chart type |
|
|
84
84
|
| `valueType` | `"percent" \| "value"` | `"percent"` | Whether labels and data labels show percentages or raw values |
|
|
85
85
|
| `roundValue` | `boolean` | `false` | Round displayed values to whole numbers |
|
|
86
|
-
| `legendPos` | `"top" \| "bottom" \| "right" \| "left"` | `"top"` | Position of the legend
|
|
86
|
+
| `legendPos` | `"top" \| "bottom" \| "right" \| "left"` | `"top"` | Position of the legend. Left and Right move below, centred, while the chart is narrower than 360px (`PIE_SIDE_LEGEND_MIN_WIDTH`), and return beside it from 384px |
|
|
87
87
|
| `legendAlign` | `"right" \| "left" \| "center"` | `"right"` | Horizontal alignment of the legend |
|
|
88
|
+
| `showTotal` | `boolean` | `false` | Show the sum of the series in the centre of a donut. Ignored for `type="pie"`. Selecting a slice shows its name and value there instead |
|
|
89
|
+
| `showDataLabel` | `boolean` | `true` | Draw each slice's value on the pie. The legend and tooltip still show it when `false` |
|
|
88
90
|
| `newTabUrl` | `string` | `""` | Makes the chart title a link that opens in a new tab |
|
|
89
91
|
|
|
90
92
|
> Note: Legend labels are truncated to 10 characters. The full label is shown in the tooltip on hover.
|
|
@@ -137,9 +137,25 @@ export const PieChart = () => {
|
|
|
137
137
|
</div>
|
|
138
138
|
</StorySection>
|
|
139
139
|
|
|
140
|
+
<StorySection
|
|
141
|
+
title="Total And Slice Values"
|
|
142
|
+
description='showTotal=true puts the sum in the centre of a donut, its "Total" caption smaller and lighter than the number. showDataLabel=false hides the value on each slice; the legend and tooltip still show it.'
|
|
143
|
+
>
|
|
144
|
+
<div className="w-full">
|
|
145
|
+
<PieChartImpl
|
|
146
|
+
title="Orders By Channel"
|
|
147
|
+
series={series}
|
|
148
|
+
labels={labels}
|
|
149
|
+
showTotal
|
|
150
|
+
showDataLabel={false}
|
|
151
|
+
roundValue={true}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
</StorySection>
|
|
155
|
+
|
|
140
156
|
<StorySection
|
|
141
157
|
title="Dark Mode"
|
|
142
|
-
description="Pass darkMode=true and optionally darkModeBg to customise the background."
|
|
158
|
+
description="Pass darkMode=true and optionally darkModeBg to customise the background. The total in the centre turns white, with a lighter grey caption."
|
|
143
159
|
dark
|
|
144
160
|
>
|
|
145
161
|
<div className="w-full">
|
|
@@ -150,6 +166,7 @@ export const PieChart = () => {
|
|
|
150
166
|
tool={<PencilIcon className="h-4 w-4 text-primary-600" />}
|
|
151
167
|
darkMode={true}
|
|
152
168
|
darkModeBg="bg-gradient-to-r from-slate-700 to-gray-900"
|
|
169
|
+
showTotal
|
|
153
170
|
roundValue={true}
|
|
154
171
|
/>
|
|
155
172
|
</div>
|
|
@@ -177,6 +177,9 @@ Custom layout via renderSettings:
|
|
|
177
177
|
| `cardIcon` | `string` | Heroicon name (solid) for the card icon, e.g. `"UserIcon"`. Defaults to `FolderIcon`. |
|
|
178
178
|
| `cardSize` | `"default" \| "compact"` | `card` layout; `"compact"` is the short stat-first card. Defaults to `"default"`. |
|
|
179
179
|
| `pieType` | `"donut" \| "pie"` | `pie` style. Defaults to `"donut"`. |
|
|
180
|
+
| `legendPosition` | `"top" \| "bottom" \| "left" \| "right"` | `pie` legend placement. Defaults to `"top"`. |
|
|
181
|
+
| `showDataLabel` | `boolean` | `pie` only: `false` hides the value on each slice; the legend and tooltip still show it. Defaults to shown. |
|
|
182
|
+
| `showTotal` | `boolean` | `pie` donut only: the total in the centre. Ignored for averages. Defaults to hidden. |
|
|
180
183
|
| `cardEmoji` | `string` | Emoji shown instead of `cardIcon` when set, e.g. `"📦"`. Clearing it brings `cardIcon` back. |
|
|
181
184
|
| `cardIconBgColor` | `string` | Tailwind bg class for the card icon background |
|
|
182
185
|
| `colors` | `string[]` | Custom color array for chart segments/bars/lines |
|
|
@@ -187,15 +190,29 @@ Custom layout via renderSettings:
|
|
|
187
190
|
| `segmentBy` | `string` | Second column split into stacked series (bar, line, table), at most 10 values plus "Others". |
|
|
188
191
|
| `sortBy` | `"value" \| "label"` | Sort the groups. `sortDirection` is `"asc"` or `"desc"` (default). |
|
|
189
192
|
| `topN` | `number` | Keep the first N groups; the rest are added up as "Others" unless `showOthers` is `false` (averages never get "Others"). |
|
|
190
|
-
| `showPercent` | `boolean` | Show each group as a percentage of the total (count and sum, unsplit charts).
|
|
193
|
+
| `showPercent` | `boolean` | Show each group as a percentage of the total (count and sum, unsplit charts). Ignored for `pie`, which already draws slices as shares. |
|
|
191
194
|
| `cumulative` | `boolean` | Running total over the date buckets. |
|
|
192
195
|
| `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
196
|
| `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. |
|
|
197
|
+
| `benchmarks` | `{ label?, value, colour? }[]` | `bar` only: goals drawn as dashed lines across the chart, each with its label. `colour` is `purple` (default) `red` `amber` `green` `blue`, the same five as `highlights`, each with a lighter shade ready for dark mode (`handleChartBenchmarkLines(chart, darkMode)`); DataGraph's cards are light today, so it uses the light one. Picked from a row of swatches in the builder's Benchmark lines section. |
|
|
198
|
+
| `valuePosition` | `"inside" \| "outside" \| "hidden"` | `bar` only: where the values are printed, or `"hidden"` for none. Unset is inside; with `benchmarks` it is outside, and inside counts as outside. A chart split by a column (`segmentBy`) has no benchmark lines and keeps its values inside or hidden. Set in the builder's Values position dropdown. |
|
|
194
199
|
| `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
200
|
| `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
201
|
| `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
202
|
| `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. |
|
|
198
203
|
|
|
204
|
+
##### Chart builder sections
|
|
205
|
+
|
|
206
|
+
ChartView's Edit Chart form groups these settings into collapsible sections, like monday.com's Widget settings. A header opens or closes its section, and a closed one keeps its fields in the form and shows a one-line summary.
|
|
207
|
+
|
|
208
|
+
| Section | Opens by default | Fields | Summary when closed |
|
|
209
|
+
| --------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------ |
|
|
210
|
+
| Chart | Yes | Datasheet, Title, type, Emoji (card), Orientation / Style, Width | The chart type |
|
|
211
|
+
| Data | Only when a setting is missing | Measure, Of column, Group By, Date Range On / Over Time, Interval, Split By | e.g. "Sum of Amount by Status", or what's missing in red, such as "Pick a column to sum" |
|
|
212
|
+
| Customize | No | Values position, Legend, Show values / Show total, Sort / Show, Show as %, Multicolour bars, the table options, Running total. Hidden when the chart has none of these, such as a card | — |
|
|
213
|
+
| Benchmark lines | No | `bar` only: the goal lines. A chart split by a column says why it has none | e.g. "2 lines", "None" |
|
|
214
|
+
| Records list | No | Columns, Sort by, Order, Rows per page | e.g. "20 rows per page, sorted by Amount" |
|
|
215
|
+
|
|
199
216
|
##### WorkspaceDevSettings
|
|
200
217
|
|
|
201
218
|
| Name | Type | Description |
|
|
@@ -750,6 +750,8 @@ A `type: "formula"` column is a monday.com-style formula. It is worked out from
|
|
|
750
750
|
- **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.
|
|
751
751
|
- **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.
|
|
752
752
|
- **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.
|
|
753
|
+
- **On a server**, wrap evaluation in `handleWithFormulaTimezone(timezone, () => …)` so `TODAY()`, `NOW()` and stored dates are read on the reader's calendar rather than the server's (UTC). Inside it, a `Date` or zoned text (`…Z`, `…+08:00`) becomes that timezone's wall-clock time; text without a zone is read as written. An invalid or missing timezone changes nothing, and the browser needs no wrapper.
|
|
754
|
+
- **Charts** offer formula columns to group by (bar, pie, table) and to sum or average (`handleGetMeasureOptions`). Their values are never stored, so the server answering the chart works them out per row; Space does this.
|
|
753
755
|
- **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.
|
|
754
756
|
|
|
755
757
|
---
|
|
@@ -809,6 +811,70 @@ edit={{
|
|
|
809
811
|
}}
|
|
810
812
|
```
|
|
811
813
|
|
|
814
|
+
#### Attachments and reactions on comments
|
|
815
|
+
|
|
816
|
+
Both are off until configured, and an app that sets neither behaves as before.
|
|
817
|
+
|
|
818
|
+
**Attachments.** Pass `edit.activityLog.attachments` and the composer gets a paperclip (a hidden file input) and a chip per picked file. A comment may be only files. Up to 5 files of 20 MB each (`maxFiles`, `maxBytes`), photos, video, audio and PDFs.
|
|
819
|
+
|
|
820
|
+
```tsx
|
|
821
|
+
edit={{
|
|
822
|
+
activityLog: {
|
|
823
|
+
enableComments: true,
|
|
824
|
+
commentCollectionId: "tasks-comments",
|
|
825
|
+
currentUserId: currentUser.id,
|
|
826
|
+
attachments: {
|
|
827
|
+
// Called when the comment is sent, once per file, in order.
|
|
828
|
+
upload: async (file, { commentId, collectionId, itemId }) => {
|
|
829
|
+
const path = await storeFile(file, `${commentId}/${file.name}`);
|
|
830
|
+
return { name: file.name, type: file.type, size: file.size, path };
|
|
831
|
+
},
|
|
832
|
+
// After a failed send, an edit that drops a file, and a deleted comment. Errors are ignored.
|
|
833
|
+
remove: async (attachment) => deleteFile(attachment.path),
|
|
834
|
+
// The link the browser loads; defaults to `path`.
|
|
835
|
+
getUrl: (attachment) => `/files/${attachment.path}`,
|
|
836
|
+
},
|
|
837
|
+
},
|
|
838
|
+
}}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
Files are uploaded **when the comment is sent**, not when they are picked. If an upload fails nothing is saved, the files already uploaded are removed with `remove`, and the draft and chips stay for another try. If the comment itself cannot be saved the uploaded files are removed too. The comment keeps `attachments: [{ name, type, size?, path, width?, height? }]`, replaced as a whole on an edit.
|
|
842
|
+
|
|
843
|
+
In the feed nothing is fetched up front. Images are `loading="lazy"` (give `width` and `height` to keep their place while loading), a video is a card until it is played, audio uses `preload="none"`, and PDFs and other files are a link. A picture the browser cannot draw (a HEIC photo outside Safari) falls back to the file card. The library does not resize or convert images.
|
|
844
|
+
|
|
845
|
+
**Reactions.** Set `enableReactions`, `reactionCollectionId` and `currentUserId`. Each comment gets a Like button (the first of `reactionEmojis`, default 👍), a chip per other emoji with its count (hold one to see who), and an add-reaction popover with quick emoji and a lazily loaded emoji-mart picker.
|
|
846
|
+
|
|
847
|
+
```tsx
|
|
848
|
+
activityLog: {
|
|
849
|
+
enableReactions: true,
|
|
850
|
+
reactionCollectionId: "tasks-reactions",
|
|
851
|
+
reactionEmojis: ["👍", "❤️", "😂", "🎉"], // optional; the first is the Like
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
**Who reacted.** Holding any reaction (about half a second; right-click or the keyboard's context-menu key also work) opens a small panel against it, in the style of a log row's details: a "Reactions" title over a divider, then each emoji with the names of the people who chose it under it. The emoji held comes first. A plain click still adds or takes back your own reaction, and the click that ends a hold does neither.
|
|
856
|
+
|
|
857
|
+
Reactions are not part of the comment. The collection holds one document per comment, person and emoji, with the id `<commentId>:<userId>:<emoji code points in hex joined by ->` (`handleReactionId`), shaped `{ _id | id, commentId, emoji, userId, collectionId, itemId, createdAt }`. A reaction is a **create** and taking it back is a **delete** of that one id, so a repeat cannot duplicate and nobody's click overwrites another's. The panel reads the reactions of the comments on screen with one list (`collectionId` = and `commentId` in), and reacting never changes the comment (no "Edited"). The backend should set `userId` from the signed-in user, accept only the matching id, and delete a comment's reactions when the comment goes; the panel's `userId` is only a request.
|
|
858
|
+
|
|
859
|
+
#### Replies and who may edit
|
|
860
|
+
|
|
861
|
+
**Replies.** Set `enableReplies` and each comment gets a Reply button. Choosing it shows "Replying to <name> <first line>" above the box (with a cancel), and the reply is saved as a comment of its own with `replyTo: { commentId }` on the record of the comment it answers. A reply to a reply quotes that reply, so there is no nesting: the feed stays flat and paged.
|
|
862
|
+
|
|
863
|
+
In the feed a reply shows a quote over its text: the author's name and **one line** of the original (its first line, cut with an ellipsis; "📷 Photo", "🎥 Video" and so on when the original has only files), never the whole comment. Clicking it scrolls to the original and highlights it when it is on the page. The feed is paged, so an original on another page opens in a window with its full text. The quoted comments of a page are read in one list (`collectionId` = and the id `in`), and one that is gone shows "Original comment was deleted". `replyTo` is set when the reply is made and never changed by an edit.
|
|
864
|
+
|
|
865
|
+
```tsx
|
|
866
|
+
activityLog: {
|
|
867
|
+
enableReplies: true,
|
|
868
|
+
// After a reply is saved, for the author of the comment answered (not for a reply to your own comment).
|
|
869
|
+
// It works like onMention: a failure keeps the comment and offers "Retry notifications", and without it the panel says notifications are not connected.
|
|
870
|
+
onReply: async (event) => sendReplyNotification(event),
|
|
871
|
+
}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
`ActivityReplyEvent` is exported from `data/dataTemplate/dataStateModel`: `commentId`, `commentCollectionId`, `collectionId`, `itemId`, `comment`, `userId`, `replyToCommentId` and `replyToUserId`. The backend should check the original exists on the same datasheet and record, as it does for reactions.
|
|
875
|
+
|
|
876
|
+
**Who may edit.** With `currentUserId` given, Edit shows only on a comment you wrote, and Delete on yours and, when `canModerate` is set, on everyone's. A comment with no author is open to everyone, and so is every comment when `currentUserId` is not given. An edit never sends `userId`, so editing a comment without an author does not make the editor its author. These only decide what is offered: the backend has to refuse the rest.
|
|
877
|
+
|
|
812
878
|
`ActivityMentionEvent` is exported from `data/dataTemplate/dataStateModel`. It contains `commentId`, `commentCollectionId`, `collectionId`, `itemId`, `comment`, `userId`, and `mentionedUserIds`. A failed callback displays a retry action that repeats only notification delivery, never the comment write. The handler must be idempotent per comment ID and recipient, validate recipients' access server-side, and use trusted server-side identity. UI props and mention IDs are not an authorization boundary. Existing backend access controls continue to govern comment creation, editing, and deletion.
|
|
813
879
|
|
|
814
880
|
The panel keeps DataTemplateWrapper for fetching and paging, and uses the shared WriteQuery transport for composer mutations, forwarding configured API and user tokens. Custom comment-collection schemas must accept the added mention fields. No notification endpoint is assumed by the library.
|
|
@@ -921,7 +987,7 @@ Enable `organise.exportSettings.enableExportReadPDF` to add a PDF export button
|
|
|
921
987
|
hideExportPDF: false, // hide list-view PDF export (not read-view)
|
|
922
988
|
hideExportPrint: false, // hide Print option
|
|
923
989
|
hideExportQR: false, // hide Export QR option
|
|
924
|
-
hideExportDraft: false, // hide Filter Draft option
|
|
990
|
+
hideExportDraft: false, // hide Filter Draft option, shown only with edit.enableUpdateDraft
|
|
925
991
|
hideSubItems: false, // hide nested rows in exports
|
|
926
992
|
},
|
|
927
993
|
}}
|
|
@@ -1187,6 +1253,7 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
1187
1253
|
| `additionalQuery` | `AdditionalQuery[]` | Filter/search controls rendered above the list — see Additional Query below |
|
|
1188
1254
|
| `darkMode` | `boolean` | Applies dark colour scheme to the organise bar |
|
|
1189
1255
|
| `hideSearch` | `boolean` | Hides the search input |
|
|
1256
|
+
| `hideSearchLabel` | `boolean` | Hides the "Search" label above the search field, keeping its placeholder, so the list header is shorter |
|
|
1190
1257
|
| `hidePagination` | `boolean` | Hides pagination on Table, List, Grid, and Feeds views |
|
|
1191
1258
|
| `hideNewTabs` | `boolean` | Hides the new-tab creation button |
|
|
1192
1259
|
| `splitView` | `boolean` | Renders a split list/detail layout in read mode |
|
|
@@ -1210,14 +1277,14 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
1210
1277
|
| `addColumnEnabled` | `boolean` | Shows an add-column button in the table header |
|
|
1211
1278
|
| `persistViewState` | `"persistent" \| "session"` | Remembers the list's sort, filters, hidden columns, page, rows per page, search and reading position per collection, so a Back navigation lands where the user left. `"persistent"` uses `localStorage`; `"session"` writes `sessionStorage` and reads it first, falling back to `localStorage`. The reading position is restored on mount and on returning from a record, not on a tab switch; with several lists on one page, only the list a record was last opened from restores (tracked in `blixify-dtw-persist-last-drill`). Covers table, list, grid, kanban and feeds; not map. List view only |
|
|
1212
1279
|
| `persistKey` | `string` | Keeps a separate saved view state for this instance: the storage key becomes `blixify-dtw-persist-<collectionId>-<persistKey>`. Saved state is read once, on mount, so give the component a React `key` that changes with `persistKey` |
|
|
1213
|
-
| `savedViews` | `boolean` | Shows a row of view tags under the tab bar: save the current filters, flex filters, sort, rows per page, grouping, columns and column widths under a name, reapply it, update
|
|
1280
|
+
| `savedViews` | `boolean` | Shows a row of view tags under the tab bar: save the current filters, flex filters, sort, rows per page, grouping, columns and column widths under a name, reapply it, update, rename or delete it. Past eight views the rest fold behind “+N more”; the open view always shows. Views belong to the tab they were saved on and are kept in `localStorage` under `blixify-dtw-views-<collectionId>[-<persistKey>][-<currentUser.userEmail>]`. Sort is held by the template while it is on, as with `persistViewState`, and applying a view calls `onChangeSort`, `onChangeGroupBy`, `onChangePagination(0)` and `bareListHandleFilter` when those change. Each write re-reads storage first, so views saved in another browser tab are kept. With `onChangeHide` / `onChangeDrag`, a view's hides and column order stay on screen only: a toggle or reorder after applying one writes back the schema's columns plus that single change |
|
|
1214
1281
|
| `defaultSavedViews` | `DefaultSavedView[]` | Views provided by the app (`{ id?, name, tabId?, state }`), listed before the user's own and never deleted. Without `tabId` a view is listed on every tab. A field left out of `state` is reset to its default when the view is applied. `id` must be unique; without one the view gets `default-<index>`, its position in the whole array |
|
|
1215
1282
|
| `sharedViews` | `SharedSavedView[]` | Views the app stores for everyone (`{ id, name, tabId, state }`), e.g. on the record the list belongs to. Given, it replaces this browser's storage: the list shows only these, saving goes to `onChangeSharedViews`, and nothing is read from or written to `localStorage` |
|
|
1216
1283
|
| `onChangeSharedViews` | `(views: SharedSavedView[]) => void` | Called with the whole list after a view is saved, updated or deleted, for the app to store and pass back as `sharedViews`; a second change before it comes back builds on the list just sent. The whole list is replaced each time, so two editors saving at once is last-writer-wins. Without it the app's views are read-only and “+ Save view” is hidden |
|
|
1217
1284
|
| `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` |
|
|
1218
1285
|
| `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` |
|
|
1219
1286
|
| `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` |
|
|
1220
|
-
| `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 |
|
|
1287
|
+
| `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, one band at a time, and a band not read yet shows the grouped count's total (a band it counts empty stays hidden until its own read), 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 |
|
|
1221
1288
|
| `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 |
|
|
1222
1289
|
| `onTimeTrackingAction` | `(columnId: string, row: any, action: "start" \| "stop") => Promise<any>` | Starts or stops a `timeTracking` timer on the host's server, with the stored record. Resolve with the value the server stored; the timer shows it, and goes back to where it was if the promise rejects. Without it a timer works its value out on the device and saves it through the single-field update |
|
|
1223
1290
|
| `allView` | `PersistedViewState` | All's settings as the app stores them for everyone (`{}` when none are saved yet): sort, grouping, filters, hidden columns, column order, column widths (`columnWidths`, `"180px"` by column id) and rows per page. A setting left out falls back to the model and the props. Needs `savedViews`. Given, the list opens on it, and every change a person makes to All or a saved view is kept in their browser (`blixify-dtw-draft-<collectionId>[-<persistKey>][-<currentUser.userEmail>]-<all \| view id>`) until they choose Update or Reset; a saved view with no width of its own for a column follows All's |
|
|
@@ -1264,7 +1331,7 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
1264
1331
|
| `hideExportPDF` | `boolean` | Hides the Export PDF option |
|
|
1265
1332
|
| `hideExportPrint` | `boolean` | Hides the Print option |
|
|
1266
1333
|
| `hideExportQR` | `boolean` | Hides the Export QR option |
|
|
1267
|
-
| `hideExportDraft` | `boolean` | Hides the Filter Draft option
|
|
1334
|
+
| `hideExportDraft` | `boolean` | Hides the Filter Draft option, which shows only with `edit.enableUpdateDraft` |
|
|
1268
1335
|
| `enableExportReadPDF` | `boolean` | Enables PDF export from read view |
|
|
1269
1336
|
| `exportReadPDFHeader` | `PDFHeaderConfig` | Header config for read-view PDF export — see PDFHeaderConfig below |
|
|
1270
1337
|
| `exportReadPDFDimensions` | `PDFDimensionsConfig` | Page/capture dimensions for read-view PDF export — see PDFDimensionsConfig below |
|
|
@@ -1359,6 +1426,7 @@ Injects custom React components and callbacks into the list, read, and update vi
|
|
|
1359
1426
|
| `bareCSVImportEndpoint` | `string` | Custom CSV import endpoint |
|
|
1360
1427
|
| `bareCSVExportEndpoint` | `string` | Custom CSV export endpoint |
|
|
1361
1428
|
| `bareHideTableExtraMultipleAction` | `boolean` | Hides the extra multiple-action button in table view |
|
|
1429
|
+
| `bareListMobileToolbar` | `boolean` | On a phone, moves refresh and view settings into the floating search bar and keeps the title on one line. Needs `bareListMobileFloatingSearch` |
|
|
1362
1430
|
|
|
1363
1431
|
### Extended Tab
|
|
1364
1432
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
2
|
import axios from "axios";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
3
4
|
import MockAdapter from "axios-mock-adapter";
|
|
4
5
|
import { ActivityModal } from "./activityModal";
|
|
5
6
|
import { DataStructure, DevSettings } from "./model";
|
|
6
7
|
import { DataTemplateWrapper } from "../dataWrapper";
|
|
7
8
|
import { ActivityLog } from "./dataStateModel";
|
|
8
9
|
import { Modal } from "../../display/modal";
|
|
10
|
+
import { Drawer } from "../../display/drawer";
|
|
9
11
|
import { handleApplyStoryQueries } from "../dataWrapper/storyBackend.fixtures";
|
|
10
12
|
|
|
11
13
|
const meta = { title: "Data/Activity Panel" };
|
|
@@ -59,11 +61,16 @@ const ActivityDemo = ({
|
|
|
59
61
|
logsOnly = false,
|
|
60
62
|
integrated = false,
|
|
61
63
|
inModal = false,
|
|
64
|
+
inDrawer = false,
|
|
65
|
+
extras = false,
|
|
62
66
|
}: {
|
|
63
67
|
darkMode?: boolean;
|
|
64
68
|
logsOnly?: boolean;
|
|
65
69
|
integrated?: boolean;
|
|
66
70
|
inModal?: boolean;
|
|
71
|
+
inDrawer?: boolean;
|
|
72
|
+
// INFO: switches on file attachments and emoji reactions
|
|
73
|
+
extras?: boolean;
|
|
67
74
|
}) => {
|
|
68
75
|
const [ready, setReady] = useState(false);
|
|
69
76
|
const [failPings, setFailPings] = useState(false);
|
|
@@ -72,6 +79,7 @@ const ActivityDemo = ({
|
|
|
72
79
|
const [selectedId, setSelectedId] = useState("");
|
|
73
80
|
useEffect(() => {
|
|
74
81
|
const rows: Record<string, any[]> = {
|
|
82
|
+
"tasks-reactions": [],
|
|
75
83
|
tasks: [
|
|
76
84
|
{
|
|
77
85
|
_id: "task-1",
|
|
@@ -223,6 +231,28 @@ const ActivityDemo = ({
|
|
|
223
231
|
enableLogs: true,
|
|
224
232
|
logsCollectionId: "tasks-logs",
|
|
225
233
|
currentUserId: "alex",
|
|
234
|
+
...(extras
|
|
235
|
+
? {
|
|
236
|
+
// INFO: the demo keeps a picked file as a browser link; an app
|
|
237
|
+
// stores it and answers where it lives
|
|
238
|
+
attachments: {
|
|
239
|
+
upload: async (file: File) => ({
|
|
240
|
+
name: file.name,
|
|
241
|
+
type: file.type,
|
|
242
|
+
size: file.size,
|
|
243
|
+
path: URL.createObjectURL(file),
|
|
244
|
+
}),
|
|
245
|
+
},
|
|
246
|
+
enableReactions: true,
|
|
247
|
+
reactionCollectionId: "tasks-reactions",
|
|
248
|
+
enableReplies: true,
|
|
249
|
+
onReply: async (event: { replyToUserId: string | null }) => {
|
|
250
|
+
if (failPings) throw new Error("Demo notification failure");
|
|
251
|
+
setRecipients(event.replyToUserId ?? "");
|
|
252
|
+
},
|
|
253
|
+
canModerate: true,
|
|
254
|
+
}
|
|
255
|
+
: {}),
|
|
226
256
|
onMention: async (event) => {
|
|
227
257
|
if (failPings) throw new Error("Demo notification failure");
|
|
228
258
|
setRecipients(event.mentionedUserIds.join(", "));
|
|
@@ -313,6 +343,26 @@ const ActivityDemo = ({
|
|
|
313
343
|
onClose={() => {}}
|
|
314
344
|
renderContent={renderPanel}
|
|
315
345
|
/>
|
|
346
|
+
) : inDrawer ? (
|
|
347
|
+
// INFO: mirrors how DTW mounts the panel in Space: the library Drawer, portalled to the
|
|
348
|
+
// body inside a z-50 wrapper. Its frame clips, its content scrolls and its panel is
|
|
349
|
+
// translated, so a pop-up drawn inside the comment row is cut off by it
|
|
350
|
+
createPortal(
|
|
351
|
+
<div className="relative z-50">
|
|
352
|
+
<Drawer
|
|
353
|
+
darkMode={darkMode}
|
|
354
|
+
testId="story-activity-drawer"
|
|
355
|
+
open
|
|
356
|
+
onClose={() => {}}
|
|
357
|
+
anchor="responsive"
|
|
358
|
+
maxWidth="2xl"
|
|
359
|
+
maxHeight="75"
|
|
360
|
+
title="Activity Log"
|
|
361
|
+
content={renderPanel()}
|
|
362
|
+
/>
|
|
363
|
+
</div>,
|
|
364
|
+
document.body,
|
|
365
|
+
)
|
|
316
366
|
) : (
|
|
317
367
|
renderPanel()
|
|
318
368
|
))}
|
|
@@ -325,3 +375,5 @@ export const DarkMode = () => <ActivityDemo darkMode />;
|
|
|
325
375
|
export const LogsOnly = () => <ActivityDemo logsOnly />;
|
|
326
376
|
export const WithDataTemplateWrapper = () => <ActivityDemo integrated />;
|
|
327
377
|
export const InModal = () => <ActivityDemo inModal />;
|
|
378
|
+
export const WithAttachmentsAndReactions = () => <ActivityDemo extras />;
|
|
379
|
+
export const ReactionsInDrawer = () => <ActivityDemo extras inDrawer />;
|