refactor: implement enterprise storage provider
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { CoreAppShell, CoreAppShellConfig } from '@repo/ui/components';
|
import { CoreAppShell, CoreAppShellConfig } from '@repo/ui/components';
|
||||||
|
import { EnterpriseStorageProvider } from '@repo/ui/foundations';
|
||||||
import { registerModuleNamespace } from '@repo/core-i18n';
|
import { registerModuleNamespace } from '@repo/core-i18n';
|
||||||
import HeaderLayout from './components/header.layout';
|
import HeaderLayout from './components/header.layout';
|
||||||
import { SidebarMenu } from './components/sidebar';
|
import { SidebarMenu } from './components/sidebar';
|
||||||
@@ -6,6 +7,7 @@ import { HistoryDrawer } from './components/history';
|
|||||||
import { BookmarkDrawer } from './components/bookmark';
|
import { BookmarkDrawer } from './components/bookmark';
|
||||||
import { useHistoryTracker } from './hooks/useHistoryTracker';
|
import { useHistoryTracker } from './hooks/useHistoryTracker';
|
||||||
import { MENU_ITEMS } from './data/menu.data';
|
import { MENU_ITEMS } from './data/menu.data';
|
||||||
|
import { enterpriseStorageAdapter } from '../../../core/lib/enterprise-storage-adapter';
|
||||||
|
|
||||||
import navEn from './languages/en/nav.json';
|
import navEn from './languages/en/nav.json';
|
||||||
import navId from './languages/id/nav.json';
|
import navId from './languages/id/nav.json';
|
||||||
@@ -40,17 +42,19 @@ export default function ModuleLayout({ children }: { children: React.ReactNode }
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CoreAppShell
|
<EnterpriseStorageProvider adapter={enterpriseStorageAdapter}>
|
||||||
config={configAppShell}
|
<CoreAppShell
|
||||||
slots={{
|
config={configAppShell}
|
||||||
header: <HeaderLayout />,
|
slots={{
|
||||||
sidebar: <SidebarMenu items={MENU_ITEMS} withToggle />,
|
header: <HeaderLayout />,
|
||||||
sidebarMobile: <SidebarMenu items={MENU_ITEMS} variantOverride="expanded" />,
|
sidebar: <SidebarMenu items={MENU_ITEMS} withToggle />,
|
||||||
}}
|
sidebarMobile: <SidebarMenu items={MENU_ITEMS} variantOverride="expanded" />,
|
||||||
>
|
}}
|
||||||
{children}
|
>
|
||||||
<HistoryDrawer />
|
{children}
|
||||||
<BookmarkDrawer />
|
<HistoryDrawer />
|
||||||
</CoreAppShell>
|
<BookmarkDrawer />
|
||||||
|
</CoreAppShell>
|
||||||
|
</EnterpriseStorageProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 `<EnterpriseStorageProvider adapter={enterpriseStorageAdapter}>`.
|
||||||
|
*/
|
||||||
|
export const enterpriseStorageAdapter: EnterpriseStorageAdapter = {
|
||||||
|
async getPrivileges(moduleKey: string): Promise<PrivilegeEntity | null> {
|
||||||
|
const allPrivileges: any = await appDatabase.getItem(AppDatabaseKey.USER_PRIVILEGE);
|
||||||
|
if (!allPrivileges) return null;
|
||||||
|
return allPrivileges[moduleKey] ?? null;
|
||||||
|
},
|
||||||
|
|
||||||
|
async getDrafts(): Promise<Record<string, any> | null> {
|
||||||
|
return appDatabase.getItem<Record<string, any>>(AppDatabaseKey.OFFLINE_DRAFT);
|
||||||
|
},
|
||||||
|
|
||||||
|
async setDrafts(drafts: Record<string, any>): Promise<void> {
|
||||||
|
await appDatabase.setItem(AppDatabaseKey.OFFLINE_DRAFT, drafts);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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<PrivilegeEntity | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve all offline draft entries.
|
||||||
|
* The returned object is keyed by draft identifiers (e.g. `draft:MODULE_KEY:CREATE`).
|
||||||
|
*/
|
||||||
|
getDrafts(): Promise<Record<string, any> | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persist the full drafts object back to storage.
|
||||||
|
*/
|
||||||
|
setDrafts(drafts: Record<string, any>): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Context
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const EnterpriseStorageContext = createContext<EnterpriseStorageAdapter | null>(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 <EnterpriseStorageProvider adapter={...}>.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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 <EnterpriseStorageContext.Provider value={adapter}>{children}</EnterpriseStorageContext.Provider>;
|
||||||
|
}
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
import { useEnterpriseModuleConfigContext } from './use-module.context';
|
import { useEnterpriseModuleConfigContext } from './use-module.context';
|
||||||
|
import { useEnterpriseStorage } from './enterprise-storage.context';
|
||||||
import { DraftConfig, FormPageType } from '../entities/entity';
|
import { DraftConfig, FormPageType } from '../entities/entity';
|
||||||
import {
|
|
||||||
appDatabase,
|
|
||||||
AppDatabaseKey,
|
|
||||||
} from '../../../../../../apps/web/src/core/storage/local';
|
|
||||||
|
|
||||||
interface FormDraftContextProps {
|
interface FormDraftContextProps {
|
||||||
formType: FormPageType;
|
formType: FormPageType;
|
||||||
@@ -15,6 +12,7 @@ interface FormDraftContextProps {
|
|||||||
|
|
||||||
export function useFormDraftContext({ config }: FormDraftContextProps) {
|
export function useFormDraftContext({ config }: FormDraftContextProps) {
|
||||||
const { config: moduleConfig } = useEnterpriseModuleConfigContext();
|
const { config: moduleConfig } = useEnterpriseModuleConfigContext();
|
||||||
|
const storage = useEnterpriseStorage();
|
||||||
const [hasDraft, setHasDraft] = useState(false);
|
const [hasDraft, setHasDraft] = useState(false);
|
||||||
const [draftData, setDraftData] = useState<any>(null);
|
const [draftData, setDraftData] = useState<any>(null);
|
||||||
|
|
||||||
@@ -26,9 +24,9 @@ export function useFormDraftContext({ config }: FormDraftContextProps) {
|
|||||||
// Check for existing draft on mount
|
// Check for existing draft on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled) return;
|
||||||
appDatabase
|
storage
|
||||||
.getItem(AppDatabaseKey.OFFLINE_DRAFT)
|
.getDrafts()
|
||||||
.then((allDrafts: any) => {
|
.then((allDrafts) => {
|
||||||
const drafts = allDrafts || {};
|
const drafts = allDrafts || {};
|
||||||
if (drafts[draftKey]) {
|
if (drafts[draftKey]) {
|
||||||
setDraftData(drafts[draftKey]);
|
setDraftData(drafts[draftKey]);
|
||||||
@@ -36,52 +34,52 @@ export function useFormDraftContext({ config }: FormDraftContextProps) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.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(
|
const saveDraft = useCallback(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
if (!isEnabled || !data) return;
|
if (!isEnabled || !data) return;
|
||||||
appDatabase
|
storage
|
||||||
.getItem(AppDatabaseKey.OFFLINE_DRAFT)
|
.getDrafts()
|
||||||
.then((allDrafts: any) => {
|
.then((allDrafts) => {
|
||||||
const drafts = allDrafts || {};
|
const drafts = allDrafts || {};
|
||||||
drafts[draftKey] = {
|
drafts[draftKey] = {
|
||||||
...data,
|
...data,
|
||||||
_draftSavedAt: new Date().toISOString(),
|
_draftSavedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
appDatabase.setItem(AppDatabaseKey.OFFLINE_DRAFT, drafts).catch((err) => {
|
storage.setDrafts(drafts).catch((err) => {
|
||||||
console.error('[Draft Saving] Failed to save to appDatabase:', err);
|
console.error('[Draft Saving] Failed to save drafts:', err);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((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(() => {
|
const clearDraft = useCallback(() => {
|
||||||
if (!isEnabled) return;
|
if (!isEnabled) return;
|
||||||
|
|
||||||
appDatabase
|
storage
|
||||||
.getItem(AppDatabaseKey.OFFLINE_DRAFT)
|
.getDrafts()
|
||||||
.then((allDrafts: any) => {
|
.then((allDrafts) => {
|
||||||
if (!allDrafts) return;
|
if (!allDrafts) return;
|
||||||
delete allDrafts[draftKey];
|
delete allDrafts[draftKey];
|
||||||
appDatabase.setItem(AppDatabaseKey.OFFLINE_DRAFT, allDrafts).catch((err) => {
|
storage.setDrafts(allDrafts).catch((err) => {
|
||||||
console.error('[Draft Clearing] Failed to save to appDatabase:', err);
|
console.error('[Draft Clearing] Failed to save drafts:', err);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('[Draft Clearing] Failed to read from appDatabase:', err);
|
console.error('[Draft Clearing] Failed to read drafts:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
setHasDraft(false);
|
setHasDraft(false);
|
||||||
setDraftData(null);
|
setDraftData(null);
|
||||||
}, [draftKey, isEnabled]);
|
}, [draftKey, isEnabled, storage]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isEnabled,
|
isEnabled,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export * from './constant/';
|
|||||||
|
|
||||||
export * from './entities/entity';
|
export * from './entities/entity';
|
||||||
|
|
||||||
|
export * from './hooks/enterprise-storage.context';
|
||||||
|
|
||||||
export * from './hooks/use-detail-page.context';
|
export * from './hooks/use-detail-page.context';
|
||||||
export * from './hooks/use-form-draft.context';
|
export * from './hooks/use-form-draft.context';
|
||||||
export * from './hooks/use-form-page.context';
|
export * from './hooks/use-form-page.context';
|
||||||
|
|||||||
Reference in New Issue
Block a user