chore: consolidate platform split checks
This commit is contained in:
@@ -1,34 +1,46 @@
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
|
||||
import { fetchMe } from "./api/auth";
|
||||
import { fetchMe, updateProfile } from "./api/auth";
|
||||
import { fetchPlatformModules, fetchPlatformStatus } from "./api/platform";
|
||||
import { AUTH_REQUIRED_EVENT, loadApiSettings, saveApiSettings, type AuthRequiredEventDetail } from "./api/client";
|
||||
import type { ApiSettings, AuthInfo, LoginResponse, PlatformModuleInfo, PlatformWebModule } from "./types";
|
||||
import type { ApiSettings, AuthInfo, AuthTenant, AuthTenantMembership, AuthUser, LoginResponse, PlatformModuleInfo, PlatformWebModule, UserUiPreferences } from "./types";
|
||||
import AppShell from "./layout/AppShell";
|
||||
import PublicLandingPage from "./features/auth/PublicLandingPage";
|
||||
import LoginModal from "./features/auth/LoginModal";
|
||||
import { PermissionBoundary } from "./components/AccessBoundary";
|
||||
import { firstAccessibleRoute, loadRemoteWebModules, navItemsForModules, resolveInstalledWebModules, routeContributionsForModules } from "./platform/modules";
|
||||
import { firstAccessibleRoute, loadRemoteWebModules, moduleInstalled, navItemsForModules, resolveInstalledWebModules, routeContributionsForModules } from "./platform/modules";
|
||||
import { PlatformModulesProvider } from "./platform/ModuleContext";
|
||||
import { PLATFORM_MODULES_CHANGED_EVENT } from "./platform/moduleEvents";
|
||||
import { UnsavedChangesProvider } from "./components/UnsavedChangesGuard";
|
||||
import { PlatformLanguageProvider, type PlatformLanguage } from "./i18n/LanguageContext";
|
||||
|
||||
const DashboardPage = lazy(() => import("./features/dashboard/DashboardPage"));
|
||||
const SettingsPage = lazy(() => import("./features/settings/SettingsPage"));
|
||||
|
||||
const DEFAULT_UI_PREFERENCES: UserUiPreferences = {
|
||||
compact_tables: false,
|
||||
show_inline_help_hints: true,
|
||||
reduce_motion: false,
|
||||
sticky_section_sidebars: true,
|
||||
theme: "system"
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [settings, setSettings] = useState<ApiSettings>(() => loadApiSettings());
|
||||
const [auth, setAuth] = useState<AuthInfo | null>(null);
|
||||
const [checkingSession, setCheckingSession] = useState(true);
|
||||
const [platformModules, setPlatformModules] = useState<PlatformModuleInfo[] | null>(null);
|
||||
const [remoteWebModules, setRemoteWebModules] = useState<PlatformWebModule[]>([]);
|
||||
const [maintenanceMode, setMaintenanceMode] = useState<{ enabled: boolean; message?: string | null }>({ enabled: false, message: null });
|
||||
const [maintenanceMode, setMaintenanceMode] = useState<{enabled: boolean;message?: string | null;}>({ enabled: false, message: null });
|
||||
const [systemLanguages, setSystemLanguages] = useState<{available: PlatformLanguage[];enabled: string[];defaultLanguage: string;} | null>(null);
|
||||
const [reloginMessage, setReloginMessage] = useState("");
|
||||
|
||||
const localWebModules = useMemo(() => resolveInstalledWebModules(platformModules), [platformModules]);
|
||||
const webModules = useMemo(() => mergeWebModules(localWebModules, remoteWebModules), [localWebModules, remoteWebModules]);
|
||||
const navItems = useMemo(() => navItemsForModules(webModules), [webModules]);
|
||||
const moduleRoutes = useMemo(() => routeContributionsForModules(webModules), [webModules]);
|
||||
const moduleTranslations = useMemo(() => webModules.map((module) => module.translations).filter(Boolean), [webModules]);
|
||||
const dashboardModuleInstalled = useMemo(() => moduleInstalled("dashboard", webModules), [webModules]);
|
||||
|
||||
function updateSettings(next: ApiSettings) {
|
||||
setSettings(next);
|
||||
@@ -37,7 +49,7 @@ export default function App() {
|
||||
|
||||
function updateAuth(next: AuthInfo | null, accessToken?: string) {
|
||||
const nextSettings = accessToken !== undefined ? { ...settings, accessToken } : settings;
|
||||
setAuth(next);
|
||||
setAuth(next ? normalizeAuthInfo(next) : null);
|
||||
if (accessToken !== undefined) {
|
||||
setSettings(nextSettings);
|
||||
saveApiSettings(nextSettings);
|
||||
@@ -45,16 +57,7 @@ export default function App() {
|
||||
}
|
||||
|
||||
function authFromLoginResponse(response: LoginResponse): AuthInfo {
|
||||
const active = response.active_tenant ?? response.tenant;
|
||||
return {
|
||||
user: response.user,
|
||||
tenant: active,
|
||||
active_tenant: active,
|
||||
tenants: response.tenants ?? [active],
|
||||
scopes: response.scopes,
|
||||
roles: response.roles,
|
||||
groups: response.groups
|
||||
};
|
||||
return normalizeAuthInfo(response);
|
||||
}
|
||||
|
||||
function handlePublicLogin(response: LoginResponse) {
|
||||
@@ -70,7 +73,7 @@ export default function App() {
|
||||
function handleAuthRequired(event: Event) {
|
||||
if (!auth) return;
|
||||
const detail = (event as CustomEvent<AuthRequiredEventDetail>).detail;
|
||||
setReloginMessage(detail?.message || "Your session has expired. Sign in again to continue.");
|
||||
setReloginMessage(detail?.message || "i18n:govoplan-core.your_session_has_expired_sign_in_again_to_contin.4cc6a1b3");
|
||||
}
|
||||
|
||||
window.addEventListener(AUTH_REQUIRED_EVENT, handleAuthRequired);
|
||||
@@ -79,35 +82,57 @@ export default function App() {
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchPlatformStatus(settings)
|
||||
.then((response) => {
|
||||
if (!cancelled) setMaintenanceMode(response.maintenance_mode);
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
async function refreshPlatformStatus() {
|
||||
try {
|
||||
const response = await fetchPlatformStatus(settings);
|
||||
if (cancelled) return;
|
||||
setMaintenanceMode(response.maintenance_mode);
|
||||
if (response.i18n) {
|
||||
setSystemLanguages({
|
||||
available: response.i18n.available_languages.map((item) => ({
|
||||
code: item.code,
|
||||
label: item.label,
|
||||
nativeLabel: item.native_label ?? undefined
|
||||
})),
|
||||
enabled: response.i18n.enabled_languages,
|
||||
defaultLanguage: response.i18n.default_language
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setMaintenanceMode({ enabled: false, message: null });
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}
|
||||
}
|
||||
|
||||
void refreshPlatformStatus();
|
||||
const interval = window.setInterval(() => {void refreshPlatformStatus();}, 30_000);
|
||||
window.addEventListener("focus", refreshPlatformStatus);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearInterval(interval);
|
||||
window.removeEventListener("focus", refreshPlatformStatus);
|
||||
};
|
||||
}, [settings.apiBaseUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setCheckingSession(true);
|
||||
fetchMe(settings)
|
||||
.then((me) => { if (!cancelled) setAuth(me); })
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
const cleared = { ...settings, accessToken: "" };
|
||||
setSettings(cleared);
|
||||
saveApiSettings(cleared);
|
||||
setAuth(null);
|
||||
setPlatformModules(null);
|
||||
setRemoteWebModules([]);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setCheckingSession(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
fetchMe(settings).
|
||||
then((me) => {if (!cancelled) setAuth(normalizeAuthInfo(me));}).
|
||||
catch(() => {
|
||||
if (!cancelled) {
|
||||
const cleared = { ...settings, accessToken: "" };
|
||||
setSettings(cleared);
|
||||
saveApiSettings(cleared);
|
||||
setAuth(null);
|
||||
setPlatformModules(null);
|
||||
setRemoteWebModules([]);
|
||||
}
|
||||
}).
|
||||
finally(() => {
|
||||
if (!cancelled) setCheckingSession(false);
|
||||
});
|
||||
return () => {cancelled = true;};
|
||||
}, [settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -119,10 +144,10 @@ export default function App() {
|
||||
function loadModules() {
|
||||
inFlight = true;
|
||||
lastRefreshAt = Date.now();
|
||||
return fetchPlatformModules(settings)
|
||||
.then((response) => { if (!cancelled) setPlatformModules(response.modules); })
|
||||
.catch(() => { if (!cancelled) setPlatformModules(null); })
|
||||
.finally(() => { inFlight = false; });
|
||||
return fetchPlatformModules(settings).
|
||||
then((response) => {if (!cancelled) setPlatformModules(response.modules);}).
|
||||
catch(() => {if (!cancelled) setPlatformModules(null);}).
|
||||
finally(() => {inFlight = false;});
|
||||
}
|
||||
|
||||
void loadModules();
|
||||
@@ -146,19 +171,39 @@ export default function App() {
|
||||
window.removeEventListener("focus", refreshVisibleModules);
|
||||
document.removeEventListener("visibilitychange", refreshVisibleModules);
|
||||
};
|
||||
}, [auth?.user.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]);
|
||||
}, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
useEffect(() => {
|
||||
const preferences = auth?.user.ui_preferences ?? DEFAULT_UI_PREFERENCES;
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle("ui-compact-tables", preferences.compact_tables);
|
||||
root.classList.toggle("ui-hide-help-hints", !preferences.show_inline_help_hints);
|
||||
root.classList.toggle("ui-reduce-motion", preferences.reduce_motion);
|
||||
root.classList.toggle("ui-no-sticky-section-sidebars", !preferences.sticky_section_sidebars);
|
||||
if (preferences.theme === "system") {
|
||||
delete root.dataset.theme;
|
||||
} else {
|
||||
root.dataset.theme = preferences.theme;
|
||||
}
|
||||
}, [
|
||||
auth?.user.ui_preferences?.compact_tables,
|
||||
auth?.user.ui_preferences?.show_inline_help_hints,
|
||||
auth?.user.ui_preferences?.reduce_motion,
|
||||
auth?.user.ui_preferences?.sticky_section_sidebars,
|
||||
auth?.user.ui_preferences?.theme
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
if (!auth || !platformModules?.length) {
|
||||
setRemoteWebModules([]);
|
||||
return () => { cancelled = true; };
|
||||
return () => {cancelled = true;};
|
||||
}
|
||||
loadRemoteWebModules(platformModules, localWebModules)
|
||||
.then((modules) => { if (!cancelled) setRemoteWebModules(modules); })
|
||||
.catch(() => { if (!cancelled) setRemoteWebModules([]); });
|
||||
return () => { cancelled = true; };
|
||||
}, [auth?.user.id, auth?.active_tenant?.id, auth?.tenant.id, platformModules, localWebModules]);
|
||||
loadRemoteWebModules(platformModules, localWebModules).
|
||||
then((modules) => {if (!cancelled) setRemoteWebModules(modules);}).
|
||||
catch(() => {if (!cancelled) setRemoteWebModules([]);});
|
||||
return () => {cancelled = true;};
|
||||
}, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, platformModules, localWebModules]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!auth) return;
|
||||
@@ -174,12 +219,12 @@ export default function App() {
|
||||
inFlight = true;
|
||||
lastRefreshAt = now;
|
||||
try {
|
||||
setAuth(await fetchMe(settings));
|
||||
setAuth(normalizeAuthInfo(await fetchMe(settings)));
|
||||
} catch {
|
||||
|
||||
|
||||
// A background refresh must not log the user out on a transient network error.
|
||||
} finally {
|
||||
inFlight = false;
|
||||
}
|
||||
} finally {inFlight = false;}
|
||||
}
|
||||
|
||||
window.addEventListener("focus", refreshVisibleSession);
|
||||
@@ -188,87 +233,189 @@ export default function App() {
|
||||
window.removeEventListener("focus", refreshVisibleSession);
|
||||
document.removeEventListener("visibilitychange", refreshVisibleSession);
|
||||
};
|
||||
}, [auth?.user.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]);
|
||||
}, [auth?.user?.id, auth?.active_tenant?.id, auth?.tenant.id, settings.apiBaseUrl, settings.apiKey]);
|
||||
|
||||
if (checkingSession) {
|
||||
return (
|
||||
<PlatformLanguageProvider systemAvailableLanguages={systemLanguages?.available} systemEnabledLanguageCodes={systemLanguages?.enabled} defaultLanguage={systemLanguages?.defaultLanguage} moduleTranslations={moduleTranslations}>
|
||||
<PlatformModulesProvider modules={webModules}>
|
||||
<UnsavedChangesProvider>
|
||||
<AppShell settings={settings} auth={null} onSettingsChange={updateSettings} onAuthChange={updateAuth} publicMode navItems={navItems}>
|
||||
<AppShell settings={settings} auth={null} onSettingsChange={updateSettings} onAuthChange={updateAuth} publicMode navItems={navItems} maintenanceMode={maintenanceMode}>
|
||||
<div className="public-landing">
|
||||
<section className="public-card">
|
||||
<div className="public-kicker">GovOPlaN</div>
|
||||
<h1>Checking session...</h1>
|
||||
<p>Please wait while the local session is verified.</p>
|
||||
<div className="public-kicker">i18n:govoplan-core.govoplan.a84c0a85</div>
|
||||
<h1>i18n:govoplan-core.checking_session.e7d81968</h1>
|
||||
<p>i18n:govoplan-core.please_wait_while_the_local_session_is_verified.1dbd8fd3</p>
|
||||
</section>
|
||||
</div>
|
||||
</AppShell>
|
||||
</UnsavedChangesProvider>
|
||||
</PlatformModulesProvider>
|
||||
);
|
||||
</PlatformLanguageProvider>);
|
||||
|
||||
}
|
||||
|
||||
if (!auth) {
|
||||
return (
|
||||
<PlatformLanguageProvider systemAvailableLanguages={systemLanguages?.available} systemEnabledLanguageCodes={systemLanguages?.enabled} defaultLanguage={systemLanguages?.defaultLanguage} moduleTranslations={moduleTranslations}>
|
||||
<PlatformModulesProvider modules={webModules}>
|
||||
<UnsavedChangesProvider>
|
||||
<AppShell settings={settings} auth={null} onSettingsChange={updateSettings} onAuthChange={updateAuth} publicMode navItems={navItems}>
|
||||
<AppShell settings={settings} auth={null} onSettingsChange={updateSettings} onAuthChange={updateAuth} publicMode navItems={navItems} maintenanceMode={maintenanceMode}>
|
||||
<PublicLandingPage settings={settings} maintenanceMode={maintenanceMode} onLogin={handlePublicLogin} />
|
||||
</AppShell>
|
||||
</UnsavedChangesProvider>
|
||||
</PlatformModulesProvider>
|
||||
);
|
||||
</PlatformLanguageProvider>);
|
||||
|
||||
}
|
||||
|
||||
const defaultRoute = firstAccessibleRoute(auth, webModules);
|
||||
const authAvailableLanguages = auth.available_languages?.map((item) => ({
|
||||
code: item.code,
|
||||
label: item.label,
|
||||
nativeLabel: item.native_label ?? undefined
|
||||
}));
|
||||
|
||||
function persistLanguagePreference(code: string) {
|
||||
void updateProfile(settings, { preferred_language: code }).then((next) => updateAuth(next)).catch(() => undefined);
|
||||
}
|
||||
|
||||
return (
|
||||
<PlatformLanguageProvider
|
||||
systemAvailableLanguages={authAvailableLanguages ?? systemLanguages?.available}
|
||||
systemEnabledLanguageCodes={auth.enabled_language_codes ?? systemLanguages?.enabled}
|
||||
userEnabledLanguageCodes={auth.user?.enabled_language_codes}
|
||||
defaultLanguage={auth.default_language ?? systemLanguages?.defaultLanguage ?? (auth.active_tenant ?? auth.tenant).default_locale}
|
||||
preferredLanguageCode={auth.user?.preferred_language ?? auth.default_language}
|
||||
onLanguageChange={persistLanguagePreference}
|
||||
moduleTranslations={moduleTranslations}>
|
||||
<PlatformModulesProvider modules={webModules}>
|
||||
<UnsavedChangesProvider>
|
||||
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems}>
|
||||
<Suspense fallback={<div className="content-pad"><p className="muted">Loading module...</p></div>}>
|
||||
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} maintenanceMode={maintenanceMode}>
|
||||
<Suspense fallback={<div className="content-pad"><p className="muted">i18n:govoplan-core.loading_module.50161f3c</p></div>}>
|
||||
<Routes key={(auth.active_tenant ?? auth.tenant).id}>
|
||||
<Route path="/" element={<Navigate to={defaultRoute} replace />} />
|
||||
<Route path="/dashboard" element={<DashboardPage settings={settings} />} />
|
||||
{moduleRoutes.map((route) => (
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<PermissionBoundary auth={auth} anyOf={route.anyOf} allOf={route.allOf} fallback={defaultRoute}>
|
||||
{!dashboardModuleInstalled && <Route path="/dashboard" element={<DashboardPage />} />}
|
||||
{moduleRoutes.map((route) =>
|
||||
<Route
|
||||
key={route.path}
|
||||
path={route.path}
|
||||
element={
|
||||
<PermissionBoundary auth={auth} anyOf={route.anyOf} allOf={route.allOf} fallback={defaultRoute}>
|
||||
{route.render({ settings, auth, onAuthChange: updateAuth })}
|
||||
</PermissionBoundary>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
} />
|
||||
|
||||
)}
|
||||
<Route path="/settings" element={<SettingsPage settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} />} />
|
||||
<Route path="*" element={<Navigate to={defaultRoute} replace />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
{reloginMessage && (
|
||||
<LoginModal
|
||||
settings={settings}
|
||||
title="Session expired"
|
||||
message={reloginMessage}
|
||||
onClose={() => setReloginMessage("")}
|
||||
onLogin={handleRelogin}
|
||||
/>
|
||||
)}
|
||||
{reloginMessage &&
|
||||
<LoginModal
|
||||
settings={settings}
|
||||
title="i18n:govoplan-core.session_expired.b828190e"
|
||||
message={reloginMessage}
|
||||
onClose={() => setReloginMessage("")}
|
||||
onLogin={handleRelogin} />
|
||||
|
||||
}
|
||||
</AppShell>
|
||||
</UnsavedChangesProvider>
|
||||
</PlatformModulesProvider>
|
||||
);
|
||||
</PlatformLanguageProvider>);
|
||||
|
||||
}
|
||||
|
||||
type AuthPayload = Partial<AuthInfo> & {
|
||||
principal?: AuthInfo["principal"];
|
||||
user?: Partial<AuthUser> | null;
|
||||
tenant?: AuthTenant | null;
|
||||
active_tenant?: AuthTenant | null;
|
||||
tenants?: AuthTenantMembership[] | null;
|
||||
};
|
||||
|
||||
function normalizeAuthInfo(response: AuthPayload): AuthInfo {
|
||||
const principal = response.principal ?? null;
|
||||
const activeTenant = response.active_tenant ?? response.tenant ?? response.tenants?.[0] ?? null;
|
||||
const user = normalizeAuthUser(response.user, principal);
|
||||
|
||||
if (!activeTenant) {
|
||||
throw new Error("Authentication response did not include an active tenant.");
|
||||
}
|
||||
if (!user) {
|
||||
throw new Error("Authentication response did not include a user.");
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
tenant: activeTenant,
|
||||
active_tenant: activeTenant,
|
||||
tenants: response.tenants ?? [activeTenant],
|
||||
scopes: response.scopes ?? principal?.scopes ?? [],
|
||||
roles: response.roles ?? [],
|
||||
groups: response.groups ?? [],
|
||||
principal,
|
||||
available_languages: response.available_languages ?? [],
|
||||
enabled_language_codes: response.enabled_language_codes ?? activeTenant.enabled_language_codes ?? [],
|
||||
default_language: response.default_language ?? user.preferred_language ?? activeTenant.default_locale ?? "en"
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAuthUser(user: Partial<AuthUser> | null | undefined, principal: AuthInfo["principal"]): AuthUser | null {
|
||||
if (user?.id && user.account_id) {
|
||||
return {
|
||||
id: user.id,
|
||||
account_id: user.account_id,
|
||||
email: user.email ?? principal?.email ?? "",
|
||||
display_name: user.display_name ?? principal?.display_name ?? principal?.email ?? null,
|
||||
tenant_display_name: user.tenant_display_name ?? null,
|
||||
is_tenant_admin: user.is_tenant_admin ?? false,
|
||||
password_reset_required: user.password_reset_required ?? false,
|
||||
preferred_language: user.preferred_language ?? null,
|
||||
enabled_language_codes: user.enabled_language_codes ?? [],
|
||||
ui_preferences: normalizeUiPreferences(user.ui_preferences)
|
||||
};
|
||||
}
|
||||
|
||||
if (!principal?.membership_id || !principal.account_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: principal.membership_id,
|
||||
account_id: principal.account_id,
|
||||
email: principal.email ?? "",
|
||||
display_name: principal.display_name ?? principal.email ?? null,
|
||||
tenant_display_name: principal.display_name ?? null,
|
||||
is_tenant_admin: false,
|
||||
password_reset_required: false,
|
||||
preferred_language: null,
|
||||
enabled_language_codes: [],
|
||||
ui_preferences: DEFAULT_UI_PREFERENCES
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeUiPreferences(value: Partial<UserUiPreferences> | null | undefined): UserUiPreferences {
|
||||
const theme = value?.theme === "light" || value?.theme === "dark" || value?.theme === "system" ? value.theme : "system";
|
||||
return {
|
||||
compact_tables: Boolean(value?.compact_tables ?? DEFAULT_UI_PREFERENCES.compact_tables),
|
||||
show_inline_help_hints: Boolean(value?.show_inline_help_hints ?? DEFAULT_UI_PREFERENCES.show_inline_help_hints),
|
||||
reduce_motion: Boolean(value?.reduce_motion ?? DEFAULT_UI_PREFERENCES.reduce_motion),
|
||||
sticky_section_sidebars: Boolean(value?.sticky_section_sidebars ?? DEFAULT_UI_PREFERENCES.sticky_section_sidebars),
|
||||
theme
|
||||
};
|
||||
}
|
||||
|
||||
function mergeWebModules(localModules: PlatformWebModule[], remoteModules: PlatformWebModule[]): PlatformWebModule[] {
|
||||
if (remoteModules.length === 0) return localModules;
|
||||
const seen = new Set(localModules.map((module) => module.id));
|
||||
return [
|
||||
...localModules,
|
||||
...remoteModules.filter((module) => {
|
||||
if (seen.has(module.id)) return false;
|
||||
seen.add(module.id);
|
||||
return true;
|
||||
})
|
||||
];
|
||||
...localModules,
|
||||
...remoteModules.filter((module) => {
|
||||
if (seen.has(module.id)) return false;
|
||||
seen.add(module.id);
|
||||
return true;
|
||||
})];
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user