feat: enhance confirmation dialogs and form error handling; add new utility functions
This commit is contained in:
@@ -97,13 +97,28 @@
|
|||||||
"hold": {
|
"hold": {
|
||||||
"title": "Hold Data",
|
"title": "Hold Data",
|
||||||
"description": "Are you sure you want to hold this 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": {
|
"draft": {
|
||||||
"recoveryTitle": "Draft Found",
|
"recoveryTitle": "Draft Found",
|
||||||
"recoveryMessage": "You have an unsaved draft from {{date}}. Would you like to continue editing?",
|
"recoveryMessage": "You have an unsaved draft from {{date}}. Would you like to continue editing?",
|
||||||
"continueEditing": "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": {
|
"bulkAction": {
|
||||||
"totalData": "Total Data",
|
"totalData": "Total Data",
|
||||||
|
|||||||
@@ -97,13 +97,28 @@
|
|||||||
"hold": {
|
"hold": {
|
||||||
"title": "Hold Data",
|
"title": "Hold Data",
|
||||||
"description": "Apakah Anda yakin ingin menahan data ini?"
|
"description": "Apakah Anda yakin ingin menahan data ini?"
|
||||||
|
},
|
||||||
|
"save": {
|
||||||
|
"title": "Simpan Data",
|
||||||
|
"description": "Apakah Anda yakin ingin menyimpan data ini?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"draft": {
|
"draft": {
|
||||||
"recoveryTitle": "Draf Ditemukan",
|
"recoveryTitle": "Draf Ditemukan",
|
||||||
"recoveryMessage": "Anda memiliki draf yang belum disimpan dari {{date}}. Apakah Anda ingin melanjutkan?",
|
"recoveryMessage": "Anda memiliki draf yang belum disimpan dari {{date}}. Apakah Anda ingin melanjutkan?",
|
||||||
"continueEditing": "Lanjutkan",
|
"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": {
|
"bulkAction": {
|
||||||
"totalData": "Total Data",
|
"totalData": "Total Data",
|
||||||
|
|||||||
+5
@@ -63,6 +63,11 @@ export const ACTION_TRANSLATION_MAP: Record<string, { titleKey: string; confirmK
|
|||||||
confirmKey: 'common:actions.hold',
|
confirmKey: 'common:actions.hold',
|
||||||
descriptionKey: 'common:confirmDialog.hold.description',
|
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;
|
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
|
// Action Confirmation Modal Configuration
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -336,10 +289,46 @@ export interface BulkActionResult {
|
|||||||
messages?: string[];
|
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 {
|
export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> extends BasePageConfig {
|
||||||
editMode?: 'FULL' | 'PARTIAL';
|
editMode?: 'FULL' | 'PARTIAL';
|
||||||
customPageActions?: (data: E, actions: PageActionsProps['actions']) => PageActionsProps['actions'];
|
customPageActions?: (data: E, actions: PageActionsProps['actions']) => PageActionsProps['actions'];
|
||||||
onDetailLoaded?: (data: E) => void;
|
onDataLoaded?: (data: E) => void;
|
||||||
|
|
||||||
showHighlightData?: boolean;
|
showHighlightData?: boolean;
|
||||||
showHighlightDataOnBreadcrumbs?: boolean;
|
showHighlightDataOnBreadcrumbs?: boolean;
|
||||||
@@ -375,24 +364,24 @@ export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> e
|
|||||||
holdModalConfig?: ActionModalConfig;
|
holdModalConfig?: ActionModalConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrivilegeEntity {
|
export interface EnterpriseFormPageConfig<E extends BaseEntity = BaseEntity> extends BasePageConfig {
|
||||||
ALLOW_VIEW: boolean;
|
formPageType: FormPageType;
|
||||||
ALLOW_CREATE: boolean;
|
customPageActions?: (data: E, actions: PageActionsProps['actions']) => PageActionsProps['actions'];
|
||||||
ALLOW_EDIT: boolean;
|
onDataLoaded?: (data: E) => void;
|
||||||
ALLOW_DELETE: boolean;
|
|
||||||
|
|
||||||
ALLOW_PRINT: boolean;
|
showHighlightData?: boolean;
|
||||||
ALLOW_PRINT_COPY: boolean;
|
showHighlightDataOnBreadcrumbs?: boolean;
|
||||||
|
highlightDataKey?: string;
|
||||||
|
|
||||||
ALLOW_APPROVAL: boolean;
|
/** Key to reference status data in the entity, used to render the status badge automatically. @default 'status' */
|
||||||
ALLOW_ACTIVATE: boolean;
|
statusKey?: string;
|
||||||
ALLOW_DEACTIVATE: boolean;
|
/** Custom callback to provide dynamic status badge properties */
|
||||||
|
getCustomStatusBadgeConfig?: (status: string) => Partial<import('@mantine/core').BadgeProps>;
|
||||||
|
|
||||||
ALLOW_CONFIRM: boolean;
|
/** Type-safe array of entity keys to exclude during an update operation. */
|
||||||
ALLOW_CANCEL: boolean;
|
ignoreKeyUpdate?: (keyof E)[];
|
||||||
ALLOW_ROLLBACK: boolean;
|
/** Type-safe array of entity keys to exclude when duplicating a record. */
|
||||||
ALLOW_HOLD: boolean;
|
ignoreKeyDuplicate?: (keyof E)[];
|
||||||
|
initialValueCreate?: Partial<E>;
|
||||||
ALLOW_LOGS: boolean;
|
presetDuplicate?(payload: any): Promise<any>;
|
||||||
ALLOW_NOTES: boolean;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,17 +20,17 @@ import { shortcutsData } from '../../../constants';
|
|||||||
export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(props: EnterpriseDetailPageConfig<E>) {
|
export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(props: EnterpriseDetailPageConfig<E>) {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
editMode = 'FULL',
|
|
||||||
pageHeaderProps,
|
pageHeaderProps,
|
||||||
px,
|
px,
|
||||||
py,
|
py,
|
||||||
|
|
||||||
|
editMode = 'FULL',
|
||||||
showHighlightData = true,
|
showHighlightData = true,
|
||||||
showHighlightDataOnBreadcrumbs = true,
|
showHighlightDataOnBreadcrumbs = true,
|
||||||
highlightDataKey = 'code',
|
highlightDataKey = 'code',
|
||||||
statusKey = 'status',
|
statusKey = 'status',
|
||||||
getCustomStatusBadgeConfig,
|
getCustomStatusBadgeConfig,
|
||||||
onDetailLoaded,
|
onDataLoaded,
|
||||||
|
|
||||||
customPageActions,
|
customPageActions,
|
||||||
onClickCreate,
|
onClickCreate,
|
||||||
@@ -77,7 +77,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
|||||||
if (response && response.data) {
|
if (response && response.data) {
|
||||||
const data = response.data?.data;
|
const data = response.data?.data;
|
||||||
setDetailData(data as E);
|
setDetailData(data as E);
|
||||||
if (onDetailLoaded) onDetailLoaded(data as E);
|
if (onDataLoaded) onDataLoaded(data as E);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
@@ -377,7 +377,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
|
|||||||
|
|
||||||
/** Platform-aware shortcut label for the Create action. */
|
/** Platform-aware shortcut label for the Create action. */
|
||||||
const CREATE_SHORTCUT_LABEL = useMemo(() => {
|
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(' ');
|
return IS_MACOS ? shortcutData?.macKeyIcons.join(' ') : shortcutData?.winKeyIcons.join(' ');
|
||||||
}, [IS_MACOS]);
|
}, [IS_MACOS]);
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function EnterpriseIndexPageProvider(props: EnterpriseIndexPageConfig) {
|
|||||||
|
|
||||||
/** Platform-aware shortcut label for the Create action. */
|
/** Platform-aware shortcut label for the Create action. */
|
||||||
const CREATE_SHORTCUT_LABEL = useMemo(() => {
|
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(' ');
|
return IS_MACOS ? shortcutData?.macKeyIcons.join(' ') : shortcutData?.winKeyIcons.join(' ');
|
||||||
}, [IS_MACOS]);
|
}, [IS_MACOS]);
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"dayjs": "^1.11.19"
|
"dayjs": "^1.11.19",
|
||||||
|
"lodash": "^4.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@repo/eslint-config": "workspace:*",
|
||||||
"@repo/typescript-config": "workspace:*",
|
"@repo/typescript-config": "workspace:*",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/lodash": "^4.17.24",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"typescript": "5.5.4",
|
"typescript": "5.5.4",
|
||||||
"vitest": "^4.0.17"
|
"vitest": "^4.0.17"
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export * from './encryption/encryption.utils';
|
|||||||
export * from './date/date.utils';
|
export * from './date/date.utils';
|
||||||
export * from './currency/currency.utils';
|
export * from './currency/currency.utils';
|
||||||
export * from './string/string.utils';
|
export * from './string/string.utils';
|
||||||
|
export * from './lodash/lodash.utils';
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import lodash from 'lodash';
|
||||||
|
|
||||||
|
export { lodash };
|
||||||
Generated
+14
@@ -670,6 +670,9 @@ importers:
|
|||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.19
|
specifier: ^1.11.19
|
||||||
version: 1.11.19
|
version: 1.11.19
|
||||||
|
lodash:
|
||||||
|
specifier: ^4.18.1
|
||||||
|
version: 4.18.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@repo/eslint-config':
|
'@repo/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
@@ -680,6 +683,9 @@ importers:
|
|||||||
'@types/crypto-js':
|
'@types/crypto-js':
|
||||||
specifier: ^4.2.2
|
specifier: ^4.2.2
|
||||||
version: 4.2.2
|
version: 4.2.2
|
||||||
|
'@types/lodash':
|
||||||
|
specifier: ^4.17.24
|
||||||
|
version: 4.17.24
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.1
|
specifier: ^8.57.1
|
||||||
version: 8.57.1
|
version: 8.57.1
|
||||||
@@ -3835,6 +3841,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/lodash@4.17.24:
|
||||||
|
resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/markdown-it@14.1.2:
|
/@types/markdown-it@14.1.2:
|
||||||
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8815,6 +8825,10 @@ packages:
|
|||||||
/lodash@4.17.21:
|
/lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
|
/lodash@4.18.1:
|
||||||
|
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/log-symbols@4.1.0:
|
/log-symbols@4.1.0:
|
||||||
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|||||||
Reference in New Issue
Block a user