nexus-shared 2.0.0 → 3.0.0
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/CHANGELOG.md +262 -0
- package/README.md +122 -25
- package/dist/Client.Index.d.ts +11 -0
- package/dist/Client.Index.js +11 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +100 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -0
- package/dist/Components/Documents/Menu.js +24 -20
- package/dist/Components/Documents/SplitButton.js +5 -3
- package/dist/Components/Documents/TabButtons.d.ts +24 -6
- package/dist/Components/Documents/TabButtons.js +23 -4
- package/dist/Components/Forms/ApiForm.d.ts +6 -4
- package/dist/Components/Forms/ApiForm.js +15 -14
- package/dist/Components/Forms/Crud.js +202 -50
- package/dist/Components/Forms/ExcelImport.d.ts +42 -0
- package/dist/Components/Forms/ExcelImport.js +190 -0
- package/dist/Components/Forms/Form.js +5 -1
- package/dist/Components/Inputs/DateTimePicker.js +2 -1
- package/dist/Components/Inputs/GroupForm.js +4 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
- package/dist/Components/Inputs/InputRenderer.js +1 -1
- package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
- package/dist/Components/Inputs/RowsInput.d.ts +12 -1
- package/dist/Components/Inputs/RowsInput.js +53 -4
- package/dist/Components/Inputs/TabularForm.d.ts +7 -3
- package/dist/Components/Inputs/TabularForm.js +85 -20
- package/dist/Components/Inputs/TimePicker.js +2 -1
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +2 -2
- package/dist/Components/Layouts/ThemeSwitcher.js +35 -15
- package/dist/Components/Viewers/DataTable.js +88 -34
- package/dist/Components/Viewers/DataTableColumns.d.ts +28 -0
- package/dist/Components/Viewers/DataTableColumns.js +243 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +6 -2
- package/dist/Components/Viewers/DataTableParts.js +21 -9
- package/dist/Helpers/ApiClient.d.ts +22 -0
- package/dist/Helpers/ApiClient.js +4 -1
- package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
- package/dist/Helpers/ApiFormHelpers.js +73 -9
- package/dist/Helpers/ApiModules.d.ts +0 -4
- package/dist/Helpers/ApiModules.js +1 -0
- package/dist/Helpers/ApiResponses.d.ts +14 -4
- package/dist/Helpers/ApiResponses.js +116 -25
- package/dist/Helpers/ApiRoutes.d.ts +0 -10
- package/dist/Helpers/ApiRoutes.js +2 -10
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/CrudBackend.d.ts +47 -5
- package/dist/Helpers/CrudBackend.js +99 -4
- package/dist/Helpers/CrudHelpers.d.ts +39 -4
- package/dist/Helpers/CrudHelpers.js +76 -11
- package/dist/Helpers/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +290 -0
- package/dist/Helpers/ExcelHelpers.d.ts +98 -0
- package/dist/Helpers/ExcelHelpers.js +302 -0
- package/dist/Helpers/FormStore.d.ts +2 -0
- package/dist/Helpers/FormStore.js +19 -1
- package/dist/Helpers/MessageBuilder.js +0 -2
- package/dist/Helpers/PopoverHelpers.d.ts +31 -6
- package/dist/Helpers/PopoverHelpers.js +116 -12
- package/dist/Helpers/RowsStore.d.ts +7 -1
- package/dist/Helpers/RowsStore.js +22 -0
- package/dist/Helpers/TableColumns.d.ts +7 -0
- package/dist/Helpers/TableColumns.js +41 -13
- package/dist/Helpers/TableHelpers.d.ts +9 -1
- package/dist/Helpers/TableHelpers.js +20 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +29 -4
- package/dist/Interfaces/ApiInterfaces.js +16 -10
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/dist/Interfaces/ChatInterfaces.js +1 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +73 -12
- package/dist/Interfaces/ExcelInterfaces.d.ts +215 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +52 -2
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/TableInterfaces.d.ts +62 -2
- package/dist/Services/BrowserApi.d.ts +18 -1
- package/dist/Services/BrowserApi.js +18 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +46 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +6 -1
- package/dist/Services/ServerApi.js +3 -0
- package/dist/Shared.Index.d.ts +8 -0
- package/dist/Shared.Index.js +8 -0
- package/package.json +6 -3
- package/src/Styles/Nexus.Button.css +2 -0
- package/src/Styles/Nexus.Chat.css +1489 -0
- package/src/Styles/Nexus.Crud.css +77 -0
- package/src/Styles/Nexus.Excel.css +370 -0
- package/src/Styles/Nexus.Form.css +23 -0
- package/src/Styles/Nexus.Index.css +2 -0
- package/src/Styles/Nexus.Menu.css +12 -2
- package/src/Styles/Nexus.Rows.css +78 -48
- package/src/Styles/Nexus.Tab.Buttons.css +55 -0
- package/src/Styles/Nexus.Table.css +286 -4
- package/src/Styles/Nexus.Theme.Picker.css +75 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to `nexus-shared`. The **values** of exported constants count as API here: several of them are the
|
|
4
|
+
Nexus backend's own numbers, and a value that differs from the backend by one does not fail loudly — it silently
|
|
5
|
+
means the wrong thing on every record and every permission check.
|
|
6
|
+
|
|
7
|
+
## 3.0.0
|
|
8
|
+
|
|
9
|
+
**Breaking. This changes the value of exported constants, and `2.0.0` is on npm with the old ones.** Publishing it
|
|
10
|
+
as a patch or a minor would hand `^2.0.0` consumers different numbers under a compatible-looking range, which is the
|
|
11
|
+
worst way to ship this, so it takes a major. The owner chose the number on 2026-10-02, together with `nexus-public`
|
|
12
|
+
and `nexus-admin`, which both depend on this package and so move to `3.0.0` with it; their `nexus-shared` range is
|
|
13
|
+
now `^3.0.0`, because a bump of this one alone would leave them pulling the old, wrong constants.
|
|
14
|
+
|
|
15
|
+
**`nexus-icons` must be installed at `1.1.0` or newer**, and the dependency range here says so. The chat components
|
|
16
|
+
added in this version use `iconChecks`, `iconCornerUpLeft`, `iconMessageCircle`, `iconPaperclip`, `iconSend` and
|
|
17
|
+
`iconUserPlus`, none of which exist in `nexus-icons@1.0.0`: with the old range a fresh install resolved to `1.0.0`
|
|
18
|
+
and the build failed on six missing exports.
|
|
19
|
+
|
|
20
|
+
### `NexusModule` — `Chat` added, and every module after it moved up one bit
|
|
21
|
+
|
|
22
|
+
The backend's `EModules` gained `Chat` at bit 6, and everything after it shifted. `NexusModule` follows it exactly.
|
|
23
|
+
A permission is a bitmask of these flags, so a package that kept the old numbers would grant and deny the wrong
|
|
24
|
+
modules without any error to show for it.
|
|
25
|
+
|
|
26
|
+
| Module | Was | Now |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `None` | 0 | 0 |
|
|
29
|
+
| `Migrations` | 1 | 1 |
|
|
30
|
+
| `Configuration` | 2 | 2 |
|
|
31
|
+
| `Identity` | 4 | 4 |
|
|
32
|
+
| `Enterprise` | 8 | 8 |
|
|
33
|
+
| `Drive` | 16 | 16 |
|
|
34
|
+
| `Message` | 32 | 32 |
|
|
35
|
+
| **`Chat`** | *(did not exist)* | **64** |
|
|
36
|
+
| `Accounting` | 64 | **128** |
|
|
37
|
+
| `Report` | 128 | **256** |
|
|
38
|
+
| `Requisition` | 256 | **512** |
|
|
39
|
+
| `Tenant` | 512 | **1024** |
|
|
40
|
+
| `Subscription` | 1024 | **2048** |
|
|
41
|
+
| `Reservation` | 2048 | **4096** |
|
|
42
|
+
| `School` | 4096 | **8192** |
|
|
43
|
+
| `Pharmacy` | 8192 | **16384** |
|
|
44
|
+
| `Staffing` | 16384 | **32768** |
|
|
45
|
+
| `Portfolio` | 32768 | **65536** |
|
|
46
|
+
|
|
47
|
+
`NEXUS_MODULES` gained the matching entry (`key: "chat"`, `label: "Chat"`, `env: "API_CHAT"`), so `Chat` can be
|
|
48
|
+
looked up, proxied and named in messages like every other module.
|
|
49
|
+
|
|
50
|
+
**What to check when you upgrade.** Code that uses the names (`NexusModule.Reservation`) needs nothing. Anything
|
|
51
|
+
that stored, transmitted or hard-coded a **number** has to be revisited:
|
|
52
|
+
|
|
53
|
+
- permission masks saved in a database, a cache, a token claim or a settings file;
|
|
54
|
+
- proxy URLs written with a number in place of a key (`/api/app/2048/…` is now `/api/app/4096/…`, and `reservation`
|
|
55
|
+
by key has always worked and still does);
|
|
56
|
+
- a backend that has not taken the `Chat` shift yet — both sides move together or neither does.
|
|
57
|
+
|
|
58
|
+
### `NEXUS_FLAGGED_STATUS` — 15 → 25
|
|
59
|
+
|
|
60
|
+
The owner settled the record-status bands, and `ERecordStatus.Flagged` is seeded at **25**. `NexusCrudBackend`
|
|
61
|
+
decides whether a row is flagged by comparing `recordStatus` to this constant, so a package on 15 while the backend
|
|
62
|
+
writes 25 shows the wrong flag on every row, and flagging one writes a status the backend does not recognise.
|
|
63
|
+
|
|
64
|
+
The bands, for anything that reasons about them:
|
|
65
|
+
|
|
66
|
+
| Band | Range |
|
|
67
|
+
|---|---|
|
|
68
|
+
| Active | 0–39 |
|
|
69
|
+
| — Active account | 0–29 |
|
|
70
|
+
| — Flag | **25–29** |
|
|
71
|
+
| — Pending, unverified, in review, approval required | 30–39 |
|
|
72
|
+
| Trash | 40–59 |
|
|
73
|
+
| Delete | 60 and above |
|
|
74
|
+
|
|
75
|
+
`NEXUS_ACTIVE_STATUS` stays 0, and the `recordStatus < 40` reading of "a live record" that the rest of the code
|
|
76
|
+
already assumes stays right.
|
|
77
|
+
|
|
78
|
+
Note that **the flag is a range, 25–29, and this release still checks a single value.** Only the seeded member at 25
|
|
79
|
+
has a meaning today, and the bands are about to become configurable per project, so a range-aware check belongs with
|
|
80
|
+
that work rather than here. Do not read the single value as the final answer.
|
|
81
|
+
|
|
82
|
+
### Removed: the archive
|
|
83
|
+
|
|
84
|
+
The owner removed the archive on 2026-09-30: a record is live, in the trash, or deleted, and the way back from
|
|
85
|
+
deleted lands it in the trash. The second put-away list and its second recovery path were the only reason the CRUD
|
|
86
|
+
had both Restore and Recover, and the bands close up behind it — **Trash is 40–59 and Delete 60 and above**, so
|
|
87
|
+
nothing stored keeps a meaning it no longer has. `recordStatus < 40` still reads "a live record".
|
|
88
|
+
|
|
89
|
+
What goes, all of it breaking for anything that named it:
|
|
90
|
+
|
|
91
|
+
- `CrudMode` loses `"archive"`, so a CRUD shows All, Flagged, Trash and Deleted.
|
|
92
|
+
- `CrudRowActionName` and `CrudMassActionName` lose `"archive"` and `"restore"`. Recover, the trash's own way back,
|
|
93
|
+
is untouched: a CRUD that offered both now offers one.
|
|
94
|
+
- `CrudEndpoints` loses `archive` and `restore`; `apiController` stops naming `Archive`, `Restore`, `MassArchive`,
|
|
95
|
+
`MassRestore`, `ArchiveListing` and `ArchivePagination`, and `lists`/`pages` lose their `archive` key.
|
|
96
|
+
- `MessageActionName` loses `"archive"`, and the message builder its words and its "It can be restored from the
|
|
97
|
+
archive." confirmation detail. `"restore"` stays in the vocabulary: it is a plain verb, not the archive's.
|
|
98
|
+
|
|
99
|
+
**What to check when you upgrade.** A CRUD that listed `"archive"` in `modes` or `actions` no longer compiles,
|
|
100
|
+
which is the point — the endpoint behind it is gone from the backend too. A backend that still answers
|
|
101
|
+
`ArchiveListing` is not asked for it any more.
|
|
102
|
+
|
|
103
|
+
### Added: the chat
|
|
104
|
+
|
|
105
|
+
A discussion on any record — a leave request the manager and a co-worker talk through, a purchase order, an
|
|
106
|
+
applicant, a tender — behind one icon that says whether anything is waiting. Nothing that does not use it changes.
|
|
107
|
+
|
|
108
|
+
- A conversation is named by three values, `ChatScope`: the **domain** (which kind of record), the **parent id** (the
|
|
109
|
+
record) and the **user id** (who is reading). The domain is the app's own enum, given either as a template
|
|
110
|
+
(`<Chat<EDomainTypes> …>`) or once in `ChatRegister`, the same way `ApiRegister` names the module enum. Without one
|
|
111
|
+
it is a plain number.
|
|
112
|
+
- **No domain numbers are defined here.** They belong to the backend's `EDomainTypes`, and the warning at the top of
|
|
113
|
+
this file applies to them in full. `defineChatDomains` only gives an app's domains their names on screen.
|
|
114
|
+
- `ChatButton` is an icon in three looks: quiet with no messages, the accent on its tint once there are messages, and
|
|
115
|
+
the accent filled with an unread badge once some are unread. One size in all three, with the badge over the corner,
|
|
116
|
+
so a column of them in a table does not move as the counts arrive.
|
|
117
|
+
- `ChatSummaryCache` fills every icon that mounted in the same moment with **one call per domain**. A list whose rows
|
|
118
|
+
already carry their counts passes `summary` and makes no call at all.
|
|
119
|
+
- `Chat` is the icon with the conversation in a popup; `ChatPanel` is the conversation on its own, for a record's own
|
|
120
|
+
page. Messages are **Markdown**: written in `MarkdownEditor` and drawn by `MarkdownView`. Enter sends and
|
|
121
|
+
Shift+Enter starts a line, except in a list or a code fence, where the editor keeps Enter; `sendKey="modifier"`
|
|
122
|
+
moves sending to Ctrl+Enter.
|
|
123
|
+
- One mark where the time sits says how far a message of your own has got: a spinner while sending, a tick once the
|
|
124
|
+
server has it, two once `deliveredTo` holds somebody, two in the accent once `readBy` does.
|
|
125
|
+
- Where the data comes from is one interface, `ChatSource`. `apiChatSource` goes through the app's `ChatBackend`
|
|
126
|
+
(`NexusChatBackend` by default); `localChatSource` keeps a conversation in memory for a demo or a test.
|
|
127
|
+
- **The Nexus endpoints are the shape the Chat module is expected to take, not an API that answers today.**
|
|
128
|
+
`NexusModule.Chat` is a number and a proxy key; the controller is not built. Each difference, when it lands, is one
|
|
129
|
+
override on `NexusChatBackend`.
|
|
130
|
+
- `check:themes` gained a rule: body text on the primary tint, which is a message bubble of your own. Every theme
|
|
131
|
+
clears AAA except catppuccin light at 6.84, so it is held to the AA 4.5 that applies to body text.
|
|
132
|
+
- **A record holds many conversations.** `ChatScope` names the record; each conversation in it is a
|
|
133
|
+
`ChatConversation` with its own id and a type, `one-to-one` or `group`. `ChatPlaceSource` lists them and starts
|
|
134
|
+
them, `useChatPlace` is the hook, and `apiChatPlace` is the API one. A panel given a `source` still shows that one
|
|
135
|
+
conversation and no switch, so nothing that used the first round changes.
|
|
136
|
+
- The panel's body has three views — the thread, the list of conversations, and New chat — and the bar moves between
|
|
137
|
+
them. With several conversations the title is the switch; with one it is just the title.
|
|
138
|
+
- Starting a chat never asks for the type: one person makes a one-to-one, more make a group, and a name makes a
|
|
139
|
+
group either way. A one-to-one with somebody who already has one is expected to come back rather than be made
|
|
140
|
+
twice; `localChatPlace` keeps that rule.
|
|
141
|
+
- `@` at the start of a word offers the people in the conversation, narrows as it is typed, and owns Enter while it
|
|
142
|
+
is open. A mention is plain `@Name`, or a Markdown link with `mentionHref`; either way the ids travel in the
|
|
143
|
+
draft's `mentions`.
|
|
144
|
+
- `ChatSnapshot` gained `loaded`: a store that has read nothing yet holds empty counts, and a panel must not write
|
|
145
|
+
those over what a conversation list already knows.
|
|
146
|
+
|
|
147
|
+
### Added: several at once, `TabButtons multiple`
|
|
148
|
+
|
|
149
|
+
`TabButtons` picks one option; with `multiple` it picks several, so a list of choices that must be seen all at once
|
|
150
|
+
(the weekdays a rule runs on, the letters or symbols a pattern allows) needs no second control. Nothing that does
|
|
151
|
+
not pass it changes.
|
|
152
|
+
|
|
153
|
+
- `value` is an array and `onChange` hands back the chosen values **in option order**, whatever order they were
|
|
154
|
+
clicked in. Values that match no option are ignored and never handed back.
|
|
155
|
+
- The options are native checkboxes in a `role="group"`: Tab moves between them and Space toggles one. The group
|
|
156
|
+
still needs `aria-label` or `aria-labelledby`.
|
|
157
|
+
- There is no sliding indicator: each chosen option is raised with the indicator's own colors, border and shadow,
|
|
158
|
+
and keeps the pointer, since a click takes it away again. An option that is not chosen sits flat on the track.
|
|
159
|
+
- The options **wrap** onto more rows when they do not fit, each row as tall as a one-row track's options, and an
|
|
160
|
+
option is never narrower than it is tall, so a row of single letters lines up. On a touch screen
|
|
161
|
+
(`pointer: coarse`) every option is a medium control's height: 26 letters on a phone are 34px targets, not 22px.
|
|
162
|
+
- Made and checked for the `nav` design (`/demo/tab-buttons`, "Several at once"); `design="list"` with `multiple`
|
|
163
|
+
renders checkboxes too, but has not been checked yet.
|
|
164
|
+
|
|
165
|
+
### Added: inputs that follow others, `dependsOn`, and a row's own params, `cellParams`
|
|
166
|
+
|
|
167
|
+
Two optional keys, added 2026-09-26 for the builder's class form, whose company model shows only when the class has a
|
|
168
|
+
company or a branch and whose type arguments offer a key type in one row and a model in the next. Nothing that does
|
|
169
|
+
not pass them changes.
|
|
170
|
+
|
|
171
|
+
- **`dependsOn: ["hasCompany"]`** on an input's params (Form only). When a named input's value changes — typed, or
|
|
172
|
+
set from code — this input draws again **without mounting again** (nothing typed in it is lost) and its params are
|
|
173
|
+
read afresh, so getters for `hidden`, `readOnly`, `readOnlyMessage`, `label`, `hint` or `options` answer for the
|
|
174
|
+
values as they are now. Its rule is asked again too, so a `required` a getter switched on counts at once. An input
|
|
175
|
+
that names any is asked `hidden` each time it draws, so it can come and go; one that names none is asked once, when
|
|
176
|
+
the form draws, as before. Name the input itself to follow its own typing.
|
|
177
|
+
- **`cellParams(row, name, index)`** on `TabularForm` and `GroupForm`: the params of one cell that differ from its
|
|
178
|
+
column's, such as a picker's `options` and `selectedOptions`. Asked as the row draws; return nothing for the
|
|
179
|
+
column's own params, and the **same object** for the same answer (the merged params are kept per answer, so the cell
|
|
180
|
+
does not draw again for nothing). Getters on either side stay getters. Rules still come from the column.
|
|
181
|
+
|
|
182
|
+
### Added: a generated column in a tabular or group form
|
|
183
|
+
|
|
184
|
+
Two optional props on `TabularForm` and `GroupForm`, for a column whose values a row's **position** decides rather
|
|
185
|
+
than the user — a bitwise enum's value, a line number, a running total. Nothing that does not pass them changes.
|
|
186
|
+
|
|
187
|
+
- `deriveRows(rows)` returns one patch per row and runs after every change, including a row being added, removed,
|
|
188
|
+
duplicated or **moved**. The patches are written as code writes, so only the cells whose value really changed
|
|
189
|
+
mount again and focus stays on the row's own menu: a keyboard can move a row twice in a row.
|
|
190
|
+
- `cellReadOnly(row, name, index)` is asked for every cell as it renders and returns the reason a cell is
|
|
191
|
+
generated. The cell is locked and shows that reason as its read-only note. Because it is asked per render, a
|
|
192
|
+
column can lock and unlock while the form around it changes, which params cannot do — they are fixed once a form
|
|
193
|
+
opens.
|
|
194
|
+
|
|
195
|
+
### Added: rows nobody may change, `rowReadOnly`
|
|
196
|
+
|
|
197
|
+
One optional prop on `TabularForm` and `GroupForm`, for rows the user did not write and may not change — the fields
|
|
198
|
+
a class inherits from its parent, shown first in the same grid as its own. Nothing that does not pass it changes.
|
|
199
|
+
|
|
200
|
+
- `rowReadOnly(row, index)` returns the reason (or `true`). Every cell of such a row is read-only with that reason
|
|
201
|
+
as its note, the row's menu in a tabular form offers nothing (no insert, duplicate, move or remove), and the row
|
|
202
|
+
below it cannot move up past it. Nothing reorders the rows: keep the locked ones first.
|
|
203
|
+
- A locked row says nothing about its columns: the column-header lock is worked out from the other rows in view, so
|
|
204
|
+
a grid of inherited rows and one own row does not put a lock on every header.
|
|
205
|
+
|
|
206
|
+
### Added: a panel beside a CRUD form, `form.aside`
|
|
207
|
+
|
|
208
|
+
An Add or Edit popup can show a panel next to its form: a preview of what a save would produce, a summary, a help
|
|
209
|
+
panel. It takes a node, or a function handed `{ form, action, row }` — `form` being that popup's own `FormStore`, so
|
|
210
|
+
a panel follows what is typed through `subscribeValue()` and the form around it never re-renders for it.
|
|
211
|
+
|
|
212
|
+
The two are columns above 1000px, each scrolling on its own so a long form cannot push the panel off the screen, and
|
|
213
|
+
they stack below that with the **form first**, in the DOM as well as on the screen. `--nx-crud-aside-width` sets the
|
|
214
|
+
panel's share (default `clamp(20rem, 38%, 44rem)`). Pair it with a wide `form.size` — `xl` or `full`; a panel in a
|
|
215
|
+
560px popup leaves the form nothing. A CRUD that passes no `aside` renders exactly what it did before, wrapper and
|
|
216
|
+
all.
|
|
217
|
+
|
|
218
|
+
### `TabularForm` reworked: one row control, rows that do not move, one lock per column
|
|
219
|
+
|
|
220
|
+
The owner called the tabular design bad, from a screenshot of an 18-row table with one generated column. Every fault
|
|
221
|
+
was in this component rather than in the page that used it, so every fix is here. Nothing needs a new prop, and a
|
|
222
|
+
table that passed nothing new looks different — deliberately.
|
|
223
|
+
|
|
224
|
+
**Breaking, visually and for tests.** Two things a consumer may be reaching for have moved:
|
|
225
|
+
|
|
226
|
+
- **The remove column is gone.** Remove was a column of unlabelled `×` buttons at the end, beside a row menu that
|
|
227
|
+
already held Remove: two ways to act on one row, and a whole column spent on one of them. Now a row has one
|
|
228
|
+
control, the menu beside its number, holding Insert above, Insert below, Duplicate, Move up, Move down and Remove.
|
|
229
|
+
`removable` still decides whether Remove is offered. A test that clicked a button named `Remove <row>` opens the
|
|
230
|
+
row's menu instead: `Row 2 actions`, then the `Remove` item. `.nx-tabular__end` and `.nx-tabular__remove` are gone
|
|
231
|
+
from the DOM and the stylesheet; so is `--nx-tabular-end`, replaced by `--nx-tabular-menu` (the menu's slot width).
|
|
232
|
+
- **The row number no longer opens the menu.** It used to *become* the menu on hover: the number faded out and dots
|
|
233
|
+
faded in over it, so the numbering looked broken and the grid felt unstable as the pointer crossed it. The number
|
|
234
|
+
now stays where it is, right-aligned, at every moment, and the menu button sits beside it in its own space,
|
|
235
|
+
always present. The number column is that much wider.
|
|
236
|
+
|
|
237
|
+
**Fixed, with no change to how a table is written:**
|
|
238
|
+
|
|
239
|
+
- **Every row is one line high, whatever it holds.** The row's grid track was sized by its tallest cell, so a single
|
|
240
|
+
`textarea` column of `rows: 2` made every cell of its row 20px taller than the row's own box — drawing the cells,
|
|
241
|
+
their edges and their focus rings over the rows below. One row of content therefore looked taller than its
|
|
242
|
+
neighbours, and a focus ring bled outside its cell. The row now has one track exactly its own height, cells keep
|
|
243
|
+
what they hold inside their bounds, and a `textarea` fills its cell and scrolls inside it.
|
|
244
|
+
- **No resize grips in cells.** A `textarea` in a cell took `resize: vertical` from the multi-line input rule, so
|
|
245
|
+
every cell of such a column offered a grip that would break the alignment of its row. They are `resize: none` now.
|
|
246
|
+
- **A filled cell and an empty one are drawn the same.** Same consequence as the above: only the cell with text was
|
|
247
|
+
tall enough to show what the overflow was doing.
|
|
248
|
+
- **`maxHeight` comes down to whole rows.** The scroller used to end wherever the number fell, cutting the last row
|
|
249
|
+
in half, which reads as broken rather than as scrollable. The component measures its header, its frame and its
|
|
250
|
+
real row height and caps itself just under what was asked for. A number too small for one row is left alone.
|
|
251
|
+
- **A generated column says so once.** `cellReadOnly` put a lock in every cell of the column it locked — eighteen
|
|
252
|
+
locks for one fact. The lock is now on the **column header**, with the reason as its title, and the cells keep the
|
|
253
|
+
read-only tint and their own reason, which still shows under the table when one is used. A lock that really is per
|
|
254
|
+
row (some rows locked, not all) leaves the header alone and is drawn per cell as before.
|
|
255
|
+
- **Less chrome.** The frame is `--nx-border` rather than `--nx-border-strong`: the sheet draws every line it needs
|
|
256
|
+
inside itself, and in a popup or a panel the strong edge was a box around a box around boxes.
|
|
257
|
+
|
|
258
|
+
## 2.0.0
|
|
259
|
+
|
|
260
|
+
The rewrite: compiled `dist` with the entry points `.`, `/client`, `/server` and `/styles.css`, in place of 1.x's
|
|
261
|
+
raw TypeScript from `src` with `.`, `/client` and `/interface`. A major version, so nobody on `^1.1.21` is moved
|
|
262
|
+
into a different API.
|
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ All styles sit in `@layer nexus.*`, so an app's own CSS overrides any token or c
|
|
|
97
97
|
| Component | Import | Notes |
|
|
98
98
|
|---|---|---|
|
|
99
99
|
| `Button` | `nexus-shared` | Variants `primary`, `secondary`, `ghost`, `danger`; sizes `sm`, `md`, `lg`; `icon`, `iconEnd`, `loading`, `block`; `hoverTone` |
|
|
100
|
-
| `TabButtons` | `nexus-shared/client` | Pick one option; sliding indicator; sizes, `tone="primary"`, `labels="selected"` (icons, label on the selected option), `block`, disabled options; `design="list"` for a vertical list with descriptions and badges |
|
|
100
|
+
| `TabButtons` | `nexus-shared/client` | Pick one option; sliding indicator; sizes, `tone="primary"`, `labels="selected"` (icons, label on the selected option), `block`, disabled options; `design="list"` for a vertical list with descriptions and badges; `multiple` to pick several, wrapping onto more rows |
|
|
101
101
|
| `SplitButton` | `nexus-shared/client` | A main action with a menu of alternatives; the pick becomes the main button, and `storageKey` remembers it for the next visit |
|
|
102
102
|
| `Menu`, `ContextMenu` | `nexus-shared/client` | A dropdown menu under a button, and a right-click menu: icons, shortcuts, descriptions, `tone="danger"`, links, `kind="radio"` or `"checkbox"` |
|
|
103
103
|
| `Toaster`, `toast` | `nexus-shared/client` | Toast messages from anywhere: `toast.success(title, message)`, `toast.promise`, `toast.loading`, actions, positions; one `<Toaster />` in the layout |
|
|
@@ -126,14 +126,14 @@ All styles sit in `@layer nexus.*`, so an app's own CSS overrides any token or c
|
|
|
126
126
|
| `FormButtons` | `nexus-shared/client` | A form's status line and buttons on their own, such as in a popup's footer |
|
|
127
127
|
| `useForm`, `useFormValue`, `useFormValues`, `useFormStatus` | `nexus-shared/client` | A store for the page to hold, and hooks that render again when a value, any value, or the status changes |
|
|
128
128
|
| `FormStore` | `nexus-shared` | The store and the form's handle: `getValue`, `getChanges`, `set`, `load` (with `keepChanged`), `reset`, `validate`, `setErrors`, `focus`, `submit` |
|
|
129
|
-
| `DataTable`, `useTable` | `nexus-shared/client` | Rows with a search (rows per page inside it), sorting by column, pages, a selection with one split button of actions over the headings, icon buttons and a menu per row, columns the user orders, resizes, pins, and hides, an options menu (CSV, print, import), and loading, empty, and failed states; rows given, loaded once, or a page per query from a server; page size, sort, and the columns remembered with `stateKey` |
|
|
129
|
+
| `DataTable`, `useTable` | `nexus-shared/client` | Rows with a search (rows per page inside it), sorting by column, pages, a selection with one split button of actions over the headings, icon buttons and a menu per row, columns the user orders, resizes, pins, and hides, a band of headings over the columns that belong together (`group`), an options menu (CSV, print, import), and loading, empty, and failed states; rows given, loaded once, or a page per query from a server; page size, sort, and the columns remembered with `stateKey` |
|
|
130
130
|
| `TableStore` | `nexus-shared` | The table's state and handle, outside React: `reload`, `upsert`, `patch`, `remove`, `select`, `getSelected`, `setSearch`, `setSort`, `setPage` |
|
|
131
|
-
| `Crud` | `nexus-shared/client` | A module's records in one component: a controller's endpoints, the table's columns, and the form's input params in; the lists (all, flagged,
|
|
131
|
+
| `Crud` | `nexus-shared/client` | A module's records in one component: a controller's endpoints, the table's columns, and the form's input params in; the lists (all, flagged, trash, deleted) and actions the user may use, the records the user pinned above the list, Add and Edit popups on `ApiForm`, details, actions on one record or many, and a cache of lists while the page is open |
|
|
132
132
|
| `Permissions`, `NexusPermissions`, `definePermissions`, `canCall` | `nexus-shared` | May the user call this endpoint? The app registers its rules once; `NexusPermissions` reads the Nexus backend's access actions. Everything is allowed until then |
|
|
133
133
|
| `CrudBackend`, `NexusCrudBackend`, `defineCrudBackend` | `nexus-shared` | How a CRUD talks to a backend beyond its endpoints: page requests and answers, mass action bodies, flag and pin marks. Nexus's by default |
|
|
134
134
|
| `configureCrud`, `getCrudSettings` | `nexus-shared` | What every CRUD in the app follows: how many records a user may pin (100 by default) |
|
|
135
135
|
| `queryRows`, `formatCellText`, `sortRows`, `pageButtons`, `CrudCache`, `crudRowActions` | `nexus-shared` | Pure helpers behind the table and the CRUD: search, sort, and page rows as the table does, a value as its cell writes it, the page buttons, the cache, and which actions a list offers |
|
|
136
|
-
| `readApiResponse`, `sendApiRequest`, `matchFieldErrors` | `nexus-shared` | Pure helpers behind the API form: read an answer as success or failure, send a URL as it is,
|
|
136
|
+
| `readApiResponse`, `readErrorMessages`, `sendApiRequest`, `matchFieldErrors`, `matchInputName`, `formErrorsFromResult` | `nexus-shared` | Pure helpers behind the API form: read an answer as success or failure, every problem it carries as one list, send a URL as it is, match server field names to inputs, and turn a failed answer into what a form shows |
|
|
137
137
|
| `api`, `apiOptionsLoader`, `configureApi` | `nexus-shared/client` | API calls from the browser, through the app's proxy or straight to a module: toasts worded by the message builder, `api.confirm` to ask first, one request for the same GET at once; a dropdown's `loadOptions` from a pagination endpoint |
|
|
138
138
|
| `serverApi`, `configureServerApi`, `createApiProxy` | `nexus-shared/server` | API calls from the server, straight to each module's root, with the user's token and Next.js caching; the proxy route the browser calls |
|
|
139
139
|
| `ApiModules`, `NexusApiModules`, `defineApiModules`, `ApiRegister` | `nexus-shared` | An app's API modules: its own module enum (named once in `ApiRegister`, so endpoints take only those), where each root is, and the proxy on or off. Extend `ApiModules` for another backend |
|
|
@@ -179,6 +179,10 @@ import { TabButtons } from "nexus-shared/client";
|
|
|
179
179
|
`icon`, a `description` on a second line, and a `badge` (a count) at the end. The selected row is tinted, marked
|
|
180
180
|
by a bar at its start, and its label turns bold; `tone="primary"` tints it with the primary color. ↑ and ↓ move
|
|
181
181
|
the selection. The nav design is the old project's `nav-tabs`, this one its `list-tabs`.
|
|
182
|
+
- `multiple`: several at once. `value` is an array and `onChange` hands back the chosen values in option order.
|
|
183
|
+
Each chosen option is raised as the one choice's indicator is, the options wrap onto more rows when they do not
|
|
184
|
+
fit (never narrower than tall, so single letters line up), and on a touch screen each grows to a medium control's
|
|
185
|
+
height. They are checkboxes: Tab moves between them and Space toggles one.
|
|
182
186
|
- Native radio buttons: arrow keys move the selection, and `name` makes it part of a form. Needs `aria-label` or `aria-labelledby`.
|
|
183
187
|
`form` sets the form the choice belongs to, as in HTML; an id that matches no form keeps a switch that is not a
|
|
184
188
|
value (the Markdown editor's views) out of the form around it.
|
|
@@ -715,9 +719,14 @@ const ROOM_INPUTS: InputParams[] = [
|
|
|
715
719
|
- **One save at a time.** Save waits with a spinner; another click, Enter, or Ctrl+S does nothing until it ends. A save
|
|
716
720
|
makes the values the form's own ("Unsaved changes" clears; values typed meanwhile stay changes). `onSubmit` returns
|
|
717
721
|
`{ ok: false, errors, message }` to say why not, and a thrown error shows its message.
|
|
718
|
-
- **Server messages.**
|
|
719
|
-
|
|
720
|
-
|
|
722
|
+
- **Server messages.** The server's own words are what the user reads. `ApiForm` shows **every** message a refusal
|
|
723
|
+
carried - a Nexus answer's `errorMessages` (a list, or one message object, or one message and its codes at the top
|
|
724
|
+
level) and ASP.NET Core problem details (`errors` by field) - each on the input its `inputName` names, matched without
|
|
725
|
+
case and by the first part of a path (`Addresses[1].City`). Names that match no input of this form, messages that name
|
|
726
|
+
none, and failures without fields show beside the buttons; nothing is swallowed and nothing is shown twice. A sentence
|
|
727
|
+
of ours by HTTP status ("Someone else changed this record…") is the fallback for an answer that explained nothing.
|
|
728
|
+
`messageCode` and `errorCode` ride along in `answer.errorMessages` for the log and for `onError` to react to, and are
|
|
729
|
+
never shown as the sentence. An input's message clears when its value changes.
|
|
721
730
|
- **Nothing lost.** `draftKey` keeps unsaved changes in IndexedDB (after a 600 ms pause, and at once when the page hides
|
|
722
731
|
or the form leaves) and brings them back with a note and Discard; drafts changed on the server since are named. Cancel
|
|
723
732
|
asks before it drops changes, and the page asks before it closes with some (`confirmLeave`, on without a draft).
|
|
@@ -755,6 +764,10 @@ export const { GET, HEAD, POST, PUT, PATCH, DELETE } = createApiProxy({ token: r
|
|
|
755
764
|
an endpoint has registered the modules on either side. `NexusApiModules` is the Nexus set (roots from `API_<MODULE>`,
|
|
756
765
|
as in the previous project; `NexusModule.None` for the app's own `/api` routes) and runs when nothing is registered.
|
|
757
766
|
The list is checked: keys and values must be unique, and keys fit in a URL.
|
|
767
|
+
- **`NexusModule`'s numbers are the backend's `EModules` flags**, and a permission is a bitmask of them, so a value
|
|
768
|
+
that disagrees with the backend by one bit grants or denies the wrong module in silence. They changed after
|
|
769
|
+
2.0.0: `Chat` took bit 6 and `Accounting` through `Portfolio` each moved up one. Code that uses the names needs
|
|
770
|
+
nothing; anything holding a stored or hard-coded **number** does. [CHANGELOG.md](CHANGELOG.md) has the table.
|
|
758
771
|
- **Module types from one place.** An app names its enum once, next to `defineApiModules`:
|
|
759
772
|
`declare module "nexus-shared" { interface ApiRegister { module: HrModule } }`. Every endpoint, call, `ApiForm`,
|
|
760
773
|
and CRUD then takes only those modules (`AppModule`), so `{ module: NexusModule.Reservation, … }` in an HR app is a
|
|
@@ -766,11 +779,37 @@ export const { GET, HEAD, POST, PUT, PATCH, DELETE } = createApiProxy({ token: r
|
|
|
766
779
|
`proxies(module)` mixes the two, such as uploads straight to the Drive module and the rest through the proxy.
|
|
767
780
|
- **One engine, two clients.** `api` and `serverApi` are two settings of `createApiClient`: it fills `{id}` in the path
|
|
768
781
|
from `params` or the body, adds the query, headers, and token, waits up to `timeout` (60 s), tries a failed GET once
|
|
769
|
-
more (a save never twice), and reads every answer as an `ApiResult` (`ok`, `status`, `result`, `message`,
|
|
770
|
-
`messageCode`, `errorCode`, `network`, `timeout`, `aborted`). A failed call is an answer,
|
|
771
|
-
shows nothing. Next.js's own signals (`headers()` while prerendering, `redirect()`) pass
|
|
782
|
+
more (a save never twice), and reads every answer as an `ApiResult` (`ok`, `status`, `result`, `message`,
|
|
783
|
+
`errorMessages`, `errors`, `messageCode`, `errorCode`, `network`, `timeout`, `aborted`). A failed call is an answer,
|
|
784
|
+
never a throw; an aborted one shows nothing. Next.js's own signals (`headers()` while prerendering, `redirect()`) pass
|
|
785
|
+
through untouched.
|
|
786
|
+
- **What the server said wins.** A refusal is shown in the backend's own words whenever it sent any:
|
|
787
|
+
`{ isSuccess, code, errorMessages: [{ message, messageCode, errorCode, inputName }] }` becomes
|
|
788
|
+
`result.errorMessages` in that order (one message object instead of a list, a single `errorMessage`, and an older
|
|
789
|
+
answer's one message at the top level all read into the same list), `result.errors` holds the ones that name an input,
|
|
790
|
+
and `result.message` is all of them joined for a toast. `messageCode` and `errorCode` are for the log and for a page to
|
|
791
|
+
react to - never the sentence a user reads. The messages built from the HTTP status are the fallback for an answer that
|
|
792
|
+
explained nothing.
|
|
793
|
+
- **Answers of another shape.** Nexus answers and ASP.NET Core problem
|
|
794
|
+
details are read out of the box. A backend that answers its own envelope registers one reader for the whole app:
|
|
795
|
+
|
|
796
|
+
```ts
|
|
797
|
+
// Reads { success, message, data, error: { code, message, fields } } — the answer becomes ok, result, message, errors
|
|
798
|
+
const read: ApiResponseReader = (body, status) => {
|
|
799
|
+
const answer = body as BackendAnswer | null;
|
|
800
|
+
if (answer?.success) return { ok: true, status, result: answer.data, message: answer.message };
|
|
801
|
+
const fields = Object.entries(answer?.error?.fields ?? {}).map(([name, list]) => [name, list.join(" ")]);
|
|
802
|
+
return { ok: false, status, message: answer?.error?.message, messageCode: answer?.error?.code, errors: Object.fromEntries(fields) };
|
|
803
|
+
};
|
|
804
|
+
configureApi({ read }); // the browser
|
|
805
|
+
configureServerApi({ read }); // and the server, so both sides read one envelope
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
It is the fallback of every call that does not bring a `read` of its own, which is what the components that call
|
|
809
|
+
inside themselves need: a `Crud`'s lists and actions and an `ApiForm`'s save have no place to pass one. A call's own
|
|
810
|
+
`read`, and an `ApiForm`'s `readResponse`, still win.
|
|
772
811
|
- **Toasts in the browser.** Errors always, successes for POST, PUT, PATCH, and DELETE; the same failure twice at once
|
|
773
|
-
shows once; `toast: false`, `true`, or `{ success, error, loading }` per call; `configureApi({ toast, headers,
|
|
812
|
+
shows once; `toast: false`, `true`, or `{ success, error, loading }` per call; `configureApi({ toast, headers, read,
|
|
774
813
|
onSignedOut })` for the app. The server shows nothing and logs network failures, timeouts, and 5xx.
|
|
775
814
|
- **One message builder.** `buildMessage(kind, { action, subject, count, status })` words every message, and
|
|
776
815
|
`messages.success`, `.failure`, `.reason`, `.loading`, `.confirm`, `.label`, and `.forResult` are short ways to call
|
|
@@ -778,8 +817,8 @@ export const { GET, HEAD, POST, PUT, PATCH, DELETE } = createApiProxy({ token: r
|
|
|
778
817
|
texts and action words are one table: `configureMessages` rewords or translates all of them at once, and an action
|
|
779
818
|
of the app's own is its words (`{ verb: "check in", done: "checked in", doing: "Checking in", object: "the guest" }`).
|
|
780
819
|
- **A controller's lists.** `apiController` names the lists as the Nexus backend does: `listing` and `pagination` are the
|
|
781
|
-
active records' (`NormalListing`, `NormalPagination`), and `lists.flag`, `lists.trash`, `pages.
|
|
782
|
-
(`FlagListing`, `TrashListing`, `
|
|
820
|
+
active records' (`NormalListing`, `NormalPagination`), and `lists.flag`, `lists.trash`, `pages.trash`, … the others
|
|
821
|
+
(`FlagListing`, `TrashListing`, `TrashPagination`, …), for the CRUD component.
|
|
783
822
|
- `npm run test:api` checks the builder, routes, modules (another app's enum, the proxy on and off), the engine, the
|
|
784
823
|
server client, and the proxy; `scripts/browser-checks/api-calls.cjs` in the template checks `/demo/api`.
|
|
785
824
|
|
|
@@ -792,7 +831,8 @@ import { DataTable, useTable } from "nexus-shared/client";
|
|
|
792
831
|
const COLUMNS: TableColumn<Booking>[] = [
|
|
793
832
|
{ key: "no", label: "Booking", width: "7rem", movable: false, pinnable: false }, // stays first, and stays put
|
|
794
833
|
{ key: "guest.name", label: "Guest" }, // a path into the row
|
|
795
|
-
{ key: "amount", label: "Amount", format: "money", prefix: "Rs. " },
|
|
834
|
+
{ key: "amount", label: "Amount", format: "money", prefix: "Rs. ", group: "Amount details" }, // number, money, percent, date, …
|
|
835
|
+
{ key: "due", label: "Due", format: "money", prefix: "Rs. ", group: "Amount details" }, // one heading over both: the header is two rows
|
|
796
836
|
{ key: "status", label: "Status", options: [{ value: 1, label: "Confirmed", tone: "success" }] }, // a tag
|
|
797
837
|
{ key: "paid", label: "Paid", format: "boolean", resizable: false }, // keeps its width
|
|
798
838
|
{ key: "created", label: "Booked", format: "date-time", hidden: true }, // shown from the Columns menu
|
|
@@ -806,11 +846,19 @@ const table = useTable<Booking>();
|
|
|
806
846
|
table.upsert(saved); table.patch(7, { paid: true }, { flash: true }); table.remove([7, 8]); await table.reload();
|
|
807
847
|
```
|
|
808
848
|
|
|
809
|
-
- **Columns are params**, one plain object each, as inputs are: `key` (a field or a path), `label`, `
|
|
810
|
-
`prefix`, `suffix`, `options` (labels for an enum's values; a `tone` draws a tag with a dot of its color),
|
|
811
|
-
`render`, `text`, `width`, `minWidth`, `align`, `wrap`, `sortable`, `sortKey`, `searchable`, `hidden`,
|
|
812
|
-
`movable`, `resizable`, `pinnable`, `pinned`. Numbers line up at the end in tabular figures; dates and
|
|
813
|
-
user's settings; moments show in the user's zone once the page has hydrated; long text ends in … at
|
|
849
|
+
- **Columns are params**, one plain object each, as inputs are: `key` (a field or a path), `label`, `group`, `format`,
|
|
850
|
+
`decimals`, `prefix`, `suffix`, `options` (labels for an enum's values; a `tone` draws a tag with a dot of its color),
|
|
851
|
+
`value`, `render`, `text`, `width`, `minWidth`, `align`, `wrap`, `sortable`, `sortKey`, `searchable`, `hidden`,
|
|
852
|
+
`hideable`, `movable`, `resizable`, `pinnable`, `pinned`. Numbers line up at the end in tabular figures; dates and
|
|
853
|
+
times follow the user's settings; moments show in the user's zone once the page has hydrated; long text ends in … at
|
|
854
|
+
the column's width.
|
|
855
|
+
- **Columns in bands.** `group` is the heading above a column (`{ key: "amount", label: "Total", group: "Amount
|
|
856
|
+
details" }`); the columns next to each other that name the same one share it, and the header reads in two rows, the
|
|
857
|
+
bands on top and the columns' own headings under them. A column with no group keeps one heading as tall as both rows,
|
|
858
|
+
and so do the numbers and checkbox column and the actions. A band is a heading and nothing more: it sorts nothing, has
|
|
859
|
+
no menu and no edge to drag, and the columns stay one flat list, so hiding, ordering, widths, pins and the export are
|
|
860
|
+
as they were. The bands are read off the order on screen: a column dragged out of its run takes its band's heading
|
|
861
|
+
with it, and the run it left draws that heading again. With no group anywhere the header is one row.
|
|
814
862
|
- **Rows three ways.** Given (`rows`) or loaded once (`load`), the table searches, sorts, and pages them itself: every word
|
|
815
863
|
must match, in any column, with or without accents, and 10,000 rows search in about 0.5 ms a keystroke. A page at a time
|
|
816
864
|
(`load` with `paging="server"`), each search (after 300 ms without typing, or Enter), sort, and page is one request; a
|
|
@@ -865,7 +913,7 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
865
913
|
/>
|
|
866
914
|
```
|
|
867
915
|
|
|
868
|
-
- **Lists.** All (`NormalListing` or `NormalPagination`), Flagged,
|
|
916
|
+
- **Lists.** All (`NormalListing` or `NormalPagination`), Flagged, Trash, and Deleted, as icon tabs with the
|
|
869
917
|
chosen one named. A list shows when it is in `modes`, has its endpoint, and the user may call it.
|
|
870
918
|
- **Pinned records.** `PinListing` answers the records the user pinned: they sit in a section above the active list that
|
|
871
919
|
folds open and shut, and in the list and its pages as well. One limit holds for every CRUD in the app
|
|
@@ -873,13 +921,12 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
873
921
|
in the order they were picked, and says so; lowering the limit never unpins anything, so a user over it can still
|
|
874
922
|
unpin and can pin again once they are under it.
|
|
875
923
|
- **Actions.** On a record in the lists of active records: View details, Edit (an icon on the row), Flag, Pin,
|
|
876
|
-
|
|
877
|
-
back to trash. On selected records: the same, plus a new Description for all of them. Each shows only when it is in
|
|
924
|
+
Move to trash; in the trash: Recover, Delete permanently; in the deleted list: Move back to trash. On selected records: the same, plus a new Description for all of them. Each shows only when it is in
|
|
878
925
|
`actions`, has its endpoint, and is allowed. A controller without a trash (no endpoint, or left out of `actions`) offers
|
|
879
926
|
Delete where Move to trash would be; a trash the user may not use never becomes Delete.
|
|
880
|
-
- **Questions and messages.**
|
|
881
|
-
undone."); Flag, Pin,
|
|
882
|
-
builder ("Sita Sharma
|
|
927
|
+
- **Questions and messages.** Move to trash and Delete ask first ("Move 3 guests to the trash?", "This cannot be
|
|
928
|
+
undone."); Flag, Pin, and Recover are undone by a click and do not ask. Toasts come from the message
|
|
929
|
+
builder ("Sita Sharma moved to the trash.", "3 guests unpinned.").
|
|
883
930
|
- **Rows from the answers.** A record that moves to another list leaves the one in view (and the pinned section); flags,
|
|
884
931
|
pins, and saved values change in place; a new record goes first; a server page loads again in the background to fill up. When a mass
|
|
885
932
|
answer lists the records it changed, only those change.
|
|
@@ -900,4 +947,54 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
900
947
|
- `scripts/browser-checks/crud.cjs` in the template checks `/demo/crud`: every list and action, the popups, the roles, the
|
|
901
948
|
cache, a phone, and contrast in every theme.
|
|
902
949
|
|
|
950
|
+
### Excel import and export
|
|
951
|
+
|
|
952
|
+
A model's records in and out of a spreadsheet, against the backend's Excel controller bases
|
|
953
|
+
(`NexusExcelController<TContract>` and `NexusExcelExportController<TContract>`): `Excel/Settings`, `Excel/Template`,
|
|
954
|
+
`Excel/Verify`, `Excel/Import`, and the model's own export action.
|
|
955
|
+
|
|
956
|
+
```tsx
|
|
957
|
+
export const CITIES = MODULES.controller(NexusModule.Configuration, "City", "city");
|
|
958
|
+
export const CITY_SHEET = excelEndpoints(CITIES, { subject: "city" }); // the four, and Excel/Export
|
|
959
|
+
|
|
960
|
+
openExcelImport({ endpoints: CITY_SHEET, subject: "city", onImported: reload }); // the panel as a window
|
|
961
|
+
<ExcelImport endpoints={CITY_SHEET} subject="city" onImported={reload} /> // or in the page
|
|
962
|
+
|
|
963
|
+
await downloadExcelTemplate(CITY_SHEET);
|
|
964
|
+
await exportExcel(CITY_SHEET, { subject: "city", rows: table.matches, query: { search, sort: "cityName", direction: "asc", filters } });
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
- **The template** is the backend's file, downloaded under the name its `Content-Disposition` gives it. Every list in it
|
|
968
|
+
is a dropdown and the ids beside a choice fill themselves, so the person types no code and pastes no id.
|
|
969
|
+
- **A verify writes nothing.** The file goes up as form data, and the answer is the rows, judged: what would be added,
|
|
970
|
+
what is already a record, and what is waiting on a fix.
|
|
971
|
+
- **The review is two collapsible editors** over those rows - **New rows** (which includes the rows to fix, since a row
|
|
972
|
+
with a problem is not yet a record) and **Already there** - built from the sheet's columns with the rules the sheet
|
|
973
|
+
told the person about, so a fix made here is checked the way the file was. What the server found wrong is listed under
|
|
974
|
+
the rows, each entry naming the cell as a spreadsheet does (`B7 - City name`) and taking the caret to it.
|
|
975
|
+
- **The import sends rows, never the file again**, or every fix made on screen would be lost; every check runs again on
|
|
976
|
+
the way in. **A duplicate is never updated**: one left alone is not sent at all, and editing one sends it as a new
|
|
977
|
+
record. An import writes what it can and names the rest; only one that wrote nothing at all is a failure.
|
|
978
|
+
- **A choice travels as its key**, out and back. The plain id in the sheet is for the person to read and is never read
|
|
979
|
+
back, so nothing copied out of a file or out of an answer can be posted to an endpoint as an id.
|
|
980
|
+
- **The export is the server's file**, written from the search, the sort and the filters on screen - not a CSV of the
|
|
981
|
+
rows this browser holds. The row limit is the backend's (`Excel/Settings`, 5,000 by default, kept for the page's life);
|
|
982
|
+
over it the person is asked first, and the answer's own headers say what was written and what was left out.
|
|
983
|
+
- **Generic, like the rest.** `ExcelBackend` is the class an app on another backend registers
|
|
984
|
+
(`defineExcelBackend(new MyExcelBackend())`): the request shapes, how the answers read, the upload field, and the names
|
|
985
|
+
of a download's headers. `NexusExcelBackend` is the default. `readExcelResponse` reads a refusal's `errorMessages`, so
|
|
986
|
+
"no city could be written" reaches the person in the backend's own words.
|
|
987
|
+
- **A column draws its own input**: text, note, whole number, decimal, amount, date, time, Yes/No, and a choice shown
|
|
988
|
+
read-only with the words the person picked. A **date and time** is a text box, not a picker: the sheet's moment is a
|
|
989
|
+
zone-less wall clock while a picker's value is a UTC moment, so a picker would shift every value by the reader's offset.
|
|
990
|
+
- `scripts/test-excel.mts` checks the endpoints, the answers, the inputs and the round trip; the template's
|
|
991
|
+
`scripts/browser-checks/excel.cjs` checks `/demo/excel`: every state, the two rules about the wire, both downloads, the
|
|
992
|
+
warning before a capped export, a phone, and contrast in every theme.
|
|
993
|
+
|
|
994
|
+
In a table, `excel={{ endpoints: CITY_SHEET, subject: "city" }}` is the whole of it: the options menu (⋮) then offers
|
|
995
|
+
**Download template**, **Import from a spreadsheet** (the panel as a window, after which the table reads its rows
|
|
996
|
+
again) and **Export to Excel**, which sends the table's own search and sort with the page's `filters()`. A `Crud`
|
|
997
|
+
passes it through as `table={{ excel }}` and drops the lists it had kept once an import has written. It replaces
|
|
998
|
+
`onImport`, which stays for a page that imports its own way.
|
|
999
|
+
|
|
903
1000
|
Planned, following the previous Nexus project: a page status layout, and in the CRUD, audit logs in the details view.
|
package/dist/Client.Index.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
export * from "./Components/Chats/Chat.tsx";
|
|
2
|
+
export * from "./Components/Chats/ChatButton.tsx";
|
|
3
|
+
export * from "./Components/Chats/ChatComposer.tsx";
|
|
4
|
+
export * from "./Components/Chats/ChatConversations.tsx";
|
|
5
|
+
export * from "./Components/Chats/ChatPanel.tsx";
|
|
6
|
+
export * from "./Components/Chats/ChatParts.tsx";
|
|
7
|
+
export * from "./Components/Chats/ChatThread.tsx";
|
|
1
8
|
export * from "./Components/Documents/Menu.tsx";
|
|
2
9
|
export * from "./Components/Documents/SplitButton.tsx";
|
|
3
10
|
export * from "./Components/Documents/TabButtons.tsx";
|
|
4
11
|
export * from "./Components/Forms/ApiForm.tsx";
|
|
5
12
|
export * from "./Components/Forms/Crud.tsx";
|
|
6
13
|
export * from "./Components/Forms/CrudParts.tsx";
|
|
14
|
+
export * from "./Components/Forms/ExcelImport.tsx";
|
|
7
15
|
export * from "./Components/Forms/Form.tsx";
|
|
8
16
|
export * from "./Components/Forms/SubmitForm.tsx";
|
|
9
17
|
export * from "./Components/Inputs/Calendar.tsx";
|
|
@@ -37,8 +45,11 @@ export * from "./Components/Layouts/ThemeSwitcher.tsx";
|
|
|
37
45
|
export * from "./Components/Layouts/Toaster.tsx";
|
|
38
46
|
export * from "./Components/Viewers/DataTable.tsx";
|
|
39
47
|
export * from "./Components/Viewers/DataTableParts.tsx";
|
|
48
|
+
export * from "./Helpers/ChatHooks.ts";
|
|
40
49
|
export * from "./Helpers/DatePreferences.ts";
|
|
41
50
|
export * from "./Helpers/DragHelpers.ts";
|
|
42
51
|
export * from "./Helpers/PopoverHelpers.ts";
|
|
43
52
|
export * from "./Services/BrowserApi.ts";
|
|
53
|
+
export * from "./Services/ChatApi.ts";
|
|
54
|
+
export * from "./Services/ExcelApi.ts";
|
|
44
55
|
export * from "./Services/ThemeService.ts";
|
package/dist/Client.Index.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
// Client components and browser services: files here start with "use client" or use browser APIs.
|
|
2
2
|
// Import from "nexus-shared/client".
|
|
3
3
|
// Components
|
|
4
|
+
export * from "./Components/Chats/Chat.js";
|
|
5
|
+
export * from "./Components/Chats/ChatButton.js";
|
|
6
|
+
export * from "./Components/Chats/ChatComposer.js";
|
|
7
|
+
export * from "./Components/Chats/ChatConversations.js";
|
|
8
|
+
export * from "./Components/Chats/ChatPanel.js";
|
|
9
|
+
export * from "./Components/Chats/ChatParts.js";
|
|
10
|
+
export * from "./Components/Chats/ChatThread.js";
|
|
4
11
|
export * from "./Components/Documents/Menu.js";
|
|
5
12
|
export * from "./Components/Documents/SplitButton.js";
|
|
6
13
|
export * from "./Components/Documents/TabButtons.js";
|
|
7
14
|
export * from "./Components/Forms/ApiForm.js";
|
|
8
15
|
export * from "./Components/Forms/Crud.js";
|
|
9
16
|
export * from "./Components/Forms/CrudParts.js";
|
|
17
|
+
export * from "./Components/Forms/ExcelImport.js";
|
|
10
18
|
export * from "./Components/Forms/Form.js";
|
|
11
19
|
export * from "./Components/Forms/SubmitForm.js";
|
|
12
20
|
export * from "./Components/Inputs/Calendar.js";
|
|
@@ -41,9 +49,12 @@ export * from "./Components/Layouts/Toaster.js";
|
|
|
41
49
|
export * from "./Components/Viewers/DataTable.js";
|
|
42
50
|
export * from "./Components/Viewers/DataTableParts.js";
|
|
43
51
|
// Helpers
|
|
52
|
+
export * from "./Helpers/ChatHooks.js";
|
|
44
53
|
export * from "./Helpers/DatePreferences.js";
|
|
45
54
|
export * from "./Helpers/DragHelpers.js";
|
|
46
55
|
export * from "./Helpers/PopoverHelpers.js";
|
|
47
56
|
// Services
|
|
48
57
|
export * from "./Services/BrowserApi.js";
|
|
58
|
+
export * from "./Services/ChatApi.js";
|
|
59
|
+
export * from "./Services/ExcelApi.js";
|
|
49
60
|
export * from "./Services/ThemeService.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ChatDomainValue } from "../../Interfaces/ChatInterfaces.ts";
|
|
3
|
+
import { type PopupSize } from "../Layouts/Popup.tsx";
|
|
4
|
+
import { type ChatPanelProps } from "./ChatPanel.tsx";
|
|
5
|
+
export interface ChatProps<TDomain extends ChatDomainValue = ChatDomainValue> extends Omit<ChatPanelProps<TDomain>, "scope" | "header" | "enabled" | "height"> {
|
|
6
|
+
/** Which kind of record: `EDomainTypes.Leave`. */
|
|
7
|
+
domain: TDomain;
|
|
8
|
+
/** The record's own id. */
|
|
9
|
+
parentId: string | number;
|
|
10
|
+
/** Who is reading and writing. */
|
|
11
|
+
userId: string | number;
|
|
12
|
+
/** What the icon is called, in its tooltip and for a screen reader. Default: the domain's name, else "Discussion". */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** Default `md`, as the other controls in a row; `sm` for a dense table. */
|
|
15
|
+
buttonSize?: "sm" | "md";
|
|
16
|
+
/** How wide the popup is. Default `md` (560px). */
|
|
17
|
+
popupSize?: PopupSize;
|
|
18
|
+
/** How tall the conversation is inside the popup. Default `min(60vh, 520px)`. */
|
|
19
|
+
height?: number | string;
|
|
20
|
+
/** The popup's title bar. Default: the panel's `title`, else the domain's name and the record's id. */
|
|
21
|
+
popupTitle?: ReactNode;
|
|
22
|
+
/** Opened or closed. */
|
|
23
|
+
onOpenChange?: (open: boolean) => void;
|
|
24
|
+
/** Class of the trigger icon. */
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
/** The chat icon for a record, and the conversation in a popup. */
|
|
28
|
+
export declare function Chat<TDomain extends ChatDomainValue = ChatDomainValue>({ domain, parentId, userId, label, buttonSize, popupSize, height, popupTitle, onOpenChange, className, summary, title, description, ...panel }: ChatProps<TDomain>): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconMessageCircle, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { chatDomainLabel } from "../../Helpers/ChatBackend.js";
|
|
6
|
+
import { Popup } from "../Layouts/Popup.js";
|
|
7
|
+
import { ChatButton } from "./ChatButton.js";
|
|
8
|
+
import { ChatPanel } from "./ChatPanel.js";
|
|
9
|
+
/** The chat icon for a record, and the conversation in a popup. */
|
|
10
|
+
export function Chat({ domain, parentId, userId, label, buttonSize = "md", popupSize = "md", height, popupTitle, onOpenChange, className, summary, title, description, ...panel }) {
|
|
11
|
+
const [open, setOpen] = useState(false);
|
|
12
|
+
const scope = { domain, parentId, userId };
|
|
13
|
+
const named = title ?? (chatDomainLabel(domain) ? `${chatDomainLabel(domain)} · #${String(parentId)}` : `Discussion · #${String(parentId)}`);
|
|
14
|
+
const change = (next) => {
|
|
15
|
+
setOpen(next);
|
|
16
|
+
onOpenChange?.(next);
|
|
17
|
+
};
|
|
18
|
+
return (_jsxs(_Fragment, { children: [_jsx(ChatButton, { scope: scope, summary: summary, label: label ?? chatDomainLabel(domain) ?? "Discussion", size: buttonSize, className: className, onClick: () => change(true) }), _jsx(Popup, { open: open, onClose: () => change(false), title: popupTitle ?? named, description: description, icon: _jsx(InlineIcon, { icon: iconMessageCircle }), variant: "window", size: popupSize, reopen: "restore", flush: true, closeOnBackdrop: false, children: _jsx(ChatPanel, { ...panel, scope: scope, summary: summary, title: named, header: false, enabled: open, height: height ?? "100%" }) })] }));
|
|
19
|
+
}
|