立项配置版本管理优化(回滚逻辑、UI、下载功能)
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { apiGet, apiPatch, apiPost, apiDelete, apiPut } from "./axios";
|
||||
import type { ApiListResponse, Study } from "../types/api";
|
||||
import type {
|
||||
StudySetupConfigCheckoutBranchPayload,
|
||||
StudySetupConfigDraftActionPayload,
|
||||
StudySetupConfigPublishPayload,
|
||||
StudySetupConfigMergeMainPayload,
|
||||
StudySetupConfigResponse,
|
||||
StudySetupConfigRollbackPayload,
|
||||
StudySetupConfigUpsertPayload,
|
||||
@@ -42,3 +45,17 @@ export const deleteStudySetupConfigVersion = (studyId: string, targetVersion: nu
|
||||
|
||||
export const rollbackStudySetupConfig = (studyId: string, payload: StudySetupConfigRollbackPayload) =>
|
||||
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/rollback`, payload, { suppressErrorMessage: true });
|
||||
|
||||
export const checkoutStudySetupConfigBranchDraft = (studyId: string, payload: StudySetupConfigCheckoutBranchPayload) =>
|
||||
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/draft/checkout-branch`, payload, {
|
||||
suppressErrorMessage: true,
|
||||
});
|
||||
|
||||
export const clearStudySetupConfigDraft = (studyId: string, payload: StudySetupConfigDraftActionPayload) =>
|
||||
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/draft/clear`, payload, { suppressErrorMessage: true });
|
||||
|
||||
export const refillStudySetupConfigDraft = (studyId: string, payload: StudySetupConfigDraftActionPayload) =>
|
||||
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/draft/refill`, payload, { suppressErrorMessage: true });
|
||||
|
||||
export const mergeStudySetupConfigToMain = (studyId: string, payload: StudySetupConfigMergeMainPayload) =>
|
||||
apiPost<StudySetupConfigResponse>(`/api/v1/studies/${studyId}/setup-config/merge-main`, payload, { suppressErrorMessage: true });
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import {
|
||||
checkoutStudySetupConfigBranchDraft,
|
||||
clearStudySetupConfigDraft,
|
||||
deleteStudySetupConfigVersion,
|
||||
fetchStudySetupConfig,
|
||||
fetchStudySetupConfigVersions,
|
||||
mergeStudySetupConfigToMain,
|
||||
publishStudySetupConfig,
|
||||
refillStudySetupConfigDraft,
|
||||
rollbackStudySetupConfig,
|
||||
saveStudySetupConfig,
|
||||
} from "../api/studies";
|
||||
import type {
|
||||
StudySetupConfigCheckoutBranchPayload,
|
||||
StudySetupConfigDraftActionPayload,
|
||||
StudySetupConfigMergeMainPayload,
|
||||
StudySetupConfigPublishPayload,
|
||||
StudySetupConfigRollbackPayload,
|
||||
StudySetupConfigUpsertPayload,
|
||||
@@ -16,16 +23,25 @@ export const useSetupConfig = () => {
|
||||
const getConfig = (studyId: string) => fetchStudySetupConfig(studyId);
|
||||
const saveConfig = (studyId: string, payload: StudySetupConfigUpsertPayload) => saveStudySetupConfig(studyId, payload);
|
||||
const publishConfig = (studyId: string, payload: StudySetupConfigPublishPayload) => publishStudySetupConfig(studyId, payload);
|
||||
const mergeToMain = (studyId: string, payload: StudySetupConfigMergeMainPayload) => mergeStudySetupConfigToMain(studyId, payload);
|
||||
const listVersions = (studyId: string) => fetchStudySetupConfigVersions(studyId);
|
||||
const rollbackConfig = (studyId: string, payload: StudySetupConfigRollbackPayload) => rollbackStudySetupConfig(studyId, payload);
|
||||
const checkoutBranchDraft = (studyId: string, payload: StudySetupConfigCheckoutBranchPayload) =>
|
||||
checkoutStudySetupConfigBranchDraft(studyId, payload);
|
||||
const clearDraft = (studyId: string, payload: StudySetupConfigDraftActionPayload) => clearStudySetupConfigDraft(studyId, payload);
|
||||
const refillDraft = (studyId: string, payload: StudySetupConfigDraftActionPayload) => refillStudySetupConfigDraft(studyId, payload);
|
||||
const deleteVersion = (studyId: string, targetVersion: number) => deleteStudySetupConfigVersion(studyId, targetVersion);
|
||||
|
||||
return {
|
||||
getConfig,
|
||||
saveConfig,
|
||||
publishConfig,
|
||||
mergeToMain,
|
||||
listVersions,
|
||||
rollbackConfig,
|
||||
checkoutBranchDraft,
|
||||
clearDraft,
|
||||
refillDraft,
|
||||
deleteVersion,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -98,6 +98,11 @@ export interface StudySetupConfigResponse {
|
||||
id: string;
|
||||
study_id: string;
|
||||
version: number;
|
||||
current_branch_name?: string;
|
||||
current_published_version_id?: string | null;
|
||||
current_published_version_label?: string | null;
|
||||
active_branch_base_version_id?: string | null;
|
||||
active_branch_base_version_label?: string | null;
|
||||
data: SetupConfigDraft;
|
||||
publish_status: "DRAFT" | "PUBLISHED" | string;
|
||||
published_data?: SetupConfigDraft | null;
|
||||
@@ -134,14 +139,36 @@ export interface StudySetupConfigRollbackPayload {
|
||||
target_version: number;
|
||||
}
|
||||
|
||||
export interface StudySetupConfigCheckoutBranchPayload {
|
||||
expected_version?: number | null;
|
||||
target_version: number;
|
||||
}
|
||||
|
||||
export interface StudySetupConfigMergeMainPayload {
|
||||
expected_version?: number | null;
|
||||
source_version: number;
|
||||
}
|
||||
|
||||
export interface StudySetupConfigDraftActionPayload {
|
||||
expected_version?: number | null;
|
||||
}
|
||||
|
||||
export interface StudySetupConfigVersionItem {
|
||||
id: string;
|
||||
study_id: string;
|
||||
study_setup_config_id: string;
|
||||
version: number;
|
||||
display_version: number;
|
||||
branch_name?: string;
|
||||
branch_seq?: number;
|
||||
version_label?: string;
|
||||
source_version?: number | null;
|
||||
parent_version_id?: string | null;
|
||||
merged_from_version_id?: string | null;
|
||||
config: SetupConfigDraft;
|
||||
published_project_snapshot?: ProjectPublishSnapshot | null;
|
||||
is_current_published?: boolean;
|
||||
is_active_draft_base?: boolean;
|
||||
published_by?: string | null;
|
||||
published_by_name?: string | null;
|
||||
published_at: string;
|
||||
|
||||
@@ -107,6 +107,38 @@ const getArrayRowIdentity = (moduleKey: keyof SetupConfigDraft, value: unknown):
|
||||
return "";
|
||||
};
|
||||
|
||||
type ArrayRowEntry = {
|
||||
key: string;
|
||||
value: unknown;
|
||||
rowIdentity: string;
|
||||
};
|
||||
|
||||
const buildArrayRowEntry = (moduleKey: keyof SetupConfigDraft, value: unknown, index: number): ArrayRowEntry => {
|
||||
const rowIdentity = getArrayRowIdentity(moduleKey, value);
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
const id = String((value as Record<string, unknown>).id || "").trim();
|
||||
if (id) {
|
||||
return {
|
||||
key: `id:${id}`,
|
||||
value,
|
||||
rowIdentity,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (rowIdentity) {
|
||||
return {
|
||||
key: `identity:${rowIdentity}`,
|
||||
value,
|
||||
rowIdentity,
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: `index:${index}`,
|
||||
value,
|
||||
rowIdentity: "",
|
||||
};
|
||||
};
|
||||
|
||||
const formatReadablePath = (moduleKey: keyof SetupConfigDraft, path: string): string => {
|
||||
let normalized = path;
|
||||
if (path === moduleKey) {
|
||||
@@ -247,11 +279,47 @@ const collectDiffRows = (
|
||||
if (Array.isArray(localValue) || Array.isArray(serverValue)) {
|
||||
const localArr = Array.isArray(localValue) ? localValue : [];
|
||||
const serverArr = Array.isArray(serverValue) ? serverValue : [];
|
||||
const maxLen = Math.max(localArr.length, serverArr.length);
|
||||
for (let i = 0; i < maxLen; i += 1) {
|
||||
const rowIdentity = getArrayRowIdentity(moduleKey, localArr[i] ?? serverArr[i]);
|
||||
const indexLabel = rowIdentity ? `${i}|${rowIdentity}` : `${i}`;
|
||||
collectDiffRows(moduleKey, moduleLabel, localArr[i], serverArr[i], `${path}[${indexLabel}]`, rows);
|
||||
|
||||
const localEntryMap = new Map<string, ArrayRowEntry[]>();
|
||||
localArr.forEach((item, index) => {
|
||||
const entry = buildArrayRowEntry(moduleKey, item, index);
|
||||
const list = localEntryMap.get(entry.key);
|
||||
if (list) {
|
||||
list.push(entry);
|
||||
} else {
|
||||
localEntryMap.set(entry.key, [entry]);
|
||||
}
|
||||
});
|
||||
|
||||
const consumeLocalEntry = (key: string): ArrayRowEntry | null => {
|
||||
const list = localEntryMap.get(key);
|
||||
if (!list || list.length === 0) return null;
|
||||
const entry = list.shift() || null;
|
||||
if (list.length === 0) localEntryMap.delete(key);
|
||||
return entry;
|
||||
};
|
||||
|
||||
let pairIndex = 0;
|
||||
serverArr.forEach((item, index) => {
|
||||
const serverEntry = buildArrayRowEntry(moduleKey, item, index);
|
||||
const localEntry = consumeLocalEntry(serverEntry.key);
|
||||
const rowIdentity = (localEntry?.rowIdentity || serverEntry.rowIdentity || "").trim();
|
||||
const indexLabel = rowIdentity ? `${pairIndex}|${rowIdentity}` : `${pairIndex}`;
|
||||
collectDiffRows(moduleKey, moduleLabel, localEntry?.value, serverEntry.value, `${path}[${indexLabel}]`, rows);
|
||||
pairIndex += 1;
|
||||
});
|
||||
|
||||
localEntryMap.forEach((list) => {
|
||||
list.forEach((localEntry) => {
|
||||
const rowIdentity = localEntry.rowIdentity.trim();
|
||||
const indexLabel = rowIdentity ? `${pairIndex}|${rowIdentity}` : `${pairIndex}`;
|
||||
collectDiffRows(moduleKey, moduleLabel, localEntry.value, undefined, `${path}[${indexLabel}]`, rows);
|
||||
pairIndex += 1;
|
||||
});
|
||||
});
|
||||
if (localArr.length === 0 && serverArr.length === 0) {
|
||||
// Keep behavior explicit for empty array pair to avoid extra recursion.
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ export type SetupWorkflowStatus = "UNSAVED_DRAFT" | "SAVED_DRAFT" | "PUBLISHED";
|
||||
export type SetupWorkflowTagMeta = { label: string; type: "success" | "warning" | "info" };
|
||||
|
||||
export const SETUP_WORKFLOW_TAG_META: Record<SetupWorkflowStatus, SetupWorkflowTagMeta> = {
|
||||
UNSAVED_DRAFT: { label: "草稿未保存", type: "warning" },
|
||||
SAVED_DRAFT: { label: "草稿已保存(未发布)", type: "info" },
|
||||
UNSAVED_DRAFT: { label: "未保存", type: "warning" },
|
||||
SAVED_DRAFT: { label: "已保存", type: "info" },
|
||||
PUBLISHED: { label: "已发布", type: "success" },
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user