Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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).

Expand All @@ -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).
Expand Down
4 changes: 1 addition & 3 deletions packages/admin/src/composables/useField.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.'])
})
Expand Down
6 changes: 4 additions & 2 deletions packages/admin/src/composables/useField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,21 +207,23 @@ export function useField(
set(newValue) {
field.value.value = newValue
hasEmittedInput = true
isEdited = true
if (focused.value) {
isEdited = true
}
emitEvent('input')
}
})

const handlers: FieldEventHandlers = {
onFocus() {
focused.value = true
isEdited = false
emitEvent('focus')
},

onBlur() {
focused.value = false
if (isEdited) {
isEdited = false
validate()
}
emitEvent('blur')
Expand Down
22 changes: 22 additions & 0 deletions packages/admin/src/types/DitoTypeColor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' } } },
Expand Down
14 changes: 6 additions & 8 deletions packages/admin/src/types/DitoTypeColor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
}
Expand Down
4 changes: 4 additions & 0 deletions packages/admin/src/types/DitoTypeNumber.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([])
Expand All @@ -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([])
Expand Down
2 changes: 2 additions & 0 deletions packages/admin/src/types/DitoTypeText.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([])
Expand Down
Loading