parent
52bf79d603
commit
7e965b4ff4
@ -0,0 +1,73 @@
|
||||
const CONTROL_TYPES = new Set(['text', 'select', 'radio'])
|
||||
const TARGET_TYPES = new Set(['text', 'textarea', 'date', 'datetime', 'select', 'radio'])
|
||||
|
||||
function normalizeValue(value) {
|
||||
if (value === null || value === undefined) return ''
|
||||
return String(value).trim()
|
||||
}
|
||||
|
||||
function isSimpleRadio(field) {
|
||||
if (field.type !== 'radio') return true
|
||||
return !String(field.stub || '').split(/\r?\n/).some((option) => /(?:\[[\s\S]*\]|\{[\s\S]*\})\s*$/.test(option))
|
||||
}
|
||||
|
||||
function isSupportedControl(field) {
|
||||
if (!field || !field.name || !CONTROL_TYPES.has(field.type)) return false
|
||||
if (['select', 'radio'].includes(field.type) && field.selection_model) return false
|
||||
if (field.type === 'select' && Number(field.multiple)) return false
|
||||
return isSimpleRadio(field)
|
||||
}
|
||||
|
||||
function isSupportedTarget(field) {
|
||||
return !!field && !!field.name && TARGET_TYPES.has(field.type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate persisted field linkage rules for a single main form.
|
||||
*
|
||||
* The function deliberately does not mutate form data or field definitions.
|
||||
* A bad or unsupported rule is ignored (fail open), while a valid unmatched
|
||||
* rule disables only fields that are writable at the current flow node.
|
||||
*/
|
||||
export function evaluateFieldLinkage({ fields, form, writeable }) {
|
||||
const list = Array.isArray(fields) ? fields : []
|
||||
const writableIds = new Set((Array.isArray(writeable) ? writeable : []).map((id) => Number(id)))
|
||||
const fieldsById = new Map(list.map((field) => [Number(field.id), field]))
|
||||
const disabledFieldIds = new Set()
|
||||
const disabledFieldNames = new Set()
|
||||
const states = {}
|
||||
|
||||
list.forEach((field) => {
|
||||
const rule = field && field.visibility_rule
|
||||
if (!rule) return
|
||||
|
||||
const state = { active: false, disabled: false, reason: 'invalid_rule' }
|
||||
states[field.id] = state
|
||||
if (!isSupportedTarget(field) || rule.mode !== 'show_when' || !Array.isArray(rule.values)) return
|
||||
|
||||
const values = rule.values.map(normalizeValue).filter(Boolean)
|
||||
const controlField = fieldsById.get(Number(rule.control_field_id))
|
||||
if (!values.length || !isSupportedControl(controlField)) return
|
||||
|
||||
if (!writableIds.has(Number(field.id)) || !writableIds.has(Number(controlField.id))) {
|
||||
state.reason = 'not_both_writable'
|
||||
return
|
||||
}
|
||||
|
||||
state.active = true
|
||||
const controlValue = normalizeValue((form || {})[controlField.name])
|
||||
if (values.includes(controlValue)) {
|
||||
state.reason = 'value_matched'
|
||||
return
|
||||
}
|
||||
|
||||
state.disabled = true
|
||||
state.reason = 'value_not_matched'
|
||||
disabledFieldIds.add(Number(field.id))
|
||||
disabledFieldNames.add(field.name)
|
||||
})
|
||||
|
||||
return { disabledFieldIds, disabledFieldNames, states }
|
||||
}
|
||||
|
||||
export default evaluateFieldLinkage
|
||||
@ -0,0 +1,45 @@
|
||||
import { evaluateFieldLinkage } from '@/utils/fieldLinkage'
|
||||
|
||||
const fields = [
|
||||
{ id: 1, name: 'need_invoice', type: 'select', multiple: 0 },
|
||||
{
|
||||
id: 2,
|
||||
name: 'invoice_address',
|
||||
type: 'text',
|
||||
visibility_rule: { mode: 'show_when', control_field_id: 1, values: ['是'] }
|
||||
}
|
||||
]
|
||||
|
||||
describe('evaluateFieldLinkage', () => {
|
||||
it('disables a writable controlled field when its control value does not match', () => {
|
||||
const result = evaluateFieldLinkage({ fields, form: { need_invoice: '否' }, writeable: [1, 2] })
|
||||
|
||||
expect(result.disabledFieldIds.has(2)).toBe(true)
|
||||
expect(result.disabledFieldNames.has('invoice_address')).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps the field enabled when the control value matches', () => {
|
||||
const result = evaluateFieldLinkage({ fields, form: { need_invoice: '是' }, writeable: [1, 2] })
|
||||
|
||||
expect(result.disabledFieldIds.has(2)).toBe(false)
|
||||
expect(result.states[2].reason).toBe('value_matched')
|
||||
})
|
||||
|
||||
it('does not activate the rule unless both fields are writable', () => {
|
||||
const result = evaluateFieldLinkage({ fields, form: { need_invoice: '否' }, writeable: [2] })
|
||||
|
||||
expect(result.disabledFieldIds.size).toBe(0)
|
||||
expect(result.states[2].reason).toBe('not_both_writable')
|
||||
})
|
||||
|
||||
it('fails open for unsupported multi-select controls', () => {
|
||||
const multiFields = [
|
||||
{ ...fields[0], multiple: 1 },
|
||||
fields[1]
|
||||
]
|
||||
const result = evaluateFieldLinkage({ fields: multiFields, form: { need_invoice: '否' }, writeable: [1, 2] })
|
||||
|
||||
expect(result.disabledFieldIds.size).toBe(0)
|
||||
expect(result.states[2].reason).toBe('invalid_rule')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,30 @@
|
||||
import { buildFlowLogReorderUnits, flattenFlowLogReorderUnits } from '@/utils/flowLogReorder'
|
||||
|
||||
describe('flowLogReorder', () => {
|
||||
const logs = [
|
||||
{ id: 4, jointly_sign_id: 4, status: 0, created_at: '2026-07-15 10:00:00', node: { category: 'process' } },
|
||||
{ id: 2, jointly_sign_id: 2, status: 1, created_at: '2026-07-15 08:10:00', node: { category: 'process' } },
|
||||
{ id: 1, jointly_sign_id: 1, status: 1, created_at: '2026-07-15 08:00:00', node: { category: 'start' } },
|
||||
{ id: 3, jointly_sign_id: 2, status: 1, created_at: '2026-07-15 08:11:00', node: { category: 'process' } },
|
||||
]
|
||||
|
||||
it('shows start first as locked, suggests handling last, and keeps jointly signed records together', () => {
|
||||
const units = buildFlowLogReorderUnits(logs)
|
||||
|
||||
expect(units).toHaveLength(3)
|
||||
expect(units[0].logs.map((log) => log.id)).toEqual([1])
|
||||
expect(units[0].locked).toBe(true)
|
||||
expect(units[0].lockReason).toBe('开始节点固定展示,不参与拖拽排序')
|
||||
expect(units[1].logs.map((log) => log.id)).toEqual([2, 3])
|
||||
expect(units[1].locked).toBe(false)
|
||||
expect(units[2].logs.map((log) => log.id)).toEqual([4])
|
||||
expect(units[2].locked).toBe(false)
|
||||
expect(flattenFlowLogReorderUnits(units)).toEqual([1, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('preserves persisted unit order when normalization is disabled', () => {
|
||||
const units = buildFlowLogReorderUnits(logs, false)
|
||||
|
||||
expect(flattenFlowLogReorderUnits(units)).toEqual([4, 2, 3, 1])
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,40 @@
|
||||
import FlowCreate from '@/views/flow/create.vue'
|
||||
import FlowDetail from '@/views/flow/detailCommon.vue'
|
||||
import FlowLogReorderDialog from '@/views/flow/components/FlowLogReorderDialog.vue'
|
||||
|
||||
describe('flow record reorder UI', () => {
|
||||
it('keeps sorting out of the record edit form and exposes the independent reorder flow', () => {
|
||||
const state = FlowCreate.data()
|
||||
|
||||
expect(state.editFlowLogForm.sort).toBeUndefined()
|
||||
expect(state.isShowReorderFlowLog).toBe(false)
|
||||
expect(typeof FlowCreate.methods.openReorderFlowLogDialog).toBe('function')
|
||||
expect(typeof FlowCreate.methods.submitReorderFlowLog).toBe('function')
|
||||
expect(typeof FlowCreate.methods.validateCurrentFlowLogOrder).toBe('function')
|
||||
})
|
||||
|
||||
it('exposes the same restricted reorder dialog on the flow detail page', () => {
|
||||
const state = FlowDetail.data()
|
||||
|
||||
expect(state.isShowReorderFlowLog).toBe(false)
|
||||
expect(FlowDetail.components.FlowLogReorderDialog).toBe(FlowLogReorderDialog)
|
||||
expect(typeof FlowLogReorderDialog.methods.validatePersistedOrder).toBe('function')
|
||||
})
|
||||
|
||||
it('allows warning-only reorder submissions without a required reason', async () => {
|
||||
const confirm = jest.fn(() => Promise.reject(new Error('stop after confirm')))
|
||||
const context = {
|
||||
validateOrder: jest.fn(() => Promise.resolve()),
|
||||
errorCount: 0,
|
||||
warningCount: 1,
|
||||
reason: '',
|
||||
changedCount: 1,
|
||||
$confirm: confirm,
|
||||
}
|
||||
|
||||
await FlowLogReorderDialog.methods.save.call(context)
|
||||
|
||||
expect(context.validateOrder).toHaveBeenCalled()
|
||||
expect(confirm).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Loading…
Reference in new issue