From dd18787ee767a5bdae6b8b048bea464f92eae043 Mon Sep 17 00:00:00 2001 From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:09:46 +0700 Subject: [PATCH] refactor: integrate Zustand store into EnterpriseModuleProvider and update module configurations to use store-based state management --- .../domain/constants/full-page.constants.ts | 5 +-- .../full-page/presentation/factory/index.tsx | 13 ++++-- .../pages/full-page.page.detail.tsx | 4 +- .../full-page/presentation/store/index.ts | 19 +++++++++ .../domain/constants/single-page.constants.ts | 2 +- .../presentation/factory/index.tsx | 13 ++++-- .../single-page/presentation/store/index.ts | 19 +++++++++ packages/ui/package.json | 3 +- .../enterprise-module/entities/entity.ts | 31 ++++++++++---- .../providers/module.provider.tsx | 41 +++++++++++++------ pnpm-lock.yaml | 3 ++ 11 files changed, 116 insertions(+), 37 deletions(-) diff --git a/apps/web/src/apps/modules/example/full-page/domain/constants/full-page.constants.ts b/apps/web/src/apps/modules/example/full-page/domain/constants/full-page.constants.ts index 9c03319..d892a85 100644 --- a/apps/web/src/apps/modules/example/full-page/domain/constants/full-page.constants.ts +++ b/apps/web/src/apps/modules/example/full-page/domain/constants/full-page.constants.ts @@ -4,7 +4,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; * Core configuration and constants for the Full Page module. * Used across Domain, Data, and Presentation layers. */ -export const FullPageModuleConfig: ModuleConfigEntity = { +export const fullPageModuleConfig: ModuleConfigEntity = { /** Unique identifier for permissions, caching, and i18n */ moduleKey: 'EXAMPLE_FULL_PAGE', @@ -21,6 +21,5 @@ export const FullPageModuleConfig: ModuleConfigEntity = { moduleCategory: 'FULL_PAGE', /** */ - // moduleType: 'MASTER_DATA', - moduleType: 'TRANSACTION', + moduleType: 'MASTER_DATA', } as const; diff --git a/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx b/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx index 78878c2..f0bcf71 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx +++ b/apps/web/src/apps/modules/example/full-page/presentation/factory/index.tsx @@ -2,9 +2,10 @@ import { lazy } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; import { EnterpriseModuleProvider } from '@repo/ui/foundations'; import { registerModuleNamespace } from '@repo/core-i18n'; -import { FullPageModuleConfig } from '../../domain/constants'; +import { fullPageModuleConfig } from '../../domain/constants'; import { fullPageDataService } from '../../domain/factories'; import { FullPageEntity } from '../../domain/entities'; +import { fullPageStore } from '../store'; import fullPageId from '../languages/id/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. // The namespace 'full-page' must match config.translationNamespace. -registerModuleNamespace(FullPageModuleConfig.translationNamespace, { +registerModuleNamespace(fullPageModuleConfig.translationNamespace, { id: fullPageId, en: fullPageEn, }); @@ -28,14 +29,18 @@ registerModuleNamespace(FullPageModuleConfig.translationNamespace, { // --------------------------------------------------------------------------- export default function FullPageModule() { return ( - config={FullPageModuleConfig} dataServices={fullPageDataService}> + + config={fullPageModuleConfig} + dataServices={fullPageDataService} + store={fullPageStore} + > } /> } /> } /> } /> } /> - } /> + } /> } /> diff --git a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx index 4114ca6..9917e82 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx +++ b/apps/web/src/apps/modules/example/full-page/presentation/pages/full-page.page.detail.tsx @@ -1,5 +1,5 @@ import { EnterpriseDetailPageProvider, useEnterpriseModuleTranslationContext } from '@repo/ui/foundations'; -import { FullPageModuleConfig } from '../../domain/constants'; +import { fullPageModuleConfig } from '../../domain/constants'; export default function FullPagePageDetail() { const { t } = useEnterpriseModuleTranslationContext(); @@ -12,7 +12,7 @@ export default function FullPagePageDetail() { // showBadges: false, breadcrumbs: [ { 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` }, ], }} > diff --git a/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts b/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts index e69de29..55527ad 100644 --- a/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts +++ b/apps/web/src/apps/modules/example/full-page/presentation/store/index.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; +import { EnterpriseModuleState } from '@repo/ui/foundations'; +import { FullPageEntity } from '../../domain/entities'; + +export interface FullPageStoreState extends EnterpriseModuleState {} + +export const fullPageStore = create((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 }), +})); diff --git a/apps/web/src/apps/modules/example/single-page/domain/constants/single-page.constants.ts b/apps/web/src/apps/modules/example/single-page/domain/constants/single-page.constants.ts index 663f7fc..7b82f75 100644 --- a/apps/web/src/apps/modules/example/single-page/domain/constants/single-page.constants.ts +++ b/apps/web/src/apps/modules/example/single-page/domain/constants/single-page.constants.ts @@ -4,7 +4,7 @@ import { ModuleConfigEntity } from '@repo/ui/foundations'; * Core configuration and constants for the Full Page module. * Used across Domain, Data, and Presentation layers. */ -export const SinglePageModuleConfig: ModuleConfigEntity = { +export const singlePageModuleConfig: ModuleConfigEntity = { /** Unique identifier for permissions, caching, and i18n */ moduleKey: 'EXAMPLE_SINGLE_PAGE', diff --git a/apps/web/src/apps/modules/example/single-page/presentation/factory/index.tsx b/apps/web/src/apps/modules/example/single-page/presentation/factory/index.tsx index b26258b..f2e1c9a 100644 --- a/apps/web/src/apps/modules/example/single-page/presentation/factory/index.tsx +++ b/apps/web/src/apps/modules/example/single-page/presentation/factory/index.tsx @@ -2,9 +2,10 @@ import { lazy } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; import { EnterpriseModuleProvider } from '@repo/ui/foundations'; import { registerModuleNamespace } from '@repo/core-i18n'; -import { SinglePageModuleConfig } from '../../domain/constants'; +import { singlePageModuleConfig } from '../../domain/constants'; import { singlePageDataService } from '../../domain/factories'; import { SinglePageEntity } from '../../domain/entities'; +import { singlePageStore } from '../store'; import singlePageId from '../languages/id/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. // The namespace 'single-page' must match config.translationNamespace. -registerModuleNamespace(SinglePageModuleConfig.translationNamespace, { +registerModuleNamespace(singlePageModuleConfig.translationNamespace, { id: singlePageId, en: singlePageEn, }); @@ -26,10 +27,14 @@ registerModuleNamespace(SinglePageModuleConfig.translationNamespace, { // --------------------------------------------------------------------------- export default function SinglePageModule() { return ( - config={SinglePageModuleConfig} dataServices={singlePageDataService}> + + config={singlePageModuleConfig} + dataServices={singlePageDataService} + store={singlePageStore} + > } /> - } /> + } /> } /> diff --git a/apps/web/src/apps/modules/example/single-page/presentation/store/index.ts b/apps/web/src/apps/modules/example/single-page/presentation/store/index.ts index e69de29..e5c4e8f 100644 --- a/apps/web/src/apps/modules/example/single-page/presentation/store/index.ts +++ b/apps/web/src/apps/modules/example/single-page/presentation/store/index.ts @@ -0,0 +1,19 @@ +import { create } from 'zustand'; +import { EnterpriseModuleState } from '@repo/ui/foundations'; +import { SinglePageEntity } from '../../domain/entities'; + +export interface SinglePageStoreState extends EnterpriseModuleState {} + +export const singlePageStore = create((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 }), +})); diff --git a/packages/ui/package.json b/packages/ui/package.json index b714c0c..99f8acd 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -49,7 +49,8 @@ "tailwind-merge": "^3.4.0", "tailwind-variants": "^3.2.2", "tailwindcss": "^4.1.18", - "zod": "^3.25.36" + "zod": "^3.25.36", + "zustand": "^5.0.14" }, "devDependencies": { "@repo/eslint-config": "workspace:*", diff --git a/packages/ui/src/foundations/enterprise-module/entities/entity.ts b/packages/ui/src/foundations/enterprise-module/entities/entity.ts index 5bc1371..68a52a5 100644 --- a/packages/ui/src/foundations/enterprise-module/entities/entity.ts +++ b/packages/ui/src/foundations/enterprise-module/entities/entity.ts @@ -120,11 +120,6 @@ export interface DataServiceSlice< S extends BaseRemoteDataServices = BaseRemoteDataServices, > { 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[]; setSelectedRows: (rows: E[]) => void; - metaData: TMeta; - setMetaData: (data: TMeta) => void; - filterData: TFilter; - setFilterData: (data: TFilter) => void; + metaData: TMeta | null; + setMetaData: (data: TMeta | null) => void; + filterData: TFilter | null; + setFilterData: (data: TFilter | null) => void; +} + +/** + * Base generic state for Enterprise Module store using Zustand. + */ +export interface EnterpriseModuleState< + E extends BaseEntity = BaseEntity, + TFilter = Record, + TMeta = Record +> { + 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 { diff --git a/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx b/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx index 80fb658..b54bc9c 100644 --- a/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx +++ b/packages/ui/src/foundations/enterprise-module/providers/module.provider.tsx @@ -1,9 +1,9 @@ import { useMemo, useState, ReactNode } from 'react'; +import type { UseBoundStore, StoreApi } from 'zustand'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from '@repo/core-i18n'; import { BaseEntity, BaseRemoteDataServices } from '@repo/core-api/data-services'; - -import { ConfigSlice, ModuleConfigEntity, SinglePageFormState, SinglePageModalState } from '../entities/entity'; +import { ConfigSlice, ModuleConfigEntity, SinglePageFormState, SinglePageModalState, EnterpriseModuleState, PrivilegeEntity } from '../entities/entity'; import { EnterpriseConfigContext, EnterpriseDataServiceContext, @@ -15,30 +15,42 @@ import { import { defaultPrivileges } from '../constant/default-privilege'; import { Forbidden } from '../../../components'; -export interface EnterpriseModuleProviderProps { +export interface EnterpriseModuleProviderProps< + E extends BaseEntity, + S extends EnterpriseModuleState = EnterpriseModuleState +> { children: ReactNode; config: ModuleConfigEntity; dataServices: BaseRemoteDataServices; + store: UseBoundStore>; } /** Detect macOS / iOS for displaying platform-specific shortcut labels. */ const IS_MACOS = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.userAgent); -export function EnterpriseModuleProvider(props: EnterpriseModuleProviderProps) { - const { children, config, dataServices } = props; +export function EnterpriseModuleProvider< + E extends BaseEntity, + S extends EnterpriseModuleState = EnterpriseModuleState +>(props: EnterpriseModuleProviderProps) { + const { children, config, dataServices, store } = props; const navigate = useNavigate(); // --------------------------------------------------------------------------- // 1. Config Slice (Static) // --------------------------------------------------------------------------- + + const storePrivileges = store((state: S) => state.privileges); + const configSlice: ConfigSlice = useMemo(() => { + // FIXME => Parsing privilege: mapping string array dari store ke format PrivilegeEntity (boolean) + const parsedPrivileges = defaultPrivileges + return { config, - // FIXME => IMPLEMENT PRIVILEGE - privileges: defaultPrivileges, + privileges: parsedPrivileges, IS_MACOS: IS_MACOS, }; - }, [config]); + }, [config, storePrivileges]); // --------------------------------------------------------------------------- // 1b. Translation Slice (Dedicated context — decoupled from config) @@ -59,15 +71,18 @@ export function EnterpriseModuleProvider(props: Enterprise // 2. Data Service Slice (Stable refs) // --------------------------------------------------------------------------- const dataServiceSlice = useMemo(() => { - return { dataServices, privilege: undefined }; + return { dataServices }; }, [dataServices]); // --------------------------------------------------------------------------- // 3. Selection Slice (Dynamic state) // --------------------------------------------------------------------------- - const [selectedRows, setSelectedRows] = useState([]); - const [metaData, setMetaData] = useState(null); - const [filterData, setFilterData] = useState(null); + const selectedRows = store((state: S) => state.selectedRows); + const setSelectedRows = store((state: S) => state.setSelectedRows); + 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( () => ({ @@ -78,7 +93,7 @@ export function EnterpriseModuleProvider(props: Enterprise filterData, setFilterData, }), - [selectedRows, metaData, filterData], + [selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData], ); // --------------------------------------------------------------------------- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6ff31f..00ed30e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -518,6 +518,9 @@ importers: zod: specifier: ^3.25.36 version: 3.25.76 + zustand: + specifier: ^5.0.14 + version: 5.0.14(@types/react@19.2.7)(react@19.2.3) devDependencies: '@repo/eslint-config': specifier: workspace:*