From 8b7c3bb6584a643912003b46bfba38fc16c60d29 Mon Sep 17 00:00:00 2001 From: Einar Date: Mon, 2 Mar 2026 16:59:03 +0100 Subject: [PATCH 1/6] Make visible optional - we have other ways of doing this with useDialog() --- Source/CommandDialog/CommandDialog.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/CommandDialog/CommandDialog.tsx b/Source/CommandDialog/CommandDialog.tsx index cf54714..071841e 100644 --- a/Source/CommandDialog/CommandDialog.tsx +++ b/Source/CommandDialog/CommandDialog.tsx @@ -15,7 +15,7 @@ import { export interface CommandDialogProps extends Omit, 'children'> { - visible: boolean; + visible?: boolean; header: string; confirmLabel?: string; cancelLabel?: string; @@ -38,7 +38,7 @@ const CommandDialogWrapper = ({ children }: { header: string; - visible: boolean; + visible?: boolean; width: string; confirmLabel: string; cancelLabel: string; @@ -58,7 +58,7 @@ const CommandDialogWrapper = ({ const result = await (commandInstance as unknown as { execute: () => Promise> }).execute(); if (result.isSuccess) { await onConfirm(result); - return true; + return false; } else { setCommandResult(result); return false; From 59f03fcf0fc4d55831b7f8edf5fa6a68e9ca8ddb Mon Sep 17 00:00:00 2001 From: Einar Date: Mon, 2 Mar 2026 16:59:25 +0100 Subject: [PATCH 2/6] Add recommended way for using Dialog and CommandDialog --- Documentation/CommandDialog/index.md | 65 ++++++++++++----- Documentation/Dialogs/dialog.md | 105 ++++++++++++++------------- 2 files changed, 101 insertions(+), 69 deletions(-) diff --git a/Documentation/CommandDialog/index.md b/Documentation/CommandDialog/index.md index 914b9cb..2ea1ca5 100644 --- a/Documentation/CommandDialog/index.md +++ b/Documentation/CommandDialog/index.md @@ -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()` +- Close from inside the dialog through `useDialogContext()` +- `await` the dialog at the call site and handle `[dialogResult, value]` + +When the value represents command execution output, use `CommandResult` 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>(); + + return ( + + command={CreateProject} + header='Create project' + onConfirm={async result => closeDialog(DialogResult.Ok, result as CommandResult)} + onCancel={() => closeDialog(DialogResult.Cancelled)} + /> + ); +}; function MyComponent() { - const [visible, setVisible] = useState(false); + const [CreateProjectDialogWrapper, showCreateProjectDialog] = useDialog>(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 ( - setVisible(false)} - > - {/* Custom form fields go here */} - + <> + + + ); } ``` +> `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") @@ -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>()` in a wrapping dialog component. ## Integration diff --git a/Documentation/Dialogs/dialog.md b/Documentation/Dialogs/dialog.md index f09b7e2..785db04 100644 --- a/Documentation/Dialogs/dialog.md +++ b/Documentation/Dialogs/dialog.md @@ -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()` at the call site and `useDialogContext()` 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(); + const [name, setName] = useState(''); return ( setVisible(false)} - buttons={null} + title='Add project' + isValid={name.trim().length > 0} + onConfirm={() => closeDialog(DialogResult.Ok, { id: crypto.randomUUID(), name })} + onCancel={() => closeDialog(DialogResult.Cancelled)} > -

Dialog content goes here

+ {/* Dialog content */}
); -} +}; + +const MyComponent = () => { + const [AddProjectDialogWrapper, showAddProjectDialog] = useDialog(AddProjectDialog); + + const handleAddProject = async () => { + const [result, project] = await showAddProjectDialog(); + if (result === DialogResult.Ok && project) { + // Use the typed result + } + }; + + return ( + <> + + + + ); +}; ``` ## 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) - } -]; - - setVisible(false)} - buttons={buttons} -> - {/* Content */} - -``` +- `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()`. From 7756da9cc32974a91056c38355aefd898126a76e Mon Sep 17 00:00:00 2001 From: Einar Date: Mon, 2 Mar 2026 17:00:02 +0100 Subject: [PATCH 3/6] Updating stories for what we recommend --- .../CommandDialog/CommandDialog.stories.tsx | 53 ++++++++- Source/Dialogs/Dialog.stories.tsx | 103 ++++++++++++++---- 2 files changed, 133 insertions(+), 23 deletions(-) diff --git a/Source/CommandDialog/CommandDialog.stories.tsx b/Source/CommandDialog/CommandDialog.stories.tsx index 62f17c2..8737bad 100644 --- a/Source/CommandDialog/CommandDialog.stories.tsx +++ b/Source/CommandDialog/CommandDialog.stories.tsx @@ -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 = { @@ -213,8 +214,58 @@ const ServerValidationWrapper = () => { ); }; +const AwaitableWithResultWrapper = () => { + const [result, setResult] = useState(''); + + const UpdateUserDialog = () => { + const { closeDialog } = useDialogContext>(); + + return ( + + 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)} + > + c.name} title="Name" placeholder="Enter name (min 2 chars)" /> + c.email} title="Email" placeholder="Enter email" type="email" /> + c.age} title="Age" placeholder="Enter age (18-120)" /> + + ); + }; + + const [UpdateUserDialogComponent, showUpdateUserDialog] = useDialog>(UpdateUserDialog); + + return ( +
+ + + {result && ( +
+ Command executed: {result} +
+ )} + + +
+ ); +}; + export const Default: Story = { - render: () => , + render: () => , }; export const WithServerValidation: Story = { diff --git a/Source/Dialogs/Dialog.stories.tsx b/Source/Dialogs/Dialog.stories.tsx index 9968b84..4c5c390 100644 --- a/Source/Dialogs/Dialog.stories.tsx +++ b/Source/Dialogs/Dialog.stories.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import { Meta, StoryObj } from '@storybook/react'; import { Dialog } from './Dialog'; -import { DialogButtons } from '@cratis/arc.react/dialogs'; +import { DialogButtons, DialogResult, useDialog, useDialogContext } from '@cratis/arc.react/dialogs'; import { Button } from 'primereact/button'; import { InputText } from 'primereact/inputtext'; @@ -20,19 +20,28 @@ export default meta; type Story = StoryObj; const DialogWrapper = ({ buttons, title, children, isValid }: { buttons: DialogButtons; title: string; children: React.ReactNode; isValid?: boolean }) => { - const [visible, setVisible] = useState(false); - return ( - <> -