Add shared automation and WebUI editing primitives

This commit is contained in:
2026-07-31 02:48:56 +02:00
parent f0898fcdee
commit 5b55f59a92
42 changed files with 3788 additions and 554 deletions
+112 -8
View File
@@ -1,4 +1,4 @@
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import { Navigate, Route, Routes, useLocation } from "react-router";
import { lazy, useEffect, useMemo, useState } from "react";
import { fetchSession, fetchShellAuth, updateProfile } from "./api/auth";
import { fetchPlatformModules, fetchPlatformPublicModules, fetchPlatformStatus } from "./api/platform";
@@ -11,7 +11,11 @@ import { PermissionBoundary } from "./components/AccessBoundary";
import { firstAccessibleRoute, loadInstalledPublicWebModules, loadInstalledWebModules, loadRemotePublicWebModules, loadRemoteWebModules, moduleInstalled, navItemsForModules, publicRouteContributionsForModules, routeContributionsForModules, uiCapability } from "./platform/modules";
import { PlatformModulesProvider } from "./platform/ModuleContext";
import { PlatformViewProvider } from "./platform/ViewContext";
import { PLATFORM_VIEW_CHANGED_EVENT } from "./platform/views";
import {
PLATFORM_VIEW_CHANGED_EVENT,
PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT,
type WorkflowViewChangedEventDetail
} from "./platform/views";
import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents";
import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard";
import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext";
@@ -45,7 +49,9 @@ export default function App() {
const [backendReachable, setBackendReachable] = useState(true);
const [systemLanguages, setSystemLanguages] = useState<{available: PlatformLanguage[];enabled: string[];defaultLanguage: string;} | null>(null);
const [reloginMessage, setReloginMessage] = useState("");
const [viewProjection, setViewProjection] = useState<EffectiveViewProjection | null>(null);
const [baseViewProjection, setBaseViewProjection] = useState<EffectiveViewProjection | null>(null);
const [workflowViewProjection, setWorkflowViewProjection] = useState<EffectiveViewProjection | null>(null);
const viewProjection = workflowViewProjection ?? baseViewProjection;
const webModules = useMemo(() => mergeWebModules(localWebModules, remoteWebModules), [localWebModules, remoteWebModules]);
const publicWebModules = useMemo(() => mergeWebModules(localPublicWebModules, remotePublicWebModules), [localPublicWebModules, remotePublicWebModules]);
@@ -65,7 +71,8 @@ export default function App() {
useEffect(() => {
if (!auth || !viewsRuntime) {
setViewProjection(null);
setBaseViewProjection(null);
setWorkflowViewProjection(null);
return;
}
const currentAuth = auth;
@@ -74,17 +81,22 @@ export default function App() {
async function loadEffectiveView() {
try {
const projection = await currentRuntime.loadEffectiveView(settings, currentAuth);
if (!cancelled) setViewProjection(projection);
if (!cancelled) setBaseViewProjection(projection);
} catch (error) {
if (!cancelled) setViewProjection(null);
if (!cancelled) setBaseViewProjection(null);
console.error("Failed to load the effective View", error);
}
}
function reloadNormalView() {
setWorkflowViewProjection(null);
clearStoredWorkflowView(currentAuth);
void loadEffectiveView();
}
void loadEffectiveView();
window.addEventListener(PLATFORM_VIEW_CHANGED_EVENT, loadEffectiveView);
window.addEventListener(PLATFORM_VIEW_CHANGED_EVENT, reloadNormalView);
return () => {
cancelled = true;
window.removeEventListener(PLATFORM_VIEW_CHANGED_EVENT, loadEffectiveView);
window.removeEventListener(PLATFORM_VIEW_CHANGED_EVENT, reloadNormalView);
};
}, [
auth?.user?.id,
@@ -96,6 +108,36 @@ export default function App() {
viewsRuntime
]);
useEffect(() => {
if (!auth || !viewsRuntime) {
setWorkflowViewProjection(null);
return;
}
const currentAuth = auth;
setWorkflowViewProjection(loadStoredWorkflowView(currentAuth));
function applyWorkflowView(event: Event) {
const detail = (event as CustomEvent<WorkflowViewChangedEventDetail>).detail;
const projection = detail?.projection ?? null;
setWorkflowViewProjection(projection);
storeWorkflowView(currentAuth, projection, detail?.contextId);
}
window.addEventListener(
PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT,
applyWorkflowView
);
return () => {
window.removeEventListener(
PLATFORM_WORKFLOW_VIEW_CHANGED_EVENT,
applyWorkflowView
);
};
}, [
auth?.user?.id,
auth?.active_tenant?.id,
auth?.tenant.id,
viewsRuntime
]);
function updateSettings(next: ApiSettings) {
setSettings(next);
saveApiSettings(next);
@@ -662,3 +704,65 @@ function mergeWebModules(localModules: PlatformWebModule[], remoteModules: Platf
})];
}
const WORKFLOW_VIEW_SESSION_TTL_MS = 12 * 60 * 60 * 1000;
type StoredWorkflowView = {
projection: EffectiveViewProjection;
contextId?: string | null;
expiresAt: number;
};
function workflowViewStorageKey(auth: AuthInfo): string {
const tenant = auth.active_tenant ?? auth.tenant;
return `govoplan:workflow-view:${tenant.id}:${auth.user.account_id}`;
}
function loadStoredWorkflowView(
auth: AuthInfo
): EffectiveViewProjection | null {
try {
const key = workflowViewStorageKey(auth);
const raw = window.sessionStorage.getItem(key);
if (!raw) return null;
const stored = JSON.parse(raw) as Partial<StoredWorkflowView>;
if (
typeof stored.expiresAt !== "number"
|| stored.expiresAt <= Date.now()
|| !stored.projection
|| !Array.isArray(stored.projection.visibleSurfaceIds)
) {
window.sessionStorage.removeItem(key);
return null;
}
return stored.projection;
} catch {
return null;
}
}
function storeWorkflowView(
auth: AuthInfo,
projection: EffectiveViewProjection | null,
contextId?: string | null
): void {
try {
const key = workflowViewStorageKey(auth);
if (!projection) {
window.sessionStorage.removeItem(key);
return;
}
const stored: StoredWorkflowView = {
projection,
contextId,
expiresAt: Date.now() + WORKFLOW_VIEW_SESSION_TTL_MS
};
window.sessionStorage.setItem(key, JSON.stringify(stored));
} catch {
// Session persistence is optional; the in-memory projection still applies.
}
}
function clearStoredWorkflowView(auth: AuthInfo): void {
storeWorkflowView(auth, null);
}