修复项目配置草稿原子保存逻辑
This commit is contained in:
@@ -50,15 +50,28 @@ describe("ProjectDetail setup draft publish workflow", () => {
|
||||
expect(source.indexOf("applySetupDraft(createDefaultSetupDraft(siteOptions.value));", functionIndex)).toBe(-1);
|
||||
});
|
||||
|
||||
it("does not let legacy empty projectInfo overwrite project master data", () => {
|
||||
it("applies setup drafts atomically including empty project info", () => {
|
||||
const source = readProjectDetail();
|
||||
const functionIndex = source.indexOf("const applySetupDraft = (draft: SetupConfigDraft) => {");
|
||||
const emptyGuardIndex = source.indexOf("!isProjectPublishSnapshotEmpty(projectInfo) || isSetupDraftEffectivelyEmpty(setupDraft)", functionIndex);
|
||||
const fallbackIndex = source.indexOf("setupDraft.projectInfo = buildProjectPublishSnapshot();", emptyGuardIndex);
|
||||
const nextFunctionIndex = source.indexOf("const loadLocalSetupDraft = ", functionIndex);
|
||||
const body = source.slice(functionIndex, nextFunctionIndex);
|
||||
|
||||
expect(functionIndex).toBeGreaterThan(-1);
|
||||
expect(emptyGuardIndex).toBeGreaterThan(functionIndex);
|
||||
expect(fallbackIndex).toBeGreaterThan(emptyGuardIndex);
|
||||
expect(body).toContain("setupDraft.projectInfo = projectInfo;");
|
||||
expect(body).toContain("applyProjectInfoDraft(projectInfo);");
|
||||
expect(body).not.toContain("shouldApplyProjectInfo");
|
||||
expect(body).not.toContain("isSetupDraftEffectivelyEmpty(setupDraft)");
|
||||
});
|
||||
|
||||
it("requires projectInfo in setup draft shape", () => {
|
||||
const source = readProjectDetail();
|
||||
const functionIndex = source.indexOf("const isSetupDraftShape = (value: unknown): value is SetupConfigDraft => {");
|
||||
const nextFunctionIndex = source.indexOf("const isProjectPublishSnapshotShape = ", functionIndex);
|
||||
const body = source.slice(functionIndex, nextFunctionIndex);
|
||||
|
||||
expect(functionIndex).toBeGreaterThan(-1);
|
||||
expect(body).toContain("isProjectPublishSnapshotShape(data.projectInfo)");
|
||||
expect(body).not.toContain("!Object.prototype.hasOwnProperty.call(data, \"projectInfo\")");
|
||||
});
|
||||
|
||||
it("keeps early termination out of regular visit schedule options", () => {
|
||||
|
||||
@@ -2672,6 +2672,7 @@ const setupWorkflowTagMeta = computed<SetupWorkflowTagMeta>(() => SETUP_WORKFLOW
|
||||
const setupWorkflowTagLabel = computed(() => setupWorkflowTagMeta.value.label);
|
||||
const setupWorkflowTagType = computed<"success" | "warning" | "info">(() => setupWorkflowTagMeta.value.type);
|
||||
const isSetupDraftEffectivelyEmpty = (draft: SetupConfigDraft): boolean => {
|
||||
if (!isProjectPublishSnapshotEmpty(draft.projectInfo)) return false;
|
||||
if (draft.projectMilestones.length > 0) return false;
|
||||
if (draft.siteMilestones.length > 0) return false;
|
||||
if (draft.siteEnrollmentPlans.length > 0) return false;
|
||||
@@ -2686,6 +2687,8 @@ const isSetupDraftEffectivelyEmpty = (draft: SetupConfigDraft): boolean => {
|
||||
if ((plan.stageBreakdown || "").trim()) return false;
|
||||
return true;
|
||||
};
|
||||
const isAtomicSetupDraft = (draft: SetupConfigDraft): boolean =>
|
||||
!isProjectPublishSnapshotEmpty(draft.projectInfo) || isSetupDraftEffectivelyEmpty(draft);
|
||||
const shouldShowEmptyDraftBaseInfo = computed(() => isSetupDraftEffectivelyEmpty(setupDraft));
|
||||
const setupDraftBaseVersionText = computed(() => {
|
||||
const explicit = (setupActiveDraftBaseVersion.value || "").trim();
|
||||
@@ -3988,7 +3991,7 @@ const isSetupDraftShape = (value: unknown): value is SetupConfigDraft => {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const data = value as Record<string, unknown>;
|
||||
return (
|
||||
(!Object.prototype.hasOwnProperty.call(data, "projectInfo") || isProjectPublishSnapshotShape(data.projectInfo)) &&
|
||||
isProjectPublishSnapshotShape(data.projectInfo) &&
|
||||
Array.isArray(data.projectMilestones) &&
|
||||
!!data.enrollmentPlan &&
|
||||
Array.isArray(data.siteMilestones) &&
|
||||
@@ -4009,8 +4012,9 @@ const isProjectPublishSnapshotShape = (value: unknown): value is ProjectPublishS
|
||||
);
|
||||
};
|
||||
|
||||
const isProjectPublishSnapshotEmpty = (snapshot: ProjectPublishSnapshot): boolean =>
|
||||
JSON.stringify(snapshot) === JSON.stringify(emptyProjectInfoDraft());
|
||||
function isProjectPublishSnapshotEmpty(snapshot: ProjectPublishSnapshot): boolean {
|
||||
return JSON.stringify(snapshot) === JSON.stringify(emptyProjectInfoDraft());
|
||||
}
|
||||
|
||||
const applyProjectInfoDraft = (snapshot: ProjectPublishSnapshot) => {
|
||||
suppressEnrollmentFieldSync.value = true;
|
||||
@@ -4047,11 +4051,7 @@ const applySetupDraft = (draft: SetupConfigDraft) => {
|
||||
setupDraft.siteEnrollmentPlans = normalizeSiteEnrollmentPlans(clone(draft.siteEnrollmentPlans));
|
||||
setupDraft.monitoringStrategies = clone(draft.monitoringStrategies);
|
||||
setupDraft.centerConfirm = clone(draft.centerConfirm);
|
||||
if (!isProjectPublishSnapshotEmpty(projectInfo) || isSetupDraftEffectivelyEmpty(setupDraft)) {
|
||||
applyProjectInfoDraft(projectInfo);
|
||||
} else {
|
||||
setupDraft.projectInfo = buildProjectPublishSnapshot();
|
||||
}
|
||||
applyProjectInfoDraft(projectInfo);
|
||||
formBaselineSnapshot.value = serializeFormForCompare();
|
||||
suppressDraftWatch.value = false;
|
||||
};
|
||||
@@ -4160,6 +4160,10 @@ const loadSetupDraftFromServer = async (): Promise<SetupConfigDraft | null> => {
|
||||
try {
|
||||
const { data } = await fetchSetupConfig(project.value.id);
|
||||
if (!isSetupDraftShape(data?.data)) return null;
|
||||
const draft = {
|
||||
...clone(data.data),
|
||||
projectInfo: isProjectPublishSnapshotShape(data.data.projectInfo) ? clone(data.data.projectInfo) : buildProjectPublishSnapshot(),
|
||||
};
|
||||
applySetupResponseMeta(data);
|
||||
updateLastServerSaveMeta(data.updated_at, data.saved_by_name || resolveSavedByDisplay(data.saved_by));
|
||||
setupSaveMeta.value = {
|
||||
@@ -4167,10 +4171,7 @@ const loadSetupDraftFromServer = async (): Promise<SetupConfigDraft | null> => {
|
||||
savedBy: data.saved_by_name || resolveSavedByDisplay(data.saved_by),
|
||||
serverSynced: true,
|
||||
};
|
||||
return {
|
||||
...clone(data.data),
|
||||
projectInfo: isProjectPublishSnapshotShape(data.data.projectInfo) ? clone(data.data.projectInfo) : buildProjectPublishSnapshot(),
|
||||
};
|
||||
return draft;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -4512,23 +4513,27 @@ const initializeSetupDraft = async () => {
|
||||
|
||||
const localDraft = loadLocalSetupDraft();
|
||||
if (localDraft) {
|
||||
const shouldRestore = await confirmRestoreLocalDraft("恢复立项配置草稿", localDraft.savedAt, {
|
||||
expired: localDraft.expired,
|
||||
conflict: localDraft.conflict,
|
||||
});
|
||||
if (shouldRestore) {
|
||||
applySetupDraft(localDraft.data);
|
||||
setupDirtySinceLastPersist.value = true;
|
||||
setupLocalDraftSavedAt.value = localDraft.savedAt;
|
||||
setupSaveMeta.value = {
|
||||
updatedAt: localDraft.savedAt || nowString(),
|
||||
savedBy: authStore.user?.full_name || authStore.user?.username || authStore.user?.email || "当前用户",
|
||||
serverSynced: false,
|
||||
};
|
||||
draftReady.value = true;
|
||||
return;
|
||||
if (!isAtomicSetupDraft(localDraft.data)) {
|
||||
clearLocalSetupDraft();
|
||||
} else {
|
||||
const shouldRestore = await confirmRestoreLocalDraft("恢复立项配置草稿", localDraft.savedAt, {
|
||||
expired: localDraft.expired,
|
||||
conflict: localDraft.conflict,
|
||||
});
|
||||
if (shouldRestore) {
|
||||
applySetupDraft(localDraft.data);
|
||||
setupDirtySinceLastPersist.value = true;
|
||||
setupLocalDraftSavedAt.value = localDraft.savedAt;
|
||||
setupSaveMeta.value = {
|
||||
updatedAt: localDraft.savedAt || nowString(),
|
||||
savedBy: authStore.user?.full_name || authStore.user?.username || authStore.user?.email || "当前用户",
|
||||
serverSynced: false,
|
||||
};
|
||||
draftReady.value = true;
|
||||
return;
|
||||
}
|
||||
clearLocalSetupDraft();
|
||||
}
|
||||
clearLocalSetupDraft();
|
||||
}
|
||||
|
||||
applySetupDraft(createDefaultSetupDraft(siteOptions.value));
|
||||
|
||||
Reference in New Issue
Block a user