246 lines
6.6 KiB
TypeScript
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,
|
|
);
|
|
}
|