Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 47 additions & 18 deletions Documentation/CommandDialog/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,48 +15,75 @@ CommandDialog simplifies the process of presenting a command form to users withi
- Success and cancellation handling
- Integration with Cratis Arc command system

## Recommended Usage Pattern

For new implementations, use the same dialog pattern as other typed dialogs:

- Open dialogs through `useDialog<TResult>()`
- Close from inside the dialog through `useDialogContext<TResult>()`
- `await` the dialog at the call site and handle `[dialogResult, value]`

When the value represents command execution output, use `CommandResult<TResponse>` as the dialog result type.

## Basic Usage

```typescript
import { CommandDialog } from '@cratis/components';
import { MyCommand } from './commands';
import { DialogResult, useDialog, useDialogContext } from '@cratis/arc.react/dialogs';
import { CommandResult } from '@cratis/arc/commands';
import { CommandDialog } from '@cratis/components/CommandDialog';
import { CreateProject } from './commands';

type CreateProjectResponse = {
projectId: string;
};

const CreateProjectDialog = () => {
const { closeDialog } = useDialogContext<CommandResult<CreateProjectResponse>>();

return (
<CommandDialog<CreateProject, CreateProjectResponse>
command={CreateProject}
header='Create project'
onConfirm={async result => closeDialog(DialogResult.Ok, result as CommandResult<CreateProjectResponse>)}
onCancel={() => closeDialog(DialogResult.Cancelled)}
/>
);
};

function MyComponent() {
const [visible, setVisible] = useState(false);
const [CreateProjectDialogWrapper, showCreateProjectDialog] = useDialog<CommandResult<CreateProjectResponse>>(CreateProjectDialog);

const handleConfirm = async (result) => {
if (result.isSuccess) {
// Handle success
setVisible(false);
const handleCreateProject = async () => {
const [dialogResult, commandResult] = await showCreateProjectDialog();

if (dialogResult === DialogResult.Ok && commandResult?.isSuccess) {
// Handle successful command response
}
};

return (
<CommandDialog
command={MyCommand}
visible={visible}
header="Execute Command"
onConfirm={handleConfirm}
onCancel={() => setVisible(false)}
>
{/* Custom form fields go here */}
</CommandDialog>
<>
<button onClick={handleCreateProject}>Create project</button>
<CreateProjectDialogWrapper />
</>
);
}
```

> `CommandDialog` invokes `onConfirm` only when command execution succeeds.

## Props

### Required Props

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

### Optional Props

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

## Context

The component provides a `CommandDialogContext` accessible via `useCommandDialogContext` hook for child components to access dialog state and callbacks.
`CommandDialog` is built on top of `CommandForm` and uses the command form context internally for values, validation, and execution state.

When used as an awaitable dialog, pair it with `useDialogContext<CommandResult<TResponse>>()` in a wrapping dialog component.

## Integration

Expand Down
105 changes: 54 additions & 51 deletions Documentation/Dialogs/dialog.md
Original file line number Diff line number Diff line change
@@ -1,75 +1,78 @@
# Dialog

Base dialog component for creating custom dialogs.
Base dialog component for creating typed dialogs that can be awaited.

## Purpose
## Recommended Pattern

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

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

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

## Basic Usage
## Example

```typescript
import { Dialog } from '@cratis/components';
import { useState } from 'react';
import { DialogResult, useDialog, useDialogContext } from '@cratis/arc.react/dialogs';
import { Dialog } from '@cratis/components/Dialogs';

function MyComponent() {
const [visible, setVisible] = useState(false);
type Project = {
id: string;
name: string;
};

const AddProjectDialog = () => {
const { closeDialog } = useDialogContext<Project>();
const [name, setName] = useState('');

return (
<Dialog
title="My Dialog"
visible={visible}
onCancel={() => setVisible(false)}
buttons={null}
title='Add project'
isValid={name.trim().length > 0}
onConfirm={() => closeDialog(DialogResult.Ok, { id: crypto.randomUUID(), name })}
onCancel={() => closeDialog(DialogResult.Cancelled)}
>
<p>Dialog content goes here</p>
{/* Dialog content */}
</Dialog>
);
}
};

const MyComponent = () => {
const [AddProjectDialogWrapper, showAddProjectDialog] = useDialog<Project>(AddProjectDialog);

const handleAddProject = async () => {
const [result, project] = await showAddProjectDialog();
if (result === DialogResult.Ok && project) {
// Use the typed result
}
};

return (
<>
<button onClick={handleAddProject}>Add project</button>
<AddProjectDialogWrapper />
</>
);
};
```

## Props

- `title`: Dialog header text
- `visible`: Controls visibility
- `onCancel`: Callback when dialog is closed
- `buttons`: Button configuration (or null for no buttons)
- `visible`: Controls visibility (defaults to `true`)
- `onConfirm`: Callback for confirm actions
- `onCancel`: Callback for cancel actions
- `onClose`: Fallback close callback
- `buttons`: Predefined `DialogButtons` or custom footer content
- `width`: Dialog width
- `style`: Custom CSS styles

## With Custom Buttons

```typescript
const buttons = [
{
label: 'Save',
icon: 'pi pi-check',
onClick: handleSave
},
{
label: 'Cancel',
icon: 'pi pi-times',
onClick: () => setVisible(false)
}
];

<Dialog
title="Edit Item"
visible={visible}
onCancel={() => setVisible(false)}
buttons={buttons}
>
{/* Content */}
</Dialog>
```
- `resizable`: Enables resize
- `isValid`: Enables or disables confirm actions
- `okLabel`, `cancelLabel`, `yesLabel`, `noLabel`: Button labels

## Integration
## Notes

Integrates with PrimeReact Dialog component for consistent styling and behavior.
- Prefer `onConfirm` and `onCancel` over `onClose` for clear intent.
- For typed, awaitable dialogs, let the dialog call `closeDialog(...)` from `useDialogContext<T>()`.
104 changes: 47 additions & 57 deletions Source/CommandDialog/CommandDialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CommandDialog } from './CommandDialog';
import { Command, CommandResult, CommandValidator } from '@cratis/arc/commands';
import { PropertyDescriptor } from '@cratis/arc/reflection';
import { InputTextField, NumberField, TextAreaField } from '../CommandForm/fields';
import { DialogResult, useDialog, useDialogContext } from '@cratis/arc.react/dialogs';
import '@cratis/arc/validation';

const meta: Meta<typeof CommandDialog> = {
Expand Down Expand Up @@ -87,7 +88,7 @@ class UpdateUserCommandWithServer extends Command<object> {
}
}

const DialogWrapper = () => {
const ServerValidationWrapper = () => {
const [visible, setVisible] = useState(true);
const [result, setResult] = useState<string>('');
const [validationErrors, setValidationErrors] = useState<string[]>([]);
Expand Down Expand Up @@ -122,99 +123,88 @@ const DialogWrapper = () => {
</div>
)}

<CommandDialog<UpdateUserCommand>
command={UpdateUserCommand}
<CommandDialog<UpdateUserCommandWithServer>
command={UpdateUserCommandWithServer}
visible={visible}
header="Update User Information (with Validation)"
header="Update User Information (with Server Validation)"
confirmLabel="Save"
cancelLabel="Cancel"
autoServerValidate={false}
onConfirm={async (commandResult) => {
setResult(JSON.stringify(commandResult));
setVisible(false);
}}
onCancel={() => setVisible(false)}
onFieldChange={(command) => {
// Client-side only validation - validate as fields change
const errors = command.validation?.validate(command) ?? [];
setValidationErrors(errors.map(v => v.message));
onFieldChange={async (command) => {
// Progressive validation - validate as fields change
const validationResult = await command.validate();

if (!validationResult.isValid) {
setValidationErrors(validationResult.validationResults.map(v => v.message));
} else {
setValidationErrors([]);
}
}}
>
<InputTextField value={(c: UpdateUserCommand) => c.name} title="Name" placeholder="Enter name (min 2 chars)" />
<InputTextField value={(c: UpdateUserCommand) => c.email} title="Email" placeholder="Enter email" type="email" />
<NumberField value={(c: UpdateUserCommand) => c.age} title="Age" placeholder="Enter age (18-120)" />
<InputTextField value={(c: UpdateUserCommandWithServer) => c.name} title="Name" placeholder="Enter name (min 2 chars)" />
<InputTextField value={(c: UpdateUserCommandWithServer) => c.email} title="Email" placeholder="Enter email" type="email" />
<NumberField value={(c: UpdateUserCommandWithServer) => c.age} title="Age" placeholder="Enter age (18-120)" />
</CommandDialog>
</div>
);
};

const ServerValidationWrapper = () => {
const [visible, setVisible] = useState(true);
const AwaitableWithResultWrapper = () => {
const [result, setResult] = useState<string>('');
const [validationErrors, setValidationErrors] = useState<string[]>([]);

const UpdateUserDialog = () => {
const { closeDialog } = useDialogContext<CommandResult<object>>();

return (
<CommandDialog<UpdateUserCommand>
command={UpdateUserCommand}
header="Update User Information (awaitable result)"
confirmLabel="Save"
cancelLabel="Cancel"
autoServerValidate={false}
onConfirm={async commandResult => closeDialog(DialogResult.Ok, commandResult)}
onCancel={() => closeDialog(DialogResult.Cancelled)}
>
<InputTextField value={(c: UpdateUserCommand) => c.name} title="Name" placeholder="Enter name (min 2 chars)" />
<InputTextField value={(c: UpdateUserCommand) => c.email} title="Email" placeholder="Enter email" type="email" />
<NumberField value={(c: UpdateUserCommand) => c.age} title="Age" placeholder="Enter age (18-120)" />
</CommandDialog>
);
};

const [UpdateUserDialogComponent, showUpdateUserDialog] = useDialog<CommandResult<object>>(UpdateUserDialog);

return (
<div className="storybook-wrapper">
<button
className="p-button p-component mb-3"
onClick={() => {
setVisible(true);
setValidationErrors([]);
setResult('');
onClick={async () => {
const [dialogResult, commandResult] = await showUpdateUserDialog();
if (dialogResult === DialogResult.Ok && commandResult) {
setResult(JSON.stringify(commandResult));
}
}}
>
Open Dialog
</button>

{validationErrors.length > 0 && (
<div className="p-3 mt-3 bg-red-100 border-round">
<strong>Validation Errors:</strong>
<ul className="mt-2 mb-0">
{validationErrors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
)}

{result && (
<div className="p-3 mt-3 bg-green-100 border-round">
<strong>Command executed:</strong> {result}
</div>
)}

<CommandDialog<UpdateUserCommandWithServer>
command={UpdateUserCommandWithServer}
visible={visible}
header="Update User Information (with Server Validation)"
confirmLabel="Save"
cancelLabel="Cancel"
onConfirm={async (commandResult) => {
setResult(JSON.stringify(commandResult));
setVisible(false);
}}
onCancel={() => setVisible(false)}
onFieldChange={async (command) => {
// Progressive validation - validate as fields change
const validationResult = await command.validate();

if (!validationResult.isValid) {
setValidationErrors(validationResult.validationResults.map(v => v.message));
} else {
setValidationErrors([]);
}
}}
>
<InputTextField value={(c: UpdateUserCommandWithServer) => c.name} title="Name" placeholder="Enter name (min 2 chars)" />
<InputTextField value={(c: UpdateUserCommandWithServer) => c.email} title="Email" placeholder="Enter email" type="email" />
<NumberField value={(c: UpdateUserCommandWithServer) => c.age} title="Age" placeholder="Enter age (18-120)" />
</CommandDialog>
<UpdateUserDialogComponent />
</div>
);
};

export const Default: Story = {
render: () => <DialogWrapper />,
render: () => <AwaitableWithResultWrapper />,
};

export const WithServerValidation: Story = {
Expand Down
Loading
Loading