fix(12.2): close code-review blockers on uploads, JSON caps, and pivot fill

Keep form save behind in-flight uploads, make retries idempotent via X-Upload-Id, cap remaining JSON bodies, and surface pending pivot type errors instead of zeroing them.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-02 23:10:48 +02:00
parent 6bfc0faa8a
commit 516f9c9025
22 changed files with 533 additions and 96 deletions

View File

@@ -13,7 +13,7 @@ import { mapWinterUrl } from '../app/winterUrl'
import FormErrorBanner from '../components/form/FormErrorBanner.vue'
import FormGrid from '../components/form/FormGrid.vue'
import FormTabs from '../components/form/FormTabs.vue'
import { FORM_ASSETS, FORM_LOCALE, FORM_PATCH, FORM_SESSION, FORM_VALUES } from '../components/form/formContext'
import { FORM_ASSETS, FORM_LOCALE, FORM_PATCH, FORM_SESSION, FORM_VALUES, createUploadGate } from '../components/form/formContext'
import { RELATION_MANAGER, needsRecord } from '../components/form/registry'
import {
DEFAULT_TAB,
@@ -79,6 +79,7 @@ const sessionHeader = { [SESSION_HEADER]: sessionKey } as { 'X-Session-Key': str
const pendingChanges = ref(0)
// Bumped after each successful save, so file fields reload their lists.
const revision = ref(0)
const { activeUploads, beginUpload } = createUploadGate()
// A relation manager needs a saved record: on create it is dropped with its
// tab even when the YAML forgets `context: update` (D-05, design screen 5).
@@ -151,7 +152,9 @@ const subtitle = computed(() =>
const dirty = computed(
() =>
!loading.value &&
(pendingChanges.value > 0 || snapshot(editablePayload(fields.value, values.value)) !== saved.value),
(pendingChanges.value > 0 ||
activeUploads.value > 0 ||
snapshot(editablePayload(fields.value, values.value)) !== saved.value),
)
function adopt(record: RecordEnvelope | undefined): void {
@@ -211,6 +214,8 @@ provide(FORM_SESSION, {
},
pendingChanges: readonly(pendingChanges),
revision: readonly(revision),
beginUpload,
activeUploads: readonly(activeUploads),
})
/** 422: messages under fields, the first invalid field (schema order) focused. */
@@ -236,7 +241,7 @@ function redirectTarget(kind: 'redirect' | 'redirectClose', id: unknown): string
/** Saves the record; returns the saved envelope, or null after an error. */
async function save(): Promise<RecordEnvelope | null> {
if (busy.value || !schema.value) {
if (busy.value || !schema.value || activeUploads.value > 0) {
return null
}
busy.value = true
@@ -420,10 +425,10 @@ void load()
</Button>
<div class="ml-auto flex items-center gap-2.5">
<Button variant="ghost" data-action="cancel" :to="listPath">{{ t('backend::lang.form.cancel') }}</Button>
<Button variant="outline" data-action="save-close" :disabled="busy || loading || !schema" @click="onSaveAndClose">
<Button variant="outline" data-action="save-close" :disabled="busy || loading || !schema || activeUploads > 0" @click="onSaveAndClose">
{{ t('backend::lang.form.save_and_close') }}
</Button>
<Button variant="primary" data-action="save" :disabled="busy || loading || !schema" @click="onSave">
<Button variant="primary" data-action="save" :disabled="busy || loading || !schema || activeUploads > 0" @click="onSave">
{{ busy ? t('backend::lang.form.saving') : t('backend::lang.form.save') }}
</Button>
</div>