blixify-ui-web-mcp 0.1.20 → 0.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/auth/signUp/signUp.stories.tsx +39 -1
- package/lib/components/data/dataTemplate/README.md +100 -11
- package/lib/components/data/dataTemplate/drive.stories.tsx +197 -0
- package/lib/components/data/dataWrapper/dataWrapper.edit.stories.tsx +200 -0
- package/lib/components/display/newCalendar/README.md +0 -6
- package/lib/components/display/newFeeds/README.md +1 -5
- package/lib/components/display/newKanban/README.md +0 -7
- package/lib/components/display/newList/README.md +1 -4
- package/lib/components/display/newTable/README.md +29 -9
- package/lib/components/display/newTable/newTable.stories.tsx +86 -0
- package/lib/components/display/tag/README.md +1 -1
- package/lib/components/display/tag/tag.stories.tsx +7 -0
- package/lib/components/input/uploadInput/uploadInput.stories.tsx +41 -0
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ export const SignUp = () => {
|
|
|
36
36
|
const [signUpType, setSignUpType] =
|
|
37
37
|
useState<typeof AvailableProvider | "">("");
|
|
38
38
|
const [stepIndex, setStepIndex] = useState(0);
|
|
39
|
+
const [phone, setPhone] = useState("");
|
|
39
40
|
|
|
40
41
|
const handleChangeProvider = (provider: typeof AvailableProvider | "") => {
|
|
41
42
|
setSignUpType(provider);
|
|
@@ -75,10 +76,11 @@ export const SignUp = () => {
|
|
|
75
76
|
<TextInput
|
|
76
77
|
ref={input2}
|
|
77
78
|
id="phoneNumber"
|
|
78
|
-
value=
|
|
79
|
+
value={phone}
|
|
79
80
|
type="mobile"
|
|
80
81
|
label="Mobile"
|
|
81
82
|
placeholder="+60123456789"
|
|
83
|
+
onChange={(e) => setPhone(e.target.value)}
|
|
82
84
|
/>,
|
|
83
85
|
<Select
|
|
84
86
|
value="M"
|
|
@@ -120,6 +122,21 @@ export const SignUp = () => {
|
|
|
120
122
|
],
|
|
121
123
|
};
|
|
122
124
|
|
|
125
|
+
// INFO: with the country picker on, the code comes from the selector, so the
|
|
126
|
+
// number field only asks for the local part; the other sections keep the full
|
|
127
|
+
// +60 format a plain mobile field expects
|
|
128
|
+
const pickerFormList = {
|
|
129
|
+
...formList,
|
|
130
|
+
steps: formList.steps.map((eachStep) => ({
|
|
131
|
+
...eachStep,
|
|
132
|
+
components: eachStep.components.map((eachComponent) =>
|
|
133
|
+
eachComponent.props?.id === "phoneNumber"
|
|
134
|
+
? React.cloneElement(eachComponent, { placeholder: "123456789" })
|
|
135
|
+
: eachComponent
|
|
136
|
+
),
|
|
137
|
+
})),
|
|
138
|
+
};
|
|
139
|
+
|
|
123
140
|
return (
|
|
124
141
|
<div className="font-sans max-w-4xl p-6">
|
|
125
142
|
<StorySection
|
|
@@ -150,6 +167,27 @@ export const SignUp = () => {
|
|
|
150
167
|
</div>
|
|
151
168
|
</StorySection>
|
|
152
169
|
|
|
170
|
+
<StorySection
|
|
171
|
+
title="Phone Country Picker"
|
|
172
|
+
description="Enable mobilePicker and mobileCountryEditable on SignUpTemplate to apply an editable country selector to mobile TextInput fields."
|
|
173
|
+
>
|
|
174
|
+
<div className="w-full bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden">
|
|
175
|
+
<SignUpTemplate
|
|
176
|
+
type="phone"
|
|
177
|
+
stepIndex={0}
|
|
178
|
+
leftComponents={leftComponents}
|
|
179
|
+
leftClassName="bg-gray-50"
|
|
180
|
+
handleUpdateStepIndex={setStepIndex}
|
|
181
|
+
onChangeProvider={handleChangeProvider}
|
|
182
|
+
provider={["phone"]}
|
|
183
|
+
defaultProvider="phone"
|
|
184
|
+
formList={pickerFormList}
|
|
185
|
+
mobilePicker={true}
|
|
186
|
+
mobileCountryEditable={true}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
</StorySection>
|
|
190
|
+
|
|
153
191
|
<StorySection
|
|
154
192
|
title="Colored Left Panel"
|
|
155
193
|
description={
|
|
@@ -349,7 +349,9 @@ No `edit` prop — hides all create/update/delete controls. Users can browse and
|
|
|
349
349
|
|
|
350
350
|
### Full CRUD
|
|
351
351
|
|
|
352
|
-
Enable all editing features. `allEditable` turns on create
|
|
352
|
+
Enable all editing features. `allEditable` turns on create and update; delete is
|
|
353
|
+
granted separately by `deleteBtn`, and inline cell editing by `listEditable` —
|
|
354
|
+
see the Write permission matrix below.
|
|
353
355
|
|
|
354
356
|
```tsx
|
|
355
357
|
<DataTemplate
|
|
@@ -561,6 +563,58 @@ Set `viewType="kanban"` on an `extendedTabs` entry and configure `kanbanSettings
|
|
|
561
563
|
|
|
562
564
|
---
|
|
563
565
|
|
|
566
|
+
### Drive view
|
|
567
|
+
|
|
568
|
+
A Google Drive-style folder browser. Every row in the collection is a file or a folder, so a folder is a row, and moving something only changes one column. Set `viewType="drive"` on an `extendedTabs` entry and name the four columns in `driveSettings`.
|
|
569
|
+
|
|
570
|
+
- **Browse:** the open folder's rows load as one unpaged list, with folders first and then files, sorted A–Z. A path above the list goes back up. While the DTW search has text, the view searches the whole drive and shows each result's folder.
|
|
571
|
+
- **Folder id:** pass `organise.folderId` and `organise.onChangeFolder` to keep the open folder in the url. Without them the view keeps its own state.
|
|
572
|
+
- **Actions:** New folder, and Upload (one row per file, stored in that row's own storage folder). Files dragged over the drive show a "Drop to upload to <folder>" overlay, and a drop starts uploading straight away. The record Create button is hidden on a drive tab. Each item's menu has Details (the record's read view), Rename, Move to… and Delete. Deleting a folder deletes everything inside it, rows and storage.
|
|
573
|
+
- **Permissions:** `allEditable` or `createBtn` shows New folder and Upload. `canUpdateRecord` or `canInlineUpdate` allows Rename and Move. `deleteBtn` allows Delete. Without them the drive can be browsed but not changed.
|
|
574
|
+
- **Layout:** grid or list. The choice is remembered per browser, and phones start in the list layout.
|
|
575
|
+
|
|
576
|
+
```tsx
|
|
577
|
+
<DataTemplate
|
|
578
|
+
title="Documents"
|
|
579
|
+
description=""
|
|
580
|
+
type="list"
|
|
581
|
+
collectionId="documents"
|
|
582
|
+
model={[
|
|
583
|
+
{ id: "name", name: "Name", type: "string" },
|
|
584
|
+
{ id: "kind", name: "Kind", type: "list", listSelection: ["folder", "file"] },
|
|
585
|
+
{ id: "parentId", name: "Folder", type: "string" },
|
|
586
|
+
{ id: "file", name: "File", type: "multipleFile" },
|
|
587
|
+
{ id: "size", name: "Size", type: "number" },
|
|
588
|
+
]}
|
|
589
|
+
localURLEndpoint=""
|
|
590
|
+
devSettings={devSettings}
|
|
591
|
+
lib={{ axios, Menu, Transition }}
|
|
592
|
+
edit={{ allEditable: true, listEditable: true, deleteBtn: true }}
|
|
593
|
+
organise={{
|
|
594
|
+
handleNavigate,
|
|
595
|
+
folderId,
|
|
596
|
+
onChangeFolder: setFolderId,
|
|
597
|
+
extendedTabs: [
|
|
598
|
+
{
|
|
599
|
+
name: "Drive",
|
|
600
|
+
type: "list",
|
|
601
|
+
viewType: "drive",
|
|
602
|
+
defaultTab: true,
|
|
603
|
+
driveSettings: {
|
|
604
|
+
nameColumnId: "name",
|
|
605
|
+
kindColumnId: "kind",
|
|
606
|
+
parentColumnId: "parentId",
|
|
607
|
+
fileColumnId: "file",
|
|
608
|
+
sizeColumnId: "size",
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
}}
|
|
613
|
+
/>
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
564
618
|
### With search and filter
|
|
565
619
|
|
|
566
620
|
Use `organise.additionalQuery` to add filter controls above the list. Each entry maps to a filter UI control.
|
|
@@ -858,9 +912,9 @@ const devSettings = parseWorkspaceSettings(
|
|
|
858
912
|
|
|
859
913
|
| Name | Type | Description |
|
|
860
914
|
| ----------------------- | ------------- | -------------------------------------------------------- |
|
|
861
|
-
| `allEditable` | `boolean` | Enables add and update, and gates
|
|
915
|
+
| `allEditable` | `boolean` | Enables add and update, and gates card drag and batch writes |
|
|
862
916
|
| `listAddable` | `boolean` | Enables adding rows inline in list view |
|
|
863
|
-
| `listEditable` | `boolean` | Enables inline cell editing in list view
|
|
917
|
+
| `listEditable` | `boolean` | Enables inline cell editing in list view, and gates its write |
|
|
864
918
|
| `createBtn` | `boolean` | Shows a create button even when `allEditable` is `false` |
|
|
865
919
|
| `deleteBtn` | `boolean` | Enables delete and batch delete, independent of `allEditable` |
|
|
866
920
|
| `disabledUpdate` | `boolean` | Disables update and batch update |
|
|
@@ -871,15 +925,19 @@ const devSettings = parseWorkspaceSettings(
|
|
|
871
925
|
|
|
872
926
|
#### Write permission matrix
|
|
873
927
|
|
|
874
|
-
|
|
875
|
-
|
|
928
|
+
These rules decide whether a control is **rendered**. The inline cell, Kanban
|
|
929
|
+
drag, Kanban checkbox and batch callbacks re-check them before writing, so a
|
|
930
|
+
stray callback cannot write. Delete is render-gated only, because the delete
|
|
931
|
+
modal is also opened through the imperative `handleDeleteModal`.
|
|
876
932
|
|
|
877
933
|
| Control | Requires |
|
|
878
934
|
| -------------------------------------------------- | --------------------------------------- |
|
|
879
935
|
| Kanban drag/drop and its update write | `allEditable && !disabledUpdate` |
|
|
880
|
-
| Inline cell edit
|
|
881
|
-
|
|
|
882
|
-
|
|
|
936
|
+
| Inline cell edit and its write | `listEditable && !disabledUpdate` |
|
|
937
|
+
| Kanban boolean checkbox and its write | `listEditable && !disabledUpdate` |
|
|
938
|
+
| Read-view Update button | `allEditable && !disabledUpdate` |
|
|
939
|
+
| List Update column | `allEditable && !disabledUpdate`, or `bareListMultipleSelectAddtionalActions` |
|
|
940
|
+
| Batch update, clone, change-to-draft | `allEditable && !disabledUpdate && !disableBatchUpdate` (the batch Update button also requires a non-Firebase backend) |
|
|
883
941
|
| Delete button, batch delete, map delete | `deleteBtn` (independent of `allEditable`) |
|
|
884
942
|
| Map polygon select | batch-update or delete permission |
|
|
885
943
|
|
|
@@ -892,8 +950,15 @@ The imperative API is **not** gated by these rules. `handleFormSubmit`,
|
|
|
892
950
|
`handleFormSaveDraft`, `handleDeleteModal` and `handleUploadOfflineData` are only
|
|
893
951
|
reachable from consumer code, and `DataTemplateWrapper` defaults `edit` to
|
|
894
952
|
`disabledUpdate: true, deleteBtn: false` — gating them would break every consumer
|
|
895
|
-
that drives its own form without passing an `edit` prop.
|
|
896
|
-
|
|
953
|
+
that drives its own form without passing an `edit` prop. Neither is the shared
|
|
954
|
+
write path (`handleSubmitData`), which also serves the auth modal writing a
|
|
955
|
+
changed email back to the profile record. A consumer that does not want a write
|
|
956
|
+
path should not render it.
|
|
957
|
+
|
|
958
|
+
> Reading these rules through `DataTemplateWrapper`: it spreads your `edit` over
|
|
959
|
+
> its own defaults, so `edit={{ allEditable: true }}` still inherits
|
|
960
|
+
> `disabledUpdate: true` and yields no update affordances. Pass
|
|
961
|
+
> `disabledUpdate: false` explicitly to enable update, drag and batch controls.
|
|
897
962
|
|
|
898
963
|
#### Inline editability matrix (`listEditable`)
|
|
899
964
|
|
|
@@ -970,7 +1035,8 @@ Which field types can be edited from the list table, and how. Types not listed u
|
|
|
970
1035
|
| `fixedPagination` | `boolean` | Fixes the pagination bar to the bottom of the viewport |
|
|
971
1036
|
| `filterComponentNewRow` | `boolean` | Renders each filter component on its own row |
|
|
972
1037
|
| `addColumnEnabled` | `boolean` | Shows an add-column button in the table header |
|
|
973
|
-
| `persistViewState` | `"
|
|
1038
|
+
| `persistViewState` | `"persistent" \| "session"` | Remembers the list's sort, filters, hidden columns, page and search 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`. List view only |
|
|
1039
|
+
| `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` |
|
|
974
1040
|
| `groupBy` | `string` | Column ID to group list results by |
|
|
975
1041
|
| `groupIdCollapsible` | `{ [key: string]: boolean }` | Initial collapsed/expanded state per group ID |
|
|
976
1042
|
| `sortGroupByAlphabetically` | `boolean` | Sorts group-by values alphabetically |
|
|
@@ -1083,6 +1149,8 @@ Injects custom React components and callbacks into the list, read, and update vi
|
|
|
1083
1149
|
| `bareListTablePinColumns` | `{ left?: string[]; right?: string[] }` | Columns kept sticky at the edges while the table scrolls sideways (`"update"` is the actions column); new table only |
|
|
1084
1150
|
| `bareListMobileView` | `boolean` | Phone card list instead of the table (the app passes its own isMobile) |
|
|
1085
1151
|
| `bareListMobileFloatingSearch` | `boolean` | Floating search bar at the bottom on phones; the list reserves room under its last row for it |
|
|
1152
|
+
| `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 |
|
|
1153
|
+
| `bareListMobileTooltip` | `NewListMobileTooltip` | Passed to NewList when `bareNewDTW` is on: `imageKey`, `summaryKeys`, `tagKeys` and `tagColors` for the hover card on list items |
|
|
1086
1154
|
| `bareListMaxSelectedCount` | `number` | Caps how many rows the checkbox column may select at once |
|
|
1087
1155
|
| `bareUpdateHandleData` | `(data: any, changedAttributeIds?: string[]) => void` | Called on every form field change with the current data and changed field IDs |
|
|
1088
1156
|
| `bareUpdateHandlePreComplete` | `(data: any) => Promise<boolean>` | Called before save — return `false` to cancel |
|
|
@@ -1168,6 +1236,16 @@ calendarSettings: {
|
|
|
1168
1236
|
}
|
|
1169
1237
|
```
|
|
1170
1238
|
|
|
1239
|
+
### Field visibility (`hideRead`)
|
|
1240
|
+
|
|
1241
|
+
`hideRead: true` on a DataStructure field:
|
|
1242
|
+
|
|
1243
|
+
- leaves the field out of the read view's field list;
|
|
1244
|
+
- on a `listObject`, `ganttchart` or `canvas` field, hides the whole field from the read view;
|
|
1245
|
+
- on a column inside a `listObject` (`objectData`), leaves that column out of the list-object table, in both the read and the update view. The row's edit popup still shows it, and the stored value is not changed.
|
|
1246
|
+
|
|
1247
|
+
*Pointers:* `src/components/data/dataTemplate/index.tsx`, `renderEachListObject` in `src/components/data/updateModule.tsx`
|
|
1248
|
+
|
|
1171
1249
|
### List / MultipleList Field Settings
|
|
1172
1250
|
|
|
1173
1251
|
Properties specific to `type: "list"` and `type: "multipleList"` fields in the DataStructure schema.
|
|
@@ -1224,6 +1302,17 @@ Under `bareNewDTW` the feeds tab renders `NewFeeds` (timeline card that matches
|
|
|
1224
1302
|
| `useColumnName` | `boolean` | Shows column name instead of column ID on cards |
|
|
1225
1303
|
| `query` | `Query[]` | Additional queries to filter and organise kanban cards |
|
|
1226
1304
|
|
|
1305
|
+
### Drive Settings
|
|
1306
|
+
|
|
1307
|
+
| Name | Type | Description |
|
|
1308
|
+
| ------------------ | -------- | ------------------------------------------------------------------------------------------------------------ |
|
|
1309
|
+
| `nameColumnId` | `string` | A string column holding the file or folder name |
|
|
1310
|
+
| `kindColumnId` | `string` | A list column whose value is `folder` or `file` |
|
|
1311
|
+
| `parentColumnId` | `string` | The containing folder's row id. Missing, null or `""` means the top level |
|
|
1312
|
+
| `fileColumnId` | `string` | A `multipleFile` column holding the one uploaded file as `{ token, fileName }`, so the original name is kept |
|
|
1313
|
+
| `sizeColumnId` | `string` | Optional. A number column written with the file size in bytes on upload |
|
|
1314
|
+
| `mimeTypeColumnId` | `string` | Optional. A string column written with the file type on upload |
|
|
1315
|
+
|
|
1227
1316
|
### Map Settings
|
|
1228
1317
|
|
|
1229
1318
|
| Name | Type | Description |
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// INFO: the drive view over an in-memory collection. The story mocks its own
|
|
2
|
+
// API, so it opens without a server and the e2e spec drives it as it is.
|
|
3
|
+
|
|
4
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
5
|
+
import MockAdapter from "axios-mock-adapter";
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import { DataTemplateWrapper } from "../dataWrapper";
|
|
8
|
+
import { DataStructure } from "./model";
|
|
9
|
+
import { ExtendedTabs } from "./spaceModel";
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Data/Data Template/Drive",
|
|
13
|
+
decorators: [
|
|
14
|
+
(Story: () => React.ReactElement) => (
|
|
15
|
+
<div className="bg-gray-50 min-h-screen p-4">
|
|
16
|
+
<Story />
|
|
17
|
+
</div>
|
|
18
|
+
),
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
const DRIVE_ENDPOINT = "http://localhost:3000/api/data";
|
|
24
|
+
const DRIVE_UPLOAD = "http://localhost:3000/api/asset";
|
|
25
|
+
const DRIVE_COLLECTION = "demo-drive";
|
|
26
|
+
|
|
27
|
+
const DriveSchema: DataStructure[] = [
|
|
28
|
+
{ id: "name", name: "Name", type: "string" },
|
|
29
|
+
{ id: "kind", name: "Kind", type: "list", listSelection: ["folder", "file"] },
|
|
30
|
+
{ id: "parentId", name: "Folder", type: "string" },
|
|
31
|
+
{ id: "file", name: "File", type: "multipleFile" },
|
|
32
|
+
{ id: "size", name: "Size", type: "number" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const DriveTabs: ExtendedTabs[] = [
|
|
36
|
+
{
|
|
37
|
+
name: "Drive",
|
|
38
|
+
type: "list",
|
|
39
|
+
viewType: "drive",
|
|
40
|
+
defaultTab: true,
|
|
41
|
+
driveSettings: {
|
|
42
|
+
nameColumnId: "name",
|
|
43
|
+
kindColumnId: "kind",
|
|
44
|
+
parentColumnId: "parentId",
|
|
45
|
+
fileColumnId: "file",
|
|
46
|
+
sizeColumnId: "size",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const handleBuildDriveStore = () => [
|
|
52
|
+
{ _id: "f-contracts", name: "Contracts", kind: "folder", parentId: "" },
|
|
53
|
+
{ _id: "f-invoices", name: "Invoices", kind: "folder", parentId: "" },
|
|
54
|
+
{ _id: "f-2025", name: "2025", kind: "folder", parentId: "f-invoices" },
|
|
55
|
+
{
|
|
56
|
+
_id: "d-handbook",
|
|
57
|
+
name: "Team handbook.pdf",
|
|
58
|
+
kind: "file",
|
|
59
|
+
parentId: "",
|
|
60
|
+
size: 482000,
|
|
61
|
+
file: [{ token: "demo", fileName: "demo-handbook.pdf" }],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
_id: "d-inv-01",
|
|
65
|
+
name: "Invoice 01.pdf",
|
|
66
|
+
kind: "file",
|
|
67
|
+
parentId: "f-2025",
|
|
68
|
+
size: 91000,
|
|
69
|
+
file: [{ token: "demo", fileName: "demo-inv-01.pdf" }],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
_id: "d-nda",
|
|
73
|
+
name: "Supplier NDA.docx",
|
|
74
|
+
kind: "file",
|
|
75
|
+
parentId: "f-contracts",
|
|
76
|
+
size: 34000,
|
|
77
|
+
file: [{ token: "demo", fileName: "demo-nda.docx" }],
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const handleParseBody = (config: any) => {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(config?.data ?? "{}");
|
|
84
|
+
} catch (err) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// INFO: enough of the query language for a drive — equality, `in` with null
|
|
90
|
+
// matching a missing field the way Mongo does, and a name search
|
|
91
|
+
const handleMatchQuery = (row: any, query: any[]) =>
|
|
92
|
+
query.every((eachQuery) => {
|
|
93
|
+
const value = row[eachQuery.queryId];
|
|
94
|
+
if (eachQuery.type === "=") return value === eachQuery.value;
|
|
95
|
+
if (eachQuery.type === "in")
|
|
96
|
+
return (eachQuery.value ?? []).some((eachValue: any) =>
|
|
97
|
+
eachValue === null
|
|
98
|
+
? value === undefined || value === null
|
|
99
|
+
: value === eachValue,
|
|
100
|
+
);
|
|
101
|
+
if (eachQuery.type === "search")
|
|
102
|
+
return String(row.name ?? "")
|
|
103
|
+
.toLowerCase()
|
|
104
|
+
.includes(String(eachQuery.value ?? "").toLowerCase());
|
|
105
|
+
return true;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
export const Drive = () => {
|
|
109
|
+
const [folderId, setFolderId] = useState("");
|
|
110
|
+
const [mounted, setMounted] = useState(false);
|
|
111
|
+
const storeRef = useRef<any[]>(handleBuildDriveStore());
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
const store = storeRef.current;
|
|
115
|
+
const mock = new MockAdapter(axios, {
|
|
116
|
+
onNoMatch: "passthrough",
|
|
117
|
+
delayResponse: 120,
|
|
118
|
+
});
|
|
119
|
+
const base = `${DRIVE_ENDPOINT}/${DRIVE_COLLECTION}`;
|
|
120
|
+
|
|
121
|
+
mock.onPost(`${base}/list`).reply((config) => {
|
|
122
|
+
const body = handleParseBody(config);
|
|
123
|
+
const rows = store.filter((eachRow) =>
|
|
124
|
+
handleMatchQuery(eachRow, body.query ?? []),
|
|
125
|
+
);
|
|
126
|
+
return [200, { data: rows, count: rows.length }];
|
|
127
|
+
});
|
|
128
|
+
mock.onPost(`${base}/get`).reply((config) => {
|
|
129
|
+
const body = handleParseBody(config);
|
|
130
|
+
return [200, { data: store.find((row) => row._id === body.id) ?? null }];
|
|
131
|
+
});
|
|
132
|
+
mock.onPost(`${base}/create`).reply((config) => {
|
|
133
|
+
store.push(handleParseBody(config).data);
|
|
134
|
+
return [200, { success: true }];
|
|
135
|
+
});
|
|
136
|
+
mock.onPost(`${base}/update`).reply((config) => {
|
|
137
|
+
const body = handleParseBody(config);
|
|
138
|
+
const row = store.find((eachRow) => eachRow._id === body.id);
|
|
139
|
+
if (row) Object.assign(row, body.data);
|
|
140
|
+
return [200, { success: true }];
|
|
141
|
+
});
|
|
142
|
+
mock.onPost(`${base}/batchDelete`).reply((config) => {
|
|
143
|
+
const ids: string[] = handleParseBody(config).id ?? [];
|
|
144
|
+
for (let index = store.length - 1; index >= 0; index -= 1)
|
|
145
|
+
if (ids.includes(store[index]._id)) store.splice(index, 1);
|
|
146
|
+
return [200, { success: true }];
|
|
147
|
+
});
|
|
148
|
+
mock.onPost(`${DRIVE_UPLOAD}/upload`).reply(200, { data: "demo-token" });
|
|
149
|
+
mock
|
|
150
|
+
.onPost(`${DRIVE_UPLOAD}/batchDeleteDirectory`)
|
|
151
|
+
.reply(200, { success: true });
|
|
152
|
+
|
|
153
|
+
setMounted(true);
|
|
154
|
+
return () => mock.restore();
|
|
155
|
+
}, []);
|
|
156
|
+
|
|
157
|
+
if (!mounted) return null;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className="w-full">
|
|
161
|
+
<div className="font-semibold mb-1 text-gray-900">Drive</div>
|
|
162
|
+
<div className="text-sm font-light mb-4 text-gray-500">
|
|
163
|
+
A folder browser over a collection where every row is a file or a
|
|
164
|
+
folder. Open folders, use the path to go back, create, upload, rename,
|
|
165
|
+
move and delete. Deleting a folder takes everything inside it.
|
|
166
|
+
</div>
|
|
167
|
+
<DataTemplateWrapper
|
|
168
|
+
title="Documents"
|
|
169
|
+
type="list"
|
|
170
|
+
collectionId={DRIVE_COLLECTION}
|
|
171
|
+
schema={DriveSchema}
|
|
172
|
+
workspace={{
|
|
173
|
+
apiEndpointType: "mongoDB",
|
|
174
|
+
apiEndpoint: DRIVE_ENDPOINT,
|
|
175
|
+
apiEndpointToken: "",
|
|
176
|
+
apiUserToken: "",
|
|
177
|
+
assetEndpoint:
|
|
178
|
+
"https://firebasestorage.googleapis.com/v0/b/demo-bucket.firebasestorage.app/o/",
|
|
179
|
+
assetUploadEndpoint: DRIVE_UPLOAD,
|
|
180
|
+
}}
|
|
181
|
+
edit={{
|
|
182
|
+
allEditable: true,
|
|
183
|
+
listEditable: true,
|
|
184
|
+
disabledUpdate: false,
|
|
185
|
+
deleteBtn: true,
|
|
186
|
+
}}
|
|
187
|
+
bareSettings={{ bareNewDTW: true, bareCompactMode: true }}
|
|
188
|
+
organise={{
|
|
189
|
+
extendedTabs: DriveTabs,
|
|
190
|
+
folderId,
|
|
191
|
+
onChangeFolder: setFolderId,
|
|
192
|
+
}}
|
|
193
|
+
handleNavigate={() => {}}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
@@ -756,3 +756,203 @@ export const DTWListObjectTable = () => {
|
|
|
756
756
|
);
|
|
757
757
|
};
|
|
758
758
|
DTWListObjectTable.storyName = "DTW List Object Table";
|
|
759
|
+
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
761
|
+
// DTW Callback Counts — used by e2e/dtw-callbacks.spec.ts
|
|
762
|
+
// ---------------------------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
const CALLBACK_ENDPOINT = "https://callback-counts.blixify.invalid/api/data";
|
|
765
|
+
const CALLBACK_COLLECTION = "callback-records";
|
|
766
|
+
|
|
767
|
+
const CallbackCountsSchema = [
|
|
768
|
+
{ id: "name", name: "Name", type: "string" as const },
|
|
769
|
+
{ id: "notes", name: "Notes", type: "string" as const, optional: true },
|
|
770
|
+
];
|
|
771
|
+
|
|
772
|
+
type CallbackCounterKey =
|
|
773
|
+
| "list"
|
|
774
|
+
| "get"
|
|
775
|
+
| "create"
|
|
776
|
+
| "update"
|
|
777
|
+
| "delete"
|
|
778
|
+
| "preComplete"
|
|
779
|
+
| "postComplete"
|
|
780
|
+
| "navigate";
|
|
781
|
+
|
|
782
|
+
const CALLBACK_COUNTER_KEYS: CallbackCounterKey[] = [
|
|
783
|
+
"list",
|
|
784
|
+
"get",
|
|
785
|
+
"create",
|
|
786
|
+
"update",
|
|
787
|
+
"delete",
|
|
788
|
+
"preComplete",
|
|
789
|
+
"postComplete",
|
|
790
|
+
"navigate",
|
|
791
|
+
];
|
|
792
|
+
|
|
793
|
+
const handleInstallCallbackMock = (
|
|
794
|
+
store: any[],
|
|
795
|
+
onCount: (key: CallbackCounterKey) => void,
|
|
796
|
+
) => {
|
|
797
|
+
const mock = new MockAdapter(axios, {
|
|
798
|
+
onNoMatch: "passthrough",
|
|
799
|
+
delayResponse: 100,
|
|
800
|
+
});
|
|
801
|
+
const handleParseBody = (config: any) => {
|
|
802
|
+
try {
|
|
803
|
+
return JSON.parse(config?.data ?? "{}");
|
|
804
|
+
} catch (err) {
|
|
805
|
+
return {};
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
const renderUrl = (type: string) =>
|
|
809
|
+
`${CALLBACK_ENDPOINT}/${CALLBACK_COLLECTION}/${type}`;
|
|
810
|
+
mock.onPost(renderUrl("list")).reply(() => {
|
|
811
|
+
onCount("list");
|
|
812
|
+
return [
|
|
813
|
+
200,
|
|
814
|
+
{ data: store.map((row) => ({ ...row })), count: store.length },
|
|
815
|
+
];
|
|
816
|
+
});
|
|
817
|
+
mock
|
|
818
|
+
.onPost(renderUrl("count"))
|
|
819
|
+
.reply(() => [200, { data: store.length, count: store.length }]);
|
|
820
|
+
mock.onPost(renderUrl("get")).reply((config) => {
|
|
821
|
+
onCount("get");
|
|
822
|
+
const body = handleParseBody(config);
|
|
823
|
+
const row = store.find((eachRow) => eachRow._id === body.id);
|
|
824
|
+
return [200, { data: row ? { ...row } : null }];
|
|
825
|
+
});
|
|
826
|
+
mock.onPost(renderUrl("create")).reply((config) => {
|
|
827
|
+
onCount("create");
|
|
828
|
+
store.push({ ...(handleParseBody(config).data ?? {}) });
|
|
829
|
+
return [200, { success: true }];
|
|
830
|
+
});
|
|
831
|
+
mock.onPost(renderUrl("update")).reply((config) => {
|
|
832
|
+
onCount("update");
|
|
833
|
+
const body = handleParseBody(config);
|
|
834
|
+
const index = store.findIndex((eachRow) => eachRow._id === body.id);
|
|
835
|
+
if (index >= 0) store[index] = { ...store[index], ...(body.data ?? {}) };
|
|
836
|
+
return [200, { success: true }];
|
|
837
|
+
});
|
|
838
|
+
mock.onPost(renderUrl("delete")).reply((config) => {
|
|
839
|
+
onCount("delete");
|
|
840
|
+
const body = handleParseBody(config);
|
|
841
|
+
const index = store.findIndex((eachRow) => eachRow._id === body.id);
|
|
842
|
+
if (index >= 0) store.splice(index, 1);
|
|
843
|
+
return [200, { success: true }];
|
|
844
|
+
});
|
|
845
|
+
return mock;
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* INFO: a full CRUD page that counts every request and every save callback on
|
|
850
|
+
* screen, so e2e can assert that one user action fires each of them exactly
|
|
851
|
+
* once. A doubled callback reaches consumers as a doubled email or upload,
|
|
852
|
+
* while the page itself looks fine — the counters are the only visible signal.
|
|
853
|
+
*/
|
|
854
|
+
export const DTWCallbackCounts = () => {
|
|
855
|
+
// 1. State
|
|
856
|
+
const [mount, setMount] = useState<boolean>(false);
|
|
857
|
+
const [, setTick] = useState<number>(0);
|
|
858
|
+
const [viewType, setViewType] = useState<"list" | "read" | "update">("list");
|
|
859
|
+
const [selectedId, setSelectedId] = useState<string>("");
|
|
860
|
+
const [lastNavigate, setLastNavigate] = useState<string>("");
|
|
861
|
+
|
|
862
|
+
// 2. Refs
|
|
863
|
+
const storeRef = useRef<any[]>([
|
|
864
|
+
{ _id: "r1", name: "Record One", notes: "first" },
|
|
865
|
+
{ _id: "r2", name: "Record Two", notes: "second" },
|
|
866
|
+
]);
|
|
867
|
+
const countersRef = useRef<Record<CallbackCounterKey, number>>({
|
|
868
|
+
list: 0,
|
|
869
|
+
get: 0,
|
|
870
|
+
create: 0,
|
|
871
|
+
update: 0,
|
|
872
|
+
delete: 0,
|
|
873
|
+
preComplete: 0,
|
|
874
|
+
postComplete: 0,
|
|
875
|
+
navigate: 0,
|
|
876
|
+
});
|
|
877
|
+
const mockRef = useRef<any>(null);
|
|
878
|
+
|
|
879
|
+
// 4. Effects
|
|
880
|
+
useEffect(() => {
|
|
881
|
+
mockRef.current = handleInstallCallbackMock(storeRef.current, handleCount);
|
|
882
|
+
setMount(true);
|
|
883
|
+
return () => {
|
|
884
|
+
mockRef.current?.restore();
|
|
885
|
+
mockRef.current = null;
|
|
886
|
+
};
|
|
887
|
+
}, []);
|
|
888
|
+
|
|
889
|
+
// 5. Handlers
|
|
890
|
+
function handleCount(key: CallbackCounterKey) {
|
|
891
|
+
countersRef.current[key] += 1;
|
|
892
|
+
setTick((previous) => previous + 1);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const handleNavigate = (type: any, id: any) => {
|
|
896
|
+
handleCount("navigate");
|
|
897
|
+
setLastNavigate(`${type}:${id ?? ""}`);
|
|
898
|
+
setViewType(type);
|
|
899
|
+
if (id !== undefined) setSelectedId(id);
|
|
900
|
+
};
|
|
901
|
+
|
|
902
|
+
// 7. Return
|
|
903
|
+
if (!mount) return <Loading />;
|
|
904
|
+
return (
|
|
905
|
+
<div className="w-full">
|
|
906
|
+
<div className="font-semibold mb-1 text-gray-900">
|
|
907
|
+
DTW Callback Counts
|
|
908
|
+
</div>
|
|
909
|
+
<div className="text-sm font-light mb-4 text-gray-500">
|
|
910
|
+
Every request and save callback is counted below. One create, update or
|
|
911
|
+
delete should move each of its counters by exactly one.
|
|
912
|
+
</div>
|
|
913
|
+
<div className="flex flex-wrap gap-4 mb-4 text-xs text-gray-600 font-mono">
|
|
914
|
+
{CALLBACK_COUNTER_KEYS.map((eachKey) => (
|
|
915
|
+
<span key={eachKey}>
|
|
916
|
+
{eachKey}:{" "}
|
|
917
|
+
<span data-testid={`callback-count-${eachKey}`}>
|
|
918
|
+
{countersRef.current[eachKey]}
|
|
919
|
+
</span>
|
|
920
|
+
</span>
|
|
921
|
+
))}
|
|
922
|
+
<span>
|
|
923
|
+
last navigate:{" "}
|
|
924
|
+
<span data-testid="callback-last-navigate">{lastNavigate}</span>
|
|
925
|
+
</span>
|
|
926
|
+
</div>
|
|
927
|
+
<DataTemplateWrapper
|
|
928
|
+
title="Records"
|
|
929
|
+
type={viewType}
|
|
930
|
+
id={selectedId}
|
|
931
|
+
collectionId={CALLBACK_COLLECTION}
|
|
932
|
+
schema={CallbackCountsSchema}
|
|
933
|
+
workspace={{
|
|
934
|
+
apiEndpointType: "mongoDB",
|
|
935
|
+
apiEndpoint: CALLBACK_ENDPOINT,
|
|
936
|
+
apiEndpointToken: "",
|
|
937
|
+
apiUserToken: "",
|
|
938
|
+
assetEndpoint: "",
|
|
939
|
+
assetUploadEndpoint: "",
|
|
940
|
+
}}
|
|
941
|
+
edit={{ allEditable: true, disabledUpdate: false, deleteBtn: true }}
|
|
942
|
+
organise={{ breadcrumbType: "button" }}
|
|
943
|
+
bareSettings={{
|
|
944
|
+
bareNewDTW: true,
|
|
945
|
+
bareUpdateHandlePreComplete: async () => {
|
|
946
|
+
handleCount("preComplete");
|
|
947
|
+
return true;
|
|
948
|
+
},
|
|
949
|
+
bareUpdateHandlePostComplete: async () => {
|
|
950
|
+
handleCount("postComplete");
|
|
951
|
+
},
|
|
952
|
+
}}
|
|
953
|
+
handleNavigate={handleNavigate}
|
|
954
|
+
/>
|
|
955
|
+
</div>
|
|
956
|
+
);
|
|
957
|
+
};
|
|
958
|
+
DTWCallbackCounts.storyName = "DTW Callback Counts";
|
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Calendar surface with six variants. Mobile default should be **weekStrip + dayTimeline** (not month grid).
|
|
4
4
|
|
|
5
|
-
## Design reference
|
|
6
|
-
|
|
7
|
-
PR #1666 — `uiRefresh.stories.tsx`, sections **Calendar — Variant 1 through Variant 5** (~line 979 `MiniMonthCalendar`, ~line 1091 `MobileWeekStrip`, ~line 1136 `DayTimeline`, etc.).
|
|
8
|
-
|
|
9
|
-
Note: an earlier draft of `NewCalendar` lived at `src/components/display/newCalendar/index.tsx` (1,243 LOC) but was deleted by Sean's commit `bbd0117c` ("fix removed new stories and implemented redesign directly to components"). The full source is recoverable via `git show <sha>:src/components/display/newCalendar/index.tsx` and should be the starting point for this implementation — don't rewrite from scratch.
|
|
10
|
-
|
|
11
5
|
## Variants
|
|
12
6
|
|
|
13
7
|
| Variant | Pattern | Best for |
|
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Social-style activity feed. Composes `NewListRow` so the row geometry matches `NewList` — same avatar size, same trailing-time position, same border treatment. The Feed-specific bits (quoted content, inline media, reactions row) attach via `renderExtra`.
|
|
4
4
|
|
|
5
|
-
## Design
|
|
6
|
-
|
|
7
|
-
PR #1666 — `uiRefresh.stories.tsx`, section **"Feeds View — Modern Mobile (Social-style Activity)"** (~line 1676, `ModernFeedsView`).
|
|
8
|
-
|
|
9
|
-
Patterns to honour:
|
|
5
|
+
## Design patterns
|
|
10
6
|
|
|
11
7
|
- Avatar + actor name + relative time on the row header
|
|
12
8
|
- Quoted comment blocks with left-border accent
|
|
@@ -2,13 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Sectioned card board. Desktop: horizontal columns. Mobile: tab switcher between columns + swipe-card-to-move-column + FAB for primary CTA.
|
|
4
4
|
|
|
5
|
-
## Design reference
|
|
6
|
-
|
|
7
|
-
PR #1666 — `uiRefresh.stories.tsx`:
|
|
8
|
-
|
|
9
|
-
- **"Kanban — Desktop Column View"** (~line 650, `DesktopKanban`)
|
|
10
|
-
- **"Kanban — Mobile Tab View"** (~line 675, `MobileKanban`) — already includes the tab switcher, swipe hint, and FAB pattern. Lift those into the real component.
|
|
11
|
-
|
|
12
5
|
## Mobile interactions
|
|
13
6
|
|
|
14
7
|
- Tap a column tab to switch (no horizontal scroll)
|
|
@@ -6,11 +6,8 @@ Mobile-native list foundation. Other components (`NewFeeds`, `NewTable` mobile b
|
|
|
6
6
|
|
|
7
7
|
`Calendar` / `Table` / `Kanban` / `List` / `Feeds` already exist as legacy components. The `New*` prefix lets us ship the modernised foundation without breaking consumers. Migration plan TBD (likely v2.0.0 deprecation).
|
|
8
8
|
|
|
9
|
-
## Design
|
|
9
|
+
## Design patterns
|
|
10
10
|
|
|
11
|
-
PR #1666 — `uiRefresh.stories.tsx`, section **"List View — Modern Mobile (Sticky Section Headers)"** (~line 1561, `ModernListView`).
|
|
12
|
-
|
|
13
|
-
Key patterns to honour:
|
|
14
11
|
- Sticky section headers (TODAY / YESTERDAY / THIS WEEK) — stay pinned while scrolling
|
|
15
12
|
- Avatar + 2-line preview row (`NewListRow`)
|
|
16
13
|
- Unread state — bold sender + small blue dot
|
|
@@ -2,15 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
Tabular display with a List-shaped mobile breakpoint. The architectural key: **`mobileView=true` delegates row rendering to `NewListRow`**, so the same tap-rhythm and avatar/title/trailing geometry shows up across NewList, NewFeeds, and NewTable mobile.
|
|
4
4
|
|
|
5
|
-
## Design reference
|
|
6
|
-
|
|
7
|
-
PR #1666 — `uiRefresh.stories.tsx`:
|
|
8
|
-
|
|
9
|
-
- **"Table — Desktop (clean borderless)"** (~line 491, `DesktopTable`)
|
|
10
|
-
- **"Table — Mobile Card View"** (~line 552, `MobileCardTable`)
|
|
11
|
-
|
|
12
|
-
Note: the Mobile Card View mock in #1666 invented a bespoke avatar/name/status layout. That pattern is being replaced — the mobile rendering should use `NewListRow` (composition), not duplicate it.
|
|
13
|
-
|
|
14
5
|
## Mobile column flags
|
|
15
6
|
|
|
16
7
|
Columns expose three optional booleans that map them to the `NewListItem` shape:
|
|
@@ -23,6 +14,35 @@ Columns expose three optional booleans that map them to the `NewListItem` shape:
|
|
|
23
14
|
|
|
24
15
|
Columns flagged `hideOnMobile` are dropped from the mobile rendering entirely.
|
|
25
16
|
|
|
17
|
+
## Mobile tooltip (`mobileTooltip`)
|
|
18
|
+
|
|
19
|
+
With `mobileView`, `mobileTooltip` adds row data to the collapsed row and to a hover card. Through `DataTemplate` it is `bareSettings.bareMobileTooltip`, which applies when `bareNewDTW` and `bareListMobileView` are both on.
|
|
20
|
+
|
|
21
|
+
| Key | Type | Shows |
|
|
22
|
+
| --------------------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `titleAdjacent.tagKey` | `string` | That row field's value as a Tag next to the row title |
|
|
24
|
+
| `titleAdjacent.tagColor` | `Record<string, TagColor>` | The Tag colour for each value |
|
|
25
|
+
| `titleAdjacent.trailingKey` | `string` | That row field's value as trailing text, e.g. a price |
|
|
26
|
+
| `titleAdjacent.summaryKeys` | `string[]` | A second line under the title while the row is collapsed, as label + value pairs. Labels come from the column titles; a dotted key (`shippingAddress.name`) falls back to its parent column's title. Empty values are skipped. Tapping the line expands the row |
|
|
27
|
+
| `tooltip.imageKey` | `string` | The image URL field shown in the hover card |
|
|
28
|
+
| `tooltip.summaryKeys` | `string[]` | Label + value pairs in the hover card |
|
|
29
|
+
| `tooltip.tagKeys` | `string[]` | Tag pills in the hover card |
|
|
30
|
+
| `tooltip.tagColors` | `Record<string, TagColor>` | The Tag colour for each value |
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
<NewTable
|
|
34
|
+
mobileView
|
|
35
|
+
mobileTooltip={{
|
|
36
|
+
titleAdjacent: {
|
|
37
|
+
tagKey: "status",
|
|
38
|
+
tagColor: { Delivered: "green", Pending: "yellow" },
|
|
39
|
+
summaryKeys: ["shippingAddress.name"],
|
|
40
|
+
},
|
|
41
|
+
}}
|
|
42
|
+
{...tableProps}
|
|
43
|
+
/>
|
|
44
|
+
```
|
|
45
|
+
|
|
26
46
|
## Column width
|
|
27
47
|
|
|
28
48
|
Drag the grip on a header's right border to resize that column. The line is
|
|
@@ -372,6 +372,85 @@ const ElementCellsMatrix = () => {
|
|
|
372
372
|
);
|
|
373
373
|
};
|
|
374
374
|
|
|
375
|
+
// INFO: an editable table with disabled schema columns beside editable ones —
|
|
376
|
+
// hovering Value or Notes opens an editor, while Unit, Source and the coloured
|
|
377
|
+
// Status column stay read-only, matching how the form treats disabled fields
|
|
378
|
+
const lockedColumns: NewTableColumn[] = [
|
|
379
|
+
{
|
|
380
|
+
key: "metric",
|
|
381
|
+
title: "Metric",
|
|
382
|
+
type: "list",
|
|
383
|
+
listSelection: ["Steps", "Heart Rate", "Weight"],
|
|
384
|
+
listSelectionColour: ["blue", "red", "yellow"],
|
|
385
|
+
},
|
|
386
|
+
{ key: "value", title: "Value", type: "number" },
|
|
387
|
+
{ key: "unit", title: "Unit (disabled)", type: "string", disabled: true },
|
|
388
|
+
{ key: "notes", title: "Notes", type: "string" },
|
|
389
|
+
{
|
|
390
|
+
key: "source",
|
|
391
|
+
title: "Source (disabled)",
|
|
392
|
+
type: "string",
|
|
393
|
+
disabled: true,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
key: "status",
|
|
397
|
+
title: "Status (disabled)",
|
|
398
|
+
type: "list",
|
|
399
|
+
listSelection: ["Synced", "Pending"],
|
|
400
|
+
listSelectionColour: ["green", "yellow"],
|
|
401
|
+
disabled: true,
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
|
|
405
|
+
const lockedRows: NewTableRow[] = [
|
|
406
|
+
{
|
|
407
|
+
id: "1",
|
|
408
|
+
metric: "Heart Rate",
|
|
409
|
+
value: 72,
|
|
410
|
+
unit: "bpm",
|
|
411
|
+
notes: "After walk",
|
|
412
|
+
source: "Manual entry",
|
|
413
|
+
status: "Synced",
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: "2",
|
|
417
|
+
metric: "Weight",
|
|
418
|
+
value: 68.5,
|
|
419
|
+
unit: "kg",
|
|
420
|
+
notes: "Morning",
|
|
421
|
+
source: "Device sync",
|
|
422
|
+
status: "Pending",
|
|
423
|
+
},
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
const EditableWithDisabledColumns = () => {
|
|
427
|
+
const [rows, setRows] = useState<NewTableRow[]>(lockedRows);
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<NewTableImpl
|
|
431
|
+
columns={lockedColumns}
|
|
432
|
+
data={rows}
|
|
433
|
+
paginationType="page"
|
|
434
|
+
size={rows.length}
|
|
435
|
+
limit={10}
|
|
436
|
+
pageIndex={0}
|
|
437
|
+
handleUpdatePageIndex={() => {}}
|
|
438
|
+
editable={{
|
|
439
|
+
active: true,
|
|
440
|
+
onUpdateData: async (rowId, columnId, value) => {
|
|
441
|
+
setRows((previous) =>
|
|
442
|
+
previous.map((eachRow) =>
|
|
443
|
+
eachRow.id === rowId
|
|
444
|
+
? { ...eachRow, [columnId]: value }
|
|
445
|
+
: eachRow,
|
|
446
|
+
),
|
|
447
|
+
);
|
|
448
|
+
},
|
|
449
|
+
}}
|
|
450
|
+
/>
|
|
451
|
+
);
|
|
452
|
+
};
|
|
453
|
+
|
|
375
454
|
export const NewTable = () => {
|
|
376
455
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
377
456
|
|
|
@@ -512,6 +591,13 @@ export const NewTable = () => {
|
|
|
512
591
|
<EditableWithAddColumn />
|
|
513
592
|
</StorySection>
|
|
514
593
|
|
|
594
|
+
<StorySection
|
|
595
|
+
title="Editable with disabled columns"
|
|
596
|
+
description="A column marked disabled in the schema stays read-only in an editable table, the same as in the form. Hover Value or Notes to edit; Unit, Source and the coloured Status column never open an editor, and a row click still reaches the row."
|
|
597
|
+
>
|
|
598
|
+
<EditableWithDisabledColumns />
|
|
599
|
+
</StorySection>
|
|
600
|
+
|
|
515
601
|
<StorySection
|
|
516
602
|
title="Element cells with row actions (config matrix)"
|
|
517
603
|
description="Cells can be React elements: kit inputs with cellInput={{ active: true }} (the same compact mode the DTW inline edit matrix uses) and IconButtons per row, while the parent owns the row state. Typed columns are what make the inline 'Create New Item' row render inputs — element-only columns render nothing there. Do NOT combine with editable.active: edit mode wraps element cells in its own editors (from 1.2.66 a React element cell passes through, but its inputs are ~286px wide). Wrap in overflow-x-auto when the columns outgrow the pane."
|
|
@@ -68,7 +68,7 @@ return (
|
|
|
68
68
|
| `disabled` | `boolean` | |
|
|
69
69
|
| `rounded` | `none / md /lg` | Border with |
|
|
70
70
|
| `variant` | `flat / outline /flatOutline` | |
|
|
71
|
-
| `color` | `red / blue / green / yellow / indigo / dark` | Background color & text color |
|
|
71
|
+
| `color` | `TagColor \| string` — `primary / red / blue / green / yellow / indigo / purple / lime / sky / pink / orange / dark` | Background color & text color. `TagColor` is exported from this component |
|
|
72
72
|
| `customFontSize` | `string` | Tailwind font class name |
|
|
73
73
|
| `customPadding` | `string` | Tailwind padding class name |
|
|
74
74
|
| `onClick` | `(value: string) => void` | Value is tagText |
|
|
@@ -67,6 +67,13 @@ export const Tag = () => {
|
|
|
67
67
|
<TagImpl tagText="React" color="dark" variant="flatOutline" />
|
|
68
68
|
<TagImpl tagText="Default" color="default" variant="flatOutline" />
|
|
69
69
|
</div>
|
|
70
|
+
<h1 className="text-sm font-medium">Custom Hex</h1>
|
|
71
|
+
<div className="flex flex-wrap gap-4">
|
|
72
|
+
<TagImpl tagText="Joel Michael" color="#2563eb" />
|
|
73
|
+
<TagImpl tagText="cychanyi" color="#0d9488" />
|
|
74
|
+
<TagImpl tagText="Backlogs" color="#a16207" />
|
|
75
|
+
<TagImpl tagText="Small" size="small" color="#db2777" />
|
|
76
|
+
</div>
|
|
70
77
|
</div>
|
|
71
78
|
</div>
|
|
72
79
|
);
|
|
@@ -34,6 +34,10 @@ export const UploadInput = () => {
|
|
|
34
34
|
const [multiPdfs, setMultiPdfs] = useState<any>({ ...emptyFiles });
|
|
35
35
|
const [noDupFiles, setNoDupFiles] = useState<any>({ ...emptyFiles });
|
|
36
36
|
const [compactFiles, setCompactFiles] = useState<any>({ ...preloadedImages });
|
|
37
|
+
const [chooserFiles, setChooserFiles] = useState<any>({ ...emptyFiles });
|
|
38
|
+
const [chooserMobileFiles, setChooserMobileFiles] = useState<any>({
|
|
39
|
+
...emptyFiles,
|
|
40
|
+
});
|
|
37
41
|
const [tagFiles, setTagFiles] = useState<any>({
|
|
38
42
|
data: [
|
|
39
43
|
{
|
|
@@ -104,6 +108,43 @@ export const UploadInput = () => {
|
|
|
104
108
|
|
|
105
109
|
return (
|
|
106
110
|
<>
|
|
111
|
+
<StorySection
|
|
112
|
+
title="Camera / Gallery Chooser"
|
|
113
|
+
description="showCameraGalleryOptionModal anchors a Camera / Gallery panel to the upload trigger. Android needs it: a WebView only reaches the camera when the input carries `capture`, so without a chooser an image field drops straight into the file picker with no camera option. iOS presents its own sheet, so consumers should gate this on platform. Camera uses capture='environment'; both honour the field's accept."
|
|
114
|
+
>
|
|
115
|
+
<div className="w-full max-w-sm">
|
|
116
|
+
<UploadInputImpl
|
|
117
|
+
lib={{ axios }}
|
|
118
|
+
id="chooserImages"
|
|
119
|
+
label="Roof Aerial / Overview Image"
|
|
120
|
+
type="multi"
|
|
121
|
+
showModal
|
|
122
|
+
accept="image/*"
|
|
123
|
+
files={chooserFiles}
|
|
124
|
+
onChange={(value: any) => setChooserFiles(value)}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
</StorySection>
|
|
128
|
+
|
|
129
|
+
<StorySection
|
|
130
|
+
title="Camera / Gallery Chooser — Mobile Row"
|
|
131
|
+
description="The same chooser in the mobileView row layout, which is what a phone renders below 768px. The panel is anchored to its trigger, so it travels with the field as the form scrolls."
|
|
132
|
+
>
|
|
133
|
+
<div className="w-full max-w-sm">
|
|
134
|
+
<UploadInputImpl
|
|
135
|
+
lib={{ axios }}
|
|
136
|
+
id="chooserMobileImages"
|
|
137
|
+
label="Ridgecapping Location Image"
|
|
138
|
+
type="multi"
|
|
139
|
+
showModal
|
|
140
|
+
mobileView
|
|
141
|
+
accept="image/*,video/*"
|
|
142
|
+
files={chooserMobileFiles}
|
|
143
|
+
onChange={(value: any) => setChooserMobileFiles(value)}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</StorySection>
|
|
147
|
+
|
|
107
148
|
<StorySection
|
|
108
149
|
title="Single Image Upload"
|
|
109
150
|
description="Default single-file mode. Accepts PNG, JPG, JPEG, HEIC, HEIF. HEIC/HEIF files are auto-converted to JPEG. compress reduces file size before upload."
|