优化构架
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
|
||||
describe('unsaved work store', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('blocks dirty work only when a role switch removes permission or leaves the page', () => {
|
||||
const store = useUnsavedWorkStore()
|
||||
store.upsert({
|
||||
key: 'customer-form',
|
||||
label: '客户资料',
|
||||
dirty: true,
|
||||
requiredPermission: 'customer:create',
|
||||
})
|
||||
store.upsert({
|
||||
key: 'order-form',
|
||||
label: '头程订单',
|
||||
dirty: true,
|
||||
requiredPermission: 'first-leg:create',
|
||||
})
|
||||
|
||||
expect(store.findBlocking(new Set(['customer:create', 'first-leg:create']), false)).toEqual([])
|
||||
expect(store.findBlocking(new Set(), false).map((entry) => entry.key)).toEqual([
|
||||
'customer-form',
|
||||
'order-form',
|
||||
])
|
||||
expect(store.findBlocking(new Set(['customer:create', 'first-leg:create']), true)
|
||||
.map((entry) => entry.key)).toEqual(['customer-form', 'order-form'])
|
||||
})
|
||||
|
||||
it('does not block clean work', () => {
|
||||
const store = useUnsavedWorkStore()
|
||||
store.upsert({
|
||||
key: 'customer-form',
|
||||
label: '客户资料',
|
||||
dirty: false,
|
||||
requiredPermission: 'customer:create',
|
||||
})
|
||||
|
||||
expect(store.findBlocking(new Set(), true)).toEqual([])
|
||||
})
|
||||
|
||||
it('removes an entry by key', () => {
|
||||
const store = useUnsavedWorkStore()
|
||||
store.upsert({ key: 'customer-form', label: '客户资料', dirty: true, requiredPermission: null })
|
||||
|
||||
store.remove('customer-form')
|
||||
|
||||
expect(store.entries).toEqual([])
|
||||
})
|
||||
|
||||
it('replaces an existing entry with the same key', () => {
|
||||
const store = useUnsavedWorkStore()
|
||||
store.upsert({ key: 'customer-form', label: '旧名称', dirty: false, requiredPermission: null })
|
||||
|
||||
store.upsert({
|
||||
key: 'customer-form',
|
||||
label: '客户资料',
|
||||
dirty: true,
|
||||
requiredPermission: 'customer:create',
|
||||
})
|
||||
|
||||
expect(store.entries).toEqual([{
|
||||
key: 'customer-form',
|
||||
label: '客户资料',
|
||||
dirty: true,
|
||||
requiredPermission: 'customer:create',
|
||||
}])
|
||||
})
|
||||
|
||||
it('clears every registered entry', () => {
|
||||
const store = useUnsavedWorkStore()
|
||||
store.upsert({ key: 'customer-form', label: '客户资料', dirty: true, requiredPermission: null })
|
||||
store.upsert({ key: 'order-form', label: '头程订单', dirty: true, requiredPermission: null })
|
||||
|
||||
store.clear()
|
||||
|
||||
expect(store.entries).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface UnsavedWorkEntry {
|
||||
key: string
|
||||
label: string
|
||||
dirty: boolean
|
||||
requiredPermission: string | null
|
||||
}
|
||||
|
||||
export const useUnsavedWorkStore = defineStore('unsaved-work', () => {
|
||||
const entries = ref<UnsavedWorkEntry[]>([])
|
||||
|
||||
function upsert(entry: UnsavedWorkEntry) {
|
||||
const index = entries.value.findIndex((item) => item.key === entry.key)
|
||||
if (index === -1) {
|
||||
entries.value.push(entry)
|
||||
return
|
||||
}
|
||||
entries.value[index] = entry
|
||||
}
|
||||
|
||||
function remove(key: string) {
|
||||
const index = entries.value.findIndex((item) => item.key === key)
|
||||
if (index !== -1) entries.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function findBlocking(candidatePermissions: ReadonlySet<string>, willLeave: boolean) {
|
||||
return entries.value.filter((entry) => entry.dirty && (
|
||||
willLeave
|
||||
|| (entry.requiredPermission !== null && !candidatePermissions.has(entry.requiredPermission))
|
||||
))
|
||||
}
|
||||
|
||||
function clear() {
|
||||
entries.value = []
|
||||
}
|
||||
|
||||
return { entries, upsert, remove, findBlocking, clear }
|
||||
})
|
||||
@@ -0,0 +1,101 @@
|
||||
import { computed, defineComponent, nextTick, reactive, ref } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
|
||||
interface HarnessModel {
|
||||
name: string
|
||||
}
|
||||
|
||||
function mountHarness() {
|
||||
const isOpen = ref(false)
|
||||
const label = ref('客户资料')
|
||||
const requiredPermission = ref<string | null>('customer:create')
|
||||
const model = reactive<HarnessModel>({ name: '' })
|
||||
let captureBaseline: () => void = () => undefined
|
||||
|
||||
const wrapper = mount(defineComponent({
|
||||
setup() {
|
||||
const registration = useUnsavedWorkRegistration({
|
||||
key: 'customer-form',
|
||||
label: computed(() => label.value),
|
||||
isOpen,
|
||||
model,
|
||||
requiredPermission: computed(() => requiredPermission.value),
|
||||
})
|
||||
captureBaseline = registration.captureBaseline
|
||||
return () => null
|
||||
},
|
||||
}))
|
||||
|
||||
return { wrapper, isOpen, label, requiredPermission, model, captureBaseline }
|
||||
}
|
||||
|
||||
describe('useUnsavedWorkRegistration', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
it('tracks open model changes against a captured baseline and removes closed work', async () => {
|
||||
const store = useUnsavedWorkStore()
|
||||
const harness = mountHarness()
|
||||
|
||||
harness.captureBaseline()
|
||||
expect(store.entries).toEqual([])
|
||||
|
||||
harness.isOpen.value = true
|
||||
await nextTick()
|
||||
expect(store.entries[0]?.dirty).toBe(false)
|
||||
|
||||
harness.model.name = '柏悦物流'
|
||||
await nextTick()
|
||||
expect(store.entries[0]?.dirty).toBe(true)
|
||||
|
||||
harness.isOpen.value = false
|
||||
await nextTick()
|
||||
expect(store.entries).toEqual([])
|
||||
|
||||
harness.wrapper.unmount()
|
||||
})
|
||||
|
||||
it('updates label and permission reactively and becomes clean after restoring the baseline', async () => {
|
||||
const store = useUnsavedWorkStore()
|
||||
const harness = mountHarness()
|
||||
harness.captureBaseline()
|
||||
harness.isOpen.value = true
|
||||
await nextTick()
|
||||
|
||||
harness.label.value = '新增客户'
|
||||
harness.requiredPermission.value = 'customer:update'
|
||||
harness.model.name = '柏悦物流'
|
||||
await nextTick()
|
||||
expect(store.entries[0]).toEqual({
|
||||
key: 'customer-form',
|
||||
label: '新增客户',
|
||||
dirty: true,
|
||||
requiredPermission: 'customer:update',
|
||||
})
|
||||
|
||||
harness.model.name = ''
|
||||
await nextTick()
|
||||
expect(store.entries[0]?.dirty).toBe(false)
|
||||
|
||||
harness.wrapper.unmount()
|
||||
})
|
||||
|
||||
it('removes registered work when its component scope is disposed', async () => {
|
||||
const store = useUnsavedWorkStore()
|
||||
const harness = mountHarness()
|
||||
harness.captureBaseline()
|
||||
harness.isOpen.value = true
|
||||
harness.model.name = '柏悦物流'
|
||||
await nextTick()
|
||||
expect(store.entries).toHaveLength(1)
|
||||
|
||||
harness.wrapper.unmount()
|
||||
|
||||
expect(store.entries).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import { computed, onScopeDispose, ref, watch, type ComputedRef, type Ref } from 'vue'
|
||||
import { useUnsavedWorkStore } from '@/core/unsaved-work/unsavedWork'
|
||||
|
||||
interface UnsavedWorkRegistrationOptions<T extends object> {
|
||||
key: string
|
||||
label: ComputedRef<string>
|
||||
isOpen: Ref<boolean>
|
||||
model: T
|
||||
requiredPermission: ComputedRef<string | null>
|
||||
}
|
||||
|
||||
export function useUnsavedWorkRegistration<T extends object>(
|
||||
options: UnsavedWorkRegistrationOptions<T>,
|
||||
) {
|
||||
const store = useUnsavedWorkStore()
|
||||
const snapshot = computed(() => JSON.stringify(options.model))
|
||||
const baseline = ref(snapshot.value)
|
||||
|
||||
function sync() {
|
||||
if (!options.isOpen.value) {
|
||||
store.remove(options.key)
|
||||
return
|
||||
}
|
||||
|
||||
store.upsert({
|
||||
key: options.key,
|
||||
label: options.label.value,
|
||||
dirty: snapshot.value !== baseline.value,
|
||||
requiredPermission: options.requiredPermission.value,
|
||||
})
|
||||
}
|
||||
|
||||
function captureBaseline() {
|
||||
baseline.value = snapshot.value
|
||||
sync()
|
||||
}
|
||||
|
||||
watch(
|
||||
[options.isOpen, snapshot, options.label, options.requiredPermission],
|
||||
sync,
|
||||
{ flush: 'sync' },
|
||||
)
|
||||
sync()
|
||||
onScopeDispose(() => store.remove(options.key))
|
||||
|
||||
return { captureBaseline }
|
||||
}
|
||||
Reference in New Issue
Block a user