@panneau/core 5.0.0-alpha.0 → 5.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { type Condition, type Field, type Item, type RuleFailure } from '@panneau/protocol';
1
+ import type { Condition, Field, Item, RuleFailure } from '@panneau/protocol';
2
2
  import type { FormMode } from '../definition/form';
3
3
  import { type Values } from '../values';
4
4
  import { type FormStore } from './store';
@@ -1 +1 @@
1
- {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/form/form.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/form/form.ts"],"names":[],"mappings":"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"}
package/dist/form/form.js CHANGED
@@ -3,7 +3,7 @@ import { formatRuleFailure } from "./messages.js";
3
3
  import { getItemValues, getProblem, isItem } from "./problem.js";
4
4
  import { selectErrorCount, selectErrors, selectValue, selectVisible } from "./selectors.js";
5
5
  import { createLocalStore } from "./store.js";
6
- import { checkValues, getValue } from "@panneau/protocol";
6
+ import { checkValues, getValue } from "@panneau/protocol/runtime";
7
7
  //#region src/form/form.ts
8
8
  /** Initial values of fields that have a `default` and no value. */
9
9
  function getDefaultValues(fields, values = {}) {
@@ -1 +1 @@
1
- {"version":3,"file":"form.js","names":[],"sources":["../../src/form/form.ts"],"sourcesContent":["import {\n type Condition,\n type Field,\n type Item,\n type RuleFailure,\n checkValues,\n getValue,\n} from '@panneau/protocol';\n\nimport type { FormMode } from '../definition/form';\nimport { type Values, isEqual, isPathWithin } from '../values';\nimport { formatRuleFailure } from './messages';\nimport { getItemValues, getProblem, isItem } from './problem';\nimport { selectErrorCount, selectErrors, selectValue, selectVisible } from './selectors';\nimport { type FormStore, createLocalStore } from './store';\n\n/**\n * - `idle`: nothing submitted yet, or after a reset;\n * - `submitting`: waiting for `onSubmit`;\n * - `submitted`: the last submit succeeded;\n * - `invalid`: client validation or the server (`422`) found errors;\n * - `conflict`: the item changed since the form's version (`412`), see `resolveConflict()`;\n * - `error`: the last submit failed for another reason (network, `403`, `500`…), see `error`.\n */\nexport type FormStatus = 'idle' | 'submitting' | 'submitted' | 'invalid' | 'conflict' | 'error';\n\nexport interface FormConflict {\n /** The current version of the item, when the backend sent it. */\n current: Item | null;\n}\n\n/** A snapshot of a form. A new object is created on every change. */\nexport interface FormState {\n values: Values;\n /** The values the form started from, or was last saved with. `dirty` compares to them. */\n initialValues: Values;\n /** The version of the item the values were edited from, sent with the submit. */\n version: string | null;\n /** Whether the values differ from the initial values. */\n dirty: boolean;\n /**\n * Errors shown, by dot path (`title.fr`): client errors of touched fields (all of them after a\n * submit), then server errors. A server error goes away when its value changes.\n */\n errors: Record<string, string[]>;\n /** Paths of the fields that lost focus at least once. */\n touched: ReadonlySet<string>;\n status: FormStatus;\n /** With the `conflict` status: what the backend says is current. */\n conflict: FormConflict | null;\n /** With the `error` status: what `onSubmit` threw. */\n error: unknown;\n submitCount: number;\n}\n\nexport interface SubmitContext {\n mode: FormMode;\n /** The version to send as `If-Match`. */\n version: string | null;\n /** The top-level values that differ from the initial values, for partial updates. */\n changes: Values;\n}\n\nexport type SubmitResult =\n | { status: 'submitted'; result: unknown }\n | { status: 'invalid'; errors: Record<string, string[]> }\n | { status: 'conflict'; current: Item | null }\n | { status: 'error'; error: unknown };\n\nexport interface FormOptions {\n /** The fields of the form, as resolved by `resolveForm()`. */\n fields: Field[];\n /** Initial values. In `create` mode, fields without a value get their `default`. */\n values?: Values;\n /** The version of the item the values come from. */\n version?: string | null;\n /** Defaults to `update` when a version is given, `create` otherwise. */\n mode?: FormMode;\n /** Locales a `required` localized field must be filled in (all of them, usually). */\n locales?: string[];\n /** Where the values live. Defaults to a local store. */\n store?: FormStore;\n /** Message of a failed rule. Defaults to `formatRuleFailure()`. */\n formatError?: (failure: RuleFailure, path: string) => string;\n /**\n * Saves the values. When it returns an item, the form continues from that item. When it\n * throws an error that carries problem details (`getProblem()`), `422` errors are shown by\n * path and `412` puts the form in the `conflict` status.\n */\n onSubmit?: (values: Values, context: SubmitContext) => unknown;\n}\n\n/** Form options that can change after the form is created. */\nexport type UpdatableFormOptions = Pick<\n FormOptions,\n 'fields' | 'locales' | 'formatError' | 'onSubmit'\n>;\n\nexport interface GetErrorsOptions {\n /** Also return the errors of nested paths (`title.fr` for `title`). */\n nested?: boolean;\n}\n\n/**\n * A headless form: values, errors, visibility and submit. React components use `useForm()`.\n *\n * The methods that read the state (`getValue()`, `getErrors()`, `countErrors()`, `isVisible()`)\n * are not React APIs: the React Compiler memoizes their calls by their arguments, so they return\n * stale results in components. Components read the state they subscribed to with the selectors\n * (`selectValue()`, `selectErrors()`, `selectErrorCount()`, `selectVisible()`).\n */\nexport interface Form {\n getState(): FormState;\n subscribe(listener: () => void): () => void;\n /** Updates the options that can change between renders. */\n setOptions(options: Partial<UpdatableFormOptions>): void;\n /**\n * The value at a dot path. Not a React API: components use `selectValue(state, path)` (see\n * {@link Form}).\n */\n getValue(path: string): unknown;\n setValue(path: string, value: unknown): void;\n /** Marks a field as touched, which shows its client errors. */\n blur(path: string): void;\n /**\n * Errors at a path. Not a React API: components use `selectErrors(state, path, options)` (see\n * {@link Form}).\n */\n getErrors(path: string, options?: GetErrorsOptions): string[];\n /**\n * The number of paths with errors within the given paths, for counts on tabs and sections. Not\n * a React API: components use `selectErrorCount(state, paths)` (see {@link Form}).\n */\n countErrors(paths: readonly string[]): number;\n /**\n * Whether a field or a layout node is visible, from its `visibleWhen`. For nested fields, pass\n * the path of the object they belong to (`blocks.2`): conditions are relative to it. Not a\n * React API: components use `selectVisible(state, target, parentPath)` (see {@link Form}).\n */\n isVisible(target: { visibleWhen?: Condition | undefined }, parentPath?: string): boolean;\n /** Checks every value with the portable rules and shows every error. Returns whether valid. */\n validate(): boolean;\n /** Shows server errors, by dot path. */\n setServerErrors(errors: Record<string, string[]>): void;\n /** Validates, then calls `onSubmit`. */\n submit(): Promise<SubmitResult>;\n /** Starts over from new values (or the initial ones), and a new version. */\n reset(values?: Values, version?: string | null): void;\n /**\n * Leaves the `conflict` status:\n *\n * - `reload` drops the local changes and continues from the current item;\n * - `overwrite` keeps the local changes and submits them again over the current version.\n */\n resolveConflict(choice: 'reload' | 'overwrite'): Promise<SubmitResult | null>;\n}\n\n/** Initial values of fields that have a `default` and no value. */\nexport function getDefaultValues(fields: readonly Field[], values: Values = {}): Values {\n return fields.reduce<Values>(\n (result, field) =>\n field.default !== undefined && result[field.name] === undefined\n ? { ...result, [field.name]: field.default }\n : result,\n values,\n );\n}\n\nfunction mergeErrors(...sources: Record<string, string[]>[]): Record<string, string[]> {\n const merged: Record<string, string[]> = {};\n sources.forEach((source) =>\n Object.entries(source).forEach(([path, messages]) => {\n merged[path] = [...(merged[path] ?? []), ...messages];\n }),\n );\n return merged;\n}\n\n/** Creates a headless form. */\nexport function createForm(options: FormOptions): Form {\n const current: Required<UpdatableFormOptions> = {\n fields: options.fields,\n locales: options.locales ?? [],\n formatError: options.formatError ?? formatRuleFailure,\n onSubmit: options.onSubmit ?? (() => undefined),\n };\n const mode: FormMode =\n options.mode ??\n (options.version !== undefined && options.version !== null ? 'update' : 'create');\n const startValues =\n mode === 'create'\n ? getDefaultValues(options.fields, options.values)\n : (options.values ?? {});\n const store = options.store ?? createLocalStore(startValues);\n if (options.store !== undefined && options.values !== undefined) {\n store.setValues(startValues);\n }\n\n let initialValues = store.getValues();\n let version = options.version ?? null;\n let touched: ReadonlySet<string> = new Set();\n let serverErrors: Record<string, string[]> = {};\n let status: FormStatus = 'idle';\n let conflict: FormConflict | null = null;\n let error: unknown = null;\n let submitCount = 0;\n let pending: Promise<SubmitResult> | null = null;\n\n const listeners = new Set<() => void>();\n let unsubscribeStore: (() => void) | null = null;\n\n const clientErrors = (values: Values, all: boolean): Record<string, string[]> => {\n const failures = checkValues(current.fields, values, { mode, locales: current.locales });\n return Object.fromEntries(\n Object.entries(failures)\n .filter(\n ([path]) => all || [...touched].some((parent) => isPathWithin(path, parent)),\n )\n .map(([path, list]) => [\n path,\n list.map((failure) => current.formatError(failure, path)),\n ]),\n );\n };\n\n const compute = (values: Values): FormState => ({\n values,\n initialValues,\n version,\n dirty: !isEqual(values, initialValues),\n errors: mergeErrors(clientErrors(values, submitCount > 0), serverErrors),\n touched,\n status,\n conflict,\n error,\n submitCount,\n });\n\n let state = compute(store.getValues());\n\n const update = () => {\n state = compute(store.getValues());\n listeners.forEach((listener) => listener());\n };\n\n /** Server errors go away when their value changes. */\n const onStoreChange = () => {\n const previous = state.values;\n const values = store.getValues();\n if (values === previous) {\n return;\n }\n serverErrors = Object.fromEntries(\n Object.entries(serverErrors).filter(([path]) =>\n isEqual(getValue(previous, path), getValue(values, path)),\n ),\n );\n update();\n };\n\n const changes = (values: Values): Values =>\n Object.fromEntries(\n Object.entries({ ...initialValues, ...values }).flatMap(([key]) =>\n isEqual(values[key], initialValues[key]) ? [] : [[key, values[key]]],\n ),\n );\n\n const fieldNames = () => current.fields.map((field) => field.name);\n\n const reset = (values?: Values, nextVersion?: string | null) => {\n if (nextVersion !== undefined) {\n version = nextVersion;\n }\n touched = new Set();\n serverErrors = {};\n status = 'idle';\n conflict = null;\n error = null;\n submitCount = 0;\n if (values !== undefined) {\n initialValues = values;\n store.setValues(values);\n } else {\n store.setValues(initialValues);\n }\n update();\n };\n\n const runSubmit = async (): Promise<SubmitResult> => {\n submitCount += 1;\n serverErrors = {};\n const values = store.getValues();\n const errors = clientErrors(values, true);\n if (Object.keys(errors).length > 0) {\n status = 'invalid';\n update();\n return { status: 'invalid', errors };\n }\n status = 'submitting';\n conflict = null;\n error = null;\n update();\n try {\n const result = await current.onSubmit(values, {\n mode,\n version,\n changes: changes(values),\n });\n if (isItem(result)) {\n const saved = getItemValues(result, fieldNames());\n version = result.version;\n initialValues = saved;\n // Keep what was typed while saving; otherwise take the saved values.\n if (store.getValues() === values) {\n store.setValues(saved);\n }\n } else {\n initialValues = values;\n }\n touched = new Set();\n submitCount = 0;\n status = 'submitted';\n update();\n return { status: 'submitted', result };\n } catch (caught) {\n const problem = getProblem(caught);\n if (problem?.status === 412) {\n status = 'conflict';\n conflict = { current: problem.current ?? null };\n update();\n return { status: 'conflict', current: conflict.current };\n }\n if (problem?.status === 422 && problem.errors !== undefined) {\n status = 'invalid';\n serverErrors = problem.errors;\n update();\n return { status: 'invalid', errors: problem.errors };\n }\n status = 'error';\n error = caught;\n update();\n return { status: 'error', error: caught };\n }\n };\n\n const form: Form = {\n getState() {\n if (unsubscribeStore === null) {\n onStoreChange();\n }\n return state;\n },\n subscribe(listener) {\n listeners.add(listener);\n unsubscribeStore ??= store.subscribe(onStoreChange);\n onStoreChange();\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0 && unsubscribeStore !== null) {\n unsubscribeStore();\n unsubscribeStore = null;\n }\n };\n },\n setOptions(next) {\n const { fields, locales, formatError, onSubmit } = next;\n if (onSubmit !== undefined) {\n current.onSubmit = onSubmit;\n }\n if (formatError !== undefined) {\n current.formatError = formatError;\n }\n // Fields and locales change what is valid: compare them by value, since a render can\n // create new arrays with the same content.\n const changed =\n (fields !== undefined && !isEqual(fields, current.fields)) ||\n (locales !== undefined && !isEqual(locales, current.locales));\n if (fields !== undefined) {\n current.fields = fields;\n }\n if (locales !== undefined) {\n current.locales = locales;\n }\n if (changed) {\n update();\n }\n },\n getValue: (path) => selectValue(form.getState(), path),\n setValue(path, value) {\n store.setValue(path, value);\n if (unsubscribeStore === null) {\n onStoreChange();\n }\n },\n blur(path) {\n if (touched.has(path)) {\n return;\n }\n touched = new Set([...touched, path]);\n update();\n },\n getErrors: (path, getErrorsOptions) =>\n selectErrors(form.getState(), path, getErrorsOptions),\n countErrors: (paths) => selectErrorCount(form.getState(), paths),\n isVisible: (target, parentPath) => selectVisible(form.getState(), target, parentPath),\n validate() {\n submitCount = Math.max(submitCount, 1);\n update();\n return Object.keys(clientErrors(store.getValues(), true)).length === 0;\n },\n setServerErrors(errors) {\n serverErrors = errors;\n update();\n },\n submit() {\n pending ??= runSubmit().finally(() => {\n pending = null;\n });\n return pending;\n },\n reset,\n async resolveConflict(choice) {\n if (status !== 'conflict' || conflict === null) {\n return null;\n }\n const latest = conflict.current;\n if (choice === 'reload') {\n if (latest !== null) {\n reset(getItemValues(latest, fieldNames()), latest.version);\n } else {\n status = 'idle';\n conflict = null;\n update();\n }\n return null;\n }\n if (latest !== null) {\n version = latest.version;\n }\n conflict = null;\n return form.submit();\n },\n };\n return form;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"form.js","names":[],"sources":["../../src/form/form.ts"],"sourcesContent":["import type { Condition, Field, Item, RuleFailure } from '@panneau/protocol';\nimport { checkValues, getValue } from '@panneau/protocol/runtime';\n\nimport type { FormMode } from '../definition/form';\nimport { type Values, isEqual, isPathWithin } from '../values';\nimport { formatRuleFailure } from './messages';\nimport { getItemValues, getProblem, isItem } from './problem';\nimport { selectErrorCount, selectErrors, selectValue, selectVisible } from './selectors';\nimport { type FormStore, createLocalStore } from './store';\n\n/**\n * - `idle`: nothing submitted yet, or after a reset;\n * - `submitting`: waiting for `onSubmit`;\n * - `submitted`: the last submit succeeded;\n * - `invalid`: client validation or the server (`422`) found errors;\n * - `conflict`: the item changed since the form's version (`412`), see `resolveConflict()`;\n * - `error`: the last submit failed for another reason (network, `403`, `500`…), see `error`.\n */\nexport type FormStatus = 'idle' | 'submitting' | 'submitted' | 'invalid' | 'conflict' | 'error';\n\nexport interface FormConflict {\n /** The current version of the item, when the backend sent it. */\n current: Item | null;\n}\n\n/** A snapshot of a form. A new object is created on every change. */\nexport interface FormState {\n values: Values;\n /** The values the form started from, or was last saved with. `dirty` compares to them. */\n initialValues: Values;\n /** The version of the item the values were edited from, sent with the submit. */\n version: string | null;\n /** Whether the values differ from the initial values. */\n dirty: boolean;\n /**\n * Errors shown, by dot path (`title.fr`): client errors of touched fields (all of them after a\n * submit), then server errors. A server error goes away when its value changes.\n */\n errors: Record<string, string[]>;\n /** Paths of the fields that lost focus at least once. */\n touched: ReadonlySet<string>;\n status: FormStatus;\n /** With the `conflict` status: what the backend says is current. */\n conflict: FormConflict | null;\n /** With the `error` status: what `onSubmit` threw. */\n error: unknown;\n submitCount: number;\n}\n\nexport interface SubmitContext {\n mode: FormMode;\n /** The version to send as `If-Match`. */\n version: string | null;\n /** The top-level values that differ from the initial values, for partial updates. */\n changes: Values;\n}\n\nexport type SubmitResult =\n | { status: 'submitted'; result: unknown }\n | { status: 'invalid'; errors: Record<string, string[]> }\n | { status: 'conflict'; current: Item | null }\n | { status: 'error'; error: unknown };\n\nexport interface FormOptions {\n /** The fields of the form, as resolved by `resolveForm()`. */\n fields: Field[];\n /** Initial values. In `create` mode, fields without a value get their `default`. */\n values?: Values;\n /** The version of the item the values come from. */\n version?: string | null;\n /** Defaults to `update` when a version is given, `create` otherwise. */\n mode?: FormMode;\n /** Locales a `required` localized field must be filled in (all of them, usually). */\n locales?: string[];\n /** Where the values live. Defaults to a local store. */\n store?: FormStore;\n /** Message of a failed rule. Defaults to `formatRuleFailure()`. */\n formatError?: (failure: RuleFailure, path: string) => string;\n /**\n * Saves the values. When it returns an item, the form continues from that item. When it\n * throws an error that carries problem details (`getProblem()`), `422` errors are shown by\n * path and `412` puts the form in the `conflict` status.\n */\n onSubmit?: (values: Values, context: SubmitContext) => unknown;\n}\n\n/** Form options that can change after the form is created. */\nexport type UpdatableFormOptions = Pick<\n FormOptions,\n 'fields' | 'locales' | 'formatError' | 'onSubmit'\n>;\n\nexport interface GetErrorsOptions {\n /** Also return the errors of nested paths (`title.fr` for `title`). */\n nested?: boolean;\n}\n\n/**\n * A headless form: values, errors, visibility and submit. React components use `useForm()`.\n *\n * The methods that read the state (`getValue()`, `getErrors()`, `countErrors()`, `isVisible()`)\n * are not React APIs: the React Compiler memoizes their calls by their arguments, so they return\n * stale results in components. Components read the state they subscribed to with the selectors\n * (`selectValue()`, `selectErrors()`, `selectErrorCount()`, `selectVisible()`).\n */\nexport interface Form {\n getState(): FormState;\n subscribe(listener: () => void): () => void;\n /** Updates the options that can change between renders. */\n setOptions(options: Partial<UpdatableFormOptions>): void;\n /**\n * The value at a dot path. Not a React API: components use `selectValue(state, path)` (see\n * {@link Form}).\n */\n getValue(path: string): unknown;\n setValue(path: string, value: unknown): void;\n /** Marks a field as touched, which shows its client errors. */\n blur(path: string): void;\n /**\n * Errors at a path. Not a React API: components use `selectErrors(state, path, options)` (see\n * {@link Form}).\n */\n getErrors(path: string, options?: GetErrorsOptions): string[];\n /**\n * The number of paths with errors within the given paths, for counts on tabs and sections. Not\n * a React API: components use `selectErrorCount(state, paths)` (see {@link Form}).\n */\n countErrors(paths: readonly string[]): number;\n /**\n * Whether a field or a layout node is visible, from its `visibleWhen`. For nested fields, pass\n * the path of the object they belong to (`blocks.2`): conditions are relative to it. Not a\n * React API: components use `selectVisible(state, target, parentPath)` (see {@link Form}).\n */\n isVisible(target: { visibleWhen?: Condition | undefined }, parentPath?: string): boolean;\n /** Checks every value with the portable rules and shows every error. Returns whether valid. */\n validate(): boolean;\n /** Shows server errors, by dot path. */\n setServerErrors(errors: Record<string, string[]>): void;\n /** Validates, then calls `onSubmit`. */\n submit(): Promise<SubmitResult>;\n /** Starts over from new values (or the initial ones), and a new version. */\n reset(values?: Values, version?: string | null): void;\n /**\n * Leaves the `conflict` status:\n *\n * - `reload` drops the local changes and continues from the current item;\n * - `overwrite` keeps the local changes and submits them again over the current version.\n */\n resolveConflict(choice: 'reload' | 'overwrite'): Promise<SubmitResult | null>;\n}\n\n/** Initial values of fields that have a `default` and no value. */\nexport function getDefaultValues(fields: readonly Field[], values: Values = {}): Values {\n return fields.reduce<Values>(\n (result, field) =>\n field.default !== undefined && result[field.name] === undefined\n ? { ...result, [field.name]: field.default }\n : result,\n values,\n );\n}\n\nfunction mergeErrors(...sources: Record<string, string[]>[]): Record<string, string[]> {\n const merged: Record<string, string[]> = {};\n sources.forEach((source) =>\n Object.entries(source).forEach(([path, messages]) => {\n merged[path] = [...(merged[path] ?? []), ...messages];\n }),\n );\n return merged;\n}\n\n/** Creates a headless form. */\nexport function createForm(options: FormOptions): Form {\n const current: Required<UpdatableFormOptions> = {\n fields: options.fields,\n locales: options.locales ?? [],\n formatError: options.formatError ?? formatRuleFailure,\n onSubmit: options.onSubmit ?? (() => undefined),\n };\n const mode: FormMode =\n options.mode ??\n (options.version !== undefined && options.version !== null ? 'update' : 'create');\n const startValues =\n mode === 'create'\n ? getDefaultValues(options.fields, options.values)\n : (options.values ?? {});\n const store = options.store ?? createLocalStore(startValues);\n if (options.store !== undefined && options.values !== undefined) {\n store.setValues(startValues);\n }\n\n let initialValues = store.getValues();\n let version = options.version ?? null;\n let touched: ReadonlySet<string> = new Set();\n let serverErrors: Record<string, string[]> = {};\n let status: FormStatus = 'idle';\n let conflict: FormConflict | null = null;\n let error: unknown = null;\n let submitCount = 0;\n let pending: Promise<SubmitResult> | null = null;\n\n const listeners = new Set<() => void>();\n let unsubscribeStore: (() => void) | null = null;\n\n const clientErrors = (values: Values, all: boolean): Record<string, string[]> => {\n const failures = checkValues(current.fields, values, { mode, locales: current.locales });\n return Object.fromEntries(\n Object.entries(failures)\n .filter(\n ([path]) => all || [...touched].some((parent) => isPathWithin(path, parent)),\n )\n .map(([path, list]) => [\n path,\n list.map((failure) => current.formatError(failure, path)),\n ]),\n );\n };\n\n const compute = (values: Values): FormState => ({\n values,\n initialValues,\n version,\n dirty: !isEqual(values, initialValues),\n errors: mergeErrors(clientErrors(values, submitCount > 0), serverErrors),\n touched,\n status,\n conflict,\n error,\n submitCount,\n });\n\n let state = compute(store.getValues());\n\n const update = () => {\n state = compute(store.getValues());\n listeners.forEach((listener) => listener());\n };\n\n /** Server errors go away when their value changes. */\n const onStoreChange = () => {\n const previous = state.values;\n const values = store.getValues();\n if (values === previous) {\n return;\n }\n serverErrors = Object.fromEntries(\n Object.entries(serverErrors).filter(([path]) =>\n isEqual(getValue(previous, path), getValue(values, path)),\n ),\n );\n update();\n };\n\n const changes = (values: Values): Values =>\n Object.fromEntries(\n Object.entries({ ...initialValues, ...values }).flatMap(([key]) =>\n isEqual(values[key], initialValues[key]) ? [] : [[key, values[key]]],\n ),\n );\n\n const fieldNames = () => current.fields.map((field) => field.name);\n\n const reset = (values?: Values, nextVersion?: string | null) => {\n if (nextVersion !== undefined) {\n version = nextVersion;\n }\n touched = new Set();\n serverErrors = {};\n status = 'idle';\n conflict = null;\n error = null;\n submitCount = 0;\n if (values !== undefined) {\n initialValues = values;\n store.setValues(values);\n } else {\n store.setValues(initialValues);\n }\n update();\n };\n\n const runSubmit = async (): Promise<SubmitResult> => {\n submitCount += 1;\n serverErrors = {};\n const values = store.getValues();\n const errors = clientErrors(values, true);\n if (Object.keys(errors).length > 0) {\n status = 'invalid';\n update();\n return { status: 'invalid', errors };\n }\n status = 'submitting';\n conflict = null;\n error = null;\n update();\n try {\n const result = await current.onSubmit(values, {\n mode,\n version,\n changes: changes(values),\n });\n if (isItem(result)) {\n const saved = getItemValues(result, fieldNames());\n version = result.version;\n initialValues = saved;\n // Keep what was typed while saving; otherwise take the saved values.\n if (store.getValues() === values) {\n store.setValues(saved);\n }\n } else {\n initialValues = values;\n }\n touched = new Set();\n submitCount = 0;\n status = 'submitted';\n update();\n return { status: 'submitted', result };\n } catch (caught) {\n const problem = getProblem(caught);\n if (problem?.status === 412) {\n status = 'conflict';\n conflict = { current: problem.current ?? null };\n update();\n return { status: 'conflict', current: conflict.current };\n }\n if (problem?.status === 422 && problem.errors !== undefined) {\n status = 'invalid';\n serverErrors = problem.errors;\n update();\n return { status: 'invalid', errors: problem.errors };\n }\n status = 'error';\n error = caught;\n update();\n return { status: 'error', error: caught };\n }\n };\n\n const form: Form = {\n getState() {\n if (unsubscribeStore === null) {\n onStoreChange();\n }\n return state;\n },\n subscribe(listener) {\n listeners.add(listener);\n unsubscribeStore ??= store.subscribe(onStoreChange);\n onStoreChange();\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0 && unsubscribeStore !== null) {\n unsubscribeStore();\n unsubscribeStore = null;\n }\n };\n },\n setOptions(next) {\n const { fields, locales, formatError, onSubmit } = next;\n if (onSubmit !== undefined) {\n current.onSubmit = onSubmit;\n }\n if (formatError !== undefined) {\n current.formatError = formatError;\n }\n // Fields and locales change what is valid: compare them by value, since a render can\n // create new arrays with the same content.\n const changed =\n (fields !== undefined && !isEqual(fields, current.fields)) ||\n (locales !== undefined && !isEqual(locales, current.locales));\n if (fields !== undefined) {\n current.fields = fields;\n }\n if (locales !== undefined) {\n current.locales = locales;\n }\n if (changed) {\n update();\n }\n },\n getValue: (path) => selectValue(form.getState(), path),\n setValue(path, value) {\n store.setValue(path, value);\n if (unsubscribeStore === null) {\n onStoreChange();\n }\n },\n blur(path) {\n if (touched.has(path)) {\n return;\n }\n touched = new Set([...touched, path]);\n update();\n },\n getErrors: (path, getErrorsOptions) =>\n selectErrors(form.getState(), path, getErrorsOptions),\n countErrors: (paths) => selectErrorCount(form.getState(), paths),\n isVisible: (target, parentPath) => selectVisible(form.getState(), target, parentPath),\n validate() {\n submitCount = Math.max(submitCount, 1);\n update();\n return Object.keys(clientErrors(store.getValues(), true)).length === 0;\n },\n setServerErrors(errors) {\n serverErrors = errors;\n update();\n },\n submit() {\n pending ??= runSubmit().finally(() => {\n pending = null;\n });\n return pending;\n },\n reset,\n async resolveConflict(choice) {\n if (status !== 'conflict' || conflict === null) {\n return null;\n }\n const latest = conflict.current;\n if (choice === 'reload') {\n if (latest !== null) {\n reset(getItemValues(latest, fieldNames()), latest.version);\n } else {\n status = 'idle';\n conflict = null;\n update();\n }\n return null;\n }\n if (latest !== null) {\n version = latest.version;\n }\n conflict = null;\n return form.submit();\n },\n };\n return form;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { type Item, type Problem } from '@panneau/protocol';
1
+ import type { Item, Problem } from '@panneau/protocol';
2
2
  import type { Values } from '../values';
3
3
  /**
4
4
  * The problem details carried by an error: the error itself when it is a problem, or its
@@ -1 +1 @@
1
- {"version":3,"file":"problem.d.ts","sourceRoot":"","sources":["../../src/form/problem.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,OAAO,EAAa,MAAM,mBAAmB,CAAC;AAEvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAQzD;AAID,yFAAyF;AACzF,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,IAAI,CAWpD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAM3E"}
1
+ {"version":3,"file":"problem.d.ts","sourceRoot":"","sources":["../../src/form/problem.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAGvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAQzD;AAID,yFAAyF;AACzF,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,IAAI,CAWpD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAM3E"}
@@ -1,4 +1,4 @@
1
- import { isProblem } from "@panneau/protocol";
1
+ import { isProblem } from "@panneau/protocol/runtime";
2
2
  //#region src/form/problem.ts
3
3
  /**
4
4
  * The problem details carried by an error: the error itself when it is a problem, or its
@@ -1 +1 @@
1
- {"version":3,"file":"problem.js","names":[],"sources":["../../src/form/problem.ts"],"sourcesContent":["import { type Item, type Problem, isProblem } from '@panneau/protocol';\n\nimport type { Values } from '../values';\n\n/**\n * The problem details carried by an error: the error itself when it is a problem, or its\n * `problem` property, as on the errors of `@panneau/data`. A custom adapter only needs to throw\n * errors with a `problem` for forms to show validation errors and conflicts.\n */\nexport function getProblem(error: unknown): Problem | null {\n if (isProblem(error)) {\n return error;\n }\n if (error !== null && typeof error === 'object' && 'problem' in error) {\n return isProblem(error.problem) ? error.problem : null;\n }\n return null;\n}\n\nconst ITEM_KEYS = new Set(['id', 'version', 'abilities']);\n\n/** Whether a value looks like an item of the protocol (`id`, `version`, `abilities`). */\nexport function isItem(value: unknown): value is Item {\n if (value === null || typeof value !== 'object') {\n return false;\n }\n const item = value as Record<string, unknown>;\n return (\n typeof item.id === 'string' &&\n (typeof item.version === 'string' || item.version === null) &&\n item.abilities !== null &&\n typeof item.abilities === 'object'\n );\n}\n\n/**\n * The values of an item: every property except `id`, `version` and `abilities`, or only the\n * given field names.\n */\nexport function getItemValues(item: Item, names?: readonly string[]): Values {\n return Object.fromEntries(\n Object.entries(item).filter(([key]) =>\n names !== undefined ? names.includes(key) : !ITEM_KEYS.has(key),\n ),\n );\n}\n"],"mappings":";;;;;;;AASA,SAAgB,WAAW,OAAgC;CACvD,IAAI,UAAU,KAAK,GACf,OAAO;CAEX,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,aAAa,OAC5D,OAAO,UAAU,MAAM,OAAO,IAAI,MAAM,UAAU;CAEtD,OAAO;AACX;AAEA,IAAM,4BAAY,IAAI,IAAI;CAAC;CAAM;CAAW;AAAW,CAAC;;AAGxD,SAAgB,OAAO,OAA+B;CAClD,IAAI,UAAU,QAAQ,OAAO,UAAU,UACnC,OAAO;CAEX,MAAM,OAAO;CACb,OACI,OAAO,KAAK,OAAO,aAClB,OAAO,KAAK,YAAY,YAAY,KAAK,YAAY,SACtD,KAAK,cAAc,QACnB,OAAO,KAAK,cAAc;AAElC;;;;;AAMA,SAAgB,cAAc,MAAY,OAAmC;CACzE,OAAO,OAAO,YACV,OAAO,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,SAC1B,UAAU,KAAA,IAAY,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,IAAI,GAAG,CAClE,CACJ;AACJ"}
1
+ {"version":3,"file":"problem.js","names":[],"sources":["../../src/form/problem.ts"],"sourcesContent":["import type { Item, Problem } from '@panneau/protocol';\nimport { isProblem } from '@panneau/protocol/runtime';\n\nimport type { Values } from '../values';\n\n/**\n * The problem details carried by an error: the error itself when it is a problem, or its\n * `problem` property, as on the errors of `@panneau/data`. A custom adapter only needs to throw\n * errors with a `problem` for forms to show validation errors and conflicts.\n */\nexport function getProblem(error: unknown): Problem | null {\n if (isProblem(error)) {\n return error;\n }\n if (error !== null && typeof error === 'object' && 'problem' in error) {\n return isProblem(error.problem) ? error.problem : null;\n }\n return null;\n}\n\nconst ITEM_KEYS = new Set(['id', 'version', 'abilities']);\n\n/** Whether a value looks like an item of the protocol (`id`, `version`, `abilities`). */\nexport function isItem(value: unknown): value is Item {\n if (value === null || typeof value !== 'object') {\n return false;\n }\n const item = value as Record<string, unknown>;\n return (\n typeof item.id === 'string' &&\n (typeof item.version === 'string' || item.version === null) &&\n item.abilities !== null &&\n typeof item.abilities === 'object'\n );\n}\n\n/**\n * The values of an item: every property except `id`, `version` and `abilities`, or only the\n * given field names.\n */\nexport function getItemValues(item: Item, names?: readonly string[]): Values {\n return Object.fromEntries(\n Object.entries(item).filter(([key]) =>\n names !== undefined ? names.includes(key) : !ITEM_KEYS.has(key),\n ),\n );\n}\n"],"mappings":";;;;;;;AAUA,SAAgB,WAAW,OAAgC;CACvD,IAAI,UAAU,KAAK,GACf,OAAO;CAEX,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,aAAa,OAC5D,OAAO,UAAU,MAAM,OAAO,IAAI,MAAM,UAAU;CAEtD,OAAO;AACX;AAEA,IAAM,4BAAY,IAAI,IAAI;CAAC;CAAM;CAAW;AAAW,CAAC;;AAGxD,SAAgB,OAAO,OAA+B;CAClD,IAAI,UAAU,QAAQ,OAAO,UAAU,UACnC,OAAO;CAEX,MAAM,OAAO;CACb,OACI,OAAO,KAAK,OAAO,aAClB,OAAO,KAAK,YAAY,YAAY,KAAK,YAAY,SACtD,KAAK,cAAc,QACnB,OAAO,KAAK,cAAc;AAElC;;;;;AAMA,SAAgB,cAAc,MAAY,OAAmC;CACzE,OAAO,OAAO,YACV,OAAO,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,SAC1B,UAAU,KAAA,IAAY,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,IAAI,GAAG,CAClE,CACJ;AACJ"}
@@ -1,4 +1,4 @@
1
- import { type Condition } from '@panneau/protocol';
1
+ import type { Condition } from '@panneau/protocol';
2
2
  import type { FormState, GetErrorsOptions } from './form';
3
3
  /** The value at a dot path (`title.fr`). */
4
4
  export declare function selectValue(state: FormState, path: string): unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../src/form/selectors.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,KAAK,SAAS,EAA+B,MAAM,mBAAmB,CAAC;AAGhF,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAE1D,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CACxB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,gBAAqB,GAC/B,MAAM,EAAE,CAKV;AAED,+FAA+F;AAC/F,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAGnF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CACzB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,EAC/C,UAAU,SAAK,GAChB,OAAO,CAKT"}
1
+ {"version":3,"file":"selectors.d.ts","sourceRoot":"","sources":["../../src/form/selectors.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAInD,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAE1D,4CAA4C;AAC5C,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,2FAA2F;AAC3F,wBAAgB,YAAY,CACxB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,gBAAqB,GAC/B,MAAM,EAAE,CAKV;AAED,+FAA+F;AAC/F,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAGnF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CACzB,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE;IAAE,WAAW,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,EAC/C,UAAU,SAAK,GAChB,OAAO,CAKT"}
@@ -1,5 +1,5 @@
1
1
  import { isPathWithin } from "../values.js";
2
- import { evaluateCondition, getValue } from "@panneau/protocol";
2
+ import { evaluateCondition, getValue } from "@panneau/protocol/runtime";
3
3
  //#region src/form/selectors.ts
4
4
  /** The value at a dot path (`title.fr`). */
5
5
  function selectValue(state, path) {
@@ -1 +1 @@
1
- {"version":3,"file":"selectors.js","names":[],"sources":["../../src/form/selectors.ts"],"sourcesContent":["/*\n * Reads of a form state. React components read the state they subscribed to (`useForm()`, or\n * `useSyncExternalStore(form.subscribe, form.getState)`) through these selectors, not through the\n * form's methods: the React Compiler memoizes calls by their arguments, and a method such as\n * `form.getErrors(path)` reads the store, so its memoized result goes stale. A selector takes the\n * state as an argument, so the compiler calls it again when the state changes.\n */\nimport { type Condition, evaluateCondition, getValue } from '@panneau/protocol';\n\nimport { isPathWithin } from '../values';\nimport type { FormState, GetErrorsOptions } from './form';\n\n/** The value at a dot path (`title.fr`). */\nexport function selectValue(state: FormState, path: string): unknown {\n return getValue(state.values, path);\n}\n\n/** Errors at a path, and with `nested` at the paths inside it (`title.fr` for `title`). */\nexport function selectErrors(\n state: FormState,\n path: string,\n options: GetErrorsOptions = {},\n): string[] {\n const { nested = false } = options;\n return Object.entries(state.errors).flatMap(([key, messages]) =>\n key === path || (nested && isPathWithin(key, path)) ? messages : [],\n );\n}\n\n/** The number of paths with errors within the given paths, for counts on tabs and sections. */\nexport function selectErrorCount(state: FormState, paths: readonly string[]): number {\n return Object.keys(state.errors).filter((key) => paths.some((path) => isPathWithin(key, path)))\n .length;\n}\n\n/**\n * Whether a field or a layout node is visible, from its `visibleWhen`. For nested fields, pass\n * the path of the object they belong to (`blocks.2`): conditions are relative to it.\n */\nexport function selectVisible(\n state: FormState,\n target: { visibleWhen?: Condition | undefined },\n parentPath = '',\n): boolean {\n return (\n target.visibleWhen === undefined ||\n evaluateCondition(target.visibleWhen, getValue(state.values, parentPath))\n );\n}\n"],"mappings":";;;;AAaA,SAAgB,YAAY,OAAkB,MAAuB;CACjE,OAAO,SAAS,MAAM,QAAQ,IAAI;AACtC;;AAGA,SAAgB,aACZ,OACA,MACA,UAA4B,CAAC,GACrB;CACR,MAAM,EAAE,SAAS,UAAU;CAC3B,OAAO,OAAO,QAAQ,MAAM,MAAM,CAAC,CAAC,SAAS,CAAC,KAAK,cAC/C,QAAQ,QAAS,UAAU,aAAa,KAAK,IAAI,IAAK,WAAW,CAAA,CACrE;AACJ;;AAGA,SAAgB,iBAAiB,OAAkB,OAAkC;CACjF,OAAO,OAAO,KAAK,MAAM,MAAM,CAAC,CAAC,QAAQ,QAAQ,MAAM,MAAM,SAAS,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,CAC1F;AACT;;;;;AAMA,SAAgB,cACZ,OACA,QACA,aAAa,IACN;CACP,OACI,OAAO,gBAAgB,KAAA,KACvB,kBAAkB,OAAO,aAAa,SAAS,MAAM,QAAQ,UAAU,CAAC;AAEhF"}
1
+ {"version":3,"file":"selectors.js","names":[],"sources":["../../src/form/selectors.ts"],"sourcesContent":["/*\n * Reads of a form state. React components read the state they subscribed to (`useForm()`, or\n * `useSyncExternalStore(form.subscribe, form.getState)`) through these selectors, not through the\n * form's methods: the React Compiler memoizes calls by their arguments, and a method such as\n * `form.getErrors(path)` reads the store, so its memoized result goes stale. A selector takes the\n * state as an argument, so the compiler calls it again when the state changes.\n */\nimport type { Condition } from '@panneau/protocol';\nimport { evaluateCondition, getValue } from '@panneau/protocol/runtime';\n\nimport { isPathWithin } from '../values';\nimport type { FormState, GetErrorsOptions } from './form';\n\n/** The value at a dot path (`title.fr`). */\nexport function selectValue(state: FormState, path: string): unknown {\n return getValue(state.values, path);\n}\n\n/** Errors at a path, and with `nested` at the paths inside it (`title.fr` for `title`). */\nexport function selectErrors(\n state: FormState,\n path: string,\n options: GetErrorsOptions = {},\n): string[] {\n const { nested = false } = options;\n return Object.entries(state.errors).flatMap(([key, messages]) =>\n key === path || (nested && isPathWithin(key, path)) ? messages : [],\n );\n}\n\n/** The number of paths with errors within the given paths, for counts on tabs and sections. */\nexport function selectErrorCount(state: FormState, paths: readonly string[]): number {\n return Object.keys(state.errors).filter((key) => paths.some((path) => isPathWithin(key, path)))\n .length;\n}\n\n/**\n * Whether a field or a layout node is visible, from its `visibleWhen`. For nested fields, pass\n * the path of the object they belong to (`blocks.2`): conditions are relative to it.\n */\nexport function selectVisible(\n state: FormState,\n target: { visibleWhen?: Condition | undefined },\n parentPath = '',\n): boolean {\n return (\n target.visibleWhen === undefined ||\n evaluateCondition(target.visibleWhen, getValue(state.values, parentPath))\n );\n}\n"],"mappings":";;;;AAcA,SAAgB,YAAY,OAAkB,MAAuB;CACjE,OAAO,SAAS,MAAM,QAAQ,IAAI;AACtC;;AAGA,SAAgB,aACZ,OACA,MACA,UAA4B,CAAC,GACrB;CACR,MAAM,EAAE,SAAS,UAAU;CAC3B,OAAO,OAAO,QAAQ,MAAM,MAAM,CAAC,CAAC,SAAS,CAAC,KAAK,cAC/C,QAAQ,QAAS,UAAU,aAAa,KAAK,IAAI,IAAK,WAAW,CAAA,CACrE;AACJ;;AAGA,SAAgB,iBAAiB,OAAkB,OAAkC;CACjF,OAAO,OAAO,KAAK,MAAM,MAAM,CAAC,CAAC,QAAQ,QAAQ,MAAM,MAAM,SAAS,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,CAC1F;AACT;;;;;AAMA,SAAgB,cACZ,OACA,QACA,aAAa,IACN;CACP,OACI,OAAO,gBAAgB,KAAA,KACvB,kBAAkB,OAAO,aAAa,SAAS,MAAM,QAAQ,UAAU,CAAC;AAEhF"}
@@ -1,4 +1,4 @@
1
- import { type FilterValue, type ListQuery, type Selection } from '@panneau/protocol';
1
+ import type { FilterValue, ListQuery, Selection } from '@panneau/protocol';
2
2
  import type { ResolvedList } from '../definition/list';
3
3
  import type { Router } from './router';
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/list/list.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,KAAK,SAAS,EAGjB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAEzF,MAAM,WAAW,SAAS;IACtB,gFAAgF;IAChF,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,aAAa,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,mDAAmD;IACnD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,8EAA8E;AAC9E,MAAM,WAAW,IAAI;IACjB,QAAQ,IAAI,SAAS,CAAC;IACtB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,2DAA2D;IAC3D,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qFAAqF;IACrF,OAAO,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACvC,mFAAmF;IACnF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qGAAqG;IACrG,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAAC;IAC5D,8EAA8E;IAC9E,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wEAAwE;IACxE,UAAU,IAAI,IAAI,CAAC;IACnB,2FAA2F;IAC3F,MAAM,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACrC,2FAA2F;IAC3F,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4DAA4D;IAC5D,iBAAiB,IAAI,IAAI,CAAC;IAC1B,cAAc,IAAI,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,gEAAgE;IAChE,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACxC,gFAAgF;IAChF,YAAY,IAAI,SAAS,GAAG,IAAI,CAAC;CACpC;AAkCD,+BAA+B;AAC/B,wBAAgB,UAAU,CAAC,OAAO,GAAE,WAAgB,GAAG,IAAI,CAgL1D"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/list/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAG3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAEzF,MAAM,WAAW,SAAS;IACtB,gFAAgF;IAChF,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,aAAa,CAAC;CAC5B;AAED,MAAM,WAAW,YAAY;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IACxB,oEAAoE;IACpE,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,mDAAmD;IACnD,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,8EAA8E;AAC9E,MAAM,WAAW,IAAI;IACjB,QAAQ,IAAI,SAAS,CAAC;IACtB,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC5C,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,2DAA2D;IAC3D,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,qFAAqF;IACrF,OAAO,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACvC,mFAAmF;IACnF,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,qGAAqG;IACrG,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAAC;IAC5D,8EAA8E;IAC9E,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,wEAAwE;IACxE,UAAU,IAAI,IAAI,CAAC;IACnB,2FAA2F;IAC3F,MAAM,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACrC,2FAA2F;IAC3F,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IACvC,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4DAA4D;IAC5D,iBAAiB,IAAI,IAAI,CAAC;IAC1B,cAAc,IAAI,IAAI,CAAC;IACvB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,gEAAgE;IAChE,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACxC,gFAAgF;IAChF,YAAY,IAAI,SAAS,GAAG,IAAI,CAAC;CACpC;AAqCD,+BAA+B;AAC/B,wBAAgB,UAAU,CAAC,OAAO,GAAE,WAAgB,GAAG,IAAI,CAgL1D"}
package/dist/list/list.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isEqual } from "../values.js";
2
- import { fromSearchParams, toSearchParams } from "@panneau/protocol";
2
+ import { fromSearchParams, isFilterRange, toSearchParams } from "@panneau/protocol/runtime";
3
3
  //#region src/list/list.ts
4
4
  var EMPTY_SELECTION = {
5
5
  mode: "ids",
@@ -12,7 +12,7 @@ function normalize(query, defaults) {
12
12
  if (query.page !== void 0 && query.page > 1) result.page = query.page;
13
13
  if (query.perPage !== void 0 && query.perPage !== defaults.perPage) result.perPage = query.perPage;
14
14
  if (query.sort !== void 0 && !isEqual(query.sort, defaults.sort)) result.sort = [...query.sort];
15
- const filter = Object.fromEntries(Object.entries(query.filter ?? {}).filter(([, value]) => value !== "" && !(Array.isArray(value) && value.length === 0)));
15
+ const filter = Object.fromEntries(Object.entries(query.filter ?? {}).filter(([, value]) => value !== "" && !(Array.isArray(value) && value.length === 0) && !(isFilterRange(value) && Object.keys(value).length === 0)));
16
16
  if (Object.keys(filter).length > 0) result.filter = filter;
17
17
  if (query.q !== void 0 && query.q !== "") result.q = query.q;
18
18
  if (query.fields !== void 0) result.fields = query.fields;
@@ -1 +1 @@
1
- {"version":3,"file":"list.js","names":[],"sources":["../../src/list/list.ts"],"sourcesContent":["import {\n type FilterValue,\n type ListQuery,\n type Selection,\n fromSearchParams,\n toSearchParams,\n} from '@panneau/protocol';\n\nimport type { ResolvedList } from '../definition/list';\nimport { isEqual } from '../values';\nimport type { Router } from './router';\n\n/**\n * Selected items: some ids, or every item matching the list query (filters and search) except\n * some ids. \"All matching\" covers the items of every page.\n */\nexport type ListSelection =\n { mode: 'ids'; ids: readonly string[] } | { mode: 'all'; except: readonly string[] };\n\nexport interface ListState {\n /** The query to fetch: the list's defaults, overridden by the current state. */\n query: ListQuery;\n selection: ListSelection;\n}\n\nexport interface ListDefaults {\n perPage?: number;\n /** Default sort, as `sort` query values (`['-published_at']`). */\n sort?: string[];\n}\n\nexport interface ListOptions {\n /** A resolved list, for the defaults (`perPage`, `defaultSort`). */\n list?: ResolvedList;\n /** Defaults, over those of `list`. Default values are not written to the URL. */\n defaults?: ListDefaults;\n /** The starting query, when there is no router. */\n query?: ListQuery;\n /** Keeps the query in the URL. The router's query string wins over `query`. */\n router?: Router | null;\n}\n\n/** A headless list: query and selection. React components use `useList()`. */\nexport interface List {\n getState(): ListState;\n subscribe(listener: () => void): () => void;\n setPage(page: number): void;\n /** Sets the page size, and goes back to the first page. */\n setPerPage(perPage: number): void;\n /** Sets the sort (`['-published_at', 'title']`), and goes back to the first page. */\n setSort(sort: readonly string[]): void;\n /** Sorts by a field: ascending, then descending, then back to the default sort. */\n toggleSort(field: string): void;\n /** Sets or removes (`undefined`) a filter, goes back to the first page, and clears the selection. */\n setFilter(id: string, value: FilterValue | undefined): void;\n /** Sets the search, goes back to the first page, and clears the selection. */\n setSearch(q: string): void;\n /** Removes the page, filters, search and sort: back to the defaults. */\n resetQuery(): void;\n /** Adds ids to the selection (removes them from the exceptions in \"all matching\" mode). */\n select(ids: readonly string[]): void;\n /** Removes ids from the selection (adds them to the exceptions in \"all matching\" mode). */\n deselect(ids: readonly string[]): void;\n toggle(id: string): void;\n /** Selects every item matching the query, on every page. */\n selectAllMatching(): void;\n clearSelection(): void;\n isSelected(id: string): boolean;\n /** How many items are selected, given the total of the list. */\n getSelectedCount(total: number): number;\n /** The selection to send with an action, or `null` when nothing is selected. */\n getSelection(): Selection | null;\n}\n\nconst EMPTY_SELECTION: ListSelection = { mode: 'ids', ids: [] };\nconst LIST_PARAM = /^(page|perPage|sort|q|fields|filter\\[.+)$/;\n\n/** Removes the values that are the defaults, so the URL only keeps what the user changed. */\nfunction normalize(query: ListQuery, defaults: Required<ListDefaults>): ListQuery {\n const result: ListQuery = {};\n if (query.page !== undefined && query.page > 1) {\n result.page = query.page;\n }\n if (query.perPage !== undefined && query.perPage !== defaults.perPage) {\n result.perPage = query.perPage;\n }\n if (query.sort !== undefined && !isEqual(query.sort, defaults.sort)) {\n result.sort = [...query.sort];\n }\n const filter = Object.fromEntries(\n Object.entries(query.filter ?? {}).filter(\n ([, value]) => value !== '' && !(Array.isArray(value) && value.length === 0),\n ),\n );\n if (Object.keys(filter).length > 0) {\n result.filter = filter;\n }\n if (query.q !== undefined && query.q !== '') {\n result.q = query.q;\n }\n if (query.fields !== undefined) {\n result.fields = query.fields;\n }\n return result;\n}\n\n/** Creates a headless list. */\nexport function createList(options: ListOptions = {}): List {\n const defaults: Required<ListDefaults> = {\n perPage: options.defaults?.perPage ?? options.list?.perPage ?? 20,\n sort: options.defaults?.sort ?? options.list?.defaultSort ?? [],\n };\n const router = options.router ?? null;\n const readRouter = (): ListQuery =>\n router !== null\n ? normalize(fromSearchParams(new URLSearchParams(router.getSearch())), defaults)\n : {};\n\n let params: ListQuery =\n router !== null ? readRouter() : normalize(options.query ?? {}, defaults);\n let selection: ListSelection = EMPTY_SELECTION;\n\n const compute = (): ListState => ({\n query: {\n ...params,\n page: params.page ?? 1,\n perPage: params.perPage ?? defaults.perPage,\n ...(params.sort !== undefined || defaults.sort.length > 0\n ? { sort: params.sort ?? defaults.sort }\n : {}),\n },\n selection,\n });\n\n let state = compute();\n const listeners = new Set<() => void>();\n const notify = () => {\n state = compute();\n listeners.forEach((listener) => listener());\n };\n\n const writeRouter = (replace: boolean) => {\n if (router === null) {\n return;\n }\n const search = new URLSearchParams(router.getSearch());\n [...search.keys()]\n .filter((key) => LIST_PARAM.test(key))\n .forEach((key) => search.delete(key));\n toSearchParams(params).forEach((value, key) => search.append(key, value));\n const next = search.toString();\n if (next !== router.getSearch()) {\n router.setSearch(next, { replace });\n }\n };\n\n const setParams = (next: ListQuery, { replace = false } = {}) => {\n const normalized = normalize(next, defaults);\n if (isEqual(normalized, params)) {\n return;\n }\n const scopeChanged =\n !isEqual(normalized.filter, params.filter) || normalized.q !== params.q;\n params = normalized;\n if (scopeChanged) {\n selection = EMPTY_SELECTION;\n }\n writeRouter(replace);\n notify();\n };\n\n const setSelection = (next: ListSelection) => {\n selection = next;\n notify();\n };\n\n let unsubscribeRouter: (() => void) | null = null;\n const onRouterChange = () => {\n const next = readRouter();\n if (isEqual(next, params)) {\n return;\n }\n if (!isEqual(next.filter, params.filter) || next.q !== params.q) {\n selection = EMPTY_SELECTION;\n }\n params = next;\n notify();\n };\n\n const list: List = {\n getState() {\n if (router !== null && unsubscribeRouter === null) {\n onRouterChange();\n }\n return state;\n },\n subscribe(listener) {\n listeners.add(listener);\n if (router !== null && unsubscribeRouter === null) {\n unsubscribeRouter = router.subscribe(onRouterChange);\n onRouterChange();\n }\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0 && unsubscribeRouter !== null) {\n unsubscribeRouter();\n unsubscribeRouter = null;\n }\n };\n },\n setPage: (page) => setParams({ ...params, page }),\n setPerPage: (perPage) => setParams({ ...params, perPage, page: 1 }),\n setSort: (sort) => setParams({ ...params, sort: [...sort], page: 1 }),\n toggleSort(field) {\n const sort = state.query.sort ?? [];\n if (sort[0] === field) {\n setParams({ ...params, sort: [`-${field}`], page: 1 });\n } else if (sort[0] === `-${field}` && params.sort !== undefined) {\n const { sort: _sort, ...rest } = params;\n setParams({ ...rest, page: 1 });\n } else {\n setParams({ ...params, sort: [field], page: 1 });\n }\n },\n setFilter(id, value) {\n const { [id]: _removed, ...filter } = params.filter ?? {};\n setParams({\n ...params,\n filter: value === undefined ? filter : { ...filter, [id]: value },\n page: 1,\n });\n },\n setSearch: (q) => setParams({ ...params, q, page: 1 }, { replace: true }),\n resetQuery: () => setParams({}),\n select(ids) {\n if (selection.mode === 'all') {\n setSelection({\n mode: 'all',\n except: selection.except.filter((id) => !ids.includes(id)),\n });\n } else {\n setSelection({ mode: 'ids', ids: [...new Set([...selection.ids, ...ids])] });\n }\n },\n deselect(ids) {\n if (selection.mode === 'all') {\n setSelection({ mode: 'all', except: [...new Set([...selection.except, ...ids])] });\n } else {\n setSelection({ mode: 'ids', ids: selection.ids.filter((id) => !ids.includes(id)) });\n }\n },\n toggle(id) {\n if (list.isSelected(id)) {\n list.deselect([id]);\n } else {\n list.select([id]);\n }\n },\n selectAllMatching: () => setSelection({ mode: 'all', except: [] }),\n clearSelection: () => setSelection(EMPTY_SELECTION),\n isSelected: (id) =>\n selection.mode === 'all' ? !selection.except.includes(id) : selection.ids.includes(id),\n getSelectedCount: (total) =>\n selection.mode === 'all'\n ? Math.max(0, total - selection.except.length)\n : selection.ids.length,\n getSelection() {\n if (selection.mode === 'ids') {\n return selection.ids.length > 0 ? { ids: [...selection.ids] } : null;\n }\n const query: { filter?: Record<string, unknown>; q?: string } = {};\n if (params.filter !== undefined) {\n query.filter = params.filter;\n }\n if (params.q !== undefined) {\n query.q = params.q;\n }\n return selection.except.length > 0\n ? { query, except: [...selection.except] }\n : { query };\n },\n };\n return list;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"list.js","names":[],"sources":["../../src/list/list.ts"],"sourcesContent":["import type { FilterValue, ListQuery, Selection } from '@panneau/protocol';\nimport { fromSearchParams, isFilterRange, toSearchParams } from '@panneau/protocol/runtime';\n\nimport type { ResolvedList } from '../definition/list';\nimport { isEqual } from '../values';\nimport type { Router } from './router';\n\n/**\n * Selected items: some ids, or every item matching the list query (filters and search) except\n * some ids. \"All matching\" covers the items of every page.\n */\nexport type ListSelection =\n { mode: 'ids'; ids: readonly string[] } | { mode: 'all'; except: readonly string[] };\n\nexport interface ListState {\n /** The query to fetch: the list's defaults, overridden by the current state. */\n query: ListQuery;\n selection: ListSelection;\n}\n\nexport interface ListDefaults {\n perPage?: number;\n /** Default sort, as `sort` query values (`['-published_at']`). */\n sort?: string[];\n}\n\nexport interface ListOptions {\n /** A resolved list, for the defaults (`perPage`, `defaultSort`). */\n list?: ResolvedList;\n /** Defaults, over those of `list`. Default values are not written to the URL. */\n defaults?: ListDefaults;\n /** The starting query, when there is no router. */\n query?: ListQuery;\n /** Keeps the query in the URL. The router's query string wins over `query`. */\n router?: Router | null;\n}\n\n/** A headless list: query and selection. React components use `useList()`. */\nexport interface List {\n getState(): ListState;\n subscribe(listener: () => void): () => void;\n setPage(page: number): void;\n /** Sets the page size, and goes back to the first page. */\n setPerPage(perPage: number): void;\n /** Sets the sort (`['-published_at', 'title']`), and goes back to the first page. */\n setSort(sort: readonly string[]): void;\n /** Sorts by a field: ascending, then descending, then back to the default sort. */\n toggleSort(field: string): void;\n /** Sets or removes (`undefined`) a filter, goes back to the first page, and clears the selection. */\n setFilter(id: string, value: FilterValue | undefined): void;\n /** Sets the search, goes back to the first page, and clears the selection. */\n setSearch(q: string): void;\n /** Removes the page, filters, search and sort: back to the defaults. */\n resetQuery(): void;\n /** Adds ids to the selection (removes them from the exceptions in \"all matching\" mode). */\n select(ids: readonly string[]): void;\n /** Removes ids from the selection (adds them to the exceptions in \"all matching\" mode). */\n deselect(ids: readonly string[]): void;\n toggle(id: string): void;\n /** Selects every item matching the query, on every page. */\n selectAllMatching(): void;\n clearSelection(): void;\n isSelected(id: string): boolean;\n /** How many items are selected, given the total of the list. */\n getSelectedCount(total: number): number;\n /** The selection to send with an action, or `null` when nothing is selected. */\n getSelection(): Selection | null;\n}\n\nconst EMPTY_SELECTION: ListSelection = { mode: 'ids', ids: [] };\nconst LIST_PARAM = /^(page|perPage|sort|q|fields|filter\\[.+)$/;\n\n/** Removes the values that are the defaults, so the URL only keeps what the user changed. */\nfunction normalize(query: ListQuery, defaults: Required<ListDefaults>): ListQuery {\n const result: ListQuery = {};\n if (query.page !== undefined && query.page > 1) {\n result.page = query.page;\n }\n if (query.perPage !== undefined && query.perPage !== defaults.perPage) {\n result.perPage = query.perPage;\n }\n if (query.sort !== undefined && !isEqual(query.sort, defaults.sort)) {\n result.sort = [...query.sort];\n }\n const filter = Object.fromEntries(\n Object.entries(query.filter ?? {}).filter(\n ([, value]) =>\n value !== '' &&\n !(Array.isArray(value) && value.length === 0) &&\n !(isFilterRange(value) && Object.keys(value).length === 0),\n ),\n );\n if (Object.keys(filter).length > 0) {\n result.filter = filter;\n }\n if (query.q !== undefined && query.q !== '') {\n result.q = query.q;\n }\n if (query.fields !== undefined) {\n result.fields = query.fields;\n }\n return result;\n}\n\n/** Creates a headless list. */\nexport function createList(options: ListOptions = {}): List {\n const defaults: Required<ListDefaults> = {\n perPage: options.defaults?.perPage ?? options.list?.perPage ?? 20,\n sort: options.defaults?.sort ?? options.list?.defaultSort ?? [],\n };\n const router = options.router ?? null;\n const readRouter = (): ListQuery =>\n router !== null\n ? normalize(fromSearchParams(new URLSearchParams(router.getSearch())), defaults)\n : {};\n\n let params: ListQuery =\n router !== null ? readRouter() : normalize(options.query ?? {}, defaults);\n let selection: ListSelection = EMPTY_SELECTION;\n\n const compute = (): ListState => ({\n query: {\n ...params,\n page: params.page ?? 1,\n perPage: params.perPage ?? defaults.perPage,\n ...(params.sort !== undefined || defaults.sort.length > 0\n ? { sort: params.sort ?? defaults.sort }\n : {}),\n },\n selection,\n });\n\n let state = compute();\n const listeners = new Set<() => void>();\n const notify = () => {\n state = compute();\n listeners.forEach((listener) => listener());\n };\n\n const writeRouter = (replace: boolean) => {\n if (router === null) {\n return;\n }\n const search = new URLSearchParams(router.getSearch());\n [...search.keys()]\n .filter((key) => LIST_PARAM.test(key))\n .forEach((key) => search.delete(key));\n toSearchParams(params).forEach((value, key) => search.append(key, value));\n const next = search.toString();\n if (next !== router.getSearch()) {\n router.setSearch(next, { replace });\n }\n };\n\n const setParams = (next: ListQuery, { replace = false } = {}) => {\n const normalized = normalize(next, defaults);\n if (isEqual(normalized, params)) {\n return;\n }\n const scopeChanged =\n !isEqual(normalized.filter, params.filter) || normalized.q !== params.q;\n params = normalized;\n if (scopeChanged) {\n selection = EMPTY_SELECTION;\n }\n writeRouter(replace);\n notify();\n };\n\n const setSelection = (next: ListSelection) => {\n selection = next;\n notify();\n };\n\n let unsubscribeRouter: (() => void) | null = null;\n const onRouterChange = () => {\n const next = readRouter();\n if (isEqual(next, params)) {\n return;\n }\n if (!isEqual(next.filter, params.filter) || next.q !== params.q) {\n selection = EMPTY_SELECTION;\n }\n params = next;\n notify();\n };\n\n const list: List = {\n getState() {\n if (router !== null && unsubscribeRouter === null) {\n onRouterChange();\n }\n return state;\n },\n subscribe(listener) {\n listeners.add(listener);\n if (router !== null && unsubscribeRouter === null) {\n unsubscribeRouter = router.subscribe(onRouterChange);\n onRouterChange();\n }\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0 && unsubscribeRouter !== null) {\n unsubscribeRouter();\n unsubscribeRouter = null;\n }\n };\n },\n setPage: (page) => setParams({ ...params, page }),\n setPerPage: (perPage) => setParams({ ...params, perPage, page: 1 }),\n setSort: (sort) => setParams({ ...params, sort: [...sort], page: 1 }),\n toggleSort(field) {\n const sort = state.query.sort ?? [];\n if (sort[0] === field) {\n setParams({ ...params, sort: [`-${field}`], page: 1 });\n } else if (sort[0] === `-${field}` && params.sort !== undefined) {\n const { sort: _sort, ...rest } = params;\n setParams({ ...rest, page: 1 });\n } else {\n setParams({ ...params, sort: [field], page: 1 });\n }\n },\n setFilter(id, value) {\n const { [id]: _removed, ...filter } = params.filter ?? {};\n setParams({\n ...params,\n filter: value === undefined ? filter : { ...filter, [id]: value },\n page: 1,\n });\n },\n setSearch: (q) => setParams({ ...params, q, page: 1 }, { replace: true }),\n resetQuery: () => setParams({}),\n select(ids) {\n if (selection.mode === 'all') {\n setSelection({\n mode: 'all',\n except: selection.except.filter((id) => !ids.includes(id)),\n });\n } else {\n setSelection({ mode: 'ids', ids: [...new Set([...selection.ids, ...ids])] });\n }\n },\n deselect(ids) {\n if (selection.mode === 'all') {\n setSelection({ mode: 'all', except: [...new Set([...selection.except, ...ids])] });\n } else {\n setSelection({ mode: 'ids', ids: selection.ids.filter((id) => !ids.includes(id)) });\n }\n },\n toggle(id) {\n if (list.isSelected(id)) {\n list.deselect([id]);\n } else {\n list.select([id]);\n }\n },\n selectAllMatching: () => setSelection({ mode: 'all', except: [] }),\n clearSelection: () => setSelection(EMPTY_SELECTION),\n isSelected: (id) =>\n selection.mode === 'all' ? !selection.except.includes(id) : selection.ids.includes(id),\n getSelectedCount: (total) =>\n selection.mode === 'all'\n ? Math.max(0, total - selection.except.length)\n : selection.ids.length,\n getSelection() {\n if (selection.mode === 'ids') {\n return selection.ids.length > 0 ? { ids: [...selection.ids] } : null;\n }\n const query: { filter?: Record<string, unknown>; q?: string } = {};\n if (params.filter !== undefined) {\n query.filter = params.filter;\n }\n if (params.q !== undefined) {\n query.q = params.q;\n }\n return selection.except.length > 0\n ? { query, except: [...selection.except] }\n : { query };\n },\n };\n return list;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panneau/core",
3
- "version": "5.0.0-alpha.0",
3
+ "version": "5.0.0-alpha.1",
4
4
  "description": "The engine of Panneau: plugin registry, definition to forms and lists, form and list state, with React hooks. No UI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,6 +33,6 @@
33
33
  "react": "^19.2.0"
34
34
  },
35
35
  "dependencies": {
36
- "@panneau/protocol": "5.0.0-alpha.0"
36
+ "@panneau/protocol": "5.0.0-alpha.1"
37
37
  }
38
38
  }