refactor: pre serve route /

This commit is contained in:
taDuc
2026-05-27 13:58:36 +07:00
parent 4c60e2d773
commit b3d2f56797
5 changed files with 362 additions and 251 deletions
+30 -132
View File
@@ -10,7 +10,7 @@ import TimelineBar from "@/uhm/components/ui/TimelineBar";
import SelectedGeometryPanel from "@/uhm/components/editor/SelectedGeometryPanel";
import ReplayTimelineSidebar from "@/uhm/components/editor/ReplayTimelineSidebar";
import ReplayEffectsSidebar from "@/uhm/components/editor/ReplayEffectsSidebar";
import PreviewLayout from "@/uhm/components/preview/PreviewLayout";
import PreviewLayout, { type PreviewLayoutHandle } from "@/uhm/components/preview/PreviewLayout";
import WikiSidebarPanel from "@/uhm/components/wiki/WikiSidebarPanel";
import ProjectEntityRefsPanel from "@/uhm/components/editor/ProjectEntityRefsPanel";
import EntityWikiBindingsPanel from "@/uhm/components/editor/EntityWikiBindingsPanel";
@@ -81,6 +81,10 @@ import {
normalizeReplaysForCompare,
normalizeWikisForCompare,
} from "@/uhm/lib/editor/editorPageUtils";
import {
buildEntityLabelContextDraft as buildPreviewEntityLabelContextDraft,
buildSnapshotPreviewRelationIndex,
} from "@/uhm/lib/preview/relationIndex";
const CURRENT_YEAR = new Date().getUTCFullYear();
const DEFAULT_EDITOR_USER_ID = "local-editor";
@@ -99,18 +103,6 @@ type ReplayPreviewSession = {
mapViewState: ReturnType<MapHandle["getViewState"]>;
};
type PreviewRelationIndex = {
entitiesById: Record<string, Entity>;
entityGeometriesById: Record<string, FeatureCollection>;
entityWikisById: Record<string, Wiki[]>;
geometryEntityIds: Record<string, string[]>;
wikiEntityIdsById: Record<string, string[]>;
wikiEntityIdsBySlug: Record<string, string[]>;
wikiById: Record<string, Wiki>;
wikiBySlug: Record<string, Wiki>;
};
export default function Page() {
return (
<EditorStoreProvider
@@ -547,7 +539,7 @@ function EditorPageContent() {
return 420;
});
const [isLargeScreen, setIsLargeScreen] = useState(false);
const previewLayoutRef = useRef<any>(null);
const previewLayoutRef = useRef<PreviewLayoutHandle | null>(null);
// Responsive listener for preview sidebar/viewport offsets
useEffect(() => {
@@ -601,7 +593,7 @@ function EditorPageContent() {
const [previewWikiCache, setPreviewWikiCache] = useState<Record<string, Wiki>>({});
const previewRelations = useMemo(() => {
return buildPreviewRelationIndex({
return buildSnapshotPreviewRelationIndex({
draft: previewSession?.draft || EMPTY_FEATURE_COLLECTION,
entities: previewSession?.entities || [],
wikis: previewSession?.wikis || [],
@@ -635,12 +627,12 @@ function EditorPageContent() {
const activeTimelineYear = isReplayPreviewMode
? replayPreviewTimelineYear
: isViewerPreviewMode
? previewSession?.timelineYear ?? timelineDraftYear
? replayPreviewTimelineYear
: timelineDraftYear;
const activeTimelineFilterEnabled = isReplayPreviewMode
? replayPreviewTimelineFilterEnabled
: isViewerPreviewMode
? previewSession?.timelineFilterEnabled ?? timelineFilterEnabled
? replayPreviewTimelineFilterEnabled
: timelineFilterEnabled;
// Render draft is the only FeatureCollection that decides what appears on the map.
@@ -817,12 +809,30 @@ function EditorPageContent() {
const activeMapDraft = useMemo(() => {
if (isAnyPreviewMode) {
return isReplayPreviewMode
const previewDraft = isReplayPreviewMode
? replayPreviewDraft
: (previewSession?.draft || EMPTY_FEATURE_COLLECTION);
if (!activeTimelineFilterEnabled) {
return previewDraft;
}
const safeYear = clampYearToFixedRange(Math.trunc(activeTimelineYear));
return {
...previewDraft,
features: previewDraft.features.filter((feature) =>
isFeatureVisibleAtYear(feature, safeYear)
),
};
}
return mapRenderDraft;
}, [isAnyPreviewMode, isReplayPreviewMode, replayPreviewDraft, previewSession?.draft, mapRenderDraft]);
}, [
activeTimelineFilterEnabled,
activeTimelineYear,
isAnyPreviewMode,
isReplayPreviewMode,
mapRenderDraft,
previewSession?.draft,
replayPreviewDraft,
]);
const localFeatureIds = useMemo(() => {
const ids = new Set<string | number>();
@@ -2420,7 +2430,7 @@ function EditorPageContent() {
const entitiesForLabel = isAnyPreviewMode
? previewSession?.entities || []
: entities;
return buildEntityLabelContextDraft(labelContextBaseDraft, entitiesForLabel);
return buildPreviewEntityLabelContextDraft(labelContextBaseDraft, entitiesForLabel);
}, [entities, isAnyPreviewMode, labelContextBaseDraft, previewSession?.entities]);
if (blockedPendingSubmissionId) {
@@ -2909,118 +2919,6 @@ function readImageAspectRatio(url: string): Promise<number> {
});
}
function buildPreviewRelationIndex(options: {
draft: FeatureCollection;
entities: Entity[];
wikis: WikiSnapshot[];
entityWikiLinks: EntityWikiLinkSnapshot[];
wikiCache: Record<string, Wiki>;
projectId: string;
}): PreviewRelationIndex {
const next: PreviewRelationIndex = {
entitiesById: {},
entityGeometriesById: {},
entityWikisById: {},
geometryEntityIds: {},
wikiEntityIdsById: {},
wikiEntityIdsBySlug: {},
wikiById: {},
wikiBySlug: {},
};
for (const entity of options.entities || []) {
const id = String(entity?.id || "").trim();
if (!id) continue;
next.entitiesById[id] = entity;
}
for (const wikiSnapshot of options.wikis || []) {
if (!wikiSnapshot || wikiSnapshot.operation === "delete") continue;
const wiki = snapshotWikiToWiki(wikiSnapshot, options.wikiCache, options.projectId);
if (!wiki?.id) continue;
next.wikiById[wiki.id] = wiki;
const slug = String(wiki.slug || "").trim();
if (slug) next.wikiBySlug[slug] = wiki;
}
for (const feature of options.draft.features || []) {
const geometryId = String(feature.properties.id);
for (const entityId of normalizeFeatureEntityIds(feature)) {
if (!next.entitiesById[entityId]) {
next.entitiesById[entityId] = { id: entityId, name: entityId };
}
pushUniqueString(next.geometryEntityIds, geometryId, entityId);
if (!next.entityGeometriesById[entityId]) {
next.entityGeometriesById[entityId] = { type: "FeatureCollection", features: [] };
}
if (!next.entityGeometriesById[entityId].features.some((item) => String(item.properties.id) === geometryId)) {
next.entityGeometriesById[entityId].features.push(feature);
}
}
}
for (const link of options.entityWikiLinks || []) {
if (!link || link.operation === "delete") continue;
const entityId = String(link.entity_id || "").trim();
const wikiId = String(link.wiki_id || "").trim();
const entity = next.entitiesById[entityId] || null;
const wiki = next.wikiById[wikiId] || null;
if (!entity || !wiki) continue;
if (!next.entityWikisById[entityId]) next.entityWikisById[entityId] = [];
if (!next.entityWikisById[entityId].some((item) => item.id === wiki.id)) {
next.entityWikisById[entityId].push(wiki);
}
pushUniqueString(next.wikiEntityIdsById, wiki.id, entityId);
const slug = String(wiki.slug || "").trim();
if (slug) pushUniqueString(next.wikiEntityIdsBySlug, slug, entityId);
}
normalizeRelationArrays(next.geometryEntityIds);
normalizeRelationArrays(next.wikiEntityIdsById);
normalizeRelationArrays(next.wikiEntityIdsBySlug);
return next;
}
function snapshotWikiToWiki(snapshot: WikiSnapshot, wikiCache: Record<string, Wiki>, projectId: string): Wiki {
if (typeof snapshot.doc === "string") {
return {
id: snapshot.id,
project_id: projectId,
title: snapshot.title,
slug: snapshot.slug ?? null,
content: snapshot.doc || "",
};
}
return wikiCache[snapshot.id] || {
id: snapshot.id,
project_id: projectId,
title: snapshot.title,
slug: snapshot.slug ?? null,
content: "",
};
}
function pushUniqueString(target: Record<string, string[]>, key: string, value: string) {
if (!target[key]) {
target[key] = [value];
return;
}
if (!target[key].includes(value)) {
target[key].push(value);
}
}
function normalizeRelationArrays(target: Record<string, string[]>) {
for (const key of Object.keys(target)) {
target[key] = Array.from(new Set(target[key]));
}
}
function isTypingTarget(target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
const tagName = target.tagName.toLowerCase();