mirror of
https://github.com/cozystack/cozystack.git
synced 2026-11-12 23:54:01 +09:00
fix(dashboard): resolve a field whose map key contains a dot
Continuing the id namespace into a map entry's nested form put the literal key into the DOM id, which made a pre-existing gap reachable with a shipped schema: a mariadb user named john.doe renders root_users_john.doe_maxUserConnections, while the error path .users.john.doe.maxUserConnections split into one segment too many and resolved nothing. The flattened path cannot say whether a dot separates two properties or sits inside one name, so try the plain split first and then each way of gluing adjacent segments back together, bounded, taking the first id that exists. Only exact ids are tried this way; the descendant fallback stays on the plain split, where it is meaningful. Assisted-by: LLM Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
1 parent
b504e41bf3
commit
3b83fc17da
2 files changed
+62
-17
No files matched your search
+20
-9
@@ -44,17 +44,28 @@ describe("focusFirstError", () => {
|
||||
expect(document.activeElement).toBe(field)
|
||||
})
|
||||
|
||||
// Pinned gap: RJSF gives the same dotted path whether the dot separates two
|
||||
// properties or sits inside one name, so a map keyed `ghcr.io` cannot be told
|
||||
// apart from a nested `ghcr` -> `io` and its field is never focused. The
|
||||
// submit still blocks and the inline error still renders; only the scroll is
|
||||
// lost. Fixing it needs the schema, not the error.
|
||||
it("does not resolve a property whose own name contains a dot", () => {
|
||||
document.body.innerHTML = `<input id="root_talos_registryMirrors_ghcr.io" />`
|
||||
// A map key can itself contain a dot -- a mariadb user `john.doe`, a registry
|
||||
// mirror `ghcr.io` -- and the flattened path cannot say whether a dot
|
||||
// separates two properties or sits inside one, so both readings are tried.
|
||||
it("resolves a property whose own name contains a dot", () => {
|
||||
document.body.innerHTML = `<input id="root_users_john.doe_maxUserConnections" />`
|
||||
const field = document.getElementById("root_users_john.doe_maxUserConnections")
|
||||
|
||||
focusFirstError({ property: ".talos.registryMirrors.ghcr.io" })
|
||||
focusFirstError({ property: ".users.john.doe.maxUserConnections" })
|
||||
|
||||
expect(document.activeElement).toBe(document.body)
|
||||
expect(document.activeElement).toBe(field)
|
||||
})
|
||||
|
||||
it("prefers the plain split when both readings are present", () => {
|
||||
document.body.innerHTML = `
|
||||
<input id="root_a.b" />
|
||||
<input id="root_a_b" />
|
||||
`
|
||||
const plain = document.getElementById("root_a_b")
|
||||
|
||||
focusFirstError({ property: ".a.b" })
|
||||
|
||||
expect(document.activeElement).toBe(plain)
|
||||
})
|
||||
|
||||
it("uses the same custom prefix and separator as the form", () => {
|
||||
|
||||
@@ -4,16 +4,41 @@ import type { RJSFValidationError } from "@rjsf/utils"
|
||||
* RJSF builds `property` as the JSON-pointer instance path with the slashes
|
||||
* swapped for dots (`processRawValidationErrors.js`), so the path is always
|
||||
* dotted and never bracketed -- an array index arrives as `.items.0.name`.
|
||||
*
|
||||
* The flattening is lossy for a property whose own name contains a dot, which
|
||||
* an additionalProperties map keyed `ghcr.io` produces: the split yields one
|
||||
* segment too many and the generated id does not match the rendered one, so
|
||||
* that field is not brought into view. Pinned in the tests.
|
||||
*/
|
||||
function propertySegments(property: string): string[] {
|
||||
return property.split(".").filter((segment) => segment !== "")
|
||||
}
|
||||
|
||||
/**
|
||||
* The flattening is lossy: a dot inside one property name is indistinguishable
|
||||
* from the separator between two, and a map keyed `john.doe` or `ghcr.io`
|
||||
* produces exactly that. So try the plain split first, then every way of
|
||||
* gluing adjacent segments back together with the dot they may have come from.
|
||||
* Bounded, because the count doubles with each segment and only the first
|
||||
* matching id is used.
|
||||
*/
|
||||
function candidateIds(
|
||||
segments: string[],
|
||||
idPrefix: string,
|
||||
idSeparator: string,
|
||||
): string[] {
|
||||
const ids: string[] = []
|
||||
const build = (index: number, parts: string[]) => {
|
||||
if (ids.length >= 64) return
|
||||
if (index === segments.length) {
|
||||
ids.push([idPrefix, ...parts].join(idSeparator))
|
||||
return
|
||||
}
|
||||
build(index + 1, [...parts, segments[index]])
|
||||
if (parts.length > 0) {
|
||||
const glued = `${parts[parts.length - 1]}.${segments[index]}`
|
||||
build(index + 1, [...parts.slice(0, -1), glued])
|
||||
}
|
||||
}
|
||||
build(0, [])
|
||||
return ids
|
||||
}
|
||||
|
||||
/**
|
||||
* Errors render inline with the error list hidden, so a blocked submit is
|
||||
* invisible unless the offending field is brought into view. RJSF's built-in
|
||||
@@ -33,7 +58,12 @@ export function focusFirstError(
|
||||
idPrefix = "root",
|
||||
idSeparator = "_",
|
||||
) {
|
||||
const id = [idPrefix, ...propertySegments(error.property ?? "")].join(idSeparator)
|
||||
const candidates = candidateIds(
|
||||
propertySegments(error.property ?? ""),
|
||||
idPrefix,
|
||||
idSeparator,
|
||||
)
|
||||
const id = candidates[0]
|
||||
const quote = (v: string) => v.replace(/["\\]/g, "\\$&")
|
||||
const escaped = quote(id)
|
||||
const escapedSeparator = quote(idSeparator)
|
||||
@@ -44,8 +74,12 @@ export function focusFirstError(
|
||||
...controls.map((tag) => `${tag}[id^="${escaped}${escapedSeparator}"]`),
|
||||
...controls.map((tag) => `${tag}[name^="${escaped}-"]`),
|
||||
].join(", ")
|
||||
const field =
|
||||
document.getElementById(id) ?? document.querySelector<HTMLElement>(fallback)
|
||||
// An exact id is unambiguous, so try every candidate before falling back to
|
||||
// the heuristic descendant match, which only makes sense for the plain split.
|
||||
const exact = candidates
|
||||
.map((candidate) => document.getElementById(candidate))
|
||||
.find((element): element is HTMLElement => element !== null)
|
||||
const field = exact ?? document.querySelector<HTMLElement>(fallback)
|
||||
field?.scrollIntoView?.({ block: "center" })
|
||||
field?.focus?.({ preventScroll: true })
|
||||
}
|
||||
Reference in new issue
Block a user