@panneau/core 4.1.0 → 5.0.0-alpha.0
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.
- package/README.md +83 -0
- package/dist/definition/abilities.d.ts +21 -0
- package/dist/definition/abilities.d.ts.map +1 -0
- package/dist/definition/abilities.js +47 -0
- package/dist/definition/abilities.js.map +1 -0
- package/dist/definition/form.d.ts +31 -0
- package/dist/definition/form.d.ts.map +1 -0
- package/dist/definition/form.js +54 -0
- package/dist/definition/form.js.map +1 -0
- package/dist/definition/list.d.ts +44 -0
- package/dist/definition/list.d.ts.map +1 -0
- package/dist/definition/list.js +70 -0
- package/dist/definition/list.js.map +1 -0
- package/dist/definition/resource.d.ts +21 -0
- package/dist/definition/resource.d.ts.map +1 -0
- package/dist/definition/resource.js +24 -0
- package/dist/definition/resource.js.map +1 -0
- package/dist/form/form.d.ts +148 -0
- package/dist/form/form.d.ts.map +1 -0
- package/dist/form/form.js +239 -0
- package/dist/form/form.js.map +1 -0
- package/dist/form/messages.d.ts +7 -0
- package/dist/form/messages.d.ts.map +1 -0
- package/dist/form/messages.js +25 -0
- package/dist/form/messages.js.map +1 -0
- package/dist/form/problem.d.ts +16 -0
- package/dist/form/problem.d.ts.map +1 -0
- package/dist/form/problem.js +34 -0
- package/dist/form/problem.js.map +1 -0
- package/dist/form/selectors.d.ts +16 -0
- package/dist/form/selectors.d.ts.map +1 -0
- package/dist/form/selectors.js +27 -0
- package/dist/form/selectors.js.map +1 -0
- package/dist/form/store.d.ts +19 -0
- package/dist/form/store.d.ts.map +1 -0
- package/dist/form/store.js +29 -0
- package/dist/form/store.js.map +1 -0
- package/dist/index.d.ts +17 -651
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -536
- package/dist/list/list.d.ts +68 -0
- package/dist/list/list.d.ts.map +1 -0
- package/dist/list/list.js +191 -0
- package/dist/list/list.js.map +1 -0
- package/dist/list/router.d.ts +26 -0
- package/dist/list/router.d.ts.map +1 -0
- package/dist/list/router.js +54 -0
- package/dist/list/router.js.map +1 -0
- package/dist/plugins.d.ts +73 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +54 -0
- package/dist/plugins.js.map +1 -0
- package/dist/react/provider.d.ts +38 -0
- package/dist/react/provider.d.ts.map +1 -0
- package/dist/react/provider.js +63 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/useForm.d.ts +26 -0
- package/dist/react/useForm.d.ts.map +1 -0
- package/dist/react/useForm.js +115 -0
- package/dist/react/useForm.js.map +1 -0
- package/dist/react/useList.d.ts +11 -0
- package/dist/react/useList.d.ts.map +1 -0
- package/dist/react/useList.js +47 -0
- package/dist/react/useList.js.map +1 -0
- package/dist/values.d.ts +12 -0
- package/dist/values.d.ts.map +1 -0
- package/dist/values.js +44 -0
- package/dist/values.js.map +1 -0
- package/package.json +19 -81
- package/dist/contexts.d.ts +0 -638
- package/dist/contexts.js +0 -1754
- package/dist/hooks.d.ts +0 -492
- package/dist/hooks.js +0 -2146
- package/dist/utils.d.ts +0 -322
- package/dist/utils.js +0 -530
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# @panneau/core
|
|
2
|
+
|
|
3
|
+
The engine of Panneau: plugin registry, definition to forms and lists, form and list state, with React hooks. It renders no UI, and only relies on [`@panneau/protocol`](../protocol): data comes from the caller, or from [`@panneau/data`](../data).
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
import { getItemValues, resolveForm, useForm, useResource } from '@panneau/core';
|
|
7
|
+
|
|
8
|
+
function EditPage({ item, onSave }) {
|
|
9
|
+
const { resource } = useResource('pages');
|
|
10
|
+
const { fields, layout } = resolveForm(resource, { mode: 'update' });
|
|
11
|
+
const { form, values, errors, status, conflict } = useForm({
|
|
12
|
+
fields,
|
|
13
|
+
values: getItemValues(
|
|
14
|
+
item,
|
|
15
|
+
fields.map((field) => field.name),
|
|
16
|
+
),
|
|
17
|
+
version: item.version,
|
|
18
|
+
locales: ['fr', 'en'],
|
|
19
|
+
onSubmit: (values, { version, changes }) => onSave(changes, version),
|
|
20
|
+
});
|
|
21
|
+
// Render `layout` with fields from the plugins; `form.setValue('title.fr', …)` on change.
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Export | Content |
|
|
26
|
+
| --------------- | ------------------------------------------------------------ |
|
|
27
|
+
| `@panneau/core` | Plugins, definition engine, form and list state, React hooks |
|
|
28
|
+
|
|
29
|
+
## Plugins
|
|
30
|
+
|
|
31
|
+
`definePlugin()` declares what a plugin adds, keyed by the names the definition uses: `fields`, `displays`, `listLayouts`, `actions` (handlers of `client` actions), `pages`, `widgets`, `components` (layout nodes), `locales` and `themes`.
|
|
32
|
+
|
|
33
|
+
`PanneauProvider` takes plugins, the definition and a router. Nested providers add their plugins to those of their parent, and later plugins override earlier ones, so a project can replace a built-in field. `usePlugins()` returns the merged registry, and `loadMessages(registry, locale)` the merged messages of a locale.
|
|
34
|
+
|
|
35
|
+
## Definition engine
|
|
36
|
+
|
|
37
|
+
- **`resolveForm(resource, { mode, type })`**: the fields of the form and its layout. Field nodes that name unknown fields are dropped, and fields that no node references are appended at the end. `create` drops `updateOnly` fields; `update` makes `createOnly` fields read-only. `type` adds the fields of an item type.
|
|
38
|
+
- **`resolveList(resource)`**: columns (label, display and sortability from their field), filters, sorts, default sort, layouts, page size, and the actions the user can run, by placement. Without columns, the list shows the item title and the next fields.
|
|
39
|
+
- **Abilities:** `can(abilities, 'create')`, `isActionAllowedOn(action, item)`, `partitionByAction(action, items)`. Without abilities, everything is allowed and the backend decides; with abilities, only what is `true`.
|
|
40
|
+
- **`useResource(id)`** resolves a resource of the provider's definition.
|
|
41
|
+
|
|
42
|
+
## Forms
|
|
43
|
+
|
|
44
|
+
`createForm(options)`, or `useForm(options)` in a component, manages:
|
|
45
|
+
|
|
46
|
+
- **Values**, nested and localized, set by dot path (`form.setValue('title.fr', 'Accueil')`), and whether they changed (`dirty`).
|
|
47
|
+
- **Client validation** with the portable rules (`checkValues()`): errors of a field show once it was blurred (`form.blur(path)`), and every error shows after a submit. Messages are English by default; `formatError` translates them.
|
|
48
|
+
- **Server errors**, by dot path, from a `422`. They go away when their value changes.
|
|
49
|
+
- **Visibility:** `selectVisible(state, field)` evaluates `visibleWhen`. Hidden fields are not validated.
|
|
50
|
+
- **Submit:** `onSubmit(values, { version, changes, mode })`. When it returns an item, the form continues from it, with its new version. When it throws an error that carries problem details (the errors of `@panneau/data` do), a `412` puts the form in the `conflict` status with the current item. `form.resolveConflict('reload')` takes their version; `'overwrite'` submits the local changes again over it.
|
|
51
|
+
|
|
52
|
+
Values live in a `FormStore`. The default one is local; a shared store (Yjs) can replace it without changing the fields. `useFormField(form, path)` gives the props of a controlled field.
|
|
53
|
+
|
|
54
|
+
### Reading the state in components
|
|
55
|
+
|
|
56
|
+
Components read the state they subscribed to (what `useForm()` returns, or `useSyncExternalStore(form.subscribe, form.getState)`) with pure selectors:
|
|
57
|
+
|
|
58
|
+
| Selector | Returns |
|
|
59
|
+
| ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| `selectValue(state, path)` | The value at a dot path |
|
|
61
|
+
| `selectErrors(state, path, { nested })` | The errors at a path, and with `nested` those of the paths inside it (`title.fr` for `title`) |
|
|
62
|
+
| `selectErrorCount(state, paths)` | The number of paths with errors within some paths, for counts on tabs and sections |
|
|
63
|
+
| `selectVisible(state, target, parentPath)` | Whether a field or layout node is visible; conditions of nested fields are relative to `parentPath` |
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
function TitleField({ form }: { form: Form }) {
|
|
67
|
+
const state = useSyncExternalStore(form.subscribe, form.getState, form.getState);
|
|
68
|
+
const errors = selectErrors(state, 'title', { nested: true });
|
|
69
|
+
// …
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The form's methods that read the state (`form.getValue()`, `form.getErrors()`, `form.countErrors()`, `form.isVisible()`) are **not React APIs**: the React Compiler memoizes calls by their arguments, and these arguments don't change when the state does, so the results go stale (the error of a field doesn't show after a blur). Use them outside of components: in event handlers, effects, tests or plain scripts.
|
|
74
|
+
|
|
75
|
+
## Lists
|
|
76
|
+
|
|
77
|
+
`createList(options)`, or `useList(options)`, manages:
|
|
78
|
+
|
|
79
|
+
- **The query**: page, page size, sort, filters and search, with the list's defaults. Changing filters, search, sort or page size goes back to the first page.
|
|
80
|
+
- **The selection**: some ids, kept across pages, or every item matching the query except some (`selectAllMatching()`). `getSelection()` returns the protocol's `Selection` for an action. Changing filters or search clears it.
|
|
81
|
+
- **URL sync** through a `Router`: `getSearch()`, `setSearch()` and `subscribe()`. Only values that differ from the defaults are written, next to the other parameters. `createHistoryRouter()` uses `window.history`; `createMemoryRouter()` keeps it in memory. `useList()` uses the provider's router unless given one, or `router: null`.
|
|
82
|
+
|
|
83
|
+
The package layout, scripts and API reports are explained in [`docs/packages.md`](../../docs/packages.md).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Abilities, Action, ActionPlacement, Item } from '@panneau/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* Whether a set of abilities allows something. Without abilities (a backend that sends none),
|
|
4
|
+
* everything is allowed and the backend decides. With abilities, only what is `true` is allowed.
|
|
5
|
+
*/
|
|
6
|
+
export declare function can(abilities: Abilities | undefined, ability: string): boolean;
|
|
7
|
+
/** Whether the current user can run an action on the resource (`abilities.run`). */
|
|
8
|
+
export declare function canRunAction(action: Action): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Whether an action is available on an item: the user can run it, and the item lists its id in
|
|
11
|
+
* its abilities. Links to show and edit are allowed unless the item says otherwise.
|
|
12
|
+
*/
|
|
13
|
+
export declare function isActionAllowedOn(action: Action, item: Item): boolean;
|
|
14
|
+
/** Splits the items of a selection by whether an action is allowed on them. */
|
|
15
|
+
export declare function partitionByAction(action: Action, items: readonly Item[]): {
|
|
16
|
+
allowed: Item[];
|
|
17
|
+
skipped: Item[];
|
|
18
|
+
};
|
|
19
|
+
/** Where an action appears when it doesn't say: rows and forms for items, the toolbar otherwise. */
|
|
20
|
+
export declare function getActionPlacements(action: Action): ActionPlacement[];
|
|
21
|
+
//# sourceMappingURL=abilities.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abilities.d.ts","sourceRoot":"","sources":["../../src/definition/abilities.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAElF;;;GAGG;AACH,wBAAgB,GAAG,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAE9E;AAED,oFAAoF;AACpF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAQrE;AAED,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAC7B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,SAAS,IAAI,EAAE,GACvB;IAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAAC,OAAO,EAAE,IAAI,EAAE,CAAA;CAAE,CAKtC;AAED,oGAAoG;AACpG,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAcrE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/definition/abilities.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether a set of abilities allows something. Without abilities (a backend that sends none),
|
|
4
|
+
* everything is allowed and the backend decides. With abilities, only what is `true` is allowed.
|
|
5
|
+
*/
|
|
6
|
+
function can(abilities, ability) {
|
|
7
|
+
return abilities === void 0 || abilities[ability] === true;
|
|
8
|
+
}
|
|
9
|
+
/** Whether the current user can run an action on the resource (`abilities.run`). */
|
|
10
|
+
function canRunAction(action) {
|
|
11
|
+
return action.abilities?.run !== false;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Whether an action is available on an item: the user can run it, and the item lists its id in
|
|
15
|
+
* its abilities. Links to show and edit are allowed unless the item says otherwise.
|
|
16
|
+
*/
|
|
17
|
+
function isActionAllowedOn(action, item) {
|
|
18
|
+
if (!canRunAction(action)) return false;
|
|
19
|
+
if (action.kind === "link") return item.abilities[action.id] !== false;
|
|
20
|
+
return item.abilities[action.id] === true;
|
|
21
|
+
}
|
|
22
|
+
/** Splits the items of a selection by whether an action is allowed on them. */
|
|
23
|
+
function partitionByAction(action, items) {
|
|
24
|
+
const allowed = [];
|
|
25
|
+
const skipped = [];
|
|
26
|
+
items.forEach((item) => (isActionAllowedOn(action, item) ? allowed : skipped).push(item));
|
|
27
|
+
return {
|
|
28
|
+
allowed,
|
|
29
|
+
skipped
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Where an action appears when it doesn't say: rows and forms for items, the toolbar otherwise. */
|
|
33
|
+
function getActionPlacements(action) {
|
|
34
|
+
if (action.placement !== void 0) return action.placement;
|
|
35
|
+
const placements = /* @__PURE__ */ new Set();
|
|
36
|
+
action.scope.forEach((scope) => {
|
|
37
|
+
if (scope === "item") {
|
|
38
|
+
placements.add("row");
|
|
39
|
+
placements.add("form");
|
|
40
|
+
} else placements.add("toolbar");
|
|
41
|
+
});
|
|
42
|
+
return [...placements];
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { can, canRunAction, getActionPlacements, isActionAllowedOn, partitionByAction };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=abilities.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"abilities.js","names":[],"sources":["../../src/definition/abilities.ts"],"sourcesContent":["import type { Abilities, Action, ActionPlacement, Item } from '@panneau/protocol';\n\n/**\n * Whether a set of abilities allows something. Without abilities (a backend that sends none),\n * everything is allowed and the backend decides. With abilities, only what is `true` is allowed.\n */\nexport function can(abilities: Abilities | undefined, ability: string): boolean {\n return abilities === undefined || abilities[ability] === true;\n}\n\n/** Whether the current user can run an action on the resource (`abilities.run`). */\nexport function canRunAction(action: Action): boolean {\n return action.abilities?.run !== false;\n}\n\n/**\n * Whether an action is available on an item: the user can run it, and the item lists its id in\n * its abilities. Links to show and edit are allowed unless the item says otherwise.\n */\nexport function isActionAllowedOn(action: Action, item: Item): boolean {\n if (!canRunAction(action)) {\n return false;\n }\n if (action.kind === 'link') {\n return item.abilities[action.id] !== false;\n }\n return item.abilities[action.id] === true;\n}\n\n/** Splits the items of a selection by whether an action is allowed on them. */\nexport function partitionByAction(\n action: Action,\n items: readonly Item[],\n): { allowed: Item[]; skipped: Item[] } {\n const allowed: Item[] = [];\n const skipped: Item[] = [];\n items.forEach((item) => (isActionAllowedOn(action, item) ? allowed : skipped).push(item));\n return { allowed, skipped };\n}\n\n/** Where an action appears when it doesn't say: rows and forms for items, the toolbar otherwise. */\nexport function getActionPlacements(action: Action): ActionPlacement[] {\n if (action.placement !== undefined) {\n return action.placement;\n }\n const placements = new Set<ActionPlacement>();\n action.scope.forEach((scope) => {\n if (scope === 'item') {\n placements.add('row');\n placements.add('form');\n } else {\n placements.add('toolbar');\n }\n });\n return [...placements];\n}\n"],"mappings":";;;;;AAMA,SAAgB,IAAI,WAAkC,SAA0B;CAC5E,OAAO,cAAc,KAAA,KAAa,UAAU,aAAa;AAC7D;;AAGA,SAAgB,aAAa,QAAyB;CAClD,OAAO,OAAO,WAAW,QAAQ;AACrC;;;;;AAMA,SAAgB,kBAAkB,QAAgB,MAAqB;CACnE,IAAI,CAAC,aAAa,MAAM,GACpB,OAAO;CAEX,IAAI,OAAO,SAAS,QAChB,OAAO,KAAK,UAAU,OAAO,QAAQ;CAEzC,OAAO,KAAK,UAAU,OAAO,QAAQ;AACzC;;AAGA,SAAgB,kBACZ,QACA,OACoC;CACpC,MAAM,UAAkB,CAAA;CACxB,MAAM,UAAkB,CAAA;CACxB,MAAM,SAAS,UAAU,kBAAkB,QAAQ,IAAI,IAAI,UAAU,QAAA,CAAS,KAAK,IAAI,CAAC;CACxF,OAAO;EAAE;EAAS;CAAQ;AAC9B;;AAGA,SAAgB,oBAAoB,QAAmC;CACnE,IAAI,OAAO,cAAc,KAAA,GACrB,OAAO,OAAO;CAElB,MAAM,6BAAa,IAAI,IAAqB;CAC5C,OAAO,MAAM,SAAS,UAAU;EAC5B,IAAI,UAAU,QAAQ;GAClB,WAAW,IAAI,KAAK;GACpB,WAAW,IAAI,MAAM;EACzB,OACI,WAAW,IAAI,SAAS;CAEhC,CAAC;CACD,OAAO,CAAC,GAAG,UAAU;AACzB"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Field, LayoutNode, Resource } from '@panneau/protocol';
|
|
2
|
+
/** Whether a form creates an item or updates one. */
|
|
3
|
+
export type FormMode = 'create' | 'update';
|
|
4
|
+
/** A form ready to render: its fields and a layout that references every one of them. */
|
|
5
|
+
export interface ResolvedForm {
|
|
6
|
+
/** The fields of the form, in definition order. */
|
|
7
|
+
fields: Field[];
|
|
8
|
+
/** The fields, by name. */
|
|
9
|
+
fieldsByName: ReadonlyMap<string, Field>;
|
|
10
|
+
/**
|
|
11
|
+
* The layout tree. Field nodes that name an unknown field are removed, and fields that no
|
|
12
|
+
* node references are appended at the end, as `field` nodes.
|
|
13
|
+
*/
|
|
14
|
+
layout: LayoutNode[];
|
|
15
|
+
}
|
|
16
|
+
export interface ResolveFormOptions {
|
|
17
|
+
/**
|
|
18
|
+
* `create` removes `updateOnly` fields; `update` makes `createOnly` fields read-only. Without a
|
|
19
|
+
* mode, every field is kept as is.
|
|
20
|
+
*/
|
|
21
|
+
mode?: FormMode;
|
|
22
|
+
/** The type of the item, among the resource's `types`: its fields are added. */
|
|
23
|
+
type?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Resolves fields and a layout into a form. */
|
|
26
|
+
export declare function resolveFields(fields: readonly Field[], layout?: readonly LayoutNode[], { mode }?: Pick<ResolveFormOptions, 'mode'>): ResolvedForm;
|
|
27
|
+
/** Resolves the form of a resource: its fields (plus those of the item's type) and its layout. */
|
|
28
|
+
export declare function resolveForm(resource: Resource, options?: ResolveFormOptions): ResolvedForm;
|
|
29
|
+
/** The field names a layout references, in order. */
|
|
30
|
+
export declare function getLayoutFieldNames(layout: readonly LayoutNode[]): string[];
|
|
31
|
+
//# sourceMappingURL=form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/definition/form.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAErE,qDAAqD;AACrD,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3C,yFAAyF;AACzF,MAAM,WAAW,YAAY;IACzB,mDAAmD;IACnD,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,2BAA2B;IAC3B,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACzC;;;OAGG;IACH,MAAM,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,CAAC;CACjB;AA2BD,gDAAgD;AAChD,wBAAgB,aAAa,CACzB,MAAM,EAAE,SAAS,KAAK,EAAE,EACxB,MAAM,GAAE,SAAS,UAAU,EAAO,EAClC,EAAE,IAAI,EAAE,GAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,CAAM,GAChD,YAAY,CAiBd;AAED,kGAAkG;AAClG,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,GAAE,kBAAuB,GAAG,YAAY,CAM9F;AAED,qDAAqD;AACrD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,MAAM,EAAE,CAE3E"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//#region src/definition/form.ts
|
|
2
|
+
function referencedNames(nodes, names = /* @__PURE__ */ new Set()) {
|
|
3
|
+
nodes.forEach((node) => {
|
|
4
|
+
if (node.type === "field" && node.name !== void 0) names.add(node.name);
|
|
5
|
+
referencedNames(node.children ?? [], names);
|
|
6
|
+
});
|
|
7
|
+
return names;
|
|
8
|
+
}
|
|
9
|
+
function pruneLayout(nodes, fields) {
|
|
10
|
+
return nodes.flatMap((node) => {
|
|
11
|
+
if (node.type === "field" && (node.name === void 0 || !fields.has(node.name))) return [];
|
|
12
|
+
if (node.children === void 0) return [node];
|
|
13
|
+
return [{
|
|
14
|
+
...node,
|
|
15
|
+
children: pruneLayout(node.children, fields)
|
|
16
|
+
}];
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/** Resolves fields and a layout into a form. */
|
|
20
|
+
function resolveFields(fields, layout = [], { mode } = {}) {
|
|
21
|
+
const resolved = fields.flatMap((field) => {
|
|
22
|
+
if (mode === "create" && field.updateOnly === true) return [];
|
|
23
|
+
if (mode === "update" && field.createOnly === true) return [{
|
|
24
|
+
...field,
|
|
25
|
+
readOnly: true
|
|
26
|
+
}];
|
|
27
|
+
return [field];
|
|
28
|
+
});
|
|
29
|
+
const fieldsByName = new Map(resolved.map((field) => [field.name, field]));
|
|
30
|
+
const pruned = pruneLayout(layout, fieldsByName);
|
|
31
|
+
const referenced = referencedNames(pruned);
|
|
32
|
+
const appended = resolved.filter((field) => !referenced.has(field.name)).map((field) => ({
|
|
33
|
+
type: "field",
|
|
34
|
+
name: field.name
|
|
35
|
+
}));
|
|
36
|
+
return {
|
|
37
|
+
fields: resolved,
|
|
38
|
+
fieldsByName,
|
|
39
|
+
layout: [...pruned, ...appended]
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Resolves the form of a resource: its fields (plus those of the item's type) and its layout. */
|
|
43
|
+
function resolveForm(resource, options = {}) {
|
|
44
|
+
const typeFields = options.type !== void 0 ? resource.types?.find((type) => type.id === options.type)?.fields ?? [] : [];
|
|
45
|
+
return resolveFields([...resource.fields, ...typeFields], resource.form?.layout ?? [], options);
|
|
46
|
+
}
|
|
47
|
+
/** The field names a layout references, in order. */
|
|
48
|
+
function getLayoutFieldNames(layout) {
|
|
49
|
+
return [...referencedNames(layout)];
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { getLayoutFieldNames, resolveFields, resolveForm };
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.js","names":[],"sources":["../../src/definition/form.ts"],"sourcesContent":["import type { Field, LayoutNode, Resource } from '@panneau/protocol';\n\n/** Whether a form creates an item or updates one. */\nexport type FormMode = 'create' | 'update';\n\n/** A form ready to render: its fields and a layout that references every one of them. */\nexport interface ResolvedForm {\n /** The fields of the form, in definition order. */\n fields: Field[];\n /** The fields, by name. */\n fieldsByName: ReadonlyMap<string, Field>;\n /**\n * The layout tree. Field nodes that name an unknown field are removed, and fields that no\n * node references are appended at the end, as `field` nodes.\n */\n layout: LayoutNode[];\n}\n\nexport interface ResolveFormOptions {\n /**\n * `create` removes `updateOnly` fields; `update` makes `createOnly` fields read-only. Without a\n * mode, every field is kept as is.\n */\n mode?: FormMode;\n /** The type of the item, among the resource's `types`: its fields are added. */\n type?: string;\n}\n\nfunction referencedNames(nodes: readonly LayoutNode[], names = new Set<string>()): Set<string> {\n nodes.forEach((node) => {\n if (node.type === 'field' && node.name !== undefined) {\n names.add(node.name);\n }\n referencedNames(node.children ?? [], names);\n });\n return names;\n}\n\nfunction pruneLayout(\n nodes: readonly LayoutNode[],\n fields: ReadonlyMap<string, Field>,\n): LayoutNode[] {\n return nodes.flatMap((node) => {\n if (node.type === 'field' && (node.name === undefined || !fields.has(node.name))) {\n return [];\n }\n if (node.children === undefined) {\n return [node];\n }\n return [{ ...node, children: pruneLayout(node.children, fields) }];\n });\n}\n\n/** Resolves fields and a layout into a form. */\nexport function resolveFields(\n fields: readonly Field[],\n layout: readonly LayoutNode[] = [],\n { mode }: Pick<ResolveFormOptions, 'mode'> = {},\n): ResolvedForm {\n const resolved = fields.flatMap((field) => {\n if (mode === 'create' && field.updateOnly === true) {\n return [];\n }\n if (mode === 'update' && field.createOnly === true) {\n return [{ ...field, readOnly: true }];\n }\n return [field];\n });\n const fieldsByName = new Map(resolved.map((field) => [field.name, field]));\n const pruned = pruneLayout(layout, fieldsByName);\n const referenced = referencedNames(pruned);\n const appended: LayoutNode[] = resolved\n .filter((field) => !referenced.has(field.name))\n .map((field) => ({ type: 'field', name: field.name }));\n return { fields: resolved, fieldsByName, layout: [...pruned, ...appended] };\n}\n\n/** Resolves the form of a resource: its fields (plus those of the item's type) and its layout. */\nexport function resolveForm(resource: Resource, options: ResolveFormOptions = {}): ResolvedForm {\n const typeFields =\n options.type !== undefined\n ? (resource.types?.find((type) => type.id === options.type)?.fields ?? [])\n : [];\n return resolveFields([...resource.fields, ...typeFields], resource.form?.layout ?? [], options);\n}\n\n/** The field names a layout references, in order. */\nexport function getLayoutFieldNames(layout: readonly LayoutNode[]): string[] {\n return [...referencedNames(layout)];\n}\n"],"mappings":";AA4BA,SAAS,gBAAgB,OAA8B,wBAAQ,IAAI,IAAY,GAAgB;CAC3F,MAAM,SAAS,SAAS;EACpB,IAAI,KAAK,SAAS,WAAW,KAAK,SAAS,KAAA,GACvC,MAAM,IAAI,KAAK,IAAI;EAEvB,gBAAgB,KAAK,YAAY,CAAA,GAAI,KAAK;CAC9C,CAAC;CACD,OAAO;AACX;AAEA,SAAS,YACL,OACA,QACY;CACZ,OAAO,MAAM,SAAS,SAAS;EAC3B,IAAI,KAAK,SAAS,YAAY,KAAK,SAAS,KAAA,KAAa,CAAC,OAAO,IAAI,KAAK,IAAI,IAC1E,OAAO,CAAA;EAEX,IAAI,KAAK,aAAa,KAAA,GAClB,OAAO,CAAC,IAAI;EAEhB,OAAO,CAAC;GAAE,GAAG;GAAM,UAAU,YAAY,KAAK,UAAU,MAAM;EAAE,CAAC;CACrE,CAAC;AACL;;AAGA,SAAgB,cACZ,QACA,SAAgC,CAAA,GAChC,EAAE,SAA2C,CAAC,GAClC;CACZ,MAAM,WAAW,OAAO,SAAS,UAAU;EACvC,IAAI,SAAS,YAAY,MAAM,eAAe,MAC1C,OAAO,CAAA;EAEX,IAAI,SAAS,YAAY,MAAM,eAAe,MAC1C,OAAO,CAAC;GAAE,GAAG;GAAO,UAAU;EAAK,CAAC;EAExC,OAAO,CAAC,KAAK;CACjB,CAAC;CACD,MAAM,eAAe,IAAI,IAAI,SAAS,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CACzE,MAAM,SAAS,YAAY,QAAQ,YAAY;CAC/C,MAAM,aAAa,gBAAgB,MAAM;CACzC,MAAM,WAAyB,SAC1B,QAAQ,UAAU,CAAC,WAAW,IAAI,MAAM,IAAI,CAAC,CAAC,CAC9C,KAAK,WAAW;EAAE,MAAM;EAAS,MAAM,MAAM;CAAK,EAAE;CACzD,OAAO;EAAE,QAAQ;EAAU;EAAc,QAAQ,CAAC,GAAG,QAAQ,GAAG,QAAQ;CAAE;AAC9E;;AAGA,SAAgB,YAAY,UAAoB,UAA8B,CAAC,GAAiB;CAC5F,MAAM,aACF,QAAQ,SAAS,KAAA,IACV,SAAS,OAAO,MAAM,SAAS,KAAK,OAAO,QAAQ,IAAI,CAAC,EAAE,UAAU,CAAA,IACrE,CAAA;CACV,OAAO,cAAc,CAAC,GAAG,SAAS,QAAQ,GAAG,UAAU,GAAG,SAAS,MAAM,UAAU,CAAA,GAAI,OAAO;AAClG;;AAGA,SAAgB,oBAAoB,QAAyC;CACzE,OAAO,CAAC,GAAG,gBAAgB,MAAM,CAAC;AACtC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Action, ActionPlacement, Field, Filter, Resource } from '@panneau/protocol';
|
|
2
|
+
/** A column ready to render. */
|
|
3
|
+
export interface ResolvedColumn {
|
|
4
|
+
/** The column id: its `id`, or the name of its field. */
|
|
5
|
+
id: string;
|
|
6
|
+
/** The field shown, when the column shows one. */
|
|
7
|
+
field: Field | null;
|
|
8
|
+
label: string;
|
|
9
|
+
/** How the value is displayed: the column's display, the field's, or the field type. */
|
|
10
|
+
display: string | null;
|
|
11
|
+
sortable: boolean;
|
|
12
|
+
width?: number | string;
|
|
13
|
+
align?: 'start' | 'center' | 'end';
|
|
14
|
+
}
|
|
15
|
+
/** A list ready to render. */
|
|
16
|
+
export interface ResolvedList {
|
|
17
|
+
columns: ResolvedColumn[];
|
|
18
|
+
filters: Filter[];
|
|
19
|
+
/** Fields the list can be sorted by. */
|
|
20
|
+
sorts: string[];
|
|
21
|
+
/** The default sort, as `sort` query values (`['-published_at', 'title']`). */
|
|
22
|
+
defaultSort: string[];
|
|
23
|
+
/** Whether the list can be searched (`q`). */
|
|
24
|
+
search: boolean;
|
|
25
|
+
/** List layouts, the first one being the default. */
|
|
26
|
+
layouts: string[];
|
|
27
|
+
perPage: number;
|
|
28
|
+
perPageOptions: number[];
|
|
29
|
+
/** Fields editable in the list. */
|
|
30
|
+
inlineEdit: string[];
|
|
31
|
+
/** Whether items can be selected: the list allows it and a selection action exists. */
|
|
32
|
+
selectable: boolean;
|
|
33
|
+
/** Actions the user can run, by placement. */
|
|
34
|
+
actions: Record<ActionPlacement, Action[]>;
|
|
35
|
+
}
|
|
36
|
+
export declare const DEFAULT_PER_PAGE = 20;
|
|
37
|
+
/** Groups the actions the user can run by placement. */
|
|
38
|
+
export declare function groupActions(actions: readonly Action[]): Record<ActionPlacement, Action[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Resolves the list of a resource. Without columns, it shows the item title and the next fields
|
|
41
|
+
* (up to four, skipping groups, repeaters and rich text).
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveList(resource: Resource): ResolvedList;
|
|
44
|
+
//# sourceMappingURL=list.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/definition/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAU,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAIlG,gCAAgC;AAChC,MAAM,WAAW,cAAc;IAC3B,yDAAyD;IACzD,EAAE,EAAE,MAAM,CAAC;IACX,kDAAkD;IAClD,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,wFAAwF;IACxF,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;CACtC;AAED,8BAA8B;AAC9B,MAAM,WAAW,YAAY;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,wCAAwC;IACxC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,+EAA+E;IAC/E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,8CAA8C;IAC9C,MAAM,EAAE,OAAO,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,mCAAmC;IACnC,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,uFAAuF;IACvF,UAAU,EAAE,OAAO,CAAC;IACpB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAAC,CAAC;CAC9C;AAMD,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAenC,wDAAwD;AACxD,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,eAAe,EAAE,MAAM,EAAE,CAAC,CAM1F;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,YAAY,CAwC5D"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { canRunAction, getActionPlacements } from "./abilities.js";
|
|
2
|
+
//#region src/definition/list.ts
|
|
3
|
+
/** Fields shown when the list declares no columns. */
|
|
4
|
+
var DEFAULT_COLUMN_COUNT = 4;
|
|
5
|
+
var NON_COLUMN_TYPES = /* @__PURE__ */ new Set([
|
|
6
|
+
"group",
|
|
7
|
+
"repeater",
|
|
8
|
+
"richtext"
|
|
9
|
+
]);
|
|
10
|
+
var DEFAULT_PER_PAGE = 20;
|
|
11
|
+
function columnsFromFields(resource) {
|
|
12
|
+
const title = resource.itemTitle;
|
|
13
|
+
const fields = resource.fields.filter((field) => !NON_COLUMN_TYPES.has(field.type));
|
|
14
|
+
return (title !== void 0 ? [...fields.filter((field) => field.name === title), ...fields.filter((field) => field.name !== title)] : fields).slice(0, DEFAULT_COLUMN_COUNT).map((field) => ({ field: field.name }));
|
|
15
|
+
}
|
|
16
|
+
/** Groups the actions the user can run by placement. */
|
|
17
|
+
function groupActions(actions) {
|
|
18
|
+
const groups = {
|
|
19
|
+
row: [],
|
|
20
|
+
toolbar: [],
|
|
21
|
+
form: [],
|
|
22
|
+
menu: []
|
|
23
|
+
};
|
|
24
|
+
actions.filter(canRunAction).forEach((action) => {
|
|
25
|
+
getActionPlacements(action).forEach((placement) => groups[placement].push(action));
|
|
26
|
+
});
|
|
27
|
+
return groups;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolves the list of a resource. Without columns, it shows the item title and the next fields
|
|
31
|
+
* (up to four, skipping groups, repeaters and rich text).
|
|
32
|
+
*/
|
|
33
|
+
function resolveList(resource) {
|
|
34
|
+
const list = resource.list ?? {};
|
|
35
|
+
const fields = new Map(resource.fields.map((field) => [field.name, field]));
|
|
36
|
+
const sorts = list.sorts ?? [];
|
|
37
|
+
const columns = (list.columns ?? columnsFromFields(resource)).map((column) => {
|
|
38
|
+
const field = column.field !== void 0 ? fields.get(column.field) ?? null : null;
|
|
39
|
+
const id = column.id ?? column.field ?? "";
|
|
40
|
+
const resolved = {
|
|
41
|
+
id,
|
|
42
|
+
field,
|
|
43
|
+
label: column.label ?? field?.label ?? id,
|
|
44
|
+
display: column.display ?? field?.display ?? field?.type ?? null,
|
|
45
|
+
sortable: column.sortable ?? (column.field !== void 0 && sorts.includes(column.field))
|
|
46
|
+
};
|
|
47
|
+
if (column.width !== void 0) resolved.width = column.width;
|
|
48
|
+
if (column.align !== void 0) resolved.align = column.align;
|
|
49
|
+
return resolved;
|
|
50
|
+
});
|
|
51
|
+
const actions = groupActions(resource.actions ?? []);
|
|
52
|
+
const hasSelectionAction = (resource.actions ?? []).some((action) => action.scope.includes("selection") && canRunAction(action));
|
|
53
|
+
return {
|
|
54
|
+
columns,
|
|
55
|
+
filters: list.filters ?? [],
|
|
56
|
+
sorts,
|
|
57
|
+
defaultSort: (list.defaultSort ?? "").split(",").filter((part) => part !== ""),
|
|
58
|
+
search: list.search ?? false,
|
|
59
|
+
layouts: list.layouts ?? ["table"],
|
|
60
|
+
perPage: list.perPage ?? 20,
|
|
61
|
+
perPageOptions: list.perPageOptions ?? [],
|
|
62
|
+
inlineEdit: list.inlineEdit ?? [],
|
|
63
|
+
selectable: (list.selectable ?? true) && hasSelectionAction,
|
|
64
|
+
actions
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
68
|
+
export { DEFAULT_PER_PAGE, groupActions, resolveList };
|
|
69
|
+
|
|
70
|
+
//# sourceMappingURL=list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list.js","names":[],"sources":["../../src/definition/list.ts"],"sourcesContent":["import type { Action, ActionPlacement, Column, Field, Filter, Resource } from '@panneau/protocol';\n\nimport { canRunAction, getActionPlacements } from './abilities';\n\n/** A column ready to render. */\nexport interface ResolvedColumn {\n /** The column id: its `id`, or the name of its field. */\n id: string;\n /** The field shown, when the column shows one. */\n field: Field | null;\n label: string;\n /** How the value is displayed: the column's display, the field's, or the field type. */\n display: string | null;\n sortable: boolean;\n width?: number | string;\n align?: 'start' | 'center' | 'end';\n}\n\n/** A list ready to render. */\nexport interface ResolvedList {\n columns: ResolvedColumn[];\n filters: Filter[];\n /** Fields the list can be sorted by. */\n sorts: string[];\n /** The default sort, as `sort` query values (`['-published_at', 'title']`). */\n defaultSort: string[];\n /** Whether the list can be searched (`q`). */\n search: boolean;\n /** List layouts, the first one being the default. */\n layouts: string[];\n perPage: number;\n perPageOptions: number[];\n /** Fields editable in the list. */\n inlineEdit: string[];\n /** Whether items can be selected: the list allows it and a selection action exists. */\n selectable: boolean;\n /** Actions the user can run, by placement. */\n actions: Record<ActionPlacement, Action[]>;\n}\n\n/** Fields shown when the list declares no columns. */\nconst DEFAULT_COLUMN_COUNT = 4;\nconst NON_COLUMN_TYPES = new Set(['group', 'repeater', 'richtext']);\n\nexport const DEFAULT_PER_PAGE = 20;\n\nfunction columnsFromFields(resource: Resource): Column[] {\n const title = resource.itemTitle;\n const fields = resource.fields.filter((field) => !NON_COLUMN_TYPES.has(field.type));\n const ordered =\n title !== undefined\n ? [\n ...fields.filter((field) => field.name === title),\n ...fields.filter((field) => field.name !== title),\n ]\n : fields;\n return ordered.slice(0, DEFAULT_COLUMN_COUNT).map((field) => ({ field: field.name }));\n}\n\n/** Groups the actions the user can run by placement. */\nexport function groupActions(actions: readonly Action[]): Record<ActionPlacement, Action[]> {\n const groups: Record<ActionPlacement, Action[]> = { row: [], toolbar: [], form: [], menu: [] };\n actions.filter(canRunAction).forEach((action) => {\n getActionPlacements(action).forEach((placement) => groups[placement].push(action));\n });\n return groups;\n}\n\n/**\n * Resolves the list of a resource. Without columns, it shows the item title and the next fields\n * (up to four, skipping groups, repeaters and rich text).\n */\nexport function resolveList(resource: Resource): ResolvedList {\n const list = resource.list ?? {};\n const fields = new Map(resource.fields.map((field) => [field.name, field]));\n const sorts = list.sorts ?? [];\n const columns = (list.columns ?? columnsFromFields(resource)).map((column): ResolvedColumn => {\n const field = column.field !== undefined ? (fields.get(column.field) ?? null) : null;\n const id = column.id ?? column.field ?? '';\n const resolved: ResolvedColumn = {\n id,\n field,\n label: column.label ?? field?.label ?? id,\n display: column.display ?? field?.display ?? field?.type ?? null,\n sortable:\n column.sortable ?? (column.field !== undefined && sorts.includes(column.field)),\n };\n if (column.width !== undefined) {\n resolved.width = column.width;\n }\n if (column.align !== undefined) {\n resolved.align = column.align;\n }\n return resolved;\n });\n const actions = groupActions(resource.actions ?? []);\n const hasSelectionAction = (resource.actions ?? []).some(\n (action) => action.scope.includes('selection') && canRunAction(action),\n );\n return {\n columns,\n filters: list.filters ?? [],\n sorts,\n defaultSort: (list.defaultSort ?? '').split(',').filter((part) => part !== ''),\n search: list.search ?? false,\n layouts: list.layouts ?? ['table'],\n perPage: list.perPage ?? DEFAULT_PER_PAGE,\n perPageOptions: list.perPageOptions ?? [],\n inlineEdit: list.inlineEdit ?? [],\n selectable: (list.selectable ?? true) && hasSelectionAction,\n actions,\n };\n}\n"],"mappings":";;;AAyCA,IAAM,uBAAuB;AAC7B,IAAM,mCAAmB,IAAI,IAAI;CAAC;CAAS;CAAY;AAAU,CAAC;AAElE,IAAa,mBAAmB;AAEhC,SAAS,kBAAkB,UAA8B;CACrD,MAAM,QAAQ,SAAS;CACvB,MAAM,SAAS,SAAS,OAAO,QAAQ,UAAU,CAAC,iBAAiB,IAAI,MAAM,IAAI,CAAC;CAQlF,QANI,UAAU,KAAA,IACJ,CACI,GAAG,OAAO,QAAQ,UAAU,MAAM,SAAS,KAAK,GAChD,GAAG,OAAO,QAAQ,UAAU,MAAM,SAAS,KAAK,CAAC,IAErD,OAAA,CACK,MAAM,GAAG,oBAAoB,CAAC,CAAC,KAAK,WAAW,EAAE,OAAO,MAAM,KAAK,EAAE;AACxF;;AAGA,SAAgB,aAAa,SAA+D;CACxF,MAAM,SAA4C;EAAE,KAAK,CAAA;EAAI,SAAS,CAAA;EAAI,MAAM,CAAA;EAAI,MAAM,CAAA;CAAG;CAC7F,QAAQ,OAAO,YAAY,CAAC,CAAC,SAAS,WAAW;EAC7C,oBAAoB,MAAM,CAAC,CAAC,SAAS,cAAc,OAAO,UAAU,CAAC,KAAK,MAAM,CAAC;CACrF,CAAC;CACD,OAAO;AACX;;;;;AAMA,SAAgB,YAAY,UAAkC;CAC1D,MAAM,OAAO,SAAS,QAAQ,CAAC;CAC/B,MAAM,SAAS,IAAI,IAAI,SAAS,OAAO,KAAK,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;CAC1E,MAAM,QAAQ,KAAK,SAAS,CAAA;CAC5B,MAAM,WAAW,KAAK,WAAW,kBAAkB,QAAQ,EAAA,CAAG,KAAK,WAA2B;EAC1F,MAAM,QAAQ,OAAO,UAAU,KAAA,IAAa,OAAO,IAAI,OAAO,KAAK,KAAK,OAAQ;EAChF,MAAM,KAAK,OAAO,MAAM,OAAO,SAAS;EACxC,MAAM,WAA2B;GAC7B;GACA;GACA,OAAO,OAAO,SAAS,OAAO,SAAS;GACvC,SAAS,OAAO,WAAW,OAAO,WAAW,OAAO,QAAQ;GAC5D,UACI,OAAO,aAAa,OAAO,UAAU,KAAA,KAAa,MAAM,SAAS,OAAO,KAAK;EACrF;EACA,IAAI,OAAO,UAAU,KAAA,GACjB,SAAS,QAAQ,OAAO;EAE5B,IAAI,OAAO,UAAU,KAAA,GACjB,SAAS,QAAQ,OAAO;EAE5B,OAAO;CACX,CAAC;CACD,MAAM,UAAU,aAAa,SAAS,WAAW,CAAA,CAAE;CACnD,MAAM,sBAAsB,SAAS,WAAW,CAAA,EAAA,CAAI,MAC/C,WAAW,OAAO,MAAM,SAAS,WAAW,KAAK,aAAa,MAAM,CACzE;CACA,OAAO;EACH;EACA,SAAS,KAAK,WAAW,CAAA;EACzB;EACA,cAAc,KAAK,eAAe,GAAA,CAAI,MAAM,GAAG,CAAC,CAAC,QAAQ,SAAS,SAAS,EAAE;EAC7E,QAAQ,KAAK,UAAU;EACvB,SAAS,KAAK,WAAW,CAAC,OAAO;EACjC,SAAS,KAAK,WAAA;EACd,gBAAgB,KAAK,kBAAkB,CAAA;EACvC,YAAY,KAAK,cAAc,CAAA;EAC/B,aAAa,KAAK,cAAc,SAAS;EACzC;CACJ;AACJ"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Definition, Resource } from '@panneau/protocol';
|
|
2
|
+
import { type ResolveFormOptions, type ResolvedForm } from './form';
|
|
3
|
+
import { type ResolvedList } from './list';
|
|
4
|
+
/** What the current user can do with a resource as a whole. */
|
|
5
|
+
export interface ResourceAbilities {
|
|
6
|
+
viewAny: boolean;
|
|
7
|
+
create: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** A resource with its form, its list and its abilities resolved. */
|
|
10
|
+
export interface ResolvedResource {
|
|
11
|
+
resource: Resource;
|
|
12
|
+
/** The form to create an item. Use `resolveForm()` for an update, or an item type. */
|
|
13
|
+
form: ResolvedForm;
|
|
14
|
+
list: ResolvedList;
|
|
15
|
+
abilities: ResourceAbilities;
|
|
16
|
+
}
|
|
17
|
+
/** Resolves a resource into its form, its list and its abilities. */
|
|
18
|
+
export declare function resolveResource(resource: Resource, formOptions?: ResolveFormOptions): ResolvedResource;
|
|
19
|
+
/** Finds a resource of a definition by id. */
|
|
20
|
+
export declare function findResource(definition: Definition, id: string): Resource | undefined;
|
|
21
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/definition/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG9D,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,YAAY,EAAe,MAAM,QAAQ,CAAC;AACjF,OAAO,EAAE,KAAK,YAAY,EAAe,MAAM,QAAQ,CAAC;AAExD,+DAA+D;AAC/D,MAAM,WAAW,iBAAiB;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACnB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,QAAQ,CAAC;IACnB,sFAAsF;IACtF,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,iBAAiB,CAAC;CAChC;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,QAAQ,EAClB,WAAW,GAAE,kBAAuC,GACrD,gBAAgB,CAUlB;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAErF"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { can } from "./abilities.js";
|
|
2
|
+
import { resolveForm } from "./form.js";
|
|
3
|
+
import { resolveList } from "./list.js";
|
|
4
|
+
//#region src/definition/resource.ts
|
|
5
|
+
/** Resolves a resource into its form, its list and its abilities. */
|
|
6
|
+
function resolveResource(resource, formOptions = { mode: "create" }) {
|
|
7
|
+
return {
|
|
8
|
+
resource,
|
|
9
|
+
form: resolveForm(resource, formOptions),
|
|
10
|
+
list: resolveList(resource),
|
|
11
|
+
abilities: {
|
|
12
|
+
viewAny: can(resource.abilities, "viewAny"),
|
|
13
|
+
create: can(resource.abilities, "create")
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Finds a resource of a definition by id. */
|
|
18
|
+
function findResource(definition, id) {
|
|
19
|
+
return definition.resources.find((resource) => resource.id === id);
|
|
20
|
+
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { findResource, resolveResource };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=resource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource.js","names":[],"sources":["../../src/definition/resource.ts"],"sourcesContent":["import type { Definition, Resource } from '@panneau/protocol';\n\nimport { can } from './abilities';\nimport { type ResolveFormOptions, type ResolvedForm, resolveForm } from './form';\nimport { type ResolvedList, resolveList } from './list';\n\n/** What the current user can do with a resource as a whole. */\nexport interface ResourceAbilities {\n viewAny: boolean;\n create: boolean;\n}\n\n/** A resource with its form, its list and its abilities resolved. */\nexport interface ResolvedResource {\n resource: Resource;\n /** The form to create an item. Use `resolveForm()` for an update, or an item type. */\n form: ResolvedForm;\n list: ResolvedList;\n abilities: ResourceAbilities;\n}\n\n/** Resolves a resource into its form, its list and its abilities. */\nexport function resolveResource(\n resource: Resource,\n formOptions: ResolveFormOptions = { mode: 'create' },\n): ResolvedResource {\n return {\n resource,\n form: resolveForm(resource, formOptions),\n list: resolveList(resource),\n abilities: {\n viewAny: can(resource.abilities, 'viewAny'),\n create: can(resource.abilities, 'create'),\n },\n };\n}\n\n/** Finds a resource of a definition by id. */\nexport function findResource(definition: Definition, id: string): Resource | undefined {\n return definition.resources.find((resource) => resource.id === id);\n}\n"],"mappings":";;;;;AAsBA,SAAgB,gBACZ,UACA,cAAkC,EAAE,MAAM,SAAS,GACnC;CAChB,OAAO;EACH;EACA,MAAM,YAAY,UAAU,WAAW;EACvC,MAAM,YAAY,QAAQ;EAC1B,WAAW;GACP,SAAS,IAAI,SAAS,WAAW,SAAS;GAC1C,QAAQ,IAAI,SAAS,WAAW,QAAQ;EAC5C;CACJ;AACJ;;AAGA,SAAgB,aAAa,YAAwB,IAAkC;CACnF,OAAO,WAAW,UAAU,MAAM,aAAa,SAAS,OAAO,EAAE;AACrE"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { type Condition, type Field, type Item, type RuleFailure } from '@panneau/protocol';
|
|
2
|
+
import type { FormMode } from '../definition/form';
|
|
3
|
+
import { type Values } from '../values';
|
|
4
|
+
import { type FormStore } from './store';
|
|
5
|
+
/**
|
|
6
|
+
* - `idle`: nothing submitted yet, or after a reset;
|
|
7
|
+
* - `submitting`: waiting for `onSubmit`;
|
|
8
|
+
* - `submitted`: the last submit succeeded;
|
|
9
|
+
* - `invalid`: client validation or the server (`422`) found errors;
|
|
10
|
+
* - `conflict`: the item changed since the form's version (`412`), see `resolveConflict()`;
|
|
11
|
+
* - `error`: the last submit failed for another reason (network, `403`, `500`…), see `error`.
|
|
12
|
+
*/
|
|
13
|
+
export type FormStatus = 'idle' | 'submitting' | 'submitted' | 'invalid' | 'conflict' | 'error';
|
|
14
|
+
export interface FormConflict {
|
|
15
|
+
/** The current version of the item, when the backend sent it. */
|
|
16
|
+
current: Item | null;
|
|
17
|
+
}
|
|
18
|
+
/** A snapshot of a form. A new object is created on every change. */
|
|
19
|
+
export interface FormState {
|
|
20
|
+
values: Values;
|
|
21
|
+
/** The values the form started from, or was last saved with. `dirty` compares to them. */
|
|
22
|
+
initialValues: Values;
|
|
23
|
+
/** The version of the item the values were edited from, sent with the submit. */
|
|
24
|
+
version: string | null;
|
|
25
|
+
/** Whether the values differ from the initial values. */
|
|
26
|
+
dirty: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Errors shown, by dot path (`title.fr`): client errors of touched fields (all of them after a
|
|
29
|
+
* submit), then server errors. A server error goes away when its value changes.
|
|
30
|
+
*/
|
|
31
|
+
errors: Record<string, string[]>;
|
|
32
|
+
/** Paths of the fields that lost focus at least once. */
|
|
33
|
+
touched: ReadonlySet<string>;
|
|
34
|
+
status: FormStatus;
|
|
35
|
+
/** With the `conflict` status: what the backend says is current. */
|
|
36
|
+
conflict: FormConflict | null;
|
|
37
|
+
/** With the `error` status: what `onSubmit` threw. */
|
|
38
|
+
error: unknown;
|
|
39
|
+
submitCount: number;
|
|
40
|
+
}
|
|
41
|
+
export interface SubmitContext {
|
|
42
|
+
mode: FormMode;
|
|
43
|
+
/** The version to send as `If-Match`. */
|
|
44
|
+
version: string | null;
|
|
45
|
+
/** The top-level values that differ from the initial values, for partial updates. */
|
|
46
|
+
changes: Values;
|
|
47
|
+
}
|
|
48
|
+
export type SubmitResult = {
|
|
49
|
+
status: 'submitted';
|
|
50
|
+
result: unknown;
|
|
51
|
+
} | {
|
|
52
|
+
status: 'invalid';
|
|
53
|
+
errors: Record<string, string[]>;
|
|
54
|
+
} | {
|
|
55
|
+
status: 'conflict';
|
|
56
|
+
current: Item | null;
|
|
57
|
+
} | {
|
|
58
|
+
status: 'error';
|
|
59
|
+
error: unknown;
|
|
60
|
+
};
|
|
61
|
+
export interface FormOptions {
|
|
62
|
+
/** The fields of the form, as resolved by `resolveForm()`. */
|
|
63
|
+
fields: Field[];
|
|
64
|
+
/** Initial values. In `create` mode, fields without a value get their `default`. */
|
|
65
|
+
values?: Values;
|
|
66
|
+
/** The version of the item the values come from. */
|
|
67
|
+
version?: string | null;
|
|
68
|
+
/** Defaults to `update` when a version is given, `create` otherwise. */
|
|
69
|
+
mode?: FormMode;
|
|
70
|
+
/** Locales a `required` localized field must be filled in (all of them, usually). */
|
|
71
|
+
locales?: string[];
|
|
72
|
+
/** Where the values live. Defaults to a local store. */
|
|
73
|
+
store?: FormStore;
|
|
74
|
+
/** Message of a failed rule. Defaults to `formatRuleFailure()`. */
|
|
75
|
+
formatError?: (failure: RuleFailure, path: string) => string;
|
|
76
|
+
/**
|
|
77
|
+
* Saves the values. When it returns an item, the form continues from that item. When it
|
|
78
|
+
* throws an error that carries problem details (`getProblem()`), `422` errors are shown by
|
|
79
|
+
* path and `412` puts the form in the `conflict` status.
|
|
80
|
+
*/
|
|
81
|
+
onSubmit?: (values: Values, context: SubmitContext) => unknown;
|
|
82
|
+
}
|
|
83
|
+
/** Form options that can change after the form is created. */
|
|
84
|
+
export type UpdatableFormOptions = Pick<FormOptions, 'fields' | 'locales' | 'formatError' | 'onSubmit'>;
|
|
85
|
+
export interface GetErrorsOptions {
|
|
86
|
+
/** Also return the errors of nested paths (`title.fr` for `title`). */
|
|
87
|
+
nested?: boolean;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A headless form: values, errors, visibility and submit. React components use `useForm()`.
|
|
91
|
+
*
|
|
92
|
+
* The methods that read the state (`getValue()`, `getErrors()`, `countErrors()`, `isVisible()`)
|
|
93
|
+
* are not React APIs: the React Compiler memoizes their calls by their arguments, so they return
|
|
94
|
+
* stale results in components. Components read the state they subscribed to with the selectors
|
|
95
|
+
* (`selectValue()`, `selectErrors()`, `selectErrorCount()`, `selectVisible()`).
|
|
96
|
+
*/
|
|
97
|
+
export interface Form {
|
|
98
|
+
getState(): FormState;
|
|
99
|
+
subscribe(listener: () => void): () => void;
|
|
100
|
+
/** Updates the options that can change between renders. */
|
|
101
|
+
setOptions(options: Partial<UpdatableFormOptions>): void;
|
|
102
|
+
/**
|
|
103
|
+
* The value at a dot path. Not a React API: components use `selectValue(state, path)` (see
|
|
104
|
+
* {@link Form}).
|
|
105
|
+
*/
|
|
106
|
+
getValue(path: string): unknown;
|
|
107
|
+
setValue(path: string, value: unknown): void;
|
|
108
|
+
/** Marks a field as touched, which shows its client errors. */
|
|
109
|
+
blur(path: string): void;
|
|
110
|
+
/**
|
|
111
|
+
* Errors at a path. Not a React API: components use `selectErrors(state, path, options)` (see
|
|
112
|
+
* {@link Form}).
|
|
113
|
+
*/
|
|
114
|
+
getErrors(path: string, options?: GetErrorsOptions): string[];
|
|
115
|
+
/**
|
|
116
|
+
* The number of paths with errors within the given paths, for counts on tabs and sections. Not
|
|
117
|
+
* a React API: components use `selectErrorCount(state, paths)` (see {@link Form}).
|
|
118
|
+
*/
|
|
119
|
+
countErrors(paths: readonly string[]): number;
|
|
120
|
+
/**
|
|
121
|
+
* Whether a field or a layout node is visible, from its `visibleWhen`. For nested fields, pass
|
|
122
|
+
* the path of the object they belong to (`blocks.2`): conditions are relative to it. Not a
|
|
123
|
+
* React API: components use `selectVisible(state, target, parentPath)` (see {@link Form}).
|
|
124
|
+
*/
|
|
125
|
+
isVisible(target: {
|
|
126
|
+
visibleWhen?: Condition | undefined;
|
|
127
|
+
}, parentPath?: string): boolean;
|
|
128
|
+
/** Checks every value with the portable rules and shows every error. Returns whether valid. */
|
|
129
|
+
validate(): boolean;
|
|
130
|
+
/** Shows server errors, by dot path. */
|
|
131
|
+
setServerErrors(errors: Record<string, string[]>): void;
|
|
132
|
+
/** Validates, then calls `onSubmit`. */
|
|
133
|
+
submit(): Promise<SubmitResult>;
|
|
134
|
+
/** Starts over from new values (or the initial ones), and a new version. */
|
|
135
|
+
reset(values?: Values, version?: string | null): void;
|
|
136
|
+
/**
|
|
137
|
+
* Leaves the `conflict` status:
|
|
138
|
+
*
|
|
139
|
+
* - `reload` drops the local changes and continues from the current item;
|
|
140
|
+
* - `overwrite` keeps the local changes and submits them again over the current version.
|
|
141
|
+
*/
|
|
142
|
+
resolveConflict(choice: 'reload' | 'overwrite'): Promise<SubmitResult | null>;
|
|
143
|
+
}
|
|
144
|
+
/** Initial values of fields that have a `default` and no value. */
|
|
145
|
+
export declare function getDefaultValues(fields: readonly Field[], values?: Values): Values;
|
|
146
|
+
/** Creates a headless form. */
|
|
147
|
+
export declare function createForm(options: FormOptions): Form;
|
|
148
|
+
//# sourceMappingURL=form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/form/form.ts"],"names":[],"mappings":"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"}
|