diff --git a/CHANGELOG.md b/CHANGELOG.md index e51ea1f31..6cc36bd50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ version, see the upgrade guides in `docs/`. ### Behaviour changes - **router:** Requests for paths that no method handles respond with 404, where they responded with 405 and an empty `Allow` header, or with 501 and an `Allow` header listing every method with routes. 405 and 501 remain for paths that other methods handle, with these methods in `Allow`. OPTIONS requests for such paths respond with 404 instead of 200 with an empty `Allow` header (#328). -- **router:** HEAD requests, e.g. of link checkers and health probes, are handled by the GET route of their path when no HEAD route matches, where they responded with 501, and `Allow` lists HEAD wherever it lists GET. Misses of HEAD requests respond with 404 like those of GET requests (#329). +- **router:** HEAD requests, e.g. of link checkers and health probes, are handled by the GET route of their path when no HEAD route matches, where they responded with 501, and `Allow` lists HEAD wherever it lists GET, also in `getAllowedMethods()`. Misses of HEAD requests respond with 404 like those of GET requests. GET handlers therefore also run for HEAD requests, so handlers with side effects, e.g. ones that log downloads, need to check `ctx.method` (#329). - **router:** `add()`, `find()` and the `exclude` argument of `getAllowedMethods()` take methods in any case, e.g. `add('get', …)` matches `find('GET', …)`. This only affects code that calls the router directly with lowercase methods, as the server always passes them uppercase (#335). - **admin:** The login and logout labels read "Sign In" and "Sign Out", in the header, the login dialog and the account menu, where they read "Login" and "Logout". Tests that find these buttons by their names need the new ones (#344). - **admin:** Dialogs focus their submit button, also in confirmations, unless a component sets `autofocus`; pressing Enter submits. They focused their first focusable element, e.g. Cancel in confirmations. @@ -18,8 +18,8 @@ version, see the upgrade guides in `docs/`. - **utils:** `debounceAsync()` resolves calls made while a run is pending with the result of their own run, where they received the pending run's result, and `cancel()` only settles calls that wait for the timer, where it also resolved those of a pending run, whose rejection then threw an unhandled `TypeError`. Debounced `search` filters of the admin now get the options of the search term they were called with (#347, #349). - **utils:** `equals()` never treats values of different types as equal, e.g. a buffer and an object with the same keys, or a `Uint8Array` and an array, where it compared all objects by their keys. Typed arrays are compared element by element, like arrays (#373). - **utils:** `*` in `getValueAtDataPath()` only merges the values it finds into one array when the rest of the path has wildcards too, so values that are arrays themselves are kept, e.g. `'*/x'` returns `[[1, 2], [3]]` where it returned `[1, 2, 3]`, like `**` and `getEntriesAtDataPath()` do. This affects app code that reads array values through `*`, e.g. with `dataPath` in schemas (#357). -- **utils:** `normalizeDataPath()` throws for a `..` that leads above the root, e.g. `'../items'`, where it dropped it silently, and resolves a `.` or `..` right after a `//`, where it kept it. `resolveDataPath()` resolves through `normalizeDataPath()`, so its error names the joined path, `Invalid data path: 'items/0/../../..' leads above the root.`, where it named both paths. This affects app code that passes such paths to `normalizeDataPath()`, and tests that check the error of `resolveDataPath()` or of the schema context's `validate()`, `verify()` and `clearErrors()` (#358). -- **utils:** `capitalize()` doesn't start new words after digits, e.g. `capitalize('the 1st place')` returns `'The 1st Place'`, where it returned `'The 1St Place'` (#356). +- **utils:** `normalizeDataPath()` throws for a `..` that leads above the root, e.g. `'../items'`, where it dropped it silently, and resolves a `.` right after a `//`, where it kept it, and throws for a `..` there, as `//` starts again at the root. `resolveDataPath()` resolves through `normalizeDataPath()`, so its error names the joined path, `Invalid data path: 'items/0/../../..' leads above the root.`, where it named both paths. This affects app code that passes such paths to `normalizeDataPath()`, and tests that check the error of `resolveDataPath()` or of the schema context's `validate()`, `verify()` and `clearErrors()` (#358). +- **utils:** `capitalize()` doesn't start new words after digits, e.g. `capitalize('the 1st place')` returns `'The 1st Place'`, where it returned `'The 1St Place'`. This also changes the column names that the server derives with it for the join tables of `through` relations, for property names with such words (#356). - **utils:** `escapeHtml()` escapes single quotes as `'`, so its result is safe inside single-quoted attributes, where it left them unescaped (#359). - **utils:** `isAbsoluteUrl()` only accepts letters, digits, plus, period and hyphen in schemes, e.g. `isAbsoluteUrl('a,b:c')` returns `false`, where commas were accepted too (#360). @@ -37,6 +37,7 @@ version, see the upgrade guides in `docs/`. - **router:** Of several `**` routes that match at the same position, the one with more segments wins, then the one with more static segments, e.g. `/f/**/edit` over `/f/**`, where the one added first won. Adding the same `**` route again replaces its handler, as with all other routes, instead of keeping the first one (#332). - **router:** The text after `**` is matched literally, e.g. `/f/**/x.json` doesn't match `/f/a/xajson` anymore, and params after `**` can have any name, e.g. `:my-id`, which threw an `Invalid capture group name` error (#333). - **router:** Params starting with `$` keep their names next to wildcards, e.g. `$x` in `/a/*/:$x`, instead of being renamed to `$` or `$$` and replacing the wildcard's value (#334). +- **admin:** Color fields validate the colors typed into them on blur, where they wrote them only after the blur, so they were only validated on submit. - **admin:** Everything behind the topmost dialog is inert: the page, the navigation, the sidebar and other dialogs. Select All in a dialog no longer selects the values behind it, and assistive technology can't reach them either. - **server:** `toJSON()` signs copies of the asset files inside JSON columns, e.g. `content.header.file`, where it replaced the record's own file objects with the signed copies (#388). - **utils:** `isUrl()` returns `false` for invalid URLs with long paths right away, where the time doubled with each path segment, taking seconds for 28 segments. URLs ending in a lone `?` or `#` are valid too (#348). diff --git a/packages/admin/src/composables/useField.test.js b/packages/admin/src/composables/useField.test.js index e383d5ffc..d4fb0eae7 100644 --- a/packages/admin/src/composables/useField.test.js +++ b/packages/admin/src/composables/useField.test.js @@ -100,9 +100,7 @@ describe('useField()', () => { it('shows the errors of the value right after it changed', async () => { const { findField, getErrors, settle } = await mountRequiredTitle('Emma') - const input = findField('title').find('input') - await enterValue(input, '') - await input.trigger('blur') + await edit(findField('title').find('input'), '') await settle() expect(getErrors('title')).toEqual(['The title field is required.']) }) diff --git a/packages/admin/src/composables/useField.ts b/packages/admin/src/composables/useField.ts index 346f42816..f385b282b 100644 --- a/packages/admin/src/composables/useField.ts +++ b/packages/admin/src/composables/useField.ts @@ -207,7 +207,9 @@ export function useField( set(newValue) { field.value.value = newValue hasEmittedInput = true - isEdited = true + if (focused.value) { + isEdited = true + } emitEvent('input') } }) @@ -215,13 +217,13 @@ export function useField( const handlers: FieldEventHandlers = { onFocus() { focused.value = true - isEdited = false emitEvent('focus') }, onBlur() { focused.value = false if (isEdited) { + isEdited = false validate() } emitEvent('blur') diff --git a/packages/admin/src/types/DitoTypeColor.test.js b/packages/admin/src/types/DitoTypeColor.test.js index 86c27d39d..72a51936d 100644 --- a/packages/admin/src/types/DitoTypeColor.test.js +++ b/packages/admin/src/types/DitoTypeColor.test.js @@ -44,6 +44,28 @@ describe('DitoTypeColor', () => { await vi.waitFor(() => expect(data.background).toBe('#00ff00')) }) + it('validates entered colors on blur', async () => { + const { findField, getErrors, settle } = await mountSchema({ + schema: { + components: { + background: { + type: 'color', + validate: ({ value }) => value !== '#000000' + } + } + }, + data: { background: '#ff8000' } + }) + const input = findField('background').find('input') + await input.trigger('focus') + await enterValue(input, '000000') + await input.trigger('blur') + await settle() + expect(getErrors('background')).toEqual([ + 'The background field is invalid.' + ]) + }) + it('ignores invalid colors', async () => { const { findField, data } = await mountSchema({ schema: { components: { background: { type: 'color' } } }, diff --git a/packages/admin/src/types/DitoTypeColor.vue b/packages/admin/src/types/DitoTypeColor.vue index 62f4e61c3..4aab79706 100644 --- a/packages/admin/src/types/DitoTypeColor.vue +++ b/packages/admin/src/types/DitoTypeColor.vue @@ -87,16 +87,14 @@ const DitoTypeColor = { } function onBlur() { - field.onBlur() - changeOnceEdited.emitIfDone() - } - - watch(canUpdateValue, canUpdateValue => { - if (canUpdateValue && convertedValue.value !== null) { + // Write the value typed into the input before the blur validates it: + if (convertedValue.value !== null) { setEditedValue(convertedValue.value) convertedValue.value = null } - }) + field.onBlur() + changeOnceEdited.emitIfDone() + } watch(showPopup, showPopup => { if (!showPopup) { @@ -144,7 +142,7 @@ const DitoTypeColor = { if (canUpdateValue.value) { setEditedValue(converted) } else { - // Store to change later, once `canUpdateValue` is true again. + // Store to write on blur, see `onBlur()`: convertedValue.value = converted } } diff --git a/packages/admin/src/types/DitoTypeNumber.test.js b/packages/admin/src/types/DitoTypeNumber.test.js index f614ebe29..875972a56 100644 --- a/packages/admin/src/types/DitoTypeNumber.test.js +++ b/packages/admin/src/types/DitoTypeNumber.test.js @@ -92,9 +92,11 @@ describe('DitoTypeNumber', () => { } }) const input = findField('rating').find('input') + await input.trigger('focus') await enterValue(input, '7') await input.trigger('blur') expect(getErrors('rating')).toHaveLength(1) + await input.trigger('focus') await enterValue(input, '4') await input.trigger('blur') expect(getErrors('rating')).toEqual([]) @@ -107,9 +109,11 @@ describe('DitoTypeNumber', () => { } }) const input = findField('price').find('input') + await input.trigger('focus') await enterValue(input, '1.255') await input.trigger('blur') expect(getErrors('price')).toHaveLength(1) + await input.trigger('focus') await enterValue(input, '1.25') await input.trigger('blur') expect(getErrors('price')).toEqual([]) diff --git a/packages/admin/src/types/DitoTypeText.test.js b/packages/admin/src/types/DitoTypeText.test.js index c73e1eb9a..cd26443b1 100644 --- a/packages/admin/src/types/DitoTypeText.test.js +++ b/packages/admin/src/types/DitoTypeText.test.js @@ -225,9 +225,11 @@ describe('DitoTypeText', () => { schema: { components: { email: { type: 'email' } } } }) const input = findField('email').find('input') + await input.trigger('focus') await input.setValue('reader@') await input.trigger('blur') expect(getErrors('email')).toHaveLength(1) + await input.trigger('focus') await input.setValue('reader@example.com') await input.trigger('blur') expect(getErrors('email')).toEqual([])