Files
paseo/packages/app/src/stores/navigation-active-workspace-store.ts
T

246 lines
6.6 KiB
TypeScript

import { useMemo, useSyncExternalStore } from "react";
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/shim/with-selector";
import {
buildHostWorkspaceRoute,
decodeWorkspaceIdFromPathSegment,
parseHostWorkspaceRouteFromPathname,
} from "@/utils/host-routes";
import { isWeb } from "@/constants/platform";
export interface ActiveWorkspaceSelection {
serverId: string;
workspaceId: string;
}
interface ActivateWorkspaceSelectionOptions {
updateBrowserHistory?: boolean;
historyMode?: "push" | "replace";
}
type NavigationRouteParams = {
serverId?: string | string[];
workspaceId?: string | string[];
};
type NavigationRouteLike = {
params?: NavigationRouteParams | null;
path?: string | null;
};
type NavigationWorkspaceRouteState =
| { kind: "workspace"; selection: ActiveWorkspaceSelection }
| { kind: "nonWorkspace" }
| { kind: "unknown" };
interface NavigationObserverRef {
current: {
getCurrentRoute(): NavigationRouteLike | null | undefined;
} | null;
}
let snapshot: ActiveWorkspaceSelection | null = null;
const listeners = new Set<() => void>();
function subscribe(listener: () => void): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
function getSnapshot(): ActiveWorkspaceSelection | null {
return snapshot;
}
function emitIfChanged(next: ActiveWorkspaceSelection | null) {
if (snapshot?.serverId === next?.serverId && snapshot?.workspaceId === next?.workspaceId) {
return;
}
snapshot = next;
for (const listener of listeners) {
listener();
}
}
function getBrowserLocationWorkspace(): ActiveWorkspaceSelection | null {
if (!isWeb || typeof window === "undefined") {
return null;
}
return parseHostWorkspaceRouteFromPathname(window.location.pathname);
}
function writeBrowserWorkspaceUrl(
next: ActiveWorkspaceSelection,
options: ActivateWorkspaceSelectionOptions,
) {
if (!options.updateBrowserHistory || !isWeb || typeof window === "undefined") {
return;
}
const nextPath = buildHostWorkspaceRoute(next.serverId, next.workspaceId);
const currentUrl = new URL(window.location.href);
if (currentUrl.pathname === nextPath && !currentUrl.search && !currentUrl.hash) {
return;
}
const nextUrl = new URL(nextPath, window.location.origin);
const mode = options.historyMode ?? "push";
if (mode === "replace") {
window.history.replaceState(null, "", nextUrl.toString());
return;
}
window.history.pushState(null, "", nextUrl.toString());
}
function extractActiveWorkspaceFromRoute(
route: NavigationRouteLike | null | undefined,
): ActiveWorkspaceSelection | null {
if (!route) {
return null;
}
if (typeof route.path === "string") {
const parsed = parseHostWorkspaceRouteFromPathname(route.path);
if (parsed) {
return parsed;
}
}
const serverValue = Array.isArray(route.params?.serverId)
? route.params.serverId[0]
: route.params?.serverId;
const workspaceValue = Array.isArray(route.params?.workspaceId)
? route.params.workspaceId[0]
: route.params?.workspaceId;
const serverId = typeof serverValue === "string" ? serverValue.trim() : "";
const workspaceId =
typeof workspaceValue === "string"
? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? "")
: "";
if (!serverId || !workspaceId) {
return null;
}
return { serverId, workspaceId };
}
function classifyNavigationWorkspaceRoute(
route: NavigationRouteLike | null | undefined,
): NavigationWorkspaceRouteState {
if (!route) {
return { kind: "unknown" };
}
if (typeof route.path === "string") {
const selection = parseHostWorkspaceRouteFromPathname(route.path);
if (selection) {
return { kind: "workspace", selection };
}
return { kind: "nonWorkspace" };
}
const selection = extractActiveWorkspaceFromRoute(route);
if (selection) {
return { kind: "workspace", selection };
}
return { kind: "unknown" };
}
function getActiveWorkspaceForNavigationSync(
route: NavigationRouteLike | null | undefined,
): ActiveWorkspaceSelection | null {
const routeState = classifyNavigationWorkspaceRoute(route);
if (routeState.kind === "workspace") {
const browserWorkspace = getBrowserLocationWorkspace();
return browserWorkspace ?? routeState.selection;
}
if (routeState.kind === "nonWorkspace") {
return null;
}
return getBrowserLocationWorkspace();
}
export function syncNavigationActiveWorkspace(navigationRef: NavigationObserverRef) {
emitIfChanged(getActiveWorkspaceForNavigationSync(navigationRef.current?.getCurrentRoute()));
}
export function activateNavigationWorkspaceSelection(
next: ActiveWorkspaceSelection,
options: ActivateWorkspaceSelectionOptions = {},
) {
writeBrowserWorkspaceUrl(next, options);
emitIfChanged(next);
}
export function getNavigationActiveWorkspaceSelection(): ActiveWorkspaceSelection | null {
return getSnapshot();
}
export function syncBrowserActiveWorkspaceFromLocation() {
emitIfChanged(getBrowserLocationWorkspace());
}
export function addBrowserActiveWorkspaceLocationListener(): () => void {
if (!isWeb || typeof window === "undefined") {
return () => {};
}
const handlePopState = () => {
syncBrowserActiveWorkspaceFromLocation();
};
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
}
export function useNavigationActiveWorkspaceSelection(): ActiveWorkspaceSelection | null {
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
}
export function useIsNavigationWorkspaceSelected(input: {
serverId: string | null;
workspaceId: string | null;
enabled?: boolean;
}): boolean {
const enabled = input.enabled !== false;
return useSyncExternalStoreWithSelector(
subscribe,
getSnapshot,
getSnapshot,
(selection) =>
enabled &&
Boolean(input.serverId) &&
Boolean(input.workspaceId) &&
selection?.serverId === input.serverId &&
selection.workspaceId === input.workspaceId,
Object.is,
);
}
export function useIsNavigationProjectActive(input: {
serverId: string | null;
workspaceIds: readonly string[];
enabled?: boolean;
}): boolean {
const enabled = input.enabled !== false;
const workspaceIdsKey = input.workspaceIds.join("\0");
const workspaceIdSet = useMemo(() => new Set(input.workspaceIds), [workspaceIdsKey]);
return useSyncExternalStoreWithSelector(
subscribe,
getSnapshot,
getSnapshot,
(selection) =>
enabled &&
Boolean(input.serverId) &&
selection?.serverId === input.serverId &&
workspaceIdSet.has(selection.workspaceId),
Object.is,
);
}