feat: add full page index component with pagination and actions
- Implemented FullPagePageIndex component with mock data for database clusters. - Added pagination and bulk actions for managing database clusters. - Created navigation context hooks for detail, edit, duplicate, and create actions. - Introduced a new store for managing state in full-page and single-page modules. feat: add navigation localization files - Added English and Indonesian localization files for navigation menu items. - Included translations for various modules including CRM, Sales, Supply Chain, and more. feat: create system information shortcuts component - Developed Shortcut component to display keyboard shortcuts with search functionality. - Implemented System component to show placeholder information when system details are unavailable. - Added localization for shortcuts and system information in English and Indonesian. feat: implement global theme store - Created a Zustand store for managing theme color scheme with localStorage persistence. feat: add module page header component - Developed ModulePageHeader component for consistent page header across modules. - Included breadcrumb navigation, title, description, and action buttons. feat: define default privileges for enterprise module - Established default privileges for CRUD operations and other actions in the enterprise module.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
import { BaseEntity } from '@repo/core-api/data-services';
|
||||
import { EnterpriseIndexPageConfig, ModuleAction } from '../entities/entity';
|
||||
import { IndexPageContext } from '../hooks/use-index-page.context';
|
||||
import { CorePageContainer, PageActionProps } from '../../../components';
|
||||
import { ModulePageHeader } from '../components/module-page-header';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import {
|
||||
useEnterpriseModuleConfigContext,
|
||||
useEnterpriseModuleNavigationContext,
|
||||
useEnterpriseModuleTranslationContext,
|
||||
} from '../hooks/use-module.context';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Detect macOS / iOS for displaying platform-specific shortcut labels. */
|
||||
const IS_MAC = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
|
||||
|
||||
/** Platform-aware shortcut label for the Create action. */
|
||||
const CREATE_SHORTCUT_LABEL = IS_MAC ? '⇧⌘N' : 'Ctrl+Shift+N';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Component
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function EnterpriseIndexPageProvider<E extends BaseEntity = BaseEntity>(props: EnterpriseIndexPageConfig<E>) {
|
||||
const { children, pageHeaderProps, px, py, customPageActions, onClickCreate } = props;
|
||||
|
||||
const { t } = useEnterpriseModuleTranslationContext();
|
||||
const navigation = useEnterpriseModuleNavigationContext();
|
||||
const { config, privileges } = useEnterpriseModuleConfigContext();
|
||||
const { moduleKey } = config;
|
||||
const { ALLOW_CREATE } = privileges;
|
||||
|
||||
// Stable reference so the useEffect doesn't re-attach on every render.
|
||||
const handleActionClick = useCallback(
|
||||
(key: string) => {
|
||||
if (key === ModuleAction.CREATE && ALLOW_CREATE) {
|
||||
if (onClickCreate) onClickCreate(key);
|
||||
else navigation.navigateToCreate();
|
||||
}
|
||||
},
|
||||
[onClickCreate, navigation, ALLOW_CREATE],
|
||||
);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Global keyboard shortcut: Ctrl+Shift+N / ⌘+Shift+N → Create
|
||||
// -------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const isShortcut = (e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'n';
|
||||
if (!isShortcut) return;
|
||||
|
||||
// Prevent browser default (e.g., Chrome's "new incognito window").
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
handleActionClick(ModuleAction.CREATE);
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', onKeyDown, { capture: true });
|
||||
return () => window.removeEventListener('keydown', onKeyDown, { capture: true });
|
||||
}, [handleActionClick]);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Action definitions
|
||||
// -------------------------------------------------------------------------
|
||||
const pageActions = useMemo(() => {
|
||||
const actions: PageActionProps[] = [];
|
||||
if (ALLOW_CREATE) {
|
||||
actions.push({
|
||||
key: ModuleAction.CREATE,
|
||||
label: t('common:actions.create'),
|
||||
icon: <Plus size={16} />,
|
||||
intent: 'primary',
|
||||
variant: 'filled',
|
||||
shortcutLabel: CREATE_SHORTCUT_LABEL,
|
||||
onClick: (key) => handleActionClick(key),
|
||||
});
|
||||
}
|
||||
return customPageActions ? customPageActions(actions) : (actions as any[]);
|
||||
}, [t, customPageActions, handleActionClick, ALLOW_CREATE]);
|
||||
|
||||
return (
|
||||
<IndexPageContext.Provider value={{}}>
|
||||
<CorePageContainer
|
||||
px={px}
|
||||
py={py}
|
||||
headerSlot={<ModulePageHeader actions={pageActions} {...pageHeaderProps} moduleKey={moduleKey} />}
|
||||
>
|
||||
{children}
|
||||
</CorePageContainer>
|
||||
</IndexPageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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 { ConfigSlice, ModuleConfigEntity, SinglePageFormState, SinglePageModalState } from '../entities/entity';
|
||||
import {
|
||||
EnterpriseConfigContext,
|
||||
EnterpriseDataServiceContext,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
EnterpriseModalContext,
|
||||
EnterpriseTranslationContext,
|
||||
} from '../hooks/use-module.context';
|
||||
import { defaultPrivileges } from '../constant/default-privilege';
|
||||
|
||||
export interface EnterpriseModuleProviderProps<E extends BaseEntity> {
|
||||
children: ReactNode;
|
||||
@@ -26,15 +27,18 @@ export function EnterpriseModuleProvider<E extends BaseEntity>(props: Enterprise
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Config Slice (Static)
|
||||
// ---------------------------------------------------------------------------
|
||||
const configSlice = useMemo(() => ({ config }), [config]);
|
||||
const configSlice: ConfigSlice = useMemo(() => {
|
||||
return {
|
||||
config,
|
||||
// FIXME => IMPLEMENT PRIVILEGE
|
||||
privileges: defaultPrivileges,
|
||||
};
|
||||
}, [config]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1b. Translation Slice (Dedicated context — decoupled from config)
|
||||
// ---------------------------------------------------------------------------
|
||||
const namespaces = useMemo(
|
||||
() => [config.translationNamespace, 'common'],
|
||||
[config.translationNamespace],
|
||||
);
|
||||
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]);
|
||||
|
||||
@@ -132,6 +136,17 @@ export function EnterpriseModuleProvider<E extends BaseEntity>(props: Enterprise
|
||||
// ---------------------------------------------------------------------------
|
||||
// Render
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { ALLOW_VIEW } = configSlice.privileges;
|
||||
|
||||
useEffect(() => {
|
||||
if (!ALLOW_VIEW) navigate('/403', { replace: true });
|
||||
}, [ALLOW_VIEW, navigate]);
|
||||
|
||||
if (!ALLOW_VIEW) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<EnterpriseConfigContext.Provider value={configSlice}>
|
||||
<EnterpriseTranslationContext.Provider value={translationSlice}>
|
||||
|
||||
Reference in New Issue
Block a user