diff --git a/seo_mode/seo_mode/app/src/App.tsx b/seo_mode/seo_mode/app/src/App.tsx index 86639cf..c07bff6 100644 --- a/seo_mode/seo_mode/app/src/App.tsx +++ b/seo_mode/seo_mode/app/src/App.tsx @@ -2,6 +2,7 @@ import { Activity, Archive, CheckCircle2, + ChevronDown, CircleAlert, Copy, Database, @@ -220,6 +221,10 @@ function getScanStatusLabel(status: string) { } function getPageTitle(page: ProjectScanSummary["pages"][number]) { + if (page.scope === "template_block" && page.structureLabel) { + return page.structureLabel; + } + if (isHomePage(page)) { return page.title || "Главная страница"; } @@ -263,6 +268,30 @@ function isIndexableScanPage(page: ProjectScanSummary["pages"][number]) { return page.scope === "indexable_page"; } +function isWorkspaceScopePage(page: ProjectScanSummary["pages"][number]) { + if (!page.selected) { + return false; + } + + if (page.previewKind === "none" && !page.previewSourcePath && !page.previewTargetSelector) { + return false; + } + + return true; +} + +function getWorkspaceScopeOrder(page: ProjectScanSummary["pages"][number], originalIndex: number) { + if (isHomePage(page)) { + return -1; + } + + if (page.structureOrder != null) { + return page.structureOrder; + } + + return 1000 + originalIndex; +} + function getScanPageScopeLabel(scope: ProjectScanSummary["pages"][number]["scope"]) { const labels = { admin_page: "админка", @@ -903,7 +932,11 @@ function getSelectedPagesCount(scan: ProjectScanSummary) { } function getSelectedCoveragePages(scan: ProjectScanSummary) { - return scan.pages.filter((page) => page.selected && isIndexableScanPage(page)); + return scan.pages + .map((page, index) => ({ index, page })) + .filter(({ page }) => isWorkspaceScopePage(page)) + .sort((left, right) => getWorkspaceScopeOrder(left.page, left.index) - getWorkspaceScopeOrder(right.page, right.index)) + .map(({ page }) => page); } function getWorkspaceKey(projectId: string, pageId: string) { @@ -930,6 +963,7 @@ type WorkspaceTarget = { }; type WorkspacePreviewMode = "desktop" | "mobile"; +type WorkspaceDrawerPanel = "fields" | "issues" | "scope"; const WORKSPACE_PREVIEW_MODES: Array<{ id: WorkspacePreviewMode; label: string }> = [ { id: "desktop", label: "Desktop" }, @@ -1369,6 +1403,9 @@ export function App() { const [activeWorkspaceSectionIds, setActiveWorkspaceSectionIds] = useState>({}); const [activeWorkspaceTargetIds, setActiveWorkspaceTargetIds] = useState>({}); const [workspacePreviewMarkers, setWorkspacePreviewMarkers] = useState>({}); + const [workspaceDrawers, setWorkspaceDrawers] = useState>({}); + const [workspacePageMenus, setWorkspacePageMenus] = useState>({}); + const [workspacePreviewPageIds, setWorkspacePreviewPageIds] = useState>({}); const [pageWorkspaces, setPageWorkspaces] = useState>({}); const [workspaceDraftTexts, setWorkspaceDraftTexts] = useState>({}); const [workspaceSectionTexts, setWorkspaceSectionTexts] = useState>({}); @@ -1557,27 +1594,51 @@ export function App() { }, []); useEffect(() => { - const timers: number[] = []; - - for (const [workspaceKey, workspace] of Object.entries(pageWorkspaces)) { - const [projectId = "", pageId = ""] = workspaceKey.split(":"); - const scanSummary = scanSummaries[projectId]; - const issues = getPageIssues(scanSummary, pageId); - const targets = buildWorkspaceTargets(workspace, issues); - - if (targets.length === 0) { - continue; - } - - timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 180)); - timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 700)); - timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 1600)); + if (activeStageIndex !== 2) { + return; } + const scrollToWorkspace = () => { + document.querySelector(".page-workspace-panel")?.scrollIntoView({ behavior: "smooth", block: "start" }); + }; + const firstTimer = window.setTimeout(scrollToWorkspace, 120); + const secondTimer = window.setTimeout(scrollToWorkspace, 650); + + return () => { + window.clearTimeout(firstTimer); + window.clearTimeout(secondTimer); + }; + }, [activeProjectId, activeStageIndex]); + + useEffect(() => { + const timers: number[] = []; + const projectId = activeProjectId ?? ""; + const scanSummary = projectId ? scanSummaries[projectId] : null; + + if (activeStageIndex !== 2 || !projectId || !scanSummary) { + return () => undefined; + } + + const scopePages = getSelectedCoveragePages(scanSummary); + const activePageId = activeWorkspacePageIds[projectId] ?? scopePages[0]?.pageId ?? ""; + const workspaceKey = activePageId ? getWorkspaceKey(projectId, activePageId) : ""; + const workspace = workspaceKey ? pageWorkspaces[workspaceKey] : null; + + if (!workspaceKey || !workspace) { + return () => undefined; + } + + const issues = getPageIssues(scanSummary, activePageId); + const targets = buildWorkspaceTargets(workspace, issues); + + timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 80)); + timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 360)); + timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 1000)); + return () => { timers.forEach((timer) => window.clearTimeout(timer)); }; - }, [activeStageIndex, activeWorkspacePageIds, pageWorkspaces, scanSummaries, workspacePreviewModes]); + }, [activeProjectId, activeStageIndex, activeWorkspacePageIds, pageWorkspaces, scanSummaries, workspacePreviewModes]); function scrollWorkspaceFieldIntoView(workspaceKey: string, targetId: string, attempt = 0) { window.requestAnimationFrame(() => { @@ -1599,6 +1660,46 @@ export function App() { const data = event.data; if (!data || typeof data !== "object" || data.type !== "seo-mode:preview-markers") { + if (data?.type === "seo-mode:active-scope-page") { + const sourceWorkspaceKey = typeof data.workspaceKey === "string" ? data.workspaceKey : ""; + const pageId = typeof data.pageId === "string" ? data.pageId : ""; + const [projectId = ""] = sourceWorkspaceKey.split(":"); + + if (!projectId || !pageId) { + return; + } + + const nextWorkspaceKey = getWorkspaceKey(projectId, pageId); + + if (!pageWorkspaces[nextWorkspaceKey]) { + const project = projects.find((item) => item.id === projectId); + const scanSummary = scanSummaries[projectId]; + const page = scanSummary ? getSelectedCoveragePages(scanSummary).find((item) => item.pageId === pageId) : null; + + if (project && page && busyWorkspaceKey !== nextWorkspaceKey) { + void handleOpenPageWorkspace(project, page, { + deferActiveUntilLoaded: true, + navigatePreview: false, + silent: true + }); + } + + return; + } + + setActiveWorkspacePageIds((currentPageIds) => { + if (currentPageIds[projectId] === pageId) { + return currentPageIds; + } + + return { + ...currentPageIds, + [projectId]: pageId + }; + }); + return; + } + if (data?.type !== "seo-mode:activate-target") { return; } @@ -1619,6 +1720,7 @@ export function App() { ...currentSectionIds, [workspaceKey]: sectionId || targetId })); + openWorkspaceDrawer(workspaceKey, "fields"); scrollWorkspaceFieldIntoView(workspaceKey, targetId); return; @@ -1669,7 +1771,7 @@ export function App() { return () => { window.removeEventListener("message", handlePreviewMessage); }; - }, []); + }, [busyWorkspaceKey, pageWorkspaces, projects, scanSummaries]); useEffect(() => { if (projects.length === 0) { @@ -2166,7 +2268,7 @@ export function App() { async function handleOpenPageWorkspace( project: ProjectSummary, page: ProjectScanSummary["pages"][number], - options: { silent?: boolean } = {} + options: { deferActiveUntilLoaded?: boolean; navigatePreview?: boolean; silent?: boolean } = {} ) { const workspaceKey = getWorkspaceKey(project.id, page.pageId); @@ -2178,10 +2280,19 @@ export function App() { setProjectActionProjectId(project.id); } - setActiveWorkspacePageIds((currentPageIds) => ({ - ...currentPageIds, - [project.id]: page.pageId - })); + if (options.navigatePreview !== false) { + setWorkspacePreviewPageIds((currentPageIds) => ({ + ...currentPageIds, + [project.id]: page.pageId + })); + } + + if (!options.deferActiveUntilLoaded) { + setActiveWorkspacePageIds((currentPageIds) => ({ + ...currentPageIds, + [project.id]: page.pageId + })); + } try { const result = await openPageWorkspace(project.id, page.pageId); @@ -2206,6 +2317,12 @@ export function App() { ...currentTargetIds, [workspaceKey]: currentTargetIds[workspaceKey] ?? result.workspace.sections[0]?.id ?? "" })); + if (options.deferActiveUntilLoaded) { + setActiveWorkspacePageIds((currentPageIds) => ({ + ...currentPageIds, + [project.id]: page.pageId + })); + } if (!options.silent) { setProjectActionMessage(`Рабочая зона открыта: ${getPageTitle(page)}.`); @@ -2320,11 +2437,67 @@ export function App() { ...currentSectionIds, [workspaceKey]: target.sectionId })); + openWorkspaceDrawer(workspaceKey, "fields"); scrollWorkspaceFieldIntoView(workspaceKey, target.id); } - function updateWorkspacePreviewMarkers(workspaceKey: string, targets: WorkspaceTarget[]) { + function openWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) { + setWorkspaceDrawers((currentDrawers) => { + const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? []; + + if (currentWorkspaceDrawers.includes(drawer)) { + return currentDrawers; + } + + return { + ...currentDrawers, + [workspaceKey]: [...currentWorkspaceDrawers, drawer] + }; + }); + } + + function toggleWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) { + setWorkspaceDrawers((currentDrawers) => { + const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? []; + const nextWorkspaceDrawers = currentWorkspaceDrawers.includes(drawer) + ? currentWorkspaceDrawers.filter((currentDrawer) => currentDrawer !== drawer) + : [...currentWorkspaceDrawers, drawer]; + + return { + ...currentDrawers, + [workspaceKey]: nextWorkspaceDrawers + }; + }); + } + + function closeWorkspaceDrawer(workspaceKey: string, drawer: WorkspaceDrawerPanel) { + setWorkspaceDrawers((currentDrawers) => { + const currentWorkspaceDrawers = currentDrawers[workspaceKey] ?? []; + + if (!currentWorkspaceDrawers.includes(drawer)) { + return currentDrawers; + } + + return { + ...currentDrawers, + [workspaceKey]: currentWorkspaceDrawers.filter((currentDrawer) => currentDrawer !== drawer) + }; + }); + } + + function toggleWorkspacePageMenu(workspaceKey: string) { + setWorkspacePageMenus((currentMenus) => ({ + ...currentMenus, + [workspaceKey]: !currentMenus[workspaceKey] + })); + } + + function updateWorkspacePreviewMarkers( + workspaceKey: string, + targets: WorkspaceTarget[], + scopePages: ProjectScanSummary["pages"] = [] + ) { const frameWindow = previewFrameRefs.current[workspaceKey]?.contentWindow; if (!frameWindow) { @@ -2340,6 +2513,14 @@ export function App() { { type: "seo-mode:measure-targets", workspaceKey, + activePageId: workspaceKey.split(":")[1] ?? "", + scopePages: scopePages + .filter((page) => page.previewTargetSelector) + .map((page) => ({ + pageId: page.pageId, + label: getPageTitle(page), + selector: page.previewTargetSelector + })), targets: targets .filter((target) => (target.markerSelectors.length > 0 || target.markerFallbackSelector) && target.issues.length > 0) .map((target) => ({ @@ -2358,20 +2539,29 @@ export function App() { ); } - function changeWorkspacePreviewMode(workspaceKey: string, mode: WorkspacePreviewMode, targets: WorkspaceTarget[]) { + function changeWorkspacePreviewMode( + workspaceKey: string, + mode: WorkspacePreviewMode, + targets: WorkspaceTarget[], + scopePages: ProjectScanSummary["pages"] = [] + ) { setWorkspacePreviewModes((currentModes) => ({ ...currentModes, [workspaceKey]: mode })); - window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 120); - window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 450); + window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 120); + window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 450); } - function handleWorkspacePreviewLoad(workspaceKey: string, targets: WorkspaceTarget[]) { - updateWorkspacePreviewMarkers(workspaceKey, targets); - window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 350); - window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets), 1000); + function handleWorkspacePreviewLoad( + workspaceKey: string, + targets: WorkspaceTarget[], + scopePages: ProjectScanSummary["pages"] = [] + ) { + updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages); + window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 350); + window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 1000); } function setAuditIssuesVisibleCount(scanId: string, visibleCount: number) { @@ -2429,6 +2619,11 @@ export function App() { delete nextPageIds[project.id]; return nextPageIds; }); + setWorkspacePreviewPageIds((currentPageIds) => { + const nextPageIds = { ...currentPageIds }; + delete nextPageIds[project.id]; + return nextPageIds; + }); setPageWorkspaces((currentWorkspaces) => Object.fromEntries( Object.entries(currentWorkspaces).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`)) @@ -2440,6 +2635,14 @@ export function App() { setWorkspaceSectionTexts((currentTexts) => Object.fromEntries(Object.entries(currentTexts).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))) ); + setWorkspaceDrawers((currentDrawers) => + Object.fromEntries( + Object.entries(currentDrawers).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`)) + ) + ); + setWorkspacePageMenus((currentMenus) => + Object.fromEntries(Object.entries(currentMenus).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`))) + ); setActiveWorkspaceSectionIds((currentSectionIds) => Object.fromEntries( Object.entries(currentSectionIds).filter(([workspaceKey]) => !workspaceKey.startsWith(`${project.id}:`)) @@ -2516,24 +2719,30 @@ export function App() { continue; } - const selectedPage = scanSummary.pages.find((page) => page.selected); + const selectedWorkspacePages = getSelectedCoveragePages(scanSummary); + const selectedPage = selectedWorkspacePages[0]; if (!selectedPage) { continue; } const activePageId = activeWorkspacePageIds[project.id] ?? selectedPage.pageId; - const pageToOpen = scanSummary.pages.find((page) => page.pageId === activePageId) ?? selectedPage; + const pageToOpen = + selectedWorkspacePages.find((page) => page.pageId === activePageId) ?? + selectedPage; const workspaceKey = getWorkspaceKey(project.id, pageToOpen.pageId); if (pageWorkspaces[workspaceKey] || busyWorkspaceKey === workspaceKey) { continue; } - void handleOpenPageWorkspace(project, pageToOpen, { silent: true }); + void handleOpenPageWorkspace(project, pageToOpen, { + navigatePreview: !workspacePreviewPageIds[project.id], + silent: true + }); break; } - }, [activeWorkspacePageIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries]); + }, [activeWorkspacePageIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries, workspacePreviewPageIds]); const createNameConflict = pickedFolder ? hasProjectNameConflict(pickedFolder.rootName) : false; const canCreateProject = Boolean(pickedFolder) && !isSubmitting && !createNameConflict; @@ -3144,19 +3353,26 @@ export function App() { null; const activeWorkspaceMarkers = activeWorkspaceKey ? (workspacePreviewMarkers[activeWorkspaceKey] ?? []) : []; const activeWorkspacePreviewMode = activeWorkspaceKey ? (workspacePreviewModes[activeWorkspaceKey] ?? "desktop") : "desktop"; - const activeWorkspaceSectionId = - activeWorkspaceKey && activeWorkspace - ? (activeWorkspaceTarget?.sectionId ?? activeWorkspaceSectionIds[activeWorkspaceKey] ?? activeWorkspace.sections[0]?.id ?? "") - : ""; - const activeWorkspaceSection = - activeWorkspace?.sections.find((section) => section.id === activeWorkspaceSectionId) ?? - activeWorkspace?.sections[0] ?? - null; - const activeWorkspaceSectionText = - activeWorkspaceKey && activeWorkspaceSection - ? (workspaceSectionTexts[activeWorkspaceKey]?.[activeWorkspaceSection.id] ?? - activeWorkspaceSection.workingText) + const activeWorkspaceDrawers = activeWorkspaceKey ? (workspaceDrawers[activeWorkspaceKey] ?? []) : []; + const isWorkspacePageMenuOpen = activeWorkspaceKey ? Boolean(workspacePageMenus[activeWorkspaceKey]) : false; + const previewWorkspacePageId = workspacePreviewPageIds[project.id] ?? activeWorkspacePageId; + const previewWorkspacePage = + selectedCoveragePages.find((page) => page.pageId === previewWorkspacePageId) ?? + activeWorkspacePage; + const activeWorkspacePageIndex = Math.max( + 0, + selectedCoveragePages.findIndex((page) => page.pageId === activeWorkspacePageId) + ); + const activeWorkspacePreviewSourcePath = + activeWorkspace && previewWorkspacePage + ? (previewWorkspacePage.previewSourcePath ?? previewWorkspacePage.sourcePath) : ""; + const activeWorkspacePreviewFocusSelector = previewWorkspacePage?.previewTargetSelector ?? null; + const activeWorkspaceIssueCounts = getAuditIssueCounts(activeWorkspaceIssues); + const selectedCoverageIssueCount = selectedCoveragePages.reduce( + (total, page) => total + getPageIssues(scanSummary, page.pageId).length, + 0 + ); const selectedAuditIssuesAll = scanSummary?.audit ? getSelectedAuditIssues(scanSummary, scanSummary.audit.issues) : []; @@ -4362,7 +4578,7 @@ export function App() {
- Рабочая зона выбранных посадочных + Рабочая зона выбранного scope Original read-only, working draft сохраняется в Postgres + MinIO.
Apply пока не трогаем @@ -4388,41 +4604,157 @@ export function App() { })}
{activeWorkspace && activeWorkspacePage && activeWorkspaceKey ? ( -
+
- Выбранная страница + + Активная страница {activeWorkspacePageIndex + 1}/{selectedCoveragePages.length} · выбранный scope + {getPageTitle(activeWorkspacePage)} {getPagePathLabel(activeWorkspacePage)}
- {activeWorkspaceIssues.length} замечаний
-
-
-
- Зеркало страницы - Оранжевые точки ведут к полям правки ниже. +
0 ? "drawer-open" : ""}`}> +
+
+ Замечаний на активной странице: {activeWorkspaceIssues.length} + В выбранном scope: {selectedCoverageIssueCount} + Warning: {activeWorkspaceIssueCounts.warnings} · Info: {activeWorkspaceIssueCounts.info}
-
- {WORKSPACE_PREVIEW_MODES.map((mode) => ( +
+
- ))} + {isWorkspacePageMenuOpen ? ( +
+
+ Страницы и секции scope + Переключает preview, ошибки и поля рабочей зоны. +
+ {selectedCoveragePages.length > 0 ? ( +
+ {selectedCoveragePages.map((page, pageIndex) => { + const pageWorkspaceKey = getWorkspaceKey(project.id, page.pageId); + const isActivePage = activeWorkspacePageId === page.pageId; + const isOpening = busyWorkspaceKey === pageWorkspaceKey; + const pageIssues = getPageIssues(scanSummary, page.pageId); + + return ( + + ); + })} +
+ ) : ( + + В выбранном scope пока нет рабочих страниц или секций. + + )} +
+ ) : null} +
+ + + +
+ {WORKSPACE_PREVIEW_MODES.map((mode) => ( + + ))} +
+
+