feat: add RowActions component for enhanced row-level actions in data grids
- Introduced RowActions component to manage row-level actions with tooltips and dropdown menus. - Created types for row actions and page actions to standardize action properties. - Implemented utility function to map action intents to Mantine theme colors. - Updated CoreAppShell component to support optional slots for better flexibility. - Added enterprise module structure with context hooks for managing module state and actions. - Implemented draft management for forms to enhance user experience during data entry. - Established context providers for detail, form, and index pages to streamline data handling. - Updated dependencies to ensure compatibility with the latest versions.
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
import { ReactNode } from 'react';
|
||||
import type { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Module Constants & Base Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Standardized action constants across all enterprise modules.
|
||||
* Used to define RBAC permissions, UI rendering logic, and event handling.
|
||||
*/
|
||||
export const ModuleAction = {
|
||||
CREATE: 'CREATE',
|
||||
EDIT: 'EDIT',
|
||||
DELETE: 'DELETE',
|
||||
DUPLICATE: 'DUPLICATE',
|
||||
SAVE: 'SAVE',
|
||||
|
||||
PRINT: 'PRINT',
|
||||
PRINT_COPY: 'PRINT_COPY',
|
||||
|
||||
APPROVAL: 'APPROVAL',
|
||||
ACTIVATE: 'ACTIVATE',
|
||||
DEACTIVATE: 'DEACTIVATE',
|
||||
|
||||
CONFIRM: 'CONFIRM',
|
||||
CANCEL: 'CANCEL',
|
||||
ROLLBACK: 'ROLLBACK',
|
||||
HOLD: 'HOLD',
|
||||
|
||||
LOGS: 'LOGS',
|
||||
NOTES: 'NOTES',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Represents a valid module action.
|
||||
* Utilizes the `(string & {})` pattern to preserve IDE autocomplete for standard actions
|
||||
* while allowing implementers to extend it with custom string identifiers.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type ModuleActionType = (typeof ModuleAction)[keyof typeof ModuleAction] | (string & {});
|
||||
|
||||
/**
|
||||
* Defines the architectural category of the module (e.g., routed vs. modal-driven).
|
||||
*/
|
||||
export type ModuleCategoryType = 'SINGLE_PAGE' | 'FULL_PAGE';
|
||||
|
||||
/**
|
||||
* Represents the current operational mode of a form instance.
|
||||
*/
|
||||
export type FormPageType = 'CREATE' | 'EDIT' | 'DUPLICATE';
|
||||
|
||||
export interface SinglePageConfig {
|
||||
type?: 'MODAL' | 'DRAWER';
|
||||
size?: string | number;
|
||||
}
|
||||
|
||||
export interface SinglePageModalState {
|
||||
open: boolean;
|
||||
dataId?: string;
|
||||
}
|
||||
|
||||
export interface SinglePageFormState extends SinglePageModalState {
|
||||
formType: FormPageType;
|
||||
}
|
||||
|
||||
export interface DraftConfig {
|
||||
enableDraft?: boolean;
|
||||
/** @default 5000 */
|
||||
autoSaveIntervalMs?: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Root Provider Configuration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Foundational configuration injected into the root ModuleProvider.
|
||||
* @template E The base database entity for the module.
|
||||
* @property moduleKey Unique identifier for the module, used for permissions, caching, and i18n.
|
||||
* @property webUrl Base URL for the module's web routes.
|
||||
* @property apiUrl Base API endpoint for the module's remote data services.
|
||||
* @property tabTitle Optional title for browser tabs. When omitted, the Provider will auto-set it from the module's translation key `title`.
|
||||
* @property moduleCategory Architectural category of the module, used for rendering and routing logic.
|
||||
* @property translationNamespace Translation namespace for i18n resource bundles. Must match the namespace string used in `registerModuleNamespace()`.
|
||||
* @property singlePageFormConfig Optional configuration for single-page form modals or drawers.
|
||||
* @property singlePageDetailConfig Optional configuration for single-page detail modals or drawers.
|
||||
*/
|
||||
export interface ModuleConfigEntity<E extends BaseEntity = BaseEntity> {
|
||||
moduleKey: string;
|
||||
webUrl: string;
|
||||
apiUrl: string;
|
||||
tabTitle?: string;
|
||||
moduleCategory: ModuleCategoryType;
|
||||
translationNamespace: string;
|
||||
|
||||
singlePageFormConfig?: SinglePageConfig;
|
||||
singlePageDetailConfig?: SinglePageConfig;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context Slices (Anti-Rerender Strategy)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ConfigSlice<E extends BaseEntity = BaseEntity> {
|
||||
config: ModuleConfigEntity<E>;
|
||||
}
|
||||
|
||||
export interface DataServiceSlice<
|
||||
E extends BaseEntity,
|
||||
S extends BaseRemoteDataServices<E> = BaseRemoteDataServices<E>,
|
||||
> {
|
||||
dataServices: S;
|
||||
/**
|
||||
* User privileges payload for the current module.
|
||||
* Typed as `unknown` to enforce strict type checking before consumption via RBAC utilities.
|
||||
*/
|
||||
privilege: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* State management slice for grid/table selections and data filtering.
|
||||
* Utilizes generic parameters to ensure type safety for arbitrary filter and metadata payloads.
|
||||
*/
|
||||
export interface SelectionSlice<
|
||||
E extends BaseEntity = BaseEntity,
|
||||
TFilter = Record<string, unknown>,
|
||||
TMeta = Record<string, unknown>,
|
||||
> {
|
||||
selectedRows: E[];
|
||||
setSelectedRows: (rows: E[]) => void;
|
||||
metaData: TMeta;
|
||||
setMetaData: (data: TMeta) => void;
|
||||
filterData: TFilter;
|
||||
setFilterData: (data: TFilter) => void;
|
||||
}
|
||||
|
||||
export interface NavigationSlice {
|
||||
navigateToIndex: () => void;
|
||||
navigateToCreate: () => void;
|
||||
navigateToEdit: (id: string) => void;
|
||||
navigateToDetail: (id: string) => void;
|
||||
navigateToDuplicate: (id: string) => void;
|
||||
}
|
||||
|
||||
export interface ModalSlice {
|
||||
formState: SinglePageFormState;
|
||||
setFormState: (state: SinglePageFormState) => void;
|
||||
detailState: SinglePageModalState;
|
||||
setDetailState: (state: SinglePageModalState) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translation slice for enterprise modules, providing a standardized interface for i18n operations.
|
||||
*
|
||||
* The `t` function is scoped to `[moduleNamespace, 'common']`, meaning:
|
||||
* - Keys are first resolved in the module's own namespace.
|
||||
* - If not found, they fall back to the `common` namespace.
|
||||
* - Explicit namespace prefix (e.g. `common:save`) is still supported.
|
||||
*
|
||||
* @property t - Translation function scoped to the module's namespace with common fallback.
|
||||
*/
|
||||
export interface TranslationSlice {
|
||||
/** Translation function scoped to [moduleNamespace, 'common']. No prefix needed for module keys. */
|
||||
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>;
|
||||
}
|
||||
|
||||
export interface EnterpriseIndexPageConfig<E extends BaseEntity = BaseEntity> {
|
||||
children?: ReactNode;
|
||||
showPageHeader?: boolean;
|
||||
useDefaultPadding?: boolean;
|
||||
customHiddenActions?: (selected: E[], defaultHidden: string[]) => string[];
|
||||
/** Strongly typed event handler to prevent arbitrary string usage for actions. */
|
||||
onCustomActionClick?: (key: ModuleActionType, data?: unknown) => void;
|
||||
filterDrawerContent?: ReactNode;
|
||||
|
||||
registerRefreshCallback?: (callback: () => void) => 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>>;
|
||||
}
|
||||
|
||||
export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> {
|
||||
children?: ReactNode;
|
||||
showPageHeader?: boolean;
|
||||
useDefaultPadding?: boolean;
|
||||
customHiddenActions?: (data: E | null, defaultHidden: string[]) => string[];
|
||||
onCustomActionClick?: (key: ModuleActionType, data?: unknown) => void;
|
||||
|
||||
editMode?: 'FULL' | 'PARTIAL';
|
||||
afterGetData?: (data: E) => Promise<unknown>;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||
|
||||
export interface DetailPageContextValue<E extends BaseEntity = BaseEntity> {
|
||||
detailData: E | null;
|
||||
isLoading: boolean;
|
||||
reload: () => Promise<void>;
|
||||
isPartialEdit: boolean;
|
||||
}
|
||||
|
||||
export const DetailPageContext = createContext<DetailPageContextValue<any> | null>(null);
|
||||
|
||||
export function useDetailPageContext<E extends BaseEntity = BaseEntity>(): DetailPageContextValue<E> {
|
||||
const context = useContext(DetailPageContext);
|
||||
if (!context) {
|
||||
throw new Error('useDetailPageContext must be used within an EnterpriseDetailPageProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { createLocalStorage } from '@repo/core-storage';
|
||||
import { useEnterpriseModuleConfigContext } from './use-module.context';
|
||||
import { DraftConfig, FormPageType } from '../entities/entity';
|
||||
|
||||
interface FormDraftContextProps {
|
||||
formType: FormPageType;
|
||||
dataId?: string;
|
||||
config?: DraftConfig;
|
||||
userID: string; // Optional user ID for multi-user scenarios
|
||||
}
|
||||
|
||||
const draftStorage = createLocalStorage<string>({
|
||||
// No encryption needed for general drafts usually, but could be added
|
||||
});
|
||||
|
||||
export function useFormDraftContext({ userID, formType, dataId, config }: FormDraftContextProps) {
|
||||
const { config: moduleConfig } = useEnterpriseModuleConfigContext();
|
||||
const [hasDraft, setHasDraft] = useState(false);
|
||||
const [draftData, setDraftData] = useState<any>(null);
|
||||
|
||||
const isEnabled = config?.enableDraft === true;
|
||||
const draftKey = `${userID || 'UNRESOLVED_PRINCIPAL'}:draft:${moduleConfig.moduleKey}:${formType}:${dataId || 'new'}`;
|
||||
|
||||
// Check for existing draft on mount
|
||||
useEffect(() => {
|
||||
if (!isEnabled) return;
|
||||
|
||||
draftStorage.getItem(draftKey).then((data: any) => {
|
||||
if (data) {
|
||||
setDraftData(data);
|
||||
setHasDraft(true);
|
||||
}
|
||||
});
|
||||
}, [draftKey, isEnabled]);
|
||||
|
||||
const saveDraft = useCallback(
|
||||
(data: any) => {
|
||||
if (!isEnabled) return;
|
||||
draftStorage.setItem(draftKey, {
|
||||
...data,
|
||||
_draftSavedAt: new Date().toISOString(),
|
||||
});
|
||||
},
|
||||
[draftKey, isEnabled],
|
||||
);
|
||||
|
||||
const clearDraft = useCallback(() => {
|
||||
if (!isEnabled) return;
|
||||
draftStorage.removeItem(draftKey);
|
||||
setHasDraft(false);
|
||||
setDraftData(null);
|
||||
}, [draftKey, isEnabled]);
|
||||
|
||||
return {
|
||||
isEnabled,
|
||||
hasDraft,
|
||||
draftData,
|
||||
saveDraft,
|
||||
clearDraft,
|
||||
setHasDraft, // To close the recovery dialog
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||
import { FormPageType } from '../entities/entity';
|
||||
|
||||
export interface FormPageContextValue<E extends BaseEntity = BaseEntity> {
|
||||
formType: FormPageType;
|
||||
isCreate: boolean;
|
||||
isEdit: boolean;
|
||||
isDuplicate: boolean;
|
||||
dataId?: string;
|
||||
initialData: Partial<E> | null;
|
||||
isLoading: boolean;
|
||||
isSaving: boolean;
|
||||
save: (data: any) => Promise<void>;
|
||||
cancel: () => void;
|
||||
// Draft specific
|
||||
hasDraft: boolean;
|
||||
applyDraft: () => void;
|
||||
discardDraft: () => void;
|
||||
}
|
||||
|
||||
export const FormPageContext = createContext<FormPageContextValue<any> | null>(null);
|
||||
|
||||
export function useFormPageContext<E extends BaseEntity = BaseEntity>(): FormPageContextValue<E> {
|
||||
const context = useContext(FormPageContext);
|
||||
if (!context) {
|
||||
throw new Error('useFormPageContext must be used within an EnterpriseFormPageProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||
|
||||
export interface IndexPageContextValue<E extends BaseEntity = BaseEntity> {
|
||||
renderRowActions: (row: E) => React.ReactNode;
|
||||
refreshGrid: () => void;
|
||||
// State for batch modals
|
||||
isConfirmModalOpen: boolean;
|
||||
setIsConfirmModalOpen: (open: boolean) => void;
|
||||
isDeleteModalOpen: boolean;
|
||||
setIsDeleteModalOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const IndexPageContext = createContext<IndexPageContextValue<any> | null>(null);
|
||||
|
||||
export function useIndexPageContext<E extends BaseEntity = BaseEntity>(): IndexPageContextValue<E> {
|
||||
const context = useContext(IndexPageContext);
|
||||
if (!context) {
|
||||
throw new Error('useIndexPageContext must be used within an IndexPageProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
import type { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||
import type {
|
||||
ConfigSlice,
|
||||
DataServiceSlice,
|
||||
SelectionSlice,
|
||||
NavigationSlice,
|
||||
ModalSlice,
|
||||
TranslationSlice,
|
||||
} from '../entities/entity';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context Definitions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const EnterpriseConfigContext = createContext<ConfigSlice<any> | null>(null);
|
||||
export const EnterpriseDataServiceContext = createContext<DataServiceSlice<any, any> | null>(null);
|
||||
export const EnterpriseSelectionContext = createContext<SelectionSlice<any> | null>(null);
|
||||
export const EnterpriseNavigationContext = createContext<NavigationSlice | null>(null);
|
||||
export const EnterpriseModalContext = createContext<ModalSlice | null>(null);
|
||||
export const EnterpriseTranslationContext = createContext<TranslationSlice | null>(null);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Granular Hooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useEnterpriseModuleConfigContext<E extends BaseEntity = BaseEntity>(): ConfigSlice<E> {
|
||||
const context = useContext(EnterpriseConfigContext);
|
||||
if (!context) {
|
||||
throw new Error('useEnterpriseModuleConfigContext must be used within an EnterpriseModuleProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useEnterpriseModuleDataServiceContext<
|
||||
E extends BaseEntity = BaseEntity,
|
||||
S extends BaseRemoteDataServices<E> = BaseRemoteDataServices<E>,
|
||||
>(): DataServiceSlice<E, S> {
|
||||
const context = useContext(EnterpriseDataServiceContext);
|
||||
if (!context) {
|
||||
throw new Error('useEnterpriseModuleDataServiceContext must be used within an EnterpriseModuleProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useEnterpriseModuleSelectionContext<E extends BaseEntity = BaseEntity>(): SelectionSlice<E> {
|
||||
const context = useContext(EnterpriseSelectionContext);
|
||||
if (!context) {
|
||||
throw new Error('useEnterpriseModuleSelectionContext must be used within an EnterpriseModuleProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useEnterpriseModuleNavigationContext(): NavigationSlice {
|
||||
const context = useContext(EnterpriseNavigationContext);
|
||||
if (!context) {
|
||||
throw new Error('useEnterpriseModuleNavigationContext must be used within an EnterpriseModuleProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useEnterpriseModuleModalContext(): ModalSlice {
|
||||
const context = useContext(EnterpriseModalContext);
|
||||
if (!context) {
|
||||
throw new Error('useEnterpriseModuleModalContext must be used within an EnterpriseModuleProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function useEnterpriseModuleTranslationContext(): TranslationSlice {
|
||||
const context = useContext(EnterpriseTranslationContext);
|
||||
if (!context) {
|
||||
throw new Error('useEnterpriseModuleTranslationContext must be used within an EnterpriseModuleProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Composed Hook (Use Sparingly)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function useEnterpriseModuleContext<E extends BaseEntity = BaseEntity>() {
|
||||
return {
|
||||
...useEnterpriseModuleConfigContext<E>(),
|
||||
...useEnterpriseModuleDataServiceContext<E>(),
|
||||
...useEnterpriseModuleSelectionContext<E>(),
|
||||
...useEnterpriseModuleNavigationContext(),
|
||||
...useEnterpriseModuleModalContext(),
|
||||
...useEnterpriseModuleTranslationContext(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './entities/entity';
|
||||
|
||||
export * from './hooks/use-module.context';
|
||||
|
||||
export * from './providers/module.provider';
|
||||
@@ -0,0 +1,148 @@
|
||||
import { useMemo, useState, useEffect, ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from '@repo/core-i18n';
|
||||
import { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||
|
||||
import { ModuleConfigEntity, SinglePageFormState, SinglePageModalState } from '../entities/entity';
|
||||
import {
|
||||
EnterpriseConfigContext,
|
||||
EnterpriseDataServiceContext,
|
||||
EnterpriseSelectionContext,
|
||||
EnterpriseNavigationContext,
|
||||
EnterpriseModalContext,
|
||||
EnterpriseTranslationContext,
|
||||
} from '../hooks/use-module.context';
|
||||
|
||||
export interface EnterpriseModuleProviderProps<E extends BaseEntity> {
|
||||
children: ReactNode;
|
||||
config: ModuleConfigEntity<E>;
|
||||
dataServices: BaseRemoteDataServices<E>;
|
||||
}
|
||||
|
||||
export function EnterpriseModuleProvider<E extends BaseEntity>(props: EnterpriseModuleProviderProps<E>) {
|
||||
const { children, config, dataServices } = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Config Slice (Static)
|
||||
// ---------------------------------------------------------------------------
|
||||
const configSlice = useMemo(() => ({ config }), [config]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1b. Translation Slice (Dedicated context — decoupled from config)
|
||||
// ---------------------------------------------------------------------------
|
||||
const namespaces = useMemo(
|
||||
() => [config.translationNamespace, 'common'],
|
||||
[config.translationNamespace],
|
||||
);
|
||||
const { t } = useTranslation(namespaces);
|
||||
const translationSlice = useMemo(() => ({ t: t as (key: string, options?: Record<string, unknown>) => string }), [t]);
|
||||
|
||||
// Set document title — uses explicit tabTitle or falls back to translation key 'title'
|
||||
useEffect(() => {
|
||||
const title = config.tabTitle || t('title');
|
||||
if (title) {
|
||||
document.title = title;
|
||||
}
|
||||
}, [config.tabTitle, t]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Data Service Slice (Stable refs)
|
||||
// ---------------------------------------------------------------------------
|
||||
const dataServiceSlice = useMemo(() => {
|
||||
return { dataServices, privilege: undefined };
|
||||
}, [dataServices]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Selection Slice (Dynamic state)
|
||||
// ---------------------------------------------------------------------------
|
||||
const [selectedRows, setSelectedRows] = useState<E[]>([]);
|
||||
const [metaData, setMetaData] = useState<any>(null);
|
||||
const [filterData, setFilterData] = useState<any>(null);
|
||||
|
||||
const selectionSlice = useMemo(
|
||||
() => ({
|
||||
selectedRows,
|
||||
setSelectedRows,
|
||||
metaData,
|
||||
setMetaData,
|
||||
filterData,
|
||||
setFilterData,
|
||||
}),
|
||||
[selectedRows, metaData, filterData],
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Modal Slice (For Single Page mode)
|
||||
// ---------------------------------------------------------------------------
|
||||
const [formState, setFormState] = useState<SinglePageFormState>({ open: false, formType: 'CREATE' });
|
||||
const [detailState, setDetailState] = useState<SinglePageModalState>({ open: false });
|
||||
|
||||
const modalSlice = useMemo(
|
||||
() => ({ formState, setFormState, detailState, setDetailState }),
|
||||
[formState, detailState],
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 5. Navigation Slice
|
||||
// ---------------------------------------------------------------------------
|
||||
const navigationSlice = useMemo(() => {
|
||||
const isSingle = config.moduleCategory === 'SINGLE_PAGE';
|
||||
|
||||
return {
|
||||
navigateToIndex: () => {
|
||||
if (isSingle) {
|
||||
setFormState({ open: false, formType: 'CREATE' });
|
||||
setDetailState({ open: false });
|
||||
} else {
|
||||
navigate(`${config.webUrl}/index`);
|
||||
}
|
||||
},
|
||||
navigateToCreate: () => {
|
||||
if (isSingle) {
|
||||
setFormState({ open: true, formType: 'DUPLICATE' });
|
||||
} else {
|
||||
navigate(`${config.webUrl}/create`);
|
||||
}
|
||||
},
|
||||
navigateToEdit: (id: string) => {
|
||||
if (isSingle) {
|
||||
setFormState({ open: true, formType: 'EDIT', dataId: id });
|
||||
} else {
|
||||
navigate(`${config.webUrl}/edit/${id}`);
|
||||
}
|
||||
},
|
||||
navigateToDetail: (id: string) => {
|
||||
if (isSingle) {
|
||||
setDetailState({ open: true, dataId: id });
|
||||
} else {
|
||||
navigate(`${config.webUrl}/detail/${id}`);
|
||||
}
|
||||
},
|
||||
navigateToDuplicate: (id: string) => {
|
||||
if (isSingle) {
|
||||
setFormState({ open: true, formType: 'DUPLICATE', dataId: id });
|
||||
} else {
|
||||
navigate(`${config.webUrl}/duplicate/${id}`);
|
||||
}
|
||||
},
|
||||
};
|
||||
}, [config.moduleCategory, config.webUrl, navigate]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render
|
||||
// ---------------------------------------------------------------------------
|
||||
return (
|
||||
<EnterpriseConfigContext.Provider value={configSlice}>
|
||||
<EnterpriseTranslationContext.Provider value={translationSlice}>
|
||||
<EnterpriseDataServiceContext.Provider value={dataServiceSlice}>
|
||||
<EnterpriseNavigationContext.Provider value={navigationSlice}>
|
||||
<EnterpriseSelectionContext.Provider value={selectionSlice}>
|
||||
<EnterpriseModalContext.Provider value={modalSlice}>{children}</EnterpriseModalContext.Provider>
|
||||
</EnterpriseSelectionContext.Provider>
|
||||
</EnterpriseNavigationContext.Provider>
|
||||
</EnterpriseDataServiceContext.Provider>
|
||||
</EnterpriseTranslationContext.Provider>
|
||||
</EnterpriseConfigContext.Provider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user