@sincpro/web-framework 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +224 -2
- package/dist/core/cache.d.ts +30 -0
- package/dist/core/cache.js +47 -0
- package/dist/core/copy.d.ts +69 -0
- package/dist/core/copy.js +111 -0
- package/dist/core/edit.d.ts +27 -0
- package/dist/core/edit.js +51 -0
- package/dist/core/entity.d.ts +79 -0
- package/dist/core/entity.js +32 -0
- package/dist/core/history.d.ts +20 -0
- package/dist/core/history.js +40 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +19 -0
- package/dist/core/lookup.d.ts +16 -0
- package/dist/core/lookup.js +10 -0
- package/dist/core/memory.d.ts +43 -0
- package/dist/core/memory.js +85 -0
- package/dist/core/meta.d.ts +114 -0
- package/dist/core/meta.js +130 -0
- package/dist/core/metas.d.ts +18 -0
- package/dist/core/metas.js +7 -0
- package/dist/core/present.d.ts +48 -0
- package/dist/core/present.js +165 -0
- package/dist/core/related.d.ts +22 -0
- package/dist/core/related.js +21 -0
- package/dist/core/rest.d.ts +30 -0
- package/dist/core/rest.js +50 -0
- package/dist/core/schema.d.ts +46 -0
- package/dist/core/schema.js +128 -0
- package/dist/core/transport.d.ts +59 -0
- package/dist/core/transport.js +43 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/react/context.d.ts +27 -0
- package/dist/react/context.js +18 -0
- package/dist/react/index.d.ts +12 -0
- package/dist/react/index.js +12 -0
- package/dist/react/useCommand.d.ts +28 -0
- package/dist/react/useCommand.js +52 -0
- package/dist/react/useHistory.d.ts +5 -0
- package/dist/react/useHistory.js +41 -0
- package/dist/react/useListing.d.ts +75 -0
- package/dist/react/useListing.js +121 -0
- package/dist/react/useLookup.d.ts +22 -0
- package/dist/react/useLookup.js +15 -0
- package/dist/react/useRecord.d.ts +74 -0
- package/dist/react/useRecord.js +176 -0
- package/dist/react/useRelated.d.ts +21 -0
- package/dist/react/useRelated.js +32 -0
- package/dist/web/Refused.d.ts +9 -0
- package/dist/web/Refused.js +14 -0
- package/dist/web/index.d.ts +17 -0
- package/dist/web/index.js +17 -0
- package/dist/web/list/EntityList.d.ts +69 -0
- package/dist/web/list/EntityList.js +82 -0
- package/dist/web/list/EntitySearchBar.d.ts +26 -0
- package/dist/web/list/EntitySearchBar.js +43 -0
- package/dist/web/list/columns.d.ts +22 -0
- package/dist/web/list/columns.js +27 -0
- package/dist/web/list/search.d.ts +21 -0
- package/dist/web/list/search.js +72 -0
- package/dist/web/record/CommandButton.d.ts +22 -0
- package/dist/web/record/CommandButton.js +40 -0
- package/dist/web/record/EntityPicker.d.ts +27 -0
- package/dist/web/record/EntityPicker.js +39 -0
- package/dist/web/record/RelatedList.d.ts +18 -0
- package/dist/web/record/RelatedList.js +16 -0
- package/dist/web/record/edit.d.ts +35 -0
- package/dist/web/record/edit.js +63 -0
- package/dist/web/record/record.d.ts +25 -0
- package/dist/web/record/record.js +56 -0
- package/dist/web/workspace/SincproApp.d.ts +37 -0
- package/dist/web/workspace/SincproApp.js +45 -0
- package/dist/web/workspace/composer.d.ts +33 -0
- package/dist/web/workspace/composer.js +37 -0
- package/dist/web/workspace/skin.d.ts +11 -0
- package/dist/web/workspace/skin.js +33 -0
- package/dist/web/workspace/workspace.d.ts +16 -0
- package/dist/web/workspace/workspace.js +29 -0
- package/package.json +86 -4
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AnyRecord, Criteria } from "@sincpro/criteria";
|
|
2
|
+
import type { Entity } from "../core/entity.js";
|
|
3
|
+
import type { Listing, ListingOptions } from "./useListing.js";
|
|
4
|
+
import type { RecordReading } from "./useRecord.js";
|
|
5
|
+
/**
|
|
6
|
+
* The records a relation of ONE record points to, as a live listing of the related entity.
|
|
7
|
+
*
|
|
8
|
+
* Context: sincpro_framework sends a one2many inside its record as ONE page (`RecordLines` draws
|
|
9
|
+
* it). A customer with forty thousand invoices, a tenant with a thousand members, the lines of a
|
|
10
|
+
* big order need more than that page: search in them, order them, walk them. The model says how
|
|
11
|
+
* to ask — the related aggregate's key that points back (`identified_by`) — so the relation is
|
|
12
|
+
* a criteria over the related entity, `where <that key> = <this record's key>`, answered by the
|
|
13
|
+
* server like any list. The relation's scope is AND-ed with everything else, so no search, filter
|
|
14
|
+
* or page can ever leave it.
|
|
15
|
+
*
|
|
16
|
+
* const facturas = useRelated(cliente, "invoices", invoicesEntity);
|
|
17
|
+
* <EntityList listing={facturas} embedded />
|
|
18
|
+
*/
|
|
19
|
+
export declare function useRelated<P extends AnyRecord, T extends AnyRecord>(record: RecordReading<P>, name: string, related: Entity<T>, options?: Omit<ListingOptions<T>, "criteria"> & {
|
|
20
|
+
criteria?: Criteria<T>;
|
|
21
|
+
}): Listing<T>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useListing } from "./useListing.js";
|
|
3
|
+
/**
|
|
4
|
+
* The records a relation of ONE record points to, as a live listing of the related entity.
|
|
5
|
+
*
|
|
6
|
+
* Context: sincpro_framework sends a one2many inside its record as ONE page (`RecordLines` draws
|
|
7
|
+
* it). A customer with forty thousand invoices, a tenant with a thousand members, the lines of a
|
|
8
|
+
* big order need more than that page: search in them, order them, walk them. The model says how
|
|
9
|
+
* to ask — the related aggregate's key that points back (`identified_by`) — so the relation is
|
|
10
|
+
* a criteria over the related entity, `where <that key> = <this record's key>`, answered by the
|
|
11
|
+
* server like any list. The relation's scope is AND-ed with everything else, so no search, filter
|
|
12
|
+
* or page can ever leave it.
|
|
13
|
+
*
|
|
14
|
+
* const facturas = useRelated(cliente, "invoices", invoicesEntity);
|
|
15
|
+
* <EntityList listing={facturas} embedded />
|
|
16
|
+
*/
|
|
17
|
+
export function useRelated(record, name, related, options = {}) {
|
|
18
|
+
const field = record.meta?.fields[name];
|
|
19
|
+
const key = field?.identified_by ?? "";
|
|
20
|
+
const own = record.meta?.get_id || record.meta?.identity || "id";
|
|
21
|
+
const value = record.record === null ? null : record.record[own];
|
|
22
|
+
const scope = {
|
|
23
|
+
...options.criteria,
|
|
24
|
+
where: {
|
|
25
|
+
all: [
|
|
26
|
+
...(options.criteria?.where ? [options.criteria.where] : []),
|
|
27
|
+
{ field: key, operator: "=", value: value },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
return useListing(related, { pageSize: 10, ...options, criteria: scope });
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A failure, said the way the server said it: its title, what happened and what to do —
|
|
3
|
+
* `guidance` included, which is the part a person acts on. Retry only when retrying can help:
|
|
4
|
+
* a «you have no access» with a retry button is a button that lies.
|
|
5
|
+
*/
|
|
6
|
+
export declare function Refused({ error, onRetry }: {
|
|
7
|
+
error: unknown;
|
|
8
|
+
onRetry?: () => void;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { frameworkCopy } from "../core/copy.js";
|
|
4
|
+
import { explain } from "../core/transport.js";
|
|
5
|
+
import { Button, Failed } from "@sincpro/web-ui";
|
|
6
|
+
/**
|
|
7
|
+
* A failure, said the way the server said it: its title, what happened and what to do —
|
|
8
|
+
* `guidance` included, which is the part a person acts on. Retry only when retrying can help:
|
|
9
|
+
* a «you have no access» with a retry button is a button that lies.
|
|
10
|
+
*/
|
|
11
|
+
export function Refused({ error, onRetry }) {
|
|
12
|
+
const { heading, sentence, retryable } = explain(error);
|
|
13
|
+
return (_jsx(Failed, { heading: heading, sentence: sentence, action: onRetry !== undefined && retryable ? (_jsx(Button, { size: "sm", role: "secondary", onClick: onRetry, children: frameworkCopy().failure.retry })) : undefined }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web bindings: what draws a `Listing` and a record with `@sincpro/web-ui`.
|
|
3
|
+
*/
|
|
4
|
+
export { type ColumnOptions, columnsOf } from "./list/columns.js";
|
|
5
|
+
export { EntityList, type EntityListProps, type NamedFilter, } from "./list/EntityList.js";
|
|
6
|
+
export { EntitySearchBar } from "./list/EntitySearchBar.js";
|
|
7
|
+
export { facetsOf, modelOf } from "./list/search.js";
|
|
8
|
+
export { CommandButton } from "./record/CommandButton.js";
|
|
9
|
+
export { dirtyOf, type FieldControl, RecordField, } from "./record/edit.js";
|
|
10
|
+
export { EntityPicker } from "./record/EntityPicker.js";
|
|
11
|
+
export { countOf, factsOf, RecordLines } from "./record/record.js";
|
|
12
|
+
export { RelatedList } from "./record/RelatedList.js";
|
|
13
|
+
export { Refused } from "./Refused.js";
|
|
14
|
+
export { entityView, type EntityViewOptions, } from "./workspace/composer.js";
|
|
15
|
+
export { SincproApp } from "./workspace/SincproApp.js";
|
|
16
|
+
export { webUiSkin } from "./workspace/skin.js";
|
|
17
|
+
export { useRecordInWorkspace } from "./workspace/workspace.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The web bindings: what draws a `Listing` and a record with `@sincpro/web-ui`.
|
|
3
|
+
*/
|
|
4
|
+
export { columnsOf } from "./list/columns.js";
|
|
5
|
+
export { EntityList, } from "./list/EntityList.js";
|
|
6
|
+
export { EntitySearchBar } from "./list/EntitySearchBar.js";
|
|
7
|
+
export { facetsOf, modelOf } from "./list/search.js";
|
|
8
|
+
export { CommandButton } from "./record/CommandButton.js";
|
|
9
|
+
export { dirtyOf, RecordField, } from "./record/edit.js";
|
|
10
|
+
export { EntityPicker } from "./record/EntityPicker.js";
|
|
11
|
+
export { countOf, factsOf, RecordLines } from "./record/record.js";
|
|
12
|
+
export { RelatedList } from "./record/RelatedList.js";
|
|
13
|
+
export { Refused } from "./Refused.js";
|
|
14
|
+
export { entityView, } from "./workspace/composer.js";
|
|
15
|
+
export { SincproApp } from "./workspace/SincproApp.js";
|
|
16
|
+
export { webUiSkin } from "./workspace/skin.js";
|
|
17
|
+
export { useRecordInWorkspace } from "./workspace/workspace.js";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type AnyRecord } from "@sincpro/criteria";
|
|
2
|
+
import { type WorkspaceApi } from "@sincpro/web-composer";
|
|
3
|
+
import type { Listing } from "../../react/useListing.js";
|
|
4
|
+
import { type NamedFilter } from "./EntitySearchBar.js";
|
|
5
|
+
import { type ListCrumb } from "@sincpro/web-ui";
|
|
6
|
+
import { type ReactNode } from "react";
|
|
7
|
+
/**
|
|
8
|
+
* A whole collection from a `Listing`: the bar, the search, the set in the views it offers,
|
|
9
|
+
* and the way through it — the screen forge wrote five times by hand, written once.
|
|
10
|
+
*
|
|
11
|
+
* Context: this is the ready-made composition, not a cage. Everything it does is reachable on
|
|
12
|
+
* its own — `columnsOf` for the columns, `EntitySearchBar` for the search, `useListing` for the
|
|
13
|
+
* state — so a screen that needs its own bar or a second table writes `List` with those.
|
|
14
|
+
*
|
|
15
|
+
* Built for sets of any size: in `pages` mode (the default) the screen holds ONE page and walks
|
|
16
|
+
* the server's cursor, so a million rows cost what fifty do; in `more` mode it stops growing at
|
|
17
|
+
* its ceiling and asks to narrow the search. Nothing is filtered, ordered or counted on the
|
|
18
|
+
* page: every gesture is a question to the server.
|
|
19
|
+
*
|
|
20
|
+
* Where a record opens is the composer's question: with `api`, a row opens the page and the
|
|
21
|
+
* identity column is the composer's `RecordLink` (peek, side panel, window); without it, the
|
|
22
|
+
* screen passes `onOpen`.
|
|
23
|
+
*/
|
|
24
|
+
export type { NamedFilter };
|
|
25
|
+
export type EntityListProps<T extends AnyRecord> = {
|
|
26
|
+
listing: Listing<T>;
|
|
27
|
+
/**
|
|
28
|
+
* The workspace this list is a view of. With it, a row opens its record as a page, the
|
|
29
|
+
* identity column is the composer's `RecordLink` (peek, side panel, window on hover) and the
|
|
30
|
+
* breadcrumb is the composer's — nothing for the screen to write.
|
|
31
|
+
*/
|
|
32
|
+
api?: WorkspaceApi;
|
|
33
|
+
/** The fields shown, in order. Without it, every scalar field of the model. */
|
|
34
|
+
columns?: readonly string[];
|
|
35
|
+
/** The cells that are not plain text: a link, a tag. Memoise it: compared by identity. */
|
|
36
|
+
cells?: Readonly<Partial<Record<string, (row: T) => ReactNode>>>;
|
|
37
|
+
/**
|
|
38
|
+
* The ways to look at the set, the first one shown: `table` (columns), `rows` (a name and
|
|
39
|
+
* its facts, what a narrow panel wants) and `board` (cards). The same set, the same search,
|
|
40
|
+
* the same page in each.
|
|
41
|
+
*/
|
|
42
|
+
views?: readonly ("table" | "rows" | "board")[];
|
|
43
|
+
/** Without it, the model's own name. */
|
|
44
|
+
title?: string;
|
|
45
|
+
description?: string;
|
|
46
|
+
crumbs?: readonly ListCrumb[];
|
|
47
|
+
actions?: ReactNode;
|
|
48
|
+
onOpen?: (row: T) => void;
|
|
49
|
+
onPeek?: (row: T) => void;
|
|
50
|
+
/** What an empty list says. */
|
|
51
|
+
empty?: string;
|
|
52
|
+
/**
|
|
53
|
+
* `"bar"` (the default) is the full search bar with the criteria builder; `"box"` is one
|
|
54
|
+
* text box over the entity's search template — a chooser, a narrow panel; `false` is none.
|
|
55
|
+
*/
|
|
56
|
+
search?: "bar" | "box" | false;
|
|
57
|
+
/** The cuts this list offers by name: «sólo pagadas», «vencidas». */
|
|
58
|
+
filters?: readonly NamedFilter<T>[];
|
|
59
|
+
/**
|
|
60
|
+
* Rows can be ticked. With `api`, what is ticked is PUBLISHED to the workspace
|
|
61
|
+
* (`api.select`): a panel beside it follows the selection without knowing this list.
|
|
62
|
+
*/
|
|
63
|
+
selectable?: boolean;
|
|
64
|
+
/** What to do with what is ticked: drawn in the bar while something is. */
|
|
65
|
+
selectionActions?: ReactNode;
|
|
66
|
+
/** The list is a FIELD of a record — its lines, its members — and not a screen. */
|
|
67
|
+
embedded?: boolean;
|
|
68
|
+
};
|
|
69
|
+
export declare function EntityList<T extends AnyRecord>({ listing, api, columns, cells, views, title, description, crumbs, actions, onOpen, onPeek, empty, search, filters, selectable, selectionActions, embedded, }: EntityListProps<T>): import("react").JSX.Element;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { nameOf } from "@sincpro/criteria";
|
|
4
|
+
import { RecordLink } from "@sincpro/web-composer";
|
|
5
|
+
import { frameworkCopy } from "../../core/copy.js";
|
|
6
|
+
import { textOf, viewOf } from "../../core/present.js";
|
|
7
|
+
import { explain } from "../../core/transport.js";
|
|
8
|
+
import { columnsOf } from "./columns.js";
|
|
9
|
+
import { EntitySearchBar, } from "./EntitySearchBar.js";
|
|
10
|
+
import { Button, List, Pagination, SearchField, Tag } from "@sincpro/web-ui";
|
|
11
|
+
import { useMemo, useState } from "react";
|
|
12
|
+
export function EntityList({ listing, api, columns, cells, views = ["table"], title, description, crumbs, actions, onOpen, onPeek, empty, search = "bar", filters = [], selectable = false, selectionActions, embedded = false, }) {
|
|
13
|
+
const copy = frameworkCopy().list;
|
|
14
|
+
const { meta, locale, count, page } = listing;
|
|
15
|
+
const identity = meta?.identity ?? "id";
|
|
16
|
+
const idOf = (row) => String(row[identity]);
|
|
17
|
+
const failed = listing.status === "failed" ? explain(listing.error) : null;
|
|
18
|
+
const dropped = listing.dropped.map((d) => viewOf(meta, d.field, locale).label);
|
|
19
|
+
const [present, setPresent] = useState(views[0] ?? "table");
|
|
20
|
+
const opening = onOpen ?? (api ? (row) => api.open(listing.entity.ref(idOf(row))) : undefined);
|
|
21
|
+
// A row click opens the page — the composer's one predictable gesture. web-ui's table turns a
|
|
22
|
+
// row click into a PEEK whenever `onPeek` is given, so the list never passes one by itself:
|
|
23
|
+
// the peek, the side panel and the window are the composer's `RecordLink` on the identity.
|
|
24
|
+
const linked = api === undefined || cells?.[identity] !== undefined
|
|
25
|
+
? cells
|
|
26
|
+
: {
|
|
27
|
+
...cells,
|
|
28
|
+
[identity]: (row) => (_jsx(RecordLink, { api: api, resource: listing.entity.ref(idOf(row)), children: idOf(row) })),
|
|
29
|
+
};
|
|
30
|
+
const trail = crumbs ?? (embedded ? undefined : api?.crumbs());
|
|
31
|
+
const [ticked, setTicked] = useState(() => new Set());
|
|
32
|
+
const tick = (next) => {
|
|
33
|
+
setTicked(next);
|
|
34
|
+
api?.select([...next].map((id) => listing.entity.ref(id)));
|
|
35
|
+
};
|
|
36
|
+
const selection = selectable
|
|
37
|
+
? {
|
|
38
|
+
has: (id) => ticked.has(id),
|
|
39
|
+
toggle: (id) => {
|
|
40
|
+
const next = new Set(ticked);
|
|
41
|
+
if (next.has(id))
|
|
42
|
+
next.delete(id);
|
|
43
|
+
else
|
|
44
|
+
next.add(id);
|
|
45
|
+
tick(next);
|
|
46
|
+
},
|
|
47
|
+
count: ticked.size,
|
|
48
|
+
clear: () => tick(new Set()),
|
|
49
|
+
...(selectionActions === undefined ? {} : { actions: selectionActions }),
|
|
50
|
+
}
|
|
51
|
+
: undefined;
|
|
52
|
+
// Rebuilt only when what it reads changes: a new set of columns on every render redraws
|
|
53
|
+
// every cell of the table. `columns` is compared by content (it is usually a literal);
|
|
54
|
+
// `cells` by identity, since its functions may close over state — memoise it to keep this.
|
|
55
|
+
const names = columns?.join("\u0000");
|
|
56
|
+
const shown = useMemo(() => columnsOf(meta, columns, { locale, ...(linked ? { cells: linked } : {}) }),
|
|
57
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
58
|
+
[meta, locale, names, cells, api]);
|
|
59
|
+
// A row and a card read the record by its display and the first fields shown beside it.
|
|
60
|
+
const display = meta?.display || identity;
|
|
61
|
+
const facts = shown
|
|
62
|
+
.filter((c) => c.id !== display && c.id !== identity && !c.defaultHidden)
|
|
63
|
+
.slice(0, 3);
|
|
64
|
+
const rowOf = (row) => ({
|
|
65
|
+
name: textOf(meta, display, row[display], locale) ?? idOf(row),
|
|
66
|
+
meta: [
|
|
67
|
+
...(display === identity ? [] : [idOf(row)]),
|
|
68
|
+
...facts.map((c) => textOf(meta, c.id, row[c.id], locale)).filter(Boolean),
|
|
69
|
+
].join(" · "),
|
|
70
|
+
});
|
|
71
|
+
return (_jsxs(List, { items: listing.rows, keyOf: idOf, embedded: embedded, present: present, onPresentChange: (next) => setPresent(next), sort: listing.order === null
|
|
72
|
+
? null
|
|
73
|
+
: { key: listing.order.field, direction: listing.order.descending ? "desc" : "asc" }, onSortChange: (next) => listing.orderBy(next === null ? null : { field: next.key, descending: next.direction === "desc" }), state: {
|
|
74
|
+
loading: listing.status === "loading",
|
|
75
|
+
error: failed === null
|
|
76
|
+
? null
|
|
77
|
+
: [failed.heading, failed.sentence].filter(Boolean).join(". "),
|
|
78
|
+
}, filtered: listing.where !== undefined || listing.text !== "", empty: {
|
|
79
|
+
sentence: empty ?? copy.empty,
|
|
80
|
+
whenFiltered: { sentence: copy.emptyFiltered },
|
|
81
|
+
}, ...(selection === undefined ? {} : { selection }), ...(opening === undefined ? {} : { onOpen: opening }), ...(onPeek === undefined ? {} : { onPeek }), children: [_jsxs(List.Bar, { children: [trail === undefined || trail.length === 0 ? null : _jsx(List.Crumbs, { crumbs: trail }), embedded ? null : (_jsx(List.Identity, { title: title ?? nameOf(meta, locale), ...(description === undefined ? {} : { description }) })), search === "bar" ? (_jsx(List.Search, { children: _jsx(EntitySearchBar, { listing: listing, filters: filters }) })) : null, search === "box" && meta?.search != null ? (_jsx(List.Search, { children: _jsx(SearchField, { value: listing.text, onChange: listing.search, placeholder: copy.search, "aria-label": copy.search }) })) : null, actions === undefined ? null : _jsx(List.Actions, { children: actions }), _jsxs(List.Pagination, { children: [dropped.length === 0 ? null : (_jsx(Tag, { tone: "warn", title: copy.dropped(dropped), children: copy.dropped(dropped) })), listing.paging === "pages" ? (page.from === 0 ? null : (_jsx(Pagination, { from: page.from, to: page.to, total: count?.value ?? page.to, ...(page.hasPrev ? { onPrev: listing.prev } : {}), ...(page.hasNext ? { onNext: listing.next } : {}) }))) : (_jsxs(_Fragment, { children: [count === null ? null : (_jsx("span", { className: "text-meta text-muted", children: copy.shown(listing.rows.length, count.value, count.exact) })), listing.capped ? (_jsx("span", { className: "text-meta text-muted", children: copy.capped(listing.rows.length) })) : listing.hasMore ? (_jsx(Button, { size: "sm", role: "secondary", disabled: listing.status === "loadingMore", onClick: listing.more, children: listing.status === "loadingMore" ? copy.loading : copy.loadMore })) : null] }))] })] }), views.includes("table") ? _jsx(List.Table, { columns: shown }) : null, views.includes("rows") ? _jsx(List.Rows, { row: rowOf }) : null, views.includes("board") ? _jsx(List.Board, {}) : null] }));
|
|
82
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AnyRecord, Expression } from "@sincpro/criteria";
|
|
2
|
+
import type { Listing } from "../../react/useListing.js";
|
|
3
|
+
/**
|
|
4
|
+
* web-ui's search bar and criteria builder, bound to a `Listing`.
|
|
5
|
+
*
|
|
6
|
+
* Context: this is the «search over a set» every collection screen needs — a list, the lines
|
|
7
|
+
* of a record, a chooser — so it is a piece of its own, not something only `EntityList` can do.
|
|
8
|
+
* A screen that writes its own `List` drops it in `List.Search` and gets the same behaviour:
|
|
9
|
+
*
|
|
10
|
+
* - «search by name»: facets are the fields the entity's search template names first;
|
|
11
|
+
* - a typed question («total > 5000») is checked against each field's operators;
|
|
12
|
+
* - a named filter is one click; the builder writes what nobody foresaw (all/any/not);
|
|
13
|
+
* - everything put becomes ONE `where` the SERVER answers (`listing.filter`).
|
|
14
|
+
*
|
|
15
|
+
* <List.Search><EntitySearchBar listing={listing} filters={…} /></List.Search>
|
|
16
|
+
*/
|
|
17
|
+
export type NamedFilter<T> = {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
where: Expression<T>;
|
|
21
|
+
};
|
|
22
|
+
export declare function EntitySearchBar<T extends AnyRecord>({ listing, filters, }: {
|
|
23
|
+
listing: Listing<T>;
|
|
24
|
+
/** The cuts this set offers by name: «sólo pagadas», «vencidas». */
|
|
25
|
+
filters?: readonly NamedFilter<T>[];
|
|
26
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { frameworkCopy } from "../../core/copy.js";
|
|
4
|
+
import { facetsOf, modelOf } from "./search.js";
|
|
5
|
+
import { CriteriaBuilder, describeFilter, Dialog, filterCriteria, hasNamed, SearchBar, toggleNamed, whereOf, } from "@sincpro/web-ui";
|
|
6
|
+
import { useMemo, useState } from "react";
|
|
7
|
+
export function EntitySearchBar({ listing, filters = [], }) {
|
|
8
|
+
const copy = frameworkCopy().list;
|
|
9
|
+
const { meta, locale } = listing;
|
|
10
|
+
const [put, setPut] = useState([]);
|
|
11
|
+
const [building, setBuilding] = useState(false);
|
|
12
|
+
const model = useMemo(() => (meta ? modelOf(meta, locale) : undefined), [meta, locale]);
|
|
13
|
+
const facets = useMemo(() => (meta ? facetsOf(meta, locale) : []), [meta, locale]);
|
|
14
|
+
if (model === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const place = (next) => {
|
|
17
|
+
setPut(next);
|
|
18
|
+
listing.filter(whereOf(next));
|
|
19
|
+
};
|
|
20
|
+
return (_jsxs(_Fragment, { children: [_jsx(SearchBar, { facets: facets, model: model, criteria: put, onCriteriaChange: place, placeholder: copy.searchBar, ...(filters.length === 0
|
|
21
|
+
? {}
|
|
22
|
+
: {
|
|
23
|
+
filters: [
|
|
24
|
+
{
|
|
25
|
+
options: filters.map((one) => ({
|
|
26
|
+
id: one.id,
|
|
27
|
+
label: one.label,
|
|
28
|
+
checked: hasNamed(put, one.id),
|
|
29
|
+
})),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
onFilterSelect: (id) => {
|
|
33
|
+
const named = filters.find((one) => one.id === id);
|
|
34
|
+
if (named !== undefined)
|
|
35
|
+
place(toggleNamed(put, { ...named, where: named.where }));
|
|
36
|
+
},
|
|
37
|
+
}), onOpenBuilder: () => setBuilding(true) }), building ? (_jsx(Dialog, { open: true, width: "lg", title: copy.builder, onClose: () => setBuilding(false), children: _jsx(CriteriaBuilder, { model: model, ...(whereOf(put) === undefined ? {} : { initial: { where: whereOf(put) } }), onApply: (built) => {
|
|
38
|
+
place(built?.where === undefined
|
|
39
|
+
? []
|
|
40
|
+
: [filterCriteria(built.where, describeFilter(built.where, model))]);
|
|
41
|
+
setBuilding(false);
|
|
42
|
+
}, onCancel: () => setBuilding(false) }) })) : null] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnyRecord, Meta } from "@sincpro/criteria";
|
|
2
|
+
import type { DataColumn } from "@sincpro/web-ui";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* The columns of a table, from the model: name the fields, the rest follows.
|
|
6
|
+
*
|
|
7
|
+
* Context: forge's lists wrote every column by hand — `header: "nombre"`, `value: (p) =>
|
|
8
|
+
* p.display_name`, a date formatter per screen — while the server already said what each field
|
|
9
|
+
* is called and what type it holds. Here a column is a NAME: its header is the model's label,
|
|
10
|
+
* its cell the text the value reads as (`textOf`), its sort key the raw value, and whether it
|
|
11
|
+
* can be ordered is what the server said (`sortable`). A cell that needs more than text — a
|
|
12
|
+
* link, a tag — is the one thing a screen writes (`cells`).
|
|
13
|
+
*
|
|
14
|
+
* The fields NOT named still exist: they come after, `optional` and hidden, so the columns
|
|
15
|
+
* menu offers them and nobody edits the screen to see a field the server already sends.
|
|
16
|
+
*/
|
|
17
|
+
export type ColumnOptions<T> = {
|
|
18
|
+
locale?: string | undefined;
|
|
19
|
+
/** The cells that are not plain text, by field name. */
|
|
20
|
+
cells?: Readonly<Partial<Record<string, (row: T) => ReactNode>>>;
|
|
21
|
+
};
|
|
22
|
+
export declare function columnsOf<T extends AnyRecord>(meta: Meta | null | undefined, names?: readonly string[], options?: ColumnOptions<T>): DataColumn<T>[];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BOOKKEEPING, defaultFields, sortKeyOf, textOf, viewOf, } from "../../core/present.js";
|
|
2
|
+
function columnOf(meta, name, options, hidden) {
|
|
3
|
+
const view = viewOf(meta, name, options.locale);
|
|
4
|
+
const text = (row) => textOf(meta, name, row[name], options.locale) ?? "";
|
|
5
|
+
const own = options.cells?.[name];
|
|
6
|
+
return {
|
|
7
|
+
id: name,
|
|
8
|
+
header: view.label,
|
|
9
|
+
field: name,
|
|
10
|
+
value: (row) => sortKeyOf(meta, name, row[name]),
|
|
11
|
+
cell: own ?? text,
|
|
12
|
+
text,
|
|
13
|
+
numeric: view.numeric,
|
|
14
|
+
sortable: view.sortable,
|
|
15
|
+
...(view.name === meta?.identity ? { mono: true } : {}),
|
|
16
|
+
...(hidden ? { optional: true, defaultHidden: true } : {}),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function columnsOf(meta, names, options = {}) {
|
|
20
|
+
const all = defaultFields(meta);
|
|
21
|
+
const shown = names ?? all.filter((name) => !BOOKKEEPING.has(name));
|
|
22
|
+
const rest = all.filter((name) => !shown.includes(name));
|
|
23
|
+
return [
|
|
24
|
+
...shown.map((name) => columnOf(meta, name, options, false)),
|
|
25
|
+
...rest.map((name) => columnOf(meta, name, options, true)),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Meta } from "@sincpro/criteria";
|
|
2
|
+
import type { ModelDefinition, SearchFacet } from "@sincpro/web-ui";
|
|
3
|
+
/**
|
|
4
|
+
* The model, as web-ui's search bar and criteria builder read it.
|
|
5
|
+
*
|
|
6
|
+
* Context: web-ui keeps its OWN vocabulary of a model (`ModelDefinition`), written before the
|
|
7
|
+
* framework settled its wire, and the two drifted: `identifiedBy` against `identified_by`, one
|
|
8
|
+
* `translations` object against a `label` on each field. web-ui is right to own it — it is a
|
|
9
|
+
* design system and must not depend on one backend — so the translation lives here, the one
|
|
10
|
+
* place that knows both.
|
|
11
|
+
*
|
|
12
|
+
* Known gap: web-ui's field has `choices` but no word for each choice, so the bar offers an
|
|
13
|
+
* enum's raw values («paid») where a cell says «Pagada». Fixing it is web-ui's.
|
|
14
|
+
*/
|
|
15
|
+
export declare function modelOf(meta: Meta, locale?: string): ModelDefinition;
|
|
16
|
+
/**
|
|
17
|
+
* Where a typed text can be looked for: the fields the entity's own search template names
|
|
18
|
+
* first (what «search by name» means for THIS entity), then every other text field it can
|
|
19
|
+
* filter on.
|
|
20
|
+
*/
|
|
21
|
+
export declare function facetsOf(meta: Meta, locale?: string): SearchFacet[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { labelOf } from "@sincpro/criteria";
|
|
2
|
+
/**
|
|
3
|
+
* The model, as web-ui's search bar and criteria builder read it.
|
|
4
|
+
*
|
|
5
|
+
* Context: web-ui keeps its OWN vocabulary of a model (`ModelDefinition`), written before the
|
|
6
|
+
* framework settled its wire, and the two drifted: `identifiedBy` against `identified_by`, one
|
|
7
|
+
* `translations` object against a `label` on each field. web-ui is right to own it — it is a
|
|
8
|
+
* design system and must not depend on one backend — so the translation lives here, the one
|
|
9
|
+
* place that knows both.
|
|
10
|
+
*
|
|
11
|
+
* Known gap: web-ui's field has `choices` but no word for each choice, so the bar offers an
|
|
12
|
+
* enum's raw values («paid») where a cell says «Pagada». Fixing it is web-ui's.
|
|
13
|
+
*/
|
|
14
|
+
export function modelOf(meta, locale) {
|
|
15
|
+
const names = Object.keys(meta.fields);
|
|
16
|
+
return {
|
|
17
|
+
aggregate: meta.aggregate,
|
|
18
|
+
identity: meta.identity,
|
|
19
|
+
fields: Object.fromEntries(Object.entries(meta.fields).map(([name, field]) => [
|
|
20
|
+
name,
|
|
21
|
+
{
|
|
22
|
+
type: field.type,
|
|
23
|
+
nullable: field.nullable,
|
|
24
|
+
sortable: field.sortable,
|
|
25
|
+
ops: field.ops,
|
|
26
|
+
kind: field.kind,
|
|
27
|
+
...(field.many === undefined ? {} : { many: field.many }),
|
|
28
|
+
...(field.relation === undefined ? {} : { relation: field.relation }),
|
|
29
|
+
...(field.identified_by === undefined ? {} : { identifiedBy: field.identified_by }),
|
|
30
|
+
...(field.choices && field.choices.length > 0 ? { choices: field.choices } : {}),
|
|
31
|
+
},
|
|
32
|
+
])),
|
|
33
|
+
translations: {
|
|
34
|
+
name: {
|
|
35
|
+
default: meta.name?.[locale ?? ""] ?? meta.name?.["default"] ?? meta.aggregate,
|
|
36
|
+
},
|
|
37
|
+
labels: Object.fromEntries(names.map((name) => [name, { default: labelOf(meta, name, locale) }])),
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The fields its search template looks in, as named in `meta.search`. */
|
|
42
|
+
function searchedFields(meta) {
|
|
43
|
+
const found = [];
|
|
44
|
+
const walk = (node) => {
|
|
45
|
+
if (typeof node !== "object" || node === null)
|
|
46
|
+
return;
|
|
47
|
+
const record = node;
|
|
48
|
+
if (typeof record["field"] === "string")
|
|
49
|
+
found.push(record["field"]);
|
|
50
|
+
for (const value of Object.values(record)) {
|
|
51
|
+
if (Array.isArray(value))
|
|
52
|
+
value.forEach(walk);
|
|
53
|
+
else
|
|
54
|
+
walk(value);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
walk(meta.search?.["where"]);
|
|
58
|
+
return found;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Where a typed text can be looked for: the fields the entity's own search template names
|
|
62
|
+
* first (what «search by name» means for THIS entity), then every other text field it can
|
|
63
|
+
* filter on.
|
|
64
|
+
*/
|
|
65
|
+
export function facetsOf(meta, locale) {
|
|
66
|
+
const first = searchedFields(meta);
|
|
67
|
+
const texts = Object.entries(meta.fields)
|
|
68
|
+
.filter(([, field]) => field.type === "text" && field.ops.includes("like") && !field.choices?.length)
|
|
69
|
+
.map(([name]) => name);
|
|
70
|
+
const order = [...new Set([...first, ...texts])];
|
|
71
|
+
return order.map((name) => ({ id: name, label: labelOf(meta, name, locale) }));
|
|
72
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnyRecord } from "@sincpro/criteria";
|
|
2
|
+
import type { Entity } from "../../core/entity.js";
|
|
3
|
+
/**
|
|
4
|
+
* An entity's Command as a button.
|
|
5
|
+
*
|
|
6
|
+
* Context: two kinds of action, two gestures. One that can be undone runs on the click — asking
|
|
7
|
+
* «are you sure?» before every confirm teaches people to click «yes» without reading. One that
|
|
8
|
+
* destroys (`destructive`) asks the server FIRST what it would do — the Command's dry run — and
|
|
9
|
+
* shows THAT in the window: the confirmation is the consequence, in the server's words, not a
|
|
10
|
+
* question. A refusal is shown where it happened, with what to do.
|
|
11
|
+
*/
|
|
12
|
+
export declare function CommandButton<T extends AnyRecord, R = unknown>({ entity, command, id, body, label, destructive, role, onDone, }: {
|
|
13
|
+
entity: Entity<T>;
|
|
14
|
+
command: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
body?: AnyRecord;
|
|
17
|
+
label: string;
|
|
18
|
+
/** Asks the server what it would do before doing it. */
|
|
19
|
+
destructive?: boolean;
|
|
20
|
+
role?: "primary" | "secondary" | "ghost";
|
|
21
|
+
onDone?: (answer: R) => void;
|
|
22
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { frameworkCopy } from "../../core/copy.js";
|
|
4
|
+
import { useCommand } from "../../react/useCommand.js";
|
|
5
|
+
import { Refused } from "../Refused.js";
|
|
6
|
+
import { Button, Dialog } from "@sincpro/web-ui";
|
|
7
|
+
import { useState } from "react";
|
|
8
|
+
/**
|
|
9
|
+
* An entity's Command as a button.
|
|
10
|
+
*
|
|
11
|
+
* Context: two kinds of action, two gestures. One that can be undone runs on the click — asking
|
|
12
|
+
* «are you sure?» before every confirm teaches people to click «yes» without reading. One that
|
|
13
|
+
* destroys (`destructive`) asks the server FIRST what it would do — the Command's dry run — and
|
|
14
|
+
* shows THAT in the window: the confirmation is the consequence, in the server's words, not a
|
|
15
|
+
* question. A refusal is shown where it happened, with what to do.
|
|
16
|
+
*/
|
|
17
|
+
export function CommandButton({ entity, command, id, body, label, destructive = false, role = "secondary", onDone, }) {
|
|
18
|
+
const copy = frameworkCopy().command;
|
|
19
|
+
const action = useCommand(entity, command);
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
const run = async () => {
|
|
22
|
+
const answer = await action.run(id, body);
|
|
23
|
+
if (answer !== undefined) {
|
|
24
|
+
setOpen(false);
|
|
25
|
+
action.reset();
|
|
26
|
+
onDone?.(answer);
|
|
27
|
+
}
|
|
28
|
+
else
|
|
29
|
+
setOpen(true);
|
|
30
|
+
};
|
|
31
|
+
const ask = async () => {
|
|
32
|
+
setOpen(true);
|
|
33
|
+
await action.rehearse(id, body);
|
|
34
|
+
};
|
|
35
|
+
const close = () => {
|
|
36
|
+
setOpen(false);
|
|
37
|
+
action.reset();
|
|
38
|
+
};
|
|
39
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", role: destructive ? "secondary" : role, disabled: action.busy, onClick: () => void (destructive ? ask() : run()), children: label }), open ? (_jsx(Dialog, { open: true, title: label, onClose: close, footer: destructive && action.error === null ? (_jsxs("div", { className: "flex justify-end gap-2", children: [_jsx(Button, { size: "sm", role: "secondary", onClick: close, children: copy.cancel }), _jsx(Button, { size: "sm", role: "primary", disabled: action.busy || action.rehearsal === null, onClick: () => void run(), children: copy.confirm })] })) : undefined, children: action.error !== null ? (_jsx(Refused, { error: action.error })) : action.rehearsal === null ? (_jsx("p", { className: "text-meta text-muted", children: copy.rehearsing })) : (_jsx("p", { className: "text-ink", children: action.rehearsal.text })) })) : null] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AnyRecord } from "@sincpro/criteria";
|
|
2
|
+
import type { Entity } from "../../core/entity.js";
|
|
3
|
+
/**
|
|
4
|
+
* A many2one: pick ONE record of an entity — web-ui's `Picker` fed by `useLookup`, with a way
|
|
5
|
+
* into the whole set however big it is.
|
|
6
|
+
*
|
|
7
|
+
* Context: two ways to choose, for two sizes. The picker holds the first page of choices and
|
|
8
|
+
* narrows it as you type — instant for a set of tens. For a set of millions, «browse
|
|
9
|
+
* everything» opens a chooser: the entity's full list, searched, ordered and paged by the
|
|
10
|
+
* SERVER, where clicking a row answers the field. web-ui's `Picker` keeps what is typed to
|
|
11
|
+
* itself and cannot ask the server by text; the chooser is how this scales without it.
|
|
12
|
+
*
|
|
13
|
+
* Inside a workspace, «open» and the chooser can be the composer's instead (`api.open`,
|
|
14
|
+
* `api.pick`): pass `onOpen` / `onBrowseAll` and they replace the defaults.
|
|
15
|
+
*/
|
|
16
|
+
export declare function EntityPicker<T extends AnyRecord>({ entity, value, onChange, onOpen, onBrowseAll, placeholder, disabled, "aria-label": ariaLabel, }: {
|
|
17
|
+
entity: Entity<T>;
|
|
18
|
+
value?: string;
|
|
19
|
+
onChange: (id: string) => void;
|
|
20
|
+
/** Open the chosen record — in composer, `(id) => api.open(entity.ref(id), { to: "window" })`. */
|
|
21
|
+
onOpen?: (id: string) => void;
|
|
22
|
+
/** Replace the built-in chooser — in composer, `api.pick(entity.ref(), …)`. */
|
|
23
|
+
onBrowseAll?: () => void;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
"aria-label"?: string;
|
|
27
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { nameOf } from "@sincpro/criteria";
|
|
4
|
+
import { useListing } from "../../react/useListing.js";
|
|
5
|
+
import { useLookup } from "../../react/useLookup.js";
|
|
6
|
+
import { EntityList } from "../list/EntityList.js";
|
|
7
|
+
import { Dialog, Picker } from "@sincpro/web-ui";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
/**
|
|
10
|
+
* A many2one: pick ONE record of an entity — web-ui's `Picker` fed by `useLookup`, with a way
|
|
11
|
+
* into the whole set however big it is.
|
|
12
|
+
*
|
|
13
|
+
* Context: two ways to choose, for two sizes. The picker holds the first page of choices and
|
|
14
|
+
* narrows it as you type — instant for a set of tens. For a set of millions, «browse
|
|
15
|
+
* everything» opens a chooser: the entity's full list, searched, ordered and paged by the
|
|
16
|
+
* SERVER, where clicking a row answers the field. web-ui's `Picker` keeps what is typed to
|
|
17
|
+
* itself and cannot ask the server by text; the chooser is how this scales without it.
|
|
18
|
+
*
|
|
19
|
+
* Inside a workspace, «open» and the chooser can be the composer's instead (`api.open`,
|
|
20
|
+
* `api.pick`): pass `onOpen` / `onBrowseAll` and they replace the defaults.
|
|
21
|
+
*/
|
|
22
|
+
export function EntityPicker({ entity, value, onChange, onOpen, onBrowseAll, placeholder, disabled, "aria-label": ariaLabel, }) {
|
|
23
|
+
const lookup = useLookup(entity);
|
|
24
|
+
const [choosing, setChoosing] = useState(false);
|
|
25
|
+
return (_jsxs(_Fragment, { children: [_jsx(Picker, { items: lookup.options.map((one) => ({
|
|
26
|
+
id: one.id,
|
|
27
|
+
name: one.label,
|
|
28
|
+
...(one.detail === undefined ? {} : { meta: one.detail, haystack: one.detail }),
|
|
29
|
+
})), ...(value === undefined ? {} : { value }), onChange: onChange, loading: lookup.loading, ...(onOpen === undefined ? {} : { onOpen }), onBrowseAll: onBrowseAll ?? (() => setChoosing(true)), ...(placeholder === undefined ? {} : { placeholder }), ...(disabled === undefined ? {} : { disabled }), ...(ariaLabel === undefined ? {} : { "aria-label": ariaLabel }) }), choosing ? (_jsx(Chooser, { entity: entity, onChoose: (id) => {
|
|
30
|
+
onChange(id);
|
|
31
|
+
setChoosing(false);
|
|
32
|
+
}, onClose: () => setChoosing(false) })) : null] }));
|
|
33
|
+
}
|
|
34
|
+
/** The whole set in a window: search and pages on the server, a row answers the field. */
|
|
35
|
+
function Chooser({ entity, onChoose, onClose, }) {
|
|
36
|
+
const listing = useListing(entity, { pageSize: 20 });
|
|
37
|
+
const key = listing.meta?.get_id || listing.meta?.identity || "id";
|
|
38
|
+
return (_jsx(Dialog, { open: true, width: "lg", title: nameOf(listing.meta, listing.locale) || entity.kind, onClose: onClose, children: _jsx(EntityList, { listing: listing, embedded: true, search: "box", onOpen: (row) => onChoose(String(row[key])) }) }));
|
|
39
|
+
}
|