The selected Space used to live in sessionStorage and ride along invisibly with every collection route. It now lives in the URL: collection pages exist both globally and beneath /spaces/:spaceUid, and the Space context derives its scope from the current location. - Add a Space route branch (Home, Explore, Calendar, Attachments) guarded by a SpaceRoute that shows loading, unavailable, and retry states. - Resolve collection routes through one matchPath-based helper built from the route constants; build Space-scoped paths with buildCollectionPath. - Fetch the route Space directly, seeded from the switcher list so pages are not held behind a second round-trip. - Turn the Space switcher into plain links that preserve the current collection view and query. - Drop the remembered-Space machinery: clearSelectedSpace/selectMemos, the compose-placement policy, routeSupportsCollectionScope, and the quick-find switchToAll flag are all implied by the URL now. - Return from memo details to the full origin URL, including its Space. - Add space.unavailable and space.back-to-memos strings; align the Azerbaijani catalog with English.
94 lines
3.4 KiB
TypeScript
94 lines
3.4 KiB
TypeScript
import type { ConnectError } from "@connectrpc/connect";
|
|
import { createContext, type ReactNode, useCallback, useContext, useMemo } from "react";
|
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
import useCurrentUser from "@/hooks/useCurrentUser";
|
|
import { useSpace, useSpaces } from "@/hooks/useSpaceQueries";
|
|
import { buildCollectionScopeFilter, type CollectionScope } from "@/lib/cel-filter";
|
|
import { getDuplicateSpaceTitles } from "@/lib/space-display";
|
|
import { buildCollectionPath, ROUTES, resolveCollectionRoute } from "@/router/routes";
|
|
import type { Space } from "@/types/proto/api/v1/space_service_pb";
|
|
|
|
interface SpaceContextValue {
|
|
spaces: Space[];
|
|
spaceByName: ReadonlyMap<string, Space>;
|
|
duplicateSpaceTitles: ReadonlySet<string>;
|
|
selectedSpace?: Space;
|
|
selectedSpaceName?: string;
|
|
collectionScope: CollectionScope;
|
|
memoFilter?: string;
|
|
isLoadingSpaces: boolean;
|
|
isSpacesError: boolean;
|
|
isSpaceReady: boolean;
|
|
spaceError: ConnectError | null;
|
|
retrySpace: () => void;
|
|
/** Opens the Space Home after creation or from an invitation. */
|
|
selectSpace: (space: Space) => void;
|
|
}
|
|
|
|
const SpaceContext = createContext<SpaceContextValue | null>(null);
|
|
const NO_SPACES: Space[] = [];
|
|
|
|
export function SpaceProvider({ children }: { children: ReactNode }) {
|
|
const userName = useCurrentUser()?.name;
|
|
const location = useLocation();
|
|
const navigate = useNavigate();
|
|
const { spaceName: selectedSpaceName } = resolveCollectionRoute(location.pathname);
|
|
const spacesQuery = useSpaces(userName);
|
|
const spaceQuery = useSpace(userName, selectedSpaceName);
|
|
const spaces = spacesQuery.data ?? NO_SPACES;
|
|
const selectedSpace = spaceQuery.data;
|
|
const spaceByName = useMemo(() => {
|
|
const result = new Map(spaces.map((space) => [space.name, space]));
|
|
if (selectedSpace) result.set(selectedSpace.name, selectedSpace);
|
|
return result;
|
|
}, [spaces, selectedSpace]);
|
|
const duplicateSpaceTitles = useMemo(() => getDuplicateSpaceTitles([...spaceByName.values()]), [spaceByName]);
|
|
const collectionScope = useMemo<CollectionScope>(
|
|
() => (selectedSpaceName ? { kind: "space", name: selectedSpaceName } : { kind: "all" }),
|
|
[selectedSpaceName],
|
|
);
|
|
const selectSpace = useCallback((space: Space) => navigate(buildCollectionPath(ROUTES.HOME, space.name)), [navigate]);
|
|
const retrySpace = useCallback(() => {
|
|
void spaceQuery.refetch();
|
|
}, [spaceQuery.refetch]);
|
|
const value = useMemo<SpaceContextValue>(
|
|
() => ({
|
|
spaces,
|
|
spaceByName,
|
|
duplicateSpaceTitles,
|
|
selectedSpace,
|
|
selectedSpaceName,
|
|
collectionScope,
|
|
memoFilter: buildCollectionScopeFilter(collectionScope),
|
|
isLoadingSpaces: spacesQuery.isPending,
|
|
isSpacesError: spacesQuery.isError,
|
|
isSpaceReady: !selectedSpaceName || (Boolean(userName) && spaceQuery.isSuccess),
|
|
spaceError: spaceQuery.error,
|
|
retrySpace,
|
|
selectSpace,
|
|
}),
|
|
[
|
|
spaces,
|
|
spaceByName,
|
|
duplicateSpaceTitles,
|
|
selectedSpace,
|
|
selectedSpaceName,
|
|
collectionScope,
|
|
spacesQuery.isPending,
|
|
spacesQuery.isError,
|
|
userName,
|
|
spaceQuery.isSuccess,
|
|
spaceQuery.error,
|
|
retrySpace,
|
|
selectSpace,
|
|
],
|
|
);
|
|
|
|
return <SpaceContext.Provider value={value}>{children}</SpaceContext.Provider>;
|
|
}
|
|
|
|
export function useSpaceContext() {
|
|
const context = useContext(SpaceContext);
|
|
if (!context) throw new Error("useSpaceContext must be used within SpaceProvider");
|
|
return context;
|
|
}
|