diff --git a/apps/web/src/apps/modules/layouts/module.layout.tsx b/apps/web/src/apps/modules/layouts/module.layout.tsx index 189715e..a54d867 100644 --- a/apps/web/src/apps/modules/layouts/module.layout.tsx +++ b/apps/web/src/apps/modules/layouts/module.layout.tsx @@ -1,4 +1,5 @@ import { CoreAppShell, CoreAppShellConfig } from '@repo/ui/components'; +import { EnterpriseStorageProvider } from '@repo/ui/foundations'; import { registerModuleNamespace } from '@repo/core-i18n'; import HeaderLayout from './components/header.layout'; import { SidebarMenu } from './components/sidebar'; @@ -6,6 +7,7 @@ import { HistoryDrawer } from './components/history'; import { BookmarkDrawer } from './components/bookmark'; import { useHistoryTracker } from './hooks/useHistoryTracker'; import { MENU_ITEMS } from './data/menu.data'; +import { enterpriseStorageAdapter } from '../../../core/lib/enterprise-storage-adapter'; import navEn from './languages/en/nav.json'; import navId from './languages/id/nav.json'; @@ -40,17 +42,19 @@ export default function ModuleLayout({ children }: { children: React.ReactNode } }; return ( - , - sidebar: , - sidebarMobile: , - }} - > - {children} - - - + + , + sidebar: , + sidebarMobile: , + }} + > + {children} + + + + ); } diff --git a/apps/web/src/core/lib/enterprise-storage-adapter.ts b/apps/web/src/core/lib/enterprise-storage-adapter.ts new file mode 100644 index 0000000..ea504aa --- /dev/null +++ b/apps/web/src/core/lib/enterprise-storage-adapter.ts @@ -0,0 +1,25 @@ +import type { EnterpriseStorageAdapter } from '@repo/ui/foundations'; +import type { PrivilegeEntity } from '@repo/ui/foundations'; +import { appDatabase, AppDatabaseKey } from '../storage/local'; + +/** + * Concrete storage adapter that wires the Enterprise Module framework + * to this app's IndexedDB instance. + * + * Passed to ``. + */ +export const enterpriseStorageAdapter: EnterpriseStorageAdapter = { + async getPrivileges(moduleKey: string): Promise { + const allPrivileges: any = await appDatabase.getItem(AppDatabaseKey.USER_PRIVILEGE); + if (!allPrivileges) return null; + return allPrivileges[moduleKey] ?? null; + }, + + async getDrafts(): Promise | null> { + return appDatabase.getItem>(AppDatabaseKey.OFFLINE_DRAFT); + }, + + async setDrafts(drafts: Record): Promise { + await appDatabase.setItem(AppDatabaseKey.OFFLINE_DRAFT, drafts); + }, +}; diff --git a/packages/ui/src/foundations/enterprise-module/hooks/enterprise-storage.context.tsx b/packages/ui/src/foundations/enterprise-module/hooks/enterprise-storage.context.tsx new file mode 100644 index 0000000..eb3805c --- /dev/null +++ b/packages/ui/src/foundations/enterprise-module/hooks/enterprise-storage.context.tsx @@ -0,0 +1,82 @@ +import { createContext, useContext, ReactNode } from 'react'; +import type { PrivilegeEntity } from '../entities/entity'; + +// --------------------------------------------------------------------------- +// Adapter Interface +// --------------------------------------------------------------------------- + +/** + * Abstract storage adapter for the Enterprise Module framework. + * + * This interface decouples the enterprise module (a shared package) from any + * specific app-level storage implementation. Each consuming app provides its + * own concrete adapter that wires into its IndexedDB / localStorage setup. + * + * @example + * ```ts + * const adapter: EnterpriseStorageAdapter = { + * getPrivileges: async (moduleKey) => { + * const all = await appDatabase.getItem('user_privilege'); + * return all?.[moduleKey] ?? null; + * }, + * getDrafts: () => appDatabase.getItem('offline_draft'), + * setDrafts: (d) => appDatabase.setItem('offline_draft', d), + * }; + * ``` + */ +export interface EnterpriseStorageAdapter { + /** + * Resolve the privilege configuration for a given module key. + * Returns `null` if no privilege data is available. + */ + getPrivileges(moduleKey: string): Promise; + + /** + * Retrieve all offline draft entries. + * The returned object is keyed by draft identifiers (e.g. `draft:MODULE_KEY:CREATE`). + */ + getDrafts(): Promise | null>; + + /** + * Persist the full drafts object back to storage. + */ + setDrafts(drafts: Record): Promise; +} + +// --------------------------------------------------------------------------- +// Context +// --------------------------------------------------------------------------- + +const EnterpriseStorageContext = createContext(null); + +/** + * Hook to consume the enterprise storage adapter. + * Must be used within an `EnterpriseStorageProvider`. + */ +export function useEnterpriseStorage(): EnterpriseStorageAdapter { + const context = useContext(EnterpriseStorageContext); + if (!context) { + throw new Error( + 'useEnterpriseStorage must be used within an EnterpriseStorageProvider. ' + + 'Wrap your app layout with .', + ); + } + return context; +} + +// --------------------------------------------------------------------------- +// Provider +// --------------------------------------------------------------------------- + +export interface EnterpriseStorageProviderProps { + adapter: EnterpriseStorageAdapter; + children: ReactNode; +} + +/** + * Provides the storage adapter to all enterprise module providers and hooks. + * Place this at the app's root layout level, above any `EnterpriseModuleProvider`. + */ +export function EnterpriseStorageProvider({ adapter, children }: EnterpriseStorageProviderProps) { + return {children}; +} diff --git a/packages/ui/src/foundations/enterprise-module/hooks/use-form-draft.context.ts b/packages/ui/src/foundations/enterprise-module/hooks/use-form-draft.context.ts index 217a573..d1b9f87 100644 --- a/packages/ui/src/foundations/enterprise-module/hooks/use-form-draft.context.ts +++ b/packages/ui/src/foundations/enterprise-module/hooks/use-form-draft.context.ts @@ -1,11 +1,8 @@ import { useState, useEffect, useCallback } from 'react'; import { useEnterpriseModuleConfigContext } from './use-module.context'; +import { useEnterpriseStorage } from './enterprise-storage.context'; import { DraftConfig, FormPageType } from '../entities/entity'; -import { - appDatabase, - AppDatabaseKey, -} from '../../../../../../apps/web/src/core/storage/local'; interface FormDraftContextProps { formType: FormPageType; @@ -15,6 +12,7 @@ interface FormDraftContextProps { export function useFormDraftContext({ config }: FormDraftContextProps) { const { config: moduleConfig } = useEnterpriseModuleConfigContext(); + const storage = useEnterpriseStorage(); const [hasDraft, setHasDraft] = useState(false); const [draftData, setDraftData] = useState(null); @@ -26,9 +24,9 @@ export function useFormDraftContext({ config }: FormDraftContextProps) { // Check for existing draft on mount useEffect(() => { if (!isEnabled) return; - appDatabase - .getItem(AppDatabaseKey.OFFLINE_DRAFT) - .then((allDrafts: any) => { + storage + .getDrafts() + .then((allDrafts) => { const drafts = allDrafts || {}; if (drafts[draftKey]) { setDraftData(drafts[draftKey]); @@ -36,52 +34,52 @@ export function useFormDraftContext({ config }: FormDraftContextProps) { } }) .catch((err) => { - console.error('[Draft Recovery] Failed to read from appDatabase:', err); + console.error('[Draft Recovery] Failed to read drafts:', err); }); - }, [isEnabled, draftKey]); + }, [isEnabled, draftKey, storage]); const saveDraft = useCallback( (data: any) => { if (!isEnabled || !data) return; - appDatabase - .getItem(AppDatabaseKey.OFFLINE_DRAFT) - .then((allDrafts: any) => { + storage + .getDrafts() + .then((allDrafts) => { const drafts = allDrafts || {}; drafts[draftKey] = { ...data, _draftSavedAt: new Date().toISOString(), }; - appDatabase.setItem(AppDatabaseKey.OFFLINE_DRAFT, drafts).catch((err) => { - console.error('[Draft Saving] Failed to save to appDatabase:', err); + storage.setDrafts(drafts).catch((err) => { + console.error('[Draft Saving] Failed to save drafts:', err); }); }) .catch((err) => { - console.error('[Draft Saving] Failed to read from appDatabase:', err); + console.error('[Draft Saving] Failed to read drafts:', err); }); }, - [draftKey, isEnabled], + [draftKey, isEnabled, storage], ); const clearDraft = useCallback(() => { if (!isEnabled) return; - appDatabase - .getItem(AppDatabaseKey.OFFLINE_DRAFT) - .then((allDrafts: any) => { + storage + .getDrafts() + .then((allDrafts) => { if (!allDrafts) return; delete allDrafts[draftKey]; - appDatabase.setItem(AppDatabaseKey.OFFLINE_DRAFT, allDrafts).catch((err) => { - console.error('[Draft Clearing] Failed to save to appDatabase:', err); + storage.setDrafts(allDrafts).catch((err) => { + console.error('[Draft Clearing] Failed to save drafts:', err); }); }) .catch((err) => { - console.error('[Draft Clearing] Failed to read from appDatabase:', err); + console.error('[Draft Clearing] Failed to read drafts:', err); }); setHasDraft(false); setDraftData(null); - }, [draftKey, isEnabled]); + }, [draftKey, isEnabled, storage]); return { isEnabled, diff --git a/packages/ui/src/foundations/enterprise-module/index.ts b/packages/ui/src/foundations/enterprise-module/index.ts index 3af3efb..06bd84c 100644 --- a/packages/ui/src/foundations/enterprise-module/index.ts +++ b/packages/ui/src/foundations/enterprise-module/index.ts @@ -2,6 +2,8 @@ export * from './constant/'; export * from './entities/entity'; +export * from './hooks/enterprise-storage.context'; + export * from './hooks/use-detail-page.context'; export * from './hooks/use-form-draft.context'; export * from './hooks/use-form-page.context';