diff --git a/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.spec.tsx b/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.spec.tsx index d5ea049..bda4644 100644 --- a/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.spec.tsx +++ b/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.spec.tsx @@ -46,7 +46,6 @@ const mocks = vi.hoisted(() => { }; }); -/** Creates a fluent no-op validation rule for schema-only unit tests. */ function createRule(): any { const rule: any = {}; for (const method of ['max', 'min', 'optional', 'or', 'trim']) { @@ -60,7 +59,6 @@ vi.mock('#/adapter/form', () => ({ mocks.formOptions = options; const Form = defineComponent({ name: 'MockForm', - /** Renders a stable form marker without owning form state. */ setup() { return () => h('form', { 'data-testid': 'subscription-form' }); }, @@ -78,7 +76,6 @@ vi.mock('@vben/common-ui', () => ({ mocks.modalOptions = options; const Modal = defineComponent({ name: 'MockModal', - /** Renders modal content while the mock API controls lifecycle calls. */ setup(_, { slots }) { return () => h('section', slots.default?.()); }, @@ -92,7 +89,6 @@ vi.mock('#/api/qqbot/message-push', () => ({ updateMessageSubscription: mocks.update, })); -/** Creates the immutable source catalog fixture consumed by the modal. */ function createSources(): QqbotMessagePushApi.SystemMessageSourceDefinition[] { return [ { @@ -106,7 +102,6 @@ function createSources(): QqbotMessagePushApi.SystemMessageSourceDefinition[] { ]; } -/** Creates string-ID STUN choices including disabled API-owned reasons. */ function createStunOptions(): QqbotMessagePushApi.StunMappingPortChangedOptionsResponse { return { ddnsRecords: [ @@ -158,7 +153,6 @@ function createStunOptions(): QqbotMessagePushApi.StunMappingPortChangedOptionsR }; } -/** Creates one editable row with only the global subscription contract. */ function createRow(): QqbotMessagePushApi.MessageSubscriptionView { return { createTime: '2026-07-24 10:00:00', @@ -179,7 +173,14 @@ function createRow(): QqbotMessagePushApi.MessageSubscriptionView { }; } -/** Mounts the modal with page-owned source and STUN metadata. */ +function createDeferred() { + let resolve!: (value: T) => void; + const promise = new Promise((done) => { + resolve = done; + }); + return { promise, resolve }; +} + function mountModal() { return mount(MessageSubscriptionModal, { props: { @@ -265,6 +266,151 @@ describe('message subscription modal', () => { expect(setOrder).toBeLessThan(validateOrder); }); + it('discards an old create confirmation before a new edit session submits its own row', async () => { + const validation = createDeferred<{ valid: boolean }>(); + mocks.formApi.validate.mockReturnValueOnce(validation.promise); + const wrapper = mountModal(); + (wrapper.vm as any).openCreate(); + await flushPromises(); + + const staleConfirmation = mocks.modalOptions.onConfirm(); + await flushPromises(); + const editRow = { + ...createRow(), + enabled: false, + id: '10000000000000002', + name: '新编辑订阅', + remark: '新编辑备注', + }; + (wrapper.vm as any).openEdit(editRow); + await flushPromises(); + const editValues = { + ddnsRecordId: '2041700000000000002', + enabled: false, + name: ' 新编辑订阅 ', + portForwardId: '2041700000000000001', + remark: ' 新编辑备注 ', + sourceKey: 'network.stun.mapping-port-changed', + }; + mocks.formApi.getValues.mockResolvedValue(editValues); + const closeCountBeforeResume = mocks.modalApi.close.mock.calls.length; + + validation.resolve({ valid: true }); + await staleConfirmation; + await flushPromises(); + + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.update).not.toHaveBeenCalled(); + expect(mocks.modalApi.lock).not.toHaveBeenCalled(); + expect(mocks.modalApi.unlock).not.toHaveBeenCalled(); + expect(mocks.modalApi.close).toHaveBeenCalledTimes(closeCountBeforeResume); + expect(wrapper.emitted('saved')).toBeUndefined(); + expect(mocks.formApi.setValues).toHaveBeenLastCalledWith({ + ddnsRecordId: '2041700000000000002', + enabled: false, + name: '新编辑订阅', + portForwardId: '2041700000000000001', + remark: '新编辑备注', + sourceKey: 'network.stun.mapping-port-changed', + }); + + await mocks.modalOptions.onConfirm(); + + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.update).toHaveBeenCalledOnce(); + expect(mocks.update).toHaveBeenCalledWith('10000000000000002', { + enabled: false, + name: '新编辑订阅', + remark: '新编辑备注', + sourceConfig: { + ddnsRecordId: '2041700000000000002', + portForwardId: '2041700000000000001', + }, + sourceKey: 'network.stun.mapping-port-changed', + }); + expect(mocks.modalApi.close).toHaveBeenCalledTimes( + closeCountBeforeResume + 1, + ); + expect(wrapper.emitted('saved')).toHaveLength(1); + }); + + it('discards an old edit confirmation before a new create session submits its own payload', async () => { + const values = createDeferred<{ + ddnsRecordId: string; + enabled: boolean; + name: string; + portForwardId: string; + remark: string; + sourceKey: string; + }>(); + mocks.formApi.getValues.mockReturnValueOnce(values.promise); + const wrapper = mountModal(); + (wrapper.vm as any).openEdit(createRow()); + await flushPromises(); + + const staleConfirmation = mocks.modalOptions.onConfirm(); + await flushPromises(); + expect(mocks.formApi.getValues).toHaveBeenCalledOnce(); + + (wrapper.vm as any).openCreate(); + await flushPromises(); + const createValues = { + ddnsRecordId: '2041700000000000002', + enabled: true, + name: ' 新创建订阅 ', + portForwardId: '2041700000000000001', + remark: ' 新创建备注 ', + sourceKey: 'network.stun.mapping-port-changed', + }; + mocks.formApi.getValues.mockResolvedValue(createValues); + const closeCountBeforeResume = mocks.modalApi.close.mock.calls.length; + + values.resolve({ + ddnsRecordId: '2041700000000002', + enabled: false, + name: '旧编辑订阅', + portForwardId: '2041700000000001', + remark: '旧编辑备注', + sourceKey: 'network.stun.mapping-port-changed', + }); + await staleConfirmation; + await flushPromises(); + + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.update).not.toHaveBeenCalled(); + expect(mocks.modalApi.lock).not.toHaveBeenCalled(); + expect(mocks.modalApi.unlock).not.toHaveBeenCalled(); + expect(mocks.modalApi.close).toHaveBeenCalledTimes(closeCountBeforeResume); + expect(wrapper.emitted('saved')).toBeUndefined(); + expect(mocks.formApi.setValues).toHaveBeenLastCalledWith({ + ddnsRecordId: undefined, + enabled: true, + name: '', + portForwardId: undefined, + remark: '', + sourceKey: 'network.stun.mapping-port-changed', + }); + + await mocks.modalOptions.onConfirm(); + + expect(mocks.update).not.toHaveBeenCalled(); + expect(mocks.create).toHaveBeenCalledOnce(); + expect(mocks.create).toHaveBeenCalledWith({ + enabled: true, + name: '新创建订阅', + remark: '新创建备注', + sourceConfig: { + ddnsRecordId: '2041700000000000002', + portForwardId: '2041700000000000001', + }, + sourceKey: 'network.stun.mapping-port-changed', + }); + expect(mocks.modalApi.close).toHaveBeenCalledTimes( + closeCountBeforeResume + 1, + ); + expect(wrapper.emitted('saved')).toHaveLength(1); + }); + it('preserves string IDs and exposes disabled reasons on matching options', async () => { const wrapper = mountModal(); (wrapper.vm as any).openEdit(createRow()); diff --git a/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.tsx b/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.tsx index 6d7d757..dcc7e11 100644 --- a/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.tsx +++ b/apps/web-antdv-next/src/views/qqbot/message-subscription/components/MessageSubscriptionModal.tsx @@ -48,21 +48,14 @@ export default defineComponent({ }, }, emits: ['saved'], - /** - * Owns one source-only create/edit form without fetching page metadata or lists. - */ setup(props, { emit, expose }) { const editingRow = ref(); const selectedPortForwardId = ref(); + let sessionRevision = 0; const [SubscriptionForm, formApi] = useVbenForm({ commonConfig: { labelClass: 'w-24', }, - /** - * Keeps the DDNS choice compatible with the selected port-forward option. - * @param values - Current form values after the change. - * @param fieldsChanged - Field names changed by this form event. - */ async handleValuesChange(values, fieldsChanged) { if (!fieldsChanged.includes('portForwardId')) return; selectedPortForwardId.value = @@ -95,10 +88,6 @@ export default defineComponent({ const [Modal, modalApi] = useVbenModal({ class: 'w-[680px]', fullscreenButton: false, - /** - * Persists the current session while containing failures at ModalApi's - * non-awaited callback boundary. - */ async onConfirm() { try { await submit(); @@ -106,10 +95,6 @@ export default defineComponent({ // The request/form layer already presents the persistence error. } }, - /** - * Restores session values after destroy-on-close modal content has mounted. - * @param isOpen - Whether the modal content is visible. - */ async onOpenChange(isOpen: boolean) { if (!isOpen) return; const { values } = modalApi.getData(); @@ -118,8 +103,8 @@ export default defineComponent({ }, }); - /** Opens a fresh subscription form with no values retained from an edit. */ function openCreate() { + sessionRevision += 1; editingRow.value = undefined; modalApi .setData({ @@ -135,11 +120,8 @@ export default defineComponent({ .open(); } - /** - * Opens an edit session with only the six user-editable values. - * @param row - Subscription row selected from the page-owned KtTable. - */ function openEdit(row: QqbotMessagePushApi.MessageSubscriptionView) { + sessionRevision += 1; editingRow.value = row; modalApi .setData({ @@ -155,21 +137,20 @@ export default defineComponent({ .open(); } - /** - * Resets the mounted form before installing one isolated modal session. - * @param values - Exact editable values for the current create/edit session. - */ async function resetForm(values: MessageSubscriptionFormValues) { await formApi.resetForm(); await formApi.setValues(values); await formApi.resetValidate(); } - /** Persists the exact source-only payload and closes only after success. */ async function submit() { + const revision = sessionRevision; + const editingId = editingRow.value?.id; const { valid } = await formApi.validate(); + if (revision !== sessionRevision) return; if (!valid) return; const values = await formApi.getValues(); + if (revision !== sessionRevision) return; const payload: QqbotMessagePushApi.MessageSubscriptionInput = { enabled: !!values.enabled, name: values.name.trim(), @@ -180,12 +161,14 @@ export default defineComponent({ }, sourceKey: values.sourceKey, }; + if (revision !== sessionRevision) return; modalApi.lock(); try { - await (editingRow.value - ? updateMessageSubscription(editingRow.value.id, payload) + await (editingId + ? updateMessageSubscription(editingId, payload) : createMessageSubscription(payload)); + if (revision !== sessionRevision) return; await modalApi.close(); emit('saved'); } finally { @@ -203,12 +186,6 @@ export default defineComponent({ }, }); -/** - * Builds the locked six-field schema from page-owned metadata. - * @param props - Source catalog and STUN choices loaded once by the page. - * @param selectedPortForwardId - Current port used to filter matching DDNS rows. - * @returns Vben form fields in the approved source-only order. - */ function createFormSchema( props: Readonly<{ sources: QqbotMessagePushApi.SystemMessageSourceDefinition[]; @@ -280,11 +257,6 @@ function createFormSchema( ]; } -/** - * Formats one server-evaluated port choice without coercing its string ID. - * @param option - Port-forward option returned by the source options API. - * @returns Select option preserving eligibility and stable reason code. - */ function formatPortForwardOption( option: QqbotMessagePushApi.StunMappingPortChangedOptionsResponse['portForwards'][number], ) { @@ -298,11 +270,6 @@ function formatPortForwardOption( }; } -/** - * Formats one server-evaluated DDNS choice without coercing its string ID. - * @param option - DDNS option already filtered to the selected port. - * @returns Select option preserving eligibility and stable reason code. - */ function formatDdnsOption( option: QqbotMessagePushApi.StunMappingPortChangedOptionsResponse['ddnsRecords'][number], ) { diff --git a/apps/web-antdv-next/src/views/qqbot/message-template/components/MessageTemplateModal.spec.tsx b/apps/web-antdv-next/src/views/qqbot/message-template/components/MessageTemplateModal.spec.tsx index 2c41f6d..12e5754 100644 --- a/apps/web-antdv-next/src/views/qqbot/message-template/components/MessageTemplateModal.spec.tsx +++ b/apps/web-antdv-next/src/views/qqbot/message-template/components/MessageTemplateModal.spec.tsx @@ -49,7 +49,6 @@ const mocks = vi.hoisted(() => { }; }); -/** Creates a fluent no-op validation rule for schema behavior tests. */ function createRule(): any { const rule: any = {}; for (const method of ['max', 'min', 'optional', 'or', 'trim']) { @@ -63,7 +62,6 @@ vi.mock('#/adapter/form', () => ({ mocks.formOptions = options; const Form = defineComponent({ name: 'MockTemplateForm', - /** Renders the production schema's local content component with live props. */ setup() { return () => { const content = mocks.formOptions.schema.find( @@ -98,7 +96,6 @@ vi.mock('@vben/common-ui', () => ({ mocks.modalOptions = options; const Modal = defineComponent({ name: 'MockTemplateModal', - /** Renders the actual form plus the explicit prepend-footer action slot. */ setup(_, { slots }) { return () => h('section', { 'data-testid': 'template-modal' }, [ @@ -118,7 +115,6 @@ vi.mock('#/api/qqbot/message-push', () => ({ updateMessageTemplate: mocks.update, })); -/** Creates one source with a distinguishable variable catalog. */ function createSource( sourceKey = 'source-a', variableKey = 'endpoint', @@ -141,7 +137,6 @@ function createSource( }; } -/** Creates one unsafe-integer string-ID editable template row. */ function createRow(): QqbotMessagePushApi.MessageTemplateView { return { content: 'old [CQ:at,qq=12345]', @@ -157,7 +152,14 @@ function createRow(): QqbotMessagePushApi.MessageTemplateView { }; } -/** Mounts the modal with page-owned source labels. */ +function createDeferred() { + let resolve!: (value: T) => void; + const promise = new Promise((done) => { + resolve = done; + }); + return { promise, resolve }; +} + function mountModal(canPreview = true) { return mount(MessageTemplateModal, { props: { @@ -260,6 +262,137 @@ describe('message template modal', () => { expect(setOrder).toBeLessThan(validateOrder); }); + it('discards an old create confirmation before a new edit session submits its own row', async () => { + const validation = createDeferred<{ valid: boolean }>(); + mocks.formApi.validate.mockReturnValueOnce(validation.promise); + const wrapper = mountModal(); + (wrapper.vm as any).openCreate(); + await flushPromises(); + Object.assign(mocks.formValues, { + content: 'old create', + enabled: true, + name: 'old create', + remark: '', + sourceKey: 'source-a', + }); + + const staleConfirmation = mocks.modalOptions.onConfirm(); + await flushPromises(); + const editRow = { + ...createRow(), + content: 'new edit ${{port}}', + id: '10000000000000002', + name: ' new edit template ', + remark: ' new edit note ', + }; + (wrapper.vm as any).openEdit(editRow); + await flushPromises(); + const closeCountBeforeResume = mocks.modalApi.close.mock.calls.length; + + validation.resolve({ valid: true }); + await staleConfirmation; + await flushPromises(); + + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.update).not.toHaveBeenCalled(); + expect(mocks.modalApi.lock).not.toHaveBeenCalled(); + expect(mocks.modalApi.unlock).not.toHaveBeenCalled(); + expect(mocks.modalApi.close).toHaveBeenCalledTimes(closeCountBeforeResume); + expect(wrapper.emitted('saved')).toBeUndefined(); + expect(mocks.formValues).toEqual({ + content: 'new edit ${{port}}', + enabled: false, + name: ' new edit template ', + remark: ' new edit note ', + sourceKey: 'source-b', + }); + + await mocks.modalOptions.onConfirm(); + + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.update).toHaveBeenCalledOnce(); + expect(mocks.update).toHaveBeenCalledWith('10000000000000002', { + content: 'new edit ${{port}}', + enabled: false, + name: 'new edit template', + remark: 'new edit note', + sourceKey: 'source-b', + }); + expect(mocks.modalApi.close).toHaveBeenCalledTimes( + closeCountBeforeResume + 1, + ); + expect(wrapper.emitted('saved')).toHaveLength(1); + }); + + it('discards an old edit confirmation before a new create session submits its own payload', async () => { + const values = createDeferred<{ + content: string; + enabled: boolean; + name: string; + remark: string; + sourceKey: string; + }>(); + mocks.formApi.getValues.mockReturnValueOnce(values.promise); + const wrapper = mountModal(); + (wrapper.vm as any).openEdit(createRow()); + await flushPromises(); + + const staleConfirmation = mocks.modalOptions.onConfirm(); + await flushPromises(); + expect(mocks.formApi.getValues).toHaveBeenCalledOnce(); + + (wrapper.vm as any).openCreate(); + await flushPromises(); + Object.assign(mocks.formValues, { + content: 'new create ${{endpoint}}', + enabled: true, + name: ' new create template ', + remark: ' new create note ', + sourceKey: 'source-a', + }); + const closeCountBeforeResume = mocks.modalApi.close.mock.calls.length; + + values.resolve({ + content: 'old edit [CQ:at,qq=12345]', + enabled: false, + name: 'old edit template', + remark: 'old edit note', + sourceKey: 'source-b', + }); + await staleConfirmation; + await flushPromises(); + + expect(mocks.create).not.toHaveBeenCalled(); + expect(mocks.update).not.toHaveBeenCalled(); + expect(mocks.modalApi.lock).not.toHaveBeenCalled(); + expect(mocks.modalApi.unlock).not.toHaveBeenCalled(); + expect(mocks.modalApi.close).toHaveBeenCalledTimes(closeCountBeforeResume); + expect(wrapper.emitted('saved')).toBeUndefined(); + expect(mocks.formValues).toEqual({ + content: 'new create ${{endpoint}}', + enabled: true, + name: ' new create template ', + remark: ' new create note ', + sourceKey: 'source-a', + }); + + await mocks.modalOptions.onConfirm(); + + expect(mocks.update).not.toHaveBeenCalled(); + expect(mocks.create).toHaveBeenCalledOnce(); + expect(mocks.create).toHaveBeenCalledWith({ + content: 'new create ${{endpoint}}', + enabled: true, + name: 'new create template', + remark: 'new create note', + sourceKey: 'source-a', + }); + expect(mocks.modalApi.close).toHaveBeenCalledTimes( + closeCountBeforeResume + 1, + ); + expect(wrapper.emitted('saved')).toHaveLength(1); + }); + it('loads exact details and prevents a late source from overwriting the latest', async () => { const deferred = new Map< string, diff --git a/apps/web-antdv-next/src/views/qqbot/message-template/components/MessageTemplateModal.tsx b/apps/web-antdv-next/src/views/qqbot/message-template/components/MessageTemplateModal.tsx index 5d9bb26..2df318b 100644 --- a/apps/web-antdv-next/src/views/qqbot/message-template/components/MessageTemplateModal.tsx +++ b/apps/web-antdv-next/src/views/qqbot/message-template/components/MessageTemplateModal.tsx @@ -53,7 +53,6 @@ export default defineComponent({ }, }, emits: ['saved'], - /** Owns one isolated template edit session, source detail cache, and preview. */ setup(props, { emit, expose }) { const editingRow = ref(); const variables = ref< @@ -69,15 +68,11 @@ export default defineComponent({ >(); let sourceRevision = 0; let previewRevision = 0; + let sessionRevision = 0; const [TemplateForm, formApi] = useVbenForm({ commonConfig: { labelClass: 'w-24', }, - /** - * Clears stale preview state and refreshes variables only for source changes. - * @param values - Current form values after the update. - * @param fieldsChanged - Exact fields changed by this form event. - */ async handleValuesChange(values, fieldsChanged) { if (fieldsChanged.includes('content')) clearPreview(); if (!fieldsChanged.includes('sourceKey')) return; @@ -96,17 +91,12 @@ export default defineComponent({ showDefaultActions: false, wrapperClass: 'grid-cols-1', }); - /** Tracks the title from isolated create/edit identity only. */ const modalTitle = computed(() => editingRow.value ? '编辑消息模板' : '新建消息模板', ); const [Modal, modalApi] = useVbenModal({ class: 'w-[760px]', fullscreenButton: false, - /** - * Persists the exact payload while containing failures at ModalApi's - * non-awaited callback boundary. - */ async onConfirm() { try { await submit(); @@ -114,10 +104,6 @@ export default defineComponent({ // The request/form layer already presents the persistence error. } }, - /** - * Resets every mounted form/session state before loading authoritative detail. - * @param isOpen - Whether destroy-on-close modal content is mounted. - */ async onOpenChange(isOpen: boolean) { if (!isOpen) return; const { values } = modalApi.getData(); @@ -128,8 +114,8 @@ export default defineComponent({ }, }); - /** Opens a fresh create session without values from the preceding edit. */ function openCreate() { + sessionRevision += 1; editingRow.value = undefined; modalApi .setData({ @@ -144,11 +130,8 @@ export default defineComponent({ .open(); } - /** - * Opens an edit session with only the five user-editable values. - * @param row - Template row selected from the page-owned KtTable. - */ function openEdit(row: QqbotMessagePushApi.MessageTemplateView) { + sessionRevision += 1; editingRow.value = row; modalApi .setData({ @@ -163,17 +146,12 @@ export default defineComponent({ .open(); } - /** - * Resets and installs the current session values in the required order. - * @param values - Exact editable values stored before the modal opened. - */ async function resetForm(values: MessageTemplateFormValues) { await formApi.resetForm(); await formApi.setValues(values); await formApi.resetValidate(); } - /** Invalidates stale source/preview work and clears every transient display. */ function invalidateSession() { sourceRevision += 1; detailLoading.value = false; @@ -181,18 +159,12 @@ export default defineComponent({ clearPreview(); } - /** Clears authoritative preview data and invalidates any in-flight preview. */ function clearPreview() { previewRevision += 1; preview.value = undefined; previewLoading.value = false; } - /** - * Reuses an exact-key in-flight/resolved source detail promise. - * @param sourceKey - Stable exact source identity selected in the form. - * @returns Authoritative source detail promise, evicted when rejected. - */ function getCachedSourceDetail(sourceKey: string) { const cached = detailCache.get(sourceKey); if (cached) return cached; @@ -206,11 +178,6 @@ export default defineComponent({ return request; } - /** - * Replaces suggestions only when this exact source request remains latest. - * Expected detail failures stay inside the non-awaited form/modal callbacks. - * @param sourceKey - Newly selected source identity. - */ async function loadSourceDetail(sourceKey: string) { const revision = ++sourceRevision; clearPreview(); @@ -241,7 +208,6 @@ export default defineComponent({ } } - /** Requests server-authoritative preview only after an explicit valid click. */ async function handlePreview() { if (!props.canPreview) return; const [sourceValidation, contentValidation] = await Promise.all([ @@ -265,11 +231,14 @@ export default defineComponent({ } } - /** Persists content byte-for-byte and closes/emits only after success. */ async function submit() { + const revision = sessionRevision; + const editingId = editingRow.value?.id; const { valid } = await formApi.validate(); + if (revision !== sessionRevision) return; if (!valid) return; const values = await formApi.getValues(); + if (revision !== sessionRevision) return; const payload: QqbotMessagePushApi.MessageTemplateInput = { content: values.content, enabled: !!values.enabled, @@ -277,11 +246,13 @@ export default defineComponent({ remark: values.remark?.trim() || '', sourceKey: values.sourceKey, }; + if (revision !== sessionRevision) return; modalApi.lock(); try { - await (editingRow.value - ? updateMessageTemplate(editingRow.value.id, payload) + await (editingId + ? updateMessageTemplate(editingId, payload) : createMessageTemplate(payload)); + if (revision !== sessionRevision) return; await modalApi.close(); emit('saved'); } finally { @@ -289,7 +260,6 @@ export default defineComponent({ } } - /** Renders the permission-gated explicit preview action. */ function renderPreviewAction() { return props.canPreview ? ( @@ -298,7 +268,6 @@ export default defineComponent({ ) : null; } - /** Renders server-returned preview text and variables as escaped text nodes. */ function renderPreview() { const result = preview.value; if (!result) return null; @@ -331,14 +300,6 @@ export default defineComponent({ }, }); -/** - * Builds the exact template schema with a local controlled Mentions component. - * @param props - Page-owned source labels and preview permission. - * @param variables - Variables from the latest exact source detail. - * @param loading - Source-detail loading state forwarded to Mentions. - * @param selectedSourceKey - Current source identity used to disable no-source input. - * @returns Five fields in the locked source/name/content/enabled/remark order. - */ function createFormSchema( props: Readonly<{ canPreview: boolean;