ui phase 11

This commit is contained in:
hamid
2026-07-19 19:19:44 +03:30
parent b4b8c9ea79
commit 87fa4cd497
74 changed files with 3115 additions and 506 deletions
@@ -41,4 +41,58 @@ describe('<ConfirmDialog/>', () => {
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onClose).toHaveBeenCalledTimes(1);
});
it('renders the typed-confirmation field when requireTypedConfirmation is set', () => {
wrap(
<ConfirmDialog
open
onConfirm={jest.fn()}
{...base}
requireTypedConfirmation={['CONFIRM', '150000']}
typedConfirmationLabel="Type to confirm"
/>,
);
expect(screen.getByLabelText('Type to confirm')).toBeInTheDocument();
});
it('keeps confirm disabled until the typed value matches, case/whitespace-insensitively', () => {
const onConfirm = jest.fn();
wrap(
<ConfirmDialog
open
onConfirm={onConfirm}
{...base}
requireTypedConfirmation={['CONFIRM', '150000']}
typedConfirmationLabel="Type to confirm"
/>,
);
const confirmBtn = screen.getByRole('button', { name: 'Run' });
const field = screen.getByLabelText('Type to confirm');
expect(confirmBtn).toBeDisabled();
fireEvent.change(field, { target: { value: 'nope' } });
expect(confirmBtn).toBeDisabled();
fireEvent.change(field, { target: { value: ' confirm ' } });
expect(confirmBtn).not.toBeDisabled();
fireEvent.click(confirmBtn);
expect(onConfirm).toHaveBeenCalledWith(undefined);
});
it('enables confirm on an exact match against a different allowed value (e.g. an amount)', () => {
wrap(
<ConfirmDialog
open
onConfirm={jest.fn()}
{...base}
requireTypedConfirmation={['CONFIRM', '150000']}
typedConfirmationLabel="Type to confirm"
/>,
);
const confirmBtn = screen.getByRole('button', { name: 'Run' });
fireEvent.change(screen.getByLabelText('Type to confirm'), { target: { value: '150000' } });
expect(confirmBtn).not.toBeDisabled();
});
});
@@ -27,6 +27,15 @@ export interface ConfirmDialogProps {
requireReason?: boolean;
reasonLabel?: string;
reasonPlaceholder?: string;
/**
* When set, confirm stays disabled until the typed value case-insensitively matches ONE of these
* (e.g. the literal word «تایید» OR the exact amount digit-string) — the guard for an irreversible
* action like running a payout batch. A gate, not a payload: `onConfirm`'s signature is unchanged, the
* typed value itself is never passed to the caller.
*/
requireTypedConfirmation?: string[];
typedConfirmationLabel?: string;
typedConfirmationPlaceholder?: string;
/** MUI color for the confirm button — `error` for a destructive action. */
confirmColor?: 'primary' | 'error' | 'secondary';
}
@@ -52,21 +61,31 @@ const ConfirmDialog: FunctionComponent<ConfirmDialogProps> = ({
requireReason = false,
reasonLabel,
reasonPlaceholder,
requireTypedConfirmation,
typedConfirmationLabel,
typedConfirmationPlaceholder,
confirmColor = 'primary',
}) => {
const [reason, setReason] = useState('');
const [typedValue, setTypedValue] = useState('');
const close = () => {
setReason('');
setTypedValue('');
onClose();
};
const confirm = () => {
onConfirm(requireReason ? reason.trim() : undefined);
setReason('');
setTypedValue('');
};
const confirmDisabled = loading || (requireReason && reason.trim().length === 0);
const confirmDisabled =
loading ||
(requireReason && reason.trim().length === 0) ||
(requireTypedConfirmation != null &&
!requireTypedConfirmation.some((v) => v.trim().toLowerCase() === typedValue.trim().toLowerCase()));
return (
<Dialog open={open} onClose={loading ? undefined : close} fullWidth maxWidth="xs">
@@ -92,6 +111,17 @@ const ConfirmDialog: FunctionComponent<ConfirmDialogProps> = ({
sx={{ mt: 2 }}
/>
) : null}
{requireTypedConfirmation ? (
<TextField
autoFocus
fullWidth
value={typedValue}
onChange={(e) => setTypedValue(e.target.value)}
label={typedConfirmationLabel}
placeholder={typedConfirmationPlaceholder}
sx={{ mt: 2 }}
/>
) : null}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<AppButton variant="text" color="inherit" onClick={close} disabled={loading}>