Skip to content

Commit 39b3106

Browse files
alistair3149claude
andcommitted
Surface server violations on untouched single relations
RelationInput gated its field message and status on the field being touched (focused then blurred). For a single (non-multiple) relation, a server-sourced violation — such as a required error from the save-time 422 — stayed hidden until the user focused the field, and was also excluded from the dialog's anchorless banner because its property name matches a rendered field. An empty required relation could pass review invisibly. Server-sourced violations now surface regardless of touch; only the live client check still waits for blur. The creator is unaffected: its dry-run already strips required and label-required, so an untouched required relation produces no server violation to show. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5208600 commit 39b3106

2 files changed

Lines changed: 44 additions & 10 deletions

File tree

resources/ext.neowiki/src/components/Value/RelationInput.vue

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,18 +80,22 @@ function relevantServerViolations(): readonly SubjectViolation[] {
8080
return ( props.serverViolations ?? [] ).filter( ( v ) => v.propertyName === name );
8181
}
8282
83-
const fieldMessages = computed<ValidationMessages>( () => {
84-
if ( Object.keys( liveFieldMessages.value ).length > 0 ) {
85-
return liveFieldMessages.value;
86-
}
87-
// Fall back to server violations (field-level, since NeoMultiLookupInput has no per-index slot).
83+
// Field-level server violation (NeoMultiLookupInput has no per-index slot).
84+
function serverFieldMessages(): ValidationMessages {
8885
const hit = relevantServerViolations()[ 0 ];
8986
if ( hit ) {
9087
return {
9188
error: mw.message( `neowiki-field-${ hit.code }`, ...( hit.args as string[] ) ).text()
9289
};
9390
}
9491
return {};
92+
}
93+
94+
const fieldMessages = computed<ValidationMessages>( () => {
95+
if ( Object.keys( liveFieldMessages.value ).length > 0 ) {
96+
return liveFieldMessages.value;
97+
}
98+
return serverFieldMessages();
9599
} );
96100
97101
const displayedFieldMessages = computed( (): ValidationMessages => {
@@ -101,17 +105,16 @@ const displayedFieldMessages = computed( (): ValidationMessages => {
101105
if ( props.property.multiple || touched.value ) {
102106
return fieldMessages.value;
103107
}
104-
return {};
108+
// Server-sourced violations (dry-run / save-time 422) surface before the field
109+
// is touched; only the live client check waits for blur.
110+
return serverFieldMessages();
105111
} );
106112
107113
const fieldStatus = computed( (): 'error' | 'default' => {
108114
if ( props.property.multiple || singleHasUnmatchedText.value ) {
109115
return 'default';
110116
}
111-
if ( touched.value && fieldMessages.value.error ) {
112-
return 'error';
113-
}
114-
return 'default';
117+
return displayedFieldMessages.value.error !== undefined ? 'error' : 'default';
115118
} );
116119
117120
function emitClearIfServerViolationPresent(): void {

resources/ext.neowiki/tests/components/Value/RelationInput.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,37 @@ describe( 'RelationInput', () => {
166166
expect( field.props( 'messages' ) ).toEqual( {} );
167167
expect( field.props( 'status' ) ).toBe( 'default' );
168168
} );
169+
170+
it( 'surfaces a server violation on an untouched single relation', () => {
171+
const wrapper = newWrapper( {
172+
// Optional field so the live client check produces nothing; the message
173+
// can only originate from the server-sourced violation.
174+
property: newRelationProperty( { name: 'Owner', targetSchema: 'Company' } ),
175+
serverViolations: [
176+
{ propertyName: 'Owner', code: 'type-mismatch', args: [], valuePartIndex: null },
177+
],
178+
} );
179+
180+
const field = wrapper.findComponent( CdxField );
181+
expect( field.props( 'messages' ) ).toEqual( { error: 'neowiki-field-type-mismatch' } );
182+
expect( field.props( 'status' ) ).toBe( 'error' );
183+
} );
184+
185+
it( 'keeps a server violation suppressed while the lookup reports unmatched text', async () => {
186+
const wrapper = newWrapper( {
187+
property: newRelationProperty( { name: 'Owner', targetSchema: 'Company' } ),
188+
serverViolations: [
189+
{ propertyName: 'Owner', code: 'type-mismatch', args: [], valuePartIndex: null },
190+
],
191+
} );
192+
193+
wrapper.findComponent( SubjectLookup ).vm.$emit( 'blur', true );
194+
await wrapper.vm.$nextTick();
195+
196+
const field = wrapper.findComponent( CdxField );
197+
expect( field.props( 'messages' ) ).toEqual( {} );
198+
expect( field.props( 'status' ) ).toBe( 'default' );
199+
} );
169200
} );
170201

171202
describe( 'multiple mode', () => {

0 commit comments

Comments
 (0)