feat: add StatusBadge component and integrate action confirmation modal

- Introduced StatusBadge component for displaying various status indicators with customizable colors and icons.
- Updated ModulePageHeader to conditionally render badges based on props.
- Implemented ActionConfirmationModal for handling lifecycle actions (delete, activate, etc.) with support for forms and custom body rendering.
- Enhanced EnterpriseDetailPageProvider to manage action confirmation modal state and execution of actions.
- Added new modal configurations for various actions in EnterpriseDetailPageConfig.
- Updated theme provider to include ModalsProvider and Notifications for better user feedback.
This commit is contained in:
Firman Ramdhani
2026-07-15 17:27:29 +07:00
parent 7ce4ced7d6
commit 43f9d3d491
21 changed files with 966 additions and 93 deletions
@@ -0,0 +1,222 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Button, Group, Modal, Stack, Text } from '@mantine/core';
import { useForm, FormProvider } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import type { BaseEntity } from '@repo/core-api/data-services';
import type { ActionModalConfig, ActionModalState, ModuleActionType } from '../entities/entity';
import { ModuleAction } from '../entities/entity';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface ActionConfirmationModalProps<E extends BaseEntity = BaseEntity> {
/** Current modal state (opened, action, data, config) */
modalState: ActionModalState<E>;
/** Close the modal */
onClose: () => void;
/** Execute the action with optional meta from form */
onExecute: (action: ModuleActionType, data: E, meta?: Record<string, unknown>) => Promise<void>;
/** Translation function scoped to [moduleNamespace, 'common'] */
t: (key: string, options?: Record<string, unknown>) => string;
}
// ---------------------------------------------------------------------------
// Default Translation Keys per Action
// ---------------------------------------------------------------------------
export const ACTION_TRANSLATION_MAP: Record<string, { titleKey: string; confirmKey: string; descriptionKey: string }> =
{
[ModuleAction.DELETE]: {
titleKey: 'common:confirmDialog.delete.title',
confirmKey: 'common:actions.delete',
descriptionKey: 'common:confirmDialog.delete.description',
},
[ModuleAction.ACTIVATE]: {
titleKey: 'common:confirmDialog.activate.title',
confirmKey: 'common:actions.activate',
descriptionKey: 'common:confirmDialog.activate.description',
},
[ModuleAction.DEACTIVATE]: {
titleKey: 'common:confirmDialog.deactivate.title',
confirmKey: 'common:actions.deactivate',
descriptionKey: 'common:confirmDialog.deactivate.description',
},
[ModuleAction.CONFIRM]: {
titleKey: 'common:confirmDialog.confirm.title',
confirmKey: 'common:actions.confirm',
descriptionKey: 'common:confirmDialog.confirm.description',
},
[ModuleAction.CANCEL]: {
titleKey: 'common:confirmDialog.cancel.title',
confirmKey: 'common:actions.cancel_action',
descriptionKey: 'common:confirmDialog.cancel.description',
},
[ModuleAction.ROLLBACK]: {
titleKey: 'common:confirmDialog.rollback.title',
confirmKey: 'common:actions.rollback',
descriptionKey: 'common:confirmDialog.rollback.description',
},
[ModuleAction.HOLD]: {
titleKey: 'common:confirmDialog.hold.title',
confirmKey: 'common:actions.hold',
descriptionKey: 'common:confirmDialog.hold.description',
},
};
// ---------------------------------------------------------------------------
// Inner Form Body (memoized to prevent re-renders of the entire modal)
// ---------------------------------------------------------------------------
const ModalFormBody = React.memo(function ModalFormBody<
TMeta extends Record<string, unknown> = Record<string, unknown>,
>({
renderBody,
form,
}: {
renderBody: NonNullable<ActionModalConfig<TMeta>['renderBody']>;
form?: ReturnType<typeof useForm<TMeta>>;
}) {
return <>{renderBody(form)}</>;
}) as <TMeta extends Record<string, unknown>>(props: {
renderBody: NonNullable<ActionModalConfig<TMeta>['renderBody']>;
form?: ReturnType<typeof useForm<TMeta>>;
}) => React.ReactElement;
// ---------------------------------------------------------------------------
// ActionConfirmationModal
// ---------------------------------------------------------------------------
/**
* Controlled confirmation modal for lifecycle actions (delete, activate, etc.).
*
* Supports three modes:
* 1. **Simple** — no custom body, just title + default description + confirm/cancel.
* 2. **Static body** — custom body without form (informational content).
* 3. **Form body** — custom body with RHF FormProvider for validation + meta payload.
*
* @performance
* - Modal is NOT mounted when `opened === false` (uses `keepMounted={false}`).
* - Form is created with `mode: 'onSubmit'` to avoid re-renders on every keystroke.
* - Inner body is wrapped in React.memo to isolate re-renders from modal state changes.
*/
export function ActionConfirmationModal<E extends BaseEntity = BaseEntity>(props: ActionConfirmationModalProps<E>) {
const { modalState, onClose, onExecute, t } = props;
const { opened, action, data, config } = modalState;
const [isSubmitting, setIsSubmitting] = useState(false);
// Determine if this modal has a form (schema + defaultValues provided)
const hasForm = Boolean(config?.schema && config?.defaultValues);
// Create form instance only when form mode is active.
// `useForm` is always called (Rules of Hooks) but resolver/defaultValues
// are conditionally set so it becomes a no-op when hasForm is false.
const form = useForm<Record<string, unknown>>({
mode: 'onSubmit',
resolver: config?.schema ? zodResolver(config.schema as any) : undefined,
defaultValues: config?.defaultValues ?? {},
});
// Resolve translation keys for the current action
const translations = useMemo(() => {
const actionKey = action ?? '';
const map = ACTION_TRANSLATION_MAP[actionKey];
return {
title: config?.title ?? (map ? t(map.titleKey) : (action ?? '')),
confirmLabel: config?.confirmLabel ?? (map ? t(map.confirmKey) : t('common:actions.confirm')),
cancelLabel: config?.cancelLabel ?? t('common:actions.cancel'),
description: map ? t(map.descriptionKey) : '',
};
}, [action, config?.title, config?.confirmLabel, config?.cancelLabel, t]);
// Determine button color based on action type
const confirmColor = useMemo(() => {
if (action === ModuleAction.DELETE) return 'red';
// if (action === ModuleAction.DEACTIVATE || action === ModuleAction.CANCEL) return 'orange';
return 'brand';
}, [action]);
// Handle confirm button click
const handleConfirmClick = useCallback(async () => {
if (!action || !data) return;
if (hasForm) {
// Trigger RHF validation, then execute if valid
const isValid = await form.trigger();
if (!isValid) return;
const formValues = form.getValues();
setIsSubmitting(true);
try {
await onExecute(action, data, formValues);
} finally {
setIsSubmitting(false);
}
} else {
// No form — execute directly
setIsSubmitting(true);
try {
await onExecute(action, data, undefined);
} finally {
setIsSubmitting(false);
}
}
}, [action, data, hasForm, form, onExecute]);
// Handle modal close (prevent close while submitting)
const handleClose = useCallback(() => {
if (isSubmitting) return;
form.reset();
onClose();
}, [isSubmitting, form, onClose]);
return (
<Modal
opened={opened}
onClose={handleClose}
title={translations.title}
size={config?.size ?? 'md'}
centered
closeOnClickOutside={!isSubmitting}
closeOnEscape={!isSubmitting}
keepMounted={false}
padding="lg"
styles={{
title: { fontWeight: 600, fontSize: 'var(--mantine-font-size-xl)' },
header: { paddingBottom: 'var(--mantine-spacing-md)' },
}}
>
<Stack gap="xl">
{/* Body: custom or default description */}
{config?.renderBody ? (
hasForm ? (
<FormProvider {...form}>
<ModalFormBody renderBody={config.renderBody} form={form} />
</FormProvider>
) : (
<ModalFormBody renderBody={config.renderBody} />
)
) : (
translations.description && (
<Text size="md" c="dimmed">
{translations.description}
</Text>
)
)}
{/* Footer actions */}
<Group justify="flex-end" mt="sm">
<Button size="xs" variant="default" onClick={handleClose} disabled={isSubmitting}>
{translations.cancelLabel}
</Button>
<Button size="xs" color={confirmColor} onClick={handleConfirmClick} loading={isSubmitting}>
{translations.confirmLabel}
</Button>
</Group>
</Stack>
</Modal>
);
}
@@ -32,7 +32,9 @@ export interface ModulePageHeaderProps {
miniTitleProps?: Omit<TitleProps, 'children'>;
description?: React.ReactNode;
icon?: LucideIcon;
badges?: React.ReactNode;
showBadges?: boolean;
breadcrumbs?: BreadcrumbItem[];
showPageHeader?: boolean;
@@ -148,6 +150,8 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
description,
icon: Icon,
badges,
showBadges = true,
breadcrumbs,
actions,
customButtonProps,
@@ -209,7 +213,7 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
{title}
</Title>
)}
{badges && <Box style={{ flexShrink: 0 }}>{badges}</Box>}
{showBadges && badges && <Box style={{ flexShrink: 0 }}>{badges}</Box>}
</Flex>
{/* Right: actions + inline toggle (separated by divider) */}
@@ -304,7 +308,7 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
{title}
</Title>
)}
{badges && <Box>{badges}</Box>}
{showBadges && badges && <Box>{badges}</Box>}
</Flex>
{description && (
@@ -312,7 +316,7 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
fz={{ base: 'xs', sm: 'sm' }}
c="dimmed"
fw={400}
lineClamp={2}
lineClamp={1}
mt={4}
style={{
wordBreak: 'break-word',
@@ -1,4 +1,6 @@
import { ReactNode } from 'react';
import type { UseFormReturn } from 'react-hook-form';
import type { ZodType } from 'zod';
import type { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
import type { ModulePageHeaderProps } from '../components/module-page-header';
import { PageActionsProps } from '../../../components';
@@ -219,9 +221,84 @@ export interface EnterpriseFormPageConfig<E extends BaseEntity = BaseEntity> ext
presetDuplicate?: (data: E) => Promise<Partial<E>>;
}
// ---------------------------------------------------------------------------
// Action Confirmation Modal Configuration
// ---------------------------------------------------------------------------
/**
* Configuration for the confirmation modal shown before executing a lifecycle action.
*
* Supports three modes:
* 1. **Simple confirmation** — no `renderBody`, just a confirm/cancel dialog.
* 2. **Static body** — `renderBody` returns static JSX (text, checkboxes, etc.).
* 3. **Form body** — `renderBody` receives an RHF `UseFormReturn` instance.
* The implementer renders `Field*` components bound to the form.
* Form data is validated via `schema` before submission and sent as `meta`.
*
* @template TMeta The shape of the form data (defaults to Record<string, unknown>).
*/
export interface ActionModalConfig<TMeta extends Record<string, unknown> = Record<string, unknown>> {
/** Modal title override. Defaults to action-specific translation (e.g., "Delete Item?") */
title?: string;
/**
* Custom modal body renderer.
*
* When provided WITHOUT `schema`/`defaultValues`, receives `undefined` — render static content.
* When provided WITH `schema`/`defaultValues`, receives a fully-typed `UseFormReturn<TMeta>`
* instance. Use `Field*` components from `@repo/ui/form` bound to this form.
*
* @example
* ```tsx
* // Static body (no form)
* renderBody: () => <Text>Are you sure you want to delete this item?</Text>
*
* // Form body
* renderBody: (form) => (
* <FieldTextarea name="reason" control={form.control} label="Reason" />
* )
* ```
*/
renderBody?: (form?: UseFormReturn<TMeta>) => ReactNode;
/** Zod schema for validating the form body. When omitted, no validation is applied */
schema?: ZodType<TMeta>;
/** Default values for the form. Required when schema is provided */
defaultValues?: TMeta;
/** Confirm button label override. Defaults to action translation */
confirmLabel?: string;
/** Cancel button label override. Defaults to t('common:actions.cancel') */
cancelLabel?: string;
/** Size of the modal. @default 'md' */
size?: string | number;
/** Custom success message or a function to generate it after action completes successfully */
successMessage?: string | ((data?: any) => string);
/** Custom error message or a function to extract it from the network error */
errorMessage?: string | ((error: any) => string);
}
/**
* Internal state for the action confirmation modal managed by the detail page provider.
* @internal
*/
export interface ActionModalState<E extends BaseEntity = BaseEntity> {
opened: boolean;
action: ModuleActionType | null;
data: E | null;
config?: ActionModalConfig<any>;
}
export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> extends BasePageConfig {
editMode?: 'FULL' | 'PARTIAL';
customPageActions?: (data: E, actions: PageActionsProps['actions']) => PageActionsProps['actions'];
onDetailLoaded?: (data: E) => void;
showHighlightData?: boolean;
showHighlightDataOnBreadcrumbs?: boolean;
highlightDataKey?: string;
/** 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 */
getCustomStatusConfig?: (status: string) => Partial<import('@mantine/core').BadgeProps>;
onClickCreate?: () => void;
onClickDuplicate?: (data: E) => void;
@@ -238,9 +315,14 @@ export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> e
onClickRollback?: (data: E) => void;
onClickHold?: (data: E) => void;
showHighlightData?: boolean;
showHighlightDataOnBreadcrumbs?: boolean;
highlightDataKey?: string;
// Action Modal Configurations
deleteModalConfig?: ActionModalConfig;
activateModalConfig?: ActionModalConfig;
deactivateModalConfig?: ActionModalConfig;
confirmModalConfig?: ActionModalConfig;
cancelModalConfig?: ActionModalConfig;
rollbackModalConfig?: ActionModalConfig;
holdModalConfig?: ActionModalConfig;
}
export interface PrivilegeEntity {
@@ -8,3 +8,4 @@ export * from './providers/module.provider';
export * from './providers/index-page.provider';
export * from './providers/detail-page.provider';
export * from './components/module-page-header';
export * from './components/action-confirmation-modal';
@@ -4,9 +4,11 @@ import { BaseEntity } from '@repo/core-api/data-services';
import { Check, CheckCircle, Copy, Edit2, PauseCircle, Plus, RotateCcw, Trash2, X, XCircle } from 'lucide-react';
import { DetailPageContext } from '../hooks/use-detail-page.context';
import { EnterpriseDetailPageConfig, ModuleAction, ModuleActionType } from '../entities/entity';
import { CorePageContainer, PageActionProps } from '../../../components';
import { EnterpriseDetailPageConfig, ModuleAction, ModuleActionType, ActionModalState } from '../entities/entity';
import { CorePageContainer, PageActionProps, StatusBadge } from '../../../components';
import { ModulePageHeader, ModulePageHeaderProps } from '../components/module-page-header';
import { ActionConfirmationModal, ACTION_TRANSLATION_MAP } from '../components/action-confirmation-modal';
import { notifications } from '@mantine/notifications';
import {
useEnterpriseModuleConfigContext,
useEnterpriseModuleDataServiceContext,
@@ -22,6 +24,14 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
pageHeaderProps,
px,
py,
showHighlightData = true,
showHighlightDataOnBreadcrumbs = true,
highlightDataKey = 'code',
statusKey = 'status',
getCustomStatusConfig,
onDetailLoaded,
customPageActions,
onClickCreate,
onClickDuplicate,
@@ -34,9 +44,14 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
onClickRollback,
onClickHold,
showHighlightData = true,
showHighlightDataOnBreadcrumbs = true,
highlightDataKey = 'code',
// Modal configurations per action
deleteModalConfig,
activateModalConfig,
deactivateModalConfig,
confirmModalConfig,
cancelModalConfig,
rollbackModalConfig,
holdModalConfig,
} = props;
const { t } = useEnterpriseModuleTranslationContext();
@@ -51,7 +66,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
const { moduleKey, moduleType } = config;
const [detailData, setDetailData] = useState<E | any>({ id: 1, code: 'ABC-001' });
const [detailData, setDetailData] = useState<E | any>();
const [isLoading, setIsLoading] = useState(false);
const loadData = useCallback(async () => {
@@ -60,9 +75,16 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
try {
const response = await dataServices.getOne(dataId);
if (response && response.data) {
setDetailData(response.data as E);
const data = response.data;
setDetailData(data as E);
if (onDetailLoaded) onDetailLoaded(data as E);
}
} catch (error) {
// FIXME => remove this example later;
const data = { id: 1, code: 'ABCD-0001', status: 'open' } as any;
setDetailData(data as E);
if (onDetailLoaded) onDetailLoaded(data as E);
console.error('Failed to load detail data', error);
} finally {
setIsLoading(false);
@@ -74,41 +96,178 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
}, [loadData]);
// ---------------------------------------------------------------------------
// 1. Stub Handlers (Sudah diperbaiki typonya & lengkap)
// 1. Action Confirmation Modal State & Handlers
// ---------------------------------------------------------------------------
async function handleDelete(data: E): Promise<void> {
// implementation later
console.log({ data });
/** Default (closed) modal state — stable reference to avoid re-creating on every render. */
const CLOSED_MODAL: ActionModalState<E> = useMemo(() => ({ opened: false, action: null, data: null }), []);
const [actionModalState, setActionModalState] = useState<ActionModalState<E>>(CLOSED_MODAL);
/** Map action type → modal config provided by the implementer. */
const modalConfigMap = useMemo(
() => ({
[ModuleAction.DELETE]: deleteModalConfig,
[ModuleAction.ACTIVATE]: activateModalConfig,
[ModuleAction.DEACTIVATE]: deactivateModalConfig,
[ModuleAction.CONFIRM]: confirmModalConfig,
[ModuleAction.CANCEL]: cancelModalConfig,
[ModuleAction.ROLLBACK]: rollbackModalConfig,
[ModuleAction.HOLD]: holdModalConfig,
}),
[
deleteModalConfig,
activateModalConfig,
deactivateModalConfig,
confirmModalConfig,
cancelModalConfig,
rollbackModalConfig,
holdModalConfig,
],
);
/** Open the confirmation modal for the given action. */
const openActionModal = useCallback(
(action: ModuleActionType, data: E) => {
const config = modalConfigMap[action as keyof typeof modalConfigMap];
setActionModalState({ opened: true, action, data, config });
},
[modalConfigMap],
);
/** Close the confirmation modal and reset state. */
const closeActionModal = useCallback(() => {
setActionModalState(CLOSED_MODAL);
}, [CLOSED_MODAL]);
/**
* Execute a lifecycle action against the data service.
*
* Called by ActionConfirmationModal after the user confirms.
* Dispatches to the correct `dataServices.*` method based on action type.
* Passes optional `meta` (form data from the modal body) wrapped in the request payload.
*
* Post-action behavior:
* - DELETE → navigates back to index (record no longer exists)
* - All others → reloads detail data (record still exists, status changed)
*/
const executeAction = useCallback(
async (action: ModuleActionType, data: E, meta?: Record<string, unknown>) => {
const id = data.id;
if (!id) return;
const currentConfig = modalConfigMap[action as keyof typeof modalConfigMap];
try {
switch (action) {
case ModuleAction.DELETE:
await dataServices.delete(id, meta);
break;
case ModuleAction.ACTIVATE:
await dataServices.activate(id, meta);
break;
case ModuleAction.DEACTIVATE:
await dataServices.deactivate(id, meta);
break;
case ModuleAction.CONFIRM:
await dataServices.confirmData(id, meta);
break;
case ModuleAction.CANCEL:
await dataServices.cancelData(id, meta);
break;
case ModuleAction.ROLLBACK:
await dataServices.rollbackData(id, meta);
break;
case ModuleAction.HOLD:
await dataServices.holdData(id, meta);
break;
default:
console.warn(`[executeAction] Unhandled action: ${action}`);
return;
}
const actionKey = ACTION_TRANSLATION_MAP[action]?.confirmKey || `common:actions.${action.toLowerCase()}`;
const defaultSuccessMessage = t('common:notifications.actionSuccess', { action: t(actionKey) });
const customSuccessMessage =
typeof currentConfig?.successMessage === 'function'
? currentConfig.successMessage(data)
: currentConfig?.successMessage;
notifications.show({
title: t('common:notifications.successTitle', { defaultValue: 'Success' }),
message: customSuccessMessage || defaultSuccessMessage,
color: 'teal',
});
if (action === ModuleAction.DELETE) {
closeActionModal();
navigation.navigateToIndex();
} else {
closeActionModal();
await loadData();
}
} catch (error: any) {
const actionKey = ACTION_TRANSLATION_MAP[action]?.confirmKey || `common:actions.${action.toLowerCase()}`;
const defaultErrorMessage = t('common:notifications.actionFailed', {
action: t(actionKey),
message: error?.message || 'Unknown error',
});
const customErrorMessage =
typeof currentConfig?.errorMessage === 'function'
? currentConfig.errorMessage(error)
: currentConfig?.errorMessage;
notifications.show({
title: t('common:notifications.errorTitle', { defaultValue: 'Error' }),
message: customErrorMessage || defaultErrorMessage,
color: 'red',
});
// Rethrow the error so the modal knows the submission failed
// and re-enables the buttons for the user to try again or cancel
throw error;
}
},
[dataServices, closeActionModal, navigation, loadData, modalConfigMap, t],
);
// --- Handler functions that open the confirmation modal ---
function handleDelete(data: E): void {
openActionModal(ModuleAction.DELETE, data);
}
async function handleActivate(data: E): Promise<void> {
// implementation later
console.log({ data });
function handleActivate(data: E): void {
openActionModal(ModuleAction.ACTIVATE, data);
}
async function handleDeactivate(data: E): Promise<void> {
// implementation later
console.log({ data });
function handleDeactivate(data: E): void {
openActionModal(ModuleAction.DEACTIVATE, data);
}
async function handleConfirm(data: E): Promise<void> {
// implementation later
console.log({ data });
function handleConfirm(data: E): void {
openActionModal(ModuleAction.CONFIRM, data);
}
async function handleCancel(data: E): Promise<void> {
// implementation later
console.log({ data });
function handleCancel(data: E): void {
openActionModal(ModuleAction.CANCEL, data);
}
async function handleRollback(data: E): Promise<void> {
// implementation later
console.log({ data });
function handleRollback(data: E): void {
openActionModal(ModuleAction.ROLLBACK, data);
}
async function handleHold(data: E): Promise<void> {
// implementation later
console.log({ data });
function handleHold(data: E): void {
openActionModal(ModuleAction.HOLD, data);
}
// ---------------------------------------------------------------------------
@@ -164,28 +323,28 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
else await handleDeactivate(currentData);
break;
// --- CONFIRM (Tambahan Baru) ---
// --- CONFIRM ---
case ModuleAction.CONFIRM:
if (!privileges.ALLOW_CONFIRM || !hasValidData) return;
if (onClickConfirm) onClickConfirm(currentData);
else await handleConfirm(currentData);
break;
// --- CANCEL (Tambahan Baru) ---
// --- CANCEL ---
case ModuleAction.CANCEL:
if (!privileges.ALLOW_CANCEL || !hasValidData) return;
if (onClickCancel) onClickCancel(currentData);
else await handleCancel(currentData);
break;
// --- ROLLBACK (Tambahan Baru) ---
// --- ROLLBACK ---
case ModuleAction.ROLLBACK:
if (!privileges.ALLOW_ROLLBACK || !hasValidData) return;
if (onClickRollback) onClickRollback(currentData);
else await handleRollback(currentData);
break;
// --- HOLD (Tambahan Baru) ---
// --- HOLD ---
case ModuleAction.HOLD:
if (!privileges.ALLOW_HOLD || !hasValidData) return;
if (onClickHold) onClickHold(currentData);
@@ -397,10 +556,10 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
function makeTitle(showHighlight: boolean, key: string, pageProvide: ModulePageHeaderProps | any, detailData: any) {
const staticTitle = pageProvide?.title;
if (!showHighlight) {
if (!showHighlight || !detailData) {
return { flatTitle: staticTitle, title: staticTitle };
} else {
const highlightData = detailData[key];
const highlightData = detailData && detailData[key];
const flatTitle = `${staticTitle} | ${highlightData}`;
return {
@@ -413,7 +572,7 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
style={{
fontWeight: 400,
marginLeft: '8px',
color: 'var(--mantine-color-dimmed)',
// color: 'var(--mantine-color-dimmed)',
}}
>
| {highlightData}
@@ -432,10 +591,10 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
detailData: any,
) {
const staticBreadcrumbs = pageProvide.breadcrumbs ?? [];
if (!showHighlight || staticBreadcrumbs.length === 0) {
if (!showHighlight || staticBreadcrumbs.length === 0 || !detailData) {
return pageProvide.breadcrumbs;
} else {
const highlightData = detailData[key];
const highlightData = detailData && detailData[key];
const breadcrumbs = [
...staticBreadcrumbs,
{
@@ -478,11 +637,27 @@ export function EnterpriseDetailPageProvider<E extends BaseEntity = BaseEntity>(
miniTitleProps={{
fz: { base: 'md', sm: 'lg' },
}}
badges={
detailData ? (
<StatusBadge
status={detailData[statusKey as keyof typeof detailData] as string}
getCustomConfig={getCustomStatusConfig}
/>
) : undefined
}
/>
}
>
{children}
</CorePageContainer>
{/* Action Confirmation Modal */}
<ActionConfirmationModal<E>
modalState={actionModalState}
onClose={closeActionModal}
onExecute={executeAction}
t={t}
/>
</DetailPageContext.Provider>
);
}