feat: consolidate shared UI and harden browser authority for release
This commit is contained in:
+30
-7
@@ -2,7 +2,7 @@ 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";
|
||||
import { AUTH_REQUIRED_EVENT, isApiError, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client";
|
||||
import { AUTH_REQUIRED_EVENT, apiSettingsForAuthUpdate, clearApiReadCache, isApiError, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client";
|
||||
import type { ApiSettings, AuthInfo, AuthSessionInfo, AuthUpdate, AuthUser, EffectiveViewProjection, LoginResponse, PlatformModuleInfo, PlatformPublicModuleInfo, PlatformWebModule, UserUiPalette, UserUiPreferences, ViewsRuntimeUiCapability } from "./types";
|
||||
import AppShell from "./layout/AppShell";
|
||||
import PublicLandingPage from "./features/auth/PublicLandingPage";
|
||||
@@ -20,13 +20,16 @@ import {
|
||||
type WorkflowViewChangedEventDetail
|
||||
} from "./platform/views";
|
||||
import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents";
|
||||
import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard";
|
||||
import { UnsavedChangesProvider, useUnsavedChanges } from "./components/UnsavedChangesGuard";
|
||||
import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext";
|
||||
import ViewSurfaceRouteBoundary from "./components/ViewSurfaceRouteBoundary";
|
||||
import ModuleLoadBoundary from "./components/ModuleLoadBoundary";
|
||||
import DismissibleAlert from "./components/DismissibleAlert";
|
||||
import Button from "./components/Button";
|
||||
import { i18nMessage } from "./i18n/LanguageContext";
|
||||
import { DocumentationHelpProvider } from "./components/help/DocumentationHelpLink";
|
||||
import { hasAnyScope } from "./utils/permissions";
|
||||
import { applyAppearanceOverrides } from "./components/AppearanceOverridesEditor";
|
||||
import { applyAppearanceOverrides } from "./components/appearanceOverrides";
|
||||
|
||||
const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage"));
|
||||
const SettingsPage = lazy(() => import("./features/settings/SettingsPage"));
|
||||
@@ -54,6 +57,7 @@ export default function App() {
|
||||
const [remoteWebModules, setRemoteWebModules] = useState<PlatformWebModule[]>([]);
|
||||
const [remotePublicWebModules, setRemotePublicWebModules] = useState<PlatformWebModule[]>([]);
|
||||
const [webModulesLoading, setWebModulesLoading] = useState(true);
|
||||
const [webModuleLoadFailures, setWebModuleLoadFailures] = useState<string[]>([]);
|
||||
const [maintenanceMode, setMaintenanceMode] = useState<{enabled: boolean;message?: string | null;}>({ enabled: false, message: null });
|
||||
const [backendReachable, setBackendReachable] = useState(true);
|
||||
const [systemLanguages, setSystemLanguages] = useState<{available: PlatformLanguage[];enabled: string[];defaultLanguage: string;} | null>(null);
|
||||
@@ -172,9 +176,10 @@ export default function App() {
|
||||
}
|
||||
|
||||
function updateAuth(next: AuthUpdate | null, accessToken?: string) {
|
||||
const nextSettings = accessToken !== undefined ? { ...settings, accessToken } : settings;
|
||||
clearApiReadCache();
|
||||
const nextSettings = apiSettingsForAuthUpdate(settings, next, accessToken);
|
||||
setAuth((current) => next ? normalizeAuthInfo(mergeAuthPayload(current, next)) : null);
|
||||
if (accessToken !== undefined) {
|
||||
if (nextSettings !== settings) {
|
||||
setSettings(nextSettings);
|
||||
saveApiSettings(nextSettings);
|
||||
}
|
||||
@@ -328,6 +333,7 @@ export default function App() {
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setWebModuleLoadFailures([]);
|
||||
if (!auth) {
|
||||
setLocalWebModules([]);
|
||||
setRemoteWebModules([]);
|
||||
@@ -345,12 +351,18 @@ export default function App() {
|
||||
}
|
||||
|
||||
async function loadWebModules() {
|
||||
const local = await loadInstalledWebModules(platformModules);
|
||||
const failedIds: string[] = [];
|
||||
const local = await loadInstalledWebModules(platformModules, (moduleId) => failedIds.push(moduleId));
|
||||
if (cancelled) return;
|
||||
setLocalWebModules(local);
|
||||
setWebModuleLoadFailures(failedIds);
|
||||
setWebModulesLoading(false);
|
||||
const remote = await loadRemoteWebModules(platformModules, local);
|
||||
if (!cancelled) setRemoteWebModules(remote);
|
||||
if (!cancelled) {
|
||||
setRemoteWebModules(remote);
|
||||
const recovered = new Set(remote.map((module) => module.id));
|
||||
setWebModuleLoadFailures(failedIds.filter((moduleId) => !recovered.has(moduleId)));
|
||||
}
|
||||
}
|
||||
|
||||
void loadWebModules().catch((error) => {
|
||||
@@ -451,8 +463,10 @@ export default function App() {
|
||||
setBackendReachable(true);
|
||||
const shellRefreshDue = now - lastShellRefreshAt >= 60_000;
|
||||
if (!sessionMatchesAuth(sessionInfo, currentAuth) || shellRefreshDue) {
|
||||
clearApiReadCache();
|
||||
const shellAuth = await fetchShellAuth(settings);
|
||||
if (cancelled) return;
|
||||
clearApiReadCache();
|
||||
lastShellRefreshAt = Date.now();
|
||||
setAuth((current) => current && sessionMatchesAuth(sessionInfo, current)
|
||||
? normalizeAuthInfo(mergeAuthPayload(current, shellAuth))
|
||||
@@ -555,6 +569,7 @@ export default function App() {
|
||||
<PlatformActiveObjectProvider>
|
||||
<UnsavedChangesProvider>
|
||||
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} allToolItems={allToolItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
|
||||
{webModuleLoadFailures.length > 0 && <WebModuleLoadFailureNotice moduleIds={webModuleLoadFailures} />}
|
||||
<ModuleLoadBoundary resetKey={`${location.pathname}:${temporalRevision}`} loading={webModulesLoading}>
|
||||
<Routes key={`${(auth.active_tenant ?? auth.tenant).id}:${temporalRevision}`}>
|
||||
<Route path="/" element={<Navigate to={defaultRoute} replace />} />
|
||||
@@ -607,6 +622,14 @@ export default function App() {
|
||||
|
||||
}
|
||||
|
||||
function WebModuleLoadFailureNotice({ moduleIds }: { moduleIds: string[] }) {
|
||||
const { requestNavigation } = useUnsavedChanges();
|
||||
return <DismissibleAlert tone="warning" dismissible={false} floating>
|
||||
<p>{i18nMessage("i18n:govoplan-core.optional_module_load_failed", { value0: moduleIds.join(", ") })}</p>
|
||||
<Button onClick={() => requestNavigation(() => window.location.reload())}>i18n:govoplan-core.reload.cce71553</Button>
|
||||
</DismissibleAlert>;
|
||||
}
|
||||
|
||||
type AuthPayload = AuthUpdate;
|
||||
|
||||
function mergeAuthPayload(current: AuthInfo | null, next: AuthPayload): AuthPayload {
|
||||
|
||||
Reference in New Issue
Block a user