Protect keyword context snapshots
This commit is contained in:
parent
3951a9cbc5
commit
22ec219383
|
|
@ -4387,6 +4387,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
isBulkApproving,
|
isBulkApproving,
|
||||||
isDemandProfileDirty,
|
isDemandProfileDirty,
|
||||||
|
isReadOnly,
|
||||||
isSavingDraft,
|
isSavingDraft,
|
||||||
isManualAdding,
|
isManualAdding,
|
||||||
onDecision,
|
onDecision,
|
||||||
|
|
@ -4410,6 +4411,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
isBulkApproving: boolean;
|
isBulkApproving: boolean;
|
||||||
isDemandProfileDirty: boolean;
|
isDemandProfileDirty: boolean;
|
||||||
|
isReadOnly: boolean;
|
||||||
isSavingDraft: boolean;
|
isSavingDraft: boolean;
|
||||||
isManualAdding: boolean;
|
isManualAdding: boolean;
|
||||||
onDecision: (anchor: AnchorReviewAnchorView, decision: AnchorReviewDecisionInput) => void;
|
onDecision: (anchor: AnchorReviewAnchorView, decision: AnchorReviewDecisionInput) => void;
|
||||||
|
|
@ -4447,7 +4449,11 @@ function SeoAnchorReviewWorkspace({
|
||||||
<small>Выбираем смысловые якоря и вручную решаем, что отправлять в очередь Wordstat.</small>
|
<small>Выбираем смысловые якоря и вручную решаем, что отправлять в очередь Wordstat.</small>
|
||||||
<small className="keyword-stage-profile-line">
|
<small className="keyword-stage-profile-line">
|
||||||
Текущий активный профиль: <strong>{contextProfileLabel}</strong>
|
Текущий активный профиль: <strong>{contextProfileLabel}</strong>
|
||||||
{contextProfileStatus === "loading" ? " · загружается" : contextProfileStatus === "snapshot" ? " · снимок" : ""}
|
{contextProfileStatus === "loading"
|
||||||
|
? " · загружается"
|
||||||
|
: contextProfileStatus === "snapshot"
|
||||||
|
? " · снимок только для просмотра"
|
||||||
|
: ""}
|
||||||
</small>
|
</small>
|
||||||
<small>
|
<small>
|
||||||
{getAnchorReviewStateLabel(anchorReview.state)} · очередь Wordstat {anchorReview.readiness.wordstatQueueCount} ·
|
{getAnchorReviewStateLabel(anchorReview.state)} · очередь Wordstat {anchorReview.readiness.wordstatQueueCount} ·
|
||||||
|
|
@ -4462,10 +4468,19 @@ function SeoAnchorReviewWorkspace({
|
||||||
<button
|
<button
|
||||||
aria-label="Сохранить раскладку семантического базиса"
|
aria-label="Сохранить раскладку семантического базиса"
|
||||||
className={`keyword-stage-icon-action${draftChangeCount > 0 ? " active" : ""}`}
|
className={`keyword-stage-icon-action${draftChangeCount > 0 ? " active" : ""}`}
|
||||||
disabled={isBulkApproving || isManualAdding || isSavingDraft || draftChangeCount === 0 || anchorReview.state === "not_ready"}
|
disabled={
|
||||||
|
isReadOnly ||
|
||||||
|
isBulkApproving ||
|
||||||
|
isManualAdding ||
|
||||||
|
isSavingDraft ||
|
||||||
|
draftChangeCount === 0 ||
|
||||||
|
anchorReview.state === "not_ready"
|
||||||
|
}
|
||||||
onClick={onSaveDraft}
|
onClick={onSaveDraft}
|
||||||
title={
|
title={
|
||||||
draftChangeCount > 0
|
isReadOnly
|
||||||
|
? "Snapshot открыт только для просмотра: переключись на текущий контекст, чтобы редактировать Stage 1"
|
||||||
|
: draftChangeCount > 0
|
||||||
? `Сохранить ${draftChangeCount} изменений Stage 1`
|
? `Сохранить ${draftChangeCount} изменений Stage 1`
|
||||||
: "Нет несохранённых изменений"
|
: "Нет несохранённых изменений"
|
||||||
}
|
}
|
||||||
|
|
@ -4476,9 +4491,13 @@ function SeoAnchorReviewWorkspace({
|
||||||
<button
|
<button
|
||||||
aria-label="Ручное добавление ключа"
|
aria-label="Ручное добавление ключа"
|
||||||
className="keyword-stage-icon-action"
|
className="keyword-stage-icon-action"
|
||||||
disabled={isBulkApproving || isManualAdding || anchorReview.state === "not_ready"}
|
disabled={isReadOnly || isBulkApproving || isManualAdding || anchorReview.state === "not_ready"}
|
||||||
onClick={onManualAdd}
|
onClick={onManualAdd}
|
||||||
title="Ручное добавление ключа"
|
title={
|
||||||
|
isReadOnly
|
||||||
|
? "Snapshot открыт только для просмотра: ручные ключи добавляются в текущий контекст"
|
||||||
|
: "Ручное добавление ключа"
|
||||||
|
}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{isManualAdding ? <Loader2 className="spin" size={15} /> : <Plus size={15} />}
|
{isManualAdding ? <Loader2 className="spin" size={15} /> : <Plus size={15} />}
|
||||||
|
|
@ -4532,9 +4551,9 @@ function SeoAnchorReviewWorkspace({
|
||||||
<button
|
<button
|
||||||
aria-label="Удалить якорь"
|
aria-label="Удалить якорь"
|
||||||
className="tiny-action icon-only anchor-action-button"
|
className="tiny-action icon-only anchor-action-button"
|
||||||
disabled={isBulkApproving}
|
disabled={isReadOnly || isBulkApproving}
|
||||||
onClick={() => onDeleteRequest(anchor)}
|
onClick={() => onDeleteRequest(anchor)}
|
||||||
title="Удалить"
|
title={isReadOnly ? "Snapshot открыт только для просмотра" : "Удалить"}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<X size={14} />
|
<X size={14} />
|
||||||
|
|
@ -4542,7 +4561,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
<button
|
<button
|
||||||
aria-label="Скрыть якорь"
|
aria-label="Скрыть якорь"
|
||||||
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "disabled" ? " active" : ""}`}
|
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "disabled" ? " active" : ""}`}
|
||||||
disabled={isBulkApproving || isDisableBusy || anchor.decision.status === "disabled"}
|
disabled={isReadOnly || isBulkApproving || isDisableBusy || anchor.decision.status === "disabled"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onDecision(anchor, {
|
onDecision(anchor, {
|
||||||
anchorId: anchor.id,
|
anchorId: anchor.id,
|
||||||
|
|
@ -4550,7 +4569,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
status: "disabled"
|
status: "disabled"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
title="Скрыть"
|
title={isReadOnly ? "Snapshot открыт только для просмотра" : "Скрыть"}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{isDisableBusy ? <Loader2 className="spin" size={14} /> : <EyeOff size={14} />}
|
{isDisableBusy ? <Loader2 className="spin" size={14} /> : <EyeOff size={14} />}
|
||||||
|
|
@ -4558,7 +4577,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
<button
|
<button
|
||||||
aria-label="Оставить якорь на разборе"
|
aria-label="Оставить якорь на разборе"
|
||||||
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "pending" ? " active" : ""}`}
|
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "pending" ? " active" : ""}`}
|
||||||
disabled={isBulkApproving || isPendingBusy || anchor.decision.status === "pending"}
|
disabled={isReadOnly || isBulkApproving || isPendingBusy || anchor.decision.status === "pending"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onDecision(anchor, {
|
onDecision(anchor, {
|
||||||
anchorId: anchor.id,
|
anchorId: anchor.id,
|
||||||
|
|
@ -4566,7 +4585,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
status: "pending"
|
status: "pending"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
title="На разбор"
|
title={isReadOnly ? "Snapshot открыт только для просмотра" : "На разбор"}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{isPendingBusy ? <Loader2 className="spin" size={14} /> : <RefreshCw size={14} />}
|
{isPendingBusy ? <Loader2 className="spin" size={14} /> : <RefreshCw size={14} />}
|
||||||
|
|
@ -4574,7 +4593,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
<button
|
<button
|
||||||
aria-label="Добавить якорь в очередь Wordstat"
|
aria-label="Добавить якорь в очередь Wordstat"
|
||||||
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "approved" ? " active" : ""}`}
|
className={`tiny-action icon-only anchor-action-button${anchor.decision.status === "approved" ? " active" : ""}`}
|
||||||
disabled={isBulkApproving || isApproveBusy || anchor.decision.status === "approved"}
|
disabled={isReadOnly || isBulkApproving || isApproveBusy || anchor.decision.status === "approved"}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onDecision(anchor, {
|
onDecision(anchor, {
|
||||||
anchorId: anchor.id,
|
anchorId: anchor.id,
|
||||||
|
|
@ -4584,7 +4603,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
status: "approved"
|
status: "approved"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
title="В очередь Wordstat"
|
title={isReadOnly ? "Snapshot открыт только для просмотра" : "В очередь Wordstat"}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
{isApproveBusy ? <Loader2 className="spin" size={14} /> : <CheckCircle2 size={14} />}
|
{isApproveBusy ? <Loader2 className="spin" size={14} /> : <CheckCircle2 size={14} />}
|
||||||
|
|
@ -4611,7 +4630,7 @@ function SeoAnchorReviewWorkspace({
|
||||||
<NdcGlassSelect<DemandCollectionProfile>
|
<NdcGlassSelect<DemandCollectionProfile>
|
||||||
ariaLabel="Выбрать профиль сбора спроса"
|
ariaLabel="Выбрать профиль сбора спроса"
|
||||||
className="anchor-demand-profile-select"
|
className="anchor-demand-profile-select"
|
||||||
disabled={isBulkApproving || isManualAdding || isSavingDraft || anchorReview.state === "not_ready"}
|
disabled={isReadOnly || isBulkApproving || isManualAdding || isSavingDraft || anchorReview.state === "not_ready"}
|
||||||
menuClassName="anchor-demand-profile-menu"
|
menuClassName="anchor-demand-profile-menu"
|
||||||
onChange={onDemandProfileChange}
|
onChange={onDemandProfileChange}
|
||||||
options={demandCollectionProfileOptions}
|
options={demandCollectionProfileOptions}
|
||||||
|
|
@ -12488,7 +12507,8 @@ export function App() {
|
||||||
project: ProjectSummary,
|
project: ProjectSummary,
|
||||||
source: "expansion_checkpoint" | "manual_save",
|
source: "expansion_checkpoint" | "manual_save",
|
||||||
label?: string,
|
label?: string,
|
||||||
note?: string
|
note?: string,
|
||||||
|
options: { activate?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
const keywordMap = keywordMaps[project.id] ?? null;
|
const keywordMap = keywordMaps[project.id] ?? null;
|
||||||
const result = await saveKeywordContextSnapshot(project.id, {
|
const result = await saveKeywordContextSnapshot(project.id, {
|
||||||
|
|
@ -12506,17 +12526,19 @@ export function App() {
|
||||||
[project.id]: [result.snapshot, ...projectSnapshots.filter((snapshot) => snapshot.id !== result.snapshot.id)]
|
[project.id]: [result.snapshot, ...projectSnapshots.filter((snapshot) => snapshot.id !== result.snapshot.id)]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
if (options.activate !== false) {
|
||||||
setActiveKeywordContextSnapshotIds((currentIds) => ({
|
setActiveKeywordContextSnapshotIds((currentIds) => ({
|
||||||
...currentIds,
|
...currentIds,
|
||||||
[project.id]: result.snapshot.id
|
[project.id]: result.snapshot.id
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
return result.snapshot;
|
return result.snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveKeywordCurationLayoutToActiveSnapshot(project: ProjectSummary) {
|
async function saveKeywordCurationLayoutToActiveSnapshot(project: ProjectSummary) {
|
||||||
const projectSnapshots = keywordContextSnapshots[project.id] ?? [];
|
const projectSnapshots = keywordContextSnapshots[project.id] ?? [];
|
||||||
const activeSnapshotId = activeKeywordContextSnapshotIds[project.id] ?? projectSnapshots[0]?.id ?? "current";
|
const activeSnapshotId = activeKeywordContextSnapshotIds[project.id] ?? "current";
|
||||||
const activeSnapshot = projectSnapshots.find((snapshot) => snapshot.id === activeSnapshotId) ?? null;
|
const activeSnapshot = projectSnapshots.find((snapshot) => snapshot.id === activeSnapshotId) ?? null;
|
||||||
|
|
||||||
if (!activeSnapshot || activeSnapshotId === "current") {
|
if (!activeSnapshot || activeSnapshotId === "current") {
|
||||||
|
|
@ -12722,7 +12744,8 @@ export function App() {
|
||||||
project,
|
project,
|
||||||
"expansion_checkpoint",
|
"expansion_checkpoint",
|
||||||
`Расширение ключей · ${new Date().toLocaleString("ru-RU")}`,
|
`Расширение ключей · ${new Date().toLocaleString("ru-RU")}`,
|
||||||
"Checkpoint перед добором дополнительных вариантов: ручная канбан-раскладка не сбрасывается."
|
"Checkpoint перед добором дополнительных вариантов: ручная канбан-раскладка не сбрасывается.",
|
||||||
|
{ activate: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (keywordMap && (curation.roleOverrides.length > 0 || curation.suppressedItemIds.length > 0)) {
|
if (keywordMap && (curation.roleOverrides.length > 0 || curation.suppressedItemIds.length > 0)) {
|
||||||
|
|
@ -14285,9 +14308,13 @@ export function App() {
|
||||||
const activeProject = activeProjectId ? (projects.find((project) => project.id === activeProjectId) ?? null) : null;
|
const activeProject = activeProjectId ? (projects.find((project) => project.id === activeProjectId) ?? null) : null;
|
||||||
const activeProjectScanSummary = activeProject ? scanSummaries[activeProject.id] : null;
|
const activeProjectScanSummary = activeProject ? scanSummaries[activeProject.id] : null;
|
||||||
const activeKeywordContextSnapshotList = activeProject ? (keywordContextSnapshots[activeProject.id] ?? []) : [];
|
const activeKeywordContextSnapshotList = activeProject ? (keywordContextSnapshots[activeProject.id] ?? []) : [];
|
||||||
|
const requestedKeywordContextSnapshotId = activeProject
|
||||||
|
? (activeKeywordContextSnapshotIds[activeProject.id] ?? "current")
|
||||||
|
: "current";
|
||||||
const activeKeywordContextSnapshotId =
|
const activeKeywordContextSnapshotId =
|
||||||
activeProject && activeKeywordContextSnapshotList.length > 0
|
requestedKeywordContextSnapshotId !== "current" &&
|
||||||
? (activeKeywordContextSnapshotIds[activeProject.id] ?? activeKeywordContextSnapshotList[0]?.id ?? "current")
|
activeKeywordContextSnapshotList.some((snapshot) => snapshot.id === requestedKeywordContextSnapshotId)
|
||||||
|
? requestedKeywordContextSnapshotId
|
||||||
: "current";
|
: "current";
|
||||||
const activeKeywordContextSnapshotKey =
|
const activeKeywordContextSnapshotKey =
|
||||||
activeProject && activeKeywordContextSnapshotId !== "current"
|
activeProject && activeKeywordContextSnapshotId !== "current"
|
||||||
|
|
@ -15297,7 +15324,8 @@ export function App() {
|
||||||
project,
|
project,
|
||||||
"manual_save",
|
"manual_save",
|
||||||
`Автоснимок перед новым анализом · ${new Date().toLocaleString("ru-RU")}`,
|
`Автоснимок перед новым анализом · ${new Date().toLocaleString("ru-RU")}`,
|
||||||
"Safety snapshot: текущая карта ключей сохранена перед полным пересбором semantic/context/latest состояния проекта."
|
"Safety snapshot: текущая карта ключей сохранена перед полным пересбором semantic/context/latest состояния проекта.",
|
||||||
|
{ activate: false }
|
||||||
);
|
);
|
||||||
safetySnapshotLabel = safetySnapshot.label;
|
safetySnapshotLabel = safetySnapshot.label;
|
||||||
}
|
}
|
||||||
|
|
@ -19625,6 +19653,7 @@ export function App() {
|
||||||
isCollapsed={isAnchorStageCollapsed}
|
isCollapsed={isAnchorStageCollapsed}
|
||||||
isDemandProfileDirty={isAnchorReviewDemandProfileDirty}
|
isDemandProfileDirty={isAnchorReviewDemandProfileDirty}
|
||||||
isBulkApproving={isApprovingAnchorReview}
|
isBulkApproving={isApprovingAnchorReview}
|
||||||
|
isReadOnly={projectKeywordContextProfileStatus !== "current"}
|
||||||
isSavingDraft={savingAnchorReviewDraftProjectId === project.id}
|
isSavingDraft={savingAnchorReviewDraftProjectId === project.id}
|
||||||
isManualAdding={anchorReviewActionKey === `${project.id}:manual-anchor`}
|
isManualAdding={anchorReviewActionKey === `${project.id}:manual-anchor`}
|
||||||
onDecision={(_anchor, decision) => void handleAnchorReviewDecision(project, decision)}
|
onDecision={(_anchor, decision) => void handleAnchorReviewDecision(project, decision)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue