Files
govoplan-core/webui/src/platform/launchContext.ts
T

172 lines
5.7 KiB
TypeScript

import type {
ActiveObjectReference,
AuthInfo,
EffectiveViewProjection,
QuickAccessLaunchContext,
QuickAccessResult
} from "../types";
import type { TemporalDataSelection } from "./temporal";
export const QUICK_ACCESS_LAUNCH_CONTEXT_VERSION = "2" as const;
export const QUICK_ACCESS_REFERENCE_CONTRACT_VERSION = "1" as const;
export const QUICK_ACCESS_RESULT_CONTRACT_VERSION = "1" as const;
export const QUICK_ACCESS_LAUNCH_STATE_KEY = "govoplanQuickAccessLaunch";
export const QUICK_ACCESS_RESULT_EVENT = "govoplan:quick-access-result";
export type QuickAccessLaunchContextInput = {
pathname: string;
search?: string;
hash?: string;
historyIndex?: number | null;
auth: AuthInfo;
activeObject?: ActiveObjectReference | null;
temporalContext: TemporalDataSelection;
viewContext?: EffectiveViewProjection | null;
};
export function createQuickAccessLaunchContext({
pathname,
search = "",
hash = "",
historyIndex = null,
auth,
activeObject = null,
temporalContext,
viewContext = null
}: QuickAccessLaunchContextInput): QuickAccessLaunchContext {
const tenant = auth.active_tenant ?? auth.tenant;
const principal = auth.principal;
return {
contractVersion: QUICK_ACCESS_LAUNCH_CONTEXT_VERSION,
referenceContractVersion: QUICK_ACCESS_REFERENCE_CONTRACT_VERSION,
origin: {
pathname: normalizePathname(pathname),
search: normalizeSearch(search),
hash: normalizeHash(hash),
historyIndex: Number.isInteger(historyIndex) ? historyIndex ?? null : null
},
tenantId: tenant.id,
accountId: auth.user.account_id,
activeObject: activeObject?.tenantId === tenant.id ? activeObject : null,
actingContext: principal?.acting_assignment_id || principal?.acting_for_account_id
? {
assignmentId: principal.acting_assignment_id ?? null,
actingForAccountId: principal.acting_for_account_id ?? null
}
: null,
temporalContext: {
validityMode: temporalContext.validityMode,
validAt: temporalContext.validAt ?? null,
recordedAt: temporalContext.recordedAt ?? null
},
viewContext: viewContext?.activeViewId
? {
viewId: viewContext.activeViewId,
revisionId: viewContext.activeRevisionId,
name: viewContext.activeViewName,
recommendedToolIds: viewContext.presentation?.quickAccessRecommendedToolIds ?? [],
focusedToolIds: viewContext.presentation?.quickAccessFocusedToolIds ?? []
}
: null
};
}
export function quickAccessLaunchState(
context: QuickAccessLaunchContext
): Record<string, QuickAccessLaunchContext> {
return { [QUICK_ACCESS_LAUNCH_STATE_KEY]: context };
}
export function quickAccessLaunchContextFromState(
value: unknown
): QuickAccessLaunchContext | null {
if (!isRecord(value)) return null;
const candidate = value[QUICK_ACCESS_LAUNCH_STATE_KEY];
if (
!isRecord(candidate)
|| candidate.contractVersion !== QUICK_ACCESS_LAUNCH_CONTEXT_VERSION
|| candidate.referenceContractVersion !== QUICK_ACCESS_REFERENCE_CONTRACT_VERSION
) {
return null;
}
if (!isRecord(candidate.origin) || typeof candidate.origin.pathname !== "string") {
return null;
}
if (
typeof candidate.tenantId !== "string"
|| typeof candidate.accountId !== "string"
|| !candidate.origin.pathname.startsWith("/")
) {
return null;
}
return candidate as QuickAccessLaunchContext;
}
export function quickAccessReturnPath(context: QuickAccessLaunchContext): string {
return `${context.origin.pathname}${context.origin.search}${context.origin.hash}`;
}
export function dispatchQuickAccessResult(
toolId: string,
launchContext: QuickAccessLaunchContext,
result: QuickAccessResult,
returnedReferenceKinds?: readonly string[]
): boolean {
if (
typeof window === "undefined"
|| !isQuickAccessResultAllowed(result, launchContext, returnedReferenceKinds)
) return false;
window.dispatchEvent(new CustomEvent(QUICK_ACCESS_RESULT_EVENT, {
detail: { toolId, launchContext, result }
}));
return true;
}
export function isQuickAccessResultAllowed(
result: QuickAccessResult,
launchContext: QuickAccessLaunchContext,
returnedReferenceKinds?: readonly string[]
): boolean {
if (result.contractVersion !== QUICK_ACCESS_RESULT_CONTRACT_VERSION) return false;
if (result.outcome === "cancelled") {
return result.action === undefined
&& result.reference === undefined
&& (result.reason === undefined
|| (["user", "dismissed", "unavailable", "failed"] as const).includes(result.reason));
}
if (result.outcome !== "completed") return false;
if (!(["created", "selected", "updated", "completed"] as const).includes(result.action)) return false;
if (!result.reference) return true;
if (
result.reference.tenantId !== launchContext.tenantId
|| !result.reference.ownerModule
|| !result.reference.kind
|| !result.reference.objectId
|| (result.reference.path !== undefined
&& result.reference.path !== null
&& (!result.reference.path.startsWith("/") || result.reference.path.startsWith("//")))
) return false;
if (returnedReferenceKinds) {
const returnedKind = `${result.reference.ownerModule}.${result.reference.kind}`;
if (!returnedReferenceKinds.includes(returnedKind)) return false;
}
return true;
}
function normalizePathname(value: string): string {
return value.startsWith("/") ? value : "/";
}
function normalizeSearch(value: string): string {
return !value ? "" : value.startsWith("?") ? value : "";
}
function normalizeHash(value: string): string {
return !value ? "" : value.startsWith("#") ? value : "";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}