mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-09-06 12:08:39 +00:00
fix(editor): 禁止缺少变更记录的历史回滚
This commit is contained in:
parent
27b2c2c685
commit
10b70c36bb
@ -26,7 +26,7 @@
|
|||||||
isCurrent: s.isCurrent,
|
isCurrent: s.isCurrent,
|
||||||
desc: describeStep(s.step),
|
desc: describeStep(s.step),
|
||||||
diffable: isStepDiffable ? isStepDiffable(s.step) : false,
|
diffable: isStepDiffable ? isStepDiffable(s.step) : false,
|
||||||
revertable: s.applied,
|
revertable: s.applied && (isStepRevertable ? isStepRevertable(s.step) : true),
|
||||||
source: s.step.source,
|
source: s.step.source,
|
||||||
time: formatHistoryTime(s.step.timestamp),
|
time: formatHistoryTime(s.step.timestamp),
|
||||||
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
||||||
@ -94,6 +94,8 @@ const props = withDefaults(
|
|||||||
describeStep: (_step: any) => string;
|
describeStep: (_step: any) => string;
|
||||||
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
|
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入;不传则一律不展示差异入口。 */
|
||||||
isStepDiffable?: (_step: any) => boolean;
|
isStepDiffable?: (_step: any) => boolean;
|
||||||
|
/** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。由父组件按业务类型注入;不传则已应用即可回滚。 */
|
||||||
|
isStepRevertable?: (_step: any) => boolean;
|
||||||
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
|
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
|
||||||
expanded: Record<string, boolean>;
|
expanded: Record<string, boolean>;
|
||||||
/** 是否支持「跳转到该记录」(goto)。默认 true。 */
|
/** 是否支持「跳转到该记录」(goto)。默认 true。 */
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
:describe-group="describeGroup"
|
:describe-group="describeGroup"
|
||||||
:describe-step="describeStep"
|
:describe-step="describeStep"
|
||||||
:is-step-diffable="isStepDiffable"
|
:is-step-diffable="isStepDiffable"
|
||||||
|
:is-step-revertable="isStepRevertable"
|
||||||
:expanded="expanded"
|
:expanded="expanded"
|
||||||
:goto-enabled="gotoEnabled"
|
:goto-enabled="gotoEnabled"
|
||||||
@toggle="(key: string) => $emit('toggle', key)"
|
@toggle="(key: string) => $emit('toggle', key)"
|
||||||
@ -48,6 +49,8 @@ withDefaults(
|
|||||||
describeStep: (_step: any) => string;
|
describeStep: (_step: any) => string;
|
||||||
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入。 */
|
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入。 */
|
||||||
isStepDiffable: (_step: any) => boolean;
|
isStepDiffable: (_step: any) => boolean;
|
||||||
|
/** 判断某个 step 是否支持回滚(如更新需带 changeRecords)。由父组件按业务类型注入;不传则已应用即可回滚。 */
|
||||||
|
isStepRevertable?: (_step: any) => boolean;
|
||||||
/**
|
/**
|
||||||
* 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
|
* 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
|
||||||
* 本 tab 使用 `${prefix}-${id}-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
|
* 本 tab 使用 `${prefix}-${id}-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
|
||||||
|
|||||||
@ -193,7 +193,8 @@ const targetText = computed(() => {
|
|||||||
'data-source': '数据源',
|
'data-source': '数据源',
|
||||||
'code-block': '代码块',
|
'code-block': '代码块',
|
||||||
};
|
};
|
||||||
const prefix = categoryText[payload.value.category] || '';
|
const { category } = payload.value;
|
||||||
|
const prefix = category ? categoryText[category] : '';
|
||||||
const label = payload.value.targetLabel || payload.value.type || '';
|
const label = payload.value.targetLabel || payload.value.type || '';
|
||||||
const { id } = payload.value;
|
const { id } = payload.value;
|
||||||
const labelWithId = id !== undefined && id !== '' ? `${label}(${id})` : label;
|
const labelWithId = id !== undefined && id !== '' ? `${label}(${id})` : label;
|
||||||
|
|||||||
@ -44,6 +44,7 @@
|
|||||||
:describe-group="describeDataSourceGroup"
|
:describe-group="describeDataSourceGroup"
|
||||||
:describe-step="describeDataSourceStep"
|
:describe-step="describeDataSourceStep"
|
||||||
:is-step-diffable="isDataSourceStepDiffable"
|
:is-step-diffable="isDataSourceStepDiffable"
|
||||||
|
:is-step-revertable="isDataSourceStepRevertable"
|
||||||
@toggle="toggleGroup"
|
@toggle="toggleGroup"
|
||||||
@goto="onDataSourceGoto"
|
@goto="onDataSourceGoto"
|
||||||
@goto-initial="onDataSourceGotoInitial"
|
@goto-initial="onDataSourceGotoInitial"
|
||||||
@ -65,6 +66,7 @@
|
|||||||
:describe-group="describeCodeBlockGroup"
|
:describe-group="describeCodeBlockGroup"
|
||||||
:describe-step="describeCodeBlockStep"
|
:describe-step="describeCodeBlockStep"
|
||||||
:is-step-diffable="isCodeBlockStepDiffable"
|
:is-step-diffable="isCodeBlockStepDiffable"
|
||||||
|
:is-step-revertable="isCodeBlockStepRevertable"
|
||||||
@toggle="toggleGroup"
|
@toggle="toggleGroup"
|
||||||
@goto="onCodeBlockGoto"
|
@goto="onCodeBlockGoto"
|
||||||
@goto-initial="onCodeBlockGotoInitial"
|
@goto-initial="onCodeBlockGotoInitial"
|
||||||
@ -141,6 +143,8 @@ import {
|
|||||||
describeCodeBlockStep,
|
describeCodeBlockStep,
|
||||||
describeDataSourceGroup,
|
describeDataSourceGroup,
|
||||||
describeDataSourceStep,
|
describeDataSourceStep,
|
||||||
|
isCodeBlockStepRevertable,
|
||||||
|
isDataSourceStepRevertable,
|
||||||
useHistoryList,
|
useHistoryList,
|
||||||
} from './composables';
|
} from './composables';
|
||||||
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
isCurrent: s.isCurrent,
|
isCurrent: s.isCurrent,
|
||||||
desc: describePageStep(s.step),
|
desc: describePageStep(s.step),
|
||||||
diffable: isPageStepDiffable(s.step),
|
diffable: isPageStepDiffable(s.step),
|
||||||
revertable: s.applied,
|
revertable: s.applied && isPageStepRevertable(s.step),
|
||||||
source: s.step.source,
|
source: s.step.source,
|
||||||
time: formatHistoryTime(s.step.timestamp),
|
time: formatHistoryTime(s.step.timestamp),
|
||||||
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
timeTitle: formatHistoryFullTime(s.step.timestamp),
|
||||||
@ -57,6 +57,7 @@ import {
|
|||||||
formatHistoryTime,
|
formatHistoryTime,
|
||||||
groupSource,
|
groupSource,
|
||||||
groupTimestamp,
|
groupTimestamp,
|
||||||
|
isPageStepRevertable,
|
||||||
} from './composables';
|
} from './composables';
|
||||||
import GroupRow from './GroupRow.vue';
|
import GroupRow from './GroupRow.vue';
|
||||||
import InitialRow from './InitialRow.vue';
|
import InitialRow from './InitialRow.vue';
|
||||||
|
|||||||
@ -251,3 +251,36 @@ export const describeCodeBlockGroup = (group: CodeBlockHistoryGroup) => {
|
|||||||
const target = labelWithId(rawName, group.id);
|
const target = labelWithId(rawName, group.id);
|
||||||
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
return pathList ? `修改 ${target} · ${pathList}${paths.size > 3 ? '…' : ''}` : `修改 ${target}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 页面 step 是否支持「回滚」(类 git revert):
|
||||||
|
* - 新增 / 删除:不依赖 changeRecords,反向应用即删除 / 加回,始终可回滚;
|
||||||
|
* - 更新:必须每个被更新节点都带有 changeRecords,才支持按 propPath 局部反向 patch。
|
||||||
|
* 缺失 changeRecords 的更新只能整节点替换,会冲掉该节点后续的无关变更,因此不支持回滚。
|
||||||
|
*/
|
||||||
|
export const isPageStepRevertable = (step: StepValue): boolean => {
|
||||||
|
if (step.opType !== 'update') return true;
|
||||||
|
const items = step.updatedItems ?? [];
|
||||||
|
if (!items.length) return false;
|
||||||
|
return items.every((item) => Boolean(item.changeRecords?.length));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源 step 是否支持「回滚」:
|
||||||
|
* - 新增(oldSchema=null)/ 删除(newSchema=null):不依赖 changeRecords,始终可回滚;
|
||||||
|
* - 更新(前后 schema 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
||||||
|
*/
|
||||||
|
export const isDataSourceStepRevertable = (step: DataSourceStepValue): boolean => {
|
||||||
|
if (step.oldSchema === null || step.newSchema === null) return true;
|
||||||
|
return Boolean(step.changeRecords?.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代码块 step 是否支持「回滚」:
|
||||||
|
* - 新增(oldContent=null)/ 删除(newContent=null):不依赖 changeRecords,始终可回滚;
|
||||||
|
* - 更新(前后 content 都存在):必须有 changeRecords 才支持局部反向 patch,否则不支持回滚。
|
||||||
|
*/
|
||||||
|
export const isCodeBlockStepRevertable = (step: CodeBlockStepValue): boolean => {
|
||||||
|
if (step.oldContent === null || step.newContent === null) return true;
|
||||||
|
return Boolean(step.changeRecords?.length);
|
||||||
|
};
|
||||||
|
|||||||
@ -394,6 +394,8 @@ class CodeBlock extends BaseService {
|
|||||||
const list = historyService.getCodeBlockStepList(id);
|
const list = historyService.getCodeBlockStepList(id);
|
||||||
const entry = list[index];
|
const entry = list[index];
|
||||||
if (!entry?.applied) return null;
|
if (!entry?.applied) return null;
|
||||||
|
// 更新类步骤(前后 content 都存在)必须带 changeRecords 才支持回滚,否则只能整内容替换,会冲掉后续无关变更。
|
||||||
|
if (entry.step.oldContent && entry.step.newContent && !entry.step.changeRecords?.length) return null;
|
||||||
const description = `回滚 #${index + 1}: ${describeRevertCodeBlockStep(entry.step)}`;
|
const description = `回滚 #${index + 1}: ${describeRevertCodeBlockStep(entry.step)}`;
|
||||||
return await this.applyRevertStep(entry.step, description);
|
return await this.applyRevertStep(entry.step, description);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -297,6 +297,8 @@ class DataSource extends BaseService {
|
|||||||
const list = historyService.getDataSourceStepList(id);
|
const list = historyService.getDataSourceStepList(id);
|
||||||
const entry = list[index];
|
const entry = list[index];
|
||||||
if (!entry?.applied) return null;
|
if (!entry?.applied) return null;
|
||||||
|
// 更新类步骤(前后 schema 都存在)必须带 changeRecords 才支持回滚,否则只能整 schema 替换,会冲掉后续无关变更。
|
||||||
|
if (entry.step.oldSchema && entry.step.newSchema && !entry.step.changeRecords?.length) return null;
|
||||||
const description = `回滚 #${index + 1}: ${describeRevertDataSourceStep(entry.step)}`;
|
const description = `回滚 #${index + 1}: ${describeRevertDataSourceStep(entry.step)}`;
|
||||||
return this.applyRevertStep(entry.step, description);
|
return this.applyRevertStep(entry.step, description);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1239,6 +1239,12 @@ class Editor extends BaseService {
|
|||||||
const root = this.get('root');
|
const root = this.get('root');
|
||||||
if (!root) return null;
|
if (!root) return null;
|
||||||
|
|
||||||
|
// 更新类步骤必须带 changeRecords 才支持回滚:缺失时只能整节点替换,会冲掉后续无关变更,故不支持。
|
||||||
|
if (step.opType === 'update') {
|
||||||
|
const items = step.updatedItems ?? [];
|
||||||
|
if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
|
||||||
|
}
|
||||||
|
|
||||||
// 反向应用产生的新 step 由内部 pushOpHistory 触发 history `change` 事件,监听一次以拿到引用。
|
// 反向应用产生的新 step 由内部 pushOpHistory 触发 history `change` 事件,监听一次以拿到引用。
|
||||||
let revertedStep: StepValue | null = null;
|
let revertedStep: StepValue | null = null;
|
||||||
const captureRevert = (s: StepValue) => {
|
const captureRevert = (s: StepValue) => {
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 4px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
bottom: 15px;
|
bottom: 15px;
|
||||||
z-index: 30;
|
z-index: 32;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -70,7 +70,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 15px;
|
right: 15px;
|
||||||
bottom: 60px;
|
bottom: 60px;
|
||||||
z-index: 30;
|
z-index: 31;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -82,7 +82,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-editor-resizer {
|
.m-editor-resizer {
|
||||||
|
|||||||
@ -1186,7 +1186,7 @@ export interface DslOpOptions extends HistoryOpOptions {
|
|||||||
/** 差异对话框的入参 */
|
/** 差异对话框的入参 */
|
||||||
export interface DiffDialogPayload {
|
export interface DiffDialogPayload {
|
||||||
/** 表单类别 */
|
/** 表单类别 */
|
||||||
category: CompareCategory;
|
category?: CompareCategory;
|
||||||
/** 节点类型 / 数据源类型 */
|
/** 节点类型 / 数据源类型 */
|
||||||
type?: string;
|
type?: string;
|
||||||
/** 代码块场景下的数据源类型 */
|
/** 代码块场景下的数据源类型 */
|
||||||
|
|||||||
@ -17,6 +17,9 @@ import {
|
|||||||
formatHistoryFullTime,
|
formatHistoryFullTime,
|
||||||
formatHistoryTime,
|
formatHistoryTime,
|
||||||
groupTimestamp,
|
groupTimestamp,
|
||||||
|
isCodeBlockStepRevertable,
|
||||||
|
isDataSourceStepRevertable,
|
||||||
|
isPageStepRevertable,
|
||||||
opLabel,
|
opLabel,
|
||||||
useHistoryList,
|
useHistoryList,
|
||||||
} from '@editor/layouts/history-list/composables';
|
} from '@editor/layouts/history-list/composables';
|
||||||
@ -607,3 +610,82 @@ describe('useHistoryList', () => {
|
|||||||
expect(buckets.map((b) => b.id).sort()).toEqual(['code_1', 'code_2']);
|
expect(buckets.map((b) => b.id).sort()).toEqual(['code_1', 'code_2']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isPageStepRevertable', () => {
|
||||||
|
test('add / remove 始终可回滚', () => {
|
||||||
|
expect(isPageStepRevertable({ opType: 'add', nodes: [{ id: 'n1' }] } as any)).toBe(true);
|
||||||
|
expect(isPageStepRevertable({ opType: 'remove', removedItems: [{ node: { id: 'n1' } }] } as any)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update 每项都有 changeRecords 才可回滚', () => {
|
||||||
|
expect(
|
||||||
|
isPageStepRevertable({
|
||||||
|
opType: 'update',
|
||||||
|
updatedItems: [{ oldNode: { id: 'n1' }, newNode: { id: 'n1' }, changeRecords: [{ propPath: 'style.color' }] }],
|
||||||
|
} as any),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update 缺少 changeRecords 不可回滚', () => {
|
||||||
|
expect(
|
||||||
|
isPageStepRevertable({
|
||||||
|
opType: 'update',
|
||||||
|
updatedItems: [{ oldNode: { id: 'n1' }, newNode: { id: 'n1' } }],
|
||||||
|
} as any),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update 多项中任一缺少 changeRecords 不可回滚', () => {
|
||||||
|
expect(
|
||||||
|
isPageStepRevertable({
|
||||||
|
opType: 'update',
|
||||||
|
updatedItems: [
|
||||||
|
{ oldNode: { id: 'n1' }, newNode: { id: 'n1' }, changeRecords: [{ propPath: 'a' }] },
|
||||||
|
{ oldNode: { id: 'n2' }, newNode: { id: 'n2' } },
|
||||||
|
],
|
||||||
|
} as any),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update 无 updatedItems 不可回滚', () => {
|
||||||
|
expect(isPageStepRevertable({ opType: 'update' } as any)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isDataSourceStepRevertable', () => {
|
||||||
|
test('新增 / 删除 始终可回滚', () => {
|
||||||
|
expect(isDataSourceStepRevertable({ oldSchema: null, newSchema: { id: 'ds_1' } } as any)).toBe(true);
|
||||||
|
expect(isDataSourceStepRevertable({ oldSchema: { id: 'ds_1' }, newSchema: null } as any)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('更新有 changeRecords 才可回滚', () => {
|
||||||
|
expect(
|
||||||
|
isDataSourceStepRevertable({
|
||||||
|
oldSchema: { id: 'ds_1' },
|
||||||
|
newSchema: { id: 'ds_1' },
|
||||||
|
changeRecords: [{ propPath: 'title' }],
|
||||||
|
} as any),
|
||||||
|
).toBe(true);
|
||||||
|
expect(isDataSourceStepRevertable({ oldSchema: { id: 'ds_1' }, newSchema: { id: 'ds_1' } } as any)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isCodeBlockStepRevertable', () => {
|
||||||
|
test('新增 / 删除 始终可回滚', () => {
|
||||||
|
expect(isCodeBlockStepRevertable({ oldContent: null, newContent: { id: 'code_1' } } as any)).toBe(true);
|
||||||
|
expect(isCodeBlockStepRevertable({ oldContent: { id: 'code_1' }, newContent: null } as any)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('更新有 changeRecords 才可回滚', () => {
|
||||||
|
expect(
|
||||||
|
isCodeBlockStepRevertable({
|
||||||
|
oldContent: { id: 'code_1' },
|
||||||
|
newContent: { id: 'code_1' },
|
||||||
|
changeRecords: [{ propPath: 'content' }],
|
||||||
|
} as any),
|
||||||
|
).toBe(true);
|
||||||
|
expect(isCodeBlockStepRevertable({ oldContent: { id: 'code_1' }, newContent: { id: 'code_1' } } as any)).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user