Skip to content

Commit 191e017

Browse files
authored
Merge pull request #13 from Cratis:fix/command-dialog
Fix/command-dialog
2 parents 9af90db + eed3a05 commit 191e017

9 files changed

Lines changed: 385 additions & 161 deletions

File tree

Documentation/CommandDialog/index.md

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,48 +15,75 @@ CommandDialog simplifies the process of presenting a command form to users withi
1515
- Success and cancellation handling
1616
- Integration with Cratis Arc command system
1717

18+
## Recommended Usage Pattern
19+
20+
For new implementations, use the same dialog pattern as other typed dialogs:
21+
22+
- Open dialogs through `useDialog<TResult>()`
23+
- Close from inside the dialog through `useDialogContext<TResult>()`
24+
- `await` the dialog at the call site and handle `[dialogResult, value]`
25+
26+
When the value represents command execution output, use `CommandResult<TResponse>` as the dialog result type.
27+
1828
## Basic Usage
1929

2030
```typescript
21-
import { CommandDialog } from '@cratis/components';
22-
import { MyCommand } from './commands';
31+
import { DialogResult, useDialog, useDialogContext } from '@cratis/arc.react/dialogs';
32+
import { CommandResult } from '@cratis/arc/commands';
33+
import { CommandDialog } from '@cratis/components/CommandDialog';
34+
import { CreateProject } from './commands';
35+
36+
type CreateProjectResponse = {
37+
projectId: string;
38+
};
39+
40+
const CreateProjectDialog = () => {
41+
const { closeDialog } = useDialogContext<CommandResult<CreateProjectResponse>>();
42+
43+
return (
44+
<CommandDialog<CreateProject, CreateProjectResponse>
45+
command={CreateProject}
46+
header='Create project'
47+
onConfirm={async result => closeDialog(DialogResult.Ok, result as CommandResult<CreateProjectResponse>)}
48+
onCancel={() => closeDialog(DialogResult.Cancelled)}
49+
/>
50+
);
51+
};
2352

2453
function MyComponent() {
25-
const [visible, setVisible] = useState(false);
54+
const [CreateProjectDialogWrapper, showCreateProjectDialog] = useDialog<CommandResult<CreateProjectResponse>>(CreateProjectDialog);
2655

27-
const handleConfirm = async (result) => {
28-
if (result.isSuccess) {
29-
// Handle success
30-
setVisible(false);
56+
const handleCreateProject = async () => {
57+
const [dialogResult, commandResult] = await showCreateProjectDialog();
58+
59+
if (dialogResult === DialogResult.Ok && commandResult?.isSuccess) {
60+
// Handle successful command response
3161
}
3262
};
3363

3464
return (
35-
<CommandDialog
36-
command={MyCommand}
37-
visible={visible}
38-
header="Execute Command"
39-
onConfirm={handleConfirm}
40-
onCancel={() => setVisible(false)}
41-
>
42-
{/* Custom form fields go here */}
43-
</CommandDialog>
65+
<>
66+
<button onClick={handleCreateProject}>Create project</button>
67+
<CreateProjectDialogWrapper />
68+
</>
4469
);
4570
}
4671
```
4772

73+
> `CommandDialog` invokes `onConfirm` only when command execution succeeds.
74+
4875
## Props
4976

5077
### Required Props
5178

5279
- `command`: Constructor for the command type
53-
- `visible`: Boolean controlling dialog visibility
5480
- `header`: Dialog title text
5581
- `onConfirm`: Callback function when command succeeds
5682
- `onCancel`: Callback function when dialog is cancelled
5783

5884
### Optional Props
5985

86+
- `visible`: Boolean controlling dialog visibility (defaults to `true`)
6087
- `initialValues`: Initial values for the command form
6188
- `currentValues`: Current values to populate the form
6289
- `confirmLabel`: Custom text for confirm button (default: "OK")
@@ -71,7 +98,9 @@ function MyComponent() {
7198

7299
## Context
73100

74-
The component provides a `CommandDialogContext` accessible via `useCommandDialogContext` hook for child components to access dialog state and callbacks.
101+
`CommandDialog` is built on top of `CommandForm` and uses the command form context internally for values, validation, and execution state.
102+
103+
When used as an awaitable dialog, pair it with `useDialogContext<CommandResult<TResponse>>()` in a wrapping dialog component.
75104

76105
## Integration
77106

Documentation/Dialogs/dialog.md

Lines changed: 54 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,78 @@
11
# Dialog
22

3-
Base dialog component for creating custom dialogs.
3+
Base dialog component for creating typed dialogs that can be awaited.
44

5-
## Purpose
5+
## Recommended Pattern
66

7-
The Dialog component provides a customizable modal dialog foundation for building various types of dialogs.
7+
Use `useDialog<T>()` at the call site and `useDialogContext<T>()` inside the dialog component.
88

9-
## Key Features
9+
- The caller opens the dialog with `await` and receives `[dialogResult, value]`
10+
- The dialog closes itself through `closeDialog(...)`
11+
- The generic `T` is the value returned from the dialog
1012

11-
- Customizable header and footer
12-
- Flexible button configuration
13-
- Modal and non-modal modes
14-
- Responsive sizing
15-
- Integration with PrimeReact Dialog
13+
This pattern gives strongly typed dialog results and a simple async flow.
1614

17-
## Basic Usage
15+
## Example
1816

1917
```typescript
20-
import { Dialog } from '@cratis/components';
18+
import { useState } from 'react';
19+
import { DialogResult, useDialog, useDialogContext } from '@cratis/arc.react/dialogs';
20+
import { Dialog } from '@cratis/components/Dialogs';
2121

22-
function MyComponent() {
23-
const [visible, setVisible] = useState(false);
22+
type Project = {
23+
id: string;
24+
name: string;
25+
};
26+
27+
const AddProjectDialog = () => {
28+
const { closeDialog } = useDialogContext<Project>();
29+
const [name, setName] = useState('');
2430

2531
return (
2632
<Dialog
27-
title="My Dialog"
28-
visible={visible}
29-
onCancel={() => setVisible(false)}
30-
buttons={null}
33+
title='Add project'
34+
isValid={name.trim().length > 0}
35+
onConfirm={() => closeDialog(DialogResult.Ok, { id: crypto.randomUUID(), name })}
36+
onCancel={() => closeDialog(DialogResult.Cancelled)}
3137
>
32-
<p>Dialog content goes here</p>
38+
{/* Dialog content */}
3339
</Dialog>
3440
);
35-
}
41+
};
42+
43+
const MyComponent = () => {
44+
const [AddProjectDialogWrapper, showAddProjectDialog] = useDialog<Project>(AddProjectDialog);
45+
46+
const handleAddProject = async () => {
47+
const [result, project] = await showAddProjectDialog();
48+
if (result === DialogResult.Ok && project) {
49+
// Use the typed result
50+
}
51+
};
52+
53+
return (
54+
<>
55+
<button onClick={handleAddProject}>Add project</button>
56+
<AddProjectDialogWrapper />
57+
</>
58+
);
59+
};
3660
```
3761

3862
## Props
3963

4064
- `title`: Dialog header text
41-
- `visible`: Controls visibility
42-
- `onCancel`: Callback when dialog is closed
43-
- `buttons`: Button configuration (or null for no buttons)
65+
- `visible`: Controls visibility (defaults to `true`)
66+
- `onConfirm`: Callback for confirm actions
67+
- `onCancel`: Callback for cancel actions
68+
- `onClose`: Fallback close callback
69+
- `buttons`: Predefined `DialogButtons` or custom footer content
4470
- `width`: Dialog width
45-
- `style`: Custom CSS styles
46-
47-
## With Custom Buttons
48-
49-
```typescript
50-
const buttons = [
51-
{
52-
label: 'Save',
53-
icon: 'pi pi-check',
54-
onClick: handleSave
55-
},
56-
{
57-
label: 'Cancel',
58-
icon: 'pi pi-times',
59-
onClick: () => setVisible(false)
60-
}
61-
];
62-
63-
<Dialog
64-
title="Edit Item"
65-
visible={visible}
66-
onCancel={() => setVisible(false)}
67-
buttons={buttons}
68-
>
69-
{/* Content */}
70-
</Dialog>
71-
```
71+
- `resizable`: Enables resize
72+
- `isValid`: Enables or disables confirm actions
73+
- `okLabel`, `cancelLabel`, `yesLabel`, `noLabel`: Button labels
7274

73-
## Integration
75+
## Notes
7476

75-
Integrates with PrimeReact Dialog component for consistent styling and behavior.
77+
- Prefer `onConfirm` and `onCancel` over `onClose` for clear intent.
78+
- For typed, awaitable dialogs, let the dialog call `closeDialog(...)` from `useDialogContext<T>()`.

Source/CommandDialog/CommandDialog.stories.tsx

Lines changed: 47 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CommandDialog } from './CommandDialog';
77
import { Command, CommandResult, CommandValidator } from '@cratis/arc/commands';
88
import { PropertyDescriptor } from '@cratis/arc/reflection';
99
import { InputTextField, NumberField, TextAreaField } from '../CommandForm/fields';
10+
import { DialogResult, useDialog, useDialogContext } from '@cratis/arc.react/dialogs';
1011
import '@cratis/arc/validation';
1112

1213
const meta: Meta<typeof CommandDialog> = {
@@ -87,7 +88,7 @@ class UpdateUserCommandWithServer extends Command<object> {
8788
}
8889
}
8990

90-
const DialogWrapper = () => {
91+
const ServerValidationWrapper = () => {
9192
const [visible, setVisible] = useState(true);
9293
const [result, setResult] = useState<string>('');
9394
const [validationErrors, setValidationErrors] = useState<string[]>([]);
@@ -122,99 +123,88 @@ const DialogWrapper = () => {
122123
</div>
123124
)}
124125

125-
<CommandDialog<UpdateUserCommand>
126-
command={UpdateUserCommand}
126+
<CommandDialog<UpdateUserCommandWithServer>
127+
command={UpdateUserCommandWithServer}
127128
visible={visible}
128-
header="Update User Information (with Validation)"
129+
header="Update User Information (with Server Validation)"
129130
confirmLabel="Save"
130131
cancelLabel="Cancel"
131-
autoServerValidate={false}
132132
onConfirm={async (commandResult) => {
133133
setResult(JSON.stringify(commandResult));
134134
setVisible(false);
135135
}}
136136
onCancel={() => setVisible(false)}
137-
onFieldChange={(command) => {
138-
// Client-side only validation - validate as fields change
139-
const errors = command.validation?.validate(command) ?? [];
140-
setValidationErrors(errors.map(v => v.message));
137+
onFieldChange={async (command) => {
138+
// Progressive validation - validate as fields change
139+
const validationResult = await command.validate();
140+
141+
if (!validationResult.isValid) {
142+
setValidationErrors(validationResult.validationResults.map(v => v.message));
143+
} else {
144+
setValidationErrors([]);
145+
}
141146
}}
142147
>
143-
<InputTextField value={(c: UpdateUserCommand) => c.name} title="Name" placeholder="Enter name (min 2 chars)" />
144-
<InputTextField value={(c: UpdateUserCommand) => c.email} title="Email" placeholder="Enter email" type="email" />
145-
<NumberField value={(c: UpdateUserCommand) => c.age} title="Age" placeholder="Enter age (18-120)" />
148+
<InputTextField value={(c: UpdateUserCommandWithServer) => c.name} title="Name" placeholder="Enter name (min 2 chars)" />
149+
<InputTextField value={(c: UpdateUserCommandWithServer) => c.email} title="Email" placeholder="Enter email" type="email" />
150+
<NumberField value={(c: UpdateUserCommandWithServer) => c.age} title="Age" placeholder="Enter age (18-120)" />
146151
</CommandDialog>
147152
</div>
148153
);
149154
};
150155

151-
const ServerValidationWrapper = () => {
152-
const [visible, setVisible] = useState(true);
156+
const AwaitableWithResultWrapper = () => {
153157
const [result, setResult] = useState<string>('');
154-
const [validationErrors, setValidationErrors] = useState<string[]>([]);
158+
159+
const UpdateUserDialog = () => {
160+
const { closeDialog } = useDialogContext<CommandResult<object>>();
161+
162+
return (
163+
<CommandDialog<UpdateUserCommand>
164+
command={UpdateUserCommand}
165+
header="Update User Information (awaitable result)"
166+
confirmLabel="Save"
167+
cancelLabel="Cancel"
168+
autoServerValidate={false}
169+
onConfirm={async commandResult => closeDialog(DialogResult.Ok, commandResult)}
170+
onCancel={() => closeDialog(DialogResult.Cancelled)}
171+
>
172+
<InputTextField value={(c: UpdateUserCommand) => c.name} title="Name" placeholder="Enter name (min 2 chars)" />
173+
<InputTextField value={(c: UpdateUserCommand) => c.email} title="Email" placeholder="Enter email" type="email" />
174+
<NumberField value={(c: UpdateUserCommand) => c.age} title="Age" placeholder="Enter age (18-120)" />
175+
</CommandDialog>
176+
);
177+
};
178+
179+
const [UpdateUserDialogComponent, showUpdateUserDialog] = useDialog<CommandResult<object>>(UpdateUserDialog);
155180

156181
return (
157182
<div className="storybook-wrapper">
158183
<button
159184
className="p-button p-component mb-3"
160-
onClick={() => {
161-
setVisible(true);
162-
setValidationErrors([]);
163-
setResult('');
185+
onClick={async () => {
186+
const [dialogResult, commandResult] = await showUpdateUserDialog();
187+
if (dialogResult === DialogResult.Ok && commandResult) {
188+
setResult(JSON.stringify(commandResult));
189+
}
164190
}}
165191
>
166192
Open Dialog
167193
</button>
168194

169-
{validationErrors.length > 0 && (
170-
<div className="p-3 mt-3 bg-red-100 border-round">
171-
<strong>Validation Errors:</strong>
172-
<ul className="mt-2 mb-0">
173-
{validationErrors.map((error, index) => (
174-
<li key={index}>{error}</li>
175-
))}
176-
</ul>
177-
</div>
178-
)}
179-
180195
{result && (
181196
<div className="p-3 mt-3 bg-green-100 border-round">
182197
<strong>Command executed:</strong> {result}
183198
</div>
184199
)}
185200

186-
<CommandDialog<UpdateUserCommandWithServer>
187-
command={UpdateUserCommandWithServer}
188-
visible={visible}
189-
header="Update User Information (with Server Validation)"
190-
confirmLabel="Save"
191-
cancelLabel="Cancel"
192-
onConfirm={async (commandResult) => {
193-
setResult(JSON.stringify(commandResult));
194-
setVisible(false);
195-
}}
196-
onCancel={() => setVisible(false)}
197-
onFieldChange={async (command) => {
198-
// Progressive validation - validate as fields change
199-
const validationResult = await command.validate();
200-
201-
if (!validationResult.isValid) {
202-
setValidationErrors(validationResult.validationResults.map(v => v.message));
203-
} else {
204-
setValidationErrors([]);
205-
}
206-
}}
207-
>
208-
<InputTextField value={(c: UpdateUserCommandWithServer) => c.name} title="Name" placeholder="Enter name (min 2 chars)" />
209-
<InputTextField value={(c: UpdateUserCommandWithServer) => c.email} title="Email" placeholder="Enter email" type="email" />
210-
<NumberField value={(c: UpdateUserCommandWithServer) => c.age} title="Age" placeholder="Enter age (18-120)" />
211-
</CommandDialog>
201+
<UpdateUserDialogComponent />
212202
</div>
213203
);
214204
};
215205

216206
export const Default: Story = {
217-
render: () => <DialogWrapper />,
207+
render: () => <AwaitableWithResultWrapper />,
218208
};
219209

220210
export const WithServerValidation: Story = {

0 commit comments

Comments
 (0)