Files
pangolin/src/lib/launcherUrlState.ts
2026-07-01 10:03:06 -04:00

279 lines
7.0 KiB
TypeScript

import type { LauncherActiveViewId } from "@app/lib/launcherLocalStorage";
import {
defaultLauncherViewConfig,
parseIdListParam,
type LauncherViewConfig,
type LauncherViewRecord
} from "@server/routers/launcher/types";
import { z } from "zod";
const launcherUrlBooleanSchema = z
.enum(["0", "1"])
.transform((value) => value === "1");
export type LauncherUrlConfigOverrides = Partial<
Pick<
LauncherViewConfig,
| "groupBy"
| "layout"
| "order"
| "showLabels"
| "siteIds"
| "labelIds"
| "query"
>
>;
export type ParsedLauncherUrlState = {
viewId: LauncherActiveViewId | null;
configOverrides: LauncherUrlConfigOverrides;
hasAnyLauncherParams: boolean;
};
export type ResolvedLauncherState = {
activeViewId: LauncherActiveViewId;
config: LauncherViewConfig;
savedConfig: LauncherViewConfig;
};
const LAUNCHER_CONFIG_PARAM_KEYS = [
"query",
"groupBy",
"layout",
"order",
"showLabels",
"siteIds",
"labelIds"
] as const;
const LAUNCHER_URL_PARAM_KEYS = [
"view",
...LAUNCHER_CONFIG_PARAM_KEYS
] as const;
export function hasLauncherConfigParams(searchParams: URLSearchParams) {
return LAUNCHER_CONFIG_PARAM_KEYS.some((key) => searchParams.has(key));
}
export function isLauncherConfigEqual(
a: LauncherViewConfig,
b: LauncherViewConfig
) {
return JSON.stringify(a) === JSON.stringify(b);
}
export function getLauncherUrlBaseConfig(
viewId: LauncherActiveViewId,
views: LauncherViewRecord[]
): LauncherViewConfig {
if (viewId === "default") {
return defaultLauncherViewConfig;
}
const savedView = views.find((view) => view.viewId === viewId);
return savedView?.config ?? defaultLauncherViewConfig;
}
export function resolveLauncherConfig(
baseConfig: LauncherViewConfig,
overrides: LauncherUrlConfigOverrides
): LauncherViewConfig {
return {
...baseConfig,
...overrides,
sortBy: "name"
};
}
function parseViewParam(value: string | null): LauncherActiveViewId | null {
if (value === null) {
return null;
}
if (value === "default") {
return "default";
}
const parsed = Number.parseInt(value, 10);
if (!Number.isFinite(parsed)) {
return "default";
}
return parsed;
}
function parseConfigOverrides(
searchParams: URLSearchParams
): LauncherUrlConfigOverrides {
const overrides: LauncherUrlConfigOverrides = {};
const query = searchParams.get("query");
if (query !== null) {
overrides.query = query;
}
const groupBy = searchParams.get("groupBy");
if (groupBy === "site" || groupBy === "label") {
overrides.groupBy = groupBy;
}
const layout = searchParams.get("layout");
if (layout === "grid" || layout === "list") {
overrides.layout = layout;
}
const order = searchParams.get("order");
if (order === "asc" || order === "desc") {
overrides.order = order;
}
const showLabels = searchParams.get("showLabels");
if (showLabels !== null) {
const parsed = launcherUrlBooleanSchema.safeParse(showLabels);
if (parsed.success) {
overrides.showLabels = parsed.data;
}
}
const siteIds = searchParams.get("siteIds");
if (siteIds !== null) {
overrides.siteIds = parseIdListParam(siteIds);
}
const labelIds = searchParams.get("labelIds");
if (labelIds !== null) {
overrides.labelIds = parseIdListParam(labelIds);
}
return overrides;
}
export function parseLauncherUrlState(
searchParams: URLSearchParams
): ParsedLauncherUrlState {
const hasAnyLauncherParams = LAUNCHER_URL_PARAM_KEYS.some((key) =>
searchParams.has(key)
);
return {
viewId: parseViewParam(searchParams.get("view")),
configOverrides: parseConfigOverrides(searchParams),
hasAnyLauncherParams
};
}
function isValidActiveViewId(
viewId: LauncherActiveViewId,
views: LauncherViewRecord[]
) {
return viewId === "default" || views.some((view) => view.viewId === viewId);
}
export function resolveLauncherStateFromUrl(
searchParams: URLSearchParams,
views: LauncherViewRecord[],
fallbackViewId: LauncherActiveViewId | null
): ResolvedLauncherState {
const parsed = parseLauncherUrlState(searchParams);
let activeViewId: LauncherActiveViewId = "default";
if (parsed.viewId !== null) {
activeViewId = isValidActiveViewId(parsed.viewId, views)
? parsed.viewId
: "default";
} else if (!parsed.hasAnyLauncherParams && fallbackViewId !== null) {
activeViewId = isValidActiveViewId(fallbackViewId, views)
? fallbackViewId
: "default";
}
const savedConfig = getLauncherUrlBaseConfig(activeViewId, views);
let config: LauncherViewConfig;
if (hasLauncherConfigParams(searchParams)) {
config = resolveLauncherConfig(
defaultLauncherViewConfig,
parsed.configOverrides
);
} else if (activeViewId !== "default") {
config = savedConfig;
} else {
config = defaultLauncherViewConfig;
}
return {
activeViewId,
config,
savedConfig
};
}
function idListsEqual(a: number[], b: number[]) {
if (a.length !== b.length) {
return false;
}
return a.every((value, index) => value === b[index]);
}
export function serializeLauncherUrlState({
viewId,
config
}: {
viewId: LauncherActiveViewId;
config: LauncherViewConfig;
}): URLSearchParams {
const baseConfig = defaultLauncherViewConfig;
const params = new URLSearchParams();
if (viewId !== "default") {
params.set("view", String(viewId));
}
if (config.query !== baseConfig.query && config.query) {
params.set("query", config.query);
} else if (config.query !== baseConfig.query && !config.query) {
params.set("query", "");
}
if (config.groupBy !== baseConfig.groupBy) {
params.set("groupBy", config.groupBy);
}
if (config.layout !== baseConfig.layout) {
params.set("layout", config.layout);
}
if (config.order !== baseConfig.order) {
params.set("order", config.order);
}
if (config.showLabels !== baseConfig.showLabels) {
params.set("showLabels", config.showLabels ? "1" : "0");
}
if (!idListsEqual(config.siteIds, baseConfig.siteIds)) {
if (config.siteIds.length > 0) {
params.set("siteIds", config.siteIds.join(","));
} else {
params.set("siteIds", "");
}
}
if (!idListsEqual(config.labelIds, baseConfig.labelIds)) {
if (config.labelIds.length > 0) {
params.set("labelIds", config.labelIds.join(","));
} else {
params.set("labelIds", "");
}
}
return params;
}
export function buildLauncherPath(orgId: string, params: URLSearchParams) {
const query = params.toString();
return query ? `/${orgId}?${query}` : `/${orgId}`;
}