feature: RowDialog validates a row as part of a larger value, such as a whole document with the row put in.
Release / release (push) Successful in 29s

This commit is contained in:
slawek committed 2026-10-30 12:38:15 +01:00
1 parent cd6ef1c120
commit 00934a7c0c
6 files changed
+23 -10

No files matched your search

+8
View File
@@ -1,5 +1,13 @@
# Changelog # Changelog
## 0.6.0
feature: RowDialog validates a row as part of a larger value, such as a whole document with the row put in.
- `RowDialog` takes `body`, the value posted to `validatePath` for the row, and `select`, the
row's problems by field and message taken from the answer.
- `useValidation` and `RowDialog` take an answer of any shape.
## 0.5.0 ## 0.5.0
feature: the entry point /shared exports the types and constants a server and its web application share. feature: the entry point /shared exports the types and constants a server and its web application share.
+1 -1
View File
@@ -84,7 +84,7 @@ API types imported by both, without the code of either: `CredentialField`, `Cred
| `ThemeMenu` | the button cycling light, dark and system theme | | `ThemeMenu` | the button cycling light, dark and system theme |
| `SwitcherMenu`, `SwitcherItem` | a button naming the item chosen, whose menu chooses another, such as a page or a connection | | `SwitcherMenu`, `SwitcherItem` | a button naming the item chosen, whose menu chooses another, such as a page or a connection |
| `TextInput` | a text field with clear and show/hide buttons | | `TextInput` | a text field with clear and show/hide buttons |
| `RowDialog`, `useValidation` | a dialog that edits a value validated by the server as it changes, and the validation hook | | `RowDialog`, `useValidation` | a dialog that edits a value validated by the server as it changes, and the validation hook; with `body` and `select` the dialog posts another value, such as a whole document with the row put in, and takes the row's problems from the answer |
| `createBrowserSettings(keys, event)` | switches kept in localStorage and followed across tabs | | `createBrowserSettings(keys, event)` | switches kept in localStorage and followed across tabs |
| `CredentialsButton` | the dialog of a credential store; see [Credential store](#credential-store) | | `CredentialsButton` | the dialog of a credential store; see [Credential store](#credential-store) |
| `TfModule`, `TfInstance`, `TfvarsFileStatus`, `TF_NAME_PATTERN`, `TFVARS_FILE_PATTERN`, `STATE_SCHEMA_PATTERN` | the types and name patterns of the [Tfvars store](#tfvars-store) | | `TfModule`, `TfInstance`, `TfvarsFileStatus`, `TF_NAME_PATTERN`, `TFVARS_FILE_PATTERN`, `STATE_SCHEMA_PATTERN` | the types and name patterns of the [Tfvars store](#tfvars-store) |
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@koszewscy/sk-react-toolkit", "name": "@koszewscy/sk-react-toolkit",
"version": "0.5.0", "version": "0.6.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@koszewscy/sk-react-toolkit", "name": "@koszewscy/sk-react-toolkit",
"version": "0.5.0", "version": "0.6.0",
"devDependencies": { "devDependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1", "@emotion/styled": "^11.14.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@koszewscy/sk-react-toolkit", "name": "@koszewscy/sk-react-toolkit",
"version": "0.5.0", "version": "0.6.0",
"description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font plugin.", "description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font plugin.",
"private": true, "private": true,
"type": "module", "type": "module",
+10 -5
View File
@@ -6,12 +6,16 @@ import { useValidation } from './useValidation.ts'
/** /**
* A dialog that edits one row. The server validates the value as it changes (validatePath); onSave writes it and * A dialog that edits one row. The server validates the value as it changes (validatePath); onSave writes it and
* the dialog closes when it succeeds. fields renders the inputs from the value, a setter and the problems by field; * the dialog closes when it succeeds. fields renders the inputs from the value, a setter and the problems by field;
* below shows extra information from the validation, such as subnet placements. * below shows extra information from the validation, such as subnet placements. body gives the value posted to
* validatePath, such as a whole document with the row put in, and select the problems by field and the message of
* the row taken from the answer; without them the row itself is posted and the answer is its validation.
*/ */
export default function RowDialog<T, V extends Validation = Validation>(props: { export default function RowDialog<T, V = Validation>(props: {
title: string title: string
initial: T initial: T
validatePath: string validatePath: string
body?: (value: T) => unknown
select?: (result: V) => Validation
saveLabel: string saveLabel: string
onSave: (value: T) => Promise<void> onSave: (value: T) => Promise<void>
onCancel: () => void onCancel: () => void
@@ -21,9 +25,10 @@ export default function RowDialog<T, V extends Validation = Validation>(props: {
const [value, setValue] = useState<T>(props.initial) const [value, setValue] = useState<T>(props.initial)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const validation = useValidation<V>(props.validatePath, value) const validation = useValidation<V>(props.validatePath, props.body ? props.body(value) : value)
const problems = validation.result?.problems ?? {} const selected = validation.result === null ? null : props.select ? props.select(validation.result) : validation.result as Validation
const message = validation.result?.message ?? '' const problems = selected?.problems ?? {}
const message = selected?.message ?? ''
const blocked = validation.pending || Object.keys(problems).length > 0 || message !== '' const blocked = validation.pending || Object.keys(problems).length > 0 || message !== ''
async function submit(event: FormEvent) { async function submit(event: FormEvent) {
+1 -1
View File
@@ -5,7 +5,7 @@ import { request, type Validation } from './api.ts'
* Asks the server to validate value at path (a .../validate endpoint) 250 ms after it last * Asks the server to validate value at path (a .../validate endpoint) 250 ms after it last
* changed. pending is true until the answer for the current value arrives; until then the previous result stays. * changed. pending is true until the answer for the current value arrives; until then the previous result stays.
*/ */
export function useValidation<T extends Validation>(path: string | null, value: unknown): { result: T | null; error: string; pending: boolean } { export function useValidation<T = Validation>(path: string | null, value: unknown): { result: T | null; error: string; pending: boolean } {
const key = path === null ? '' : `${path}\n${JSON.stringify(value)}` const key = path === null ? '' : `${path}\n${JSON.stringify(value)}`
const [answer, setAnswer] = useState<{ key: string; result: T | null; error: string } | null>(null) const [answer, setAnswer] = useState<{ key: string; result: T | null; error: string } | null>(null)