feat(module-management): 导航状态化阶段一——分 section 脏检查 + tab 脏点
- useModuleConfig 新增 dirtySections(逐段 JSON.stringify 对比),hasModuleChanges 改为其派生;先以整树对比语义为基准写等价断言(含「加载补全默认行即脏」的 既有怪癖),新 spec module-dirty-sections 7 用例全绿 - index.vue #label 插槽给脏 tab 渲染 6px 圆点(title/aria-label,不只靠颜色), ui/Tabs 组件零改动;groups 由列表/表单两面板共享编辑,两 tab 联动标脏 - 计划文档 docs/module-management-tab-status-plan.md(横排保留、不做分组为已决策)
This commit is contained in:
1 parent
05d0c7f304
commit
4f89f373e6
4 files changed
+297
-4
No files matched your search
@@ -0,0 +1,95 @@
|
|||||||
|
# `/system/module-management` 导航状态化改造计划
|
||||||
|
|
||||||
|
> 本轮只做导航与状态呈现层:让 8 个 tab 从「哑的入口」变成「会说话的入口」,并让概览页承担配置引导。
|
||||||
|
> 不改数据层、不改保存协议与 API。横排导航保留,**不做分组**(已决策:竖排浪费横向空间,8 个平铺 tab 可接受)。
|
||||||
|
|
||||||
|
## 1. 背景与问题
|
||||||
|
|
||||||
|
现状(`index.vue`):右侧横排胶囊 Tabs(`navOnly` + 面板 `v-show` 保活),由 `configGroups`/`configNavItems` 驱动;仅查询配置有「缺 N」徽标。
|
||||||
|
|
||||||
|
使用侧问题(面向开发与售后两类用户):
|
||||||
|
|
||||||
|
1. **tab 不反映状态**:改过哪些 tab、哪些 tab 缺配置,界面上看不出来,全靠用户记忆;
|
||||||
|
2. **一个任务跨多个 tab**(加字段 → 表单设计/列表/查询都要看一眼),界面不引导用户走完;
|
||||||
|
3. **概览 tab 只有基本信息**,没有承担「新模块配到哪了、还差什么」的引导职责。
|
||||||
|
|
||||||
|
## 2. 总体约束
|
||||||
|
|
||||||
|
1. 不改子面板对 index.vue 的 props/emits 契约、不改保存请求结构、不改 API 调用。
|
||||||
|
2. **不改 `ui/Tabs` 组件**:脏点与徽标全部经现有 `#label` 插槽渲染(index.vue 已在用该插槽画图标+文字+徽标)。
|
||||||
|
3. 保留 `activeSection` 机制与 `v-show` 保活,不改键盘左右切换行为。
|
||||||
|
4. 每阶段独立 commit;每阶段结束 `pnpm test && pnpm lint` 全绿。涉及导航的测试仅允许更新选择器/文案,不改行为断言。
|
||||||
|
5. 徽标/圆点不得只靠颜色表达:一律配 `title`/`aria-label`。
|
||||||
|
|
||||||
|
## 3. 阶段任务
|
||||||
|
|
||||||
|
### 阶段一:分 section 脏检查 + tab 脏点
|
||||||
|
|
||||||
|
`useModuleConfig` 新增 `dirtySections`(Set<sectionKey>):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const SECTION_KEYS = ['module', 'fields', 'groups', 'listConfig', 'editConfig', 'queryConfig', 'autoCode', 'powers']
|
||||||
|
// 仅对比已加载的 section;逐 section JSON.stringify(drafts[key]) === JSON.stringify(originals[key])
|
||||||
|
```
|
||||||
|
|
||||||
|
- 现有 `hasModuleChanges` 改为 `dirtySections.size > 0` 的派生值,**行为必须等价**(切换保护、重载确认依赖它)。
|
||||||
|
- **注意「假脏」陷阱**:`loadModuleConfig` 加载时会自动补全列表/查询默认行(useModuleConfig.js:79–85 附近)。若补全只写 `drafts` 不写 `originals`,分 section 对比会立即「脏」。实现时先确认现有整树对比对这些默认行的处理方式,以「现有 `hasModuleChanges` 的判定结果」为基准写等价断言,再动手。
|
||||||
|
- index.vue `#label` 插槽给脏 tab 追加圆点(约 6px,标签右上角,`title="有未保存修改"`)。
|
||||||
|
|
||||||
|
验收:任一 tab 内容修改 → 对应 tab 出现圆点;保存成功或放弃修改 → 全部消失;切换未保存保护、重载确认行为与改造前一致。
|
||||||
|
|
||||||
|
### 阶段二:表单设计「缺 N」徽标
|
||||||
|
|
||||||
|
数据模块下统计「启用字段中未被 `editConfig` 任何行的 `b_field_id` 引用的数量」,作为表单设计 tab 的徽标,样式与语义沿用查询配置的「缺 N」。
|
||||||
|
|
||||||
|
- 口径以 `b_field_id` 引用集合为准,不涉及布局细节(colspan/分组不影响计数)。
|
||||||
|
- `editConfig` 同时承载主表表单/子表表格两种编辑方式,此口径对两者通用。
|
||||||
|
- 查询配置现有徽标不动。
|
||||||
|
|
||||||
|
验收:字段定义新增启用字段 → 表单设计 tab 显示「缺 1」;拖入画布后徽标消失;子表表格编辑方式下口径同样成立。
|
||||||
|
|
||||||
|
### 阶段三:概览 tab 升级为完成度 checklist
|
||||||
|
|
||||||
|
在概览 tab(basic)内增加「配置完成度」区块,新组件 `components/ModuleOverviewChecklist.vue`,数据全部由 `drafts` 派生:
|
||||||
|
|
||||||
|
| 条目 | 内容 | 状态 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 字段定义 | 已启用 X 个字段 | 0 → 提示「先定义字段」 |
|
||||||
|
| 表单设计 | 缺 N 个未布局(复用阶段二统计) | N>0 → 警示色 |
|
||||||
|
| 列表配置 | X 列可见(加载自动补全,仅展示数量) | 中性 |
|
||||||
|
| 查询配置 | 缺 N(复用 `missingQueryCount`) | N>0 → 警示色 + 「一键补充」 |
|
||||||
|
| 权限 | 已配置 X 项 | 0 → 中性提示 |
|
||||||
|
|
||||||
|
- 每条目整行可点击 → `activeSection` 跳转对应 tab;「一键补充」直接调用现有 `fillMissingConfigs(['queryConfig'])`。
|
||||||
|
- 仅数据/查询模块渲染对应条目(与 `configGroups` 的显隐条件一致);分类模块不渲染 checklist。
|
||||||
|
- 条目按依赖顺序排列(字段 → 表单 → 列表 → 查询 → 权限),隐式表达配置路径,不做向导式交互。
|
||||||
|
|
||||||
|
验收:各计数与对应 tab 实际状态一致;点击跳转正确;新建数据模块的空配置显示引导文案;分类模块无 checklist。
|
||||||
|
|
||||||
|
### 阶段四(可选,单独决策后另开任务):字段中心旁路
|
||||||
|
|
||||||
|
字段定义面板表格追加三列状态点(表单/列表/查询:已配置/未配置),hover 显示说明,点击跳转对应 tab。
|
||||||
|
|
||||||
|
- 第一版只做「跳 tab」,不做「定位到该字段行」(跨面板选中态联动成本高,留待验证需求后再做)。
|
||||||
|
- 与阶段二/三共用同一套「字段引用状态」统计函数,单一定义点。
|
||||||
|
|
||||||
|
## 4. 明确不做的事
|
||||||
|
|
||||||
|
- 不做 tab 分组、不做竖排导航、不改 `ui/Tabs` 组件。
|
||||||
|
- 不动多语言 tab 的位置(是否移出主序列属独立决策,本轮不处理)。
|
||||||
|
- 不做向导式(wizard)配置流。
|
||||||
|
- 不动保存粒度、不做撤销/重做、不加离开守卫(属「安全网」另一条改造线,见对话记录,另行立项)。
|
||||||
|
|
||||||
|
## 5. 手工冒烟清单(每阶段结束过一遍)
|
||||||
|
|
||||||
|
```text
|
||||||
|
脏点:8 个 tab 各改一处 → 仅对应 tab 出点;保存/放弃后清除;
|
||||||
|
切换模块三选一弹窗、重载确认、Ctrl/Cmd+S 行为不变
|
||||||
|
徽标:新增字段未布局 → 表单设计「缺 1」;拖入后消失;查询「缺 N」回归
|
||||||
|
checklist:各计数正确;条目点击跳 tab;一键补充后查询条目转绿
|
||||||
|
通用:窄窗口下 tab 行横向滚动可用;键盘 ←/→ 切换不变;分类模块的 tab 集合不变
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 可直接交给其他 AI 的执行指令
|
||||||
|
|
||||||
|
在 `fms-vue` 项目中按照 `docs/module-management-tab-status-plan.md` 改造 `/system/module-management` 的导航状态层。先阅读 `index.vue`(`configGroups`/`configNavItems`/`#label` 插槽)、`composables/useModuleConfig.js`(`hasModuleChanges` 与加载时默认行补全)及相关测试,再按阶段一至三逐步执行(阶段四默认不做):每阶段一个 commit,阶段一必须先为 `hasModuleChanges` 写等价断言再改实现。全程不改子面板契约、不改保存协议、不改 `ui/Tabs` 组件。每阶段结束运行 `pnpm test && pnpm lint` 并按第 5 节清单手工冒烟。交付时说明:修改文件、`dirtySections` 的等价性验证方式、「假脏」问题的实际结论、测试与冒烟结果。
|
||||||
@@ -114,12 +114,32 @@ export function useModuleConfig(deps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/* ================= 脏检查 ================= */
|
/* ================= 脏检查 ================= */
|
||||||
// 是否有未保存的修改(用于「重载」按钮的确认提示)
|
// 分 section 脏检查:逐段 JSON.stringify 对比(与整树对比判定结果等价——
|
||||||
const hasModuleChanges = computed(() => {
|
// draft/original 均由 loadModuleConfig 以同一组键构造,整树不同 ⟺ 存在段不同)。
|
||||||
if (!drafts.value || !originals.value) return false
|
// 已知怪癖(既有行为,保留):加载时补全的默认列表行只写 drafts(见 loadModuleConfig),
|
||||||
return JSON.stringify(drafts.value) !== JSON.stringify(originals.value)
|
// 此时 listConfig 打开即「脏」——与原整树对比一致,不得顺手修复。
|
||||||
|
const SECTION_KEYS = [
|
||||||
|
'module',
|
||||||
|
'fields',
|
||||||
|
'groups',
|
||||||
|
'listConfig',
|
||||||
|
'editConfig',
|
||||||
|
'queryConfig',
|
||||||
|
'autoCode',
|
||||||
|
'powers',
|
||||||
|
]
|
||||||
|
const dirtySections = computed(() => {
|
||||||
|
const dirty = new Set()
|
||||||
|
if (!drafts.value || !originals.value) return dirty
|
||||||
|
for (const key of SECTION_KEYS) {
|
||||||
|
if (JSON.stringify(drafts.value[key]) !== JSON.stringify(originals.value[key])) dirty.add(key)
|
||||||
|
}
|
||||||
|
return dirty
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 是否有未保存的修改(用于切换保护与「重载」按钮的确认提示)
|
||||||
|
const hasModuleChanges = computed(() => dirtySections.value.size > 0)
|
||||||
|
|
||||||
function onModuleSelect(keys) {
|
function onModuleSelect(keys) {
|
||||||
if (!keys.length) return
|
if (!keys.length) return
|
||||||
const nextId = String(keys[0])
|
const nextId = String(keys[0])
|
||||||
@@ -369,6 +389,8 @@ export function useModuleConfig(deps) {
|
|||||||
const missingQueryCount = missingConfigCount('queryConfig')
|
const missingQueryCount = missingConfigCount('queryConfig')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
hasModuleChanges,
|
||||||
|
dirtySections,
|
||||||
reloadModule,
|
reloadModule,
|
||||||
onModuleSelect,
|
onModuleSelect,
|
||||||
onFieldsUpdate,
|
onFieldsUpdate,
|
||||||
|
|||||||
@@ -189,6 +189,26 @@ const configNavItems = computed(() =>
|
|||||||
/* 编辑设计器:form/table 两面板 props/emits 契约一致,用动态组件消除模板重复 */
|
/* 编辑设计器:form/table 两面板 props/emits 契约一致,用动态组件消除模板重复 */
|
||||||
const activeEditPanel = computed(() => (editMode.value === 'table' ? ModuleTableEditPanel : ModuleFormEditPanel))
|
const activeEditPanel = computed(() => (editMode.value === 'table' ? ModuleTableEditPanel : ModuleFormEditPanel))
|
||||||
|
|
||||||
|
/* tab 脏点:tab key → drafts section 映射。groups 在列表/表单两个面板都可编辑,
|
||||||
|
分组变化时两个 tab 都标脏;多语言 tab 的数据独立保存(不走 drafts),不参与脏检查 */
|
||||||
|
const TAB_SECTION_MAP = {
|
||||||
|
basic: ['module'],
|
||||||
|
fields: ['fields'],
|
||||||
|
list: ['listConfig', 'groups'],
|
||||||
|
edit: ['editConfig', 'groups'],
|
||||||
|
query: ['queryConfig'],
|
||||||
|
'auto-code': ['autoCode'],
|
||||||
|
powers: ['powers'],
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirtyTabKeys = computed(() => {
|
||||||
|
const keys = new Set()
|
||||||
|
for (const [tabKey, sections] of Object.entries(TAB_SECTION_MAP)) {
|
||||||
|
if (sections.some((section) => dirtySections.value.has(section))) keys.add(tabKey)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
})
|
||||||
|
|
||||||
/* ================= composables ================= */
|
/* ================= composables ================= */
|
||||||
const { saveModule } = useModuleSave({
|
const { saveModule } = useModuleSave({
|
||||||
selectedModule,
|
selectedModule,
|
||||||
@@ -296,6 +316,7 @@ const {
|
|||||||
onFieldsUpdate,
|
onFieldsUpdate,
|
||||||
onAutoCodeUpdate,
|
onAutoCodeUpdate,
|
||||||
syncFields,
|
syncFields,
|
||||||
|
dirtySections,
|
||||||
missingQueryCount,
|
missingQueryCount,
|
||||||
fillMissingConfigs,
|
fillMissingConfigs,
|
||||||
} = useModuleConfig({
|
} = useModuleConfig({
|
||||||
@@ -447,6 +468,14 @@ loadModuleTree()
|
|||||||
>
|
>
|
||||||
<template #label="{ item }">
|
<template #label="{ item }">
|
||||||
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
|
<component :is="item.icon" :size="14" class="mm-config-nav__icon" />
|
||||||
|
<!-- 脏点:有未保存修改的 tab 在图标后显示实心圆点(形状+title,不只靠颜色) -->
|
||||||
|
<span
|
||||||
|
v-if="dirtyTabKeys.has(item.key)"
|
||||||
|
class="mm-config-nav__dirty"
|
||||||
|
role="img"
|
||||||
|
title="有未保存修改"
|
||||||
|
aria-label="有未保存修改"
|
||||||
|
></span>
|
||||||
<span class="mm-config-nav__label">{{ item.label }}</span>
|
<span class="mm-config-nav__label">{{ item.label }}</span>
|
||||||
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
|
<span v-if="item.badge" class="mm-config-nav__badge">{{ item.badge }}</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -826,6 +855,15 @@ loadModuleTree()
|
|||||||
color: var(--fms-text-secondary);
|
color: var(--fms-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 脏点:作为 label 插槽的 flex 子元素排在图标后,间距由 .tabs-tab 的 gap 统一管理 */
|
||||||
|
.mm-config-nav__dirty {
|
||||||
|
flex: none;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--fms-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.mm-config-nav .tabs-tab-active .mm-config-nav__icon {
|
.mm-config-nav .tabs-tab-active .mm-config-nav__icon {
|
||||||
color: var(--fms-primary);
|
color: var(--fms-primary);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 分 section 脏检查测试:守护 hasModuleChanges 的判定语义。
|
||||||
|
* 背景见 docs/module-management-tab-status-plan.md 阶段一:
|
||||||
|
* - 加载时自动补全的默认列表行只写 drafts 不写 originals(useModuleConfig.loadModuleConfig),
|
||||||
|
* 此时视为「脏」是既有行为,分 section 对比必须原样保留,不得“修复”;
|
||||||
|
* - hasModuleChanges 从整树 JSON.stringify 对比改为 dirtySections 派生,
|
||||||
|
* 本 spec 以判定结果等价为验收基准。
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useModuleConfig } from '@/views/system/module-management/composables/useModuleConfig'
|
||||||
|
|
||||||
|
vi.mock('@/services/api', () => ({
|
||||||
|
loadDataApi: vi.fn(),
|
||||||
|
describeApi: vi.fn(),
|
||||||
|
}))
|
||||||
|
vi.mock('@/components/ui', () => ({
|
||||||
|
Message: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() },
|
||||||
|
}))
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 完整的 8 段草稿结构(与 loadModuleConfig 构造的 original 同形) */
|
||||||
|
function baseConfig(over = {}) {
|
||||||
|
const config = {
|
||||||
|
module: { b_id: '1', b_code: 'demo', b_name: '演示模块', b_module_type: 'data' },
|
||||||
|
fields: [{ b_id: '10', b_field: 'amount', b_name: '金额', b_canuse: 1 }],
|
||||||
|
groups: [],
|
||||||
|
listConfig: [],
|
||||||
|
editConfig: [],
|
||||||
|
queryConfig: [],
|
||||||
|
autoCode: [],
|
||||||
|
powers: [],
|
||||||
|
}
|
||||||
|
return Object.assign(config, over)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup({ drafts, originals }) {
|
||||||
|
const draftsRef = ref(drafts)
|
||||||
|
const originalsRef = ref(originals)
|
||||||
|
const deps = {
|
||||||
|
moduleSelectedId: ref('1'),
|
||||||
|
moduleTreeData: ref([]),
|
||||||
|
findModuleNode: () => null,
|
||||||
|
selectedModule: ref({ b_id: '1', b_code: 'demo', b_name: '演示模块' }),
|
||||||
|
busy: ref(false),
|
||||||
|
configLoading: ref(false),
|
||||||
|
configSyncing: ref(false),
|
||||||
|
activeSection: ref('basic'),
|
||||||
|
loadedModuleId: ref('1'),
|
||||||
|
originals: originalsRef,
|
||||||
|
drafts: draftsRef,
|
||||||
|
skipModuleLoad: { value: false },
|
||||||
|
showConfirm: vi.fn(),
|
||||||
|
switchConfirm: ref(null),
|
||||||
|
i18nPanelRef: ref(null),
|
||||||
|
}
|
||||||
|
return { ...useModuleConfig(deps), originalsRef, draftsRef }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useModuleConfig 脏检查', () => {
|
||||||
|
it('未修改时 hasModuleChanges 为 false', () => {
|
||||||
|
const originals = baseConfig()
|
||||||
|
const { hasModuleChanges } = setup({ drafts: JSON.parse(JSON.stringify(originals)), originals })
|
||||||
|
expect(hasModuleChanges.value).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('无草稿时 hasModuleChanges 为 false', () => {
|
||||||
|
const { hasModuleChanges } = setup({ drafts: null, originals: null })
|
||||||
|
expect(hasModuleChanges.value).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('任一 section 修改即 hasModuleChanges 为 true', () => {
|
||||||
|
const originals = baseConfig()
|
||||||
|
for (const key of ['module', 'fields', 'groups', 'listConfig', 'editConfig', 'queryConfig', 'autoCode', 'powers']) {
|
||||||
|
const drafts = JSON.parse(JSON.stringify(originals))
|
||||||
|
if (key === 'module') drafts.module.b_name = '改名'
|
||||||
|
else drafts[key] = [...drafts[key], { b_id: 'new' }]
|
||||||
|
const { hasModuleChanges } = setup({ drafts, originals })
|
||||||
|
expect(hasModuleChanges.value, `修改 ${key} 应判定为脏`).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('加载时补全的默认列表行(只写 drafts)视为脏——既有行为', () => {
|
||||||
|
const originals = baseConfig()
|
||||||
|
const drafts = JSON.parse(JSON.stringify(originals))
|
||||||
|
drafts.listConfig = [{ b_id: 'tmp1', b_field_id: '10', b_visible: 1, b_xh: 10 }]
|
||||||
|
const { hasModuleChanges } = setup({ drafts, originals })
|
||||||
|
expect(hasModuleChanges.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('查询模块缺失的 section(undefined)两侧一致时不脏,单侧出现即脏', () => {
|
||||||
|
const originals = {
|
||||||
|
module: { b_id: '2', b_code: 'q', b_module_type: 'query' },
|
||||||
|
fields: [],
|
||||||
|
groups: [],
|
||||||
|
listConfig: [],
|
||||||
|
editConfig: undefined,
|
||||||
|
queryConfig: [],
|
||||||
|
autoCode: undefined,
|
||||||
|
powers: [],
|
||||||
|
}
|
||||||
|
const same = setup({ drafts: JSON.parse(JSON.stringify(originals)), originals })
|
||||||
|
expect(same.hasModuleChanges.value).toBe(false)
|
||||||
|
|
||||||
|
const drafts = JSON.parse(JSON.stringify(originals))
|
||||||
|
drafts.editConfig = []
|
||||||
|
const diff = setup({ drafts, originals })
|
||||||
|
expect(diff.hasModuleChanges.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('dirtySections 精确指出被修改的 section', () => {
|
||||||
|
const originals = baseConfig()
|
||||||
|
const drafts = JSON.parse(JSON.stringify(originals))
|
||||||
|
drafts.module.b_name = '改名'
|
||||||
|
drafts.queryConfig = [{ b_id: 'q1' }]
|
||||||
|
const { dirtySections } = setup({ drafts, originals })
|
||||||
|
expect(dirtySections.value.has('module')).toBe(true)
|
||||||
|
expect(dirtySections.value.has('queryConfig')).toBe(true)
|
||||||
|
expect(dirtySections.value.has('fields')).toBe(false)
|
||||||
|
expect(dirtySections.value.size).toBe(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('保存后整体替换 originals/drafts 为同一份深拷贝,脏状态清空', () => {
|
||||||
|
const originals = baseConfig()
|
||||||
|
const drafts = JSON.parse(JSON.stringify(originals))
|
||||||
|
drafts.module.b_name = '改名'
|
||||||
|
const api = setup({ drafts, originals })
|
||||||
|
expect(api.hasModuleChanges.value).toBe(true)
|
||||||
|
// 模拟 useModuleSave 保存后的整体 cloneData 换引用
|
||||||
|
const saved = JSON.parse(JSON.stringify(drafts))
|
||||||
|
api.originalsRef.value = saved
|
||||||
|
api.draftsRef.value = JSON.parse(JSON.stringify(saved))
|
||||||
|
expect(api.hasModuleChanges.value).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in new issue
Block a user