blixify-ui-web-mcp 0.1.71 → 0.1.72
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 +68 -2
- package/lib/components/data/dataTemplate/activityModal.stories.tsx +52 -0
- package/lib/components/data/dataWrapper/README.md +45 -3
- package/lib/components/data/flexFilter/flexFilterRelated.stories.tsx +182 -0
- package/lib/components/display/drawer/README.md +1 -1
- 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 +1 -0
- package/package.json +1 -1
|
@@ -809,6 +809,70 @@ edit={{
|
|
|
809
809
|
}}
|
|
810
810
|
```
|
|
811
811
|
|
|
812
|
+
#### Attachments and reactions on comments
|
|
813
|
+
|
|
814
|
+
Both are off until configured, and an app that sets neither behaves as before.
|
|
815
|
+
|
|
816
|
+
**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.
|
|
817
|
+
|
|
818
|
+
```tsx
|
|
819
|
+
edit={{
|
|
820
|
+
activityLog: {
|
|
821
|
+
enableComments: true,
|
|
822
|
+
commentCollectionId: "tasks-comments",
|
|
823
|
+
currentUserId: currentUser.id,
|
|
824
|
+
attachments: {
|
|
825
|
+
// Called when the comment is sent, once per file, in order.
|
|
826
|
+
upload: async (file, { commentId, collectionId, itemId }) => {
|
|
827
|
+
const path = await storeFile(file, `${commentId}/${file.name}`);
|
|
828
|
+
return { name: file.name, type: file.type, size: file.size, path };
|
|
829
|
+
},
|
|
830
|
+
// After a failed send, an edit that drops a file, and a deleted comment. Errors are ignored.
|
|
831
|
+
remove: async (attachment) => deleteFile(attachment.path),
|
|
832
|
+
// The link the browser loads; defaults to `path`.
|
|
833
|
+
getUrl: (attachment) => `/files/${attachment.path}`,
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
}}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
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.
|
|
840
|
+
|
|
841
|
+
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.
|
|
842
|
+
|
|
843
|
+
**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.
|
|
844
|
+
|
|
845
|
+
```tsx
|
|
846
|
+
activityLog: {
|
|
847
|
+
enableReactions: true,
|
|
848
|
+
reactionCollectionId: "tasks-reactions",
|
|
849
|
+
reactionEmojis: ["👍", "❤️", "😂", "🎉"], // optional; the first is the Like
|
|
850
|
+
}
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
**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.
|
|
854
|
+
|
|
855
|
+
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.
|
|
856
|
+
|
|
857
|
+
#### Replies and who may edit
|
|
858
|
+
|
|
859
|
+
**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.
|
|
860
|
+
|
|
861
|
+
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.
|
|
862
|
+
|
|
863
|
+
```tsx
|
|
864
|
+
activityLog: {
|
|
865
|
+
enableReplies: true,
|
|
866
|
+
// After a reply is saved, for the author of the comment answered (not for a reply to your own comment).
|
|
867
|
+
// It works like onMention: a failure keeps the comment and offers "Retry notifications", and without it the panel says notifications are not connected.
|
|
868
|
+
onReply: async (event) => sendReplyNotification(event),
|
|
869
|
+
}
|
|
870
|
+
```
|
|
871
|
+
|
|
872
|
+
`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.
|
|
873
|
+
|
|
874
|
+
**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.
|
|
875
|
+
|
|
812
876
|
`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
877
|
|
|
814
878
|
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 +985,7 @@ Enable `organise.exportSettings.enableExportReadPDF` to add a PDF export button
|
|
|
921
985
|
hideExportPDF: false, // hide list-view PDF export (not read-view)
|
|
922
986
|
hideExportPrint: false, // hide Print option
|
|
923
987
|
hideExportQR: false, // hide Export QR option
|
|
924
|
-
hideExportDraft: false, // hide Filter Draft option
|
|
988
|
+
hideExportDraft: false, // hide Filter Draft option, shown only with edit.enableUpdateDraft
|
|
925
989
|
hideSubItems: false, // hide nested rows in exports
|
|
926
990
|
},
|
|
927
991
|
}}
|
|
@@ -1187,6 +1251,7 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
1187
1251
|
| `additionalQuery` | `AdditionalQuery[]` | Filter/search controls rendered above the list — see Additional Query below |
|
|
1188
1252
|
| `darkMode` | `boolean` | Applies dark colour scheme to the organise bar |
|
|
1189
1253
|
| `hideSearch` | `boolean` | Hides the search input |
|
|
1254
|
+
| `hideSearchLabel` | `boolean` | Hides the "Search" label above the search field, keeping its placeholder, so the list header is shorter |
|
|
1190
1255
|
| `hidePagination` | `boolean` | Hides pagination on Table, List, Grid, and Feeds views |
|
|
1191
1256
|
| `hideNewTabs` | `boolean` | Hides the new-tab creation button |
|
|
1192
1257
|
| `splitView` | `boolean` | Renders a split list/detail layout in read mode |
|
|
@@ -1264,7 +1329,7 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
1264
1329
|
| `hideExportPDF` | `boolean` | Hides the Export PDF option |
|
|
1265
1330
|
| `hideExportPrint` | `boolean` | Hides the Print option |
|
|
1266
1331
|
| `hideExportQR` | `boolean` | Hides the Export QR option |
|
|
1267
|
-
| `hideExportDraft` | `boolean` | Hides the Filter Draft option
|
|
1332
|
+
| `hideExportDraft` | `boolean` | Hides the Filter Draft option, which shows only with `edit.enableUpdateDraft` |
|
|
1268
1333
|
| `enableExportReadPDF` | `boolean` | Enables PDF export from read view |
|
|
1269
1334
|
| `exportReadPDFHeader` | `PDFHeaderConfig` | Header config for read-view PDF export — see PDFHeaderConfig below |
|
|
1270
1335
|
| `exportReadPDFDimensions` | `PDFDimensionsConfig` | Page/capture dimensions for read-view PDF export — see PDFDimensionsConfig below |
|
|
@@ -1359,6 +1424,7 @@ Injects custom React components and callbacks into the list, read, and update vi
|
|
|
1359
1424
|
| `bareCSVImportEndpoint` | `string` | Custom CSV import endpoint |
|
|
1360
1425
|
| `bareCSVExportEndpoint` | `string` | Custom CSV export endpoint |
|
|
1361
1426
|
| `bareHideTableExtraMultipleAction` | `boolean` | Hides the extra multiple-action button in table view |
|
|
1427
|
+
| `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
1428
|
|
|
1363
1429
|
### Extended Tab
|
|
1364
1430
|
|
|
@@ -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 />;
|
|
@@ -287,9 +287,40 @@ bareSettings={{
|
|
|
287
287
|
| Less Than 2 | `crew.1` does not exist |
|
|
288
288
|
| Between 2 and 5 | `crew.1` exists, `crew.5` does not |
|
|
289
289
|
|
|
290
|
-
Two conditions on one list may each be met by a different row.
|
|
290
|
+
Two conditions on one list may each be met by a different row.
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
**Filtering on related rows and their count**
|
|
293
|
+
|
|
294
|
+
`bareFlexFilterRelated` names other collections whose rows point to these rows, such as a referrer's referrals. Each entry has a `name`, its `collection`, the column there that points here (`foreignField`), the column here it matches (`localField`: `_id` for a reference column, or a contact column), and its `structure`. The key becomes part of the lookup alias, so it may hold only letters, digits, `_` and `-`. The lookup names a variable after `localField`, so it may hold only letters, digits and `_`. An entry that breaks either rule is left out of the filter.
|
|
295
|
+
|
|
296
|
+
The filters then offer `<Name> › <Field>` and `<Name> › Count`. The count is the number of that collection's rows pointing to the record that match the conditions on its fields. Field conditions with no count condition ask for at least one such row.
|
|
297
|
+
|
|
298
|
+
```tsx
|
|
299
|
+
bareSettings={{
|
|
300
|
+
bareFlexFilters: true,
|
|
301
|
+
bareFlexFilterRelated: {
|
|
302
|
+
referrals: {
|
|
303
|
+
name: "Referrals",
|
|
304
|
+
collection: "referrals",
|
|
305
|
+
localField: "phone",
|
|
306
|
+
foreignField: "referrerPhone",
|
|
307
|
+
structure: ReferralSchema,
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
}}
|
|
311
|
+
|
|
312
|
+
// "Referrals › Referral Date between 1 and 31 Oct" with "Referrals › Count
|
|
313
|
+
// Greater Than 3" is sent as one condition on the row count:
|
|
314
|
+
// { type: ">", queryId: "rel_referrals_count", value: 3,
|
|
315
|
+
// related: { collection: "referrals", localField: "phone", foreignField: "referrerPhone",
|
|
316
|
+
// alias: "rel_referrals", query: [{ type: "><", queryId: "referralDate", value: [...] }] } }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`ReadQuery` sends a `related` condition as a `pipeline` lookup with `referenceShowCount`, which MongoWrapper joins and counts before its `$match`, and leaves `rel_<key>_count` out of the rows it returns. The match is exact, so a contact column only matches a phone number stored in the same format. The lookup runs for every row the list's own read scope allows, before its other conditions, so index `foreignField` on a large collection: without it each row scans the whole related collection.
|
|
320
|
+
|
|
321
|
+
Referenced records, listObjects and related rows are not offered on Firebase or `unionCollections` lists.
|
|
322
|
+
|
|
323
|
+
The standalone `FlexFilter` takes all three as `extras={{ references, listObjects: true, related }}`; pass the same object to `renderFlexFilterConditions` when building the query.
|
|
293
324
|
|
|
294
325
|
**8. newTabUrl (Optional)**
|
|
295
326
|
|
|
@@ -1226,13 +1257,24 @@ needs to know about the other.
|
|
|
1226
1257
|
(`handleFormSubmit`, `handleUpdateSelectedData`, `handleDeleteModal`, `handleExportReadPDF`)
|
|
1227
1258
|
target the panel instance. `handleRefresh()` refreshes both.
|
|
1228
1259
|
|
|
1260
|
+
### Adding a row in scroll mode
|
|
1261
|
+
|
|
1262
|
+
With `organise.paginationType: "scroll"` and `edit.listAddable`, an Add button holds the bottom of the screen instead of an empty row after the last record. It opens the new row at the top of the table.
|
|
1263
|
+
|
|
1264
|
+
### bareSettings.bareFormDrawer
|
|
1265
|
+
|
|
1266
|
+
Create and edit open in the same panel over the list, so the list keeps its scroll, page
|
|
1267
|
+
and filters, and reloads when the panel closes. Off by default. Placement comes from
|
|
1268
|
+
`viewSettings.readDrawer` when set; on phones the sheet takes 90% of the height. A click
|
|
1269
|
+
outside or Escape closes the form, and asks first once something has been edited.
|
|
1270
|
+
|
|
1229
1271
|
### viewSettings.readDrawer
|
|
1230
1272
|
|
|
1231
1273
|
| Name | Type | Default | Description |
|
|
1232
1274
|
| --------------- | -------------------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------- |
|
|
1233
1275
|
| `anchor` | `"bottom" \| "right" \| "responsive"` | `"responsive"` | `responsive` = right panel above the `sm` breakpoint (640px), bottom sheet below it |
|
|
1234
1276
|
| `maxWidth` | `"lg" \| "xl" \| "2xl" \| "3xl" \| "4xl" \| "5xl" \| "6xl" \| "7xl"` | `"3xl"` | Panel width for the right anchor |
|
|
1235
|
-
| `maxHeight` | `"50" \| "75" \| "100"`
|
|
1277
|
+
| `maxHeight` | `"50" \| "75" \| "90" \| "100"` | `"75"` | Sheet height for the bottom anchor |
|
|
1236
1278
|
| `title` | `string` | `title` prop | Panel header title |
|
|
1237
1279
|
| `desc` | `string` | — | Panel header description |
|
|
1238
1280
|
| `disableExpand` | `boolean` | `false` | Hides the panel's expand-to-page button, leaving the peek as the only read surface |
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Transition } from "@headlessui/react";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { StorySection } from "../../storyUtils";
|
|
5
|
+
import { Loading } from "../../display/loading";
|
|
6
|
+
import { DataStructure, DevSettings } from "../dataTemplate/model";
|
|
7
|
+
import { DataTemplateWrapper } from "../dataWrapper";
|
|
8
|
+
import {
|
|
9
|
+
STORY_API_ENDPOINT,
|
|
10
|
+
STORY_NAMES,
|
|
11
|
+
storyWorkspace,
|
|
12
|
+
useStoryBackend,
|
|
13
|
+
} from "../dataWrapper/storyBackend.fixtures";
|
|
14
|
+
import {
|
|
15
|
+
FlexFilter,
|
|
16
|
+
FlexFilterRelatedSet,
|
|
17
|
+
renderFlexFilterConditions,
|
|
18
|
+
} from ".";
|
|
19
|
+
import { FlexFilterData } from "./model";
|
|
20
|
+
|
|
21
|
+
const meta = { title: "Data/Flex Filter/Related Rows" };
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
const USER_COLLECTION = "demo-rel-users";
|
|
25
|
+
const REFERRAL_COLLECTION = "demo-rel-referrals";
|
|
26
|
+
const REFERRAL_STATUSES = ["New", "Quoted", "Installed"];
|
|
27
|
+
|
|
28
|
+
const userSchema: DataStructure[] = [
|
|
29
|
+
{ id: "name", name: "Name", type: "string" },
|
|
30
|
+
{ id: "phone", name: "Phone", type: "contact" },
|
|
31
|
+
{
|
|
32
|
+
id: "status",
|
|
33
|
+
name: "Status",
|
|
34
|
+
type: "list",
|
|
35
|
+
listSelection: ["Active", "Inactive"],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const referralSchema: DataStructure[] = [
|
|
40
|
+
{ id: "referredName", name: "Referred Name", type: "string" },
|
|
41
|
+
{ id: "referrerPhone", name: "Referrer Phone", type: "contact" },
|
|
42
|
+
{ id: "referralDate", name: "Referral Date", type: "date" },
|
|
43
|
+
{
|
|
44
|
+
id: "status",
|
|
45
|
+
name: "Status",
|
|
46
|
+
type: "list",
|
|
47
|
+
listSelection: REFERRAL_STATUSES,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const related: FlexFilterRelatedSet = {
|
|
52
|
+
referrals: {
|
|
53
|
+
name: "Referrals",
|
|
54
|
+
collection: REFERRAL_COLLECTION,
|
|
55
|
+
localField: "phone",
|
|
56
|
+
foreignField: "referrerPhone",
|
|
57
|
+
structure: referralSchema,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// INFO: this month's dates, and last month's, so a date filter splits them
|
|
62
|
+
const handleThisMonth = (day: number) => {
|
|
63
|
+
const date = new Date();
|
|
64
|
+
date.setDate(Math.min(day, 28));
|
|
65
|
+
date.setHours(10, 0, 0, 0);
|
|
66
|
+
return date.toISOString();
|
|
67
|
+
};
|
|
68
|
+
const handleLastMonth = (day: number) => {
|
|
69
|
+
const date = new Date();
|
|
70
|
+
date.setDate(1);
|
|
71
|
+
date.setMonth(date.getMonth() - 1);
|
|
72
|
+
date.setDate(Math.min(day, 28));
|
|
73
|
+
date.setHours(10, 0, 0, 0);
|
|
74
|
+
return date.toISOString();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const users = STORY_NAMES.slice(0, 8).map((name, index) => ({
|
|
78
|
+
_id: `user-${index + 1}`,
|
|
79
|
+
name,
|
|
80
|
+
phone: `6012${String(1000000 + index)}`,
|
|
81
|
+
status: index % 4 === 3 ? "Inactive" : "Active",
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// INFO: user n has n referrals this month and (7 - n) last month, so "more
|
|
85
|
+
// than 3 this month" and "more than 3 in total" pick different people
|
|
86
|
+
const referrals = users.flatMap((eachUser, index) => [
|
|
87
|
+
...Array.from({ length: index }, (_, row) => ({
|
|
88
|
+
_id: `ref-${index}-now-${row}`,
|
|
89
|
+
referredName: STORY_NAMES[(index + row + 3) % STORY_NAMES.length],
|
|
90
|
+
referrerPhone: eachUser.phone,
|
|
91
|
+
referralDate: handleThisMonth(row + 2),
|
|
92
|
+
status: REFERRAL_STATUSES[row % REFERRAL_STATUSES.length],
|
|
93
|
+
})),
|
|
94
|
+
...Array.from({ length: 7 - index }, (_, row) => ({
|
|
95
|
+
_id: `ref-${index}-last-${row}`,
|
|
96
|
+
referredName: STORY_NAMES[(index + row + 7) % STORY_NAMES.length],
|
|
97
|
+
referrerPhone: eachUser.phone,
|
|
98
|
+
referralDate: handleLastMonth(row + 2),
|
|
99
|
+
status: REFERRAL_STATUSES[(row + 1) % REFERRAL_STATUSES.length],
|
|
100
|
+
})),
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
const extras = { related };
|
|
104
|
+
|
|
105
|
+
const devSettings: DevSettings = {
|
|
106
|
+
server: "mongoServer",
|
|
107
|
+
dbEndpoint: STORY_API_ENDPOINT,
|
|
108
|
+
imageEndpoint: "",
|
|
109
|
+
uploadEndpoint: "",
|
|
110
|
+
dbAPIToken: "",
|
|
111
|
+
dbUserToken: "",
|
|
112
|
+
schemaEndpoint: "",
|
|
113
|
+
bucketName: "",
|
|
114
|
+
organisationMembers: [],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const RelatedRows = () => {
|
|
118
|
+
const mount = useStoryBackend({
|
|
119
|
+
collections: {
|
|
120
|
+
[USER_COLLECTION]: users,
|
|
121
|
+
[REFERRAL_COLLECTION]: referrals,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
const [filters, setFilters] = useState<FlexFilterData[]>([
|
|
125
|
+
{ id: "init", columnId: "", operator: "=", value: undefined },
|
|
126
|
+
]);
|
|
127
|
+
const query = renderFlexFilterConditions(filters, userSchema, extras);
|
|
128
|
+
|
|
129
|
+
if (!mount) return <Loading />;
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className="flex flex-col space-y-12">
|
|
133
|
+
<StorySection
|
|
134
|
+
title="Referrers filtered on their referrals"
|
|
135
|
+
description="User 1 has no referrals this month and user 8 has seven; last month it is the other way round. Try Referrals › Referral Date between the 1st and the end of this month, with Referrals › Count Greater Than 3."
|
|
136
|
+
>
|
|
137
|
+
<DataTemplateWrapper
|
|
138
|
+
title="Referrers"
|
|
139
|
+
type="list"
|
|
140
|
+
collectionId={USER_COLLECTION}
|
|
141
|
+
schema={userSchema}
|
|
142
|
+
workspace={storyWorkspace()}
|
|
143
|
+
limit={20}
|
|
144
|
+
organise={{ breadcrumbType: "hidden" }}
|
|
145
|
+
bareSettings={{
|
|
146
|
+
bareNewDTW: true,
|
|
147
|
+
bareFlexFilters: true,
|
|
148
|
+
bareFlexFilterRelated: related,
|
|
149
|
+
}}
|
|
150
|
+
/>
|
|
151
|
+
</StorySection>
|
|
152
|
+
|
|
153
|
+
<StorySection
|
|
154
|
+
title="Standalone filter (notification audience)"
|
|
155
|
+
description="The same related datasheet on the bare FlexFilter, with the query it builds."
|
|
156
|
+
>
|
|
157
|
+
<div className="flex flex-col">
|
|
158
|
+
<div className="max-w-[700px]">
|
|
159
|
+
<FlexFilter
|
|
160
|
+
structure={userSchema}
|
|
161
|
+
extras={extras}
|
|
162
|
+
filters={filters}
|
|
163
|
+
onChange={setFilters}
|
|
164
|
+
devSettings={devSettings}
|
|
165
|
+
dbModule={axios}
|
|
166
|
+
variant="bare"
|
|
167
|
+
lib={{ Transition }}
|
|
168
|
+
/>
|
|
169
|
+
</div>
|
|
170
|
+
<div className="mt-4 p-4 bg-gray-50 rounded-lg border">
|
|
171
|
+
<p className="text-sm font-semibold mb-2">
|
|
172
|
+
Generated Query Conditions ({query.length})
|
|
173
|
+
</p>
|
|
174
|
+
<pre className="text-xs whitespace-pre-wrap">
|
|
175
|
+
{JSON.stringify(query, null, 2)}
|
|
176
|
+
</pre>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</StorySection>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
@@ -103,5 +103,5 @@ Two behaviours differ in controlled mode:
|
|
|
103
103
|
| `desc` | `string` | - | The description text displayed below the title |
|
|
104
104
|
| `anchor` | `"bottom" \| "right" \| "responsive"` | `right` | `responsive` uses a right panel above the `sm` breakpoint (640px) and a bottom sheet below it, where a right panel covers almost the whole viewport |
|
|
105
105
|
| `maxWidth` | `"lg" \| "xl" \| "2xl" \| "3xl" \| "4xl" \| "5xl" \| "6xl" \| "7xl"` | `lg` | Maximum width of the drawer (right anchor only) |
|
|
106
|
-
| `maxHeight` | `"50" \| "75" \| "100"` | `100` | Height of the drawer as a viewport height percentage (bottom anchor only) |
|
|
106
|
+
| `maxHeight` | `"50" \| "75" \| "90" \| "100"` | `100` | Height of the drawer as a viewport height percentage (bottom anchor only) |
|
|
107
107
|
| `expandable` | `boolean` | - | Shows a maximize toggle in the header that grows the panel to the full viewport (width on the right anchor, height on the bottom sheet) and back. Expansion resets when the drawer closes |
|
|
@@ -50,34 +50,34 @@ const ref = useRef(null);
|
|
|
50
50
|
|
|
51
51
|
### Optional props
|
|
52
52
|
|
|
53
|
-
| Name | Type | Default | Description
|
|
54
|
-
| ----------------------- | ------------------ | ---------- |
|
|
55
|
-
| `id` | `string` | — | HTML id attribute
|
|
56
|
-
| `subLabel` | `string` | — | Secondary label shown below the main label
|
|
53
|
+
| Name | Type | Default | Description |
|
|
54
|
+
| ----------------------- | ------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
55
|
+
| `id` | `string` | — | HTML id attribute |
|
|
56
|
+
| `subLabel` | `string` | — | Secondary label shown below the main label |
|
|
57
57
|
| `size` | `string` | — | `"small"` (px-3 py-2), `"mini"` (p-2) or `"xs"`; default is standard (px-5 py-3). `"xs"` looks 12px everywhere: on phones it keeps 16px text and draws the field at 75%, so iOS Safari does not zoom on focus; from `sm` up it is a plain 12px field. Give it a width through `containerClassName` when it sits in a row |
|
|
58
|
-
| `enterKeyHint` | `string` | — | Label for a phone keyboard's enter key, e.g. `"done"`, `"search"`, `"go"`
|
|
59
|
-
| `onKeyDown` | `(e) => void` | — | Passed to the `<input>` — e.g. Enter to commit, Escape to cancel
|
|
60
|
-
| `labelExtra` | `ReactElement` | — | Extra element rendered inline with the label
|
|
61
|
-
| `infoElement` | `ReactElement` | — | Content shown in an InfoWindow tooltip next to the label
|
|
62
|
-
| `customElement` | `ReactElement` | — | Custom element rendered to the right of the label row
|
|
63
|
-
| `pattern` | `string` | — | Regex string applied on submit (overrides type-based validation)
|
|
64
|
-
| `customErrMsg` | `string` | — | Overrides the default validation error message
|
|
65
|
-
| `minNumber` | `number` | — | Minimum value (type `"number"` only)
|
|
66
|
-
| `maxNumber` | `number` | — | Maximum value (type `"number"` only)
|
|
67
|
-
| `maxLength` | `number` | — | Max character length
|
|
68
|
-
| `optional` | `boolean` | `false` | Skips validation when `true`
|
|
69
|
-
| `disabled` | `boolean` | `false` | Disables the input
|
|
70
|
-
| `autoFocus` | `boolean` | `false` | Focuses the input on mount
|
|
71
|
-
| `autoComplete` | `"on"` \| `"off"` | — | HTML autocomplete attribute
|
|
72
|
-
| `capitalize` | `boolean` | `false` | Converts all input to uppercase (type `"text"` only)
|
|
73
|
-
| `showMagnifyIcon` | `boolean` | `false` | Shows a magnifying glass icon on the right
|
|
74
|
-
| `strSuggestions` | `string[]` | — | List of suggestions shown in a dropdown as the user types
|
|
75
|
-
| `mobilePicker` | `boolean` | `false` | Shows a country flag/code selector (type `"mobile"` only)
|
|
76
|
-
| `mobileCountryEditable` | `boolean` | `false` | Allows changing the country code (type `"mobile"` + `mobilePicker` only)
|
|
77
|
-
| `mobileWhitelist` | `("BN" \| "MY")[]` | — | Restricts mobile validation to specific countries
|
|
78
|
-
| `mobileView` | `boolean` | `false` | Renders a compact inline layout for mobile screens (label left, input right)
|
|
79
|
-
| `darkMode` | `boolean` | `false` | Applies dark colour scheme to labels
|
|
80
|
-
| `containerClassName` | `tailwind class` | — | Class applied to the outer container
|
|
81
|
-
| `inputClassName` | `tailwind class` | — | Additional class applied to the `<input>` element
|
|
82
|
-
| `onChange` | `(e: any) => void` | `() => {}` | Called on every change; `e.target.value` contains the new value
|
|
83
|
-
| `cellInput` | `CellInput` | — | Refer to Input Utils — enables cell/table editing mode
|
|
58
|
+
| `enterKeyHint` | `string` | — | Label for a phone keyboard's enter key, e.g. `"done"`, `"search"`, `"go"` |
|
|
59
|
+
| `onKeyDown` | `(e) => void` | — | Passed to the `<input>` — e.g. Enter to commit, Escape to cancel |
|
|
60
|
+
| `labelExtra` | `ReactElement` | — | Extra element rendered inline with the label |
|
|
61
|
+
| `infoElement` | `ReactElement` | — | Content shown in an InfoWindow tooltip next to the label |
|
|
62
|
+
| `customElement` | `ReactElement` | — | Custom element rendered to the right of the label row |
|
|
63
|
+
| `pattern` | `string` | — | Regex string applied on submit (overrides type-based validation) |
|
|
64
|
+
| `customErrMsg` | `string` | — | Overrides the default validation error message |
|
|
65
|
+
| `minNumber` | `number` | — | Minimum value (type `"number"` only) |
|
|
66
|
+
| `maxNumber` | `number` | — | Maximum value (type `"number"` only) |
|
|
67
|
+
| `maxLength` | `number` | — | Max character length |
|
|
68
|
+
| `optional` | `boolean` | `false` | Skips validation when `true` |
|
|
69
|
+
| `disabled` | `boolean` | `false` | Disables the input |
|
|
70
|
+
| `autoFocus` | `boolean` | `false` | Focuses the input on mount |
|
|
71
|
+
| `autoComplete` | `"on"` \| `"off"` | — | HTML autocomplete attribute |
|
|
72
|
+
| `capitalize` | `boolean` | `false` | Converts all input to uppercase (type `"text"` only) |
|
|
73
|
+
| `showMagnifyIcon` | `boolean` | `false` | Shows a magnifying glass icon on the right |
|
|
74
|
+
| `strSuggestions` | `string[]` | — | List of suggestions shown in a dropdown as the user types |
|
|
75
|
+
| `mobilePicker` | `boolean` | `false` | Shows a country flag/code selector (type `"mobile"` only) |
|
|
76
|
+
| `mobileCountryEditable` | `boolean` | `false` | Allows changing the country code (type `"mobile"` + `mobilePicker` only) |
|
|
77
|
+
| `mobileWhitelist` | `("BN" \| "MY")[]` | — | Restricts mobile validation to specific countries |
|
|
78
|
+
| `mobileView` | `boolean` | `false` | Renders a compact inline layout for mobile screens (label left, input right) |
|
|
79
|
+
| `darkMode` | `boolean` | `false` | Applies dark colour scheme to labels |
|
|
80
|
+
| `containerClassName` | `tailwind class` | — | Class applied to the outer container |
|
|
81
|
+
| `inputClassName` | `tailwind class` | — | Additional class applied to the `<input>` element |
|
|
82
|
+
| `onChange` | `(e: any) => void` | `() => {}` | Called on every change; `e.target.value` contains the new value |
|
|
83
|
+
| `cellInput` | `CellInput` | — | Refer to Input Utils — enables cell/table editing mode |
|
|
@@ -23,6 +23,8 @@ export const TextInput = () => {
|
|
|
23
23
|
const [suggestText, setSuggestText] = useState("");
|
|
24
24
|
const [regexText, setRegexText] = useState("");
|
|
25
25
|
const [barcodeText, setBarcodeText] = useState("");
|
|
26
|
+
const [serialText, setSerialText] = useState("");
|
|
27
|
+
const [slashSerialText, setSlashSerialText] = useState("");
|
|
26
28
|
|
|
27
29
|
return (
|
|
28
30
|
<>
|
|
@@ -241,6 +243,34 @@ export const TextInput = () => {
|
|
|
241
243
|
/>
|
|
242
244
|
</div>
|
|
243
245
|
</StorySection>
|
|
246
|
+
<StorySection
|
|
247
|
+
title="Barcode Scan (append)"
|
|
248
|
+
description="barcodeMode='append' makes a scan add to the serials already in the box and skips one that is already there. A value uses one separator, picked from the value: the first one in barcodeSeparators that it contains, else the first listed. With [',', '/'] a series starts comma separated (A, B); typing a '/' after the first serial makes later scans slash separated (A/B/C); a value with a comma keeps any '/' as part of a serial. Typing still works."
|
|
249
|
+
>
|
|
250
|
+
<div className="w-full max-w-sm space-y-6">
|
|
251
|
+
<TextInputImpl
|
|
252
|
+
id="batterySerial"
|
|
253
|
+
value={serialText}
|
|
254
|
+
type="text"
|
|
255
|
+
label="Battery Serial Number (commas only, the default)"
|
|
256
|
+
placeholder="Scan each battery label"
|
|
257
|
+
barcodeEnabled
|
|
258
|
+
barcodeMode="append"
|
|
259
|
+
onChange={(e) => setSerialText(e.target.value)}
|
|
260
|
+
/>
|
|
261
|
+
<TextInputImpl
|
|
262
|
+
id="batterySerialSlash"
|
|
263
|
+
value={slashSerialText}
|
|
264
|
+
type="text"
|
|
265
|
+
label="Battery Serial Number (comma, or slash if typed)"
|
|
266
|
+
placeholder="Scan, or type A/ then scan"
|
|
267
|
+
barcodeEnabled
|
|
268
|
+
barcodeMode="append"
|
|
269
|
+
barcodeSeparators={[",", "/"]}
|
|
270
|
+
onChange={(e) => setSlashSerialText(e.target.value)}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
</StorySection>
|
|
244
274
|
<StorySection
|
|
245
275
|
title="Small & Mini sizes"
|
|
246
276
|
description="size='small' reduces padding to px-3 py-2. size='mini' reduces further to p-2."
|
|
@@ -160,6 +160,7 @@ Below `lg` the rail moves into the mobile drawer as a row of icons above the sid
|
|
|
160
160
|
| `renderDashboardFooterContent` | `() => ReactNode` | — | Renders a slot at the bottom of the sidebar, above a border separator |
|
|
161
161
|
| `dashboardFooterClassName` | `string` | — | Tailwind classes applied to the footer slot container |
|
|
162
162
|
| `dashboardContentClassName` | `string` | — | Tailwind classes applied to the main content `<main>` element |
|
|
163
|
+
| `dashboardHeaderHeightClassName` | `string` | `h-16` | Tailwind height classes for the top header bar |
|
|
163
164
|
| `linkType` | `"react" \| "gatsby" \| "next" \| "external" \| "default"` | — | Router adapter passed through to nav links |
|
|
164
165
|
| `custom` | `any` | — | Custom link component used when `linkType` is `"react"`, `"gatsby"`, or `"next"` |
|
|
165
166
|
| `compactMode` | `boolean` | `false` | Tighter rows, smaller icons and a smaller expand chevron, keeping every label |
|