diff --git a/Documentation/CommandDialog/index.md b/Documentation/CommandDialog/index.md index 2ea1ca5..d80129b 100644 --- a/Documentation/CommandDialog/index.md +++ b/Documentation/CommandDialog/index.md @@ -41,10 +41,11 @@ const CreateProjectDialog = () => { const { closeDialog } = useDialogContext>(); return ( - + command={CreateProject} - header='Create project' - onConfirm={async result => closeDialog(DialogResult.Ok, result as CommandResult)} + title='Create project' + okLabel='Create' + onConfirm={async () => closeDialog(DialogResult.Ok)} onCancel={() => closeDialog(DialogResult.Cancelled)} /> ); @@ -77,28 +78,39 @@ function MyComponent() { ### Required Props - `command`: Constructor for the command type -- `header`: Dialog title text -- `onConfirm`: Callback function when command succeeds -- `onCancel`: Callback function when dialog is cancelled +- `title`: Dialog title text ### 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") +- `onConfirm`: Confirm callback from `Dialog` (called only after successful command execution) +- `onCancel`: Cancel callback from `Dialog` +- `onClose`: Fallback close callback from `Dialog` +- `okLabel`: Custom text for confirm button (default: "Ok") - `cancelLabel`: Custom text for cancel button (default: "Cancel") -- `confirmIcon`: Icon for confirm button -- `cancelIcon`: Icon for cancel button +- `yesLabel`, `noLabel`: Labels for `YesNo` and `YesNoCancel` button modes +- `buttons`: `DialogButtons` value or custom footer content +- `resizable`: Whether dialog can be resized +- `isValid`: Additional validity gate combined with command form validity - `onFieldValidate`: Custom validation function for fields - `onFieldChange`: Callback when field values change - `onBeforeExecute`: Transform command values before execution - `style`: Custom CSS styles - `width`: Dialog width +## Callback Behavior + +- `onConfirm` is executed only after command execution succeeds. +- If `onConfirm` returns `true`, the dialog closes; otherwise it stays open. +- If `onConfirm` is not provided, `onClose(DialogResult.Ok)` is used. +- `onCancel` follows the same behavior as `Dialog` (`true` closes). +- `onClose` closes unless it returns `false`. + ## Context -`CommandDialog` is built on top of `CommandForm` and uses the command form context internally for values, validation, and execution state. +`CommandDialog` is built on top of `CommandForm` and `Dialog`, and uses 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. diff --git a/Documentation/Dialogs/dialog.md b/Documentation/Dialogs/dialog.md index 785db04..1480d2c 100644 --- a/Documentation/Dialogs/dialog.md +++ b/Documentation/Dialogs/dialog.md @@ -68,6 +68,7 @@ const MyComponent = () => { - `onClose`: Fallback close callback - `buttons`: Predefined `DialogButtons` or custom footer content - `width`: Dialog width +- `style`: Custom dialog style forwarded to PrimeReact `Dialog` - `resizable`: Enables resize - `isValid`: Enables or disables confirm actions - `okLabel`, `cancelLabel`, `yesLabel`, `noLabel`: Button labels @@ -75,4 +76,6 @@ const MyComponent = () => { ## Notes - Prefer `onConfirm` and `onCancel` over `onClose` for clear intent. +- `onConfirm` and `onCancel` should return `true` to close when used. +- `onClose` closes unless it returns `false`. - For typed, awaitable dialogs, let the dialog call `closeDialog(...)` from `useDialogContext()`. diff --git a/Source/CommandDialog/CommandDialog.stories.tsx b/Source/CommandDialog/CommandDialog.stories.tsx index 6fba6b8..8d4395c 100644 --- a/Source/CommandDialog/CommandDialog.stories.tsx +++ b/Source/CommandDialog/CommandDialog.stories.tsx @@ -126,11 +126,11 @@ const ServerValidationWrapper = () => { command={UpdateUserCommandWithServer} visible={visible} - header="Update User Information (with Server Validation)" - confirmLabel="Save" + title="Update User Information (with Server Validation)" + okLabel="Save" cancelLabel="Cancel" - onConfirm={async (commandResult) => { - setResult(JSON.stringify(commandResult)); + onConfirm={async () => { + setResult('Command executed successfully'); setVisible(false); }} onCancel={() => setVisible(false)} @@ -162,11 +162,11 @@ const AwaitableWithResultWrapper = () => { return ( command={UpdateUserCommand} - header="Update User Information (awaitable result)" - confirmLabel="Save" + title="Update User Information (awaitable result)" + okLabel="Save" cancelLabel="Cancel" autoServerValidate={false} - onConfirm={async commandResult => closeDialog(DialogResult.Ok, commandResult)} + onConfirm={async () => closeDialog(DialogResult.Ok)} onCancel={() => closeDialog(DialogResult.Cancelled)} > c.name} title="Name" placeholder="Enter name (min 2 chars)" /> @@ -252,8 +252,8 @@ const EditUserWrapper = () => { key={selectedUser?.email ?? 'empty'} initialValues={selectedUser} visible={visible} - header={`Edit User: ${selectedUser?.name ?? ''}`} - confirmLabel="Save" + title={`Edit User: ${selectedUser?.name ?? ''}`} + okLabel="Save" cancelLabel="Cancel" autoServerValidate={false} onConfirm={async () => { @@ -299,8 +299,8 @@ const CustomValidationWrapper = () => { command={UpdateUserCommand} visible={visible} - header="Add User (with Custom Validation)" - confirmLabel="Save" + title="Add User (with Custom Validation)" + okLabel="Save" cancelLabel="Cancel" autoServerValidate={false} onConfirm={async () => { @@ -350,8 +350,8 @@ const ValidationOnBlurWrapper = () => { command={UpdateUserCommand} visible={visible} - header="Validation on Blur" - confirmLabel="Save" + title="Validation on Blur" + okLabel="Save" cancelLabel="Cancel" validateOn="blur" autoServerValidate={false} @@ -381,8 +381,8 @@ const ValidationOnChangeWrapper = () => { command={UpdateUserCommand} visible={visible} - header="Validation on Change" - confirmLabel="Save" + title="Validation on Change" + okLabel="Save" cancelLabel="Cancel" validateOn="change" autoServerValidate={false} @@ -412,8 +412,8 @@ const ValidateOnInitWrapper = () => { command={UpdateUserCommand} visible={visible} - header="Validate on Initialization" - confirmLabel="Save" + title="Validate on Initialization" + okLabel="Save" cancelLabel="Cancel" validateOnInit={true} autoServerValidate={false} @@ -444,8 +444,8 @@ const ValidateAllFieldsWrapper = () => { command={UpdateUserCommand} visible={visible} - header="Validate All Fields on Change" - confirmLabel="Save" + title="Validate All Fields on Change" + okLabel="Save" cancelLabel="Cancel" validateOn="blur" validateAllFieldsOnChange={true} @@ -485,20 +485,18 @@ const BeforeExecuteWrapper = () => { command={UpdateUserCommand} visible={visible} - header="Before Execute Callback" - confirmLabel="Save" + title="Before Execute Callback" + okLabel="Save" cancelLabel="Cancel" autoServerValidate={false} initialValues={{ name: '', email: '', age: 18 }} onBeforeExecute={(command) => { command.name = command.name.trim().replace(/\s+/g, ' '); command.email = command.email.toLowerCase().trim(); + setPreprocessedData(JSON.stringify(command, null, 2)); return command; }} - onConfirm={async (result) => { - setPreprocessedData(JSON.stringify(result, null, 2)); - setVisible(false); - }} + onConfirm={async () => setVisible(false)} onCancel={() => setVisible(false)} > c.name} title="Name" placeholder='Try " Extra Spaces "' /> @@ -524,8 +522,8 @@ const WithIconsWrapper = () => { command={UpdateUserCommand} visible={visible} - header="Fields with Icons" - confirmLabel="Save" + title="Fields with Icons" + okLabel="Save" cancelLabel="Cancel" autoServerValidate={false} onConfirm={async () => setVisible(false)} @@ -619,8 +617,8 @@ const MultiColumnWrapper = () => { command={UpdateProfileCommand} visible={visible} - header="Edit Profile" - confirmLabel="Save" + title="Edit Profile" + okLabel="Save" cancelLabel="Cancel" width="70vw" autoServerValidate={false} @@ -658,8 +656,8 @@ const MixedChildrenWrapper = () => { command={UpdateProfileCommand} visible={visible} - header="Edit Profile (Mixed Children)" - confirmLabel="Save" + title="Edit Profile (Mixed Children)" + okLabel="Save" cancelLabel="Cancel" autoServerValidate={false} onConfirm={async () => setVisible(false)} diff --git a/Source/CommandDialog/CommandDialog.tsx b/Source/CommandDialog/CommandDialog.tsx index 071841e..feb0c0f 100644 --- a/Source/CommandDialog/CommandDialog.tsx +++ b/Source/CommandDialog/CommandDialog.tsx @@ -2,8 +2,8 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { ICommandResult } from '@cratis/arc/commands'; -import { DialogButtons } from '@cratis/arc.react/dialogs'; -import { Dialog } from '../Dialogs/Dialog'; +import { DialogButtons, DialogResult } from '@cratis/arc.react/dialogs'; +import { Dialog, type DialogProps } from '../Dialogs/Dialog'; import React from 'react'; import { CommandForm, @@ -13,41 +13,48 @@ import { type CommandFormProps } from '@cratis/arc.react/commands'; -export interface CommandDialogProps - extends Omit, 'children'> { - visible?: boolean; - header: string; - confirmLabel?: string; - cancelLabel?: string; - onConfirm: (result: ICommandResult) => void | Promise; - onCancel: () => void; +export interface CommandDialogProps + extends Omit, 'children'>, + Omit { children?: React.ReactNode; - style?: React.CSSProperties; - width?: string; } const CommandDialogWrapper = ({ - header, + title, visible, width, - confirmLabel, + style, + resizable, + buttons, + okLabel, cancelLabel, + yesLabel, + noLabel, + isValid, + onClose, onConfirm, onCancel, onBeforeExecute, children }: { - header: string; + title: string; visible?: boolean; - width: string; - confirmLabel: string; - cancelLabel: string; - onConfirm: (result: ICommandResult) => void | Promise; - onCancel: () => void; + width?: string; + style?: DialogProps['style']; + resizable?: boolean; + buttons?: DialogProps['buttons']; + okLabel?: string; + cancelLabel?: string; + yesLabel?: string; + noLabel?: string; + isValid?: boolean; + onClose?: DialogProps['onClose']; + onConfirm?: DialogProps['onConfirm']; + onCancel?: DialogProps['onCancel']; onBeforeExecute?: (values: TCommand) => TCommand; - children: React.ReactNode; + children?: React.ReactNode; }) => { - const { setCommandValues, setCommandResult, isValid } = useCommandFormContext(); + const { setCommandValues, setCommandResult, isValid: isCommandFormValid } = useCommandFormContext(); const commandInstance = useCommandInstance(); const handleConfirm = async () => { @@ -55,14 +62,25 @@ const CommandDialogWrapper = ({ const transformedValues = onBeforeExecute(commandInstance); setCommandValues(transformedValues); } + const result = await (commandInstance as unknown as { execute: () => Promise> }).execute(); - if (result.isSuccess) { - await onConfirm(result); - return false; - } else { + + if (!result.isSuccess) { setCommandResult(result); return false; } + + if (onConfirm) { + const closeResult = await onConfirm(); + return closeResult === true; + } + + if (onClose) { + const closeResult = await onClose(DialogResult.Ok); + return closeResult !== false; + } + + return true; }; const processChildren = (nodes: React.ReactNode): React.ReactNode => { @@ -87,18 +105,24 @@ const CommandDialogWrapper = ({ }; const processedChildren = processChildren(children); + const isDialogValid = (isValid !== false) && isCommandFormValid; return (
{processedChildren} @@ -107,27 +131,41 @@ const CommandDialogWrapper = ({ ); }; -const CommandDialogComponent = (props: CommandDialogProps) => { +const CommandDialogComponent = (props: CommandDialogProps) => { const { + title, visible, - header, - confirmLabel = 'Confirm', - cancelLabel = 'Cancel', + width, + style, + resizable, + buttons = DialogButtons.OkCancel, + okLabel, + cancelLabel, + yesLabel, + noLabel, + isValid, + onClose, onConfirm, onCancel, children, - width = '50vw', ...commandFormProps } = props; return ( {...commandFormProps}> - header={header} + title={title} visible={visible} width={width} - confirmLabel={confirmLabel} + style={style} + resizable={resizable} + buttons={buttons} + okLabel={okLabel} cancelLabel={cancelLabel} + yesLabel={yesLabel} + noLabel={noLabel} + isValid={isValid} + onClose={onClose} onConfirm={onConfirm} onCancel={onCancel} onBeforeExecute={commandFormProps.onBeforeExecute} diff --git a/Source/CommandDialog/for_CommandDialog/when_confirming_with_close_dialog_and_command_result.ts b/Source/CommandDialog/for_CommandDialog/when_confirming_with_close_dialog_and_command_result.ts index ebcd2a8..0abfe71 100644 --- a/Source/CommandDialog/for_CommandDialog/when_confirming_with_close_dialog_and_command_result.ts +++ b/Source/CommandDialog/for_CommandDialog/when_confirming_with_close_dialog_and_command_result.ts @@ -60,9 +60,9 @@ const TestDialog = () => { return React.createElement(CommandDialog, { command: TestCommand, - header: 'Update user', - onConfirm: async result => closeWithResult(DialogResult.Ok, result), - onCancel: () => closeWithResult(DialogResult.Cancelled), + title: 'Update user', + onConfirm: async () => closeWithResult(DialogResult.Ok, commandResult), + onCancel: async () => closeWithResult(DialogResult.Cancelled), }); }; diff --git a/Source/CommandDialog/for_CommandDialog/when_given_initial_valid_values.ts b/Source/CommandDialog/for_CommandDialog/when_given_initial_valid_values.ts index 2032637..a33da44 100644 --- a/Source/CommandDialog/for_CommandDialog/when_given_initial_valid_values.ts +++ b/Source/CommandDialog/for_CommandDialog/when_given_initial_valid_values.ts @@ -47,9 +47,7 @@ describe('when CommandDialog is given initial valid values', () => { command: TestCommand as unknown as new () => object, initialValues: { name: 'John Doe' } as Partial, visible: true, - header: 'Test Dialog', - onConfirm: () => {}, - onCancel: () => {} + title: 'Test Dialog' }); html = renderToStaticMarkup(element); }); diff --git a/Source/Dialogs/Dialog.tsx b/Source/Dialogs/Dialog.tsx index 82bfd36..cf7f49e 100644 --- a/Source/Dialogs/Dialog.tsx +++ b/Source/Dialogs/Dialog.tsx @@ -4,7 +4,7 @@ import { Dialog as PrimeDialog } from 'primereact/dialog'; import { Button } from 'primereact/button'; import { DialogResult, DialogButtons, useDialogContext } from '@cratis/arc.react/dialogs'; -import { ReactNode } from 'react'; +import { CSSProperties, ReactNode } from 'react'; export type CloseDialog = (result: DialogResult) => boolean | void | Promise | Promise; export type ConfirmCallback = () => boolean | void | Promise | Promise; @@ -19,6 +19,7 @@ export interface DialogProps { buttons?: DialogButtons | ReactNode; children: ReactNode; width?: string; + style?: CSSProperties; resizable?: boolean; isValid?: boolean; okLabel?: string; @@ -36,6 +37,7 @@ export const Dialog = ({ buttons = DialogButtons.OkCancel, children, width = '450px', + style, resizable = false, isValid, okLabel = 'Ok', @@ -149,7 +151,7 @@ export const Dialog = ({ // eslint-disable-next-line @typescript-eslint/no-empty-function onHide={typeof buttons === 'number' ? () => handleClose(DialogResult.Cancelled) : () => {}} visible={visible} - style={{ width }} + style={{ width, ...style }} resizable={resizable} closable={typeof buttons === 'number'}> {children}