@cratis/pi 0.0.1
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/README.md +43 -0
- package/package.json +24 -0
- package/skills/cratis-application-react-specifications/LICENSE +2 -0
- package/skills/cratis-application-react-specifications/SKILL.md +192 -0
- package/skills/cratis-application-slice-specifications/LICENSE +2 -0
- package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
- package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
- package/skills/cratis-arc-command/LICENSE +2 -0
- package/skills/cratis-arc-command/SKILL.md +286 -0
- package/skills/cratis-arc-command/references/command-result.md +111 -0
- package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
- package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
- package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
- package/skills/cratis-arc-command-execution/LICENSE +2 -0
- package/skills/cratis-arc-command-execution/SKILL.md +148 -0
- package/skills/cratis-arc-command-validation/LICENSE +2 -0
- package/skills/cratis-arc-command-validation/SKILL.md +230 -0
- package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
- package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
- package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
- package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
- package/skills/cratis-arc-query-paging/LICENSE +2 -0
- package/skills/cratis-arc-query-paging/SKILL.md +173 -0
- package/skills/cratis-arc-react-page/LICENSE +2 -0
- package/skills/cratis-arc-react-page/SKILL.md +291 -0
- package/skills/cratis-arc-react-page/references/data-page.md +125 -0
- package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
- package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
- package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
- package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
- package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
- package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
- package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
- package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
- package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
- package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
- package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
- package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
- package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
- package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
- package/skills/cratis-chronicle-compliance/LICENSE +2 -0
- package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
- package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
- package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
- package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
- package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
- package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
- package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
- package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
- package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
- package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
- package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
- package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
- package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
- package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
- package/skills/cratis-chronicle-projection/LICENSE +2 -0
- package/skills/cratis-chronicle-projection/SKILL.md +162 -0
- package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
- package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
- package/skills/cratis-chronicle-reactor/LICENSE +2 -0
- package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
- package/skills/cratis-chronicle-read-model/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
- package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
- package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
- package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
- package/skills/cratis-chronicle-reducer/LICENSE +2 -0
- package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
- package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
- package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
- package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
- package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
- package/skills/cratis-code-review/LICENSE +2 -0
- package/skills/cratis-code-review/SKILL.md +177 -0
- package/skills/cratis-components-accessibility/LICENSE +2 -0
- package/skills/cratis-components-accessibility/SKILL.md +176 -0
- package/skills/cratis-components-schema-editor/LICENSE +2 -0
- package/skills/cratis-components-schema-editor/SKILL.md +238 -0
- package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
- package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
- package/skills/cratis-components-styling/LICENSE +2 -0
- package/skills/cratis-components-styling/SKILL.md +221 -0
- package/skills/cratis-components-toolbar/LICENSE +2 -0
- package/skills/cratis-components-toolbar/SKILL.md +259 -0
- package/skills/cratis-documentation-writing/LICENSE +2 -0
- package/skills/cratis-documentation-writing/SKILL.md +121 -0
- package/skills/cratis-event-model-diagram/LICENSE +2 -0
- package/skills/cratis-event-model-diagram/SKILL.md +142 -0
- package/skills/cratis-fundamentals-concept/LICENSE +2 -0
- package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
- package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
- package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
- package/skills/cratis-governed-release-methodology/LICENSE +2 -0
- package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
- package/skills/cratis-lens-browser-extension/LICENSE +2 -0
- package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
- package/skills/cratis-performance-review/LICENSE +2 -0
- package/skills/cratis-performance-review/SKILL.md +139 -0
- package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
- package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
- package/skills/cratis-security-review/LICENSE +2 -0
- package/skills/cratis-security-review/SKILL.md +141 -0
- package/skills/cratis-specification-by-example/LICENSE +2 -0
- package/skills/cratis-specification-by-example/SKILL.md +188 -0
- package/skills/cratis-specifications-csharp/LICENSE +2 -0
- package/skills/cratis-specifications-csharp/SKILL.md +230 -0
- package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
- package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
- package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
- package/skills/cratis-specifications-typescript/LICENSE +2 -0
- package/skills/cratis-specifications-typescript/SKILL.md +202 -0
- package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
- package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
- package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
- package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
- package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
- package/skills/cratis-studio-mcp-safety-guidance/references/observational-tools.md +8 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Dialogs reference
|
|
2
|
+
|
|
3
|
+
The Cratis dialog wrappers own command execution, validation timing, busy
|
|
4
|
+
state, and footer buttons. Never import `Dialog` from `primereact/dialog`.
|
|
5
|
+
|
|
6
|
+
The **components** live in `@cratis/components`; the **hooks, enums, and
|
|
7
|
+
context** live in `@cratis/arc.react/dialogs`. `@cratis/components` exports no
|
|
8
|
+
dialog hooks at all.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { CommandDialog } from '@cratis/components/CommandDialog';
|
|
12
|
+
import { BusyIndicatorDialog, ConfirmationDialog, Dialog } from '@cratis/components/Dialogs';
|
|
13
|
+
import { DialogInitialFocus } from '@cratis/components/Dialogs';
|
|
14
|
+
import { InputTextField } from '@cratis/components/CommandForm';
|
|
15
|
+
import {
|
|
16
|
+
DialogButtons, DialogComponents, DialogResult, useDialog, useDialogContext,
|
|
17
|
+
} from '@cratis/arc.react/dialogs';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Choose the dialog
|
|
21
|
+
|
|
22
|
+
- Confirm runs a command → `CommandDialog`.
|
|
23
|
+
- Confirm returns data without a command → `Dialog`.
|
|
24
|
+
- Asking the user to confirm, or blocking them while work runs → do not build a
|
|
25
|
+
dialog; raise the host-registered one by hook.
|
|
26
|
+
- A command split over named steps → `StepperCommandDialog`, see the
|
|
27
|
+
**cratis-components-stepper-command-dialog** skill.
|
|
28
|
+
|
|
29
|
+
## Opening a dialog
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
const [AccountDialog, showAccountDialog] = useDialog(CreateAccountDialog);
|
|
33
|
+
// render <AccountDialog /> once in the tree
|
|
34
|
+
const [result, response] = await showAccountDialog({ accountId });
|
|
35
|
+
if (result === DialogResult.Ok) { /* ... */ }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`useDialog<TResponse, TInput>(Component)` returns a three-element tuple — the
|
|
39
|
+
wrapper component, the show function, and the dialog context. Destructuring the
|
|
40
|
+
first two is the normal case. `showDialog(input?)` resolves to
|
|
41
|
+
`[DialogResult, TResponse?]` when the dialog closes.
|
|
42
|
+
|
|
43
|
+
Inside the dialog, prefer plain typed props for input and take `closeDialog`
|
|
44
|
+
from `useDialogContext<TResponse>()`:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
const { request, closeDialog } = useDialogContext<AccountCreated>();
|
|
48
|
+
closeDialog(DialogResult.Ok, response);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Dialogs that destructure `closeDialog` from a props interface extending
|
|
52
|
+
`DialogProps` remain valid.
|
|
53
|
+
|
|
54
|
+
## The enums
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
enum DialogResult { None = 0, Yes = 1, No = 2, Ok = 3, Cancelled = 4 }
|
|
58
|
+
enum DialogButtons { Ok = 1, OkCancel = 2, YesNo = 3, YesNoCancel = 4 }
|
|
59
|
+
enum DialogInitialFocus { Confirm = 1, Cancel = 2, Content = 3 }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Branch on the enum member, never on button text.
|
|
63
|
+
|
|
64
|
+
## `CommandDialog`
|
|
65
|
+
|
|
66
|
+
`CommandDialog` combines the command form and the dialog: its props are every
|
|
67
|
+
`Dialog` prop except `children`, plus every command-form prop except `children`
|
|
68
|
+
and `onBeforeExecute`, plus its own `onBeforeExecute` and `children`. `buttons`
|
|
69
|
+
defaults to `DialogButtons.OkCancel`.
|
|
70
|
+
|
|
71
|
+
The most-used props:
|
|
72
|
+
|
|
73
|
+
| Prop | Purpose |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| `command` | the generated command class |
|
|
76
|
+
| `title` | dialog header |
|
|
77
|
+
| `okLabel` / `cancelLabel` | button labels (`'Ok'` / `'Cancel'` by default) |
|
|
78
|
+
| `initialValues` | synchronous baseline; also the change-tracking baseline |
|
|
79
|
+
| `currentValues` | reactive overlay for late-loading values |
|
|
80
|
+
| `isValid` | extra validity gate on top of field validation |
|
|
81
|
+
| `onBeforeExecute` | transform values just before execute |
|
|
82
|
+
| `onSuccess` | receives the typed command response after a successful execute |
|
|
83
|
+
| `onValidationFailure` / `onFailed` | failure branches |
|
|
84
|
+
| `onConfirm` / `onCancel` / `onClose` | close gates |
|
|
85
|
+
| `validateOn` | `'blur' \| 'change' \| 'both'` |
|
|
86
|
+
| `validateOnInit` | validate on mount so pre-filled invalid values show immediately |
|
|
87
|
+
| `initialFocus` | which control receives focus when the dialog opens |
|
|
88
|
+
| `dismissable` | X, Escape, and backdrop dismissal |
|
|
89
|
+
| `pt` / `ptOptions` / `unstyled` | PrimeReact pass-through |
|
|
90
|
+
|
|
91
|
+
`CommandDialog.Column` exists for multi-column layouts:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1.5rem' }}>
|
|
95
|
+
<CommandDialog.Column>
|
|
96
|
+
<InputTextField value={(c: UpdateProfile) => c.firstName} title='First name' />
|
|
97
|
+
</CommandDialog.Column>
|
|
98
|
+
<CommandDialog.Column>
|
|
99
|
+
<InputTextField value={(c: UpdateProfile) => c.lastName} title='Last name' />
|
|
100
|
+
</CommandDialog.Column>
|
|
101
|
+
</div>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Execution flow
|
|
105
|
+
|
|
106
|
+
On confirm, `CommandDialog` applies `onBeforeExecute`, sets busy, executes the
|
|
107
|
+
command, and on failure calls `onValidationFailure(result.validationResults)`
|
|
108
|
+
or `onFailed(result)` and **keeps the dialog open**. On success it calls
|
|
109
|
+
`onSuccess(result.response)` and then the close gate.
|
|
110
|
+
|
|
111
|
+
### `initialValues` versus `onBeforeExecute`
|
|
112
|
+
|
|
113
|
+
Validation runs against the pre-transform values, so a required value seeded in
|
|
114
|
+
`onBeforeExecute` never makes the form valid and the submit button stays
|
|
115
|
+
permanently disabled. Seed required values with `initialValues`; reserve
|
|
116
|
+
`onBeforeExecute` for transforms that do not affect validity, such as a
|
|
117
|
+
generated id.
|
|
118
|
+
|
|
119
|
+
`onBeforeExecute` is a **transformer** — it receives the current values and must
|
|
120
|
+
return them (a `TCommand` or a `Promise<TCommand>`). Returning `undefined`
|
|
121
|
+
keeps the current values and logs a console warning rather than executing with
|
|
122
|
+
undefined values, but it is still a bug: always return the values.
|
|
123
|
+
|
|
124
|
+
### `onSuccess` versus `onConfirm`
|
|
125
|
+
|
|
126
|
+
`onSuccess(response)` fires only after the command succeeds and carries the
|
|
127
|
+
typed response — use it to close, refresh, or toast. `onConfirm()` receives no
|
|
128
|
+
command result; it is a close gate that must return exactly `true` to let the
|
|
129
|
+
wrapper close. Do not use `onConfirm` as a result handler.
|
|
130
|
+
|
|
131
|
+
## `CommandForm` fields
|
|
132
|
+
|
|
133
|
+
Every user-entered command value must be bound through a `CommandForm` field
|
|
134
|
+
from `@cratis/components/CommandForm` (also published as
|
|
135
|
+
`@cratis/components/CommandForm/fields`, the same module). A raw PrimeReact
|
|
136
|
+
control bypasses the field wrapper, so validation never re-runs and the submit
|
|
137
|
+
button stays disabled.
|
|
138
|
+
|
|
139
|
+
The full catalog: `InputTextField`, `PasswordField`, `NumberField`,
|
|
140
|
+
`TextAreaField`, `CheckboxField`, `ToggleSwitchField`, `DropdownField`,
|
|
141
|
+
`MultiSelectField`, `RadioButtonField`, `RadioGroupField`, `CalendarField`,
|
|
142
|
+
`ChipsField`, `ColorPickerField`, `SliderField`, `RatingField`.
|
|
143
|
+
|
|
144
|
+
Shared field props: `value` (the accessor), `title` (the **label**),
|
|
145
|
+
`description`, `required`, `icon` (a `React.ReactElement`), `initialValue`,
|
|
146
|
+
`noInitialValue`, `populationKey`. `CheckboxField`, `ToggleSwitchField`,
|
|
147
|
+
`RadioButtonField`, and `RadioGroupField` additionally take their own `label`.
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<InputTextField<CreateAccount> value={c => c.name} title='Account name' placeholder='Acme' />
|
|
151
|
+
<NumberField<CreateAccount> value={c => c.limit} title='Limit' min={0} />
|
|
152
|
+
<DropdownField<CreateAccount> value={c => c.status} title='Status' options={statuses} optionLabel='name' optionValue='id' />
|
|
153
|
+
<CheckboxField<CreateAccount> value={c => c.isActive} label='Active' />
|
|
154
|
+
<CalendarField<CreateAccount> value={c => c.opensOn} title='Opens on' showIcon />
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`DropdownField` requires `options`, `optionLabel`, and `optionValue`;
|
|
158
|
+
`RadioGroupField` requires all three too and takes `layout='horizontal' | 'vertical'`;
|
|
159
|
+
`RadioButtonField` is one component per option and requires `buttonValue`.
|
|
160
|
+
|
|
161
|
+
The `value` accessor doubles as the binding and as type-checked field
|
|
162
|
+
selection — renaming a command property produces a compile error at every
|
|
163
|
+
binding.
|
|
164
|
+
|
|
165
|
+
`AutoCommandForm` renders a field per command property from the generated
|
|
166
|
+
property descriptors, with `exclude` for the ones you do not want:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
<AutoCommandForm command={RegisterInvoice} exclude={['invoiceId']} />
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## `Dialog` — data without a command
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
export const RenameDialog = () => {
|
|
176
|
+
const { closeDialog } = useDialogContext<{ name: string }>();
|
|
177
|
+
const [name, setName] = useState('');
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Dialog
|
|
181
|
+
title='Rename'
|
|
182
|
+
width='32rem'
|
|
183
|
+
isValid={name.trim().length > 0}
|
|
184
|
+
onConfirm={() => closeDialog(DialogResult.Ok, { name })}
|
|
185
|
+
onCancel={() => closeDialog(DialogResult.Cancelled)}>
|
|
186
|
+
<InputText value={name} onChange={event => setName(event.target.value)} autoFocus />
|
|
187
|
+
</Dialog>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`DialogProps` defaults: `visible` `true`, `buttons` `DialogButtons.OkCancel`,
|
|
193
|
+
`initialFocus` `DialogInitialFocus.Confirm`, `width` `'450px'`, `isValid` true
|
|
194
|
+
when omitted, `isBusy` `false`, `okLabel` `'Ok'`, `cancelLabel` `'Cancel'`,
|
|
195
|
+
`yesLabel` `'Yes'`, `noLabel` `'No'`, `closeAriaLabel` `'Close'`.
|
|
196
|
+
|
|
197
|
+
Two behaviors worth knowing:
|
|
198
|
+
|
|
199
|
+
- `resizable` is accepted for compatibility but has **no effect** — the
|
|
200
|
+
PrimeReact 11 headless dialog has no resize.
|
|
201
|
+
- Passing a custom node to `buttons` instead of a `DialogButtons` member
|
|
202
|
+
removes the header close control, disables Escape, and means `onClose`,
|
|
203
|
+
`onCancel`, and `onConfirm` are never invoked. Close through
|
|
204
|
+
`useDialogContext().closeDialog(...)`, or set `dismissable` explicitly.
|
|
205
|
+
|
|
206
|
+
### Keeping the dialog open on failure
|
|
207
|
+
|
|
208
|
+
`onConfirm` must return exactly `true` to close. Returning `false` keeps it
|
|
209
|
+
open. Annotate the handler as `Promise<boolean>` so TypeScript does not infer
|
|
210
|
+
`Promise<false | void>`:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
const handleConfirm = async (): Promise<boolean> => {
|
|
214
|
+
const result = await command.execute();
|
|
215
|
+
return result.isSuccess;
|
|
216
|
+
};
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`onClose(result)` is the combined handler for both outcomes and closes unless it
|
|
220
|
+
returns `false`.
|
|
221
|
+
|
|
222
|
+
### Initial focus
|
|
223
|
+
|
|
224
|
+
`DialogInitialFocus.Confirm` is the default and *arms* the confirm button — a
|
|
225
|
+
browser fires `click` from the `keydown` of Enter, so a held Enter confirms.
|
|
226
|
+
For a destructive dialog use `DialogInitialFocus.Cancel`, which focuses the
|
|
227
|
+
least destructive action and degrades to `Content` when the button set has
|
|
228
|
+
nothing to dismiss with. `DialogInitialFocus.Content` focuses the title so
|
|
229
|
+
nothing is armed. There is deliberately no "focus nothing" option — a modal
|
|
230
|
+
must move focus into itself.
|
|
231
|
+
|
|
232
|
+
## Host-registered confirmation and busy indicators
|
|
233
|
+
|
|
234
|
+
`ConfirmationDialog` takes no props at all; it reads its title, message, and
|
|
235
|
+
buttons from the dialog request. `BusyIndicatorDialog` takes the busy-indicator
|
|
236
|
+
request. Register both once at the app root through `DialogComponents` and
|
|
237
|
+
raise them by hook — see the skill's step 5. From a view model, use the
|
|
238
|
+
injectable `IDialogs` abstraction instead of the hooks.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# MVVM reference
|
|
2
|
+
|
|
3
|
+
The Arc MVVM layer keeps page logic in plain TypeScript classes and leaves the
|
|
4
|
+
component declarative. `@cratis/arc.react.mvvm` builds on tsyringe for
|
|
5
|
+
injection and MobX for reactivity.
|
|
6
|
+
|
|
7
|
+
## When to use it
|
|
8
|
+
|
|
9
|
+
Extract a view model as soon as a component has three or more `useState` calls,
|
|
10
|
+
any state-synchronizing `useEffect`, derived values computed from other state,
|
|
11
|
+
or state shared by prop drilling. A trivial presentational leaf needs none.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
The app needs `experimentalDecorators`, `emitDecoratorMetadata`, and
|
|
16
|
+
`reflect-metadata` wired for the decorators to work.
|
|
17
|
+
|
|
18
|
+
Wrap the app (or the route root) in the MVVM provider — it configures MobX and
|
|
19
|
+
registers the shared bindings:
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { MVVM } from '@cratis/arc.react.mvvm';
|
|
23
|
+
|
|
24
|
+
<MVVM>
|
|
25
|
+
<App />
|
|
26
|
+
</MVVM>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`<Arc>` does **not** include `<MVVM>`; both are needed.
|
|
30
|
+
|
|
31
|
+
The provider's initialization registers `IMessenger`, `INavigation`,
|
|
32
|
+
`IIdentityProvider`, and `ILocalStorage` into the root container.
|
|
33
|
+
|
|
34
|
+
## The view model
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { injectable } from 'tsyringe';
|
|
38
|
+
|
|
39
|
+
@injectable()
|
|
40
|
+
export class AccountsViewModel {
|
|
41
|
+
selected: AccountSummary | null = null;
|
|
42
|
+
|
|
43
|
+
select(account: AccountSummary | null) { this.selected = account; }
|
|
44
|
+
|
|
45
|
+
get selectedName() { return this.selected?.name ?? ''; }
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `@injectable()` is tsyringe's — there is no Cratis-specific view-model
|
|
50
|
+
decorator.
|
|
51
|
+
- **`withViewModel` applies `makeAutoObservable` for you.** Do not call it in
|
|
52
|
+
the constructor and do not decorate plain assignments.
|
|
53
|
+
- Derived state is a getter, not a `useMemo` in the component.
|
|
54
|
+
- No JSX, no React state, no React hooks in a view model — inject the
|
|
55
|
+
abstraction instead.
|
|
56
|
+
- A non-trivial view model must be constructible directly in a spec.
|
|
57
|
+
|
|
58
|
+
## `withViewModel`
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { withViewModel } from '@cratis/arc.react.mvvm';
|
|
62
|
+
|
|
63
|
+
export const AccountsPage = withViewModel(AccountsViewModel, ({ viewModel, props }) => (
|
|
64
|
+
/* read viewModel state inside JSX; call viewModel methods from handlers */
|
|
65
|
+
));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The render function receives `{ viewModel, props }`. Behind the scenes
|
|
69
|
+
`withViewModel` creates a child DI container per component instance, registers
|
|
70
|
+
the props, route parameters, query parameters, dialog context, command scope,
|
|
71
|
+
query scope, messenger, and dialogs into it, resolves the view model, makes it
|
|
72
|
+
observable, and renders the body inside an observer.
|
|
73
|
+
|
|
74
|
+
Any query, observable query, or command resolved through that container is
|
|
75
|
+
automatically configured with the microservice, API base path, and origin from
|
|
76
|
+
the Arc context.
|
|
77
|
+
|
|
78
|
+
### Dereference late
|
|
79
|
+
|
|
80
|
+
Read `viewModel.property` **inside** JSX. Destructuring an observable at the top
|
|
81
|
+
of the component body captures the value and stops tracking changes.
|
|
82
|
+
|
|
83
|
+
`useCallback` and `useMemo` are unnecessary inside a `withViewModel` component —
|
|
84
|
+
the render is already wrapped in an observer, and their presence signals state
|
|
85
|
+
that belongs in the view model.
|
|
86
|
+
|
|
87
|
+
When passing an observable array into a raw non-observer child, materialize it
|
|
88
|
+
with `.slice()`. In practice prefer the observable data table, which handles
|
|
89
|
+
reactivity internally.
|
|
90
|
+
|
|
91
|
+
## Props, route parameters, and query parameters
|
|
92
|
+
|
|
93
|
+
| Need | Mechanism |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| Props set once at mount | `@props readonly componentProps: TProps` |
|
|
96
|
+
| Props that change after mount | implement `IHandleProps<TProps>` → `handleProps(props)` |
|
|
97
|
+
| Route parameters | `@params readonly routeParams: RouteParams` |
|
|
98
|
+
| Route parameters that change | implement `IHandleParams<T>` → `handleParams(params)` |
|
|
99
|
+
| Query-string parameters | `@queryParams readonly query: QueryParams` |
|
|
100
|
+
| Query parameters that change | implement `IHandleQueryParams<T>` → `handleQueryParams(queryParams)` |
|
|
101
|
+
|
|
102
|
+
The handler method names are `handleProps`, `handleParams`, and
|
|
103
|
+
`handleQueryParams`. Each is invoked on mount and again whenever the incoming
|
|
104
|
+
value actually differs.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
@injectable()
|
|
108
|
+
export class AccountDetailsViewModel implements IHandleProps<AccountDetailsProps> {
|
|
109
|
+
account: AccountSummary | null = null;
|
|
110
|
+
|
|
111
|
+
handleProps(props: AccountDetailsProps) { this.account = props.account; }
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Route and query parameters arrive from the URL as strings. `withViewModel`
|
|
116
|
+
deserializes them into the class you declared on the decorated constructor
|
|
117
|
+
parameter, so declare a real parameter class with typed properties rather than
|
|
118
|
+
an inline object type — that class is what drives the conversion.
|
|
119
|
+
|
|
120
|
+
## Injectable abstractions
|
|
121
|
+
|
|
122
|
+
Never touch a browser or React global from a view model.
|
|
123
|
+
|
|
124
|
+
| Abstraction | Package | Replaces |
|
|
125
|
+
| --- | --- | --- |
|
|
126
|
+
| `IMessenger` | `@cratis/arc.react.mvvm/messaging` | cross-component selection, not React context |
|
|
127
|
+
| `IDialogs` | `@cratis/arc.react.mvvm/dialogs` | imperative confirmation and busy dialogs |
|
|
128
|
+
| `IIdentityProvider` | `@cratis/arc/identity` | identity, instead of the `useIdentity` hook |
|
|
129
|
+
| `INavigation`, `ILocalStorage` | `@cratis/arc.react.mvvm/browser` | URL navigation, `localStorage` |
|
|
130
|
+
| `IViewModelDetached` | `@cratis/arc.react.mvvm` | teardown via `detached()` |
|
|
131
|
+
|
|
132
|
+
`IDialogs` exposes `show(input)`, `showConfirmation(title, message, buttons)`
|
|
133
|
+
returning a `DialogResult`, and `showBusyIndicator(title, message)` returning a
|
|
134
|
+
busy indicator with a `close()` method. For a user-triggered dialog that opens
|
|
135
|
+
a React command dialog, use `useDialog` in the component instead.
|
|
136
|
+
|
|
137
|
+
`IViewModelDetached.detached()` runs on unmount — unsubscribe subscriptions and
|
|
138
|
+
clear timers there.
|
|
139
|
+
|
|
140
|
+
## `observer`
|
|
141
|
+
|
|
142
|
+
Import `observer` from `@cratis/arc.react.mvvm`, never directly from
|
|
143
|
+
`mobx-react`. The package re-exports it deliberately so the observer boundary
|
|
144
|
+
stays a single, swappable dependency.
|
|
145
|
+
|
|
146
|
+
## Testing
|
|
147
|
+
|
|
148
|
+
A view model is a plain class: construct it directly, pass small typed fakes or
|
|
149
|
+
stubs for its dependencies, and assert on state transitions and getters. See the
|
|
150
|
+
**cratis-application-react-specifications** skill.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Queries and commands reference
|
|
2
|
+
|
|
3
|
+
A Debug build generates one typed proxy class per query and per command. The
|
|
4
|
+
proxy exposes static React hooks; the page calls those. Never edit a generated
|
|
5
|
+
file — fix the C# source and rebuild.
|
|
6
|
+
|
|
7
|
+
## Query proxy hooks
|
|
8
|
+
|
|
9
|
+
| Static | When |
|
|
10
|
+
| --- | --- |
|
|
11
|
+
| `<Query>.use(args?, sorting?)` | snapshot query, re-renders on change |
|
|
12
|
+
| `<Query>.useWithPaging(pageSize, args?, sorting?)` | server-side paging |
|
|
13
|
+
| `<Query>.useSuspense(...)` | suspense-aware; must render inside a query boundary |
|
|
14
|
+
| `<Query>.useSuspenseWithPaging(pageSize, ...)` | suspense plus paging |
|
|
15
|
+
| `<ObservableQuery>.use(...)` | live query pushed over the query hub |
|
|
16
|
+
| `<ObservableQuery>.useChangeStream(args?, getKey?, sorting?)` | item-level deltas |
|
|
17
|
+
| `<Query>.when(condition).use(args)` | conditional — never wrap a hook in `if` |
|
|
18
|
+
|
|
19
|
+
### Return tuples
|
|
20
|
+
|
|
21
|
+
These shapes are load-bearing. The observable variants have **no perform
|
|
22
|
+
function**:
|
|
23
|
+
|
|
24
|
+
| Hook | Returns |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `useQuery(query, args?, sorting?, isEnabled?)` | `[result, perform, setSorting]` |
|
|
27
|
+
| `useQueryWithPaging(query, paging, args?, sorting?, isEnabled?)` | `[result, perform, setSorting, setPage, setPageSize]` |
|
|
28
|
+
| `useSuspenseQuery(...)` | `[result, perform, setSorting]` |
|
|
29
|
+
| `useSuspenseQueryWithPaging(...)` | `[result, perform, setSorting, setPage, setPageSize]` |
|
|
30
|
+
| `useObservableQuery(query, args?, sorting?, isEnabled?)` | `[result, setSorting]` |
|
|
31
|
+
| `useObservableQueryWithPaging(query, paging, ...)` | `[result, setSorting, setPage, setPageSize]` |
|
|
32
|
+
| `useChangeStream(query, args?, getKey?, sorting?, isEnabled?)` | `ChangeSet<T>` |
|
|
33
|
+
|
|
34
|
+
`setSorting`, `setPage`, and `setPageSize` all return a promise.
|
|
35
|
+
|
|
36
|
+
The underlying hooks live in `@cratis/arc.react/queries` if you need them
|
|
37
|
+
directly.
|
|
38
|
+
|
|
39
|
+
### The result object
|
|
40
|
+
|
|
41
|
+
Every query hook returns a result carrying:
|
|
42
|
+
|
|
43
|
+
`data`, `paging`, `isSuccess`, `isAuthorized`, `isValid`, `isPerforming`,
|
|
44
|
+
`isReady`, `hasData`, `hasExceptions`, `validationResults`,
|
|
45
|
+
`exceptionMessages`, `exceptionStackTrace`, `changeSet`.
|
|
46
|
+
|
|
47
|
+
Guard rendering with `hasData` rather than checking `data.length` — a
|
|
48
|
+
conditional query that has not fired returns an empty result with
|
|
49
|
+
`hasData: false`.
|
|
50
|
+
|
|
51
|
+
### Paging
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
class Paging { constructor(page?: number, pageSize?: number); page: number; pageSize: number; get hasPaging(): boolean }
|
|
55
|
+
class Sorting { constructor(field: string, direction: SortDirection); get hasSorting(): boolean }
|
|
56
|
+
enum SortDirection { unspecified = 0, ascending = 1, descending = 2 }
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The result's paging block is a different shape from the request's — note
|
|
60
|
+
`size`, not `pageSize`:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
class PagingInfo { page: number; size: number; totalItems: number; totalPages: number }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`page` is zero-based. `totalItems` is the total count. A read model returning a
|
|
67
|
+
queryable gets server-side paging and sorting for free — use it whenever a list
|
|
68
|
+
can grow. Over the wire the parameters are `page`, `pageSize`, `sortBy`, and
|
|
69
|
+
`sortDirection`.
|
|
70
|
+
|
|
71
|
+
### Change streams
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
class ChangeSet<T> { readonly added: T[]; readonly replaced: T[]; readonly removed: T[] }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Without `getKey` removed items cannot be identified and everything arrives as
|
|
78
|
+
`added`; `getKey` is what enables `replaced` and `removed` detection. Change
|
|
79
|
+
streams exist only on observable queries returning a collection.
|
|
80
|
+
|
|
81
|
+
### Suspense
|
|
82
|
+
|
|
83
|
+
`useSuspense` throws a promise while loading and a typed error on failure, so it
|
|
84
|
+
must render inside `QueryBoundary` (`@cratis/arc.react/queries`), which combines
|
|
85
|
+
suspense with the query error boundary. It re-suspends on refresh, sorting, and
|
|
86
|
+
paging changes because the cache entry is cleared. In test teardown call
|
|
87
|
+
`clearSuspenseQueryCache()` and `clearSuspenseObservableQueryCache()`.
|
|
88
|
+
|
|
89
|
+
`QueryFailed` and `QueryUnauthorized` are the thrown error types; `QueryFailed`
|
|
90
|
+
carries `exceptionMessages` for logging.
|
|
91
|
+
|
|
92
|
+
### Query scope
|
|
93
|
+
|
|
94
|
+
`QueryScope` plus `useQueryScope()` aggregates one `isPerforming` flag across
|
|
95
|
+
the queries rendered inside it. Members register automatically and scopes nest,
|
|
96
|
+
with an inner scope reporting to the nearest outer one.
|
|
97
|
+
|
|
98
|
+
## Observable query transport
|
|
99
|
+
|
|
100
|
+
The `<Arc>` provider defaults `queryTransportMethod` to server-sent events;
|
|
101
|
+
the library-level default without the provider is WebSocket. Both run through a
|
|
102
|
+
multiplexed hub by default (`queryDirectMode` is `false`), so many observable
|
|
103
|
+
queries share one connection. Server-sent-event hub connections are capped —
|
|
104
|
+
raising `queryConnectionCount` above four warns, because HTTP/1.1 limits
|
|
105
|
+
concurrent SSE streams.
|
|
106
|
+
|
|
107
|
+
`observableQueryTransferMode` defaults to delta: the server sends a change set
|
|
108
|
+
and the client reconstructs the collection from the previously cached result.
|
|
109
|
+
Set it to full to receive every item as `added` on each push.
|
|
110
|
+
|
|
111
|
+
Identical subscriptions share one connection through the query instance cache,
|
|
112
|
+
which retains an unsubscribed entry briefly (30 seconds by default) so a
|
|
113
|
+
remount does not re-subscribe. After login or logout call `reconnectQueries()`
|
|
114
|
+
from the Arc context to force re-subscription with the new identity.
|
|
115
|
+
|
|
116
|
+
## Command proxy
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
const [command, setCommandValues, clearCommandValues] = RegisterAccount.use();
|
|
120
|
+
command.name = 'Acme'; // or setCommandValues({ name: 'Acme' })
|
|
121
|
+
const result = await command.execute();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`use(initialValues?)` returns a three-element tuple. The instance also exposes
|
|
125
|
+
`validate()`, `clear()`, `setInitialValues(values)`, `revertChanges()`, and
|
|
126
|
+
`hasChanges`.
|
|
127
|
+
|
|
128
|
+
### Read the result by the granular flag
|
|
129
|
+
|
|
130
|
+
| Flag | Meaning | Response |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `isSuccess` | authorized, valid, and no exception | happy path |
|
|
133
|
+
| `isAuthorized` | roles or policy rejected the call | redirect to login, or "not allowed" |
|
|
134
|
+
| `isValid` | validation failed — `validationResults` carries the messages | render inline field errors |
|
|
135
|
+
| `hasExceptions` | the handler threw — `exceptionMessages` carries diagnostics | generic error, and log it |
|
|
136
|
+
|
|
137
|
+
The result also carries `correlationId`, `authorizationFailureReason`,
|
|
138
|
+
`exceptionStackTrace`, and the typed `response`. Fluent callbacks
|
|
139
|
+
`onSuccess`, `onFailed`, `onException`, `onUnauthorized`, and
|
|
140
|
+
`onValidationFailure` each return the result for chaining.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
const result = await command.execute();
|
|
144
|
+
if (!result.isAuthorized) { redirectToLogin(); return; }
|
|
145
|
+
if (!result.isValid) { /* validationResults -> inline field errors */ return; }
|
|
146
|
+
if (result.hasExceptions) { console.error(result.exceptionMessages); return; }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A validation result is:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
class ValidationResult {
|
|
153
|
+
severity: ValidationResultSeverity; // Unknown 0, Information 1, Warning 2, Error 3
|
|
154
|
+
message: string;
|
|
155
|
+
members: string[]; // the properties the message applies to
|
|
156
|
+
state: unknown;
|
|
157
|
+
reason: ValidationResultReason; // 'rule' by default
|
|
158
|
+
reasonDetail?: string;
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Render per-field errors from `members` and `message`. Never branch on the raw
|
|
163
|
+
message text. `exceptionMessages` and `exceptionStackTrace` are for logging,
|
|
164
|
+
never for users.
|
|
165
|
+
|
|
166
|
+
`toastCommandResult(result, options)` from `@cratis/components/Notifications`
|
|
167
|
+
collapses the whole branch into one call — mount a `<Toaster />` near the app
|
|
168
|
+
root and it maps every flag to the right toast without showing stack traces. It
|
|
169
|
+
returns `true` when the command succeeded:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
if (toastCommandResult(result, { successTitle: 'Account registered' })) refresh();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
For ad-hoc notifications the imperative `toast.success/info/warn/error` each
|
|
176
|
+
take an **options object**, not a bare string:
|
|
177
|
+
`toast.info({ title: 'Saved', description: 'Your changes were saved.' })`.
|
|
178
|
+
|
|
179
|
+
`CommandDialog` performs all of this itself — the flags matter most when
|
|
180
|
+
executing a command outside a dialog.
|
|
181
|
+
|
|
182
|
+
### Command scope
|
|
183
|
+
|
|
184
|
+
`CommandScope` plus `useCommandScope()` (`@cratis/arc.react/commands`)
|
|
185
|
+
aggregates change and execution state across several commands on one screen.
|
|
186
|
+
Members register automatically, scopes nest, and the scope is injectable into a
|
|
187
|
+
view model as `ICommandScope`. It exposes `hasChanges`, `isPerforming`,
|
|
188
|
+
`hasValidationFailures`, `hasExceptions`, per-command and aggregated failures,
|
|
189
|
+
`execute()` (which runs only the commands that have changes), and
|
|
190
|
+
`revertChanges()`.
|
|
191
|
+
|
|
192
|
+
## `<Arc>` configuration
|
|
193
|
+
|
|
194
|
+
The top-level provider configures the microservice name, API base path, and
|
|
195
|
+
query transport. Its props include `microservice`, `origin`, `basePath`,
|
|
196
|
+
`apiBasePath`, `httpHeadersCallback` (merged into every request — bearer and
|
|
197
|
+
tenant headers), `detailsType`, `queryTransportMethod`, `queryConnectionCount`,
|
|
198
|
+
`queryDirectMode`, `observableQueryTransferMode`, and `queryCacheRetentionMs`.
|
|
199
|
+
`QueryTransportMethod` and `ObservableQueryTransferMode` import from
|
|
200
|
+
`@cratis/arc`; `Arc` imports from `@cratis/arc.react`.
|