All files / pages/collections CollectionsWorkspaceContext.tsx

94.73% Statements 36/38
50% Branches 2/4
100% Functions 11/11
97.14% Lines 34/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141                                                                        2x       2x     94x     94x   94x 94x 94x 94x 32x 32x   94x   94x 2x 2x     94x 35x 35x 35x 35x           34x   1x   35x       94x 32x     94x 64x       94x 40x       94x 91x                                                     94x                   346x 346x         346x    
/**
 * Shared state for the Collections workspace shell.
 * Provides collection list, rail collapse, and import dialog orchestration.
 */
 
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
  type ReactNode,
} from 'react';
import {
  getCollections,
  type CollectionListItem,
} from '@/services/collections.service';
import { handleApiError } from '@/lib/api';
import { isSuperadminAccount } from '@/lib/auth';
import { useAuthStore } from '@/stores/auth.store';
 
interface CollectionsWorkspaceContextValue {
  collections: CollectionListItem[];
  loading: boolean;
  error: string | null;
  refreshCollections: () => Promise<void>;
  railCollapsed: boolean;
  setRailCollapsed: (collapsed: boolean) => void;
  isSuperadmin: boolean;
  importDialogOpen: boolean;
  setImportDialogOpen: (open: boolean) => void;
  collectionNames: string[];
  getListItem: (name: string) => CollectionListItem | undefined;
}
 
const CollectionsWorkspaceContext = createContext<CollectionsWorkspaceContextValue | null>(
  null,
);
 
const RAIL_COLLAPSED_KEY = 'snackbase_collections_rail_collapsed';
 
export function CollectionsWorkspaceProvider({ children }: { children: ReactNode }) {
  const { account } = useAuthStore();
  // Superadmin = system account membership (backend require_superadmin), not role name.
  // Superadmins are seeded with role "admin" on the system account.
  const isSuperadmin = isSuperadminAccount(account);
 
  const [collections, setCollections] = useState<CollectionListItem[]>([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [railCollapsed, setRailCollapsedState] = useState(() => {
    Iif (typeof window === 'undefined') return false;
    return localStorage.getItem(RAIL_COLLAPSED_KEY) === 'true';
  });
  const [importDialogOpen, setImportDialogOpen] = useState(false);
 
  const setRailCollapsed = useCallback((collapsed: boolean) => {
    setRailCollapsedState(collapsed);
    localStorage.setItem(RAIL_COLLAPSED_KEY, String(collapsed));
  }, []);
 
  const refreshCollections = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const response = await getCollections({
        page: 1,
        page_size: 100,
        sort_by: 'name',
        sort_order: 'asc',
      });
      setCollections(response.items);
    } catch (err) {
      setError(handleApiError(err));
    } finally {
      setLoading(false);
    }
  }, []);
 
  useEffect(() => {
    void refreshCollections();
  }, [refreshCollections]);
 
  const collectionNames = useMemo(
    () => collections.map((c) => c.name),
    [collections],
  );
 
  const getListItem = useCallback(
    (name: string) => collections.find((c) => c.name === name),
    [collections],
  );
 
  const value = useMemo<CollectionsWorkspaceContextValue>(
    () => ({
      collections,
      loading,
      error,
      refreshCollections,
      railCollapsed,
      setRailCollapsed,
      isSuperadmin,
      importDialogOpen,
      setImportDialogOpen,
      collectionNames,
      getListItem,
    }),
    [
      collections,
      loading,
      error,
      refreshCollections,
      railCollapsed,
      setRailCollapsed,
      isSuperadmin,
      importDialogOpen,
      collectionNames,
      getListItem,
    ],
  );
 
  return (
    <CollectionsWorkspaceContext.Provider value={value}>
      {children}
    </CollectionsWorkspaceContext.Provider>
  );
}
 
// Context hooks are co-located with providers (standard React pattern).
// eslint-disable-next-line react-refresh/only-export-components
export function useCollectionsWorkspace() {
  const ctx = useContext(CollectionsWorkspaceContext);
  Iif (!ctx) {
    throw new Error(
      'useCollectionsWorkspace must be used within CollectionsWorkspaceProvider',
    );
  }
  return ctx;
}