feat: enhance confirmation dialogs and form error handling; add new utility functions

This commit is contained in:
Firman Ramdhani
2026-07-24 16:21:27 +07:00
parent b7e9af9b07
commit b332da4808
10 changed files with 117 additions and 73 deletions
@@ -97,13 +97,28 @@
"hold": {
"title": "Hold Data",
"description": "Are you sure you want to hold this data?"
},
"save": {
"title": "Save Data",
"description": "Are you sure you want to save this data?"
}
},
"draft": {
"recoveryTitle": "Draft Found",
"recoveryMessage": "You have an unsaved draft from {{date}}. Would you like to continue editing?",
"continueEditing": "Continue Editing",
"discardDraft": "Start Fresh"
"discardDraft": "Start Fresh",
"autoSaved": "Draft auto-saved",
"expired": "Previous draft has expired and was discarded"
},
"formPage": {
"createTitle": "Create {{module}}",
"editTitle": "Edit {{module}}",
"duplicateTitle": "Duplicate {{module}}",
"loadingData": "Loading data...",
"loadError": "Failed to load data",
"saveError": "Failed to save data",
"validationError": "Please fix the form errors before saving"
},
"bulkAction": {
"totalData": "Total Data",
@@ -97,13 +97,28 @@
"hold": {
"title": "Hold Data",
"description": "Apakah Anda yakin ingin menahan data ini?"
},
"save": {
"title": "Simpan Data",
"description": "Apakah Anda yakin ingin menyimpan data ini?"
}
},
"draft": {
"recoveryTitle": "Draf Ditemukan",
"recoveryMessage": "Anda memiliki draf yang belum disimpan dari {{date}}. Apakah Anda ingin melanjutkan?",
"continueEditing": "Lanjutkan",
"discardDraft": "Mulai Baru"
"discardDraft": "Mulai Baru",
"autoSaved": "Draf tersimpan otomatis",
"expired": "Draf sebelumnya telah kedaluwarsa dan dihapus"
},
"formPage": {
"createTitle": "Buat {{module}}",
"editTitle": "Ubah {{module}}",
"duplicateTitle": "Duplikat {{module}}",
"loadingData": "Memuat data...",
"loadError": "Gagal memuat data",
"saveError": "Gagal menyimpan data",
"validationError": "Mohon perbaiki kesalahan formulir sebelum menyimpan"
},
"bulkAction": {
"totalData": "Total Data",
@@ -63,6 +63,11 @@ export const ACTION_TRANSLATION_MAP: Record<string, { titleKey: string; confirmK
confirmKey: 'common:actions.hold',
descriptionKey: 'common:confirmDialog.hold.description',
},
[ModuleAction.SAVE]: {
titleKey: 'common:confirmDialog.save.title',
confirmKey: 'common:actions.save',
descriptionKey: 'common:confirmDialog.save.description',
},
};
// ---------------------------------------------------------------------------
@@ -190,53 +190,6 @@ export interface TranslationSlice {
t: (key: string, options?: Record<string, unknown>) => string;
}
// ---------------------------------------------------------------------------
// Page-Level Configurations
// ---------------------------------------------------------------------------
/**
* Lifecycle interceptors for form processing.
* @template E The base database entity.
* @template TFormData The payload structure (defaults to Partial<E> for DTOs).
*/
export interface EnterpriseFormLifecycleHooks<E extends BaseEntity, TFormData = Partial<E>> {
onValidate?: (data: TFormData) => Promise<boolean>;
beforeSave?: (data: TFormData) => Promise<TFormData>;
/** Overrides the default repository save implementation. */
save?: (data: TFormData) => Promise<E>;
afterSave?: (result: E) => Promise<void>;
afterGetData?: (data: E) => Promise<TFormData>;
}
interface BasePageConfig {
children?: ReactNode;
px?: string | number;
py?: string | number;
pageHeaderProps?: Omit<ModulePageHeaderProps, 'actions' | 'moduleKey'>;
}
export interface EnterpriseIndexPageConfig extends BasePageConfig {
customPageActions?: (actions: PageActionsProps['actions']) => PageActionsProps['actions'];
onClickCreate?: (key: string) => void;
}
export interface EnterpriseFormPageConfig<E extends BaseEntity = BaseEntity> extends EnterpriseFormLifecycleHooks<E> {
children?: ReactNode;
showPageHeader?: boolean;
useDefaultPadding?: boolean;
customHiddenActions?: (data: Partial<E>, defaultHidden: string[]) => string[];
/** Strongly typed event handler for custom form interactions. */
onCustomActionClick?: (key: ModuleActionType, data?: unknown) => void;
draftConfig?: DraftConfig;
/** Type-safe array of entity keys to exclude during an update operation. */
ignoreKeyUpdate?: (keyof E)[];
/** Type-safe array of entity keys to exclude when duplicating a record. */
ignoreKeyDuplicate?: (keyof E)[];
initialValue?: Partial<E>;
presetDuplicate?: (data: E) => Promise<Partial<E>>;
}
// ---------------------------------------------------------------------------
// Action Confirmation Modal Configuration
// ---------------------------------------------------------------------------
@@ -336,10 +289,46 @@ export interface BulkActionResult {
messages?: string[];
}
export interface PrivilegeEntity {
ALLOW_VIEW: boolean;
ALLOW_CREATE: boolean;
ALLOW_EDIT: boolean;
ALLOW_DELETE: boolean;
ALLOW_PRINT: boolean;
ALLOW_PRINT_COPY: boolean;
ALLOW_APPROVAL: boolean;
ALLOW_ACTIVATE: boolean;
ALLOW_DEACTIVATE: boolean;
ALLOW_CONFIRM: boolean;
ALLOW_CANCEL: boolean;
ALLOW_ROLLBACK: boolean;
ALLOW_HOLD: boolean;
ALLOW_LOGS: boolean;
ALLOW_NOTES: boolean;
}
// ---------------------------------------------------------------------------
// Page-Level Configurations
// ---------------------------------------------------------------------------
interface BasePageConfig {
children?: ReactNode;
px?: string | number;
py?: string | number;
pageHeaderProps?: Omit<ModulePageHeaderProps, 'actions' | 'moduleKey'>;
}
export interface EnterpriseIndexPageConfig extends BasePageConfig {
customPageActions?: (actions: PageActionsProps['actions']) => PageActionsProps['actions'];
onClickCreate?: (key: string) => void;
}
export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> extends BasePageConfig {
editMode?: 'FULL' | 'PARTIAL';
customPageActions?: (data: E, actions: PageActionsProps['actions']) => PageActionsProps['actions'];
onDetailLoaded?: (data: E) => void;
onDataLoaded?: (data: E) => void;
showHighlightData?: boolean;
showHighlightDataOnBreadcrumbs?: boolean;
@@ -375,24 +364,24 @@ export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> e
holdModalConfig?: ActionModalConfig;
}
export interface PrivilegeEntity {
ALLOW_VIEW: boolean;
ALLOW_CREATE: boolean;
ALLOW_EDIT: boolean;
ALLOW_DELETE: boolean;
export interface EnterpriseFormPageConfig<E extends BaseEntity = BaseEntity> extends BasePageConfig {
formPageType: FormPageType;
customPageActions?: (data: E, actions: PageActionsProps['actions']) => PageActionsProps['actions'];
onDataLoaded?: (data: E) => void;
ALLOW_PRINT: boolean;
ALLOW_PRINT_COPY: boolean;
showHighlightData?: boolean;
showHighlightDataOnBreadcrumbs?: boolean;
highlightDataKey?: string;
ALLOW_APPROVAL: boolean;
ALLOW_ACTIVATE: boolean;
ALLOW_DEACTIVATE: boolean;
/** Key to reference status data in the entity, used to render the status badge automatically. @default 'status' */
statusKey?: string;
/** Custom callback to provide dynamic status badge properties */
getCustomStatusBadgeConfig?: (status: string) => Partial<import('@mantine/core').BadgeProps>;
ALLOW_CONFIRM: boolean;
ALLOW_CANCEL: boolean;
ALLOW_ROLLBACK: boolean;
ALLOW_HOLD: boolean;
ALLOW_LOGS: boolean;
ALLOW_NOTES: boolean;
/** Type-safe array of entity keys to exclude during an update operation. */
ignoreKeyUpdate?: (keyof E)[];
/** Type-safe array of entity keys to exclude when duplicating a record. */
ignoreKeyDuplicate?: (keyof E)[];
initialValueCreate?: Partial<E>;
presetDuplicate?(payload: any): Promise<any>;
}
@@ -20,17 +20,17 @@ import { shortcutsData } from '../../../constants';
export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(props: EnterpriseDetailPageConfig<E>) {
const {
children,
editMode = 'FULL',
pageHeaderProps,
px,
py,
editMode = 'FULL',
showHighlightData = true,
showHighlightDataOnBreadcrumbs = true,
highlightDataKey = 'code',
statusKey = 'status',
getCustomStatusBadgeConfig,
onDetailLoaded,
onDataLoaded,
customPageActions,
onClickCreate,
@@ -77,7 +77,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
if (response && response.data) {
const data = response.data?.data;
setDetailData(data as E);
if (onDetailLoaded) onDetailLoaded(data as E);
if (onDataLoaded) onDataLoaded(data as E);
}
} catch (error: any) {
notifications.show({
@@ -377,7 +377,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
/** Platform-aware shortcut label for the Create action. */
const CREATE_SHORTCUT_LABEL = useMemo(() => {
const shortcutData = shortcutsData.find((s) => s.key === 'collapse_sidebar');
const shortcutData = shortcutsData.find((s) => s.key === 'create_data');
return IS_MACOS ? shortcutData?.macKeyIcons.join(' ') : shortcutData?.winKeyIcons.join(' ');
}, [IS_MACOS]);
@@ -37,7 +37,7 @@ export function EnterpriseIndexPageProvider(props: EnterpriseIndexPageConfig) {
/** Platform-aware shortcut label for the Create action. */
const CREATE_SHORTCUT_LABEL = useMemo(() => {
const shortcutData = shortcutsData.find((s) => s.key === 'collapse_sidebar');
const shortcutData = shortcutsData.find((s) => s.key === 'create_data');
return IS_MACOS ? shortcutData?.macKeyIcons.join(' ') : shortcutData?.winKeyIcons.join(' ');
}, [IS_MACOS]);
+3 -1
View File
@@ -13,12 +13,14 @@
},
"dependencies": {
"crypto-js": "^4.2.0",
"dayjs": "^1.11.19"
"dayjs": "^1.11.19",
"lodash": "^4.18.1"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/crypto-js": "^4.2.2",
"@types/lodash": "^4.17.24",
"eslint": "^8.57.1",
"typescript": "5.5.4",
"vitest": "^4.0.17"
+1
View File
@@ -4,3 +4,4 @@ export * from './encryption/encryption.utils';
export * from './date/date.utils';
export * from './currency/currency.utils';
export * from './string/string.utils';
export * from './lodash/lodash.utils';
@@ -0,0 +1,3 @@
import lodash from 'lodash';
export { lodash };