@sincpro/web-framework 0.1.0 → 0.2.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 +261 -40
- package/dist/core/cache.js +2 -2
- package/dist/core/copy.d.ts +24 -0
- package/dist/core/copy.js +30 -0
- package/dist/core/edit.d.ts +13 -0
- package/dist/core/edit.js +30 -0
- package/dist/core/entity.d.ts +34 -0
- package/dist/core/entity.js +25 -0
- package/dist/core/form.d.ts +21 -0
- package/dist/core/form.js +30 -0
- package/dist/core/index.d.ts +10 -3
- package/dist/core/index.js +9 -2
- package/dist/core/list.d.ts +64 -0
- package/dist/core/list.js +22 -0
- package/dist/core/lookup.d.ts +5 -2
- package/dist/core/lookup.js +6 -2
- package/dist/core/measure.d.ts +16 -0
- package/dist/core/measure.js +38 -0
- package/dist/core/memory.js +18 -4
- package/dist/core/relation.d.ts +28 -0
- package/dist/core/relation.js +30 -0
- package/dist/core/rest.js +13 -3
- package/dist/core/retry.d.ts +24 -0
- package/dist/core/retry.js +25 -0
- package/dist/core/summary.d.ts +50 -0
- package/dist/core/summary.js +54 -0
- package/dist/core/trail.d.ts +44 -0
- package/dist/core/trail.js +70 -0
- package/dist/core/transport.d.ts +13 -1
- package/dist/react/context.d.ts +12 -1
- package/dist/react/context.js +10 -2
- package/dist/react/index.d.ts +3 -1
- package/dist/react/index.js +3 -1
- package/dist/react/useCommand.d.ts +3 -1
- package/dist/react/useCommand.js +12 -4
- package/dist/react/useGroups.d.ts +40 -0
- package/dist/react/useGroups.js +69 -0
- package/dist/react/useListing.d.ts +21 -3
- package/dist/react/useListing.js +38 -3
- package/dist/react/useLookup.d.ts +30 -5
- package/dist/react/useLookup.js +56 -3
- package/dist/react/useRecord.d.ts +17 -2
- package/dist/react/useRecord.js +105 -9
- package/dist/react/useSummary.d.ts +15 -0
- package/dist/react/useSummary.js +22 -0
- package/dist/web/index.d.ts +9 -3
- package/dist/web/index.js +9 -3
- package/dist/web/list/EntityList.d.ts +27 -4
- package/dist/web/list/EntityList.js +162 -15
- package/dist/web/list/EntitySearchBar.d.ts +16 -8
- package/dist/web/list/EntitySearchBar.js +34 -4
- package/dist/web/list/groups.d.ts +15 -0
- package/dist/web/list/groups.js +73 -0
- package/dist/web/list/rowCommands.d.ts +41 -0
- package/dist/web/list/rowCommands.js +56 -0
- package/dist/web/record/EntityPicker.d.ts +42 -13
- package/dist/web/record/EntityPicker.js +53 -19
- package/dist/web/record/EntityTags.d.ts +38 -0
- package/dist/web/record/EntityTags.js +41 -0
- package/dist/web/record/RelatedList.d.ts +2 -1
- package/dist/web/record/RelatedList.js +12 -2
- package/dist/web/record/edit.d.ts +12 -5
- package/dist/web/record/edit.js +67 -8
- package/dist/web/record/opening.d.ts +26 -0
- package/dist/web/record/opening.js +10 -0
- package/dist/web/record/summary.d.ts +22 -0
- package/dist/web/record/summary.js +55 -0
- package/dist/web/workspace/SincproApp.d.ts +4 -1
- package/dist/web/workspace/SincproApp.js +2 -2
- package/dist/web/workspace/composer.d.ts +26 -4
- package/dist/web/workspace/composer.js +59 -2
- package/dist/web/workspace/open.d.ts +61 -0
- package/dist/web/workspace/open.js +62 -0
- package/dist/web/workspace/record.d.ts +30 -0
- package/dist/web/workspace/record.js +80 -0
- package/dist/web/workspace/skin.d.ts +0 -9
- package/dist/web/workspace/skin.js +25 -13
- package/dist/web/workspace/workspace.d.ts +31 -3
- package/dist/web/workspace/workspace.js +57 -4
- package/package.json +5 -5
package/dist/web/record/edit.js
CHANGED
|
@@ -3,13 +3,43 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { choicesOf } from "@sincpro/criteria";
|
|
4
4
|
import { frameworkCopy } from "../../core/copy.js";
|
|
5
5
|
import { textOf, viewOf } from "../../core/present.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { relationFieldOf } from "../../core/relation.js";
|
|
7
|
+
import { useEntityFor, useFramework } from "../../react/context.js";
|
|
8
|
+
import { EntityPicker } from "./EntityPicker.js";
|
|
9
|
+
import { useRecordOpening } from "./opening.js";
|
|
10
|
+
import { RecordLines } from "./record.js";
|
|
11
|
+
import { RelatedList } from "./RelatedList.js";
|
|
12
|
+
import { Checkbox, DatePicker, Field, NumberInput, ReadValue, RelationValue, Select, TagsField, TextInput, } from "@sincpro/web-ui";
|
|
13
|
+
export function RecordField({ record, name, as, bare = false, }) {
|
|
8
14
|
const { meta, locale, edit } = record;
|
|
9
15
|
const view = viewOf(meta, name, locale);
|
|
10
16
|
const state = edit.state(name);
|
|
17
|
+
const relation = relationFieldOf(meta, name);
|
|
18
|
+
const target = useEntityFor(relation?.target);
|
|
19
|
+
const opening = useRecordOpening();
|
|
20
|
+
const { metas } = useFramework();
|
|
11
21
|
if (!state.visible)
|
|
12
22
|
return null;
|
|
23
|
+
// A one2many of a declared entity is that entity's own list, filtered to this record by
|
|
24
|
+
// the server — never the page the record happened to bring — and a row opens its record.
|
|
25
|
+
// A one2many of no declared entity (the lines of an invoice): the page the record brought,
|
|
26
|
+
// with its real total.
|
|
27
|
+
if (as === undefined && relation?.field.type === "one2many" && target === undefined) {
|
|
28
|
+
const lines = (_jsx(RecordLines, { record: record.record ?? {}, meta: meta, name: name, locale: locale }));
|
|
29
|
+
return bare ? lines : _jsx(Field, { label: view.label, children: lines });
|
|
30
|
+
}
|
|
31
|
+
if (as === undefined && relation?.field.type === "one2many" && target !== undefined) {
|
|
32
|
+
const list = (_jsx(RelatedList, { record: record, name: name, entity: target, ...(opening === null
|
|
33
|
+
? {}
|
|
34
|
+
: {
|
|
35
|
+
onOpen: (row) => {
|
|
36
|
+
const known = metas.get(target.key);
|
|
37
|
+
const key = known?.get_id || known?.identity || "id";
|
|
38
|
+
opening.open(target.ref(String(row[key])), opening.targets[0] ?? "page");
|
|
39
|
+
},
|
|
40
|
+
}) }));
|
|
41
|
+
return bare ? list : _jsx(Field, { label: view.label, children: list });
|
|
42
|
+
}
|
|
13
43
|
const value = edit.values[name];
|
|
14
44
|
const error = edit.errors[name];
|
|
15
45
|
const control = {
|
|
@@ -20,9 +50,38 @@ export function RecordField({ record, name, as, }) {
|
|
|
20
50
|
label: view.label,
|
|
21
51
|
};
|
|
22
52
|
const field = meta?.fields[name];
|
|
53
|
+
/** A many2one of a declared entity: chosen by the server's literal search, opened where the
|
|
54
|
+
* view says. Written by its key; read by the record it points at. */
|
|
55
|
+
function manyToOne() {
|
|
56
|
+
if (relation === undefined || target === undefined)
|
|
57
|
+
return null;
|
|
58
|
+
const key = edit.values[relation.key];
|
|
59
|
+
const id = key === null || key === undefined ? "" : String(key);
|
|
60
|
+
if (state.readonly)
|
|
61
|
+
return (_jsx(RelationValue, { name: textOf(meta, relation.relation, edit.values[relation.relation], locale) ?? id, ...(opening === null || id === ""
|
|
62
|
+
? {}
|
|
63
|
+
: {
|
|
64
|
+
onOpenIn: (to) => opening.open(target.ref(id), to),
|
|
65
|
+
openTargets: opening.targets,
|
|
66
|
+
}) }));
|
|
67
|
+
const answer = (next) => edit.set(relation.key, next);
|
|
68
|
+
return (_jsx(EntityPicker, { entity: target, value: id, onChange: answer,
|
|
69
|
+
// «Buscar más…» is an option of the picker already: no second button beside it.
|
|
70
|
+
browse: false, "aria-label": view.label, ...(opening === null
|
|
71
|
+
? {}
|
|
72
|
+
: {
|
|
73
|
+
onOpen: (chosen, to) => opening.open(target.ref(chosen), to),
|
|
74
|
+
openTargets: opening.targets,
|
|
75
|
+
...(opening.choose === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: {
|
|
78
|
+
onBrowseAll: (query) => opening.choose?.(target, answer, query),
|
|
79
|
+
}),
|
|
80
|
+
}) }));
|
|
81
|
+
}
|
|
23
82
|
if (view.type === "boolean" && as === undefined)
|
|
24
83
|
return (_jsx(Checkbox, { checked: value === true, onChange: control.set, label: view.label, disabled: state.readonly }));
|
|
25
|
-
return (_jsx(Field, { label: view.label, required: state.required && !state.readonly, ...(view.help === undefined ? {} : { hint: view.help }), ...(error === undefined ? {} : { error }), children: as !== undefined ? (as(control)) : state.readonly ? (_jsx(ReadValue, { value: textOf(meta, name, value, locale) ?? "—" })) : (controlFor(control, view.type, field?.exact === true, choicesOf(meta, name, locale))) }));
|
|
84
|
+
return (_jsx(Field, { label: view.label, required: state.required && !state.readonly, ...(view.help === undefined ? {} : { hint: view.help }), ...(error === undefined ? {} : { error }), children: as !== undefined ? (as(control)) : relation !== undefined && !relation.many && target !== undefined ? (manyToOne()) : state.readonly ? (_jsx(ReadValue, { value: textOf(meta, name, value, locale) ?? "—" })) : (controlFor(control, view.type, field?.exact === true, choicesOf(meta, name, locale))) }));
|
|
26
85
|
}
|
|
27
86
|
function controlFor(control, type, exact, choices) {
|
|
28
87
|
const { value, set } = control;
|
|
@@ -36,11 +95,11 @@ function controlFor(control, type, exact, choices) {
|
|
|
36
95
|
onChange: (next) => set(next === "" ? null : exact ? String(next) : next) }));
|
|
37
96
|
case "date":
|
|
38
97
|
return (_jsx(DatePicker, { value: typeof value === "string" && value !== "" ? value : null, onChange: set }));
|
|
39
|
-
case "text[]":
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
case "text[]": {
|
|
99
|
+
// A list of words is chips: each word is its own key, and typing a new one adds it.
|
|
100
|
+
const words = Array.isArray(value) ? value.map(String) : [];
|
|
101
|
+
return (_jsx(TagsField, { value: words, options: words.map((word) => ({ value: word, label: word })), onChange: set, onCreate: (word) => word }));
|
|
102
|
+
}
|
|
44
103
|
default:
|
|
45
104
|
return (_jsx(TextInput, { value: value === null || value === undefined ? "" : String(value), onChange: set }));
|
|
46
105
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Entity, EntityRef } from "../../core/entity.js";
|
|
2
|
+
import type { OpenTarget } from "@sincpro/web-ui";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* How a record shown in a form opens from here — given by whoever holds the form, used by the
|
|
6
|
+
* fields inside it.
|
|
7
|
+
*
|
|
8
|
+
* Context: a form must work outside a workspace (`web/record/` imports no composer), yet a
|
|
9
|
+
* many2one inside one opens its record in a modal, with «Abrir en…» for the rest. So the form
|
|
10
|
+
* does not decide: the view that holds it says how to open (`entityView` fills it with the
|
|
11
|
+
* composer's `openIn` and the places allowed from where the view is), and a field asks. Without
|
|
12
|
+
* it a related record is a value, not a door.
|
|
13
|
+
*/
|
|
14
|
+
export type RecordOpening = {
|
|
15
|
+
open: (ref: EntityRef, to: OpenTarget) => void;
|
|
16
|
+
/** Where it may open from here, the default first. */
|
|
17
|
+
targets: readonly OpenTarget[];
|
|
18
|
+
/** «Buscar más…»: the entity's own list opened to choose (in composer, a modal that answers),
|
|
19
|
+
* on what was typed. Without it the picker brings its own chooser. */
|
|
20
|
+
choose?: (entity: Entity, answer: (id: string) => void, query: string) => void;
|
|
21
|
+
};
|
|
22
|
+
export declare function RecordOpeningProvider({ value, children, }: {
|
|
23
|
+
value: RecordOpening;
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}): import("react").JSX.Element;
|
|
26
|
+
export declare function useRecordOpening(): RecordOpening | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
const Opening = createContext(null);
|
|
5
|
+
export function RecordOpeningProvider({ value, children, }) {
|
|
6
|
+
return _jsx(Opening.Provider, { value: value, children: children });
|
|
7
|
+
}
|
|
8
|
+
export function useRecordOpening() {
|
|
9
|
+
return useContext(Opening);
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { RecordSummary } from "../../core/summary.js";
|
|
2
|
+
import { type RecordCard, type RelationValueProps } from "@sincpro/web-ui";
|
|
3
|
+
import type { ComponentProps } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* A record's summary as the props of web-ui's pieces that draw one record at a size: the card
|
|
6
|
+
* of a board and the link (with the card that floats from it) of a many2one.
|
|
7
|
+
*
|
|
8
|
+
* Context: PROPS, not wrapper components — the same reason as `factsOf`/`columnsOf`: web-ui
|
|
9
|
+
* finds its pieces by type inside a list or a board, and a component of ours around one is not
|
|
10
|
+
* found. The screen keeps writing `<RecordCard {...cardOf(s)} onOpen={…} />`, and adds or
|
|
11
|
+
* overrides whatever it wants. What the record does not have is left out, never drawn empty.
|
|
12
|
+
*/
|
|
13
|
+
type CardProps = ComponentProps<typeof RecordCard>;
|
|
14
|
+
type RelationProps = Pick<RelationValueProps, "name" | "secondary" | "media" | "summary">;
|
|
15
|
+
/** The board's card: the state above, the name, what it is, its description, its last change. */
|
|
16
|
+
export declare function cardOf(summary: RecordSummary): CardProps;
|
|
17
|
+
/**
|
|
18
|
+
* A many2one read: the name, what it is beside it, its face, and the card that floats from the
|
|
19
|
+
* name with its state and when it was made and last changed.
|
|
20
|
+
*/
|
|
21
|
+
export declare function relationOf(summary: RecordSummary): RelationProps;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { frameworkCopy } from "../../core/copy.js";
|
|
3
|
+
import { Tag } from "@sincpro/web-ui";
|
|
4
|
+
function lastChange(summary) {
|
|
5
|
+
const copy = frameworkCopy().summary;
|
|
6
|
+
const { created, updated } = summary;
|
|
7
|
+
if (updated.at !== null)
|
|
8
|
+
return copy.updatedLine(updated.at, updated.by);
|
|
9
|
+
return created.at === null ? null : copy.createdLine(created.at, created.by);
|
|
10
|
+
}
|
|
11
|
+
/** The board's card: the state above, the name, what it is, its description, its last change. */
|
|
12
|
+
export function cardOf(summary) {
|
|
13
|
+
const footer = lastChange(summary);
|
|
14
|
+
return {
|
|
15
|
+
title: summary.name,
|
|
16
|
+
...(summary.type === null ? {} : { subtitle: summary.type }),
|
|
17
|
+
...(summary.state === null ? {} : { tags: _jsx(Tag, { tone: "neutral", children: summary.state }) }),
|
|
18
|
+
...(summary.description === null ? {} : { meta: summary.description }),
|
|
19
|
+
...(summary.image === null ? {} : { media: { src: summary.image, alt: summary.name } }),
|
|
20
|
+
...(footer === null
|
|
21
|
+
? {}
|
|
22
|
+
: { footer: _jsx("span", { className: "text-micro text-muted", children: footer }) }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A many2one read: the name, what it is beside it, its face, and the card that floats from the
|
|
27
|
+
* name with its state and when it was made and last changed.
|
|
28
|
+
*/
|
|
29
|
+
export function relationOf(summary) {
|
|
30
|
+
const copy = frameworkCopy().summary;
|
|
31
|
+
const facts = [
|
|
32
|
+
...(summary.created.at === null
|
|
33
|
+
? []
|
|
34
|
+
: [{ label: copy.created, value: copy.when(summary.created.at, summary.created.by) }]),
|
|
35
|
+
...(summary.updated.at === null
|
|
36
|
+
? []
|
|
37
|
+
: [{ label: copy.updated, value: copy.when(summary.updated.at, summary.updated.by) }]),
|
|
38
|
+
];
|
|
39
|
+
const media = summary.person
|
|
40
|
+
? { person: true, ...(summary.image === null ? {} : { src: summary.image }) }
|
|
41
|
+
: summary.image === null
|
|
42
|
+
? undefined
|
|
43
|
+
: { src: summary.image, alt: summary.name };
|
|
44
|
+
const secondary = summary.type ?? (summary.id === summary.name ? null : summary.id);
|
|
45
|
+
const card = {
|
|
46
|
+
...(summary.state === null ? {} : { tag: { label: summary.state } }),
|
|
47
|
+
...(facts.length === 0 ? {} : { facts }),
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
name: summary.name,
|
|
51
|
+
...(secondary === null ? {} : { secondary }),
|
|
52
|
+
...(media === undefined ? {} : { media }),
|
|
53
|
+
...(Object.keys(card).length === 0 ? {} : { summary: card }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta } from "@sincpro/criteria";
|
|
2
2
|
import { type ComposerSkin, type ResourceRef, type ViewSpec } from "@sincpro/web-composer";
|
|
3
3
|
import { type CacheOptions } from "../../core/cache.js";
|
|
4
|
+
import type { Entity } from "../../core/entity.js";
|
|
4
5
|
import type { Transport } from "../../core/transport.js";
|
|
5
6
|
import { type ReactNode } from "react";
|
|
6
7
|
/**
|
|
@@ -14,10 +15,12 @@ import { type ReactNode } from "react";
|
|
|
14
15
|
* What it does NOT own: the session (the app builds the transport with its headers) and the
|
|
15
16
|
* application bar (the app draws its own, around this or beside it).
|
|
16
17
|
*/
|
|
17
|
-
export declare function SincproApp({ transport, views, start, language, metas, empty, skin, cache, }: {
|
|
18
|
+
export declare function SincproApp({ transport, views, start, language, metas, entities, empty, skin, cache, }: {
|
|
18
19
|
transport: Transport;
|
|
19
20
|
/** The entities as composer views — `entityView(…)` — and any other view of the app. */
|
|
20
21
|
views: readonly ViewSpec[];
|
|
22
|
+
/** The application's entities: a form draws a many2one of any of them without being told. */
|
|
23
|
+
entities?: readonly Entity[];
|
|
21
24
|
/** What opens first: a list, usually — `invoices.ref()`. */
|
|
22
25
|
start?: ResourceRef;
|
|
23
26
|
/** The interface's language: web-ui's, the composer's and the framework's words. */
|
|
@@ -19,7 +19,7 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
19
19
|
* What it does NOT own: the session (the app builds the transport with its headers) and the
|
|
20
20
|
* application bar (the app draws its own, around this or beside it).
|
|
21
21
|
*/
|
|
22
|
-
export function SincproApp({ transport, views, start, language = "es", metas, empty, skin = webUiSkin, cache = {}, }) {
|
|
22
|
+
export function SincproApp({ transport, views, start, language = "es", metas, entities, empty, skin = webUiSkin, cache = {}, }) {
|
|
23
23
|
// Installed once, before anything draws: the three tables are module state of their packages.
|
|
24
24
|
useState(() => {
|
|
25
25
|
if (language === "es") {
|
|
@@ -41,5 +41,5 @@ export function SincproApp({ transport, views, start, language = "es", metas, em
|
|
|
41
41
|
// The first screen only: a re-render must not push it again.
|
|
42
42
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
43
|
}, [workspace]);
|
|
44
|
-
return (_jsx(FrameworkProvider, { transport: cached, locale: language, ...(metas === undefined ? {} : { metas }), children: _jsx(ToastProvider, { children: _jsx("div", { className: "flex h-dvh min-h-0 flex-col bg-ground", children: _jsx(Composer, { workspace: workspace, registry: registry, skin: skin, ...(empty === undefined ? {} : { empty }) }) }) }) }));
|
|
44
|
+
return (_jsx(FrameworkProvider, { transport: cached, locale: language, ...(metas === undefined ? {} : { metas }), ...(entities === undefined ? {} : { entities }), children: _jsx(ToastProvider, { children: _jsx("div", { className: "flex h-dvh min-h-0 flex-col bg-ground", children: _jsx(Composer, { workspace: workspace, registry: registry, skin: skin, ...(empty === undefined ? {} : { empty }) }) }) }) }));
|
|
45
45
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type AnyRecord, type Meta } from "@sincpro/criteria";
|
|
2
2
|
import type { ViewProps, ViewSpec } from "@sincpro/web-composer";
|
|
3
|
-
import type
|
|
3
|
+
import { type Entity } from "../../core/entity.js";
|
|
4
|
+
import type { ListDefinition } from "../../core/list.js";
|
|
4
5
|
import type { Transport } from "../../core/transport.js";
|
|
5
|
-
import type
|
|
6
|
+
import { type ComponentType } from "react";
|
|
6
7
|
/**
|
|
7
8
|
* An entity as the composer's view: one `ViewSpec` for its list (a reference without an id)
|
|
8
9
|
* and its record (with one), searchable from ⌘K.
|
|
@@ -13,6 +14,10 @@ import type { ComponentType } from "react";
|
|
|
13
14
|
* `matching`), so its spec is written here once instead of by hand per kind (forge wrote
|
|
14
15
|
* eight). The screens stay the application's: `list` and `record` are its components.
|
|
15
16
|
*
|
|
17
|
+
* A reference with no id is the list — opened to choose one when it carries `pick` — and one
|
|
18
|
+
* with `new` is the record's own view with no id: the form of a record that does not exist yet
|
|
19
|
+
* (`entity.newRef()`), which `useRecord(entity, "")` fills with the model's defaults.
|
|
20
|
+
*
|
|
16
21
|
* ⌘K asks the server with `matching` — 8 records, identity and display — the same question a
|
|
17
22
|
* many2one asks. The model definition is asked for once, on the first search, unless `meta`
|
|
18
23
|
* is handed over.
|
|
@@ -21,8 +26,10 @@ export type EntityViewOptions = {
|
|
|
21
26
|
transport: Transport;
|
|
22
27
|
/** How it reads in the palette, a panel's frame and the breadcrumb. */
|
|
23
28
|
name: string;
|
|
24
|
-
list:
|
|
25
|
-
|
|
29
|
+
/** Its list. Without it, `EntityList` over the entity: search bar, pages, «Nuevo». */
|
|
30
|
+
list?: ComponentType<ViewProps>;
|
|
31
|
+
/** Its record. Without it, `EntityRecord`: the form the model describes. */
|
|
32
|
+
record?: ComponentType<ViewProps>;
|
|
26
33
|
icon?: ViewSpec["icon"];
|
|
27
34
|
/** Listed under «Go to» in ⌘K. On by default. */
|
|
28
35
|
goto?: boolean;
|
|
@@ -31,3 +38,18 @@ export type EntityViewOptions = {
|
|
|
31
38
|
actions?: ViewSpec["actions"];
|
|
32
39
|
};
|
|
33
40
|
export declare function entityView<T extends AnyRecord>(entity: Entity<T>, options: EntityViewOptions): ViewSpec;
|
|
41
|
+
/**
|
|
42
|
+
* A named list as a view of its own: «Por cobrar» in ⌘K's «Go to», its own breadcrumb, the
|
|
43
|
+
* list the definition declares. Its rows open the ENTITY's record — the view `entityView`
|
|
44
|
+
* registered — so a list view needs no record screen of its own.
|
|
45
|
+
*
|
|
46
|
+
* views: [entityView(facturas, …), listView(porCobrar), listView(borradores)]
|
|
47
|
+
*/
|
|
48
|
+
export type ListViewOptions = {
|
|
49
|
+
/** The screen; without it, `EntityList` over the definition, as it declares. */
|
|
50
|
+
list?: ComponentType<ViewProps>;
|
|
51
|
+
icon?: ViewSpec["icon"];
|
|
52
|
+
/** Listed under «Go to» in ⌘K. On by default. */
|
|
53
|
+
goto?: boolean;
|
|
54
|
+
};
|
|
55
|
+
export declare function listView<T extends AnyRecord>(list: ListDefinition<T>, options?: ListViewOptions): ViewSpec;
|
|
@@ -1,12 +1,51 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { matching } from "@sincpro/criteria";
|
|
4
|
+
import { NEW_PARAM } from "../../core/entity.js";
|
|
4
5
|
import { lookupOf } from "../../core/lookup.js";
|
|
6
|
+
import { useListing } from "../../react/useListing.js";
|
|
7
|
+
import { EntityList } from "../list/EntityList.js";
|
|
8
|
+
import { RecordOpeningProvider, } from "../record/opening.js";
|
|
9
|
+
import { openIn, RECORD_SIZE, targetsFor } from "./open.js";
|
|
10
|
+
import { EntityRecord } from "./record.js";
|
|
11
|
+
import { useMemo } from "react";
|
|
12
|
+
/**
|
|
13
|
+
* The fields of a view open their records in the workspace: a modal over this surface by
|
|
14
|
+
* default, beside it from inside a modal, and «Buscar más…» as the entity's list that answers.
|
|
15
|
+
*/
|
|
16
|
+
function InWorkspace({ api, children }) {
|
|
17
|
+
const targets = targetsFor(api);
|
|
18
|
+
const opening = useMemo(() => ({
|
|
19
|
+
open: (ref, to) => openIn(api, ref, to),
|
|
20
|
+
targets,
|
|
21
|
+
choose: (entity, answer, query) => api.pick(entity.chooserRef(query), (ref) => ref.id === undefined || answer(ref.id), {
|
|
22
|
+
// Never a modal over a modal: from inside one, the chooser docks beside it.
|
|
23
|
+
...(targets[0] === "modal"
|
|
24
|
+
? { to: "modal", size: RECORD_SIZE }
|
|
25
|
+
: { to: "side" }),
|
|
26
|
+
}),
|
|
27
|
+
}), [api, targets]);
|
|
28
|
+
return _jsx(RecordOpeningProvider, { value: opening, children: children });
|
|
29
|
+
}
|
|
30
|
+
/** The entity's list when the application writes none. */
|
|
31
|
+
function listOf(entity) {
|
|
32
|
+
return function DefaultList({ api }) {
|
|
33
|
+
return _jsx(EntityList, { listing: useListing(entity), api: api });
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The entity's record when the application writes none. */
|
|
37
|
+
function recordOf(entity) {
|
|
38
|
+
return function DefaultRecord(props) {
|
|
39
|
+
return _jsx(EntityRecord, { ...props, entity: entity });
|
|
40
|
+
};
|
|
41
|
+
}
|
|
5
42
|
export function entityView(entity, options) {
|
|
6
|
-
const
|
|
43
|
+
const List = options.list ?? listOf(entity);
|
|
44
|
+
const One = options.record ?? recordOf(entity);
|
|
7
45
|
let meta = options.meta ?? null;
|
|
8
46
|
function EntityView(props) {
|
|
9
|
-
|
|
47
|
+
const record = props.resource.id !== undefined || props.resource.params?.[NEW_PARAM] !== undefined;
|
|
48
|
+
return (_jsx(InWorkspace, { api: props.api, children: record ? _jsx(One, { ...props }) : _jsx(List, { ...props }) }));
|
|
10
49
|
}
|
|
11
50
|
return {
|
|
12
51
|
domain: entity.domain,
|
|
@@ -35,3 +74,21 @@ export function entityView(entity, options) {
|
|
|
35
74
|
load: async () => EntityView,
|
|
36
75
|
};
|
|
37
76
|
}
|
|
77
|
+
export function listView(list, options = {}) {
|
|
78
|
+
const Screen = options.list ?? definedList(list);
|
|
79
|
+
return {
|
|
80
|
+
domain: list.entity.domain,
|
|
81
|
+
kind: list.ref().kind,
|
|
82
|
+
title: () => list.title,
|
|
83
|
+
goto: options.goto ?? true,
|
|
84
|
+
...(options.icon === undefined ? {} : { icon: options.icon }),
|
|
85
|
+
load: async () => Screen,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function definedList(list) {
|
|
89
|
+
function DefinedList({ api }) {
|
|
90
|
+
return _jsx(EntityList, { listing: useListing(list), api: api });
|
|
91
|
+
}
|
|
92
|
+
DefinedList.displayName = `List(${list.key})`;
|
|
93
|
+
return DefinedList;
|
|
94
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { AnyRecord, Meta } from "@sincpro/criteria";
|
|
2
|
+
import type { OpenOptions, ResourceRef, WorkspaceApi } from "@sincpro/web-composer";
|
|
3
|
+
import type { Entity } from "../../core/entity.js";
|
|
4
|
+
import { type OpenTarget } from "@sincpro/web-ui";
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
/**
|
|
7
|
+
* Where a record opens, in the words of web-ui (`OpenTarget`) and the workspace of composer.
|
|
8
|
+
*
|
|
9
|
+
* Context: web-ui only SAYS where the person asked to open something — the name of a many2one
|
|
10
|
+
* opens in the first of its `openTargets`, «Abrir en…» offers the rest — and composer decides
|
|
11
|
+
* what each place is. This is the one translation between them, so every screen agrees:
|
|
12
|
+
*
|
|
13
|
+
* - a related record opens in a MODAL over the surface that asked: the person was doing
|
|
14
|
+
* something here, and comes back to exactly where they were;
|
|
15
|
+
* - from INSIDE a modal it opens beside or as a preview, never a modal over a modal (a second
|
|
16
|
+
* one would replace the first, and what was typed in it is lost) — composer says where a
|
|
17
|
+
* view is (`api.over`), so no screen has to;
|
|
18
|
+
* - a modal is the size of a record unless the caller says otherwise.
|
|
19
|
+
*
|
|
20
|
+
* The phone is composer's: a modal becomes a sheet, a side panel the page. No view asks.
|
|
21
|
+
*/
|
|
22
|
+
/** A record's form fits; a short task (register a payment) passes its own size. */
|
|
23
|
+
export declare const RECORD_SIZE: {
|
|
24
|
+
readonly w: 760;
|
|
25
|
+
readonly h: 560;
|
|
26
|
+
};
|
|
27
|
+
/** Where a related record may open from this view, the default first. */
|
|
28
|
+
export declare function targetsFor(api: Pick<WorkspaceApi, "over">): readonly OpenTarget[];
|
|
29
|
+
/** web-ui's target as composer's options. Destinations share their names, so only the size is
|
|
30
|
+
* added — to the two that have one. */
|
|
31
|
+
export declare function openOptions(to: OpenTarget, size?: {
|
|
32
|
+
w: number;
|
|
33
|
+
h: number;
|
|
34
|
+
}): OpenOptions;
|
|
35
|
+
/**
|
|
36
|
+
* Open a record where it was asked, or where it belongs from here.
|
|
37
|
+
*
|
|
38
|
+
* <RelationValue onOpenIn={(to) => openIn(api, clientes.ref(id), to)} />
|
|
39
|
+
* openIn(api, pagoRef, "modal", { w: 640, h: 460 }) // a short task, at its own size
|
|
40
|
+
*/
|
|
41
|
+
export declare function openIn(api: Pick<WorkspaceApi, "open" | "over">, ref: ResourceRef, to?: OpenTarget, size?: {
|
|
42
|
+
w: number;
|
|
43
|
+
h: number;
|
|
44
|
+
}): void;
|
|
45
|
+
/**
|
|
46
|
+
* A record this one points at — the customer of an invoice — read and opened: its name as a
|
|
47
|
+
* link, its face and what it is, the summary that floats from the name, and «Abrir en…».
|
|
48
|
+
*
|
|
49
|
+
* `value` is what the server sent for the many2one: the record it points at (expanded), or its
|
|
50
|
+
* key alone. Its model is the one this application knows for `entity`, unless given.
|
|
51
|
+
*
|
|
52
|
+
* <RelatedRecord api={api} entity={clientes} value={factura.record.customer} />
|
|
53
|
+
*/
|
|
54
|
+
export declare function RelatedRecord<T extends AnyRecord>({ api, entity, value, meta, editor, }: {
|
|
55
|
+
api: WorkspaceApi;
|
|
56
|
+
entity: Entity<T>;
|
|
57
|
+
value: AnyRecord | string | null | undefined;
|
|
58
|
+
meta?: Meta | null;
|
|
59
|
+
/** The control that edits the relation — an `EntityPicker` — drawn in a form's edit mode. */
|
|
60
|
+
editor?: ReactNode;
|
|
61
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useSummaryOf } from "../../react/useSummary.js";
|
|
4
|
+
import { relationOf } from "../record/summary.js";
|
|
5
|
+
import { RelationValue } from "@sincpro/web-ui";
|
|
6
|
+
/**
|
|
7
|
+
* Where a record opens, in the words of web-ui (`OpenTarget`) and the workspace of composer.
|
|
8
|
+
*
|
|
9
|
+
* Context: web-ui only SAYS where the person asked to open something — the name of a many2one
|
|
10
|
+
* opens in the first of its `openTargets`, «Abrir en…» offers the rest — and composer decides
|
|
11
|
+
* what each place is. This is the one translation between them, so every screen agrees:
|
|
12
|
+
*
|
|
13
|
+
* - a related record opens in a MODAL over the surface that asked: the person was doing
|
|
14
|
+
* something here, and comes back to exactly where they were;
|
|
15
|
+
* - from INSIDE a modal it opens beside or as a preview, never a modal over a modal (a second
|
|
16
|
+
* one would replace the first, and what was typed in it is lost) — composer says where a
|
|
17
|
+
* view is (`api.over`), so no screen has to;
|
|
18
|
+
* - a modal is the size of a record unless the caller says otherwise.
|
|
19
|
+
*
|
|
20
|
+
* The phone is composer's: a modal becomes a sheet, a side panel the page. No view asks.
|
|
21
|
+
*/
|
|
22
|
+
/** A record's form fits; a short task (register a payment) passes its own size. */
|
|
23
|
+
export const RECORD_SIZE = { w: 760, h: 560 };
|
|
24
|
+
const IN_PLACE = ["modal", "peek", "side", "page"];
|
|
25
|
+
const FROM_A_MODAL = ["side", "peek", "page"];
|
|
26
|
+
/** Where a related record may open from this view, the default first. */
|
|
27
|
+
export function targetsFor(api) {
|
|
28
|
+
return api.over === null ? IN_PLACE : FROM_A_MODAL;
|
|
29
|
+
}
|
|
30
|
+
/** web-ui's target as composer's options. Destinations share their names, so only the size is
|
|
31
|
+
* added — to the two that have one. */
|
|
32
|
+
export function openOptions(to, size = RECORD_SIZE) {
|
|
33
|
+
return to === "modal" || to === "window" ? { to, size } : { to };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Open a record where it was asked, or where it belongs from here.
|
|
37
|
+
*
|
|
38
|
+
* <RelationValue onOpenIn={(to) => openIn(api, clientes.ref(id), to)} />
|
|
39
|
+
* openIn(api, pagoRef, "modal", { w: 640, h: 460 }) // a short task, at its own size
|
|
40
|
+
*/
|
|
41
|
+
export function openIn(api, ref, to, size) {
|
|
42
|
+
const targets = targetsFor(api);
|
|
43
|
+
const where = to === undefined || !targets.includes(to) ? targets[0] : to;
|
|
44
|
+
api.open(ref, openOptions(where ?? "page", size));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A record this one points at — the customer of an invoice — read and opened: its name as a
|
|
48
|
+
* link, its face and what it is, the summary that floats from the name, and «Abrir en…».
|
|
49
|
+
*
|
|
50
|
+
* `value` is what the server sent for the many2one: the record it points at (expanded), or its
|
|
51
|
+
* key alone. Its model is the one this application knows for `entity`, unless given.
|
|
52
|
+
*
|
|
53
|
+
* <RelatedRecord api={api} entity={clientes} value={factura.record.customer} />
|
|
54
|
+
*/
|
|
55
|
+
export function RelatedRecord({ api, entity, value, meta, editor, }) {
|
|
56
|
+
const summarize = useSummaryOf(entity);
|
|
57
|
+
const extra = editor === undefined ? {} : { editor };
|
|
58
|
+
if (value === null || value === undefined || value === "")
|
|
59
|
+
return _jsx(RelationValue, { name: null, ...extra });
|
|
60
|
+
const summary = summarize(typeof value === "string" ? { id: value } : value, meta);
|
|
61
|
+
return (_jsx(RelationValue, { ...relationOf(summary), onOpenIn: (to) => openIn(api, entity.ref(summary.id), to), openTargets: targetsFor(api), ...extra }));
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AnyRecord } from "@sincpro/criteria";
|
|
2
|
+
import { type ViewProps } from "@sincpro/web-composer";
|
|
3
|
+
import { type Entity } from "../../core/entity.js";
|
|
4
|
+
import { type RecordReading } from "../../react/useRecord.js";
|
|
5
|
+
import { type ReactNode } from "react";
|
|
6
|
+
/**
|
|
7
|
+
* The record of an entity, drawn from its model — the screen `entityView` opens when the
|
|
8
|
+
* application writes none: in the page, a modal, the preview or a panel, the same.
|
|
9
|
+
*
|
|
10
|
+
* Context: every Sincpro record screen was the same hundred lines: read it, tell the workspace
|
|
11
|
+
* its name and its unsaved work, draw its name and state on top, its fields, its lines, the
|
|
12
|
+
* conflict and the restored draft, who made it, its history. Here once, from what the model and
|
|
13
|
+
* the entity already say (`formOf`, `summaryOf`): a new entity has a useful screen before anyone
|
|
14
|
+
* writes it. It is a default, not a cage — `fields`/`pages` choose and order, `actions` puts the
|
|
15
|
+
* record's commands, and any screen can drop this and keep `useRecord` + `RecordField`.
|
|
16
|
+
*
|
|
17
|
+
* entityView(invoices, { transport, name: "Facturas" }) // this screen
|
|
18
|
+
* entityView(invoices, { …, record: (props) =>
|
|
19
|
+
* <EntityRecord {...props} entity={invoices} fields={["customer_id", "due_date"]}
|
|
20
|
+
* actions={(invoice) => <CommandButton entity={invoices} command="post" … />} /> })
|
|
21
|
+
*/
|
|
22
|
+
export declare function EntityRecord<T extends AnyRecord>({ api, resource, entity, fields, pages, actions, }: ViewProps & {
|
|
23
|
+
entity: Entity<T>;
|
|
24
|
+
/** The fields, in this order. Without it, the model's (`formOf`). */
|
|
25
|
+
fields?: readonly string[];
|
|
26
|
+
/** The one2many shown as pages, in this order. Without it, every one the model has. */
|
|
27
|
+
pages?: readonly string[];
|
|
28
|
+
/** The record's own commands, beside «save / discard». */
|
|
29
|
+
actions?: (record: RecordReading<T>) => ReactNode;
|
|
30
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { labelOf, nameOf } from "@sincpro/criteria";
|
|
4
|
+
import { useCrumbs } from "@sincpro/web-composer";
|
|
5
|
+
import { frameworkCopy } from "../../core/copy.js";
|
|
6
|
+
import { TRAIL_PARAM } from "../../core/entity.js";
|
|
7
|
+
import { formOf } from "../../core/form.js";
|
|
8
|
+
import { placeOf, stepIn } from "../../core/trail.js";
|
|
9
|
+
import { useFramework } from "../../react/context.js";
|
|
10
|
+
import { useHistory } from "../../react/useHistory.js";
|
|
11
|
+
import { useRecord } from "../../react/useRecord.js";
|
|
12
|
+
import { useSummaryOf } from "../../react/useSummary.js";
|
|
13
|
+
import { dirtyOf, RecordField } from "../record/edit.js";
|
|
14
|
+
import { Refused } from "../Refused.js";
|
|
15
|
+
import { useRecordInWorkspace } from "./workspace.js";
|
|
16
|
+
import { Button, Callout, Record, Skeleton } from "@sincpro/web-ui";
|
|
17
|
+
import { useState } from "react";
|
|
18
|
+
/**
|
|
19
|
+
* The record of an entity, drawn from its model — the screen `entityView` opens when the
|
|
20
|
+
* application writes none: in the page, a modal, the preview or a panel, the same.
|
|
21
|
+
*
|
|
22
|
+
* Context: every Sincpro record screen was the same hundred lines: read it, tell the workspace
|
|
23
|
+
* its name and its unsaved work, draw its name and state on top, its fields, its lines, the
|
|
24
|
+
* conflict and the restored draft, who made it, its history. Here once, from what the model and
|
|
25
|
+
* the entity already say (`formOf`, `summaryOf`): a new entity has a useful screen before anyone
|
|
26
|
+
* writes it. It is a default, not a cage — `fields`/`pages` choose and order, `actions` puts the
|
|
27
|
+
* record's commands, and any screen can drop this and keep `useRecord` + `RecordField`.
|
|
28
|
+
*
|
|
29
|
+
* entityView(invoices, { transport, name: "Facturas" }) // this screen
|
|
30
|
+
* entityView(invoices, { …, record: (props) =>
|
|
31
|
+
* <EntityRecord {...props} entity={invoices} fields={["customer_id", "due_date"]}
|
|
32
|
+
* actions={(invoice) => <CommandButton entity={invoices} command="post" … />} /> })
|
|
33
|
+
*/
|
|
34
|
+
export function EntityRecord({ api, resource, entity, fields, pages, actions, }) {
|
|
35
|
+
const reading = useRecord(entity, resource.id ?? "");
|
|
36
|
+
const crumbs = useCrumbs(api);
|
|
37
|
+
useRecordInWorkspace(api, reading);
|
|
38
|
+
const history = useHistory(entity, reading.isNew ? "" : reading.id, [reading.record]);
|
|
39
|
+
const summarize = useSummaryOf(entity);
|
|
40
|
+
const [page, setPage] = useState(undefined);
|
|
41
|
+
const copy = frameworkCopy().record;
|
|
42
|
+
// Opened from a list: where it stands in it, and its neighbours (`Trail`).
|
|
43
|
+
const { trails, transport } = useFramework();
|
|
44
|
+
const trailKey = resource.params?.[TRAIL_PARAM];
|
|
45
|
+
const trail = trailKey === undefined ? undefined : trails.get(trailKey);
|
|
46
|
+
const go = (step) => () => {
|
|
47
|
+
if (trail === undefined || trailKey === undefined)
|
|
48
|
+
return;
|
|
49
|
+
void stepIn(transport, trail, reading.id, step).then((moved) => {
|
|
50
|
+
if (moved === null)
|
|
51
|
+
return;
|
|
52
|
+
trails.set(trailKey, moved.trail);
|
|
53
|
+
const ref = entity.ref(moved.id);
|
|
54
|
+
// In place, as Odoo's pager: the neighbour is not a step deeper, no crumb leads back.
|
|
55
|
+
api.replace({ ...ref, params: { ...ref.params, [TRAIL_PARAM]: trailKey } });
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
if (reading.status === "loading")
|
|
59
|
+
return _jsx(Skeleton, { rows: 6, label: copy.loading });
|
|
60
|
+
if (reading.status === "failed")
|
|
61
|
+
return _jsx(Refused, { error: reading.error, onRetry: reading.reload });
|
|
62
|
+
const { meta, locale, edit } = reading;
|
|
63
|
+
const layout = formOf(meta, entity.summary);
|
|
64
|
+
const shown = fields ?? layout.fields;
|
|
65
|
+
const tabs = pages ?? layout.pages;
|
|
66
|
+
const summary = reading.isNew ? undefined : summarize(reading.record, meta);
|
|
67
|
+
const dirty = dirtyOf(reading);
|
|
68
|
+
const created = summary?.created;
|
|
69
|
+
const updated = summary?.updated;
|
|
70
|
+
const stamp = (one) => one?.at == null ? undefined : { at: one.at, by: one.by ?? "—" };
|
|
71
|
+
const createdStamp = stamp(created);
|
|
72
|
+
const updatedStamp = stamp(updated);
|
|
73
|
+
// Moving away with something typed would lose it: «save / discard» comes first.
|
|
74
|
+
const place = trail === undefined || reading.isNew || edit.dirty ? null : placeOf(trail, reading.id);
|
|
75
|
+
return (_jsxs(Record, { identity: {
|
|
76
|
+
title: summary?.name ?? copy.creating(nameOf(meta, locale) || ""),
|
|
77
|
+
...(summary?.type == null ? {} : { subtitle: summary.type }),
|
|
78
|
+
...(summary?.state == null ? {} : { tag: { label: summary.state } }),
|
|
79
|
+
}, ...(dirty === undefined ? {} : { dirty }), ...(edit.failure === null ? {} : { error: String(edit.failure.message) }), children: [_jsxs(Record.Rail, { children: [crumbs.length === 0 ? null : _jsx(Record.Crumbs, { crumbs: crumbs }), place === null ? null : (_jsx(Record.Pager, { index: place.index, total: place.total, onPrev: go(-1), onNext: go(1) })), _jsx(Record.Actions, { children: actions?.(reading) ?? null })] }), _jsxs(Record.Paper, { layout: "horizontal", children: [_jsxs("div", { className: "flex flex-col gap-3 p-4", children: [edit.conflict ? (_jsx(Callout, { tone: "warn", title: copy.conflict, children: _jsx(Button, { size: "sm", role: "secondary", onClick: reading.reload, children: copy.reload }) })) : null, edit.restored ? (_jsx(Callout, { tone: "info", title: copy.restored, children: null })) : null, shown.map((name) => (_jsx(RecordField, { record: reading, name: name }, name))), edit.advice.map((one) => (_jsx(Callout, { tone: "info", title: one.text, children: null }, one.text)))] }), tabs.length === 0 || reading.isNew ? null : (_jsx(Record.Pages, { current: page ?? tabs[0], onChange: setPage, children: tabs.map((name) => (_jsx(Record.Page, { id: name, label: labelOf(meta, name, locale), children: _jsx(RecordField, { record: reading, name: name, bare: true }) }, name))) })), createdStamp === undefined && updatedStamp === undefined ? null : (_jsx(Record.Audit, { ...(createdStamp === undefined ? {} : { created: createdStamp }), ...(updatedStamp === undefined ? {} : { updated: updatedStamp }) }))] }), history === undefined ? null : _jsx(Record.Activity, { events: history })] }));
|
|
80
|
+
}
|
|
@@ -1,11 +1,2 @@
|
|
|
1
1
|
import type { ComposerSkin } from "@sincpro/web-composer";
|
|
2
|
-
/**
|
|
3
|
-
* The composer, dressed as web-ui.
|
|
4
|
-
*
|
|
5
|
-
* Context: since 1.0 the composer owns the BEHAVIOUR of the workspace and ships a minimal look
|
|
6
|
-
* of its own so it works alone; the look is the application's, piece by piece (`skin`). This
|
|
7
|
-
* package is married to web-ui, so its workspace should read as web-ui: lucide's icons, web-ui's
|
|
8
|
-
* `Peek` drawer, its `Dialog` for «discard what you typed?», its `Missing` where a view could not
|
|
9
|
-
* be drawn. `<SincproApp>` uses it by default; any piece can still be replaced on top of it.
|
|
10
|
-
*/
|
|
11
2
|
export declare const webUiSkin: ComposerSkin;
|