Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | /** * ActionButton — permission-aware button that triggers an action from the registry. * * Supports: * - Action name string → looks up action registry, calls via useAction * - Confirmation dialog before invocation * - Loading indicator while in-flight * - Submit form before calling (when submit=true) * - Split-button variant when menu items are provided */ import {confirmDialog, SplitButton} from '../../primereact/index.js'; import {useQueryClient} from '@tanstack/react-query'; import {useRef, useState} from 'react'; import {useAction} from '../../hooks/useAction.js'; import {usePermission} from '../../hooks/usePermission.js'; import type {IToolbarButton} from '../../index.js'; import {useAppStore} from '../../state/appStore.js'; import {Button} from '../Button/Button.js'; export interface IActionButtonProps extends IToolbarButton { /** Ref to the form element to submit before calling */ formId?: string; className?: string; /** Called with true when the action starts, false when it finishes */ onBusyChange?: (busy: boolean) => void; /** External disabled override — disables the button regardless of its configured `enabled` */ disabled?: boolean; } export function ActionButton({ label, icon, action: actionRef, method, permission, submit: shouldSubmit = false, confirm: confirmMessage, enabled = true, visible = true, menu, params: extraParams, successHint, refresh, formId, className = '', onBusyChange, disabled: externalDisabled = false, }: IActionButtonProps) { const permitted = usePermission(permission); const [loading, setLoading] = useState(false); const queryClient = useQueryClient(); const actionMethod = typeof actionRef === 'string' ? actionRef : actionRef?.method; const actionParamsOverride = typeof actionRef === 'object' && actionRef !== null ? actionRef.params : undefined; // extraParams may be a pre-resolved Record; if it arrives as a non-object (rare, e.g. a // primitive wrapped by resolveTemplate), treat it as the whole call payload. const mergedParams = extraParams !== null && typeof extraParams === 'object' && !Array.isArray(extraParams) ? {...(actionParamsOverride ?? {}), ...(extraParams as Record<string, unknown>)} : {...(actionParamsOverride ?? {})}; const callParams = extraParams !== null && typeof extraParams === 'object' && !Array.isArray(extraParams) ? mergedParams : ((extraParams as Record<string, unknown> | undefined) ?? mergedParams); const directMethod = method ?? actionMethod ?? ''; const testId = directMethod ? `action-${directMethod.replace(/[/.]/g, '-')}` : undefined; const {call} = useAction(directMethod, 'mutation', mergedParams); const clearError = useAppStore(s => s.clearError); const showHint = useAppStore(s => s.showHint); const hintTargetRef = useRef<HTMLSpanElement>(null); if (!visible || !permitted) return null; const isDisabled = externalDisabled || enabled === false || loading; const doCall = async () => { if (shouldSubmit && formId) { const form = document.getElementById(formId) as HTMLFormElement | null; if (form) { // Trigger native form submit; the form's onSubmit handles validation form.requestSubmit(); return; } } setLoading(true); onBusyChange?.(true); try { await call(callParams); if (refresh && directMethod) { const ns = directMethod.split('.').slice(0, -1).join('.'); if (ns) { void queryClient.invalidateQueries({queryKey: [ns + '.find']}); void queryClient.invalidateQueries({queryKey: [ns + '.get']}); } } if (successHint) showHint(hintTargetRef.current, successHint, false); } catch (err: unknown) { const e = err as {print?: string; message?: string}; clearError(); // dismiss the global error dialog — we're showing it locally showHint(hintTargetRef.current, e.print ?? e.message ?? 'Error', true); } finally { setLoading(false); onBusyChange?.(false); } }; const handleClick = () => { if (confirmMessage) { confirmDialog({ message: confirmMessage, header: 'Confirm', icon: 'pi pi-exclamation-triangle', accept: () => void doCall(), }); } else { void doCall(); } }; const buttonProps = { label, icon: loading ? 'pi pi-spinner pi-spin' : icon, disabled: isDisabled, onClick: handleClick, className: `blong-action-btn ${className}`, }; if (menu && menu.length > 0) { const splitItems = menu.map(item => ({ label: item.label, icon: item.icon, command: () => { const subAction = typeof item.action === 'string' ? item.action : item.action?.method; if (subAction) void call({...mergedParams, _action: subAction}); }, })); return ( <span ref={hintTargetRef} style={{display: 'inline-block'}} data-testid={testId} > <SplitButton {...buttonProps} model={splitItems} /> </span> ); } return ( <span ref={hintTargetRef} style={{display: 'inline-block'}} data-testid={testId} > <Button {...buttonProps} /> </span> ); } |