From c9c5b2c86e80d93ba2523b86a5c734b62087930c Mon Sep 17 00:00:00 2001
From: Firman Ramdhani <33869609+firmanramdhani@users.noreply.github.com>
Date: Fri, 31 Jul 2026 14:20:01 +0700
Subject: [PATCH] refactor: implement enterprise storage provider
---
.../apps/modules/layouts/module.layout.tsx | 28 ++++---
.../core/lib/enterprise-storage-adapter.ts | 25 ++++++
.../hooks/enterprise-storage.context.tsx | 82 +++++++++++++++++++
.../hooks/use-form-draft.context.ts | 44 +++++-----
.../foundations/enterprise-module/index.ts | 2 +
5 files changed, 146 insertions(+), 35 deletions(-)
create mode 100644 apps/web/src/core/lib/enterprise-storage-adapter.ts
create mode 100644 packages/ui/src/foundations/enterprise-module/hooks/enterprise-storage.context.tsx
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';