refactor: integrate Zustand store into EnterpriseModuleProvider and update module configurations to use store-based state management
This commit is contained in:
@@ -4,7 +4,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations';
|
|||||||
* Core configuration and constants for the Full Page module.
|
* Core configuration and constants for the Full Page module.
|
||||||
* Used across Domain, Data, and Presentation layers.
|
* Used across Domain, Data, and Presentation layers.
|
||||||
*/
|
*/
|
||||||
export const FullPageModuleConfig: ModuleConfigEntity = {
|
export const fullPageModuleConfig: ModuleConfigEntity = {
|
||||||
/** Unique identifier for permissions, caching, and i18n */
|
/** Unique identifier for permissions, caching, and i18n */
|
||||||
moduleKey: 'EXAMPLE_FULL_PAGE',
|
moduleKey: 'EXAMPLE_FULL_PAGE',
|
||||||
|
|
||||||
@@ -21,6 +21,5 @@ export const FullPageModuleConfig: ModuleConfigEntity = {
|
|||||||
moduleCategory: 'FULL_PAGE',
|
moduleCategory: 'FULL_PAGE',
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
// moduleType: 'MASTER_DATA',
|
moduleType: 'MASTER_DATA',
|
||||||
moduleType: 'TRANSACTION',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { lazy } from 'react';
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
|
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
|
||||||
import { registerModuleNamespace } from '@repo/core-i18n';
|
import { registerModuleNamespace } from '@repo/core-i18n';
|
||||||
import { FullPageModuleConfig } from '../../domain/constants';
|
import { fullPageModuleConfig } from '../../domain/constants';
|
||||||
import { fullPageDataService } from '../../domain/factories';
|
import { fullPageDataService } from '../../domain/factories';
|
||||||
import { FullPageEntity } from '../../domain/entities';
|
import { FullPageEntity } from '../../domain/entities';
|
||||||
|
import { fullPageStore } from '../store';
|
||||||
|
|
||||||
import fullPageId from '../languages/id/full-page.json';
|
import fullPageId from '../languages/id/full-page.json';
|
||||||
import fullPageEn from '../languages/en/full-page.json';
|
import fullPageEn from '../languages/en/full-page.json';
|
||||||
@@ -18,7 +19,7 @@ const DetailPage = lazy(() => import('../pages/full-page.page.detail'));
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Called once at import time — safe, idempotent, outside React render cycle.
|
// Called once at import time — safe, idempotent, outside React render cycle.
|
||||||
// The namespace 'full-page' must match config.translationNamespace.
|
// The namespace 'full-page' must match config.translationNamespace.
|
||||||
registerModuleNamespace(FullPageModuleConfig.translationNamespace, {
|
registerModuleNamespace(fullPageModuleConfig.translationNamespace, {
|
||||||
id: fullPageId,
|
id: fullPageId,
|
||||||
en: fullPageEn,
|
en: fullPageEn,
|
||||||
});
|
});
|
||||||
@@ -28,14 +29,18 @@ registerModuleNamespace(FullPageModuleConfig.translationNamespace, {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
export default function FullPageModule() {
|
export default function FullPageModule() {
|
||||||
return (
|
return (
|
||||||
<EnterpriseModuleProvider<FullPageEntity> config={FullPageModuleConfig} dataServices={fullPageDataService}>
|
<EnterpriseModuleProvider<FullPageEntity>
|
||||||
|
config={fullPageModuleConfig}
|
||||||
|
dataServices={fullPageDataService}
|
||||||
|
store={fullPageStore}
|
||||||
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/index" element={<IndexPage />} />
|
<Route path="/index" element={<IndexPage />} />
|
||||||
<Route path="/detail/:dataId" element={<DetailPage />} />
|
<Route path="/detail/:dataId" element={<DetailPage />} />
|
||||||
<Route path="/edit/:dataId" element={<FormPage formPageType="edit" />} />
|
<Route path="/edit/:dataId" element={<FormPage formPageType="edit" />} />
|
||||||
<Route path="/duplicate/:dataId" element={<FormPage formPageType="duplicate" />} />
|
<Route path="/duplicate/:dataId" element={<FormPage formPageType="duplicate" />} />
|
||||||
<Route path="/create" element={<FormPage formPageType="create" />} />
|
<Route path="/create" element={<FormPage formPageType="create" />} />
|
||||||
<Route path="/" element={<Navigate to={`${FullPageModuleConfig.webUrl}/index`} replace={true} />} />
|
<Route path="/" element={<Navigate to={`${fullPageModuleConfig.webUrl}/index`} replace={true} />} />
|
||||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</EnterpriseModuleProvider>
|
</EnterpriseModuleProvider>
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations';
|
||||||
import { FullPageModuleConfig } from '../../domain/constants';
|
import { fullPageModuleConfig } from '../../domain/constants';
|
||||||
|
|
||||||
export default function FullPagePageDetail() {
|
export default function FullPagePageDetail() {
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
@@ -12,7 +12,7 @@ export default function FullPagePageDetail() {
|
|||||||
// showBadges: false,
|
// showBadges: false,
|
||||||
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: 'link', href: `${fullPageModuleConfig.webUrl}/index` },
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
></EnterpriseDetailPageProvider>
|
></EnterpriseDetailPageProvider>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { EnterpriseModuleState } from '@repo/ui/foundations';
|
||||||
|
import { FullPageEntity } from '../../domain/entities';
|
||||||
|
|
||||||
|
export interface FullPageStoreState extends EnterpriseModuleState<FullPageEntity> {}
|
||||||
|
|
||||||
|
export const fullPageStore = create<FullPageStoreState>((set) => ({
|
||||||
|
metaData: null,
|
||||||
|
setMetaData: (data) => set({ metaData: data }),
|
||||||
|
|
||||||
|
filterData: { page: 1, limit: 10 },
|
||||||
|
setFilterData: (data) => set({ filterData: data }),
|
||||||
|
|
||||||
|
selectedRows: [],
|
||||||
|
setSelectedRows: (rows) => set({ selectedRows: rows }),
|
||||||
|
|
||||||
|
privileges: [],
|
||||||
|
setPrivileges: (privileges) => set({ privileges }),
|
||||||
|
}));
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations';
|
|||||||
* Core configuration and constants for the Full Page module.
|
* Core configuration and constants for the Full Page module.
|
||||||
* Used across Domain, Data, and Presentation layers.
|
* Used across Domain, Data, and Presentation layers.
|
||||||
*/
|
*/
|
||||||
export const SinglePageModuleConfig: ModuleConfigEntity = {
|
export const singlePageModuleConfig: ModuleConfigEntity = {
|
||||||
/** Unique identifier for permissions, caching, and i18n */
|
/** Unique identifier for permissions, caching, and i18n */
|
||||||
moduleKey: 'EXAMPLE_SINGLE_PAGE',
|
moduleKey: 'EXAMPLE_SINGLE_PAGE',
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { lazy } from 'react';
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
|
import { EnterpriseModuleProvider } from '@repo/ui/foundations';
|
||||||
import { registerModuleNamespace } from '@repo/core-i18n';
|
import { registerModuleNamespace } from '@repo/core-i18n';
|
||||||
import { SinglePageModuleConfig } from '../../domain/constants';
|
import { singlePageModuleConfig } from '../../domain/constants';
|
||||||
import { singlePageDataService } from '../../domain/factories';
|
import { singlePageDataService } from '../../domain/factories';
|
||||||
import { SinglePageEntity } from '../../domain/entities';
|
import { SinglePageEntity } from '../../domain/entities';
|
||||||
|
import { singlePageStore } from '../store';
|
||||||
|
|
||||||
import singlePageId from '../languages/id/single-page.json';
|
import singlePageId from '../languages/id/single-page.json';
|
||||||
import singlePageEn from '../languages/en/single-page.json';
|
import singlePageEn from '../languages/en/single-page.json';
|
||||||
@@ -16,7 +17,7 @@ const IndexPage = lazy(() => import('../pages/single-page.page.index'));
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Called once at import time — safe, idempotent, outside React render cycle.
|
// Called once at import time — safe, idempotent, outside React render cycle.
|
||||||
// The namespace 'single-page' must match config.translationNamespace.
|
// The namespace 'single-page' must match config.translationNamespace.
|
||||||
registerModuleNamespace(SinglePageModuleConfig.translationNamespace, {
|
registerModuleNamespace(singlePageModuleConfig.translationNamespace, {
|
||||||
id: singlePageId,
|
id: singlePageId,
|
||||||
en: singlePageEn,
|
en: singlePageEn,
|
||||||
});
|
});
|
||||||
@@ -26,10 +27,14 @@ registerModuleNamespace(SinglePageModuleConfig.translationNamespace, {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
export default function SinglePageModule() {
|
export default function SinglePageModule() {
|
||||||
return (
|
return (
|
||||||
<EnterpriseModuleProvider<SinglePageEntity> config={SinglePageModuleConfig} dataServices={singlePageDataService}>
|
<EnterpriseModuleProvider<SinglePageEntity>
|
||||||
|
config={singlePageModuleConfig}
|
||||||
|
dataServices={singlePageDataService}
|
||||||
|
store={singlePageStore}
|
||||||
|
>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/index" element={<IndexPage />} />
|
<Route path="/index" element={<IndexPage />} />
|
||||||
<Route path="/" element={<Navigate to={`${SinglePageModuleConfig.webUrl}/index`} replace={true} />} />
|
<Route path="/" element={<Navigate to={`${singlePageModuleConfig.webUrl}/index`} replace={true} />} />
|
||||||
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
<Route path="*" element={<Navigate to={'/404'} replace={true} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</EnterpriseModuleProvider>
|
</EnterpriseModuleProvider>
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { EnterpriseModuleState } from '@repo/ui/foundations';
|
||||||
|
import { SinglePageEntity } from '../../domain/entities';
|
||||||
|
|
||||||
|
export interface SinglePageStoreState extends EnterpriseModuleState<SinglePageEntity> {}
|
||||||
|
|
||||||
|
export const singlePageStore = create<SinglePageStoreState>((set) => ({
|
||||||
|
metaData: null,
|
||||||
|
setMetaData: (data) => set({ metaData: data }),
|
||||||
|
|
||||||
|
filterData: { page: 1, limit: 10 },
|
||||||
|
setFilterData: (data) => set({ filterData: data }),
|
||||||
|
|
||||||
|
selectedRows: [],
|
||||||
|
setSelectedRows: (rows) => set({ selectedRows: rows }),
|
||||||
|
|
||||||
|
privileges: [],
|
||||||
|
setPrivileges: (privileges) => set({ privileges }),
|
||||||
|
}));
|
||||||
|
|||||||
@@ -49,7 +49,8 @@
|
|||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tailwind-variants": "^3.2.2",
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"zod": "^3.25.36"
|
"zod": "^3.25.36",
|
||||||
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/eslint-config": "workspace:*",
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
|||||||
@@ -120,11 +120,6 @@ export interface DataServiceSlice<
|
|||||||
S extends BaseRemoteDataServices<E> = BaseRemoteDataServices<E>,
|
S extends BaseRemoteDataServices<E> = BaseRemoteDataServices<E>,
|
||||||
> {
|
> {
|
||||||
dataServices: S;
|
dataServices: S;
|
||||||
/**
|
|
||||||
* User privileges payload for the current module.
|
|
||||||
* Typed as `unknown` to enforce strict type checking before consumption via RBAC utilities.
|
|
||||||
*/
|
|
||||||
privilege: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -138,10 +133,28 @@ export interface SelectionSlice<
|
|||||||
> {
|
> {
|
||||||
selectedRows: E[];
|
selectedRows: E[];
|
||||||
setSelectedRows: (rows: E[]) => void;
|
setSelectedRows: (rows: E[]) => void;
|
||||||
metaData: TMeta;
|
metaData: TMeta | null;
|
||||||
setMetaData: (data: TMeta) => void;
|
setMetaData: (data: TMeta | null) => void;
|
||||||
filterData: TFilter;
|
filterData: TFilter | null;
|
||||||
setFilterData: (data: TFilter) => void;
|
setFilterData: (data: TFilter | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base generic state for Enterprise Module store using Zustand.
|
||||||
|
*/
|
||||||
|
export interface EnterpriseModuleState<
|
||||||
|
E extends BaseEntity = BaseEntity,
|
||||||
|
TFilter = Record<string, unknown>,
|
||||||
|
TMeta = Record<string, unknown>
|
||||||
|
> {
|
||||||
|
metaData: TMeta | null;
|
||||||
|
setMetaData: (data: TMeta | null) => void;
|
||||||
|
filterData: TFilter | null;
|
||||||
|
setFilterData: (data: TFilter | null) => void;
|
||||||
|
selectedRows: E[];
|
||||||
|
setSelectedRows: (rows: E[]) => void;
|
||||||
|
privileges: string[];
|
||||||
|
setPrivileges: (privileges: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigationSlice {
|
export interface NavigationSlice {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useMemo, useState, ReactNode } from 'react';
|
import { useMemo, useState, ReactNode } from 'react';
|
||||||
|
import type { UseBoundStore, StoreApi } from 'zustand';
|
||||||
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';
|
||||||
|
import { ConfigSlice, ModuleConfigEntity, SinglePageFormState, SinglePageModalState, EnterpriseModuleState, PrivilegeEntity } from '../entities/entity';
|
||||||
import { ConfigSlice, ModuleConfigEntity, SinglePageFormState, SinglePageModalState } from '../entities/entity';
|
|
||||||
import {
|
import {
|
||||||
EnterpriseConfigContext,
|
EnterpriseConfigContext,
|
||||||
EnterpriseDataServiceContext,
|
EnterpriseDataServiceContext,
|
||||||
@@ -15,30 +15,42 @@ import {
|
|||||||
import { defaultPrivileges } from '../constant/default-privilege';
|
import { defaultPrivileges } from '../constant/default-privilege';
|
||||||
import { Forbidden } from '../../../components';
|
import { Forbidden } from '../../../components';
|
||||||
|
|
||||||
export interface EnterpriseModuleProviderProps<E extends BaseEntity> {
|
export interface EnterpriseModuleProviderProps<
|
||||||
|
E extends BaseEntity,
|
||||||
|
S extends EnterpriseModuleState<E> = EnterpriseModuleState<E>
|
||||||
|
> {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
config: ModuleConfigEntity<E>;
|
config: ModuleConfigEntity<E>;
|
||||||
dataServices: BaseRemoteDataServices<E>;
|
dataServices: BaseRemoteDataServices<E>;
|
||||||
|
store: UseBoundStore<StoreApi<S>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Detect macOS / iOS for displaying platform-specific shortcut labels. */
|
/** Detect macOS / iOS for displaying platform-specific shortcut labels. */
|
||||||
const IS_MACOS = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
|
const IS_MACOS = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent);
|
||||||
|
|
||||||
export function EnterpriseModuleProvider<E extends BaseEntity>(props: EnterpriseModuleProviderProps<E>) {
|
export function EnterpriseModuleProvider<
|
||||||
const { children, config, dataServices } = props;
|
E extends BaseEntity,
|
||||||
|
S extends EnterpriseModuleState<E> = EnterpriseModuleState<E>
|
||||||
|
>(props: EnterpriseModuleProviderProps<E, S>) {
|
||||||
|
const { children, config, dataServices, store } = props;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 1. Config Slice (Static)
|
// 1. Config Slice (Static)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const storePrivileges = store((state: S) => state.privileges);
|
||||||
|
|
||||||
const configSlice: ConfigSlice = useMemo(() => {
|
const configSlice: ConfigSlice = useMemo(() => {
|
||||||
|
// FIXME => Parsing privilege: mapping string array dari store ke format PrivilegeEntity (boolean)
|
||||||
|
const parsedPrivileges = defaultPrivileges
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
// FIXME => IMPLEMENT PRIVILEGE
|
privileges: parsedPrivileges,
|
||||||
privileges: defaultPrivileges,
|
|
||||||
IS_MACOS: IS_MACOS,
|
IS_MACOS: IS_MACOS,
|
||||||
};
|
};
|
||||||
}, [config]);
|
}, [config, storePrivileges]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 1b. Translation Slice (Dedicated context — decoupled from config)
|
// 1b. Translation Slice (Dedicated context — decoupled from config)
|
||||||
@@ -59,15 +71,18 @@ export function EnterpriseModuleProvider<E extends BaseEntity>(props: Enterprise
|
|||||||
// 2. Data Service Slice (Stable refs)
|
// 2. Data Service Slice (Stable refs)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const dataServiceSlice = useMemo(() => {
|
const dataServiceSlice = useMemo(() => {
|
||||||
return { dataServices, privilege: undefined };
|
return { dataServices };
|
||||||
}, [dataServices]);
|
}, [dataServices]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 3. Selection Slice (Dynamic state)
|
// 3. Selection Slice (Dynamic state)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const [selectedRows, setSelectedRows] = useState<E[]>([]);
|
const selectedRows = store((state: S) => state.selectedRows);
|
||||||
const [metaData, setMetaData] = useState<any>(null);
|
const setSelectedRows = store((state: S) => state.setSelectedRows);
|
||||||
const [filterData, setFilterData] = useState<any>(null);
|
const metaData = store((state: S) => state.metaData);
|
||||||
|
const setMetaData = store((state: S) => state.setMetaData);
|
||||||
|
const filterData = store((state: S) => state.filterData);
|
||||||
|
const setFilterData = store((state: S) => state.setFilterData);
|
||||||
|
|
||||||
const selectionSlice = useMemo(
|
const selectionSlice = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -78,7 +93,7 @@ export function EnterpriseModuleProvider<E extends BaseEntity>(props: Enterprise
|
|||||||
filterData,
|
filterData,
|
||||||
setFilterData,
|
setFilterData,
|
||||||
}),
|
}),
|
||||||
[selectedRows, metaData, filterData],
|
[selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Generated
+3
@@ -518,6 +518,9 @@ importers:
|
|||||||
zod:
|
zod:
|
||||||
specifier: ^3.25.36
|
specifier: ^3.25.36
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.14
|
||||||
|
version: 5.0.14(@types/react@19.2.7)(react@19.2.3)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@repo/eslint-config':
|
'@repo/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
|
|||||||
Reference in New Issue
Block a user