mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-05 01:32:10 +08:00
refactor(editor): 历史记录数据源/代码块 tab 复用通用 BucketTab
This commit is contained in:
parent
1cd69b33fe
commit
7a161cab00
@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
<ul class="m-editor-history-list-ul">
|
<ul class="m-editor-history-list-ul">
|
||||||
<GroupRow
|
<GroupRow
|
||||||
v-for="(group, gIdx) in groups"
|
v-for="group in groups"
|
||||||
:key="`${prefix}-${bucketId}-${gIdx}`"
|
:key="`${prefix}-${bucketId}-${group.steps[0]?.index}`"
|
||||||
:group-key="`${prefix}-${bucketId}-${gIdx}`"
|
:group-key="`${prefix}-${bucketId}-${group.steps[0]?.index}`"
|
||||||
:applied="group.applied"
|
:applied="group.applied"
|
||||||
:merged="group.steps.length > 1"
|
:merged="group.steps.length > 1"
|
||||||
:op-type="group.opType"
|
:op-type="group.opType"
|
||||||
@ -27,7 +27,8 @@
|
|||||||
}))
|
}))
|
||||||
"
|
"
|
||||||
:is-current="group.isCurrent"
|
:is-current="group.isCurrent"
|
||||||
:expanded="!!expanded[`${prefix}-${bucketId}-${gIdx}`]"
|
:expanded="!!expanded[`${prefix}-${bucketId}-${group.steps[0]?.index}`]"
|
||||||
|
:goto-enabled="gotoEnabled"
|
||||||
@toggle="(key: string) => $emit('toggle', key)"
|
@toggle="(key: string) => $emit('toggle', key)"
|
||||||
@goto="(index: number) => $emit('goto', bucketId, index)"
|
@goto="(index: number) => $emit('goto', bucketId, index)"
|
||||||
@diff-step="(index: number) => $emit('diff-step', bucketId, index)"
|
@diff-step="(index: number) => $emit('diff-step', bucketId, index)"
|
||||||
@ -41,6 +42,7 @@
|
|||||||
<InitialRow
|
<InitialRow
|
||||||
v-if="showInitial !== false"
|
v-if="showInitial !== false"
|
||||||
:is-current="isInitial"
|
:is-current="isInitial"
|
||||||
|
:goto-enabled="gotoEnabled"
|
||||||
@goto-initial="$emit('goto-initial', bucketId)"
|
@goto-initial="$emit('goto-initial', bucketId)"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
@ -87,9 +89,12 @@ const props = withDefaults(
|
|||||||
isStepDiffable?: (_step: any) => boolean;
|
isStepDiffable?: (_step: any) => boolean;
|
||||||
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
|
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护以便跨 tab 复用。 */
|
||||||
expanded: Record<string, boolean>;
|
expanded: Record<string, boolean>;
|
||||||
|
/** 是否支持「跳转到该记录」(goto)。默认 true。 */
|
||||||
|
gotoEnabled?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
showInitial: true,
|
showInitial: true,
|
||||||
|
gotoEnabled: true,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
77
packages/editor/src/layouts/history-list/BucketTab.vue
Normal file
77
packages/editor/src/layouts/history-list/BucketTab.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="!buckets.length" class="m-editor-history-list-empty">暂无操作记录</div>
|
||||||
|
<TMagicScrollbar v-else max-height="360px">
|
||||||
|
<Bucket
|
||||||
|
v-for="bucket in buckets"
|
||||||
|
:key="`${prefix}-${bucket.id}`"
|
||||||
|
:title="title"
|
||||||
|
:bucket-id="bucket.id"
|
||||||
|
:prefix="prefix"
|
||||||
|
:groups="bucket.groups"
|
||||||
|
:describe-group="describeGroup"
|
||||||
|
:describe-step="describeStep"
|
||||||
|
:is-step-diffable="isStepDiffable"
|
||||||
|
:expanded="expanded"
|
||||||
|
:goto-enabled="gotoEnabled"
|
||||||
|
@toggle="(key: string) => $emit('toggle', key)"
|
||||||
|
@goto="(id: string | number, index: number) => $emit('goto', id, index)"
|
||||||
|
@goto-initial="(id: string | number) => $emit('goto-initial', id)"
|
||||||
|
@diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
|
||||||
|
@revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
|
||||||
|
/>
|
||||||
|
</TMagicScrollbar>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { TMagicScrollbar } from '@tmagic/design';
|
||||||
|
|
||||||
|
import Bucket from './Bucket.vue';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'MEditorHistoryListBucketTab',
|
||||||
|
});
|
||||||
|
|
||||||
|
withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
/** bucket 头部展示的标题,例如 "数据源" / "代码块"。 */
|
||||||
|
title: string;
|
||||||
|
/** 子项 key 的命名空间前缀(`ds` 数据源 / `cb` 代码块),与上层折叠状态 key 保持一致。 */
|
||||||
|
prefix: string;
|
||||||
|
/**
|
||||||
|
* 已按目标 id 聚拢成的 bucket 列表,每个 bucket 内部的 groups 已按时间倒序排好。
|
||||||
|
* 空数组时显示空态。
|
||||||
|
*/
|
||||||
|
buckets: { id: string | number; groups: any[] }[];
|
||||||
|
/** 组级描述文案生成器,由父组件按业务类型注入。 */
|
||||||
|
describeGroup: (_group: any) => string;
|
||||||
|
/** 单步描述文案生成器,由父组件按业务类型注入。 */
|
||||||
|
describeStep: (_step: any) => string;
|
||||||
|
/** 判断某个 step 是否可查看差异(前后值都存在)。由父组件按业务类型注入。 */
|
||||||
|
isStepDiffable: (_step: any) => boolean;
|
||||||
|
/**
|
||||||
|
* 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
|
||||||
|
* 本 tab 使用 `${prefix}-${id}-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
|
||||||
|
* 这样历史数据更新后已展开的分组状态仍能正确保持。
|
||||||
|
*/
|
||||||
|
expanded: Record<string, boolean>;
|
||||||
|
/** 是否支持「跳转到该记录」(goto),透传给 Bucket。默认 true。 */
|
||||||
|
gotoEnabled?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
gotoEnabled: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
/** 透传子组件 Bucket 的 toggle 事件给上层 panel,由其更新 expanded。 */
|
||||||
|
(_e: 'toggle', _key: string): void;
|
||||||
|
/** 透传 Bucket 的 goto 事件,携带目标 id 与目标 step 索引。 */
|
||||||
|
(_e: 'goto', _targetId: string | number, _index: number): void;
|
||||||
|
/** 透传 Bucket 的 goto-initial 事件,携带目标 id(回到该目标未修改时的状态)。 */
|
||||||
|
(_e: 'goto-initial', _targetId: string | number): void;
|
||||||
|
/** 透传 Bucket 的 diff-step 事件,携带目标 id 与 step 索引。 */
|
||||||
|
(_e: 'diff-step', _targetId: string | number, _index: number): void;
|
||||||
|
/** 透传 Bucket 的 revert-step 事件,携带目标 id 与 step 索引(类 git revert)。 */
|
||||||
|
(_e: 'revert-step', _targetId: string | number, _index: number): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@ -1,61 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="!buckets.length" class="m-editor-history-list-empty">暂无操作记录</div>
|
|
||||||
<TMagicScrollbar v-else max-height="360px">
|
|
||||||
<Bucket
|
|
||||||
v-for="bucket in buckets"
|
|
||||||
:key="`cb-${bucket.id}`"
|
|
||||||
title="代码块"
|
|
||||||
:bucket-id="bucket.id"
|
|
||||||
prefix="cb"
|
|
||||||
:groups="bucket.groups"
|
|
||||||
:describe-group="describeCodeBlockGroup"
|
|
||||||
:describe-step="describeCodeBlockStep"
|
|
||||||
:is-step-diffable="isCodeBlockStepDiffable"
|
|
||||||
:expanded="expanded"
|
|
||||||
@toggle="(key: string) => $emit('toggle', key)"
|
|
||||||
@goto="(id: string | number, index: number) => $emit('goto', id, index)"
|
|
||||||
@goto-initial="(id: string | number) => $emit('goto-initial', id)"
|
|
||||||
@diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
|
|
||||||
@revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
|
|
||||||
/>
|
|
||||||
</TMagicScrollbar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { TMagicScrollbar } from '@tmagic/design';
|
|
||||||
|
|
||||||
import type { CodeBlockHistoryGroup, CodeBlockStepValue } from '@editor/type';
|
|
||||||
|
|
||||||
import Bucket from './Bucket.vue';
|
|
||||||
import { describeCodeBlockGroup, describeCodeBlockStep } from './composables';
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: 'MEditorHistoryListCodeBlockTab',
|
|
||||||
});
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
/**
|
|
||||||
* 已按 codeBlock.id 聚拢成的 bucket 列表,每个 bucket 内部的 groups 已按时间倒序排好。
|
|
||||||
* 空数组时显示空态。
|
|
||||||
*/
|
|
||||||
buckets: { id: string | number; groups: CodeBlockHistoryGroup[] }[];
|
|
||||||
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。本 tab 使用 `cb-${id}-${idx}` 作为 key。 */
|
|
||||||
expanded: Record<string, boolean>;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
/** 透传子组件 Bucket 的 toggle 事件给上层 panel,由其更新 expanded。 */
|
|
||||||
(_e: 'toggle', _key: string): void;
|
|
||||||
/** 透传 Bucket 的 goto 事件,携带 codeBlock id 与目标 step 索引。 */
|
|
||||||
(_e: 'goto', _codeBlockId: string | number, _index: number): void;
|
|
||||||
/** 透传 Bucket 的 goto-initial 事件,携带 codeBlock id(回到该代码块未修改时的状态)。 */
|
|
||||||
(_e: 'goto-initial', _codeBlockId: string | number): void;
|
|
||||||
/** 透传 Bucket 的 diff-step 事件,携带 codeBlock id 与 step 索引。 */
|
|
||||||
(_e: 'diff-step', _codeBlockId: string | number, _index: number): void;
|
|
||||||
/** 透传 Bucket 的 revert-step 事件,携带 codeBlock id 与 step 索引(类 git revert)。 */
|
|
||||||
(_e: 'revert-step', _codeBlockId: string | number, _index: number): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
/** 仅 update(前后 content 都存在)时可查看差异。 */
|
|
||||||
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent);
|
|
||||||
</script>
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="!buckets.length" class="m-editor-history-list-empty">暂无操作记录</div>
|
|
||||||
<TMagicScrollbar v-else max-height="360px">
|
|
||||||
<Bucket
|
|
||||||
v-for="bucket in buckets"
|
|
||||||
:key="`ds-${bucket.id}`"
|
|
||||||
title="数据源"
|
|
||||||
:bucket-id="bucket.id"
|
|
||||||
prefix="ds"
|
|
||||||
:groups="bucket.groups"
|
|
||||||
:describe-group="describeDataSourceGroup"
|
|
||||||
:describe-step="describeDataSourceStep"
|
|
||||||
:is-step-diffable="isDataSourceStepDiffable"
|
|
||||||
:expanded="expanded"
|
|
||||||
@toggle="(key: string) => $emit('toggle', key)"
|
|
||||||
@goto="(id: string | number, index: number) => $emit('goto', id, index)"
|
|
||||||
@goto-initial="(id: string | number) => $emit('goto-initial', id)"
|
|
||||||
@diff-step="(id: string | number, index: number) => $emit('diff-step', id, index)"
|
|
||||||
@revert-step="(id: string | number, index: number) => $emit('revert-step', id, index)"
|
|
||||||
/>
|
|
||||||
</TMagicScrollbar>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { TMagicScrollbar } from '@tmagic/design';
|
|
||||||
|
|
||||||
import type { DataSourceHistoryGroup, DataSourceStepValue } from '@editor/type';
|
|
||||||
|
|
||||||
import Bucket from './Bucket.vue';
|
|
||||||
import { describeDataSourceGroup, describeDataSourceStep } from './composables';
|
|
||||||
|
|
||||||
defineOptions({
|
|
||||||
name: 'MEditorHistoryListDataSourceTab',
|
|
||||||
});
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
/**
|
|
||||||
* 已按 dataSource.id 聚拢成的 bucket 列表,每个 bucket 内部的 groups 已按时间倒序排好。
|
|
||||||
* 空数组时显示空态。
|
|
||||||
*/
|
|
||||||
buckets: { id: string | number; groups: DataSourceHistoryGroup[] }[];
|
|
||||||
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。本 tab 使用 `ds-${id}-${idx}` 作为 key。 */
|
|
||||||
expanded: Record<string, boolean>;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
defineEmits<{
|
|
||||||
/** 透传子组件 Bucket 的 toggle 事件给上层 panel,由其更新 expanded。 */
|
|
||||||
(_e: 'toggle', _key: string): void;
|
|
||||||
/** 透传 Bucket 的 goto 事件,携带 dataSource id 与目标 step 索引。 */
|
|
||||||
(_e: 'goto', _dataSourceId: string | number, _index: number): void;
|
|
||||||
/** 透传 Bucket 的 goto-initial 事件,携带 dataSource id(回到该数据源未修改时的状态)。 */
|
|
||||||
(_e: 'goto-initial', _dataSourceId: string | number): void;
|
|
||||||
/** 透传 Bucket 的 diff-step 事件,携带 dataSource id 与 step 索引。 */
|
|
||||||
(_e: 'diff-step', _dataSourceId: string | number, _index: number): void;
|
|
||||||
/** 透传 Bucket 的 revert-step 事件,携带 dataSource id 与 step 索引(类 git revert)。 */
|
|
||||||
(_e: 'revert-step', _dataSourceId: string | number, _index: number): void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
/** 仅 update(前后 schema 都存在)时可查看差异。 */
|
|
||||||
const isDataSourceStepDiffable = (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema);
|
|
||||||
</script>
|
|
||||||
@ -12,15 +12,9 @@
|
|||||||
<span class="m-editor-history-list-item-index" :title="headIndexTitle">{{ headIndexLabel }}</span>
|
<span class="m-editor-history-list-item-index" :title="headIndexTitle">{{ headIndexLabel }}</span>
|
||||||
<span class="m-editor-history-list-item-op" :class="`op-${opType}`">{{ opLabel(opType) }}</span>
|
<span class="m-editor-history-list-item-op" :class="`op-${opType}`">{{ opLabel(opType) }}</span>
|
||||||
<span class="m-editor-history-list-item-desc">{{ desc }}</span>
|
<span class="m-editor-history-list-item-desc">{{ desc }}</span>
|
||||||
<span v-if="isCurrent" class="m-editor-history-list-item-current">当前</span>
|
|
||||||
<span
|
|
||||||
v-if="!merged && headDiffable"
|
|
||||||
class="m-editor-history-list-item-diff"
|
|
||||||
title="查看修改差异"
|
|
||||||
@click.stop="onDiffClick(subSteps[0].index)"
|
|
||||||
>查看差异</span
|
|
||||||
>
|
|
||||||
<span v-if="merged" class="m-editor-history-list-item-merge">合并 {{ stepCount }} 步</span>
|
<span v-if="merged" class="m-editor-history-list-item-merge">合并 {{ stepCount }} 步</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="!merged && headRevertable"
|
v-if="!merged && headRevertable"
|
||||||
class="m-editor-history-list-item-revert"
|
class="m-editor-history-list-item-revert"
|
||||||
@ -28,6 +22,20 @@
|
|||||||
@click.stop="onRevertClick(subSteps[0].index)"
|
@click.stop="onRevertClick(subSteps[0].index)"
|
||||||
>回滚</span
|
>回滚</span
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="!merged && gotoEnabled && !isCurrent && subSteps.length"
|
||||||
|
class="m-editor-history-list-item-goto"
|
||||||
|
title="回到该记录"
|
||||||
|
@click.stop="onGotoClick(subSteps[0].index)"
|
||||||
|
>回到</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="!merged && headDiffable"
|
||||||
|
class="m-editor-history-list-item-diff"
|
||||||
|
title="查看修改差异"
|
||||||
|
@click.stop="onDiffClick(subSteps[0].index)"
|
||||||
|
>查看差异</span
|
||||||
|
>
|
||||||
<span v-if="merged" class="m-editor-history-list-group-toggle" :class="{ 'is-expanded': expanded }">▾</span>
|
<span v-if="merged" class="m-editor-history-list-group-toggle" :class="{ 'is-expanded': expanded }">▾</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -35,20 +43,11 @@
|
|||||||
<li
|
<li
|
||||||
v-for="s in subStepsDisplay"
|
v-for="s in subStepsDisplay"
|
||||||
:key="s.index"
|
:key="s.index"
|
||||||
:class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent, 'is-clickable': gotoEnabled && !s.isCurrent }"
|
:class="{ 'is-undone': !s.applied, 'is-current': s.isCurrent }"
|
||||||
:title="subStepTitle(s)"
|
:title="subStepTitle(s)"
|
||||||
@click="onSubStepClick(s)"
|
|
||||||
>
|
>
|
||||||
<span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
|
<span class="m-editor-history-list-item-index">#{{ s.index + 1 }}</span>
|
||||||
<span class="m-editor-history-list-substep-desc">{{ s.desc }}</span>
|
<span class="m-editor-history-list-substep-desc">{{ s.desc }}</span>
|
||||||
<span v-if="s.isCurrent" class="m-editor-history-list-item-current">当前</span>
|
|
||||||
<span
|
|
||||||
v-if="s.diffable"
|
|
||||||
class="m-editor-history-list-item-diff"
|
|
||||||
title="查看修改差异"
|
|
||||||
@click.stop="onDiffClick(s.index)"
|
|
||||||
>查看差异</span
|
|
||||||
>
|
|
||||||
<span
|
<span
|
||||||
v-if="s.revertable"
|
v-if="s.revertable"
|
||||||
class="m-editor-history-list-item-revert"
|
class="m-editor-history-list-item-revert"
|
||||||
@ -56,6 +55,20 @@
|
|||||||
@click.stop="onRevertClick(s.index)"
|
@click.stop="onRevertClick(s.index)"
|
||||||
>回滚</span
|
>回滚</span
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
v-if="gotoEnabled && !s.isCurrent"
|
||||||
|
class="m-editor-history-list-item-goto"
|
||||||
|
title="回到该记录"
|
||||||
|
@click.stop="onGotoClick(s.index)"
|
||||||
|
>回到</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="s.diffable"
|
||||||
|
class="m-editor-history-list-item-diff"
|
||||||
|
title="查看修改差异"
|
||||||
|
@click.stop="onDiffClick(s.index)"
|
||||||
|
>查看差异</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
@ -74,7 +87,7 @@ defineOptions({
|
|||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
/** 唯一标识当前组的 key,作为 toggle 事件的 payload 回传给上层。形如 `pg-${idx}` / `ds-${id}-${idx}` / `cb-${id}-${idx}`。 */
|
/** 唯一标识当前组的 key,作为 toggle 事件的 payload 回传给上层。形如 `pg-${首步 index}` / `ds-${id}-${首步 index}` / `cb-${id}-${首步 index}`,以稳定的 step 索引标识分组。 */
|
||||||
groupKey: string;
|
groupKey: string;
|
||||||
/** 该组当前是否处于已应用状态(false 表示已被 undo 撤销,UI 会显示为灰态)。 */
|
/** 该组当前是否处于已应用状态(false 表示已被 undo 撤销,UI 会显示为灰态)。 */
|
||||||
applied: boolean;
|
applied: boolean;
|
||||||
@ -142,47 +155,34 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单步组:头部可点击 goto(需 gotoEnabled);合并组:头部可点击切换展开。
|
* 仅合并组头部可点击(切换展开 / 收起);
|
||||||
* 当前组(isCurrent)或禁用 goto 时,单步组头部不可点击。
|
* 单步组的跳转改由头部的「回退」按钮触发,整行不再可点击。
|
||||||
*/
|
*/
|
||||||
const isHeadClickable = computed(() => {
|
const isHeadClickable = computed(() => props.merged);
|
||||||
if (props.merged) return true;
|
|
||||||
return props.gotoEnabled && !props.isCurrent;
|
|
||||||
});
|
|
||||||
|
|
||||||
const headTitle = computed(() => {
|
const headTitle = computed(() => {
|
||||||
if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步';
|
if (props.merged) return props.expanded ? '点击收起子步' : '点击展开子步';
|
||||||
if (props.isCurrent) return '当前所在记录';
|
if (props.isCurrent) return '当前所在记录';
|
||||||
if (!props.gotoEnabled) return '';
|
return '';
|
||||||
return '点击跳转到该记录';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 头部点击行为分流:
|
* 头部点击行为:仅合并组切换展开 / 收起;单步组不再响应整行点击。
|
||||||
* - 合并组:仅切换展开 / 收起,不触发 goto;
|
|
||||||
* - 单步组:跳转到该唯一步骤;当前组忽略点击。
|
|
||||||
*/
|
*/
|
||||||
const onHeadClick = () => {
|
const onHeadClick = () => {
|
||||||
if (props.merged) {
|
if (props.merged) {
|
||||||
emit('toggle', props.groupKey);
|
emit('toggle', props.groupKey);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (props.isCurrent) return;
|
|
||||||
if (!props.gotoEnabled) return;
|
|
||||||
if (!props.subSteps.length) return;
|
|
||||||
emit('goto', props.subSteps[0].index);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubStepClick = (s: { index: number; isCurrent?: boolean }) => {
|
const onGotoClick = (index: number) => {
|
||||||
if (s.isCurrent) return;
|
|
||||||
if (!props.gotoEnabled) return;
|
if (!props.gotoEnabled) return;
|
||||||
emit('goto', s.index);
|
emit('goto', index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const subStepTitle = (s: { isCurrent?: boolean }) => {
|
const subStepTitle = (s: { isCurrent?: boolean }) => {
|
||||||
if (s.isCurrent) return '当前所在记录';
|
if (s.isCurrent) return '当前所在记录';
|
||||||
if (!props.gotoEnabled) return '';
|
return '';
|
||||||
return '点击跳转到该记录';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
|
/** 单步组头部是否展示"查看差异"入口:要求该唯一子步本身可对比。 */
|
||||||
|
|||||||
@ -59,7 +59,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<TMagicButton size="small" @click="visible = false">关闭</TMagicButton>
|
<template v-if="onConfirm">
|
||||||
|
<TMagicButton size="small" @click="visible = false">取消</TMagicButton>
|
||||||
|
<TMagicButton size="small" type="primary" @click="onConfirmClick">确定回滚</TMagicButton>
|
||||||
|
</template>
|
||||||
|
<TMagicButton v-else size="small" @click="visible = false">关闭</TMagicButton>
|
||||||
</template>
|
</template>
|
||||||
</TMagicDialog>
|
</TMagicDialog>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@ -74,13 +78,13 @@ import type { FormState } from '@tmagic/form';
|
|||||||
|
|
||||||
import CompareForm from '@editor/components/CompareForm.vue';
|
import CompareForm from '@editor/components/CompareForm.vue';
|
||||||
import CodeEditor from '@editor/layouts/CodeEditor.vue';
|
import CodeEditor from '@editor/layouts/CodeEditor.vue';
|
||||||
import type { CompareCategory, CompareFormLoadConfig } from '@editor/type';
|
import type { CompareCategory, CompareFormLoadConfig, DiffDialogPayload } from '@editor/type';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'MEditorHistoryDiffDialog',
|
name: 'MEditorHistoryDiffDialog',
|
||||||
});
|
});
|
||||||
|
|
||||||
withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
/**
|
/**
|
||||||
* 来自 Editor 顶层的 `extendFormState`,用于扩展 MForm.formState。
|
* 来自 Editor 顶层的 `extendFormState`,用于扩展 MForm.formState。
|
||||||
@ -94,32 +98,13 @@ withDefaults(
|
|||||||
*/
|
*/
|
||||||
loadConfig?: CompareFormLoadConfig;
|
loadConfig?: CompareFormLoadConfig;
|
||||||
width?: string;
|
width?: string;
|
||||||
|
onConfirm?: () => void;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
width: '900px',
|
width: '900px',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 差异对话框的入参 */
|
|
||||||
export interface DiffDialogPayload {
|
|
||||||
/** 表单类别 */
|
|
||||||
category: CompareCategory;
|
|
||||||
/** 节点类型 / 数据源类型 */
|
|
||||||
type?: string;
|
|
||||||
/** 代码块场景下的数据源类型 */
|
|
||||||
dataSourceType?: string;
|
|
||||||
/** 该 step 修改前的值(oldNode / oldSchema / oldContent) */
|
|
||||||
lastValue: Record<string, any>;
|
|
||||||
/** 该 step 修改后的值(newNode / newSchema / newContent) */
|
|
||||||
value: Record<string, any>;
|
|
||||||
/** 当前编辑器中实际的最新值;不传或为 null 时禁用「与当前对比」 */
|
|
||||||
currentValue?: Record<string, any> | null;
|
|
||||||
/** 用于标题展示的目标名称 */
|
|
||||||
targetLabel?: string;
|
|
||||||
/** 用于标题展示的目标 id */
|
|
||||||
id?: string | number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 差异对比模式:
|
* 差异对比模式:
|
||||||
* - before:该步骤修改前 vs 该步骤修改后(默认行为,体现这一步带来的变化)
|
* - before:该步骤修改前 vs 该步骤修改后(默认行为,体现这一步带来的变化)
|
||||||
@ -184,6 +169,12 @@ const isSameAsCurrent = computed(() => {
|
|||||||
return isEqual(payload.value.value, payload.value.currentValue);
|
return isEqual(payload.value.value, payload.value.currentValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onConfirmClick = () => {
|
||||||
|
const cb = props.onConfirm;
|
||||||
|
visible.value = false;
|
||||||
|
cb?.();
|
||||||
|
};
|
||||||
|
|
||||||
const targetText = computed(() => {
|
const targetText = computed(() => {
|
||||||
if (!payload.value) return '';
|
if (!payload.value) return '';
|
||||||
const categoryText: Record<CompareCategory, string> = {
|
const categoryText: Record<CompareCategory, string> = {
|
||||||
|
|||||||
@ -36,9 +36,14 @@
|
|||||||
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||||
v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
|
v-bind="tabPaneComponent?.props({ name: 'data-source', label: `数据源 (${dataSourceGroups.length})` }) || {}"
|
||||||
>
|
>
|
||||||
<DataSourceTab
|
<BucketTab
|
||||||
|
title="数据源"
|
||||||
|
prefix="ds"
|
||||||
:buckets="dataSourceGroupsByTarget"
|
:buckets="dataSourceGroupsByTarget"
|
||||||
:expanded="expanded"
|
:expanded="expanded"
|
||||||
|
:describe-group="describeDataSourceGroup"
|
||||||
|
:describe-step="describeDataSourceStep"
|
||||||
|
:is-step-diffable="isDataSourceStepDiffable"
|
||||||
@toggle="toggleGroup"
|
@toggle="toggleGroup"
|
||||||
@goto="onDataSourceGoto"
|
@goto="onDataSourceGoto"
|
||||||
@goto-initial="onDataSourceGotoInitial"
|
@goto-initial="onDataSourceGotoInitial"
|
||||||
@ -52,9 +57,14 @@
|
|||||||
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
:is="tabPaneComponent?.component || 'el-tab-pane'"
|
||||||
v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
|
v-bind="tabPaneComponent?.props({ name: 'code-block', label: `代码块 (${codeBlockGroups.length})` }) || {}"
|
||||||
>
|
>
|
||||||
<CodeBlockTab
|
<BucketTab
|
||||||
|
title="代码块"
|
||||||
|
prefix="cb"
|
||||||
:buckets="codeBlockGroupsByTarget"
|
:buckets="codeBlockGroupsByTarget"
|
||||||
:expanded="expanded"
|
:expanded="expanded"
|
||||||
|
:describe-group="describeCodeBlockGroup"
|
||||||
|
:describe-step="describeCodeBlockStep"
|
||||||
|
:is-step-diffable="isCodeBlockStepDiffable"
|
||||||
@toggle="toggleGroup"
|
@toggle="toggleGroup"
|
||||||
@goto="onCodeBlockGoto"
|
@goto="onCodeBlockGoto"
|
||||||
@goto-initial="onCodeBlockGotoInitial"
|
@goto-initial="onCodeBlockGotoInitial"
|
||||||
@ -85,7 +95,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</TMagicPopover>
|
</TMagicPopover>
|
||||||
|
|
||||||
<HistoryDiffDialog ref="diffDialog" :extend-state="extendFormState" />
|
<HistoryDiffDialog ref="diffDialog" :extend-state="extendFormState" :on-confirm="onConfirmRevert" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@ -106,10 +116,11 @@
|
|||||||
* 此外每条 step 上提供"查看差异"入口(仅在前后值都存在的 update 步骤显示),
|
* 此外每条 step 上提供"查看差异"入口(仅在前后值都存在的 update 步骤显示),
|
||||||
* 点击后弹出 HistoryDiffDialog,使用 CompareForm 组件以表单形式展示新旧值差异。
|
* 点击后弹出 HistoryDiffDialog,使用 CompareForm 组件以表单形式展示新旧值差异。
|
||||||
*
|
*
|
||||||
* 各 tab 的内容拆分为独立的 SFC(PageTab / DataSourceTab / CodeBlockTab),
|
* 各 tab 的内容拆分为独立的 SFC:页面用 PageTab,数据源 / 代码块复用通用的 BucketTab
|
||||||
|
* (通过 title / prefix / describe* / isStepDiffable 注入差异)。
|
||||||
* 共享的描述生成与折叠状态在 composables.ts 中维护。
|
* 共享的描述生成与折叠状态在 composables.ts 中维护。
|
||||||
*/
|
*/
|
||||||
import { computed, inject, markRaw, ref, useTemplateRef, watch } from 'vue';
|
import { computed, inject, markRaw, ref, shallowRef, useTemplateRef, watch } from 'vue';
|
||||||
import { Clock, Close } from '@element-plus/icons-vue';
|
import { Clock, Close } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
|
import { getDesignConfig, TMagicButton, TMagicPopover, TMagicTabs, TMagicTooltip } from '@tmagic/design';
|
||||||
@ -117,11 +128,16 @@ import type { FormState } from '@tmagic/form';
|
|||||||
|
|
||||||
import MIcon from '@editor/components/Icon.vue';
|
import MIcon from '@editor/components/Icon.vue';
|
||||||
import { useServices } from '@editor/hooks/use-services';
|
import { useServices } from '@editor/hooks/use-services';
|
||||||
import type { HistoryListExtraTab } from '@editor/type';
|
import type { CodeBlockStepValue, DataSourceStepValue, DiffDialogPayload, HistoryListExtraTab } from '@editor/type';
|
||||||
|
|
||||||
import CodeBlockTab from './CodeBlockTab.vue';
|
import BucketTab from './BucketTab.vue';
|
||||||
import { useHistoryList } from './composables';
|
import {
|
||||||
import DataSourceTab from './DataSourceTab.vue';
|
describeCodeBlockGroup,
|
||||||
|
describeCodeBlockStep,
|
||||||
|
describeDataSourceGroup,
|
||||||
|
describeDataSourceStep,
|
||||||
|
useHistoryList,
|
||||||
|
} from './composables';
|
||||||
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
import HistoryDiffDialog from './HistoryDiffDialog.vue';
|
||||||
import PageTab from './PageTab.vue';
|
import PageTab from './PageTab.vue';
|
||||||
|
|
||||||
@ -183,6 +199,12 @@ const {
|
|||||||
codeBlockGroupsByTarget,
|
codeBlockGroupsByTarget,
|
||||||
} = useHistoryList();
|
} = useHistoryList();
|
||||||
|
|
||||||
|
/** 数据源 step 仅 update(前后 schema 都存在)时可查看差异。 */
|
||||||
|
const isDataSourceStepDiffable = (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema);
|
||||||
|
|
||||||
|
/** 代码块 step 仅 update(前后 content 都存在)时可查看差异。 */
|
||||||
|
const isCodeBlockStepDiffable = (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent);
|
||||||
|
|
||||||
/** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
|
/** 把"目标 step 索引"翻译成"目标 cursor"(已应用步骤数量)。 */
|
||||||
const indexToCursor = (index: number) => index + 1;
|
const indexToCursor = (index: number) => index + 1;
|
||||||
|
|
||||||
@ -214,40 +236,25 @@ const onCodeBlockGotoInitial = (id: string | number) => {
|
|||||||
codeBlockService.goto(id, 0);
|
codeBlockService.goto(id, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 「回滚」入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
|
|
||||||
* 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
|
|
||||||
*/
|
|
||||||
const onPageRevert = (index: number) => {
|
|
||||||
editorService.revertPageStep(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDataSourceRevert = (id: string | number, index: number) => {
|
|
||||||
dataSourceService.revert(id, index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCodeBlockRevert = (id: string | number, index: number) => {
|
|
||||||
codeBlockService.revert(id, index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const diffDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('diffDialog');
|
const diffDialogRef = useTemplateRef<InstanceType<typeof HistoryDiffDialog>>('diffDialog');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 页面 step 差异:仅 update 单节点修改可对比,传入旧/新节点。
|
* 构造页面 step 的差异弹窗入参:仅 update 单节点修改可对比,传入旧/新节点。
|
||||||
* 节点类型 `type` 优先取 newNode.type,再回退 oldNode.type。
|
* 节点类型 `type` 优先取 newNode.type,再回退 oldNode.type。
|
||||||
* `currentValue` 取自 editorService 中该节点当前实际值,用于支持「与当前对比」。
|
* `currentValue` 取自 editorService 中该节点当前实际值,用于支持「与当前对比」。
|
||||||
|
* 无可对比内容(如多节点 / add / remove)时返回 null。
|
||||||
*/
|
*/
|
||||||
const onPageDiff = (index: number) => {
|
const buildPageDiffPayload = (index: number): DiffDialogPayload | null => {
|
||||||
const groups = historyService.getPageHistoryGroups();
|
const groups = historyService.getPageHistoryGroups();
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const entry = group.steps.find((s) => s.index === index);
|
const entry = group.steps.find((s) => s.index === index);
|
||||||
if (!entry) continue;
|
if (!entry) continue;
|
||||||
const item = entry.step.updatedItems?.[0];
|
const item = entry.step.updatedItems?.[0];
|
||||||
if (!item?.oldNode || !item?.newNode) return;
|
if (!item?.oldNode || !item?.newNode) return null;
|
||||||
const type = (item.newNode.type as string) || (item.oldNode.type as string) || '';
|
const type = (item.newNode.type as string) || (item.oldNode.type as string) || '';
|
||||||
const nodeId = item.newNode.id ?? item.oldNode.id;
|
const nodeId = item.newNode.id ?? item.oldNode.id;
|
||||||
const currentNode = nodeId !== undefined ? editorService.getNodeById(nodeId) : null;
|
const currentNode = nodeId !== undefined ? editorService.getNodeById(nodeId) : null;
|
||||||
diffDialogRef.value?.open({
|
return {
|
||||||
category: 'node',
|
category: 'node',
|
||||||
type,
|
type,
|
||||||
lastValue: item.oldNode as Record<string, any>,
|
lastValue: item.oldNode as Record<string, any>,
|
||||||
@ -255,21 +262,35 @@ const onPageDiff = (index: number) => {
|
|||||||
currentValue: (currentNode as Record<string, any>) || null,
|
currentValue: (currentNode as Record<string, any>) || null,
|
||||||
targetLabel: (item.newNode.name as string) || (item.oldNode.name as string) || type,
|
targetLabel: (item.newNode.name as string) || (item.oldNode.name as string) || type,
|
||||||
id: nodeId,
|
id: nodeId,
|
||||||
});
|
};
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDataSourceDiff = (id: string | number, index: number) => {
|
/**
|
||||||
const groups = historyService.getDataSourceHistoryGroups();
|
* 在指定分组列表中按 id / index 查找命中的 step,命中后交由 build 构造差异弹窗入参。
|
||||||
|
* 用于统一数据源、代码块两类历史的查找逻辑。
|
||||||
|
*/
|
||||||
|
const findGroupStep = <G extends { id: string | number; steps: { index: number; step: any }[] }>(
|
||||||
|
groups: G[],
|
||||||
|
id: string | number,
|
||||||
|
index: number,
|
||||||
|
build: (_step: G['steps'][number]['step']) => DiffDialogPayload | null,
|
||||||
|
): DiffDialogPayload | null => {
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
if (group.id !== id) continue;
|
if (group.id !== id) continue;
|
||||||
const entry = group.steps.find((s) => s.index === index);
|
const entry = group.steps.find((s) => s.index === index);
|
||||||
if (!entry) continue;
|
if (!entry) continue;
|
||||||
const { oldSchema, newSchema } = entry.step;
|
return build(entry.step);
|
||||||
if (!oldSchema || !newSchema) return;
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildDataSourceDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
|
||||||
|
findGroupStep(historyService.getDataSourceHistoryGroups(), id, index, ({ oldSchema, newSchema }) => {
|
||||||
|
if (!oldSchema || !newSchema) return null;
|
||||||
const currentSchema = dataSourceService.getDataSourceById(`${id}`);
|
const currentSchema = dataSourceService.getDataSourceById(`${id}`);
|
||||||
diffDialogRef.value?.open({
|
return {
|
||||||
category: 'data-source',
|
category: 'data-source',
|
||||||
type: newSchema.type || oldSchema.type || 'base',
|
type: newSchema.type || oldSchema.type || 'base',
|
||||||
lastValue: oldSchema as Record<string, any>,
|
lastValue: oldSchema as Record<string, any>,
|
||||||
@ -277,29 +298,74 @@ const onDataSourceDiff = (id: string | number, index: number) => {
|
|||||||
currentValue: (currentSchema as Record<string, any>) || null,
|
currentValue: (currentSchema as Record<string, any>) || null,
|
||||||
targetLabel: newSchema.title || oldSchema.title || `${id}`,
|
targetLabel: newSchema.title || oldSchema.title || `${id}`,
|
||||||
id,
|
id,
|
||||||
});
|
};
|
||||||
return;
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCodeBlockDiff = (id: string | number, index: number) => {
|
const buildCodeBlockDiffPayload = (id: string | number, index: number): DiffDialogPayload | null =>
|
||||||
const groups = historyService.getCodeBlockHistoryGroups();
|
findGroupStep(historyService.getCodeBlockHistoryGroups(), id, index, ({ oldContent, newContent }) => {
|
||||||
for (const group of groups) {
|
if (!oldContent || !newContent) return null;
|
||||||
if (group.id !== id) continue;
|
|
||||||
const entry = group.steps.find((s) => s.index === index);
|
|
||||||
if (!entry) continue;
|
|
||||||
const { oldContent, newContent } = entry.step;
|
|
||||||
if (!oldContent || !newContent) return;
|
|
||||||
const currentContent = codeBlockService.getCodeContentById(id);
|
const currentContent = codeBlockService.getCodeContentById(id);
|
||||||
diffDialogRef.value?.open({
|
return {
|
||||||
category: 'code-block',
|
category: 'code-block',
|
||||||
lastValue: oldContent as Record<string, any>,
|
lastValue: oldContent as Record<string, any>,
|
||||||
value: newContent as Record<string, any>,
|
value: newContent as Record<string, any>,
|
||||||
currentValue: (currentContent as Record<string, any>) || null,
|
currentValue: (currentContent as Record<string, any>) || null,
|
||||||
targetLabel: newContent.name || oldContent.name || `${id}`,
|
targetLabel: newContent.name || oldContent.name || `${id}`,
|
||||||
id,
|
id,
|
||||||
});
|
};
|
||||||
return;
|
});
|
||||||
|
|
||||||
|
const onPageDiff = (index: number) => {
|
||||||
|
const payload = buildPageDiffPayload(index);
|
||||||
|
if (payload) diffDialogRef.value?.open(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDataSourceDiff = (id: string | number, index: number) => {
|
||||||
|
const payload = buildDataSourceDiffPayload(id, index);
|
||||||
|
if (payload) diffDialogRef.value?.open(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCodeBlockDiff = (id: string | number, index: number) => {
|
||||||
|
const payload = buildCodeBlockDiffPayload(id, index);
|
||||||
|
if (payload) diffDialogRef.value?.open(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onConfirmRevert = shallowRef();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「回滚」入口:把目标历史步骤的修改作为一次新操作反向应用(类 git revert),
|
||||||
|
* 不破坏原有栈结构。各 service 内部完成反向 + 入栈,并自带描述用于面板展示。
|
||||||
|
*
|
||||||
|
* 交互:先弹出该步骤的差异弹窗供用户确认,点击「确定回滚」后再真正执行回滚;
|
||||||
|
* 对没有可对比内容的步骤(如 add / remove / 多节点更新)则直接回滚。
|
||||||
|
*/
|
||||||
|
const onPageRevert = (index: number) => {
|
||||||
|
const payload = buildPageDiffPayload(index);
|
||||||
|
onConfirmRevert.value = () => editorService.revertPageStep(index);
|
||||||
|
if (payload) {
|
||||||
|
diffDialogRef.value?.open({ ...payload });
|
||||||
|
} else {
|
||||||
|
onConfirmRevert.value();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDataSourceRevert = (id: string | number, index: number) => {
|
||||||
|
const payload = buildDataSourceDiffPayload(id, index);
|
||||||
|
onConfirmRevert.value = () => dataSourceService.revert(id, index);
|
||||||
|
if (payload) {
|
||||||
|
diffDialogRef.value?.open({ ...payload });
|
||||||
|
} else {
|
||||||
|
onConfirmRevert.value();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCodeBlockRevert = (id: string | number, index: number) => {
|
||||||
|
const payload = buildCodeBlockDiffPayload(id, index);
|
||||||
|
onConfirmRevert.value = () => codeBlockService.revert(id, index);
|
||||||
|
if (payload) {
|
||||||
|
diffDialogRef.value?.open({ ...payload });
|
||||||
|
} else {
|
||||||
|
onConfirmRevert.value();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -3,12 +3,17 @@
|
|||||||
class="m-editor-history-list-item m-editor-history-list-initial"
|
class="m-editor-history-list-item m-editor-history-list-initial"
|
||||||
:class="{ 'is-current': isCurrent, 'is-clickable': !isCurrent }"
|
:class="{ 'is-current': isCurrent, 'is-clickable': !isCurrent }"
|
||||||
:title="isCurrent ? '当前已回到未修改的初始状态' : '点击回到未修改的初始状态'"
|
:title="isCurrent ? '当前已回到未修改的初始状态' : '点击回到未修改的初始状态'"
|
||||||
@click="onClick"
|
|
||||||
>
|
>
|
||||||
<span class="m-editor-history-list-item-index" title="历史步骤编号 #0(未修改的初始状态)">#0</span>
|
<span class="m-editor-history-list-item-index" title="历史步骤编号 #0(未修改的初始状态)">#0</span>
|
||||||
<span class="m-editor-history-list-item-op op-initial">初始</span>
|
<span class="m-editor-history-list-item-op op-initial">初始</span>
|
||||||
<span class="m-editor-history-list-item-desc">未修改的初始状态</span>
|
<span class="m-editor-history-list-item-desc">未修改的初始状态</span>
|
||||||
<span v-if="isCurrent" class="m-editor-history-list-item-current">当前</span>
|
<span
|
||||||
|
v-if="gotoEnabled && !isCurrent"
|
||||||
|
class="m-editor-history-list-item-goto"
|
||||||
|
title="回到该记录"
|
||||||
|
@click.stop="onClick"
|
||||||
|
>回到</span
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -24,10 +29,16 @@ defineOptions({
|
|||||||
name: 'MEditorHistoryListInitialRow',
|
name: 'MEditorHistoryListInitialRow',
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
/** 当前对应栈是否已经处于初始状态 (cursor === 0)。true 时用蓝条高亮并禁用点击。 */
|
defineProps<{
|
||||||
isCurrent: boolean;
|
/** 当前对应栈是否已经处于初始状态 (cursor === 0)。true 时用蓝条高亮并禁用点击。 */
|
||||||
}>();
|
isCurrent: boolean;
|
||||||
|
gotoEnabled?: boolean;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
gotoEnabled: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
/** 点击非当前的初始项时触发,由上层调用对应 service 的 goto 把 cursor 移到 0。 */
|
/** 点击非当前的初始项时触发,由上层调用对应 service 的 goto 把 cursor 移到 0。 */
|
||||||
|
|||||||
@ -3,9 +3,9 @@
|
|||||||
<TMagicScrollbar v-else max-height="360px">
|
<TMagicScrollbar v-else max-height="360px">
|
||||||
<ul class="m-editor-history-list-ul">
|
<ul class="m-editor-history-list-ul">
|
||||||
<GroupRow
|
<GroupRow
|
||||||
v-for="(group, gIdx) in list"
|
v-for="group in list"
|
||||||
:key="`pg-${gIdx}`"
|
:key="`pg-${group.steps[0]?.index}`"
|
||||||
:group-key="`pg-${gIdx}`"
|
:group-key="`pg-${group.steps[0]?.index}`"
|
||||||
:applied="group.applied"
|
:applied="group.applied"
|
||||||
:merged="group.steps.length > 1"
|
:merged="group.steps.length > 1"
|
||||||
:op-type="group.opType"
|
:op-type="group.opType"
|
||||||
@ -22,7 +22,7 @@
|
|||||||
}))
|
}))
|
||||||
"
|
"
|
||||||
:is-current="group.isCurrent"
|
:is-current="group.isCurrent"
|
||||||
:expanded="!!expanded[`pg-${gIdx}`]"
|
:expanded="!!expanded[`pg-${group.steps[0]?.index}`]"
|
||||||
@toggle="(key: string) => $emit('toggle', key)"
|
@toggle="(key: string) => $emit('toggle', key)"
|
||||||
@goto="(index: number) => $emit('goto', index)"
|
@goto="(index: number) => $emit('goto', index)"
|
||||||
@diff-step="(index: number) => $emit('diff-step', index)"
|
@diff-step="(index: number) => $emit('diff-step', index)"
|
||||||
@ -55,7 +55,11 @@ defineOptions({
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
/** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
|
/** 当前活动页面的历史分组列表,已按时间倒序排好(最新一组在最前)。空数组时显示空态。 */
|
||||||
list: PageHistoryGroup[];
|
list: PageHistoryGroup[];
|
||||||
/** 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。本 tab 使用 `pg-${idx}` 作为 key。 */
|
/**
|
||||||
|
* 共享的折叠状态表(key -> 是否展开),由顶层 panel 统一维护。
|
||||||
|
* 本 tab 使用 `pg-${组内首步 index}` 作为 key——以稳定的 step 索引而非展示位置标识分组,
|
||||||
|
* 这样历史数据更新(新增 / 撤销重做导致列表顺序变化)后,已展开的分组状态仍能正确保持。
|
||||||
|
*/
|
||||||
expanded: Record<string, boolean>;
|
expanded: Record<string, boolean>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,10 @@ import type {
|
|||||||
export const useHistoryList = () => {
|
export const useHistoryList = () => {
|
||||||
const { historyService } = useServices();
|
const { historyService } = useServices();
|
||||||
|
|
||||||
/** 折叠状态:key 形如 `pg-${groupIdx}` / `ds-${id}-${groupIdx}` / `cb-${id}-${groupIdx}`。 */
|
/**
|
||||||
|
* 折叠状态:key 形如 `pg-${组内首步 index}` / `ds-${id}-${组内首步 index}` / `cb-${id}-${组内首步 index}`。
|
||||||
|
* 用组内首步的稳定 index(而非展示位置)作为 key,确保历史数据更新后已展开的分组状态保持不变。
|
||||||
|
*/
|
||||||
const expanded = reactive<Record<string, boolean>>({});
|
const expanded = reactive<Record<string, boolean>>({});
|
||||||
const toggleGroup = (key: string) => {
|
const toggleGroup = (key: string) => {
|
||||||
expanded[key] = !expanded[key];
|
expanded[key] = !expanded[key];
|
||||||
|
|||||||
@ -125,19 +125,19 @@
|
|||||||
&.is-merged {
|
&.is-merged {
|
||||||
margin: 4px 0;
|
margin: 4px 0;
|
||||||
padding: 4px 8px 6px;
|
padding: 4px 8px 6px;
|
||||||
background-color: rgba(144, 105, 219, 0.06);
|
background-color: rgba(47, 84, 235, 0.06);
|
||||||
border: 1px solid rgba(144, 105, 219, 0.18);
|
border: 1px solid rgba(47, 84, 235, 0.18);
|
||||||
border-left: 3px solid #9069db;
|
border-left: 3px solid #2f54eb;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
// 卡片本体已经有背景色,hover 状态以更深的同色提示交互
|
// 卡片本体已经有背景色,hover 状态以更深的同色提示交互
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(144, 105, 219, 0.1);
|
background-color: rgba(47, 84, 235, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.m-editor-history-list-group-head {
|
.m-editor-history-list-group-head {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #5b3fa5;
|
color: #1d39c4;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已撤销态:整张卡片去色
|
// 已撤销态:整张卡片去色
|
||||||
@ -169,7 +169,7 @@
|
|||||||
margin: 6px 0 0 6px;
|
margin: 6px 0 0 6px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
border-left: 1px dashed rgba(144, 105, 219, 0.45);
|
border-left: 1px dashed rgba(47, 84, 235, 0.45);
|
||||||
|
|
||||||
li {
|
li {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -185,7 +185,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(144, 105, 219, 0.1);
|
background-color: rgba(47, 84, 235, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,7 +240,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.op-update {
|
&.op-update {
|
||||||
background-color: #409eff;
|
background-color: #e6a23c;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.op-initial {
|
&.op-initial {
|
||||||
@ -279,7 +279,7 @@
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #9069db;
|
background-color: #2f54eb;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
@ -300,21 +300,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 「跳转」按钮:将历史游标移动到该 step,替代原先点击整行跳转的交互。
|
||||||
|
// 使用与组卡片一致的紫色色系,与「查看差异」「回滚」区分开。
|
||||||
|
.m-editor-history-list-item-goto {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #606266;
|
||||||
|
background-color: rgba(96, 98, 102, 0.1);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(96, 98, 102, 0.18);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 「回滚」按钮:类 git revert,把目标 step 反向应用一次作为新提交。
|
// 「回滚」按钮:类 git revert,把目标 step 反向应用一次作为新提交。
|
||||||
// 使用与「查看差异」不同的色调(橙黄),用来区分"可逆操作"与"只读对比"。
|
// 使用红色色调,强调其为"破坏性/可逆操作",与「查看差异」「跳转」区分开。
|
||||||
.m-editor-history-list-item-revert {
|
.m-editor-history-list-item-revert {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
color: #e6a23c;
|
color: #f56c6c;
|
||||||
background-color: rgba(230, 162, 60, 0.12);
|
background-color: rgba(245, 108, 108, 0.12);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(230, 162, 60, 0.25);
|
background-color: rgba(245, 108, 108, 0.25);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1113,3 +1113,23 @@ export interface DslOpOptions extends HistoryOpOptions {
|
|||||||
doNotSelect?: boolean;
|
doNotSelect?: boolean;
|
||||||
doNotSwitchPage?: boolean;
|
doNotSwitchPage?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 差异对话框的入参 */
|
||||||
|
export interface DiffDialogPayload {
|
||||||
|
/** 表单类别 */
|
||||||
|
category: CompareCategory;
|
||||||
|
/** 节点类型 / 数据源类型 */
|
||||||
|
type?: string;
|
||||||
|
/** 代码块场景下的数据源类型 */
|
||||||
|
dataSourceType?: string;
|
||||||
|
/** 该 step 修改前的值(oldNode / oldSchema / oldContent) */
|
||||||
|
lastValue: Record<string, any>;
|
||||||
|
/** 该 step 修改后的值(newNode / newSchema / newContent) */
|
||||||
|
value: Record<string, any>;
|
||||||
|
/** 当前编辑器中实际的最新值;不传或为 null 时禁用「与当前对比」 */
|
||||||
|
currentValue?: Record<string, any> | null;
|
||||||
|
/** 用于标题展示的目标名称 */
|
||||||
|
targetLabel?: string;
|
||||||
|
/** 用于标题展示的目标 id */
|
||||||
|
id?: string | number;
|
||||||
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ describe('Bucket.vue', () => {
|
|||||||
expect(wrapper.emitted('goto')).toBeFalsy();
|
expect(wrapper.emitted('goto')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('单步组头部点击 → goto 事件被透传到 Bucket,并附带 bucketId', async () => {
|
test('单步组「回到」按钮点击 → goto 事件被透传到 Bucket,并附带 bucketId', async () => {
|
||||||
const wrapper = mount(Bucket, {
|
const wrapper = mount(Bucket, {
|
||||||
props: {
|
props: {
|
||||||
title: '代码块',
|
title: '代码块',
|
||||||
@ -102,13 +102,13 @@ describe('Bucket.vue', () => {
|
|||||||
expanded: {},
|
expanded: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await wrapper.find('.m-editor-history-list-group-head').trigger('click');
|
await wrapper.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
const events = wrapper.emitted('goto');
|
const events = wrapper.emitted('goto');
|
||||||
expect(events).toBeTruthy();
|
expect(events).toBeTruthy();
|
||||||
expect(events![0]).toEqual(['code_1', 0]);
|
expect(events![0]).toEqual(['code_1', 0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('合并组展开后点击子步 → goto 透传,附带子步 index', async () => {
|
test('合并组展开后点击子步「回到」按钮 → goto 透传,附带子步 index', async () => {
|
||||||
const wrapper = mount(Bucket, {
|
const wrapper = mount(Bucket, {
|
||||||
props: {
|
props: {
|
||||||
title: '代码块',
|
title: '代码块',
|
||||||
@ -123,7 +123,7 @@ describe('Bucket.vue', () => {
|
|||||||
const subItems = wrapper.findAll('.m-editor-history-list-substeps li');
|
const subItems = wrapper.findAll('.m-editor-history-list-substeps li');
|
||||||
expect(subItems).toHaveLength(2);
|
expect(subItems).toHaveLength(2);
|
||||||
// 子步倒序渲染:subItems[0] 对应 index=1
|
// 子步倒序渲染:subItems[0] 对应 index=1
|
||||||
await subItems[0].trigger('click');
|
await subItems[0].find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
const events = wrapper.emitted('goto');
|
const events = wrapper.emitted('goto');
|
||||||
expect(events).toBeTruthy();
|
expect(events).toBeTruthy();
|
||||||
expect(events![0]).toEqual(['code_1', 1]);
|
expect(events![0]).toEqual(['code_1', 1]);
|
||||||
@ -166,7 +166,7 @@ describe('Bucket.vue', () => {
|
|||||||
// 已有 applied 组,初始项不应为当前
|
// 已有 applied 组,初始项不应为当前
|
||||||
expect(initial.classes()).not.toContain('is-current');
|
expect(initial.classes()).not.toContain('is-current');
|
||||||
|
|
||||||
await initial.trigger('click');
|
await initial.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
const events = wrapper.emitted('goto-initial');
|
const events = wrapper.emitted('goto-initial');
|
||||||
expect(events).toBeTruthy();
|
expect(events).toBeTruthy();
|
||||||
expect(events![0]).toEqual(['ds_1']);
|
expect(events![0]).toEqual(['ds_1']);
|
||||||
|
|||||||
@ -7,8 +7,9 @@ import { describe, expect, test, vi } from 'vitest';
|
|||||||
import { defineComponent, h } from 'vue';
|
import { defineComponent, h } from 'vue';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
import CodeBlockTab from '@editor/layouts/history-list/CodeBlockTab.vue';
|
import BucketTab from '@editor/layouts/history-list/BucketTab.vue';
|
||||||
import type { CodeBlockHistoryGroup } from '@editor/type';
|
import { describeCodeBlockGroup, describeCodeBlockStep } from '@editor/layouts/history-list/composables';
|
||||||
|
import type { CodeBlockHistoryGroup, CodeBlockStepValue } from '@editor/type';
|
||||||
|
|
||||||
vi.mock('@tmagic/design', () => ({
|
vi.mock('@tmagic/design', () => ({
|
||||||
TMagicScrollbar: defineComponent({
|
TMagicScrollbar: defineComponent({
|
||||||
@ -25,17 +26,31 @@ const buildGroup = (
|
|||||||
opType: 'add' | 'remove' | 'update',
|
opType: 'add' | 'remove' | 'update',
|
||||||
steps: any[],
|
steps: any[],
|
||||||
applied = true,
|
applied = true,
|
||||||
|
startIndex = 0,
|
||||||
): CodeBlockHistoryGroup => ({
|
): CodeBlockHistoryGroup => ({
|
||||||
kind: 'code-block',
|
kind: 'code-block',
|
||||||
id,
|
id,
|
||||||
opType,
|
opType,
|
||||||
applied,
|
applied,
|
||||||
steps: steps.map((s, i) => ({ step: s, index: i, applied })),
|
steps: steps.map((s, i) => ({ step: s, index: startIndex + i, applied })),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 代码块 tab 复用通用 BucketTab,固定注入代码块的 title/prefix/describe/isStepDiffable。 */
|
||||||
|
const mountCodeBlockTab = (props: { buckets: any[]; expanded: Record<string, boolean> }) =>
|
||||||
|
mount(BucketTab, {
|
||||||
|
props: {
|
||||||
|
title: '代码块',
|
||||||
|
prefix: 'cb',
|
||||||
|
describeGroup: describeCodeBlockGroup,
|
||||||
|
describeStep: describeCodeBlockStep,
|
||||||
|
isStepDiffable: (step: CodeBlockStepValue) => Boolean(step.oldContent && step.newContent),
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('CodeBlockTab.vue', () => {
|
describe('CodeBlockTab.vue', () => {
|
||||||
test('buckets 为空时显示空态', () => {
|
test('buckets 为空时显示空态', () => {
|
||||||
const wrapper = mount(CodeBlockTab, { props: { buckets: [], expanded: {} } });
|
const wrapper = mountCodeBlockTab({ buckets: [], expanded: {} });
|
||||||
expect(wrapper.find('.m-editor-history-list-empty').exists()).toBe(true);
|
expect(wrapper.find('.m-editor-history-list-empty').exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -48,7 +63,7 @@ describe('CodeBlockTab.vue', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(CodeBlockTab, { props: { buckets, expanded: {} } });
|
const wrapper = mountCodeBlockTab({ buckets, expanded: {} });
|
||||||
expect(wrapper.find('.m-editor-history-list-bucket-title').text()).toContain('代码块');
|
expect(wrapper.find('.m-editor-history-list-bucket-title').text()).toContain('代码块');
|
||||||
expect(wrapper.find('.m-editor-history-list-bucket-title code').text()).toBe('code_1');
|
expect(wrapper.find('.m-editor-history-list-bucket-title code').text()).toBe('code_1');
|
||||||
|
|
||||||
@ -75,29 +90,35 @@ describe('CodeBlockTab.vue', () => {
|
|||||||
changeRecords: [{ propPath: 'b' }],
|
changeRecords: [{ propPath: 'b' }],
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
buildGroup('code_1', 'update', [
|
buildGroup(
|
||||||
{
|
'code_1',
|
||||||
id: 'code_1',
|
'update',
|
||||||
oldContent: { id: 'code_1', name: 'fn' },
|
[
|
||||||
newContent: { id: 'code_1', name: 'fn' },
|
{
|
||||||
changeRecords: [{ propPath: 'c' }],
|
id: 'code_1',
|
||||||
},
|
oldContent: { id: 'code_1', name: 'fn' },
|
||||||
{
|
newContent: { id: 'code_1', name: 'fn' },
|
||||||
id: 'code_1',
|
changeRecords: [{ propPath: 'c' }],
|
||||||
oldContent: { id: 'code_1', name: 'fn' },
|
},
|
||||||
newContent: { id: 'code_1', name: 'fn' },
|
{
|
||||||
changeRecords: [{ propPath: 'd' }],
|
id: 'code_1',
|
||||||
},
|
oldContent: { id: 'code_1', name: 'fn' },
|
||||||
]),
|
newContent: { id: 'code_1', name: 'fn' },
|
||||||
|
changeRecords: [{ propPath: 'd' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
2,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(CodeBlockTab, { props: { buckets, expanded: {} } });
|
const wrapper = mountCodeBlockTab({ buckets, expanded: {} });
|
||||||
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
||||||
await heads[0].trigger('click');
|
await heads[0].trigger('click');
|
||||||
expect(wrapper.emitted('toggle')![0]).toEqual(['cb-code_1-0']);
|
expect(wrapper.emitted('toggle')![0]).toEqual(['cb-code_1-0']);
|
||||||
await heads[1].trigger('click');
|
await heads[1].trigger('click');
|
||||||
expect(wrapper.emitted('toggle')![1]).toEqual(['cb-code_1-1']);
|
expect(wrapper.emitted('toggle')![1]).toEqual(['cb-code_1-2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('goto 透传:携带 codeBlock id 与最后一步 index', async () => {
|
test('goto 透传:携带 codeBlock id 与最后一步 index', async () => {
|
||||||
@ -109,8 +130,8 @@ describe('CodeBlockTab.vue', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(CodeBlockTab, { props: { buckets, expanded: {} } });
|
const wrapper = mountCodeBlockTab({ buckets, expanded: {} });
|
||||||
await wrapper.find('.m-editor-history-list-group-head').trigger('click');
|
await wrapper.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
const events = wrapper.emitted('goto');
|
const events = wrapper.emitted('goto');
|
||||||
expect(events).toBeTruthy();
|
expect(events).toBeTruthy();
|
||||||
expect(events![0]).toEqual(['code_1', 0]);
|
expect(events![0]).toEqual(['code_1', 0]);
|
||||||
@ -138,9 +159,7 @@ describe('CodeBlockTab.vue', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(CodeBlockTab, {
|
const wrapper = mountCodeBlockTab({ buckets, expanded: { 'cb-code_1-0': true } });
|
||||||
props: { buckets, expanded: { 'cb-code_1-0': true } },
|
|
||||||
});
|
|
||||||
const items = wrapper.findAll('.m-editor-history-list-substeps li');
|
const items = wrapper.findAll('.m-editor-history-list-substeps li');
|
||||||
expect(items).toHaveLength(2);
|
expect(items).toHaveLength(2);
|
||||||
// 子步倒序渲染(最新在上):params 在前,content 在后
|
// 子步倒序渲染(最新在上):params 在前,content 在后
|
||||||
|
|||||||
@ -7,8 +7,9 @@ import { describe, expect, test, vi } from 'vitest';
|
|||||||
import { defineComponent, h } from 'vue';
|
import { defineComponent, h } from 'vue';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
|
|
||||||
import DataSourceTab from '@editor/layouts/history-list/DataSourceTab.vue';
|
import BucketTab from '@editor/layouts/history-list/BucketTab.vue';
|
||||||
import type { DataSourceHistoryGroup } from '@editor/type';
|
import { describeDataSourceGroup, describeDataSourceStep } from '@editor/layouts/history-list/composables';
|
||||||
|
import type { DataSourceHistoryGroup, DataSourceStepValue } from '@editor/type';
|
||||||
|
|
||||||
vi.mock('@tmagic/design', () => ({
|
vi.mock('@tmagic/design', () => ({
|
||||||
TMagicScrollbar: defineComponent({
|
TMagicScrollbar: defineComponent({
|
||||||
@ -25,17 +26,31 @@ const buildGroup = (
|
|||||||
opType: 'add' | 'remove' | 'update',
|
opType: 'add' | 'remove' | 'update',
|
||||||
steps: any[],
|
steps: any[],
|
||||||
applied = true,
|
applied = true,
|
||||||
|
startIndex = 0,
|
||||||
): DataSourceHistoryGroup => ({
|
): DataSourceHistoryGroup => ({
|
||||||
kind: 'data-source',
|
kind: 'data-source',
|
||||||
id,
|
id,
|
||||||
opType,
|
opType,
|
||||||
applied,
|
applied,
|
||||||
steps: steps.map((s, i) => ({ step: s, index: i, applied })),
|
steps: steps.map((s, i) => ({ step: s, index: startIndex + i, applied })),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 数据源 tab 复用通用 BucketTab,固定注入数据源的 title/prefix/describe/isStepDiffable。 */
|
||||||
|
const mountDataSourceTab = (props: { buckets: any[]; expanded: Record<string, boolean> }) =>
|
||||||
|
mount(BucketTab, {
|
||||||
|
props: {
|
||||||
|
title: '数据源',
|
||||||
|
prefix: 'ds',
|
||||||
|
describeGroup: describeDataSourceGroup,
|
||||||
|
describeStep: describeDataSourceStep,
|
||||||
|
isStepDiffable: (step: DataSourceStepValue) => Boolean(step.oldSchema && step.newSchema),
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe('DataSourceTab.vue', () => {
|
describe('DataSourceTab.vue', () => {
|
||||||
test('buckets 为空时显示空态', () => {
|
test('buckets 为空时显示空态', () => {
|
||||||
const wrapper = mount(DataSourceTab, { props: { buckets: [], expanded: {} } });
|
const wrapper = mountDataSourceTab({ buckets: [], expanded: {} });
|
||||||
expect(wrapper.find('.m-editor-history-list-empty').exists()).toBe(true);
|
expect(wrapper.find('.m-editor-history-list-empty').exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,7 +67,7 @@ describe('DataSourceTab.vue', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(DataSourceTab, { props: { buckets, expanded: {} } });
|
const wrapper = mountDataSourceTab({ buckets, expanded: {} });
|
||||||
const titles = wrapper.findAll('.m-editor-history-list-bucket-title');
|
const titles = wrapper.findAll('.m-editor-history-list-bucket-title');
|
||||||
expect(titles).toHaveLength(2);
|
expect(titles).toHaveLength(2);
|
||||||
expect(titles[0].text()).toContain('数据源');
|
expect(titles[0].text()).toContain('数据源');
|
||||||
@ -86,27 +101,33 @@ describe('DataSourceTab.vue', () => {
|
|||||||
changeRecords: [{ propPath: 'b' }],
|
changeRecords: [{ propPath: 'b' }],
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
buildGroup('ds_1', 'update', [
|
buildGroup(
|
||||||
{
|
'ds_1',
|
||||||
id: 'ds_1',
|
'update',
|
||||||
oldSchema: { id: 'ds_1', title: 'A' },
|
[
|
||||||
newSchema: { id: 'ds_1', title: 'A2' },
|
{
|
||||||
changeRecords: [{ propPath: 'c' }],
|
id: 'ds_1',
|
||||||
},
|
oldSchema: { id: 'ds_1', title: 'A' },
|
||||||
{
|
newSchema: { id: 'ds_1', title: 'A2' },
|
||||||
id: 'ds_1',
|
changeRecords: [{ propPath: 'c' }],
|
||||||
oldSchema: { id: 'ds_1', title: 'A2' },
|
},
|
||||||
newSchema: { id: 'ds_1', title: 'A3' },
|
{
|
||||||
changeRecords: [{ propPath: 'd' }],
|
id: 'ds_1',
|
||||||
},
|
oldSchema: { id: 'ds_1', title: 'A2' },
|
||||||
]),
|
newSchema: { id: 'ds_1', title: 'A3' },
|
||||||
|
changeRecords: [{ propPath: 'd' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
true,
|
||||||
|
2,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(DataSourceTab, { props: { buckets, expanded: {} } });
|
const wrapper = mountDataSourceTab({ buckets, expanded: {} });
|
||||||
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
||||||
await heads[1].trigger('click');
|
await heads[1].trigger('click');
|
||||||
expect(wrapper.emitted('toggle')![0]).toEqual(['ds-ds_1-1']);
|
expect(wrapper.emitted('toggle')![0]).toEqual(['ds-ds_1-2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('goto 透传:携带 dataSource id 与最后一步 index', async () => {
|
test('goto 透传:携带 dataSource id 与最后一步 index', async () => {
|
||||||
@ -116,8 +137,8 @@ describe('DataSourceTab.vue', () => {
|
|||||||
groups: [buildGroup('ds_1', 'add', [{ id: 'ds_1', oldSchema: null, newSchema: { id: 'ds_1', title: 'A' } }])],
|
groups: [buildGroup('ds_1', 'add', [{ id: 'ds_1', oldSchema: null, newSchema: { id: 'ds_1', title: 'A' } }])],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(DataSourceTab, { props: { buckets, expanded: {} } });
|
const wrapper = mountDataSourceTab({ buckets, expanded: {} });
|
||||||
await wrapper.find('.m-editor-history-list-group-head').trigger('click');
|
await wrapper.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
const events = wrapper.emitted('goto');
|
const events = wrapper.emitted('goto');
|
||||||
expect(events).toBeTruthy();
|
expect(events).toBeTruthy();
|
||||||
expect(events![0]).toEqual(['ds_1', 0]);
|
expect(events![0]).toEqual(['ds_1', 0]);
|
||||||
@ -145,9 +166,7 @@ describe('DataSourceTab.vue', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mount(DataSourceTab, {
|
const wrapper = mountDataSourceTab({ buckets, expanded: { 'ds-ds_1-0': true } });
|
||||||
props: { buckets, expanded: { 'ds-ds_1-0': true } },
|
|
||||||
});
|
|
||||||
expect(wrapper.findAll('.m-editor-history-list-substeps li')).toHaveLength(2);
|
expect(wrapper.findAll('.m-editor-history-list-substeps li')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -103,7 +103,7 @@ describe('GroupRow.vue', () => {
|
|||||||
expect(wrapper.emitted('goto')).toBeFalsy();
|
expect(wrapper.emitted('goto')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('点击单步组(非合并)头部触发 goto,携带该唯一 step 的 index', async () => {
|
test('点击单步组(非合并)的「回到」按钮触发 goto,携带该唯一 step 的 index', async () => {
|
||||||
const wrapper = mount(GroupRow, {
|
const wrapper = mount(GroupRow, {
|
||||||
props: {
|
props: {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
@ -111,7 +111,11 @@ describe('GroupRow.vue', () => {
|
|||||||
subSteps: [{ index: 7, applied: true, desc: 'a' }],
|
subSteps: [{ index: 7, applied: true, desc: 'a' }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
// 点击头部本身不再触发 goto(整行不可点击)
|
||||||
await wrapper.find('.m-editor-history-list-group-head').trigger('click');
|
await wrapper.find('.m-editor-history-list-group-head').trigger('click');
|
||||||
|
expect(wrapper.emitted('goto')).toBeFalsy();
|
||||||
|
// 仅点击「回到」按钮才触发 goto
|
||||||
|
await wrapper.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(wrapper.emitted('goto')).toBeTruthy();
|
expect(wrapper.emitted('goto')).toBeTruthy();
|
||||||
expect(wrapper.emitted('goto')![0]).toEqual([7]);
|
expect(wrapper.emitted('goto')![0]).toEqual([7]);
|
||||||
// 单步组没有展开概念,不应触发 toggle
|
// 单步组没有展开概念,不应触发 toggle
|
||||||
@ -149,7 +153,7 @@ describe('GroupRow.vue', () => {
|
|||||||
expect(wrapper.emitted('goto')).toBeFalsy();
|
expect(wrapper.emitted('goto')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('点击子步触发 goto 携带该子步 index;当前子步点击无效', async () => {
|
test('点击子步「回退」按钮触发 goto 携带该子步 index;当前子步无回退按钮', async () => {
|
||||||
const wrapper = mount(GroupRow, {
|
const wrapper = mount(GroupRow, {
|
||||||
props: {
|
props: {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
@ -162,11 +166,14 @@ describe('GroupRow.vue', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// 子步倒序渲染:subItems[0] 为 index=1(非当前,可点击),subItems[1] 为 index=0(当前)
|
// 子步倒序渲染:subItems[0] 为 index=1(非当前,含跳转按钮),subItems[1] 为 index=0(当前,无跳转按钮)
|
||||||
const subItems = wrapper.findAll('.m-editor-history-list-substeps li');
|
const subItems = wrapper.findAll('.m-editor-history-list-substeps li');
|
||||||
await subItems[1].trigger('click');
|
expect(subItems[1].find('.m-editor-history-list-item-goto').exists()).toBe(false);
|
||||||
expect(wrapper.emitted('goto')).toBeFalsy();
|
// 点击子步行本身不再触发 goto
|
||||||
await subItems[0].trigger('click');
|
await subItems[0].trigger('click');
|
||||||
|
expect(wrapper.emitted('goto')).toBeFalsy();
|
||||||
|
// 仅点击「跳转」按钮才触发 goto
|
||||||
|
await subItems[0].find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(wrapper.emitted('goto')![0]).toEqual([1]);
|
expect(wrapper.emitted('goto')![0]).toEqual([1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -187,11 +187,11 @@ describe('HistoryListPanel.vue', () => {
|
|||||||
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
||||||
expect(heads.length).toBeGreaterThanOrEqual(2);
|
expect(heads.length).toBeGreaterThanOrEqual(2);
|
||||||
// 第二行(pg-1)对应原始 step.index = 0;cursor 应为 0+1 = 1
|
// 第二行(pg-1)对应原始 step.index = 0;cursor 应为 0+1 = 1
|
||||||
await heads[1].trigger('click');
|
await heads[1].find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(editorService.gotoPageStep).toHaveBeenCalledTimes(1);
|
expect(editorService.gotoPageStep).toHaveBeenCalledTimes(1);
|
||||||
expect(editorService.gotoPageStep).toHaveBeenCalledWith(1);
|
expect(editorService.gotoPageStep).toHaveBeenCalledWith(1);
|
||||||
|
|
||||||
// 当前组点击不触发 goto
|
// 当前组没有「回到」按钮,点击头部不触发 goto
|
||||||
await head.trigger('click');
|
await head.trigger('click');
|
||||||
expect(editorService.gotoPageStep).toHaveBeenCalledTimes(1);
|
expect(editorService.gotoPageStep).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
@ -213,7 +213,7 @@ describe('HistoryListPanel.vue', () => {
|
|||||||
// 找到数据源 tab 那一组
|
// 找到数据源 tab 那一组
|
||||||
const dsHead = heads.find((h) => h.text().includes('创建 DS'));
|
const dsHead = heads.find((h) => h.text().includes('创建 DS'));
|
||||||
expect(dsHead).toBeTruthy();
|
expect(dsHead).toBeTruthy();
|
||||||
await dsHead!.trigger('click');
|
await dsHead!.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(dataSourceService.goto).toHaveBeenCalledWith('ds_1', 1);
|
expect(dataSourceService.goto).toHaveBeenCalledWith('ds_1', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ describe('HistoryListPanel.vue', () => {
|
|||||||
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
const heads = wrapper.findAll('.m-editor-history-list-group-head');
|
||||||
const cbHead = heads.find((h) => h.text().includes('创建 CB'));
|
const cbHead = heads.find((h) => h.text().includes('创建 CB'));
|
||||||
expect(cbHead).toBeTruthy();
|
expect(cbHead).toBeTruthy();
|
||||||
await cbHead!.trigger('click');
|
await cbHead!.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(codeBlockService.goto).toHaveBeenCalledWith('code_1', 1);
|
expect(codeBlockService.goto).toHaveBeenCalledWith('code_1', 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -251,7 +251,7 @@ describe('HistoryListPanel.vue', () => {
|
|||||||
const initials = wrapper.findAll('.m-editor-history-list-initial');
|
const initials = wrapper.findAll('.m-editor-history-list-initial');
|
||||||
expect(initials.length).toBeGreaterThanOrEqual(1);
|
expect(initials.length).toBeGreaterThanOrEqual(1);
|
||||||
// 第一项(页面 tab)应为页面 tab 的初始项;page tab 在三个 tab 中最先渲染
|
// 第一项(页面 tab)应为页面 tab 的初始项;page tab 在三个 tab 中最先渲染
|
||||||
await initials[0].trigger('click');
|
await initials[0].find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(editorService.gotoPageStep).toHaveBeenCalledWith(0);
|
expect(editorService.gotoPageStep).toHaveBeenCalledWith(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -307,10 +307,10 @@ describe('HistoryListPanel.vue', () => {
|
|||||||
|
|
||||||
// 顺序:tab 渲染顺序是 page → data-source → code-block
|
// 顺序:tab 渲染顺序是 page → data-source → code-block
|
||||||
// 因此 initials[0] 属于 ds_x,initials[1] 属于 code_x
|
// 因此 initials[0] 属于 ds_x,initials[1] 属于 code_x
|
||||||
await initials[0].trigger('click');
|
await initials[0].find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(dataSourceService.goto).toHaveBeenCalledWith('ds_x', 0);
|
expect(dataSourceService.goto).toHaveBeenCalledWith('ds_x', 0);
|
||||||
|
|
||||||
await initials[1].trigger('click');
|
await initials[1].find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(codeBlockService.goto).toHaveBeenCalledWith('code_x', 0);
|
expect(codeBlockService.goto).toHaveBeenCalledWith('code_x', 0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,15 +17,15 @@ describe('InitialRow.vue', () => {
|
|||||||
expect(wrapper.find('.m-editor-history-list-item-desc').text()).toBe('未修改的初始状态');
|
expect(wrapper.find('.m-editor-history-list-item-desc').text()).toBe('未修改的初始状态');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('isCurrent=true 时附 is-current 类名并显示「当前」徽标', () => {
|
test('isCurrent=true 时附 is-current 类名且不展示「回到」按钮', () => {
|
||||||
const wrapper = mount(InitialRow, { props: { isCurrent: true } });
|
const wrapper = mount(InitialRow, { props: { isCurrent: true } });
|
||||||
expect(wrapper.find('.m-editor-history-list-initial').classes()).toContain('is-current');
|
expect(wrapper.find('.m-editor-history-list-initial').classes()).toContain('is-current');
|
||||||
expect(wrapper.find('.m-editor-history-list-item-current').exists()).toBe(true);
|
expect(wrapper.find('.m-editor-history-list-item-goto').exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('非当前时点击触发 goto-initial 事件', async () => {
|
test('非当前时点击「回到」按钮触发 goto-initial 事件', async () => {
|
||||||
const wrapper = mount(InitialRow, { props: { isCurrent: false } });
|
const wrapper = mount(InitialRow, { props: { isCurrent: false } });
|
||||||
await wrapper.find('.m-editor-history-list-initial').trigger('click');
|
await wrapper.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(wrapper.emitted('goto-initial')).toBeTruthy();
|
expect(wrapper.emitted('goto-initial')).toBeTruthy();
|
||||||
expect(wrapper.emitted('goto-initial')).toHaveLength(1);
|
expect(wrapper.emitted('goto-initial')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,6 +26,7 @@ const buildPageGroup = (
|
|||||||
applied = true,
|
applied = true,
|
||||||
targetName?: string,
|
targetName?: string,
|
||||||
targetId?: string,
|
targetId?: string,
|
||||||
|
startIndex = 0,
|
||||||
): PageHistoryGroup => ({
|
): PageHistoryGroup => ({
|
||||||
kind: 'page',
|
kind: 'page',
|
||||||
pageId: 'p1',
|
pageId: 'p1',
|
||||||
@ -33,7 +34,7 @@ const buildPageGroup = (
|
|||||||
applied,
|
applied,
|
||||||
targetId,
|
targetId,
|
||||||
targetName,
|
targetName,
|
||||||
steps: steps.map((s, i) => ({ step: s, index: i, applied })),
|
steps: steps.map((s, i) => ({ step: s, index: startIndex + i, applied })),
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PageTab.vue', () => {
|
describe('PageTab.vue', () => {
|
||||||
@ -148,6 +149,7 @@ describe('PageTab.vue', () => {
|
|||||||
true,
|
true,
|
||||||
'按钮2',
|
'按钮2',
|
||||||
'btn2',
|
'btn2',
|
||||||
|
2,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
const wrapper = mount(PageTab, { props: { list, expanded: {} } });
|
const wrapper = mount(PageTab, { props: { list, expanded: {} } });
|
||||||
@ -155,15 +157,15 @@ describe('PageTab.vue', () => {
|
|||||||
await heads[1].trigger('click');
|
await heads[1].trigger('click');
|
||||||
const events = wrapper.emitted('toggle');
|
const events = wrapper.emitted('toggle');
|
||||||
expect(events).toBeTruthy();
|
expect(events).toBeTruthy();
|
||||||
expect(events![0]).toEqual(['pg-1']);
|
expect(events![0]).toEqual(['pg-2']);
|
||||||
// 合并组头部不应触发 goto
|
// 合并组头部不应触发 goto
|
||||||
expect(wrapper.emitted('goto')).toBeFalsy();
|
expect(wrapper.emitted('goto')).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('点击单步组头部透传 goto 事件,携带该 step 的 index', async () => {
|
test('点击单步组「回到」按钮透传 goto 事件,携带该 step 的 index', async () => {
|
||||||
const list = [buildPageGroup('add', [{ opType: 'add', nodes: [{ id: 'n1', name: 'A' }] }])];
|
const list = [buildPageGroup('add', [{ opType: 'add', nodes: [{ id: 'n1', name: 'A' }] }])];
|
||||||
const wrapper = mount(PageTab, { props: { list, expanded: {} } });
|
const wrapper = mount(PageTab, { props: { list, expanded: {} } });
|
||||||
await wrapper.find('.m-editor-history-list-group-head').trigger('click');
|
await wrapper.find('.m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(wrapper.emitted('goto')).toBeTruthy();
|
expect(wrapper.emitted('goto')).toBeTruthy();
|
||||||
expect(wrapper.emitted('goto')![0]).toEqual([0]);
|
expect(wrapper.emitted('goto')![0]).toEqual([0]);
|
||||||
expect(wrapper.emitted('toggle')).toBeFalsy();
|
expect(wrapper.emitted('toggle')).toBeFalsy();
|
||||||
@ -203,10 +205,10 @@ describe('PageTab.vue', () => {
|
|||||||
expect(initial.classes()).not.toContain('is-current');
|
expect(initial.classes()).not.toContain('is-current');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('点击非当前的初始项透传 goto-initial 事件', async () => {
|
test('点击非当前初始项的「回到」按钮透传 goto-initial 事件', async () => {
|
||||||
const list = [buildPageGroup('add', [{ opType: 'add', nodes: [{ id: 'n1', name: 'A' }] }], true)];
|
const list = [buildPageGroup('add', [{ opType: 'add', nodes: [{ id: 'n1', name: 'A' }] }], true)];
|
||||||
const wrapper = mount(PageTab, { props: { list, expanded: {} } });
|
const wrapper = mount(PageTab, { props: { list, expanded: {} } });
|
||||||
await wrapper.find('.m-editor-history-list-initial').trigger('click');
|
await wrapper.find('.m-editor-history-list-initial .m-editor-history-list-item-goto').trigger('click');
|
||||||
expect(wrapper.emitted('goto-initial')).toBeTruthy();
|
expect(wrapper.emitted('goto-initial')).toBeTruthy();
|
||||||
expect(wrapper.emitted('goto-initial')).toHaveLength(1);
|
expect(wrapper.emitted('goto-initial')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user