Merge pull request 'core/page-provider' (#38) from core/page-provider into main
Reviewed-on: eigen/fe-monorepo-template#38
This commit is contained in:
@@ -16,4 +16,7 @@ export const fullPageStore = create<FullPageStoreState>((set) => ({
|
|||||||
|
|
||||||
privileges: [],
|
privileges: [],
|
||||||
setPrivileges: (privileges) => set({ privileges }),
|
setPrivileges: (privileges) => set({ privileges }),
|
||||||
|
|
||||||
|
tableConfig: null,
|
||||||
|
setTableConfig: (config) => set({ tableConfig: config }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { appStorage, AppStorageKey } from '../../../../core/storage/local';
|
||||||
|
|
||||||
|
interface SidebarState {
|
||||||
|
openedKeys: Set<string>;
|
||||||
|
isInitialized: boolean;
|
||||||
|
isAllExpanded: boolean;
|
||||||
|
searchQuery: string;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
initializeStorage: (initialKeysFromActive: string[], allParentKeys: string[]) => Promise<void>;
|
||||||
|
toggleMenu: (key: string, isOpened: boolean, allParentKeys: string[]) => Promise<void>;
|
||||||
|
expandAll: (allParentKeys: string[]) => Promise<void>;
|
||||||
|
collapseAll: () => Promise<void>;
|
||||||
|
syncActiveKeys: (keys: string[], allParentKeys: string[]) => Promise<void>;
|
||||||
|
setSearchQuery: (query: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSidebarStore = create<SidebarState>((set, get) => ({
|
||||||
|
openedKeys: new Set<string>(),
|
||||||
|
isInitialized: false,
|
||||||
|
isAllExpanded: false,
|
||||||
|
searchQuery: '',
|
||||||
|
|
||||||
|
initializeStorage: async (initialKeysFromActive, allParentKeys) => {
|
||||||
|
if (get().isInitialized) return;
|
||||||
|
|
||||||
|
const saved = await appStorage.getItem<string[]>(AppStorageKey.SIDEBAR_OPEN_MENUS);
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
const openedSet = new Set(saved);
|
||||||
|
const isAllExpanded = saved.length > 0 && allParentKeys.every((k) => openedSet.has(k));
|
||||||
|
set({ openedKeys: openedSet, isInitialized: true, isAllExpanded });
|
||||||
|
} else {
|
||||||
|
// First time load: use initial active keys
|
||||||
|
await appStorage.setItem(AppStorageKey.SIDEBAR_OPEN_MENUS, initialKeysFromActive);
|
||||||
|
const openedSet = new Set(initialKeysFromActive);
|
||||||
|
const isAllExpanded = initialKeysFromActive.length > 0 && allParentKeys.every((k) => openedSet.has(k));
|
||||||
|
set({ openedKeys: openedSet, isInitialized: true, isAllExpanded });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMenu: async (key, isOpened, allParentKeys) => {
|
||||||
|
const prevSet = get().openedKeys;
|
||||||
|
const nextSet = new Set(prevSet);
|
||||||
|
|
||||||
|
if (isOpened) {
|
||||||
|
nextSet.add(key);
|
||||||
|
} else {
|
||||||
|
nextSet.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextArr = Array.from(nextSet);
|
||||||
|
const isAllExpanded = nextArr.length > 0 && allParentKeys.every((k) => nextSet.has(k));
|
||||||
|
|
||||||
|
set({ openedKeys: nextSet, isAllExpanded });
|
||||||
|
await appStorage.setItem(AppStorageKey.SIDEBAR_OPEN_MENUS, nextArr);
|
||||||
|
},
|
||||||
|
|
||||||
|
expandAll: async (allParentKeys) => {
|
||||||
|
const nextSet = new Set(allParentKeys);
|
||||||
|
set({ openedKeys: nextSet, isAllExpanded: true });
|
||||||
|
await appStorage.setItem(AppStorageKey.SIDEBAR_OPEN_MENUS, allParentKeys);
|
||||||
|
},
|
||||||
|
|
||||||
|
collapseAll: async () => {
|
||||||
|
set({ openedKeys: new Set(), isAllExpanded: false });
|
||||||
|
await appStorage.setItem(AppStorageKey.SIDEBAR_OPEN_MENUS, []);
|
||||||
|
},
|
||||||
|
|
||||||
|
syncActiveKeys: async (keysToSync, allParentKeys) => {
|
||||||
|
const prevSet = get().openedKeys;
|
||||||
|
let hasChanges = false;
|
||||||
|
const nextSet = new Set(prevSet);
|
||||||
|
|
||||||
|
for (const key of keysToSync) {
|
||||||
|
if (!nextSet.has(key)) {
|
||||||
|
nextSet.add(key);
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
const nextArr = Array.from(nextSet);
|
||||||
|
const isAllExpanded = nextArr.length > 0 && allParentKeys.every((k) => nextSet.has(k));
|
||||||
|
set({ openedKeys: nextSet, isAllExpanded });
|
||||||
|
await appStorage.setItem(AppStorageKey.SIDEBAR_OPEN_MENUS, nextArr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setSearchQuery: (query: string) => set({ searchQuery: query }),
|
||||||
|
}));
|
||||||
@@ -1,6 +1,18 @@
|
|||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { Box, NavLink, Stack, Tooltip, ActionIcon, Divider, Menu, TextInput, Button, Group } from '@repo/ui/components';
|
import {
|
||||||
|
Box,
|
||||||
|
NavLink,
|
||||||
|
Stack,
|
||||||
|
Tooltip,
|
||||||
|
ActionIcon,
|
||||||
|
Divider,
|
||||||
|
Menu,
|
||||||
|
TextInput,
|
||||||
|
Button,
|
||||||
|
Group,
|
||||||
|
Text,
|
||||||
|
} from '@repo/ui/components';
|
||||||
import { useCoreAppShell } from '@repo/ui/components';
|
import { useCoreAppShell } from '@repo/ui/components';
|
||||||
import { ChevronsLeft, ChevronsRight, PanelTopClose, PanelTopOpen, Search, X, History, Bookmark } from 'lucide-react';
|
import { ChevronsLeft, ChevronsRight, PanelTopClose, PanelTopOpen, Search, X, History, Bookmark } from 'lucide-react';
|
||||||
import { publish } from '@repo/core-events';
|
import { publish } from '@repo/core-events';
|
||||||
@@ -9,6 +21,8 @@ import type { SidebarVariant } from '@repo/ui/components';
|
|||||||
import { useTranslation } from '@repo/core-i18n';
|
import { useTranslation } from '@repo/core-i18n';
|
||||||
import { shortcutsData } from '@repo/ui/constants';
|
import { shortcutsData } from '@repo/ui/constants';
|
||||||
import { LAYOUT_EVENTS } from '../../../../core/constants/events';
|
import { LAYOUT_EVENTS } from '../../../../core/constants/events';
|
||||||
|
import { useSidebarStore } from './sidebar.store';
|
||||||
|
import { useDebouncedValue } from '@repo/ui/hooks';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Props
|
// Props
|
||||||
@@ -39,17 +53,15 @@ interface SidebarMenuProps {
|
|||||||
interface MenuItemExpandedProps {
|
interface MenuItemExpandedProps {
|
||||||
item: MenuItemType;
|
item: MenuItemType;
|
||||||
activeKeys: Set<string>;
|
activeKeys: Set<string>;
|
||||||
isSearching?: boolean;
|
isSearching: boolean;
|
||||||
expandVersion?: number;
|
allParentKeys: string[];
|
||||||
collapseVersion?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuItemExpanded = memo(function MenuItemExpanded({
|
const MenuItemExpanded = memo(function MenuItemExpanded({
|
||||||
item,
|
item,
|
||||||
activeKeys,
|
activeKeys,
|
||||||
isSearching,
|
isSearching,
|
||||||
expandVersion = 0,
|
allParentKeys,
|
||||||
collapseVersion = 0,
|
|
||||||
}: MenuItemExpandedProps) {
|
}: MenuItemExpandedProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
@@ -59,26 +71,27 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
|
|||||||
const isExactActive = isActive && !hasChildren;
|
const isExactActive = isActive && !hasChildren;
|
||||||
const isParentActive = isActive && hasChildren;
|
const isParentActive = isActive && hasChildren;
|
||||||
|
|
||||||
const [opened, setOpened] = useState(isActive);
|
const isOpened = useSidebarStore((state) => state.openedKeys.has(item.key));
|
||||||
const isOpened = isSearching || opened;
|
const toggleMenu = useSidebarStore((state) => state.toggleMenu);
|
||||||
|
|
||||||
useEffect(() => {
|
const effectivelyOpened = isSearching || isOpened;
|
||||||
if (expandVersion > 0) setOpened(true);
|
|
||||||
}, [expandVersion]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const handleChange = useCallback(
|
||||||
if (collapseVersion > 0) setOpened(false);
|
(newOpened: boolean) => {
|
||||||
}, [collapseVersion]);
|
toggleMenu(item.key, newOpened, allParentKeys);
|
||||||
|
},
|
||||||
|
[item.key, toggleMenu, allParentKeys],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavLink
|
<NavLink
|
||||||
component={hasChildren ? 'button' : (Link as any)}
|
component={hasChildren ? 'button' : (Link as any)}
|
||||||
to={hasChildren ? undefined : item.path}
|
to={hasChildren ? undefined : item.path}
|
||||||
label={t(item.label)}
|
label={t(item.label)}
|
||||||
leftSection={<Icon size={18} />}
|
leftSection={<Icon size={18} strokeWidth={1.8} />}
|
||||||
active={isExactActive}
|
active={isExactActive}
|
||||||
opened={isOpened}
|
opened={effectivelyOpened}
|
||||||
onChange={setOpened}
|
onChange={handleChange}
|
||||||
variant="light"
|
variant="light"
|
||||||
styles={{
|
styles={{
|
||||||
root: {
|
root: {
|
||||||
@@ -89,6 +102,7 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -99,8 +113,7 @@ const MenuItemExpanded = memo(function MenuItemExpanded({
|
|||||||
item={child}
|
item={child}
|
||||||
activeKeys={activeKeys}
|
activeKeys={activeKeys}
|
||||||
isSearching={isSearching}
|
isSearching={isSearching}
|
||||||
expandVersion={expandVersion}
|
allParentKeys={allParentKeys}
|
||||||
collapseVersion={collapseVersion}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -136,7 +149,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t(item.label)}
|
aria-label={t(item.label)}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<Icon size={20} strokeWidth={1.8} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
@@ -153,6 +166,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
maxWidth: 240,
|
maxWidth: 240,
|
||||||
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -170,7 +184,7 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
size="lg"
|
size="lg"
|
||||||
aria-label={t(item.label)}
|
aria-label={t(item.label)}
|
||||||
>
|
>
|
||||||
<Icon size={20} />
|
<Icon size={20} strokeWidth={1.8} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
@@ -222,6 +236,17 @@ const MenuItemFlyout = memo(function MenuItemFlyout({ item, activeKeys, isRoot =
|
|||||||
// SidebarMenu Component
|
// SidebarMenu Component
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const getAllParentKeys = (items: MenuItemType[]): string[] => {
|
||||||
|
let keys: string[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
keys.push(item.key);
|
||||||
|
keys = keys.concat(getAllParentKeys(item.children));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
};
|
||||||
|
|
||||||
export const SidebarMenu = memo(function SidebarMenu({
|
export const SidebarMenu = memo(function SidebarMenu({
|
||||||
items,
|
items,
|
||||||
variantOverride,
|
variantOverride,
|
||||||
@@ -238,23 +263,27 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
const variant = variantOverride ?? contextVariant;
|
const variant = variantOverride ?? contextVariant;
|
||||||
const isMini = variant === 'mini';
|
const isMini = variant === 'mini';
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const searchQuery = useSidebarStore((state) => state.searchQuery);
|
||||||
|
const setSearchQuery = useSidebarStore((state) => state.setSearchQuery);
|
||||||
|
const [debouncedSearchQuery] = useDebouncedValue(searchQuery, 200);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const isSearching = searchQuery.trim().length > 0;
|
const isSearching = debouncedSearchQuery.trim().length > 0;
|
||||||
|
|
||||||
const [expandVersion, setExpandVersion] = useState(0);
|
const allParentKeys = useMemo(() => getAllParentKeys(items), [items]);
|
||||||
const [collapseVersion, setCollapseVersion] = useState(0);
|
const isInitialized = useSidebarStore((state) => state.isInitialized);
|
||||||
const [isAllExpanded, setIsAllExpanded] = useState(false);
|
const isAllExpanded = useSidebarStore((state) => state.isAllExpanded);
|
||||||
|
const initializeStorage = useSidebarStore((state) => state.initializeStorage);
|
||||||
|
const expandAll = useSidebarStore((state) => state.expandAll);
|
||||||
|
const collapseAll = useSidebarStore((state) => state.collapseAll);
|
||||||
|
const syncActiveKeys = useSidebarStore((state) => state.syncActiveKeys);
|
||||||
|
|
||||||
const handleToggleExpandAll = useCallback(() => {
|
const handleToggleExpandAll = useCallback(() => {
|
||||||
if (isAllExpanded) {
|
if (isAllExpanded) {
|
||||||
setCollapseVersion((v) => v + 1);
|
collapseAll();
|
||||||
setIsAllExpanded(false);
|
|
||||||
} else {
|
} else {
|
||||||
setExpandVersion((v) => v + 1);
|
expandAll(allParentKeys);
|
||||||
setIsAllExpanded(true);
|
|
||||||
}
|
}
|
||||||
}, [isAllExpanded]);
|
}, [isAllExpanded, collapseAll, expandAll, allParentKeys]);
|
||||||
|
|
||||||
// Recursive active state calculation
|
// Recursive active state calculation
|
||||||
const activeKeys = useMemo(() => {
|
const activeKeys = useMemo(() => {
|
||||||
@@ -299,11 +328,33 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
return keys;
|
return keys;
|
||||||
}, [pathname, items]);
|
}, [pathname, items]);
|
||||||
|
|
||||||
|
// Sync initial state to local storage or auto-expand on navigation
|
||||||
|
useEffect(() => {
|
||||||
|
const parentKeysToSync: string[] = [];
|
||||||
|
const checkActiveParents = (item: MenuItemType) => {
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
if (activeKeys.has(item.key)) {
|
||||||
|
parentKeysToSync.push(item.key);
|
||||||
|
}
|
||||||
|
item.children.forEach(checkActiveParents);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
items.forEach(checkActiveParents);
|
||||||
|
|
||||||
|
if (!isInitialized) {
|
||||||
|
initializeStorage(parentKeysToSync, allParentKeys);
|
||||||
|
} else {
|
||||||
|
if (parentKeysToSync.length > 0) {
|
||||||
|
syncActiveKeys(parentKeysToSync, allParentKeys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeKeys, items, isInitialized, initializeStorage, syncActiveKeys, allParentKeys]);
|
||||||
|
|
||||||
// Client-side recursive filtering logic
|
// Client-side recursive filtering logic
|
||||||
const filteredItems = useMemo(() => {
|
const filteredItems = useMemo(() => {
|
||||||
if (!isSearching) return items;
|
if (!isSearching) return items;
|
||||||
|
|
||||||
const query = searchQuery.toLowerCase();
|
const query = debouncedSearchQuery.toLowerCase();
|
||||||
|
|
||||||
const filterItem = (item: MenuItemType): MenuItemType | null => {
|
const filterItem = (item: MenuItemType): MenuItemType | null => {
|
||||||
const isMatch = t(item.label).toLowerCase().includes(query);
|
const isMatch = t(item.label).toLowerCase().includes(query);
|
||||||
@@ -327,7 +378,7 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return items.map(filterItem).filter((item): item is MenuItemType => item !== null);
|
return items.map(filterItem).filter((item): item is MenuItemType => item !== null);
|
||||||
}, [items, isSearching, searchQuery]);
|
}, [items, isSearching, debouncedSearchQuery, t]);
|
||||||
|
|
||||||
const handleToggle = useCallback(() => {
|
const handleToggle = useCallback(() => {
|
||||||
setSidebarVariant(isMini ? 'expanded' : 'mini');
|
setSidebarVariant(isMini ? 'expanded' : 'mini');
|
||||||
@@ -533,10 +584,13 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
onClick={handleToggleExpandAll}
|
onClick={handleToggleExpandAll}
|
||||||
size={30}
|
size={30}
|
||||||
radius="md"
|
radius="md"
|
||||||
fw={400}
|
|
||||||
aria-label={isAllExpanded ? t('common:collapseAll') : t('common:expandAll')}
|
aria-label={isAllExpanded ? t('common:collapseAll') : t('common:expandAll')}
|
||||||
>
|
>
|
||||||
{isAllExpanded ? <PanelTopOpen size={16} /> : <PanelTopClose size={16} />}
|
{isAllExpanded ? (
|
||||||
|
<PanelTopOpen size={16} strokeWidth={1.5} />
|
||||||
|
) : (
|
||||||
|
<PanelTopClose size={16} strokeWidth={1.5} />
|
||||||
|
)}
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -578,16 +632,32 @@ export const SidebarMenu = memo(function SidebarMenu({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Box p="lg" pt="md" style={{ flex: 1, overflowY: 'auto' }}>
|
<Box p="lg" pt="md" style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
{filteredItems.map((item) => (
|
{filteredItems.length === 0 && isSearching ? (
|
||||||
<MenuItemExpanded
|
<Box
|
||||||
key={item.key}
|
p="md"
|
||||||
item={item}
|
ta="center"
|
||||||
activeKeys={activeKeys}
|
style={{
|
||||||
isSearching={isSearching}
|
border: '1px dashed var(--mantine-color-default-border)',
|
||||||
expandVersion={expandVersion}
|
borderRadius: 'var(--mantine-radius-md)',
|
||||||
collapseVersion={collapseVersion}
|
}}
|
||||||
/>
|
>
|
||||||
))}
|
<Stack gap="xs" align="center">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{t('common:system_menu.notFound')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
filteredItems.map((item) => (
|
||||||
|
<MenuItemExpanded
|
||||||
|
key={item.key}
|
||||||
|
item={item}
|
||||||
|
activeKeys={activeKeys}
|
||||||
|
isSearching={isSearching}
|
||||||
|
allParentKeys={allParentKeys}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{withToggle && (
|
{withToggle && (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const AppStorageKey = {
|
|||||||
ACCESS_TOKEN: 'access_token',
|
ACCESS_TOKEN: 'access_token',
|
||||||
REFRESH_TOKEN: 'refresh_token',
|
REFRESH_TOKEN: 'refresh_token',
|
||||||
USER_ID: 'u_id',
|
USER_ID: 'u_id',
|
||||||
|
SIDEBAR_OPEN_MENUS: 'sidebar_open_menus',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
|
export type AppStorageKeyValue = (typeof AppStorageKey)[keyof typeof AppStorageKey];
|
||||||
@@ -26,7 +27,11 @@ export const APP_STORAGE_ENCRYPTED_KEYS = new Set<AppStorageKeyValue>([
|
|||||||
AppStorageKey.REFRESH_TOKEN,
|
AppStorageKey.REFRESH_TOKEN,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const APP_STORAGE_PLAIN_KEYS = new Set<AppStorageKeyValue>([AppStorageKey.LANGUAGE, AppStorageKey.THEME]);
|
export const APP_STORAGE_PLAIN_KEYS = new Set<AppStorageKeyValue>([
|
||||||
|
AppStorageKey.LANGUAGE,
|
||||||
|
AppStorageKey.THEME,
|
||||||
|
AppStorageKey.SIDEBAR_OPEN_MENUS,
|
||||||
|
]);
|
||||||
|
|
||||||
export const APP_DATABASE_ENCRYPTED_KEYS = new Set<AppDatabaseKeyValue>([]);
|
export const APP_DATABASE_ENCRYPTED_KEYS = new Set<AppDatabaseKeyValue>([]);
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,8 @@
|
|||||||
},
|
},
|
||||||
"system_menu": {
|
"system_menu": {
|
||||||
"history": "History",
|
"history": "History",
|
||||||
"bookmark": "Bookmark"
|
"bookmark": "Bookmark",
|
||||||
|
"notFound": "Menu not found"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
@@ -154,7 +154,8 @@
|
|||||||
},
|
},
|
||||||
"system_menu": {
|
"system_menu": {
|
||||||
"history": "Riwayat",
|
"history": "Riwayat",
|
||||||
"bookmark": "Bookmark"
|
"bookmark": "Bookmark",
|
||||||
|
"notFound": "Menu tidak ditemukan"
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import {
|
|||||||
MenuItemDef,
|
MenuItemDef,
|
||||||
DefaultMenuItem,
|
DefaultMenuItem,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
|
ColumnMovedEvent,
|
||||||
|
ColumnResizedEvent,
|
||||||
|
ColumnVisibleEvent,
|
||||||
|
SortChangedEvent,
|
||||||
} from 'ag-grid-community';
|
} from 'ag-grid-community';
|
||||||
import { Box, Group, TextInput, Indicator, CloseButton } from '@mantine/core';
|
import { Box, Group, TextInput, Indicator, CloseButton } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
@@ -181,6 +185,12 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
searchKey = 'search',
|
searchKey = 'search',
|
||||||
filterConfig,
|
filterConfig,
|
||||||
|
|
||||||
|
// Grid config props
|
||||||
|
onColumnMoved,
|
||||||
|
onColumnResized,
|
||||||
|
onColumnVisible,
|
||||||
|
onSortChanged,
|
||||||
|
|
||||||
// Bulk action props
|
// Bulk action props
|
||||||
customBulkActions,
|
customBulkActions,
|
||||||
batchSize,
|
batchSize,
|
||||||
@@ -200,8 +210,16 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const { t } = useEnterpriseModuleTranslationContext();
|
const { t } = useEnterpriseModuleTranslationContext();
|
||||||
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
|
const { dataServices } = useEnterpriseModuleDataServiceContext<E>();
|
||||||
const { selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData } =
|
const {
|
||||||
useEnterpriseModuleSelectionContext<E>();
|
selectedRows,
|
||||||
|
setSelectedRows,
|
||||||
|
metaData,
|
||||||
|
setMetaData,
|
||||||
|
filterData,
|
||||||
|
setFilterData,
|
||||||
|
tableConfig,
|
||||||
|
setTableConfig,
|
||||||
|
} = useEnterpriseModuleSelectionContext<E>();
|
||||||
const navigation = useEnterpriseModuleNavigationContext();
|
const navigation = useEnterpriseModuleNavigationContext();
|
||||||
|
|
||||||
const { config } = useEnterpriseModuleConfigContext();
|
const { config } = useEnterpriseModuleConfigContext();
|
||||||
@@ -791,6 +809,11 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
setSelectedRows([]);
|
setSelectedRows([]);
|
||||||
|
|
||||||
if (params.api) {
|
if (params.api) {
|
||||||
|
// Restore grid configuration on mount
|
||||||
|
if (tableConfig) {
|
||||||
|
params.api.applyColumnState({ state: tableConfig, applyOrder: true });
|
||||||
|
}
|
||||||
|
|
||||||
// Attach the server-side datasource to the grid API
|
// Attach the server-side datasource to the grid API
|
||||||
params.api.setGridOption('serverSideDatasource', datasource);
|
params.api.setGridOption('serverSideDatasource', datasource);
|
||||||
|
|
||||||
@@ -803,7 +826,7 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[datasource, setSelectedRows, isPaginated, metaData],
|
[datasource, setSelectedRows, isPaginated, metaData, tableConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Triggered whenever the row selection in the grid changes
|
// Triggered whenever the row selection in the grid changes
|
||||||
@@ -817,6 +840,50 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
[setSelectedRows],
|
[setSelectedRows],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Save Grid Configuration State Handlers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const saveGridState = useCallback(() => {
|
||||||
|
if (!gridApiRef.current || !setTableConfig) return;
|
||||||
|
const state = gridApiRef.current.getColumnState();
|
||||||
|
setTableConfig(state);
|
||||||
|
}, [setTableConfig]);
|
||||||
|
|
||||||
|
const handleColumnMoved = useCallback(
|
||||||
|
(e: ColumnMovedEvent<E>) => {
|
||||||
|
saveGridState();
|
||||||
|
if (onColumnMoved) onColumnMoved(e);
|
||||||
|
},
|
||||||
|
[saveGridState, onColumnMoved],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleColumnResized = useCallback(
|
||||||
|
(e: ColumnResizedEvent<E>) => {
|
||||||
|
// Only save when resizing is finished to prevent excessive re-renders
|
||||||
|
if (e.finished) {
|
||||||
|
saveGridState();
|
||||||
|
}
|
||||||
|
if (onColumnResized) onColumnResized(e);
|
||||||
|
},
|
||||||
|
[saveGridState, onColumnResized],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleColumnVisible = useCallback(
|
||||||
|
(e: ColumnVisibleEvent<E>) => {
|
||||||
|
saveGridState();
|
||||||
|
if (onColumnVisible) onColumnVisible(e);
|
||||||
|
},
|
||||||
|
[saveGridState, onColumnVisible],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortChanged = useCallback(
|
||||||
|
(e: SortChangedEvent<E>) => {
|
||||||
|
saveGridState();
|
||||||
|
if (onSortChanged) onSortChanged(e);
|
||||||
|
},
|
||||||
|
[saveGridState, onSortChanged],
|
||||||
|
);
|
||||||
|
|
||||||
// Configures the context menu items available when right-clicking a cell
|
// Configures the context menu items available when right-clicking a cell
|
||||||
const getContextMenuItems = useCallback(():
|
const getContextMenuItems = useCallback(():
|
||||||
| (DefaultMenuItem | MenuItemDef)[]
|
| (DefaultMenuItem | MenuItemDef)[]
|
||||||
@@ -923,6 +990,10 @@ export function EnterpriseDataTable<E extends BaseEntity>(props: EnterpriseDataT
|
|||||||
enableCellTextSelection={true}
|
enableCellTextSelection={true}
|
||||||
onSelectionChanged={handleSelectionChanged}
|
onSelectionChanged={handleSelectionChanged}
|
||||||
onGridReady={onGridReady}
|
onGridReady={onGridReady}
|
||||||
|
onColumnMoved={handleColumnMoved}
|
||||||
|
onColumnResized={handleColumnResized}
|
||||||
|
onColumnVisible={handleColumnVisible}
|
||||||
|
onSortChanged={handleSortChanged}
|
||||||
getContextMenuItems={getContextMenuItems}
|
getContextMenuItems={getContextMenuItems}
|
||||||
statusBar={statusBar}
|
statusBar={statusBar}
|
||||||
domLayout={domLayout}
|
domLayout={domLayout}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ZodType } from 'zod';
|
|||||||
import type { BaseEntity, BaseRemoteDataServices, StandardPaginationMeta } from '@repo/core-api/data-services';
|
import type { BaseEntity, BaseRemoteDataServices, StandardPaginationMeta } from '@repo/core-api/data-services';
|
||||||
import type { ModulePageHeaderProps } from '../components/module-page-header';
|
import type { ModulePageHeaderProps } from '../components/module-page-header';
|
||||||
import { PageActionsProps } from '../../../components';
|
import { PageActionsProps } from '../../../components';
|
||||||
|
import { ColumnState } from 'ag-grid-community';
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Module Constants & Base Types
|
// Module Constants & Base Types
|
||||||
@@ -140,6 +141,8 @@ export interface SelectionSlice<
|
|||||||
setMetaData: (data: TMeta | null) => void;
|
setMetaData: (data: TMeta | null) => void;
|
||||||
filterData: TFilter | null;
|
filterData: TFilter | null;
|
||||||
setFilterData: (data: TFilter | null) => void;
|
setFilterData: (data: TFilter | null) => void;
|
||||||
|
tableConfig: ColumnState[] | null;
|
||||||
|
setTableConfig: (config: ColumnState[] | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,6 +159,8 @@ export interface EnterpriseModuleState<
|
|||||||
setFilterData: (data: TFilter | null) => void;
|
setFilterData: (data: TFilter | null) => void;
|
||||||
selectedRows: E[];
|
selectedRows: E[];
|
||||||
setSelectedRows: (rows: E[]) => void;
|
setSelectedRows: (rows: E[]) => void;
|
||||||
|
tableConfig: ColumnState[] | null;
|
||||||
|
setTableConfig: (config: ColumnState[] | null) => void;
|
||||||
privileges: string[];
|
privileges: string[];
|
||||||
setPrivileges: (privileges: string[]) => void;
|
setPrivileges: (privileges: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ export function EnterpriseModuleProvider<
|
|||||||
const setMetaData = store((state: S) => state.setMetaData);
|
const setMetaData = store((state: S) => state.setMetaData);
|
||||||
const filterData = store((state: S) => state.filterData);
|
const filterData = store((state: S) => state.filterData);
|
||||||
const setFilterData = store((state: S) => state.setFilterData);
|
const setFilterData = store((state: S) => state.setFilterData);
|
||||||
|
const tableConfig = store((state: S) => state.tableConfig);
|
||||||
|
const setTableConfig = store((state: S) => state.setTableConfig);
|
||||||
|
|
||||||
const selectionSlice = useMemo(
|
const selectionSlice = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -91,8 +93,10 @@ export function EnterpriseModuleProvider<
|
|||||||
setMetaData,
|
setMetaData,
|
||||||
filterData,
|
filterData,
|
||||||
setFilterData,
|
setFilterData,
|
||||||
|
tableConfig,
|
||||||
|
setTableConfig,
|
||||||
}),
|
}),
|
||||||
[selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData],
|
[selectedRows, setSelectedRows, metaData, setMetaData, filterData, setFilterData, tableConfig, setTableConfig],
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user