Fix workspace scope section mapping

This commit is contained in:
DCCONSTRUCTIONS 2026-06-28 16:42:44 +03:00
parent 1f140c1130
commit 08894288bc
3 changed files with 334 additions and 173 deletions

View File

@ -931,16 +931,112 @@ function getSelectedPagesCount(scan: ProjectScanSummary) {
return scan.pages.filter((page) => page.selected).length; return scan.pages.filter((page) => page.selected).length;
} }
function getSelectedCoveragePages(scan: ProjectScanSummary) { function getPageScopeId(page: ProjectScanSummary["pages"][number]) {
return scan.pages return `page:${page.pageId}`;
.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) { function getSectionScopeId(section: ProjectScanSummary["siteSections"][number]) {
return `${projectId}:${pageId}`; return `section:${section.sectionId}`;
}
type WorkspaceScopeItem = {
scopeId: string;
kind: "page" | "section";
pageId: string;
sectionId?: string;
title: string;
pathLabel: string;
sourcePath: string;
previewSourcePath: string | null;
previewTargetSelector: string | null;
previewTargetIndex: number | null;
previewKind: ProjectScanSummary["pages"][number]["previewKind"];
order: number;
};
function getPageWorkspaceScopeItem(page: ProjectScanSummary["pages"][number], index: number): WorkspaceScopeItem {
return {
kind: "page",
order: getWorkspaceScopeOrder(page, index),
pageId: page.pageId,
pathLabel: getPagePathLabel(page),
previewKind: page.previewKind,
previewSourcePath: page.previewSourcePath ?? page.sourcePath,
previewTargetIndex: null,
previewTargetSelector: page.previewTargetSelector,
scopeId: getPageScopeId(page),
sourcePath: page.sourcePath,
title: getPageTitle(page)
};
}
function getSectionWorkspaceScopeItem(
section: ProjectScanSummary["siteSections"][number],
fallbackOrder: number
): WorkspaceScopeItem | null {
if (!section.pageId || !section.enabled || !section.selected) {
return null;
}
if (section.previewKind === "none" && !section.previewSourcePath && !section.previewTargetSelector) {
return null;
}
const indexSuffix = section.previewTargetIndex != null ? ` #${section.previewTargetIndex + 1}` : "";
return {
kind: "section",
order: section.order ?? fallbackOrder,
pageId: section.pageId,
pathLabel: `${section.sourcePath} · секция главной${indexSuffix}`,
previewKind: section.previewKind,
previewSourcePath: section.previewSourcePath,
previewTargetIndex: section.previewTargetIndex,
previewTargetSelector: section.previewTargetSelector,
scopeId: getSectionScopeId(section),
sectionId: section.sectionId,
sourcePath: section.sourcePath,
title: section.title
};
}
function getSelectedWorkspaceScopeItems(scan: ProjectScanSummary): WorkspaceScopeItem[] {
const sectionPageIds = new Set(
scan.siteSections
.filter((section) => section.selected && section.enabled && section.pageId)
.map((section) => section.pageId!)
);
const pageItems = scan.pages
.map((page, index) => ({ index, page }))
.filter(({ page }) => isWorkspaceScopePage(page))
.filter(({ page }) => page.scope !== "template_block" || !sectionPageIds.has(page.pageId))
.map(({ page, index }) => getPageWorkspaceScopeItem(page, index));
const sectionItems = scan.siteSections
.map((section, index) => getSectionWorkspaceScopeItem(section, index))
.filter((item): item is WorkspaceScopeItem => Boolean(item));
return [...pageItems, ...sectionItems].sort((left, right) => {
if (left.kind !== right.kind) {
if (left.kind === "page" && left.order === -1) return -1;
if (right.kind === "page" && right.order === -1) return 1;
}
return left.order - right.order || left.title.localeCompare(right.title, "ru");
});
}
function getSelectedWorkspaceIssueCount(scan: ProjectScanSummary | null | undefined, scopeItems: WorkspaceScopeItem[]) {
if (!scan?.audit) {
return 0;
}
const pageIds = new Set(scopeItems.map((item) => item.pageId));
return scan.audit.issues.filter((issue) => issue.pageId && pageIds.has(issue.pageId)).length;
}
function getWorkspaceKey(projectId: string, scopeId: string) {
return `${projectId}:${scopeId}`;
} }
type WorkspaceSection = PageWorkspace["sections"][number]; type WorkspaceSection = PageWorkspace["sections"][number];
@ -1399,13 +1495,13 @@ export function App() {
const [auditIssueLimits, setAuditIssueLimits] = useState<Record<string, number>>({}); const [auditIssueLimits, setAuditIssueLimits] = useState<Record<string, number>>({});
const [auditSeverityFilters, setAuditSeverityFilters] = useState<Record<string, AuditSeverityFilter>>({}); const [auditSeverityFilters, setAuditSeverityFilters] = useState<Record<string, AuditSeverityFilter>>({});
const [openPageAuditDetails, setOpenPageAuditDetails] = useState<Record<string, boolean>>({}); const [openPageAuditDetails, setOpenPageAuditDetails] = useState<Record<string, boolean>>({});
const [activeWorkspacePageIds, setActiveWorkspacePageIds] = useState<Record<string, string>>({}); const [activeWorkspaceScopeIds, setActiveWorkspaceScopeIds] = useState<Record<string, string>>({});
const [activeWorkspaceSectionIds, setActiveWorkspaceSectionIds] = useState<Record<string, string>>({}); const [activeWorkspaceSectionIds, setActiveWorkspaceSectionIds] = useState<Record<string, string>>({});
const [activeWorkspaceTargetIds, setActiveWorkspaceTargetIds] = useState<Record<string, string>>({}); const [activeWorkspaceTargetIds, setActiveWorkspaceTargetIds] = useState<Record<string, string>>({});
const [workspacePreviewMarkers, setWorkspacePreviewMarkers] = useState<Record<string, PreviewMarker[]>>({}); const [workspacePreviewMarkers, setWorkspacePreviewMarkers] = useState<Record<string, PreviewMarker[]>>({});
const [workspaceDrawers, setWorkspaceDrawers] = useState<Record<string, WorkspaceDrawerPanel[]>>({}); const [workspaceDrawers, setWorkspaceDrawers] = useState<Record<string, WorkspaceDrawerPanel[]>>({});
const [workspacePageMenus, setWorkspacePageMenus] = useState<Record<string, boolean>>({}); const [workspacePageMenus, setWorkspacePageMenus] = useState<Record<string, boolean>>({});
const [workspacePreviewPageIds, setWorkspacePreviewPageIds] = useState<Record<string, string>>({}); const [workspacePreviewScopeIds, setWorkspacePreviewScopeIds] = useState<Record<string, string>>({});
const [pageWorkspaces, setPageWorkspaces] = useState<Record<string, PageWorkspace>>({}); const [pageWorkspaces, setPageWorkspaces] = useState<Record<string, PageWorkspace>>({});
const [workspaceDraftTexts, setWorkspaceDraftTexts] = useState<Record<string, string>>({}); const [workspaceDraftTexts, setWorkspaceDraftTexts] = useState<Record<string, string>>({});
const [workspaceSectionTexts, setWorkspaceSectionTexts] = useState<Record<string, WorkspaceSectionTextMap>>({}); const [workspaceSectionTexts, setWorkspaceSectionTexts] = useState<Record<string, WorkspaceSectionTextMap>>({});
@ -1619,26 +1715,27 @@ export function App() {
return () => undefined; return () => undefined;
} }
const scopePages = getSelectedCoveragePages(scanSummary); const scopeItems = getSelectedWorkspaceScopeItems(scanSummary);
const activePageId = activeWorkspacePageIds[projectId] ?? scopePages[0]?.pageId ?? ""; const activeScopeId = activeWorkspaceScopeIds[projectId] ?? scopeItems[0]?.scopeId ?? "";
const workspaceKey = activePageId ? getWorkspaceKey(projectId, activePageId) : ""; const activeScopeItem = scopeItems.find((item) => item.scopeId === activeScopeId) ?? scopeItems[0] ?? null;
const workspaceKey = activeScopeItem ? getWorkspaceKey(projectId, activeScopeItem.scopeId) : "";
const workspace = workspaceKey ? pageWorkspaces[workspaceKey] : null; const workspace = workspaceKey ? pageWorkspaces[workspaceKey] : null;
if (!workspaceKey || !workspace) { if (!workspaceKey || !workspace) {
return () => undefined; return () => undefined;
} }
const issues = getPageIssues(scanSummary, activePageId); const issues = getPageIssues(scanSummary, activeScopeItem?.pageId ?? null);
const targets = buildWorkspaceTargets(workspace, issues); const targets = buildWorkspaceTargets(workspace, issues);
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 80)); timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 80));
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 360)); timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 360));
timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 1000)); timers.push(window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 1000));
return () => { return () => {
timers.forEach((timer) => window.clearTimeout(timer)); timers.forEach((timer) => window.clearTimeout(timer));
}; };
}, [activeProjectId, activeStageIndex, activeWorkspacePageIds, pageWorkspaces, scanSummaries, workspacePreviewModes]); }, [activeProjectId, activeStageIndex, activeWorkspaceScopeIds, pageWorkspaces, scanSummaries, workspacePreviewModes]);
function scrollWorkspaceFieldIntoView(workspaceKey: string, targetId: string, attempt = 0) { function scrollWorkspaceFieldIntoView(workspaceKey: string, targetId: string, attempt = 0) {
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@ -1663,21 +1760,27 @@ export function App() {
if (data?.type === "seo-mode:active-scope-page") { if (data?.type === "seo-mode:active-scope-page") {
const sourceWorkspaceKey = typeof data.workspaceKey === "string" ? data.workspaceKey : ""; const sourceWorkspaceKey = typeof data.workspaceKey === "string" ? data.workspaceKey : "";
const pageId = typeof data.pageId === "string" ? data.pageId : ""; const pageId = typeof data.pageId === "string" ? data.pageId : "";
const scopeId =
typeof data.scopeId === "string" && data.scopeId.length > 0
? data.scopeId
: pageId
? `page:${pageId}`
: "";
const [projectId = ""] = sourceWorkspaceKey.split(":"); const [projectId = ""] = sourceWorkspaceKey.split(":");
if (!projectId || !pageId) { if (!projectId || !scopeId) {
return; return;
} }
const nextWorkspaceKey = getWorkspaceKey(projectId, pageId); const nextWorkspaceKey = getWorkspaceKey(projectId, scopeId);
if (!pageWorkspaces[nextWorkspaceKey]) { if (!pageWorkspaces[nextWorkspaceKey]) {
const project = projects.find((item) => item.id === projectId); const project = projects.find((item) => item.id === projectId);
const scanSummary = scanSummaries[projectId]; const scanSummary = scanSummaries[projectId];
const page = scanSummary ? getSelectedCoveragePages(scanSummary).find((item) => item.pageId === pageId) : null; const scopeItem = scanSummary ? getSelectedWorkspaceScopeItems(scanSummary).find((item) => item.scopeId === scopeId) : null;
if (project && page && busyWorkspaceKey !== nextWorkspaceKey) { if (project && scopeItem && busyWorkspaceKey !== nextWorkspaceKey) {
void handleOpenPageWorkspace(project, page, { void handleOpenPageWorkspace(project, scopeItem, {
deferActiveUntilLoaded: true, deferActiveUntilLoaded: true,
navigatePreview: false, navigatePreview: false,
silent: true silent: true
@ -1687,14 +1790,14 @@ export function App() {
return; return;
} }
setActiveWorkspacePageIds((currentPageIds) => { setActiveWorkspaceScopeIds((currentScopeIds) => {
if (currentPageIds[projectId] === pageId) { if (currentScopeIds[projectId] === scopeId) {
return currentPageIds; return currentScopeIds;
} }
return { return {
...currentPageIds, ...currentScopeIds,
[projectId]: pageId [projectId]: scopeId
}; };
}); });
return; return;
@ -2267,10 +2370,14 @@ export function App() {
async function handleOpenPageWorkspace( async function handleOpenPageWorkspace(
project: ProjectSummary, project: ProjectSummary,
page: ProjectScanSummary["pages"][number], target: ProjectScanSummary["pages"][number] | WorkspaceScopeItem,
options: { deferActiveUntilLoaded?: boolean; navigatePreview?: boolean; silent?: boolean } = {} options: { deferActiveUntilLoaded?: boolean; navigatePreview?: boolean; silent?: boolean } = {}
) { ) {
const workspaceKey = getWorkspaceKey(project.id, page.pageId); const isScopeItem = "scopeId" in target;
const pageId = target.pageId;
const scopeId = isScopeItem ? target.scopeId : getPageScopeId(target);
const workspaceTitle = isScopeItem ? target.title : getPageTitle(target);
const workspaceKey = getWorkspaceKey(project.id, scopeId);
setBusyWorkspaceKey(workspaceKey); setBusyWorkspaceKey(workspaceKey);
@ -2281,21 +2388,21 @@ export function App() {
} }
if (options.navigatePreview !== false) { if (options.navigatePreview !== false) {
setWorkspacePreviewPageIds((currentPageIds) => ({ setWorkspacePreviewScopeIds((currentScopeIds) => ({
...currentPageIds, ...currentScopeIds,
[project.id]: page.pageId [project.id]: scopeId
})); }));
} }
if (!options.deferActiveUntilLoaded) { if (!options.deferActiveUntilLoaded) {
setActiveWorkspacePageIds((currentPageIds) => ({ setActiveWorkspaceScopeIds((currentScopeIds) => ({
...currentPageIds, ...currentScopeIds,
[project.id]: page.pageId [project.id]: scopeId
})); }));
} }
try { try {
const result = await openPageWorkspace(project.id, page.pageId); const result = await openPageWorkspace(project.id, pageId);
setPageWorkspaces((currentWorkspaces) => ({ setPageWorkspaces((currentWorkspaces) => ({
...currentWorkspaces, ...currentWorkspaces,
@ -2318,14 +2425,14 @@ export function App() {
[workspaceKey]: currentTargetIds[workspaceKey] ?? result.workspace.sections[0]?.id ?? "" [workspaceKey]: currentTargetIds[workspaceKey] ?? result.workspace.sections[0]?.id ?? ""
})); }));
if (options.deferActiveUntilLoaded) { if (options.deferActiveUntilLoaded) {
setActiveWorkspacePageIds((currentPageIds) => ({ setActiveWorkspaceScopeIds((currentScopeIds) => ({
...currentPageIds, ...currentScopeIds,
[project.id]: page.pageId [project.id]: scopeId
})); }));
} }
if (!options.silent) { if (!options.silent) {
setProjectActionMessage(`Рабочая зона открыта: ${getPageTitle(page)}.`); setProjectActionMessage(`Рабочая зона открыта: ${workspaceTitle}.`);
} }
} catch (error: unknown) { } catch (error: unknown) {
setProjectActionError(getErrorMessage(error, "Не удалось открыть рабочую зону страницы.")); setProjectActionError(getErrorMessage(error, "Не удалось открыть рабочую зону страницы."));
@ -2345,8 +2452,7 @@ export function App() {
}); });
} }
async function handleSavePageWorkspace(projectId: string, pageId: string) { async function handleSavePageWorkspace(projectId: string, pageId: string, workspaceKey = getWorkspaceKey(projectId, `page:${pageId}`)) {
const workspaceKey = getWorkspaceKey(projectId, pageId);
const workspace = pageWorkspaces[workspaceKey]; const workspace = pageWorkspaces[workspaceKey];
const workingText = workspace const workingText = workspace
? buildWorkingTextFromSections(workspace, workspaceSectionTexts[workspaceKey]) ? buildWorkingTextFromSections(workspace, workspaceSectionTexts[workspaceKey])
@ -2383,15 +2489,13 @@ export function App() {
} }
} }
async function handleResetPageWorkspace(projectId: string, pageId: string) { async function handleResetPageWorkspace(projectId: string, pageId: string, workspaceKey = getWorkspaceKey(projectId, `page:${pageId}`)) {
const confirmed = window.confirm("Сбросить working draft к original snapshot? Исходный проект не изменится."); const confirmed = window.confirm("Сбросить working draft к original snapshot? Исходный проект не изменится.");
if (!confirmed) { if (!confirmed) {
return; return;
} }
const workspaceKey = getWorkspaceKey(projectId, pageId);
setBusyWorkspaceKey(workspaceKey); setBusyWorkspaceKey(workspaceKey);
setProjectActionError(null); setProjectActionError(null);
setProjectActionMessage(null); setProjectActionMessage(null);
@ -2496,7 +2600,8 @@ export function App() {
function updateWorkspacePreviewMarkers( function updateWorkspacePreviewMarkers(
workspaceKey: string, workspaceKey: string,
targets: WorkspaceTarget[], targets: WorkspaceTarget[],
scopePages: ProjectScanSummary["pages"] = [] scopeItems: WorkspaceScopeItem[] = [],
activeScopeItem: WorkspaceScopeItem | null = null
) { ) {
const frameWindow = previewFrameRefs.current[workspaceKey]?.contentWindow; const frameWindow = previewFrameRefs.current[workspaceKey]?.contentWindow;
@ -2513,27 +2618,41 @@ export function App() {
{ {
type: "seo-mode:measure-targets", type: "seo-mode:measure-targets",
workspaceKey, workspaceKey,
activePageId: workspaceKey.split(":")[1] ?? "", activePageId: activeScopeItem?.pageId ?? "",
scopePages: scopePages activeScopeId: activeScopeItem?.scopeId ?? "",
.filter((page) => page.previewTargetSelector) scopePages: scopeItems
.map((page) => ({ .filter((item) => item.previewTargetSelector)
pageId: page.pageId, .map((item) => ({
label: getPageTitle(page), label: item.title,
selector: page.previewTargetSelector pageId: item.pageId,
scopeId: item.scopeId,
selector: item.previewTargetSelector,
selectorIndex: item.previewTargetIndex
})), })),
targets: targets targets: targets
.filter((target) => (target.markerSelectors.length > 0 || target.markerFallbackSelector) && target.issues.length > 0) .filter((target) => (target.markerSelectors.length > 0 || target.markerFallbackSelector) && target.issues.length > 0)
.map((target) => ({ .map((target) => {
const shouldAnchorToRenderedSection = activeScopeItem?.kind === "section" && Boolean(activeScopeItem.previewTargetSelector);
const sectionFallbackSelector = shouldAnchorToRenderedSection
? activeScopeItem?.previewTargetSelector ?? null
: target.markerFallbackSelector;
return {
fallbackSelector: sectionFallbackSelector,
fallbackSelectorIndex: shouldAnchorToRenderedSection ? activeScopeItem?.previewTargetIndex ?? 0 : null,
id: target.id, id: target.id,
label: target.title, label: target.title,
number: target.number, number: target.number,
pageStartFallback: !shouldAnchorToRenderedSection && target.kind !== "media",
preferPageStart:
!shouldAnchorToRenderedSection &&
(target.sectionId === "seo-title" || target.sectionId === "seo-description" || target.sectionId === "seo-h1"),
sectionId: target.sectionId,
selector: target.selector, selector: target.selector,
selectors: target.markerSelectors, selectorIndex: null,
fallbackSelector: target.markerFallbackSelector, selectors: shouldAnchorToRenderedSection ? [] : target.markerSelectors
pageStartFallback: target.kind !== "media", };
preferPageStart: target.sectionId === "seo-title" || target.sectionId === "seo-description" || target.sectionId === "seo-h1", })
sectionId: target.sectionId
}))
}, },
"*" "*"
); );
@ -2543,25 +2662,27 @@ export function App() {
workspaceKey: string, workspaceKey: string,
mode: WorkspacePreviewMode, mode: WorkspacePreviewMode,
targets: WorkspaceTarget[], targets: WorkspaceTarget[],
scopePages: ProjectScanSummary["pages"] = [] scopeItems: WorkspaceScopeItem[] = [],
activeScopeItem: WorkspaceScopeItem | null = null
) { ) {
setWorkspacePreviewModes((currentModes) => ({ setWorkspacePreviewModes((currentModes) => ({
...currentModes, ...currentModes,
[workspaceKey]: mode [workspaceKey]: mode
})); }));
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 120); window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 120);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 450); window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 450);
} }
function handleWorkspacePreviewLoad( function handleWorkspacePreviewLoad(
workspaceKey: string, workspaceKey: string,
targets: WorkspaceTarget[], targets: WorkspaceTarget[],
scopePages: ProjectScanSummary["pages"] = [] scopeItems: WorkspaceScopeItem[] = [],
activeScopeItem: WorkspaceScopeItem | null = null
) { ) {
updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages); updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 350); window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 350);
window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopePages), 1000); window.setTimeout(() => updateWorkspacePreviewMarkers(workspaceKey, targets, scopeItems, activeScopeItem), 1000);
} }
function setAuditIssuesVisibleCount(scanId: string, visibleCount: number) { function setAuditIssuesVisibleCount(scanId: string, visibleCount: number) {
@ -2614,15 +2735,15 @@ export function App() {
delete nextIds[project.id]; delete nextIds[project.id];
return nextIds; return nextIds;
}); });
setActiveWorkspacePageIds((currentPageIds) => { setActiveWorkspaceScopeIds((currentScopeIds) => {
const nextPageIds = { ...currentPageIds }; const nextScopeIds = { ...currentScopeIds };
delete nextPageIds[project.id]; delete nextScopeIds[project.id];
return nextPageIds; return nextScopeIds;
}); });
setWorkspacePreviewPageIds((currentPageIds) => { setWorkspacePreviewScopeIds((currentScopeIds) => {
const nextPageIds = { ...currentPageIds }; const nextScopeIds = { ...currentScopeIds };
delete nextPageIds[project.id]; delete nextScopeIds[project.id];
return nextPageIds; return nextScopeIds;
}); });
setPageWorkspaces((currentWorkspaces) => setPageWorkspaces((currentWorkspaces) =>
Object.fromEntries( Object.fromEntries(
@ -2719,30 +2840,30 @@ export function App() {
continue; continue;
} }
const selectedWorkspacePages = getSelectedCoveragePages(scanSummary); const selectedWorkspaceScopeItems = getSelectedWorkspaceScopeItems(scanSummary);
const selectedPage = selectedWorkspacePages[0]; const selectedScopeItem = selectedWorkspaceScopeItems[0];
if (!selectedPage) { if (!selectedScopeItem) {
continue; continue;
} }
const activePageId = activeWorkspacePageIds[project.id] ?? selectedPage.pageId; const activeScopeId = activeWorkspaceScopeIds[project.id] ?? selectedScopeItem.scopeId;
const pageToOpen = const scopeItemToOpen =
selectedWorkspacePages.find((page) => page.pageId === activePageId) ?? selectedWorkspaceScopeItems.find((item) => item.scopeId === activeScopeId) ??
selectedPage; selectedScopeItem;
const workspaceKey = getWorkspaceKey(project.id, pageToOpen.pageId); const workspaceKey = getWorkspaceKey(project.id, scopeItemToOpen.scopeId);
if (pageWorkspaces[workspaceKey] || busyWorkspaceKey === workspaceKey) { if (pageWorkspaces[workspaceKey] || busyWorkspaceKey === workspaceKey) {
continue; continue;
} }
void handleOpenPageWorkspace(project, pageToOpen, { void handleOpenPageWorkspace(project, scopeItemToOpen, {
navigatePreview: !workspacePreviewPageIds[project.id], navigatePreview: !workspacePreviewScopeIds[project.id],
silent: true silent: true
}); });
break; break;
} }
}, [activeWorkspacePageIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries, workspacePreviewPageIds]); }, [activeWorkspaceScopeIds, busyWorkspaceKey, pageWorkspaces, projects, scanSummaries, workspacePreviewScopeIds]);
const createNameConflict = pickedFolder ? hasProjectNameConflict(pickedFolder.rootName) : false; const createNameConflict = pickedFolder ? hasProjectNameConflict(pickedFolder.rootName) : false;
const canCreateProject = Boolean(pickedFolder) && !isSubmitting && !createNameConflict; const canCreateProject = Boolean(pickedFolder) && !isSubmitting && !createNameConflict;
@ -3325,23 +3446,26 @@ export function App() {
const isSelectingPages = selectingPagesProjectId === project.id; const isSelectingPages = selectingPagesProjectId === project.id;
const selectedPagesCount = scanSummary ? getSelectedPagesCount(scanSummary) : 0; const selectedPagesCount = scanSummary ? getSelectedPagesCount(scanSummary) : 0;
const selectedPages = scanSummary ? scanSummary.pages.filter((page) => page.selected) : []; const selectedPages = scanSummary ? scanSummary.pages.filter((page) => page.selected) : [];
const selectedCoveragePages = scanSummary ? getSelectedCoveragePages(scanSummary) : []; const selectedWorkspaceScopeItems = scanSummary ? getSelectedWorkspaceScopeItems(scanSummary) : [];
const scanScopeCounts = scanSummary ? getScanScopeCounts(scanSummary) : null; const scanScopeCounts = scanSummary ? getScanScopeCounts(scanSummary) : null;
const projectSiteTitle = getProjectSiteTitle(scanSummary); const projectSiteTitle = getProjectSiteTitle(scanSummary);
const pageTreeGroups = scanSummary ? getPageTreeGroups(scanSummary.pages) : []; const pageTreeGroups = scanSummary ? getPageTreeGroups(scanSummary.pages) : [];
const activeWorkspacePageId = activeWorkspacePageIds[project.id] ?? selectedCoveragePages[0]?.pageId ?? null; const activeWorkspaceScopeId =
const activeWorkspaceKey = activeWorkspacePageId ? getWorkspaceKey(project.id, activeWorkspacePageId) : null; activeWorkspaceScopeIds[project.id] ?? selectedWorkspaceScopeItems[0]?.scopeId ?? null;
const activeWorkspace = activeWorkspaceKey ? pageWorkspaces[activeWorkspaceKey] : null; const activeWorkspaceScopeItem =
const activeWorkspacePage = selectedWorkspaceScopeItems.find((item) => item.scopeId === activeWorkspaceScopeId) ??
selectedCoveragePages.find((page) => page.pageId === activeWorkspacePageId) ?? selectedWorkspaceScopeItems[0] ??
scanSummary?.pages.find((page) => page.pageId === activeWorkspacePageId) ??
null; null;
const activeWorkspaceKey = activeWorkspaceScopeItem
? getWorkspaceKey(project.id, activeWorkspaceScopeItem.scopeId)
: null;
const activeWorkspace = activeWorkspaceKey ? pageWorkspaces[activeWorkspaceKey] : null;
const activeWorkspaceText = const activeWorkspaceText =
activeWorkspaceKey && activeWorkspace activeWorkspaceKey && activeWorkspace
? (workspaceDraftTexts[activeWorkspaceKey] ?? activeWorkspace.item.workingText) ? (workspaceDraftTexts[activeWorkspaceKey] ?? activeWorkspace.item.workingText)
: ""; : "";
const isWorkspaceBusy = activeWorkspaceKey ? busyWorkspaceKey === activeWorkspaceKey : false; const isWorkspaceBusy = activeWorkspaceKey ? busyWorkspaceKey === activeWorkspaceKey : false;
const activeWorkspaceIssues = getPageIssues(scanSummary, activeWorkspacePageId); const activeWorkspaceIssues = getPageIssues(scanSummary, activeWorkspaceScopeItem?.pageId ?? null);
const activeWorkspaceTargets = activeWorkspace ? buildWorkspaceTargets(activeWorkspace, activeWorkspaceIssues) : []; const activeWorkspaceTargets = activeWorkspace ? buildWorkspaceTargets(activeWorkspace, activeWorkspaceIssues) : [];
const activeWorkspaceTargetId = const activeWorkspaceTargetId =
activeWorkspaceKey && activeWorkspaceTargets.length > 0 activeWorkspaceKey && activeWorkspaceTargets.length > 0
@ -3355,24 +3479,22 @@ export function App() {
const activeWorkspacePreviewMode = activeWorkspaceKey ? (workspacePreviewModes[activeWorkspaceKey] ?? "desktop") : "desktop"; const activeWorkspacePreviewMode = activeWorkspaceKey ? (workspacePreviewModes[activeWorkspaceKey] ?? "desktop") : "desktop";
const activeWorkspaceDrawers = activeWorkspaceKey ? (workspaceDrawers[activeWorkspaceKey] ?? []) : []; const activeWorkspaceDrawers = activeWorkspaceKey ? (workspaceDrawers[activeWorkspaceKey] ?? []) : [];
const isWorkspacePageMenuOpen = activeWorkspaceKey ? Boolean(workspacePageMenus[activeWorkspaceKey]) : false; const isWorkspacePageMenuOpen = activeWorkspaceKey ? Boolean(workspacePageMenus[activeWorkspaceKey]) : false;
const previewWorkspacePageId = workspacePreviewPageIds[project.id] ?? activeWorkspacePageId; const previewWorkspaceScopeId = workspacePreviewScopeIds[project.id] ?? activeWorkspaceScopeItem?.scopeId ?? null;
const previewWorkspacePage = const previewWorkspaceScopeItem =
selectedCoveragePages.find((page) => page.pageId === previewWorkspacePageId) ?? selectedWorkspaceScopeItems.find((item) => item.scopeId === previewWorkspaceScopeId) ??
activeWorkspacePage; activeWorkspaceScopeItem;
const activeWorkspacePageIndex = Math.max( const activeWorkspacePageIndex = Math.max(
0, 0,
selectedCoveragePages.findIndex((page) => page.pageId === activeWorkspacePageId) selectedWorkspaceScopeItems.findIndex((item) => item.scopeId === activeWorkspaceScopeItem?.scopeId)
); );
const activeWorkspacePreviewSourcePath = const activeWorkspacePreviewSourcePath =
activeWorkspace && previewWorkspacePage activeWorkspace && previewWorkspaceScopeItem
? (previewWorkspacePage.previewSourcePath ?? previewWorkspacePage.sourcePath) ? (previewWorkspaceScopeItem.previewSourcePath ?? previewWorkspaceScopeItem.sourcePath)
: ""; : "";
const activeWorkspacePreviewFocusSelector = previewWorkspacePage?.previewTargetSelector ?? null; const activeWorkspacePreviewFocusSelector = previewWorkspaceScopeItem?.previewTargetSelector ?? null;
const activeWorkspacePreviewFocusIndex = previewWorkspaceScopeItem?.previewTargetIndex ?? null;
const activeWorkspaceIssueCounts = getAuditIssueCounts(activeWorkspaceIssues); const activeWorkspaceIssueCounts = getAuditIssueCounts(activeWorkspaceIssues);
const selectedCoverageIssueCount = selectedCoveragePages.reduce( const selectedCoverageIssueCount = getSelectedWorkspaceIssueCount(scanSummary, selectedWorkspaceScopeItems);
(total, page) => total + getPageIssues(scanSummary, page.pageId).length,
0
);
const selectedAuditIssuesAll = scanSummary?.audit const selectedAuditIssuesAll = scanSummary?.audit
? getSelectedAuditIssues(scanSummary, scanSummary.audit.issues) ? getSelectedAuditIssues(scanSummary, scanSummary.audit.issues)
: []; : [];
@ -3534,7 +3656,7 @@ export function App() {
<summary>Инженерный список аудита</summary> <summary>Инженерный список аудита</summary>
<div className="page-audit-list"> <div className="page-audit-list">
{scanSummary.pages.map((page) => { {scanSummary.pages.map((page) => {
const workspaceKey = getWorkspaceKey(project.id, page.pageId); const workspaceKey = getWorkspaceKey(project.id, getPageScopeId(page));
const isOpeningPage = busyWorkspaceKey === workspaceKey; const isOpeningPage = busyWorkspaceKey === workspaceKey;
const auditDetailsKey = `${scanSummary.id}:${page.pageId}`; const auditDetailsKey = `${scanSummary.id}:${page.pageId}`;
const isAuditDetailsOpen = Boolean(openPageAuditDetails[auditDetailsKey]); const isAuditDetailsOpen = Boolean(openPageAuditDetails[auditDetailsKey]);
@ -4584,34 +4706,34 @@ export function App() {
<span>Apply пока не трогаем</span> <span>Apply пока не трогаем</span>
</div> </div>
<div className="workspace-page-tabs"> <div className="workspace-page-tabs">
{selectedCoveragePages.map((page) => { {selectedWorkspaceScopeItems.map((scopeItem) => {
const workspaceKey = getWorkspaceKey(project.id, page.pageId); const workspaceKey = getWorkspaceKey(project.id, scopeItem.scopeId);
const isActivePage = activeWorkspacePageId === page.pageId; const isActivePage = activeWorkspaceScopeItem?.scopeId === scopeItem.scopeId;
const isOpening = busyWorkspaceKey === workspaceKey; const isOpening = busyWorkspaceKey === workspaceKey;
return ( return (
<button <button
className={isActivePage ? "active" : undefined} className={isActivePage ? "active" : undefined}
disabled={isOpening} disabled={isOpening}
key={`workspace-tab-${page.pageId}`} key={`workspace-tab-${scopeItem.scopeId}`}
onClick={() => void handleOpenPageWorkspace(project, page)} onClick={() => void handleOpenPageWorkspace(project, scopeItem)}
type="button" type="button"
> >
{isOpening ? <Loader2 className="spin" size={14} /> : <FileText size={14} />} {isOpening ? <Loader2 className="spin" size={14} /> : <FileText size={14} />}
<span>{getPageTitle(page)}</span> <span>{scopeItem.title}</span>
</button> </button>
); );
})} })}
</div> </div>
{activeWorkspace && activeWorkspacePage && activeWorkspaceKey ? ( {activeWorkspace && activeWorkspaceScopeItem && activeWorkspaceKey ? (
<div className="workspace-editor-stack workspace-cockpit"> <div className="workspace-editor-stack workspace-cockpit">
<div className="workspace-current-page"> <div className="workspace-current-page">
<div> <div>
<span> <span>
Активная страница {activeWorkspacePageIndex + 1}/{selectedCoveragePages.length} · выбранный scope Активная зона {activeWorkspacePageIndex + 1}/{selectedWorkspaceScopeItems.length} · выбранный scope
</span> </span>
<strong>{getPageTitle(activeWorkspacePage)}</strong> <strong>{activeWorkspaceScopeItem.title}</strong>
<small>{getPagePathLabel(activeWorkspacePage)}</small> <small>{activeWorkspaceScopeItem.pathLabel}</small>
</div> </div>
</div> </div>
<section className={`workspace-preview-panel workspace-cockpit-panel ${activeWorkspaceDrawers.length > 0 ? "drawer-open" : ""}`}> <section className={`workspace-preview-panel workspace-cockpit-panel ${activeWorkspaceDrawers.length > 0 ? "drawer-open" : ""}`}>
@ -4633,7 +4755,7 @@ export function App() {
<FileText size={14} /> <FileText size={14} />
Страница Страница
<em> <em>
{activeWorkspacePageIndex + 1}/{selectedCoveragePages.length} {activeWorkspacePageIndex + 1}/{selectedWorkspaceScopeItems.length}
</em> </em>
<ChevronDown size={14} /> <ChevronDown size={14} />
</button> </button>
@ -4643,32 +4765,32 @@ export function App() {
<strong>Страницы и секции scope</strong> <strong>Страницы и секции scope</strong>
<small>Переключает preview, ошибки и поля рабочей зоны.</small> <small>Переключает preview, ошибки и поля рабочей зоны.</small>
</div> </div>
{selectedCoveragePages.length > 0 ? ( {selectedWorkspaceScopeItems.length > 0 ? (
<div className="workspace-page-menu-list"> <div className="workspace-page-menu-list">
{selectedCoveragePages.map((page, pageIndex) => { {selectedWorkspaceScopeItems.map((scopeItem, pageIndex) => {
const pageWorkspaceKey = getWorkspaceKey(project.id, page.pageId); const pageWorkspaceKey = getWorkspaceKey(project.id, scopeItem.scopeId);
const isActivePage = activeWorkspacePageId === page.pageId; const isActivePage = activeWorkspaceScopeItem?.scopeId === scopeItem.scopeId;
const isOpening = busyWorkspaceKey === pageWorkspaceKey; const isOpening = busyWorkspaceKey === pageWorkspaceKey;
const pageIssues = getPageIssues(scanSummary, page.pageId); const pageIssues = getPageIssues(scanSummary, scopeItem.pageId);
return ( return (
<button <button
className={isActivePage ? "active" : undefined} className={isActivePage ? "active" : undefined}
disabled={isOpening} disabled={isOpening}
key={`workspace-page-menu-${page.pageId}`} key={`workspace-page-menu-${scopeItem.scopeId}`}
onClick={() => { onClick={() => {
setWorkspacePageMenus((currentMenus) => ({ setWorkspacePageMenus((currentMenus) => ({
...currentMenus, ...currentMenus,
[activeWorkspaceKey]: false [activeWorkspaceKey]: false
})); }));
void handleOpenPageWorkspace(project, page, { silent: true }); void handleOpenPageWorkspace(project, scopeItem, { silent: true });
}} }}
role="menuitem" role="menuitem"
type="button" type="button"
> >
<span>{pageIndex + 1}</span> <span>{pageIndex + 1}</span>
<strong>{getPageTitle(page)}</strong> <strong>{scopeItem.title}</strong>
<small>{getPagePathLabel(page)}</small> <small>{scopeItem.pathLabel}</small>
<em>{pageIssues.length} замечаний</em> <em>{pageIssues.length} замечаний</em>
</button> </button>
); );
@ -4689,7 +4811,7 @@ export function App() {
> >
<FileText size={14} /> <FileText size={14} />
Scope Scope
<em>{selectedCoveragePages.length}</em> <em>{selectedWorkspaceScopeItems.length}</em>
</button> </button>
<button <button
className={activeWorkspaceDrawers.includes("issues") ? "active" : undefined} className={activeWorkspaceDrawers.includes("issues") ? "active" : undefined}
@ -4719,7 +4841,8 @@ export function App() {
activeWorkspaceKey, activeWorkspaceKey,
mode.id, mode.id,
activeWorkspaceTargets, activeWorkspaceTargets,
selectedCoveragePages selectedWorkspaceScopeItems,
activeWorkspaceScopeItem
) )
} }
type="button" type="button"
@ -4737,7 +4860,8 @@ export function App() {
handleWorkspacePreviewLoad( handleWorkspacePreviewLoad(
activeWorkspaceKey, activeWorkspaceKey,
activeWorkspaceTargets, activeWorkspaceTargets,
selectedCoveragePages selectedWorkspaceScopeItems,
activeWorkspaceScopeItem
) )
} }
ref={(element) => { ref={(element) => {
@ -4753,9 +4877,10 @@ export function App() {
src={getPreviewPageUrl( src={getPreviewPageUrl(
project.id, project.id,
activeWorkspacePreviewSourcePath, activeWorkspacePreviewSourcePath,
activeWorkspacePreviewFocusSelector activeWorkspacePreviewFocusSelector,
activeWorkspacePreviewFocusIndex
)} )}
title={`Предпросмотр ${getPageTitle(activeWorkspacePage)}`} title={`Предпросмотр ${activeWorkspaceScopeItem.title}`}
/> />
<div className="workspace-preview-markers"> <div className="workspace-preview-markers">
{activeWorkspaceMarkers.map((marker) => { {activeWorkspaceMarkers.map((marker) => {
@ -4796,7 +4921,7 @@ export function App() {
</span> </span>
<strong> <strong>
{activeWorkspaceDrawer === "scope" {activeWorkspaceDrawer === "scope"
? `${selectedCoveragePages.length} в scope` ? `${selectedWorkspaceScopeItems.length} в scope`
: activeWorkspaceDrawer === "fields" : activeWorkspaceDrawer === "fields"
? `${activeWorkspaceTargets.length} полей` ? `${activeWorkspaceTargets.length} полей`
: `${activeWorkspaceIssues.length} замечаний`} : `${activeWorkspaceIssues.length} замечаний`}
@ -4820,23 +4945,23 @@ export function App() {
</div> </div>
{activeWorkspaceDrawer === "scope" ? ( {activeWorkspaceDrawer === "scope" ? (
<div className="workspace-scope-list"> <div className="workspace-scope-list">
{selectedCoveragePages.map((page, pageIndex) => { {selectedWorkspaceScopeItems.map((scopeItem, pageIndex) => {
const pageWorkspaceKey = getWorkspaceKey(project.id, page.pageId); const pageWorkspaceKey = getWorkspaceKey(project.id, scopeItem.scopeId);
const isOpening = busyWorkspaceKey === pageWorkspaceKey; const isOpening = busyWorkspaceKey === pageWorkspaceKey;
const isActivePage = activeWorkspacePageId === page.pageId; const isActivePage = activeWorkspaceScopeItem?.scopeId === scopeItem.scopeId;
const pageIssues = getPageIssues(scanSummary, page.pageId); const pageIssues = getPageIssues(scanSummary, scopeItem.pageId);
return ( return (
<button <button
className={isActivePage ? "active" : undefined} className={isActivePage ? "active" : undefined}
disabled={isOpening} disabled={isOpening}
key={`workspace-scope-${page.pageId}`} key={`workspace-scope-${scopeItem.scopeId}`}
onClick={() => void handleOpenPageWorkspace(project, page)} onClick={() => void handleOpenPageWorkspace(project, scopeItem)}
type="button" type="button"
> >
<span>{pageIndex + 1}</span> <span>{pageIndex + 1}</span>
<strong>{getPageTitle(page)}</strong> <strong>{scopeItem.title}</strong>
<small>{getPagePathLabel(page)}</small> <small>{scopeItem.pathLabel}</small>
<em>{pageIssues.length} замечаний</em> <em>{pageIssues.length} замечаний</em>
</button> </button>
); );
@ -4902,7 +5027,11 @@ export function App() {
className="tiny-action primary" className="tiny-action primary"
disabled={isWorkspaceBusy} disabled={isWorkspaceBusy}
onClick={() => onClick={() =>
void handleSavePageWorkspace(project.id, activeWorkspace.pageId) void handleSavePageWorkspace(
project.id,
activeWorkspace.pageId,
activeWorkspaceKey
)
} }
type="button" type="button"
> >

View File

@ -1039,16 +1039,38 @@ function buildScopeFlow(
} }
function getScopeStats(scan: ProjectScanSummary) { function getScopeStats(scan: ProjectScanSummary) {
return scan.pages.reduce( const pageStats = scan.pages.reduce(
(stats, page) => { (stats, page) => {
stats.total += 1; stats.total += 1;
if (page.scope === "indexable_page") stats.indexable += 1; if (page.scope === "indexable_page") stats.indexable += 1;
if (page.scope === "admin_page") stats.admin += 1;
if (page.scope === "technical_page") stats.technical += 1;
if (page.selected && page.scope === "indexable_page") stats.selectedIndexable += 1; if (page.selected && page.scope === "indexable_page") stats.selectedIndexable += 1;
if (page.selected && page.scope === "admin_page") stats.selectedAdmin += 1;
if (page.selected && page.scope === "technical_page") stats.selectedTechnical += 1;
if (page.selected && page.scope !== "indexable_page") stats.selectedExcluded += 1; if (page.selected && page.scope !== "indexable_page") stats.selectedExcluded += 1;
return stats; return stats;
}, },
{ indexable: 0, selectedExcluded: 0, selectedIndexable: 0, total: 0 } {
admin: 0,
indexable: 0,
selectedAdmin: 0,
selectedExcluded: 0,
selectedIndexable: 0,
selectedTechnical: 0,
technical: 0,
total: 0
}
); );
const selectedSections = scan.siteSections.filter((section) => section.selected && section.enabled && section.pageId).length;
const enabledSections = scan.siteSections.filter((section) => section.enabled && section.pageId).length;
return {
...pageStats,
enabledSections,
selectedSections,
selectedWorkspaceItems: pageStats.selectedIndexable + selectedSections + pageStats.selectedAdmin + pageStats.selectedTechnical
};
} }
function shouldAutosaveNodeChanges(changes: NodeChange<ScopeFlowNode>[]) { function shouldAutosaveNodeChanges(changes: NodeChange<ScopeFlowNode>[]) {
@ -1268,6 +1290,10 @@ export default function ProjectScopeFlow({
const groups = scopePriority const groups = scopePriority
.map((scope) => ({ .map((scope) => ({
count: scope === "template_block" ? scan.siteSections.length : scan.pages.filter((page) => page.scope === scope).length, count: scope === "template_block" ? scan.siteSections.length : scan.pages.filter((page) => page.scope === scope).length,
selected:
scope === "template_block"
? scan.siteSections.filter((section) => section.selected && section.enabled && section.pageId).length
: scan.pages.filter((page) => page.scope === scope && page.selected).length,
scope scope
})) }))
.filter((group) => group.count > 0); .filter((group) => group.count > 0);
@ -1278,15 +1304,16 @@ export default function ProjectScopeFlow({
<div> <div>
<strong>Конфигурация проекта</strong> <strong>Конфигурация проекта</strong>
<small> <small>
Выбрано посадочных {stats.selectedIndexable}/{stats.indexable}; технических выбранных{" "} Выбрано рабочих зон {stats.selectedWorkspaceItems}: посадочных {stats.selectedIndexable}/{stats.indexable},
{stats.selectedExcluded}. секций {stats.selectedSections}/{stats.enabledSections}, админка {stats.selectedAdmin}/{stats.admin},
техслой {stats.selectedTechnical}/{stats.technical}.
</small> </small>
</div> </div>
<div className="project-scope-flow-actions"> <div className="project-scope-flow-actions">
<button disabled={isBusy} onClick={onApplyRecommendedScope} type="button"> <button disabled={isBusy} onClick={onApplyRecommendedScope} type="button">
SEO scope SEO scope
</button> </button>
<button disabled={isBusy || stats.selectedIndexable + stats.selectedExcluded === 0} onClick={onClearSelection} type="button"> <button disabled={isBusy || stats.selectedWorkspaceItems === 0} onClick={onClearSelection} type="button">
Снять все Снять все
</button> </button>
</div> </div>
@ -1295,7 +1322,7 @@ export default function ProjectScopeFlow({
{groups.map((group) => ( {groups.map((group) => (
<span className={`scope-legend-item ${group.scope}`} key={group.scope}> <span className={`scope-legend-item ${group.scope}`} key={group.scope}>
<FileText size={13} /> <FileText size={13} />
{scopeGroupNames[group.scope]} · {group.count} {scopeGroupNames[group.scope]} · {group.selected}/{group.count}
</span> </span>
))} ))}
</div> </div>

View File

@ -337,7 +337,7 @@ function buildPreviewBridgeScript() {
return ` return `
<script> <script>
(() => { (() => {
const state = { workspaceKey: "", targets: [], scopePages: [], activeScopePageId: "", lastScopeReportAt: 0 }; const state = { workspaceKey: "", targets: [], scopePages: [], activeScopeId: "", activeScopePageId: "", lastScopeReportAt: 0 };
const layerId = "seo-mode-preview-marker-layer"; const layerId = "seo-mode-preview-marker-layer";
let frame = 0; let frame = 0;
let lastActivation = { id: "", at: 0 }; let lastActivation = { id: "", at: 0 };
@ -428,9 +428,21 @@ function buildPreviewBridgeScript() {
return visibleRect; return visibleRect;
} }
function getUsableRect(selector) { function getElementForSelector(selector, selectorIndex) {
if (!selector) return null; if (!selector) return null;
const element = document.querySelector(selector);
try {
const index = Number.isInteger(selectorIndex) && selectorIndex >= 0 ? selectorIndex : 0;
const matches = Array.from(document.querySelectorAll(selector));
return matches[Math.max(0, Math.min(index, matches.length - 1))] || null;
} catch {
return null;
}
}
function getUsableRect(selector, selectorIndex) {
const element = getElementForSelector(selector, selectorIndex);
if (!element) return null; if (!element) return null;
const style = window.getComputedStyle(element); const style = window.getComputedStyle(element);
if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return null; if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return null;
@ -525,16 +537,6 @@ function buildPreviewBridgeScript() {
return marker; return marker;
} }
function getElementForSelector(selector) {
if (!selector) return null;
try {
return document.querySelector(selector);
} catch {
return null;
}
}
function reportActiveScopePage() { function reportActiveScopePage() {
if (!state.workspaceKey || !Array.isArray(state.scopePages) || state.scopePages.length === 0) { if (!state.workspaceKey || !Array.isArray(state.scopePages) || state.scopePages.length === 0) {
return; return;
@ -546,7 +548,7 @@ function buildPreviewBridgeScript() {
const probeLine = Math.min(Math.max(window.innerHeight * 0.38, 180), Math.max(180, window.innerHeight - 120)); const probeLine = Math.min(Math.max(window.innerHeight * 0.38, 180), Math.max(180, window.innerHeight - 120));
for (const page of state.scopePages) { for (const page of state.scopePages) {
const element = getElementForSelector(page.selector); const element = getElementForSelector(page.selector, page.selectorIndex);
if (!element) continue; if (!element) continue;
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
@ -555,26 +557,26 @@ function buildPreviewBridgeScript() {
if (rect.top <= probeLine && rect.bottom > probeLine) { if (rect.top <= probeLine && rect.bottom > probeLine) {
if (!activeAtProbe || rect.top > activeAtProbe.top) { if (!activeAtProbe || rect.top > activeAtProbe.top) {
activeAtProbe = { pageId: page.pageId, top: rect.top }; activeAtProbe = { pageId: page.pageId, scopeId: page.scopeId || page.pageId, top: rect.top };
} }
continue; continue;
} }
if (rect.top > probeLine) { if (rect.top > probeLine) {
if (!nearestBelow || rect.top < nearestBelow.top) { if (!nearestBelow || rect.top < nearestBelow.top) {
nearestBelow = { pageId: page.pageId, top: rect.top }; nearestBelow = { pageId: page.pageId, scopeId: page.scopeId || page.pageId, top: rect.top };
} }
continue; continue;
} }
if (!lastAbove || rect.bottom > lastAbove.bottom) { if (!lastAbove || rect.bottom > lastAbove.bottom) {
lastAbove = { pageId: page.pageId, bottom: rect.bottom }; lastAbove = { pageId: page.pageId, scopeId: page.scopeId || page.pageId, bottom: rect.bottom };
} }
} }
const bestPage = activeAtProbe || nearestBelow || lastAbove; const bestPage = activeAtProbe || nearestBelow || lastAbove;
if (!bestPage || bestPage.pageId === state.activeScopePageId) { if (!bestPage || bestPage.scopeId === state.activeScopeId) {
return; return;
} }
@ -583,11 +585,13 @@ function buildPreviewBridgeScript() {
return; return;
} }
state.activeScopeId = bestPage.scopeId;
state.activeScopePageId = bestPage.pageId; state.activeScopePageId = bestPage.pageId;
state.lastScopeReportAt = now; state.lastScopeReportAt = now;
window.parent?.postMessage({ window.parent?.postMessage({
type: "seo-mode:active-scope-page", type: "seo-mode:active-scope-page",
workspaceKey: state.workspaceKey, workspaceKey: state.workspaceKey,
scopeId: bestPage.scopeId,
pageId: bestPage.pageId pageId: bestPage.pageId
}, "*"); }, "*");
} }
@ -611,7 +615,7 @@ function buildPreviewBridgeScript() {
let placedFallbackMarker = false; let placedFallbackMarker = false;
for (const selector of selectors) { for (const selector of selectors) {
const rect = getUsableRect(selector); const rect = getUsableRect(selector, target.selectorIndex);
if (!rect) continue; if (!rect) continue;
placedExactMarker = true; placedExactMarker = true;
const baseLeft = Math.max(12, rect.left + window.scrollX + 10); const baseLeft = Math.max(12, rect.left + window.scrollX + 10);
@ -623,7 +627,7 @@ function buildPreviewBridgeScript() {
} }
if (!prefersPageStart && !placedExactMarker && target.fallbackSelector) { if (!prefersPageStart && !placedExactMarker && target.fallbackSelector) {
const rect = getUsableRect(target.fallbackSelector); const rect = getUsableRect(target.fallbackSelector, target.fallbackSelectorIndex);
if (rect) { if (rect) {
placedFallbackMarker = true; placedFallbackMarker = true;
const baseLeft = Math.max(12, rect.left + window.scrollX + 10); const baseLeft = Math.max(12, rect.left + window.scrollX + 10);
@ -690,6 +694,7 @@ function buildPreviewBridgeScript() {
state.workspaceKey = data.workspaceKey || ""; state.workspaceKey = data.workspaceKey || "";
state.targets = Array.isArray(data.targets) ? data.targets : []; state.targets = Array.isArray(data.targets) ? data.targets : [];
state.scopePages = Array.isArray(data.scopePages) ? data.scopePages : []; state.scopePages = Array.isArray(data.scopePages) ? data.scopePages : [];
state.activeScopeId = data.activeScopeId || state.activeScopeId || "";
state.activeScopePageId = data.activePageId || state.activeScopePageId || ""; state.activeScopePageId = data.activePageId || state.activeScopePageId || "";
scheduleMeasure(); scheduleMeasure();
}); });