fix: return completed loading tasks to the task list
This commit is contained in:
@@ -922,8 +922,13 @@
|
||||
})
|
||||
},
|
||||
persistSession() {
|
||||
const identity = this.draftIdentity()
|
||||
const completed = this.remotePick?.taskStatus === 'COMPLETED'
|
||||
if (completed) this.completedBoxes = { ...this.completedBoxes, [this.currentBoxIndex]: true }
|
||||
this.saveCurrentBoxDraft()
|
||||
savePickingSession({ boxes: this.boxes, currentBoxIndex: this.currentBoxIndex, itemStates: this.itemStates, partialQuantities: this.partialQuantities, scannedBoxCodes: this.scannedBoxCodes, boxDrafts: this.boxDrafts, completedBoxes: this.completedBoxes, scanValue: this.scanValue, scanHistory: this.scanHistory }, this.draftIdentity())
|
||||
// 完成任务仍更新作业记录的内存快照,空身份阻止重新写入其持久化草稿。
|
||||
savePickingSession({ boxes: this.boxes, currentBoxIndex: this.currentBoxIndex, itemStates: this.itemStates, partialQuantities: this.partialQuantities, scannedBoxCodes: this.scannedBoxCodes, boxDrafts: this.boxDrafts, completedBoxes: this.completedBoxes, scanValue: this.scanValue, scanHistory: this.scanHistory }, completed ? {} : identity)
|
||||
if (completed) clearPickingDraft(identity)
|
||||
},
|
||||
leavePicking() { this.persistSession(); this.activeOverlay = ''; navigateBackOr('/pages/home/home') },
|
||||
clearTimers() { clearTimeout(this._toastTimer); clearTimeout(this._evidencePreviewOpenTimer) },
|
||||
@@ -1186,6 +1191,16 @@
|
||||
this.currentBox.boxNo = task.currentContainerNo
|
||||
this.currentBox.currentContainerNo = task.currentContainerNo
|
||||
const remoteTask = normalizePickingTask(task)
|
||||
// 刷新确认后台已完成时直接返回任务列表,不再停留在已完成柜子内。
|
||||
if (remoteTask.taskStatus === 'COMPLETED') {
|
||||
this.remotePick = remoteTask
|
||||
this.pickingVersion = remoteTask.pickingVersion ?? remoteTask.version
|
||||
if (this.activeOverlay) this.closeOverlay()
|
||||
this.boxDropdownOpen = false
|
||||
this.persistSession()
|
||||
navigateBackOr('/pages/tasks/tasks')
|
||||
return
|
||||
}
|
||||
this.assignmentVersion = remoteTask.assignment?.version ?? remoteTask.assignmentVersion ?? (remoteTask.workflowStatus === 'UNASSIGNED' ? 0 : null)
|
||||
if (remoteTask.workflowStatus !== 'AWAITING_REVIEW' && this.assignmentVersion === null) throw new Error('任务领取状态尚未同步,请刷新重试')
|
||||
let claimed = null
|
||||
@@ -1470,6 +1485,11 @@
|
||||
}
|
||||
this.remoteError = ''
|
||||
this.showToast('装柜已完成', 'success')
|
||||
// 收起页面内浮层,避免 onBackPress 拦截完成后的程序化返回。
|
||||
if (this.activeOverlay) this.closeOverlay()
|
||||
this.boxDropdownOpen = false
|
||||
this.persistSession()
|
||||
navigateBackOr('/pages/tasks/tasks')
|
||||
} catch (error) {
|
||||
if (!this.isEvidenceContextActive(contextId, identity)) return
|
||||
if (accepted) {
|
||||
|
||||
@@ -63,18 +63,19 @@ test('query failure is not retried as a completion mutation and completed accept
|
||||
readTask: async () => assert.fail('no query after authoritative completion'), isActive: () => true, onTask() {} }), completed)
|
||||
})
|
||||
|
||||
test('page only announces success without persisting a replay request after authoritative completion', async () => {
|
||||
test('page only returns to the task list after authoritative completion without persisting a replay request', async () => {
|
||||
const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
|
||||
const method = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)?.[0]
|
||||
assert.ok(method)
|
||||
for (const status of ['COMPLETED', 'COMPLETION_FAILED', 'COMPLETING', 'LOADING', 'QUERY_ERROR', 'NO_RESPONSE']) {
|
||||
const messages = [], cleared = [], saved = []
|
||||
const messages = [], cleared = [], saved = [], destinations = []
|
||||
const bindings = {
|
||||
COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion,
|
||||
isRemoteReady: () => true, restoreLoadingCompletionAttempt: () => null,
|
||||
stableTaskOperationIdentity: () => 'stable-task', stableOperationId: () => 'original-operation',
|
||||
saveLoadingCompletionAttempt: (_, attempt) => saved.push(attempt),
|
||||
clearLoadingCompletionAttempt: identity => cleared.push(identity),
|
||||
navigateBackOr: url => destinations.push(url),
|
||||
completeContainerPickingTask: async () => status === 'NO_RESPONSE' ? null : { taskStatus: 'COMPLETING', version: 2 },
|
||||
normalizePickingTask: task => task,
|
||||
observeLoadingCompletion: async options => {
|
||||
@@ -99,6 +100,7 @@ test('page only announces success without persisting a replay request after auth
|
||||
assert.equal(saved.length, 0)
|
||||
assert.equal(cleared.length, 0)
|
||||
assert.equal(messages.some(message => message.message === '装柜已完成'), status === 'COMPLETED')
|
||||
assert.deepEqual(destinations, status === 'COMPLETED' ? ['/pages/tasks/tasks'] : [])
|
||||
if (status !== 'COMPLETED') assert.ok(context.remoteError)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,12 +7,13 @@ const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.
|
||||
const method = page.match(/async completeLoadingTask\(\) \{[\s\S]*?\n\t\t\t\},/)[0]
|
||||
function completion(overrides = {}, savedAttempt = null) {
|
||||
let attempt = savedAttempt
|
||||
const calls = [], cleared = [], messages = []
|
||||
const calls = [], cleared = [], messages = [], destinations = []
|
||||
const bindings = {
|
||||
COMPLETION_PHASE, resolveCompletionPhase, shouldReconcileCompletion,
|
||||
isRemoteReady: () => true, stableTaskOperationIdentity: () => 'task', stableOperationId: (_action, _identity, payload) => `complete-v${payload.expectedTaskVersion}`,
|
||||
restoreLoadingCompletionAttempt: () => attempt, saveLoadingCompletionAttempt: (_identity, value) => { attempt = value },
|
||||
clearLoadingCompletionAttempt: identity => { cleared.push(identity); attempt = null },
|
||||
navigateBackOr: url => destinations.push(url),
|
||||
completeContainerPickingTask: async (_task, payload) => { calls.push(payload); return { taskStatus: 'COMPLETED' } },
|
||||
normalizePickingTask: task => task, observeLoadingCompletion: async options => options.initialTask,
|
||||
sanitizeUserMessage: value => value, ...overrides
|
||||
@@ -27,15 +28,16 @@ function completion(overrides = {}, savedAttempt = null) {
|
||||
remoteFailureMessage: error => error.message, showToast: message => messages.push(message)
|
||||
}
|
||||
state.complete = behavior.completeLoadingTask.bind(state)
|
||||
return { state, calls, cleared, messages, attempt: () => attempt }
|
||||
return { state, calls, cleared, messages, destinations, attempt: () => attempt }
|
||||
}
|
||||
|
||||
test('completion in flight blocks a second click and releases busy after the task context changes', async () => {
|
||||
let resolve
|
||||
const { state, calls } = completion({ completeContainerPickingTask: (...args) => { calls.push(args); return new Promise(done => { resolve = done }) } })
|
||||
const { state, calls, destinations } = completion({ completeContainerPickingTask: (...args) => { calls.push(args); return new Promise(done => { resolve = done }) } })
|
||||
const first = state.complete(); await state.complete(); assert.equal(calls.length, 1)
|
||||
state.evidenceContextId++; resolve({ taskStatus: 'COMPLETED' }); await first
|
||||
assert.equal(state.submitting, false); assert.equal(state.remotePick.taskStatus, undefined)
|
||||
assert.deepEqual(destinations, [])
|
||||
})
|
||||
|
||||
test('an old completion response cannot release a newer picking submission', async () => {
|
||||
|
||||
@@ -2,6 +2,8 @@ import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import vm from 'node:vm'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { navigateBackOr } from '../utils/navigation.js'
|
||||
import { COMPLETION_PHASE, observeLoadingCompletion, resolveCompletionPhase, shouldReconcileCompletion } from '../utils/loading-completion-state.js'
|
||||
|
||||
const source = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8')
|
||||
const script = source.match(/<script>([\s\S]*?)<\/script>/)[1]
|
||||
@@ -107,6 +109,82 @@ test('failed request releases submit guard and retry keeps operation identity',
|
||||
|
||||
const task = () => ({ taskId: 'task', recordStatus: 'ACTIVE', loadingPlanId: 'plan', containerBookingId: 'container',
|
||||
currentContainerNo: 'TEST', version: 1, workflowStatus: 'UNASSIGNED', items: [{ id: 'item', packageCount: 2, itemStatus: 'PENDING' }] })
|
||||
|
||||
test('refreshing a completed task returns to the list before claiming or loading evidence', async () => {
|
||||
const destinations = []; let claims = 0, evidence = 0
|
||||
const { ctx } = mount({ getContainerPickingTask: async () => ({ ...task(), taskStatus: 'COMPLETED', workflowStatus: 'COMPLETED' }),
|
||||
navigateBackOr: url => destinations.push(url),
|
||||
claimContainerPickingTask: async () => { claims++ } }, { loadLoadingEvidence: async () => { evidence++ } })
|
||||
await ctx.syncRemoteTask()
|
||||
assert.deepEqual(destinations, ['/pages/tasks/tasks'])
|
||||
assert.equal(ctx.remotePick.taskStatus, 'COMPLETED')
|
||||
assert.equal(claims, 0); assert.equal(evidence, 0); assert.equal(ctx.remoteLoading, false)
|
||||
})
|
||||
|
||||
for (const entry of ['completion', 'refresh']) {
|
||||
for (const control of ['dropdown', 'overlay']) {
|
||||
test(`${entry} returns through onBackPress even when the ${control} opens before completion`, async t => {
|
||||
const previousUni = globalThis.uni, previousPages = globalThis.getCurrentPages
|
||||
t.after(() => { globalThis.uni = previousUni; globalThis.getCurrentPages = previousPages })
|
||||
const response = deferred(), destinations = []; let popupClosed = 0
|
||||
const completed = { ...task(), taskStatus: 'COMPLETED', workflowStatus: 'COMPLETED' }
|
||||
const { page, ctx } = mount({ navigateBackOr, COMPLETION_PHASE, observeLoadingCompletion,
|
||||
resolveCompletionPhase, shouldReconcileCompletion,
|
||||
completeContainerPickingTask: () => response.promise, getContainerPickingTask: () => response.promise },
|
||||
{ evidenceIdentity: { taskId: 'task', warehouseId: 'warehouse' }, evidenceComplete: true,
|
||||
currentBoxIndex: 0, activeOverlay: '', boxDropdownOpen: false,
|
||||
requireEvidenceStage: () => true, isEvidenceContextActive: () => true,
|
||||
$nextTick: callback => callback(), $refs: { boxListPopup: { open() {}, close() { popupClosed++ } } } })
|
||||
ctx.openOverlay = page.methods.openOverlay.bind(ctx)
|
||||
globalThis.getCurrentPages = () => [{ route: 'pages/tasks/tasks' }, { route: 'pages/picking/picking' }]
|
||||
globalThis.uni = {
|
||||
// App 将返回钩子的拦截视为成功;仅调用导航函数不能证明页面已返回。
|
||||
navigateBack() { if (!page.onBackPress.call(ctx)) destinations.push('/pages/tasks/tasks') },
|
||||
reLaunch: ({ url }) => destinations.push(url)
|
||||
}
|
||||
const pending = entry === 'completion' ? ctx.completeLoadingTask() : ctx.syncRemoteTask()
|
||||
if (control === 'dropdown') ctx.toggleBoxDropdown()
|
||||
else ctx.openOverlay('boxList')
|
||||
response.resolve(completed)
|
||||
await pending
|
||||
assert.deepEqual(destinations, ['/pages/tasks/tasks'])
|
||||
assert.equal(ctx.boxDropdownOpen, false); assert.equal(ctx.activeOverlay, '')
|
||||
assert.equal(popupClosed, control === 'overlay' ? 1 : 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
test('completed tasks update the real history state while clearing only their own persisted draft', async t => {
|
||||
const previousUni = globalThis.uni
|
||||
t.after(() => { globalThis.uni = previousUni })
|
||||
const moduleUrl = value => `data:text/javascript;base64,${Buffer.from(value).toString('base64')}`
|
||||
let storeSource = await readFile(new URL('../stores/picking.js', import.meta.url), 'utf8')
|
||||
for (const path of ['data/demo-data.js', 'utils/picking-state.js', 'utils/task-identity.js']) {
|
||||
storeSource = storeSource.replace(`@/${path}`, moduleUrl(await readFile(new URL(`../${path}`, import.meta.url), 'utf8')))
|
||||
}
|
||||
const store = await import(moduleUrl(storeSource)), storage = new Map(), writes = []
|
||||
globalThis.uni = { getStorageSync: key => storage.get(key),
|
||||
setStorageSync: (key, value) => { writes.push(key); storage.set(key, value) }, removeStorageSync: key => storage.delete(key) }
|
||||
const identity = { operatorId: 'operator', taskId: 'task', loadingPlanId: 'plan', containerBookingId: 'container' }
|
||||
const { page, ctx } = mount({ clearPickingDraft: store.clearPickingDraft, savePickingSession: store.savePickingSession },
|
||||
{ remotePick: { taskStatus: 'LOADING' }, draftIdentity: () => identity, currentBoxIndex: 0,
|
||||
boxes: [{ taskId: 'task', cabinetNo: 'D2126', items: [] }], boxDrafts: {}, scanHistory: [] })
|
||||
ctx.persistSession = page.methods.persistSession.bind(ctx)
|
||||
ctx.persistSession()
|
||||
const draftKey = store.getPickingDraftKey(identity)
|
||||
assert.equal(storage.has(draftKey), true); assert.equal(store.getPickingOverview().completedCount, 0)
|
||||
storage.set('another-task-draft', { keep: true })
|
||||
writes.length = 0
|
||||
ctx.remotePick = { taskStatus: 'COMPLETED' }
|
||||
ctx.persistSession()
|
||||
page.onUnload.call(ctx)
|
||||
assert.equal(store.getPickingOverview().completedCount, 1)
|
||||
assert.equal(store.getPickingOverview().completedBoxes[0], true)
|
||||
assert.equal(store.getPickingOverview().boxes[0].taskId, 'task')
|
||||
assert.equal(storage.has(draftKey), false); assert.equal(storage.has('another-task-draft'), true)
|
||||
assert.deepEqual(writes, [])
|
||||
})
|
||||
|
||||
for (const hook of ['onHide', 'onUnload']) {
|
||||
test(`${hook} while detail is pending prevents claim and evidence requests`, async () => {
|
||||
const detail = deferred(); let claims = 0, evidence = 0
|
||||
|
||||
Reference in New Issue
Block a user