feat: enhance enterprise module with new actions and UI improvements
- Added support for rollback and hold actions in the enterprise module. - Updated tooltip labels for action buttons to improve user experience. - Refactored PageActions component to use tooltipLabel instead of shortcutLabel. - Introduced height prop for system pages (Coming Soon, Forbidden, Maintenance, Not Found) for better layout control. - Improved ModulePageHeader to accept custom button properties and enhanced title handling. - Cleaned up default privileges by removing unused actions (ALLOW_DUPLICATE, ALLOW_SAVE). - Implemented detail page provider with comprehensive action handling for CRUD operations. - Created a new full-page detail component for better data presentation.
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="google" content="notranslate" />
|
<meta name="google" content="notranslate" />
|
||||||
<title>Vite + React</title>
|
<title>Applications</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Group,
|
Group,
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
Pagination,
|
Pagination,
|
||||||
Select,
|
Select,
|
||||||
@@ -16,7 +15,7 @@ import {
|
|||||||
import { PageActions } from '@repo/ui/components';
|
import { PageActions } from '@repo/ui/components';
|
||||||
import { useEnterpriseModuleTranslationContext, useEnterpriseModuleNavigationContext } from '@repo/ui/foundations';
|
import { useEnterpriseModuleTranslationContext, useEnterpriseModuleNavigationContext } from '@repo/ui/foundations';
|
||||||
import { FullPageEntity } from '../../domain/entities';
|
import { FullPageEntity } from '../../domain/entities';
|
||||||
import { Edit2, Eye, Copy, Trash2, Search, Filter, Plus, CheckCircle2, Ban } from 'lucide-react';
|
import { Edit2, Eye, Copy, Trash2, Search, Filter, CheckCircle2, Ban } from 'lucide-react';
|
||||||
|
|
||||||
// TODO: Replace this mock data with real API data loaded from DataService via useEnterpriseModuleDataServiceContext
|
// TODO: Replace this mock data with real API data loaded from DataService via useEnterpriseModuleDataServiceContext
|
||||||
const MOCK_DATA: FullPageEntity[] = Array.from({ length: 50 }).map((_, i) => ({
|
const MOCK_DATA: FullPageEntity[] = Array.from({ length: 50 }).map((_, i) => ({
|
||||||
@@ -29,8 +28,7 @@ const MOCK_DATA: FullPageEntity[] = Array.from({ length: 50 }).map((_, i) => ({
|
|||||||
|
|
||||||
export function DataTable() {
|
export function DataTable() {
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
const { navigateToDetail, navigateToEdit, navigateToDuplicate, navigateToCreate } =
|
const { navigateToDetail, navigateToEdit, navigateToDuplicate } = useEnterpriseModuleNavigationContext();
|
||||||
useEnterpriseModuleNavigationContext();
|
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"title": "Full Page",
|
"title": "Full Page",
|
||||||
|
"detail_page_title": "Detail Full Page",
|
||||||
|
"create_page_title": "New Full Page",
|
||||||
|
"duplicate_page_title": "Duplicate Full Page",
|
||||||
"description": "An example module of a <1>full page layout</1> for detailed forms.",
|
"description": "An example module of a <1>full page layout</1> for detailed forms.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"title": "Halaman Penuh",
|
"title": "Halaman Penuh",
|
||||||
"description": "Contoh modul <1>tata letak halaman penuh</1> untuk formulir detail.",
|
"detail_page_title": "Detail Halaman Penuh",
|
||||||
|
"create_page_title": "Buat Halaman Penuh Baru",
|
||||||
|
"duplicate_page_title": "Duplikat Halaman Penuh",
|
||||||
|
"description": "Contoh penerapan <1>tata letak halaman penuh</1> untuk formulir detail.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"name": "Nama",
|
"name": "Nama",
|
||||||
|
|||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
import { Paper, SimpleGrid, Box, Text, Group, Badge, Divider, Breadcrumbs, Anchor, Flex, Title, ActionIcon, Button } from '@repo/ui/components';
|
||||||
|
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||||
|
import { ChevronRight, CheckCircle2, Trash2, Edit2, Play } from 'lucide-react';
|
||||||
|
|
||||||
|
export default function FullPagePageDetail() {
|
||||||
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box pb="xl">
|
||||||
|
{/* --- INLINED PAGE HEADER --- */}
|
||||||
|
<Box mb="xl" mt="xs">
|
||||||
|
<Breadcrumbs
|
||||||
|
mb="md"
|
||||||
|
separator={<ChevronRight size={12} strokeWidth={3} style={{ color: 'var(--mantine-color-gray-5)' }} />}
|
||||||
|
>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Acme Corp
|
||||||
|
</Anchor>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Infrastructure
|
||||||
|
</Anchor>
|
||||||
|
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
||||||
|
Database Clusters
|
||||||
|
</Anchor>
|
||||||
|
<Text size="xs" fw={600} style={{ color: 'light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0))', letterSpacing: '0.2px' }}>
|
||||||
|
prod-db-01
|
||||||
|
</Text>
|
||||||
|
</Breadcrumbs>
|
||||||
|
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Flex gap="md" align="center">
|
||||||
|
<Box>
|
||||||
|
<Group gap="xs" align="center" mb={4}>
|
||||||
|
<Title
|
||||||
|
order={2}
|
||||||
|
fw={600}
|
||||||
|
style={{
|
||||||
|
color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))',
|
||||||
|
letterSpacing: '-0.3px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
prod-db-01
|
||||||
|
</Title>
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color="teal"
|
||||||
|
size="sm"
|
||||||
|
radius="sm"
|
||||||
|
fw={600}
|
||||||
|
leftSection={<CheckCircle2 size={10} strokeWidth={3} />}
|
||||||
|
style={{ textTransform: 'capitalize' }}
|
||||||
|
>
|
||||||
|
Running
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Text size="sm" c="dimmed" fw={400}>
|
||||||
|
PostgreSQL v15.4 · <Text span fw={500} c="var(--mantine-color-text)">us-east-1</Text> · Created Oct 12, 2023
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<Group gap="sm">
|
||||||
|
<ActionIcon variant="light" color="red" size="lg" radius="md" aria-label="Delete">
|
||||||
|
<Trash2 size={18} strokeWidth={2} />
|
||||||
|
</ActionIcon>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Edit2 size={16} strokeWidth={2} style={{ color: 'var(--mantine-color-dimmed)' }} />}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="filled"
|
||||||
|
color="indigo"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<Play size={16} strokeWidth={2.5} />}
|
||||||
|
style={{ boxShadow: '0 4px 14px 0 rgba(76, 110, 245, 0.39)' }}
|
||||||
|
>
|
||||||
|
Restart
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Divider mt={28} mb={0} color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
</Box>
|
||||||
|
{/* --- END INLINED PAGE HEADER --- */}
|
||||||
|
|
||||||
|
<Paper withBorder shadow="sm" radius="md" p="xl">
|
||||||
|
<Text size="lg" fw={600} mb="xs" style={{ color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))', letterSpacing: '-0.3px' }}>
|
||||||
|
General Information
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" c="dimmed" mb="xl">
|
||||||
|
View the complete details and configuration for this entity.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Divider mb="xl" color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
||||||
|
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xl" verticalSpacing="xl">
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.code')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>WID-001</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.name')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>Dashboard Widget</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.status')}
|
||||||
|
</Text>
|
||||||
|
<Badge variant="light" color="teal" size="sm" radius="sm" fw={600}>
|
||||||
|
ACTIVE
|
||||||
|
</Badge>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
||||||
|
{t('fields.description')}
|
||||||
|
</Text>
|
||||||
|
<Text size="sm" fw={500}>Main dashboard widget</Text>
|
||||||
|
</Box>
|
||||||
|
</SimpleGrid>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
+11
-123
@@ -1,131 +1,19 @@
|
|||||||
import { Paper, SimpleGrid, Box, Text, Group, Badge, Divider, Breadcrumbs, Anchor, Flex, Title, ActionIcon, Button } from '@repo/ui/components';
|
import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||||
import { useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
import { FullPageModuleConfig } from '../../domain/constants';
|
||||||
import { ChevronRight, CheckCircle2, Trash2, Edit2, Play } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function FullPagePageDetail() {
|
export default function FullPagePageDetail() {
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pb="xl">
|
<EnterpriseDetailPageProvider
|
||||||
{/* --- INLINED PAGE HEADER --- */}
|
pageHeaderProps={{
|
||||||
<Box mb="xl" mt="xs">
|
title: t('detail_page_title'),
|
||||||
<Breadcrumbs
|
|
||||||
mb="md"
|
|
||||||
separator={<ChevronRight size={12} strokeWidth={3} style={{ color: 'var(--mantine-color-gray-5)' }} />}
|
|
||||||
>
|
|
||||||
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
|
||||||
Acme Corp
|
|
||||||
</Anchor>
|
|
||||||
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
|
||||||
Infrastructure
|
|
||||||
</Anchor>
|
|
||||||
<Anchor href="#" c="dimmed" size="xs" fw={500} style={{ transition: 'color 0.2s ease', letterSpacing: '0.2px' }}>
|
|
||||||
Database Clusters
|
|
||||||
</Anchor>
|
|
||||||
<Text size="xs" fw={600} style={{ color: 'light-dark(var(--mantine-color-gray-8), var(--mantine-color-dark-0))', letterSpacing: '0.2px' }}>
|
|
||||||
prod-db-01
|
|
||||||
</Text>
|
|
||||||
</Breadcrumbs>
|
|
||||||
|
|
||||||
<Group justify="space-between" align="center" wrap="nowrap">
|
breadcrumbs: [
|
||||||
<Flex gap="md" align="center">
|
{ label: t('nav:example-module'), type: 'text' },
|
||||||
<Box>
|
{ label: t('nav:example-full-page'), type: 'link', href: `${FullPageModuleConfig.webUrl}/index` },
|
||||||
<Group gap="xs" align="center" mb={4}>
|
],
|
||||||
<Title
|
}}
|
||||||
order={2}
|
></EnterpriseDetailPageProvider>
|
||||||
fw={600}
|
|
||||||
style={{
|
|
||||||
color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))',
|
|
||||||
letterSpacing: '-0.3px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
prod-db-01
|
|
||||||
</Title>
|
|
||||||
<Badge
|
|
||||||
variant="light"
|
|
||||||
color="teal"
|
|
||||||
size="sm"
|
|
||||||
radius="sm"
|
|
||||||
fw={600}
|
|
||||||
leftSection={<CheckCircle2 size={10} strokeWidth={3} />}
|
|
||||||
style={{ textTransform: 'capitalize' }}
|
|
||||||
>
|
|
||||||
Running
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Text size="sm" c="dimmed" fw={400}>
|
|
||||||
PostgreSQL v15.4 · <Text span fw={500} c="var(--mantine-color-text)">us-east-1</Text> · Created Oct 12, 2023
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Group gap="sm">
|
|
||||||
<ActionIcon variant="light" color="red" size="lg" radius="md" aria-label="Delete">
|
|
||||||
<Trash2 size={18} strokeWidth={2} />
|
|
||||||
</ActionIcon>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
radius="md"
|
|
||||||
leftSection={<Edit2 size={16} strokeWidth={2} style={{ color: 'var(--mantine-color-dimmed)' }} />}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="filled"
|
|
||||||
color="indigo"
|
|
||||||
radius="md"
|
|
||||||
leftSection={<Play size={16} strokeWidth={2.5} />}
|
|
||||||
style={{ boxShadow: '0 4px 14px 0 rgba(76, 110, 245, 0.39)' }}
|
|
||||||
>
|
|
||||||
Restart
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Divider mt={28} mb={0} color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
|
||||||
</Box>
|
|
||||||
{/* --- END INLINED PAGE HEADER --- */}
|
|
||||||
|
|
||||||
<Paper withBorder shadow="sm" radius="md" p="xl">
|
|
||||||
<Text size="lg" fw={600} mb="xs" style={{ color: 'light-dark(var(--mantine-color-gray-9), var(--mantine-color-dark-0))', letterSpacing: '-0.3px' }}>
|
|
||||||
General Information
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" c="dimmed" mb="xl">
|
|
||||||
View the complete details and configuration for this entity.
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Divider mb="xl" color="light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-6))" />
|
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="xl" verticalSpacing="xl">
|
|
||||||
<Box>
|
|
||||||
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
|
||||||
{t('fields.code')}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" fw={500}>WID-001</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
|
||||||
{t('fields.name')}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" fw={500}>Dashboard Widget</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
|
||||||
{t('fields.status')}
|
|
||||||
</Text>
|
|
||||||
<Badge variant="light" color="teal" size="sm" radius="sm" fw={600}>
|
|
||||||
ACTIVE
|
|
||||||
</Badge>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text size="xs" tt="uppercase" fw={700} c="dimmed" mb={4} style={{ letterSpacing: '0.5px' }}>
|
|
||||||
{t('fields.description')}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" fw={500}>Main dashboard widget</Text>
|
|
||||||
</Box>
|
|
||||||
</SimpleGrid>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -2,7 +2,6 @@ import { EnterpriseIndexPageProvider, useEnterpriseModuleTranslationContext } fr
|
|||||||
import { Trans } from '@repo/core-i18n';
|
import { Trans } from '@repo/core-i18n';
|
||||||
import { Badge, Text } from '@repo/ui/components';
|
import { Badge, Text } from '@repo/ui/components';
|
||||||
import { Activity, LayoutDashboard } from 'lucide-react';
|
import { Activity, LayoutDashboard } from 'lucide-react';
|
||||||
import { FullPageModuleConfig } from '../../domain/constants';
|
|
||||||
import { DataTable } from '../components/data-table';
|
import { DataTable } from '../components/data-table';
|
||||||
|
|
||||||
export default function FullPagePageIndex() {
|
export default function FullPagePageIndex() {
|
||||||
@@ -37,7 +36,7 @@ export default function FullPagePageIndex() {
|
|||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
breadcrumbs: [
|
breadcrumbs: [
|
||||||
{ label: t('nav:example-module'), type: 'text' },
|
{ label: t('nav:example-module'), type: 'text' },
|
||||||
{ label: t('nav:example-full-page'), type: 'link', href: `${FullPageModuleConfig.webUrl}/index` },
|
{ label: t('nav:example-full-page'), type: 'text' },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { IDataTransformer } from './base-data.transformer';
|
|||||||
*/
|
*/
|
||||||
export interface BaseEntity {
|
export interface BaseEntity {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── API URL Map ────────────────────────────────────────────────
|
// ─── API URL Map ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -52,6 +52,8 @@
|
|||||||
"activate": "Activate",
|
"activate": "Activate",
|
||||||
"deactivate": "Deactivate",
|
"deactivate": "Deactivate",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
|
"rollback": "Rollback",
|
||||||
|
"hold": "Hold",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"reload": "Reload"
|
"reload": "Reload"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,6 +52,8 @@
|
|||||||
"activate": "Aktifkan",
|
"activate": "Aktifkan",
|
||||||
"deactivate": "Nonaktifkan",
|
"deactivate": "Nonaktifkan",
|
||||||
"duplicate": "Duplikat",
|
"duplicate": "Duplikat",
|
||||||
|
"rollback": "Rollback",
|
||||||
|
"hold": "Hold",
|
||||||
"back": "Kembali",
|
"back": "Kembali",
|
||||||
"reload": "Muat Ulang"
|
"reload": "Muat Ulang"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,13 +60,8 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
|
|||||||
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover">
|
<Menu key={action.key} position="bottom-start" withArrow withinPortal trigger="hover">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
{/* Shortcuts on the main button remain hidden in the Tooltip */}
|
{/* Shortcuts on the main button remain hidden in the Tooltip */}
|
||||||
{action.shortcutLabel ? (
|
{action.tooltipLabel ? (
|
||||||
<Tooltip
|
<Tooltip position="bottom" label={`${action.tooltipLabel}`} withArrow openDelay={500}>
|
||||||
position="bottom"
|
|
||||||
label={`${action.label} (${action.shortcutLabel})`}
|
|
||||||
withArrow
|
|
||||||
openDelay={500}
|
|
||||||
>
|
|
||||||
{ButtonWithDropdown}
|
{ButtonWithDropdown}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
@@ -110,14 +105,8 @@ export const PageActions = memo(function PageActions({ actions = [], customButto
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
return action.shortcutLabel ? (
|
return action.tooltipLabel ? (
|
||||||
<Tooltip
|
<Tooltip position="bottom" key={action.key} label={`${action.tooltipLabel}`} withArrow openDelay={500}>
|
||||||
position="bottom"
|
|
||||||
key={action.key}
|
|
||||||
label={`${action.label} (${action.shortcutLabel})`}
|
|
||||||
withArrow
|
|
||||||
openDelay={500}
|
|
||||||
>
|
|
||||||
{StandaloneButton}
|
{StandaloneButton}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -39,8 +39,9 @@ export interface PageActionProps extends BaseAction {
|
|||||||
variant?: 'filled' | 'light' | 'outline' | 'default' | 'subtle' | 'transparent';
|
variant?: 'filled' | 'light' | 'outline' | 'default' | 'subtle' | 'transparent';
|
||||||
/** Nested actions rendered as a Dropdown Menu below the main button. */
|
/** Nested actions rendered as a Dropdown Menu below the main button. */
|
||||||
children?: PageActionProps[];
|
children?: PageActionProps[];
|
||||||
/** Human-readable keyboard shortcut label (e.g., '⇧⌘N'). Shown in tooltip. */
|
|
||||||
shortcutLabel?: string;
|
/** Human-readable keyboard tooltip label (e.g., '⇧⌘N'). Shown in tooltip. */
|
||||||
|
tooltipLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
interface ComingSoonProps {
|
interface ComingSoonProps {
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
|
height?: StyleProp<React.CSSProperties['height']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Title, Text, Button, Container, Stack, Center } from '@mantine/core';
|
import { Title, Text, Button, Container, Stack, Center, StyleProp } from '@mantine/core';
|
||||||
|
|
||||||
export function ComingSoon({ showActions = false }: ComingSoonProps) {
|
export function ComingSoon({ showActions = false, height = '100vh' }: ComingSoonProps) {
|
||||||
return (
|
return (
|
||||||
<Container size="sm" h="100vh" pos="relative">
|
<Container size="sm" h={height} pos="relative">
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<Stack align="center" ta="center" gap="md">
|
<Stack align="center" ta="center" gap="md">
|
||||||
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
|
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
|
||||||
@@ -22,7 +23,7 @@ export function ComingSoon({ showActions = false }: ComingSoonProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<Button component="a" href="/" color="brand" size="md" mt="xl">
|
<Button component="a" href="/" color="brand" size="sm" mt="xl">
|
||||||
Back to Home
|
Back to Home
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ interface ForbiddenProps {
|
|||||||
onClickGoBack?(): void;
|
onClickGoBack?(): void;
|
||||||
onClickBackToHome?(): void;
|
onClickBackToHome?(): void;
|
||||||
homeUrl?: string;
|
homeUrl?: string;
|
||||||
|
height?: StyleProp<React.CSSProperties['height']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Title, Text, Button, Container, Stack, Group, Center } from '@mantine/core';
|
import { Title, Text, Button, Container, Stack, Group, Center, StyleProp } from '@mantine/core';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export function Forbidden({
|
export function Forbidden({
|
||||||
@@ -15,6 +16,7 @@ export function Forbidden({
|
|||||||
onClickGoBack,
|
onClickGoBack,
|
||||||
onClickBackToHome,
|
onClickBackToHome,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
|
height = '100vh',
|
||||||
}: ForbiddenProps) {
|
}: ForbiddenProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -28,7 +30,7 @@ export function Forbidden({
|
|||||||
else if (homeUrl) navigate(homeUrl, { replace: true });
|
else if (homeUrl) navigate(homeUrl, { replace: true });
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Container size="sm" h="100vh" pos="relative">
|
<Container size="sm" h={height} pos="relative">
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<Stack align="center" ta="center" gap="md">
|
<Stack align="center" ta="center" gap="md">
|
||||||
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
|
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
|
||||||
@@ -47,13 +49,13 @@ export function Forbidden({
|
|||||||
{(showActionsBack || showActionsHome) && (
|
{(showActionsBack || showActionsHome) && (
|
||||||
<Group mt="xl" justify="center">
|
<Group mt="xl" justify="center">
|
||||||
{showActionsBack && (
|
{showActionsBack && (
|
||||||
<Button variant="default" size="md" onClick={onBack}>
|
<Button variant="default" size="sm" onClick={onBack}>
|
||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showActionsHome && (
|
{showActionsHome && (
|
||||||
<Button color="brand" size="md" onClick={onBackHome}>
|
<Button color="brand" size="sm" onClick={onBackHome}>
|
||||||
Back to Home
|
Back to Home
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
interface MaintenanceProps {
|
interface MaintenanceProps {
|
||||||
showActions?: boolean;
|
showActions?: boolean;
|
||||||
|
height?: StyleProp<React.CSSProperties['height']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Title, Text, Button, Container, Stack, Center } from '@mantine/core';
|
import { Title, Text, Button, Container, Stack, Center, StyleProp } from '@mantine/core';
|
||||||
|
|
||||||
export function Maintenance({ showActions = false }: MaintenanceProps) {
|
export function Maintenance({ showActions = false, height = '100vh' }: MaintenanceProps) {
|
||||||
return (
|
return (
|
||||||
<Container size="sm" h="100vh" pos="relative">
|
<Container size="sm" h={height} pos="relative">
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<Stack align="center" ta="center" gap="md">
|
<Stack align="center" ta="center" gap="md">
|
||||||
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
|
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
|
||||||
@@ -23,7 +24,7 @@ export function Maintenance({ showActions = false }: MaintenanceProps) {
|
|||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<Button component="a" href="/" color="brand" size="md" mt="xl">
|
<Button component="a" href="/" color="brand" size="sm" mt="xl">
|
||||||
Back to Home
|
Back to Home
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ interface NotFoundProps {
|
|||||||
onClickGoBack?(): void;
|
onClickGoBack?(): void;
|
||||||
onClickBackToHome?(): void;
|
onClickBackToHome?(): void;
|
||||||
homeUrl?: string;
|
homeUrl?: string;
|
||||||
|
height?: StyleProp<React.CSSProperties['height']>;
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Title, Text, Button, Container, Stack, Group, Center } from '@mantine/core';
|
import { Title, Text, Button, Container, Stack, Group, Center, StyleProp } from '@mantine/core';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export function NotFound({
|
export function NotFound({
|
||||||
@@ -15,6 +16,7 @@ export function NotFound({
|
|||||||
onClickGoBack,
|
onClickGoBack,
|
||||||
onClickBackToHome,
|
onClickBackToHome,
|
||||||
homeUrl,
|
homeUrl,
|
||||||
|
height = '100vh',
|
||||||
}: NotFoundProps) {
|
}: NotFoundProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -29,7 +31,7 @@ export function NotFound({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="sm" h="100vh" pos="relative">
|
<Container size="sm" h={height} pos="relative">
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
<Stack align="center" ta="center" gap="md">
|
<Stack align="center" ta="center" gap="md">
|
||||||
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
|
<Text size="sm" fw={500} tt="uppercase" c="dimmed" lts="var(--mantine-spacing-xs)">
|
||||||
@@ -47,13 +49,13 @@ export function NotFound({
|
|||||||
{(showActionsBack || showActionsHome) && (
|
{(showActionsBack || showActionsHome) && (
|
||||||
<Group mt="xl" justify="center">
|
<Group mt="xl" justify="center">
|
||||||
{showActionsBack && (
|
{showActionsBack && (
|
||||||
<Button variant="default" size="md" onClick={onBack}>
|
<Button variant="default" size="sm" onClick={onBack}>
|
||||||
Go Back
|
Go Back
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showActionsHome && (
|
{showActionsHome && (
|
||||||
<Button color="brand" size="md" onClick={onBackHome}>
|
<Button color="brand" size="sm" onClick={onBackHome}>
|
||||||
Back to Home
|
Back to Home
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { ReactNode, useCallback, useMemo } from 'react';
|
||||||
import { Title, Breadcrumbs, Anchor, Box, Text, ThemeIcon, Flex, Divider, ActionIcon, Tooltip } from '@mantine/core';
|
import {
|
||||||
|
Title,
|
||||||
|
TitleProps,
|
||||||
|
Breadcrumbs,
|
||||||
|
Anchor,
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
|
Flex,
|
||||||
|
Divider,
|
||||||
|
ActionIcon,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mantine/core';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import { ChevronRight, LucideIcon, Maximize2, Minimize2 } from 'lucide-react'; // <-- Update Import Icon
|
import { ChevronRight, LucideIcon, Maximize2, Minimize2 } from 'lucide-react'; // <-- Update Import Icon
|
||||||
import { PageActions, PageActionsProps } from '../../../components';
|
import { PageActions, PageActionsProps } from '../../../components';
|
||||||
@@ -15,15 +27,20 @@ export interface BreadcrumbItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ModulePageHeaderProps {
|
export interface ModulePageHeaderProps {
|
||||||
title?: string;
|
title?: ReactNode;
|
||||||
|
titleProps?: Omit<TitleProps, 'children'>;
|
||||||
|
miniTitleProps?: Omit<TitleProps, 'children'>;
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
icon?: LucideIcon;
|
icon?: LucideIcon;
|
||||||
badges?: React.ReactNode;
|
badges?: React.ReactNode;
|
||||||
breadcrumbs?: BreadcrumbItem[];
|
breadcrumbs?: BreadcrumbItem[];
|
||||||
actions?: PageActionsProps['actions'];
|
|
||||||
showPageHeader?: boolean;
|
showPageHeader?: boolean;
|
||||||
disableMinimize?: boolean;
|
disableMinimize?: boolean;
|
||||||
moduleKey: string;
|
moduleKey: string;
|
||||||
|
|
||||||
|
actions?: PageActionsProps['actions'];
|
||||||
|
customButtonProps?: PageActionsProps['customButtonProps'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -46,7 +63,7 @@ function BreadcrumbBar({ breadcrumbs }: BreadcrumbBarProps) {
|
|||||||
return (
|
return (
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
style={{ flexWrap: 'wrap' }}
|
style={{ flexWrap: 'wrap' }}
|
||||||
visibleFrom="sm"
|
// visibleFrom="xs"
|
||||||
separator={
|
separator={
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
size={12}
|
size={12}
|
||||||
@@ -126,11 +143,14 @@ function HeaderToggle({ isMinimized, onToggle }: HeaderToggleProps) {
|
|||||||
export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
|
titleProps,
|
||||||
|
miniTitleProps,
|
||||||
description,
|
description,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
badges,
|
badges,
|
||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
actions,
|
actions,
|
||||||
|
customButtonProps,
|
||||||
showPageHeader = true,
|
showPageHeader = true,
|
||||||
disableMinimize = false,
|
disableMinimize = false,
|
||||||
moduleKey,
|
moduleKey,
|
||||||
@@ -148,7 +168,13 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
|||||||
|
|
||||||
const showToggle = !disableMinimize;
|
const showToggle = !disableMinimize;
|
||||||
const handleToggle = useCallback(() => setIsMinimized((v) => !v), [setIsMinimized]);
|
const handleToggle = useCallback(() => setIsMinimized((v) => !v), [setIsMinimized]);
|
||||||
const compactButtonProps = useMemo(() => () => ({ size: 'xs' as const }), []);
|
const compactButtonProps = useMemo(
|
||||||
|
() => (action: any) => {
|
||||||
|
if (customButtonProps) return customButtonProps(action);
|
||||||
|
return { size: 'xs' as const };
|
||||||
|
},
|
||||||
|
[customButtonProps],
|
||||||
|
);
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Compact / Toolbar mode
|
// Compact / Toolbar mode
|
||||||
@@ -167,19 +193,21 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
|||||||
{/* Left cluster: title + badges */}
|
{/* Left cluster: title + badges */}
|
||||||
<Flex gap="xs" align="center" style={{ flex: 1, minWidth: 0 }}>
|
<Flex gap="xs" align="center" style={{ flex: 1, minWidth: 0 }}>
|
||||||
{title && (
|
{title && (
|
||||||
<Text
|
<Title
|
||||||
fw={600}
|
fw={600}
|
||||||
fz={{ base: 'lg', sm: 'xl' }}
|
fz={{ base: 'lg', sm: 'xl' }}
|
||||||
truncate="end"
|
lineClamp={1}
|
||||||
|
{...(miniTitleProps ?? {})}
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--mantine-color-text)',
|
color: 'var(--mantine-color-text)',
|
||||||
letterSpacing: '-0.2px',
|
letterSpacing: '-0.2px',
|
||||||
lineHeight: 1.3,
|
lineHeight: 1.3,
|
||||||
...TRANSITION_STYLE,
|
...TRANSITION_STYLE,
|
||||||
|
...miniTitleProps?.style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Title>
|
||||||
)}
|
)}
|
||||||
{badges && <Box style={{ flexShrink: 0 }}>{badges}</Box>}
|
{badges && <Box style={{ flexShrink: 0 }}>{badges}</Box>}
|
||||||
</Flex>
|
</Flex>
|
||||||
@@ -262,12 +290,15 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
|||||||
<Title
|
<Title
|
||||||
order={2}
|
order={2}
|
||||||
fw={600}
|
fw={600}
|
||||||
|
lineClamp={1}
|
||||||
fz={{ base: 20, sm: 24 }}
|
fz={{ base: 20, sm: 24 }}
|
||||||
lh={{ base: 1.3, sm: 1.2 }}
|
lh={{ base: 1.3, sm: 1.2 }}
|
||||||
|
{...(titleProps ?? {})}
|
||||||
style={{
|
style={{
|
||||||
color: 'var(--mantine-color-text)',
|
color: 'var(--mantine-color-text)',
|
||||||
letterSpacing: '-0.3px',
|
letterSpacing: '-0.3px',
|
||||||
...TRANSITION_STYLE,
|
...TRANSITION_STYLE,
|
||||||
|
...titleProps?.style,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title}
|
{title}
|
||||||
@@ -302,7 +333,7 @@ export function ModulePageHeader(_props: ModulePageHeaderProps) {
|
|||||||
ml={{ base: 'lg', sm: 0 }}
|
ml={{ base: 'lg', sm: 0 }}
|
||||||
style={{ flexShrink: 0, alignSelf: 'center' }}
|
style={{ flexShrink: 0, alignSelf: 'center' }}
|
||||||
>
|
>
|
||||||
{actions && <PageActions actions={actions} />}
|
{actions && <PageActions actions={actions} customButtonProps={customButtonProps} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ export const defaultPrivileges: PrivilegeEntity = {
|
|||||||
ALLOW_CREATE: true,
|
ALLOW_CREATE: true,
|
||||||
ALLOW_EDIT: true,
|
ALLOW_EDIT: true,
|
||||||
ALLOW_DELETE: true,
|
ALLOW_DELETE: true,
|
||||||
ALLOW_DUPLICATE: true,
|
|
||||||
ALLOW_SAVE: true,
|
|
||||||
|
|
||||||
ALLOW_PRINT: true,
|
ALLOW_PRINT: true,
|
||||||
ALLOW_PRINT_COPY: true,
|
ALLOW_PRINT_COPY: true,
|
||||||
|
|||||||
@@ -190,12 +190,13 @@ export interface EnterpriseFormLifecycleHooks<E extends BaseEntity, TFormData =
|
|||||||
afterGetData?: (data: E) => Promise<TFormData>;
|
afterGetData?: (data: E) => Promise<TFormData>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnterpriseIndexPageConfig {
|
interface BasePageConfig {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
px?: string | number;
|
px?: string | number;
|
||||||
py?: string | number;
|
py?: string | number;
|
||||||
|
|
||||||
pageHeaderProps?: Omit<ModulePageHeaderProps, 'actions' | 'moduleKey'>;
|
pageHeaderProps?: Omit<ModulePageHeaderProps, 'actions' | 'moduleKey'>;
|
||||||
|
}
|
||||||
|
export interface EnterpriseIndexPageConfig extends BasePageConfig {
|
||||||
customPageActions?: (actions: PageActionsProps['actions']) => PageActionsProps['actions'];
|
customPageActions?: (actions: PageActionsProps['actions']) => PageActionsProps['actions'];
|
||||||
onClickCreate?: (key: string) => void;
|
onClickCreate?: (key: string) => void;
|
||||||
}
|
}
|
||||||
@@ -218,14 +219,28 @@ export interface EnterpriseFormPageConfig<E extends BaseEntity = BaseEntity> ext
|
|||||||
presetDuplicate?: (data: E) => Promise<Partial<E>>;
|
presetDuplicate?: (data: E) => Promise<Partial<E>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> {
|
export interface EnterpriseDetailPageConfig<E extends BaseEntity = BaseEntity> extends BasePageConfig {
|
||||||
children?: ReactNode;
|
|
||||||
showPageHeader?: boolean;
|
|
||||||
useDefaultPadding?: boolean;
|
|
||||||
editMode?: 'FULL' | 'PARTIAL';
|
editMode?: 'FULL' | 'PARTIAL';
|
||||||
|
|
||||||
pageHeaderProps?: Omit<ModulePageHeaderProps, 'actions' | 'moduleKey'>;
|
|
||||||
customPageActions?: (data: E, actions: PageActionsProps['actions']) => PageActionsProps['actions'];
|
customPageActions?: (data: E, actions: PageActionsProps['actions']) => PageActionsProps['actions'];
|
||||||
|
|
||||||
|
onClickCreate?: () => void;
|
||||||
|
onClickDuplicate?: (data: E) => void;
|
||||||
|
onClickEdit?: (data: E) => void;
|
||||||
|
onClickDelete?: (data: E) => void;
|
||||||
|
|
||||||
|
// Master Data Feature
|
||||||
|
onClickActivate?: (data: E) => void;
|
||||||
|
onClickDeactivate?: (data: E) => void;
|
||||||
|
|
||||||
|
// Transaction Feature
|
||||||
|
onClickConfirm?: (data: E) => void;
|
||||||
|
onClickCancel?: (data: E) => void;
|
||||||
|
onClickRollback?: (data: E) => void;
|
||||||
|
onClickHold?: (data: E) => void;
|
||||||
|
|
||||||
|
showHighlightData?: boolean;
|
||||||
|
showHighlightDataOnBreadcrumbs?: boolean;
|
||||||
|
highlightDataKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrivilegeEntity {
|
export interface PrivilegeEntity {
|
||||||
@@ -233,8 +248,6 @@ export interface PrivilegeEntity {
|
|||||||
ALLOW_CREATE: boolean;
|
ALLOW_CREATE: boolean;
|
||||||
ALLOW_EDIT: boolean;
|
ALLOW_EDIT: boolean;
|
||||||
ALLOW_DELETE: boolean;
|
ALLOW_DELETE: boolean;
|
||||||
ALLOW_DUPLICATE: boolean;
|
|
||||||
ALLOW_SAVE: boolean;
|
|
||||||
|
|
||||||
ALLOW_PRINT: boolean;
|
ALLOW_PRINT: boolean;
|
||||||
ALLOW_PRINT_COPY: boolean;
|
ALLOW_PRINT_COPY: boolean;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface DetailPageContextValue<E extends BaseEntity = BaseEntity> {
|
|||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
reload: () => Promise<void>;
|
reload: () => Promise<void>;
|
||||||
isPartialEdit: boolean;
|
isPartialEdit: boolean;
|
||||||
|
isActiveEditMode: boolean;
|
||||||
|
setIsActiveEditMode: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DetailPageContext = createContext<DetailPageContextValue<any> | null>(null);
|
export const DetailPageContext = createContext<DetailPageContextValue<any> | null>(null);
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export * from './hooks/use-module.context';
|
|||||||
|
|
||||||
export * from './providers/module.provider';
|
export * from './providers/module.provider';
|
||||||
export * from './providers/index-page.provider';
|
export * from './providers/index-page.provider';
|
||||||
|
export * from './providers/detail-page.provider';
|
||||||
export * from './components/module-page-header';
|
export * from './components/module-page-header';
|
||||||
|
|||||||
@@ -1,17 +1,488 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import { BaseEntity } from '@repo/core-api/data-services';
|
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 { DetailPageContext } from '../hooks/use-detail-page.context';
|
||||||
import { EnterpriseDetailPageConfig } from '../entities/entity';
|
import { EnterpriseDetailPageConfig, ModuleAction, ModuleActionType } from '../entities/entity';
|
||||||
|
import { CorePageContainer, PageActionProps } from '../../../components';
|
||||||
|
import { ModulePageHeader, ModulePageHeaderProps } from '../components/module-page-header';
|
||||||
|
import {
|
||||||
|
useEnterpriseModuleConfigContext,
|
||||||
|
useEnterpriseModuleDataServiceContext,
|
||||||
|
useEnterpriseModuleNavigationContext,
|
||||||
|
useEnterpriseModuleTranslationContext,
|
||||||
|
} from '../hooks/use-module.context';
|
||||||
|
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,
|
||||||
showPageHeader,
|
editMode = 'FULL',
|
||||||
useDefaultPadding,
|
|
||||||
editMode,
|
|
||||||
|
|
||||||
pageHeaderProps,
|
pageHeaderProps,
|
||||||
|
px,
|
||||||
|
py,
|
||||||
customPageActions,
|
customPageActions,
|
||||||
|
onClickCreate,
|
||||||
|
onClickDuplicate,
|
||||||
|
onClickEdit,
|
||||||
|
onClickDelete,
|
||||||
|
onClickActivate,
|
||||||
|
onClickDeactivate,
|
||||||
|
onClickConfirm,
|
||||||
|
onClickCancel,
|
||||||
|
onClickRollback,
|
||||||
|
onClickHold,
|
||||||
|
|
||||||
|
showHighlightData = true,
|
||||||
|
showHighlightDataOnBreadcrumbs = true,
|
||||||
|
highlightDataKey = 'code',
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return <DetailPageContext.Provider value={{}}></DetailPageContext.Provider>;
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
|
const navigation = useEnterpriseModuleNavigationContext();
|
||||||
|
const { config, privileges, IS_MACOS } = useEnterpriseModuleConfigContext();
|
||||||
|
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
|
||||||
|
|
||||||
|
const [isActiveEditMode, setIsActiveEditMode] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const params = useParams();
|
||||||
|
const dataId = params.dataId;
|
||||||
|
|
||||||
|
const { moduleKey, moduleType } = config;
|
||||||
|
|
||||||
|
const [detailData, setDetailData] = useState<E | any>({ id: 1, code: 'ABC-001' });
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const loadData = useCallback(async () => {
|
||||||
|
if (!dataId) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await dataServices.getOne(dataId);
|
||||||
|
if (response && response.data) {
|
||||||
|
setDetailData(response.data as E);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load detail data', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}, [dataId, dataServices]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadData();
|
||||||
|
}, [loadData]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 1. Stub Handlers (Sudah diperbaiki typonya & lengkap)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function handleDelete(data: E): Promise<void> {
|
||||||
|
// implementation later
|
||||||
|
console.log({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleActivate(data: E): Promise<void> {
|
||||||
|
// implementation later
|
||||||
|
console.log({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDeactivate(data: E): Promise<void> {
|
||||||
|
// implementation later
|
||||||
|
console.log({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleConfirm(data: E): Promise<void> {
|
||||||
|
// implementation later
|
||||||
|
console.log({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCancel(data: E): Promise<void> {
|
||||||
|
// implementation later
|
||||||
|
console.log({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRollback(data: E): Promise<void> {
|
||||||
|
// implementation later
|
||||||
|
console.log({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleHold(data: E): Promise<void> {
|
||||||
|
// implementation later
|
||||||
|
console.log({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 2. Action Dispatcher (Optimized with Switch Case & Complete Deps)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const handleActionClick = useCallback(
|
||||||
|
async (key: string) => {
|
||||||
|
// Guard utama: Untuk aksi selain CREATE, pastikan dataId dan detailData sudah ada
|
||||||
|
const hasValidData = Boolean(dataId && detailData);
|
||||||
|
const currentData = detailData as E;
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
// --- CREATE ---
|
||||||
|
case ModuleAction.CREATE:
|
||||||
|
if (!privileges.ALLOW_CREATE) return;
|
||||||
|
if (onClickCreate) onClickCreate();
|
||||||
|
else navigation.navigateToCreate();
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- EDIT ---
|
||||||
|
case ModuleAction.EDIT:
|
||||||
|
if (!privileges.ALLOW_EDIT || !hasValidData) return;
|
||||||
|
if (onClickEdit) onClickEdit(currentData);
|
||||||
|
else if (editMode === 'FULL') navigation.navigateToEdit(dataId!);
|
||||||
|
else setIsActiveEditMode(true);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- DUPLICATE ---
|
||||||
|
case ModuleAction.DUPLICATE:
|
||||||
|
if (!privileges.ALLOW_CREATE || !hasValidData) return;
|
||||||
|
if (onClickDuplicate) onClickDuplicate(currentData);
|
||||||
|
else navigation.navigateToDuplicate(dataId!);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- DELETE ---
|
||||||
|
case ModuleAction.DELETE:
|
||||||
|
if (!privileges.ALLOW_DELETE || !hasValidData) return;
|
||||||
|
if (onClickDelete) onClickDelete(currentData);
|
||||||
|
else await handleDelete(currentData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- ACTIVATE ---
|
||||||
|
case ModuleAction.ACTIVATE:
|
||||||
|
if (!privileges.ALLOW_ACTIVATE || !hasValidData) return;
|
||||||
|
if (onClickActivate) onClickActivate(currentData);
|
||||||
|
else await handleActivate(currentData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- DEACTIVATE ---
|
||||||
|
case ModuleAction.DEACTIVATE:
|
||||||
|
if (!privileges.ALLOW_DEACTIVATE || !hasValidData) return;
|
||||||
|
if (onClickDeactivate) onClickDeactivate(currentData);
|
||||||
|
else await handleDeactivate(currentData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- CONFIRM (Tambahan Baru) ---
|
||||||
|
case ModuleAction.CONFIRM:
|
||||||
|
if (!privileges.ALLOW_CONFIRM || !hasValidData) return;
|
||||||
|
if (onClickConfirm) onClickConfirm(currentData);
|
||||||
|
else await handleConfirm(currentData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- CANCEL (Tambahan Baru) ---
|
||||||
|
case ModuleAction.CANCEL:
|
||||||
|
if (!privileges.ALLOW_CANCEL || !hasValidData) return;
|
||||||
|
if (onClickCancel) onClickCancel(currentData);
|
||||||
|
else await handleCancel(currentData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- ROLLBACK (Tambahan Baru) ---
|
||||||
|
case ModuleAction.ROLLBACK:
|
||||||
|
if (!privileges.ALLOW_ROLLBACK || !hasValidData) return;
|
||||||
|
if (onClickRollback) onClickRollback(currentData);
|
||||||
|
else await handleRollback(currentData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// --- HOLD (Tambahan Baru) ---
|
||||||
|
case ModuleAction.HOLD:
|
||||||
|
if (!privileges.ALLOW_HOLD || !hasValidData) return;
|
||||||
|
if (onClickHold) onClickHold(currentData);
|
||||||
|
else await handleHold(currentData);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`[ActionHandler] Unhandled action key: ${key}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Semua dependensi wajib dimasukkan agar terhindar dari bug Stale Closure
|
||||||
|
navigation,
|
||||||
|
privileges,
|
||||||
|
dataId,
|
||||||
|
detailData,
|
||||||
|
editMode,
|
||||||
|
setIsActiveEditMode,
|
||||||
|
onClickCreate,
|
||||||
|
onClickEdit,
|
||||||
|
onClickDuplicate,
|
||||||
|
onClickDelete,
|
||||||
|
onClickActivate,
|
||||||
|
onClickDeactivate,
|
||||||
|
onClickConfirm,
|
||||||
|
onClickCancel,
|
||||||
|
onClickRollback,
|
||||||
|
onClickHold,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Platform-aware shortcut label for the Create action. */
|
||||||
|
const CREATE_SHORTCUT_LABEL = useMemo(() => {
|
||||||
|
const shortcutData = shortcutsData.find((s) => s.key === 'collapse_sidebar');
|
||||||
|
return IS_MACOS ? shortcutData?.macKeyIcons.join(' ') : shortcutData?.winKeyIcons.join(' ');
|
||||||
|
}, [IS_MACOS]);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
const pageActions = useMemo(() => {
|
||||||
|
const {
|
||||||
|
ALLOW_CREATE,
|
||||||
|
ALLOW_EDIT,
|
||||||
|
ALLOW_DELETE,
|
||||||
|
ALLOW_ACTIVATE,
|
||||||
|
ALLOW_DEACTIVATE,
|
||||||
|
ALLOW_CONFIRM,
|
||||||
|
ALLOW_CANCEL,
|
||||||
|
ALLOW_ROLLBACK,
|
||||||
|
ALLOW_HOLD,
|
||||||
|
} = privileges;
|
||||||
|
|
||||||
|
const isTransaction = moduleType === 'TRANSACTION';
|
||||||
|
const isMasterData = moduleType === 'MASTER_DATA';
|
||||||
|
|
||||||
|
// 1. Declare action with Privilege & Module Type conditions directly
|
||||||
|
const rawActions = [
|
||||||
|
ALLOW_DELETE && {
|
||||||
|
key: ModuleAction.DELETE,
|
||||||
|
label: t('common:actions.delete'),
|
||||||
|
icon: <Trash2 size={16} />,
|
||||||
|
intent: 'destructive',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
ALLOW_CREATE && {
|
||||||
|
key: ModuleAction.DUPLICATE,
|
||||||
|
label: t('common:actions.duplicate'),
|
||||||
|
icon: <Copy size={16} />,
|
||||||
|
intent: 'default',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
ALLOW_EDIT && {
|
||||||
|
key: ModuleAction.EDIT,
|
||||||
|
label: t('common:actions.edit'),
|
||||||
|
icon: <Edit2 size={16} />,
|
||||||
|
intent: 'default',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
|
||||||
|
{ key: 'DIVIDER_1', type: 'divider' }, // Will be cleared if empty
|
||||||
|
|
||||||
|
// Transaction Group
|
||||||
|
isTransaction &&
|
||||||
|
ALLOW_HOLD && {
|
||||||
|
key: ModuleAction.HOLD,
|
||||||
|
label: t('common:actions.hold'),
|
||||||
|
icon: <PauseCircle size={16} />,
|
||||||
|
intent: 'default',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
isTransaction &&
|
||||||
|
ALLOW_ROLLBACK && {
|
||||||
|
key: ModuleAction.ROLLBACK,
|
||||||
|
label: t('common:actions.rollback'),
|
||||||
|
icon: <RotateCcw size={16} />,
|
||||||
|
intent: 'default',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
isTransaction &&
|
||||||
|
ALLOW_CANCEL && {
|
||||||
|
key: ModuleAction.CANCEL,
|
||||||
|
label: t('common:actions.cancel'),
|
||||||
|
icon: <X size={16} />,
|
||||||
|
intent: 'default',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
isTransaction &&
|
||||||
|
ALLOW_CONFIRM && {
|
||||||
|
key: ModuleAction.CONFIRM,
|
||||||
|
label: t('common:actions.confirm'),
|
||||||
|
icon: <Check size={16} />,
|
||||||
|
intent: 'default',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
|
||||||
|
// Master Data Group
|
||||||
|
isMasterData &&
|
||||||
|
ALLOW_DEACTIVATE && {
|
||||||
|
key: ModuleAction.DEACTIVATE,
|
||||||
|
label: t('common:actions.deactivate'),
|
||||||
|
icon: <XCircle size={16} />,
|
||||||
|
intent: 'default',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
isMasterData &&
|
||||||
|
ALLOW_ACTIVATE && {
|
||||||
|
key: ModuleAction.ACTIVATE,
|
||||||
|
label: t('common:actions.activate'),
|
||||||
|
tooltipLabel: t('common:actions.activate'),
|
||||||
|
icon: <CheckCircle size={16} />,
|
||||||
|
intent: 'default',
|
||||||
|
variant: 'subtle',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
|
||||||
|
{ key: 'DIVIDER_2', type: 'divider' },
|
||||||
|
|
||||||
|
ALLOW_CREATE && {
|
||||||
|
key: ModuleAction.CREATE,
|
||||||
|
label: t('common:actions.create'),
|
||||||
|
tooltipLabel: `${t('common:actions.create')} (${CREATE_SHORTCUT_LABEL})`,
|
||||||
|
|
||||||
|
icon: <Plus size={16} />,
|
||||||
|
intent: 'primary',
|
||||||
|
variant: 'filled',
|
||||||
|
onClick: (key: ModuleActionType) => handleActionClick(key),
|
||||||
|
},
|
||||||
|
].filter(Boolean) as PageActionProps[]; // Remove all false/null/undefined
|
||||||
|
|
||||||
|
// 2. Smart Divider Cleaning Algorithm
|
||||||
|
const cleanedActions: PageActionProps[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < rawActions.length; i++) {
|
||||||
|
const current = rawActions[i];
|
||||||
|
|
||||||
|
if (current.type === 'divider') {
|
||||||
|
// Ignore if this divider is at the front (beginning of the array)
|
||||||
|
if (cleanedActions.length === 0) continue;
|
||||||
|
|
||||||
|
// Ignore if the previous item is also a divider (prevents nesting: || )
|
||||||
|
if (cleanedActions[cleanedActions.length - 1].type === 'divider') continue;
|
||||||
|
|
||||||
|
// Ignore if after this divider there are no action buttons at all (prevent at the end: | )
|
||||||
|
const hasActionAfter = rawActions.slice(i + 1).some((a) => a.type !== 'divider');
|
||||||
|
if (!hasActionAfter) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanedActions.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Inject custom actions
|
||||||
|
return customPageActions && detailData ? customPageActions(detailData, cleanedActions) : cleanedActions;
|
||||||
|
}, [t, customPageActions, handleActionClick, privileges, moduleType, detailData]);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
detailData,
|
||||||
|
isLoading,
|
||||||
|
reload: loadData,
|
||||||
|
isPartialEdit: editMode === 'PARTIAL',
|
||||||
|
isActiveEditMode,
|
||||||
|
setIsActiveEditMode,
|
||||||
|
}),
|
||||||
|
[detailData, isLoading, loadData, editMode, isActiveEditMode, setIsActiveEditMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
function makeTitle(showHighlight: boolean, key: string, pageProvide: ModulePageHeaderProps | any, detailData: any) {
|
||||||
|
const staticTitle = pageProvide?.title;
|
||||||
|
if (!showHighlight) {
|
||||||
|
return { flatTitle: staticTitle, title: staticTitle };
|
||||||
|
} else {
|
||||||
|
const highlightData = detailData[key];
|
||||||
|
const flatTitle = `${staticTitle} | ${highlightData}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
flatTitle,
|
||||||
|
title: (
|
||||||
|
<span>
|
||||||
|
{staticTitle}
|
||||||
|
{highlightData && (
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight: 400,
|
||||||
|
marginLeft: '8px',
|
||||||
|
color: 'var(--mantine-color-dimmed)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
| {highlightData}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeBreadcrumbs(
|
||||||
|
showHighlight: boolean,
|
||||||
|
key: string,
|
||||||
|
pageProvide: ModulePageHeaderProps | any,
|
||||||
|
detailData: any,
|
||||||
|
) {
|
||||||
|
const staticBreadcrumbs = pageProvide.breadcrumbs ?? [];
|
||||||
|
if (!showHighlight || staticBreadcrumbs.length === 0) {
|
||||||
|
return pageProvide.breadcrumbs;
|
||||||
|
} else {
|
||||||
|
const highlightData = detailData[key];
|
||||||
|
const breadcrumbs = [
|
||||||
|
...staticBreadcrumbs,
|
||||||
|
{
|
||||||
|
type: 'link',
|
||||||
|
label: `${highlightData}`,
|
||||||
|
href: `${config.webUrl}/detail/${dataId}`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return breadcrumbs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageHeaderPropsValue = useMemo(() => {
|
||||||
|
const title = makeTitle(showHighlightData, highlightDataKey, pageHeaderProps, detailData);
|
||||||
|
document.title = title.flatTitle;
|
||||||
|
const breadcrumbs = makeBreadcrumbs(showHighlightDataOnBreadcrumbs, highlightDataKey, pageHeaderProps, detailData);
|
||||||
|
return { ...pageHeaderProps, title: title.title, breadcrumbs: breadcrumbs };
|
||||||
|
}, [pageHeaderProps, dataId, detailData, showHighlightData, showHighlightDataOnBreadcrumbs, highlightDataKey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DetailPageContext.Provider value={contextValue}>
|
||||||
|
<CorePageContainer
|
||||||
|
px={px}
|
||||||
|
py={py}
|
||||||
|
headerSlot={
|
||||||
|
<ModulePageHeader
|
||||||
|
customButtonProps={(action) => {
|
||||||
|
return {
|
||||||
|
size: 'xs',
|
||||||
|
p: action.key === ModuleAction.CREATE ? undefined : 5,
|
||||||
|
style: { fontSize: 12 },
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
actions={pageActions}
|
||||||
|
{...pageHeaderPropsValue}
|
||||||
|
moduleKey={moduleKey}
|
||||||
|
titleProps={{
|
||||||
|
fz: { base: 16, sm: 18 },
|
||||||
|
}}
|
||||||
|
miniTitleProps={{
|
||||||
|
fz: { base: 'md', sm: 'lg' },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</CorePageContainer>
|
||||||
|
</DetailPageContext.Provider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { BaseEntity } from '@repo/core-api/data-services';
|
|
||||||
import { EnterpriseIndexPageConfig, ModuleAction } from '../entities/entity';
|
import { EnterpriseIndexPageConfig, ModuleAction } from '../entities/entity';
|
||||||
import { IndexPageContext } from '../hooks/use-index-page.context';
|
import { IndexPageContext } from '../hooks/use-index-page.context';
|
||||||
import { CorePageContainer, PageActionProps } from '../../../components';
|
import { CorePageContainer, PageActionProps } from '../../../components';
|
||||||
@@ -70,7 +69,7 @@ export function EnterpriseIndexPageProvider(props: EnterpriseIndexPageConfig) {
|
|||||||
icon: <Plus size={16} />,
|
icon: <Plus size={16} />,
|
||||||
intent: 'primary',
|
intent: 'primary',
|
||||||
variant: 'filled',
|
variant: 'filled',
|
||||||
shortcutLabel: CREATE_SHORTCUT_LABEL,
|
tooltipLabel: `${t('common:actions.create')} (${CREATE_SHORTCUT_LABEL})`,
|
||||||
onClick: (key) => handleActionClick(key),
|
onClick: (key) => handleActionClick(key),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState, useEffect, ReactNode } from 'react';
|
import { useMemo, useState, ReactNode } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from '@repo/core-i18n';
|
import { useTranslation } from '@repo/core-i18n';
|
||||||
import { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
import { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
EnterpriseTranslationContext,
|
EnterpriseTranslationContext,
|
||||||
} from '../hooks/use-module.context';
|
} from '../hooks/use-module.context';
|
||||||
import { defaultPrivileges } from '../constant/default-privilege';
|
import { defaultPrivileges } from '../constant/default-privilege';
|
||||||
|
import { Forbidden } from '../../../components';
|
||||||
|
|
||||||
export interface EnterpriseModuleProviderProps<E extends BaseEntity> {
|
export interface EnterpriseModuleProviderProps<E extends BaseEntity> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -47,12 +48,12 @@ export function EnterpriseModuleProvider<E extends BaseEntity>(props: Enterprise
|
|||||||
const translationSlice = useMemo(() => ({ t: t as (key: string, options?: Record<string, unknown>) => string }), [t]);
|
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'
|
// Set document title — uses explicit tabTitle or falls back to translation key 'title'
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const title = config.tabTitle || t('title');
|
// const title = config.tabTitle || t('title');
|
||||||
if (title) {
|
// if (title) {
|
||||||
document.title = title;
|
// document.title = title;
|
||||||
}
|
// }
|
||||||
}, [config.tabTitle, t]);
|
// }, [config.tabTitle, t]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 2. Data Service Slice (Stable refs)
|
// 2. Data Service Slice (Stable refs)
|
||||||
@@ -143,13 +144,7 @@ export function EnterpriseModuleProvider<E extends BaseEntity>(props: Enterprise
|
|||||||
|
|
||||||
const { ALLOW_VIEW } = configSlice.privileges;
|
const { ALLOW_VIEW } = configSlice.privileges;
|
||||||
|
|
||||||
useEffect(() => {
|
if (!ALLOW_VIEW) return <Forbidden homeUrl="/app" height={500} />;
|
||||||
if (!ALLOW_VIEW) navigate('/403', { replace: true });
|
|
||||||
}, [ALLOW_VIEW, navigate]);
|
|
||||||
|
|
||||||
if (!ALLOW_VIEW) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnterpriseConfigContext.Provider value={configSlice}>
|
<EnterpriseConfigContext.Provider value={configSlice}>
|
||||||
|
|||||||
Reference in New Issue
Block a user