@asteby/metacore-runtime-react 6.0.0 → 6.4.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/CHANGELOG.md +12 -2
- package/README.md +67 -44
- package/dist/dialogs/export.js +1 -1
- package/dist/dialogs/import.d.ts.map +1 -1
- package/dist/dialogs/import.js +1 -2
- package/dist/dynamic-columns.d.ts +29 -0
- package/dist/dynamic-columns.d.ts.map +1 -0
- package/dist/dynamic-columns.js +312 -0
- package/dist/dynamic-crud-page.d.ts +49 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -0
- package/dist/dynamic-crud-page.js +106 -0
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +36 -10
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/metadata-cache.js +3 -3
- package/dist/model-extension-registry.d.ts +19 -0
- package/dist/model-extension-registry.d.ts.map +1 -0
- package/dist/model-extension-registry.js +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/dialogs/export.tsx +1 -1
- package/src/dialogs/import.tsx +1 -2
- package/src/dynamic-columns.tsx +531 -0
- package/src/dynamic-crud-page.tsx +275 -0
- package/src/dynamic-table.tsx +34 -11
- package/src/index.ts +18 -0
- package/src/metadata-cache.ts +3 -3
- package/src/model-extension-registry.tsx +46 -0
- package/src/types.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 6.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- d7f1e55: Per-model extension registry, badge cell normalization, and auto-derived filter chips.
|
|
8
|
+
- `registerModelExtension(model, ext)` lets apps layer per-model UI on top of `<DynamicCRUDPage>` (header KPI strip, custom toolbar buttons, hidden create flow, title overrides) without forking the page or copy-pasting it.
|
|
9
|
+
- `defaultGetDynamicColumns` now accepts `type === 'badge'` (what the kernel emits) in addition to `cellStyle === 'badge'`. Columns marked `type: badge` previously rendered as plain text.
|
|
10
|
+
- `<DynamicTable>` derives a filter chip from every column flagged `filterable: true` plus either static options, a `searchEndpoint`, or boolean type, so apps no longer need to mirror the same options into a separate `filters` array on the metadata. Explicit `metadata.filters` still wins when present.
|
|
11
|
+
- Fixes the default `getDynamicColumns` fallback that previously read `col.name` instead of `col.key`, leaving cells blank for hosts that did not pass a custom factory.
|
|
12
|
+
|
|
3
13
|
## 6.0.0
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
|
@@ -20,7 +30,7 @@
|
|
|
20
30
|
|
|
21
31
|
- e23eede: Publicación inicial a npm del ecosistema metacore.
|
|
22
32
|
|
|
23
|
-
Propaga los 13 paquetes del SDK al registry público para que
|
|
33
|
+
Propaga los 13 paquetes del SDK al registry público para que las host applications consumidoras migren de `file:` a semver y Renovate pueda propagar updates.
|
|
24
34
|
|
|
25
35
|
### Patch Changes
|
|
26
36
|
|
|
@@ -34,7 +44,7 @@
|
|
|
34
44
|
|
|
35
45
|
- 6d243b0: Initial release of the metacore frontend ecosystem.
|
|
36
46
|
|
|
37
|
-
11 packages extracted from
|
|
47
|
+
11 packages extracted from host application frontends into a publishable monorepo with auto-propagation via Changesets + Renovate.
|
|
38
48
|
|
|
39
49
|
### Patch Changes
|
|
40
50
|
|
package/README.md
CHANGED
|
@@ -1,59 +1,82 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
-
React runtime for [
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
| `@/components/ui/*` | shadcn primitives |
|
|
15
|
-
| `@/components/data-table` | DataTableToolbar / Pagination / etc. |
|
|
16
|
-
| `@/components/dynamic/dynamic-columns` | column renderers (`DynamicIcon`, etc) |
|
|
17
|
-
| `@/components/dynamic/dynamic-record-dialog` | CRUD dialog (still host-owned) |
|
|
18
|
-
| `@/components/dynamic/export-dialog` | Export dialog |
|
|
19
|
-
| `@/components/dynamic/import-dialog` | Import dialog |
|
|
20
|
-
| `@/lib/api` | axios instance |
|
|
21
|
-
| `@/lib/utils` | `cn()` helper |
|
|
22
|
-
| `@/stores/metadata-cache` | zustand store |
|
|
23
|
-
| `@/stores/branch-store` | zustand store (optional) |
|
|
3
|
+
React runtime for [Metacore](https://github.com/asteby/metacore-sdk) hosts. The metadata-driven CRUD layer that turns a manifest declaration into a working UI: dynamic tables, forms, action dispatchers, slot extension points, capability gates, and the federated addon loader.
|
|
4
|
+
|
|
5
|
+
This is a *runtime*, not a UI kit — visual primitives come from [`@asteby/metacore-ui`](../ui). Hosts inject their HTTP client and (optionally) tenant-branch context via React providers; no bundler aliases are required.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @asteby/metacore-runtime-react @asteby/metacore-sdk @asteby/metacore-ui
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peers: `react`, `react-dom`, `react-i18next`, `i18next`, `@tanstack/react-router`, `@tanstack/react-table`, `date-fns`, `lucide-react`, `sonner`, `zustand`. They're declared as peers so React stays single-instance.
|
|
24
14
|
|
|
25
15
|
## Exports
|
|
26
16
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
17
|
+
| Export | What it does |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `<DynamicTable model="…" />` | Metadata-driven CRUD table. Sortable, paginated, filterable, URL-syncable, with built-in dialogs. |
|
|
20
|
+
| `<DynamicForm fields={…} onSubmit={…} />` | Standalone form renderer over `ActionFieldDef[]`. |
|
|
21
|
+
| `<DynamicRecordDialog />` | Create / edit / view modal driven by `/metadata/modal/<model>`. |
|
|
22
|
+
| `<ActionModalDispatcher />` | Routes a custom action to its registered component, generic form, or confirm dialog. |
|
|
23
|
+
| `<AddonLoader />` | Injects a federated `remoteEntry.js` and calls the addon's `register(api)`. |
|
|
24
|
+
| `<Slot name="…" />` / `slotRegistry` | Named extension points contributed by addons. |
|
|
25
|
+
| `<CapabilityGate require="…" />` / `<CapabilityProvider />` | Conditional UI by capability. |
|
|
26
|
+
| `<NavigationBuilder />` / `useNavigation()` / `mergeNavigation()` | Merges host sidebar with addon `manifest.navigation`. |
|
|
27
|
+
| `<I18nProvider />` | Folds `manifest.i18n` namespaces into the host's i18next instance. |
|
|
28
|
+
| `<ApiProvider client={axios} />` / `useApi()` | Inject the host's HTTP client. Required by every dynamic component. |
|
|
29
|
+
| `<BranchProvider branch={…} />` / `useCurrentBranch()` | Optional tenant-branch context. |
|
|
30
|
+
| `useMetadataCache()` | Zustand store for table/modal metadata, persisted to LocalStorage. `prefetchAll(api)` warms it from `/metadata/all`. |
|
|
31
|
+
| `defaultGetDynamicColumns` / `makeDefaultGetDynamicColumns(helpers)` | The factory `<DynamicTable>` uses to convert metadata into TanStack column defs. The default reads from `col.key` (matching the kernel contract). |
|
|
32
|
+
| `DynamicIcon` | Lucide icon resolver by name. |
|
|
36
33
|
|
|
37
34
|
## Minimal usage
|
|
38
35
|
|
|
39
36
|
```tsx
|
|
40
|
-
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
import {
|
|
38
|
+
ApiProvider,
|
|
39
|
+
CapabilityProvider,
|
|
40
|
+
DynamicTable,
|
|
41
|
+
} from '@asteby/metacore-runtime-react'
|
|
42
|
+
import { api } from './lib/api'
|
|
43
|
+
|
|
44
|
+
export function App() {
|
|
45
|
+
return (
|
|
46
|
+
<ApiProvider client={api}>
|
|
47
|
+
<CapabilityProvider capabilities={session.capabilities}>
|
|
48
|
+
<DynamicTable model="tickets" />
|
|
49
|
+
</CapabilityProvider>
|
|
50
|
+
</ApiProvider>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
For props, response shapes, customization patterns and the full surface, see [`docs/dynamic-ui.md`](https://github.com/asteby/metacore-sdk/blob/main/docs/dynamic-ui.md).
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
canonical action registry and `AddonAPI` contract. Build order:
|
|
57
|
+
## How it talks to the kernel
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
| Endpoint | Used by |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `GET /metadata/table/<model>` | `<DynamicTable>` (cached). |
|
|
62
|
+
| `GET /metadata/modal/<model>` | `<DynamicRecordDialog>` (cached). |
|
|
63
|
+
| `GET /metadata/all` | `useMetadataCache().prefetchAll()`. |
|
|
64
|
+
| `GET /data/<model>` | `<DynamicTable>` list. |
|
|
65
|
+
| `GET /data/<model>/<id>` | `<DynamicRecordDialog>` view/edit. |
|
|
66
|
+
| `POST /data/<model>` | Create. |
|
|
67
|
+
| `PUT /data/<model>/<id>` | Update. |
|
|
68
|
+
| `DELETE /data/<model>/<id>` | Delete (single + bulk). |
|
|
69
|
+
| `POST /data/<model>/<id>/action/<key>` | `<ActionModalDispatcher>`. |
|
|
70
|
+
| `GET /options/<endpoint>` | Relation pickers + select prefetch. |
|
|
71
|
+
|
|
72
|
+
All endpoints can be overridden per-component via the `endpoint` prop.
|
|
73
|
+
|
|
74
|
+
## Build
|
|
75
|
+
|
|
76
|
+
```bash
|
|
57
77
|
pnpm --filter @asteby/metacore-runtime-react build
|
|
58
|
-
pnpm --filter <host> install
|
|
59
78
|
```
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
Apache-2.0
|
package/dist/dialogs/export.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// ExportDialog — lets users pick format (csv/json) + columns and kicks off
|
|
3
3
|
// either a sync download or an async export job (polled via /exports/:id/status).
|
|
4
|
-
//
|
|
4
|
+
// Axios-like client is provided by <ApiProvider>.
|
|
5
5
|
import { useState, useEffect, useCallback } from 'react';
|
|
6
6
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Label, Checkbox, Collapsible, CollapsibleContent, CollapsibleTrigger, } from '@asteby/metacore-ui/primitives';
|
|
7
7
|
import { Progress, RadioGroup, RadioGroupItem } from './_primitives';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/dialogs/import.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"import.d.ts","sourceRoot":"","sources":["../../src/dialogs/import.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAG7C,UAAU,iBAAiB;IACvB,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,aAAa,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAA;CAC1B;AAoBD,wBAAgB,YAAY,CAAC,EACzB,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,QAAQ,EACR,UAAU,GACb,EAAE,iBAAiB,2CAwVnB"}
|
package/dist/dialogs/import.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// ImportDialog — three-step CSV/JSON import flow (upload → validate → import
|
|
3
|
-
// with per-row error report).
|
|
4
|
-
// is provided by <ApiProvider>.
|
|
3
|
+
// with per-row error report). Axios-like client is provided by <ApiProvider>.
|
|
5
4
|
import { useState, useEffect, useRef } from 'react';
|
|
6
5
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Button, Input, Label, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@asteby/metacore-ui/primitives';
|
|
7
6
|
import { Progress } from './_primitives';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GetDynamicColumns } from './dynamic-columns-shim';
|
|
2
|
+
/** Host-supplied helpers consumed by avatar/image cell renderers. */
|
|
3
|
+
export interface DynamicColumnsHelpers {
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a relative or absolute media path into a renderable URL. Hosts
|
|
6
|
+
* typically prepend their CDN/storage base. If omitted, paths are passed
|
|
7
|
+
* through verbatim.
|
|
8
|
+
*/
|
|
9
|
+
getImageUrl?: (path: string) => string;
|
|
10
|
+
/**
|
|
11
|
+
* API origin used to build avatar URLs when the row carries a bare filename
|
|
12
|
+
* instead of an absolute URL or sibling `.avatar` field. Usually
|
|
13
|
+
* `import.meta.env.VITE_API_URL.replace('/api', '')`.
|
|
14
|
+
*/
|
|
15
|
+
apiBaseUrl?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
19
|
+
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
20
|
+
* URL resolution.
|
|
21
|
+
*/
|
|
22
|
+
export declare function makeDefaultGetDynamicColumns(helpers?: DynamicColumnsHelpers): GetDynamicColumns;
|
|
23
|
+
/**
|
|
24
|
+
* Eager-built variant — equivalent to `makeDefaultGetDynamicColumns()`. Use
|
|
25
|
+
* this when the host has no helpers to inject and a stable function reference
|
|
26
|
+
* suffices.
|
|
27
|
+
*/
|
|
28
|
+
export declare const defaultGetDynamicColumns: GetDynamicColumns;
|
|
29
|
+
//# sourceMappingURL=dynamic-columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-columns.d.ts","sourceRoot":"","sources":["../src/dynamic-columns.tsx"],"names":[],"mappings":"AAqCA,OAAO,KAAK,EAER,iBAAiB,EACpB,MAAM,wBAAwB,CAAA;AAE/B,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IAClC;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAwHD;;;;GAIG;AACH,wBAAgB,4BAA4B,CACxC,OAAO,GAAE,qBAA0B,GACpC,iBAAiB,CAmVnB;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBACL,CAAA"}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Default `getDynamicColumns` factory used by hosts that don't need a custom
|
|
3
|
+
// renderer. Supports every cell type produced by kernel/dynamic metadata:
|
|
4
|
+
// badge (static + endpoint-loaded options), avatar, phone, date, boolean,
|
|
5
|
+
// relation-badge-list, media-gallery, image, plus a generic text fallback.
|
|
6
|
+
//
|
|
7
|
+
// The implementation was previously duplicated across multiple host apps
|
|
8
|
+
// (~550 LOC each, drifting). It now lives here so a single fix propagates
|
|
9
|
+
// to every host. Hosts inject app-specific URL helpers via the `helpers`
|
|
10
|
+
// argument so the SDK stays free of environment-bound code.
|
|
11
|
+
import * as React from 'react';
|
|
12
|
+
import { format } from 'date-fns';
|
|
13
|
+
import { es, enUS } from 'date-fns/locale';
|
|
14
|
+
import * as icons from 'lucide-react';
|
|
15
|
+
import { MoreHorizontal } from 'lucide-react';
|
|
16
|
+
import { Avatar, AvatarFallback, AvatarImage, Badge, Button, Checkbox, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@asteby/metacore-ui';
|
|
17
|
+
import { DataTableColumnHeader, FilterableColumnHeader, } from '@asteby/metacore-ui/data-table';
|
|
18
|
+
import { generateBadgeStyles } from '@asteby/metacore-ui/lib';
|
|
19
|
+
import { OptionsContext } from './options-context';
|
|
20
|
+
import { DynamicIcon } from './dynamic-icon';
|
|
21
|
+
const defaultGetImageUrl = (path) => path;
|
|
22
|
+
const getNestedValue = (obj, path) => path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
|
23
|
+
const lowerFirst = (value) => {
|
|
24
|
+
if (!value)
|
|
25
|
+
return value;
|
|
26
|
+
return value.charAt(0).toLowerCase() + value.slice(1);
|
|
27
|
+
};
|
|
28
|
+
const getPathVariants = (path) => {
|
|
29
|
+
if (!path)
|
|
30
|
+
return [];
|
|
31
|
+
const normalized = path
|
|
32
|
+
.split('.')
|
|
33
|
+
.map((segment) => lowerFirst(segment) || segment)
|
|
34
|
+
.join('.');
|
|
35
|
+
return Array.from(new Set([path, normalized])).filter(Boolean);
|
|
36
|
+
};
|
|
37
|
+
const getValueFromPathVariants = (obj, path) => {
|
|
38
|
+
if (!path)
|
|
39
|
+
return undefined;
|
|
40
|
+
for (const candidate of getPathVariants(path)) {
|
|
41
|
+
const value = getNestedValue(obj, candidate);
|
|
42
|
+
if (value !== undefined && value !== null)
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
};
|
|
47
|
+
const useIsDarkTheme = () => {
|
|
48
|
+
const [isDark, setIsDark] = React.useState(() => typeof document !== 'undefined' &&
|
|
49
|
+
document.documentElement.classList.contains('dark'));
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (typeof document === 'undefined')
|
|
52
|
+
return;
|
|
53
|
+
const sync = () => setIsDark(document.documentElement.classList.contains('dark'));
|
|
54
|
+
sync();
|
|
55
|
+
const observer = new MutationObserver(sync);
|
|
56
|
+
observer.observe(document.documentElement, {
|
|
57
|
+
attributes: true,
|
|
58
|
+
attributeFilter: ['class'],
|
|
59
|
+
});
|
|
60
|
+
return () => observer.disconnect();
|
|
61
|
+
}, []);
|
|
62
|
+
return isDark;
|
|
63
|
+
};
|
|
64
|
+
const renderRelationBadges = (items, col) => {
|
|
65
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
66
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
67
|
+
}
|
|
68
|
+
return (_jsx("div", { className: "flex flex-wrap gap-1", children: items.map((item, idx) => {
|
|
69
|
+
const relationTarget = col.relationPath
|
|
70
|
+
? getValueFromPathVariants(item, col.relationPath) ?? item
|
|
71
|
+
: item;
|
|
72
|
+
const displaySource = relationTarget ?? item;
|
|
73
|
+
let displayValue = col.displayField !== undefined && col.displayField !== null
|
|
74
|
+
? getValueFromPathVariants(displaySource, col.displayField)
|
|
75
|
+
: displaySource;
|
|
76
|
+
if (displayValue === undefined || displayValue === null) {
|
|
77
|
+
displayValue = displaySource;
|
|
78
|
+
}
|
|
79
|
+
const label = displayValue !== undefined && displayValue !== null
|
|
80
|
+
? String(displayValue)
|
|
81
|
+
: '-';
|
|
82
|
+
let iconValue;
|
|
83
|
+
if (col.iconField) {
|
|
84
|
+
const rawIcon = getValueFromPathVariants(displaySource, col.iconField);
|
|
85
|
+
if (rawIcon !== undefined && rawIcon !== null) {
|
|
86
|
+
iconValue = String(rawIcon);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1", children: [iconValue && (_jsx(DynamicIcon, { name: iconValue, className: "h-3 w-3" })), _jsx("span", { children: label })] }, `${col.key}-${idx}`));
|
|
90
|
+
}) }));
|
|
91
|
+
};
|
|
92
|
+
const OptionBadge = ({ option }) => {
|
|
93
|
+
const isDark = useIsDarkTheme();
|
|
94
|
+
const colorStyles = option.color ? generateBadgeStyles(option.color, { isDark }) : {};
|
|
95
|
+
return (_jsxs(Badge, { variant: "outline", className: "flex items-center gap-1 border-0", style: colorStyles, children: [option.icon && _jsx(DynamicIcon, { name: option.icon, className: "h-3.5 w-3.5" }), _jsx("span", { children: option.label })] }));
|
|
96
|
+
};
|
|
97
|
+
const BadgeWithEndpointOptions = ({ endpoint, value }) => {
|
|
98
|
+
const { optionsMap } = React.useContext(OptionsContext);
|
|
99
|
+
const options = optionsMap.get(endpoint) || [];
|
|
100
|
+
const option = options.find((opt) => opt.value === value);
|
|
101
|
+
if (option)
|
|
102
|
+
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
103
|
+
return _jsx(Badge, { variant: "outline", children: String(value) });
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Builds the canonical column factory used by `<DynamicTable>` when the host
|
|
107
|
+
* does not supply its own. Pass `{ getImageUrl, apiBaseUrl }` to wire avatar
|
|
108
|
+
* URL resolution.
|
|
109
|
+
*/
|
|
110
|
+
export function makeDefaultGetDynamicColumns(helpers = {}) {
|
|
111
|
+
const getImageUrl = helpers.getImageUrl ?? defaultGetImageUrl;
|
|
112
|
+
const apiBaseUrl = helpers.apiBaseUrl ?? '';
|
|
113
|
+
return function defaultGetDynamicColumns(metadata, onAction, t, currentLanguage, filterConfigs) {
|
|
114
|
+
const dateLocale = currentLanguage === 'en' ? enUS : es;
|
|
115
|
+
const columns = [
|
|
116
|
+
{
|
|
117
|
+
id: 'select',
|
|
118
|
+
header: ({ table }) => (_jsx(Checkbox, { checked: table.getIsAllPageRowsSelected() ||
|
|
119
|
+
(table.getIsSomePageRowsSelected() && 'indeterminate'), onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value), "aria-label": "Select all", className: "translate-y-[2px]" })),
|
|
120
|
+
cell: ({ row }) => (_jsx(Checkbox, { checked: row.getIsSelected(), onCheckedChange: (value) => row.toggleSelected(!!value), "aria-label": "Select row", className: "translate-y-[2px]" })),
|
|
121
|
+
enableSorting: false,
|
|
122
|
+
enableHiding: false,
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
metadata.columns.forEach((col) => {
|
|
126
|
+
if (col.hidden)
|
|
127
|
+
return;
|
|
128
|
+
const translatedLabel = col.label;
|
|
129
|
+
const filterConfig = filterConfigs?.get(col.key);
|
|
130
|
+
const columnMeta = {
|
|
131
|
+
label: translatedLabel,
|
|
132
|
+
};
|
|
133
|
+
if (filterConfig) {
|
|
134
|
+
const fm = {
|
|
135
|
+
filterable: true,
|
|
136
|
+
filterType: filterConfig.filterType,
|
|
137
|
+
filterKey: filterConfig.filterKey,
|
|
138
|
+
filterOptions: filterConfig.options,
|
|
139
|
+
filterLoading: filterConfig.loading,
|
|
140
|
+
filterSearchEndpoint: filterConfig.searchEndpoint,
|
|
141
|
+
selectedValues: filterConfig.selectedValues,
|
|
142
|
+
onFilterChange: filterConfig.onFilterChange,
|
|
143
|
+
};
|
|
144
|
+
Object.assign(columnMeta, fm);
|
|
145
|
+
}
|
|
146
|
+
columns.push({
|
|
147
|
+
accessorKey: col.key,
|
|
148
|
+
id: col.key,
|
|
149
|
+
meta: columnMeta,
|
|
150
|
+
header: ({ column }) => filterConfig ? (_jsx(FilterableColumnHeader, { column: column, title: translatedLabel })) : (_jsx(DataTableColumnHeader, { column: column, title: translatedLabel })),
|
|
151
|
+
cell: ({ row }) => {
|
|
152
|
+
const value = getNestedValue(row.original, col.key);
|
|
153
|
+
// Kernel emits the renderer flag as `type`; older hosts used
|
|
154
|
+
// `cellStyle`. Accept both so a single backend works across
|
|
155
|
+
// SDK versions.
|
|
156
|
+
const renderAs = col.cellStyle ?? col.type;
|
|
157
|
+
// Endpoint-loaded badge options (preloaded into OptionsContext)
|
|
158
|
+
if (renderAs === 'badge' && col.useOptions && col.searchEndpoint) {
|
|
159
|
+
if (!value)
|
|
160
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
161
|
+
return _jsx(BadgeWithEndpointOptions, { endpoint: col.searchEndpoint, value: value });
|
|
162
|
+
}
|
|
163
|
+
// Static badge options — map value → label/icon/color
|
|
164
|
+
if (renderAs === 'badge' && col.options && col.options.length > 0) {
|
|
165
|
+
if (!value && value !== 0)
|
|
166
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
167
|
+
const option = col.options.find((o) => o.value === String(value));
|
|
168
|
+
if (option)
|
|
169
|
+
return _jsx(OptionBadge, { option: option, fallback: String(value) });
|
|
170
|
+
return _jsx(Badge, { variant: "outline", children: String(value) });
|
|
171
|
+
}
|
|
172
|
+
if (renderAs === 'relation-badge-list') {
|
|
173
|
+
return renderRelationBadges(value, col);
|
|
174
|
+
}
|
|
175
|
+
switch (col.type) {
|
|
176
|
+
case 'date': {
|
|
177
|
+
if (!value)
|
|
178
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
179
|
+
try {
|
|
180
|
+
const date = new Date(value);
|
|
181
|
+
if (isNaN(date.getTime()) || date.getFullYear() <= 1) {
|
|
182
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
183
|
+
}
|
|
184
|
+
return (_jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground", children: [_jsx(icons.Calendar, { className: "h-3.5 w-3.5 opacity-70" }), _jsx("span", { className: "text-sm font-medium", children: format(date, 'PPP', { locale: dateLocale }) })] }));
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return _jsx("span", { children: String(value) });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
case 'search':
|
|
191
|
+
case 'avatar': {
|
|
192
|
+
const namePath = col.tooltip || col.key;
|
|
193
|
+
const name = getNestedValue(row.original, namePath) || 'N/A';
|
|
194
|
+
const desc = getNestedValue(row.original, col.description || '');
|
|
195
|
+
let avatarSrc;
|
|
196
|
+
if (col.key.includes('.')) {
|
|
197
|
+
const parentPath = col.key.split('.').slice(0, -1).join('.');
|
|
198
|
+
const avatarPath = `${parentPath}.avatar`;
|
|
199
|
+
const possibleAvatar = getNestedValue(row.original, avatarPath);
|
|
200
|
+
if (possibleAvatar)
|
|
201
|
+
avatarSrc = String(possibleAvatar);
|
|
202
|
+
}
|
|
203
|
+
else if (value &&
|
|
204
|
+
(String(value).startsWith('http') || String(value).startsWith('https'))) {
|
|
205
|
+
avatarSrc = String(value);
|
|
206
|
+
}
|
|
207
|
+
else if (value) {
|
|
208
|
+
avatarSrc = `${apiBaseUrl}${col.basePath || ''}${value}`;
|
|
209
|
+
}
|
|
210
|
+
return (_jsxs("div", { className: "flex items-center gap-3 min-w-0", children: [_jsxs(Avatar, { className: "h-8 w-8 rounded-lg ring-1 ring-border/50", children: [_jsx(AvatarImage, { src: getImageUrl(avatarSrc || ''), alt: String(name), className: "object-cover" }), _jsx(AvatarFallback, { className: "text-[10px] font-bold bg-primary/5 text-primary rounded-lg", children: String(name)
|
|
211
|
+
.split(' ')
|
|
212
|
+
.map((n) => n[0])
|
|
213
|
+
.slice(0, 2)
|
|
214
|
+
.join('')
|
|
215
|
+
.toUpperCase() })] }), _jsxs("div", { className: "flex flex-col min-w-0 overflow-hidden", children: [_jsx("span", { className: "font-medium text-sm truncate leading-none mb-0.5 text-foreground/90", children: String(name) }), desc && (_jsx("span", { className: "text-[11px] text-muted-foreground truncate leading-none", children: String(desc) }))] })] }));
|
|
216
|
+
}
|
|
217
|
+
case 'relation-badge-list':
|
|
218
|
+
return renderRelationBadges(value, col);
|
|
219
|
+
case 'phone': {
|
|
220
|
+
if (!value)
|
|
221
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
222
|
+
return _jsx("span", { className: "font-medium text-sm", children: String(value) });
|
|
223
|
+
}
|
|
224
|
+
case 'boolean':
|
|
225
|
+
return value ? _jsx(Badge, { children: "S\u00ED" }) : _jsx(Badge, { variant: "secondary", children: "No" });
|
|
226
|
+
case 'media-gallery': {
|
|
227
|
+
if (!value || (Array.isArray(value) && value.length === 0)) {
|
|
228
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
229
|
+
}
|
|
230
|
+
const mediaItems = Array.isArray(value) ? value : [];
|
|
231
|
+
const visibleItems = mediaItems.slice(0, 3);
|
|
232
|
+
const remaining = mediaItems.length - 3;
|
|
233
|
+
return (_jsxs("div", { className: "flex -space-x-2 overflow-hidden", children: [visibleItems.map((item, i) => {
|
|
234
|
+
const src = item.url;
|
|
235
|
+
if (item.type === 'image') {
|
|
236
|
+
return (_jsxs(Avatar, { className: "inline-block h-8 w-8 rounded-full ring-2 ring-background", children: [_jsx(AvatarImage, { src: src, className: "object-cover" }), _jsx(AvatarFallback, { children: item.type?.[0] })] }, i));
|
|
237
|
+
}
|
|
238
|
+
return (_jsx("div", { className: "inline-flex h-8 w-8 items-center justify-center rounded-full bg-muted ring-2 ring-background", children: _jsx(DynamicIcon, { name: item.type === 'video'
|
|
239
|
+
? 'Video'
|
|
240
|
+
: item.type === 'audio'
|
|
241
|
+
? 'AudioLines'
|
|
242
|
+
: 'FileText', className: "h-4 w-4" }) }, i));
|
|
243
|
+
}), remaining > 0 && (_jsxs("div", { className: "flex h-8 w-8 items-center justify-center rounded-full bg-muted text-xs font-medium ring-2 ring-background", children: ["+", remaining] }))] }));
|
|
244
|
+
}
|
|
245
|
+
case 'image': {
|
|
246
|
+
const imageValue = value ||
|
|
247
|
+
(Array.isArray(row.original.media)
|
|
248
|
+
? row.original.media.find((m) => m.type === 'image')?.url
|
|
249
|
+
: null);
|
|
250
|
+
if (!imageValue)
|
|
251
|
+
return _jsx("span", { className: "text-muted-foreground", children: "-" });
|
|
252
|
+
return (_jsx("div", { className: "h-10 w-10 relative rounded overflow-hidden bg-muted flex items-center justify-center", children: _jsx("img", { src: getImageUrl(String(imageValue)), alt: "Thumbnail", className: "h-full w-full object-contain", onError: (e) => {
|
|
253
|
+
;
|
|
254
|
+
e.currentTarget.style.display = 'none';
|
|
255
|
+
} }) }));
|
|
256
|
+
}
|
|
257
|
+
default: {
|
|
258
|
+
if (typeof value === 'object' && value !== null) {
|
|
259
|
+
return (_jsx("span", { className: "text-muted-foreground text-xs", children: JSON.stringify(value) }));
|
|
260
|
+
}
|
|
261
|
+
if (col.key === 'description' ||
|
|
262
|
+
col.key === 'features' ||
|
|
263
|
+
col.key.includes('description')) {
|
|
264
|
+
return (_jsx("div", { className: "max-w-[350px]", title: String(value), children: _jsx("span", { className: "truncate font-medium block", children: value !== null && value !== undefined ? String(value) : '-' }) }));
|
|
265
|
+
}
|
|
266
|
+
return (_jsx("span", { className: "truncate font-medium", children: value !== null && value !== undefined ? String(value) : '-' }));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
enableSorting: col.sortable,
|
|
271
|
+
enableHiding: true,
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
if (metadata.hasActions && metadata.actions.length > 0) {
|
|
275
|
+
columns.push({
|
|
276
|
+
id: 'actions',
|
|
277
|
+
header: () => _jsx("div", { className: "text-right", children: t ? t('common.actions') : 'Acciones' }),
|
|
278
|
+
size: 80,
|
|
279
|
+
maxSize: 80,
|
|
280
|
+
meta: {},
|
|
281
|
+
cell: ({ row }) => (_jsx("div", { className: "flex items-center justify-end", children: _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "h-8 w-8 p-0", children: [_jsx("span", { className: "sr-only", children: "Abrir men\u00FA" }), _jsx(MoreHorizontal, { className: "h-4 w-4" })] }) }), _jsx(DropdownMenuContent, { align: "end", children: metadata.actions
|
|
282
|
+
.filter((action) => {
|
|
283
|
+
if (!action.condition)
|
|
284
|
+
return true;
|
|
285
|
+
const { field, operator, value } = action.condition;
|
|
286
|
+
const rowValue = String(row.original[field] ?? '');
|
|
287
|
+
const values = Array.isArray(value) ? value : [value];
|
|
288
|
+
switch (operator) {
|
|
289
|
+
case 'eq':
|
|
290
|
+
return rowValue === values[0];
|
|
291
|
+
case 'neq':
|
|
292
|
+
return rowValue !== values[0];
|
|
293
|
+
case 'in':
|
|
294
|
+
return values.includes(rowValue);
|
|
295
|
+
case 'not_in':
|
|
296
|
+
return !values.includes(rowValue);
|
|
297
|
+
default:
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
})
|
|
301
|
+
.map((action) => (_jsxs(DropdownMenuItem, { onClick: () => onAction && onAction(action.key, row.original), children: [_jsx(DynamicIcon, { name: action.icon, className: "mr-2 h-4 w-4" }), action.label] }, action.key))) })] }) })),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return columns;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Eager-built variant — equivalent to `makeDefaultGetDynamicColumns()`. Use
|
|
309
|
+
* this when the host has no helpers to inject and a stable function reference
|
|
310
|
+
* suffices.
|
|
311
|
+
*/
|
|
312
|
+
export const defaultGetDynamicColumns = makeDefaultGetDynamicColumns();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface DynamicCRUDPageStrings {
|
|
3
|
+
refresh?: string;
|
|
4
|
+
export?: string;
|
|
5
|
+
import?: string;
|
|
6
|
+
/** Used as the create button label when `newLabel` is not provided.
|
|
7
|
+
* Receives the singular form of the title. */
|
|
8
|
+
newPrefix?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DynamicCRUDPageClasses {
|
|
11
|
+
root?: string;
|
|
12
|
+
container?: string;
|
|
13
|
+
header?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
toolbar?: string;
|
|
16
|
+
tableWrapper?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface DynamicCRUDPageProps {
|
|
19
|
+
/** Model key as registered on the backend (e.g. "customers"). */
|
|
20
|
+
model: string;
|
|
21
|
+
/** Override the data endpoint. Defaults to `/dynamic/<model>`. */
|
|
22
|
+
endpoint?: string;
|
|
23
|
+
/** Override the human title. Defaults to `metadata.title`. */
|
|
24
|
+
title?: string;
|
|
25
|
+
/** Override the create button label. Defaults to `${newPrefix} ${singular}`. */
|
|
26
|
+
newLabel?: string;
|
|
27
|
+
/** Strings used in default labels — pass when the host has its own i18n. */
|
|
28
|
+
i18n?: DynamicCRUDPageStrings;
|
|
29
|
+
/** Hide the create button + dialog even when metadata says CRUD is enabled. */
|
|
30
|
+
hideCreate?: boolean;
|
|
31
|
+
hideExport?: boolean;
|
|
32
|
+
hideImport?: boolean;
|
|
33
|
+
hideRefresh?: boolean;
|
|
34
|
+
/** Slot rendered above the title row (e.g. branch switcher, kpi strip). */
|
|
35
|
+
headerExtras?: React.ReactNode;
|
|
36
|
+
/** Slot rendered in the toolbar, before the create button. */
|
|
37
|
+
toolbarExtras?: React.ReactNode;
|
|
38
|
+
/** Tailwind class overrides for layout primitives. */
|
|
39
|
+
classes?: DynamicCRUDPageClasses;
|
|
40
|
+
/** Fired after a create/import/refresh successfully reloads the table. */
|
|
41
|
+
onChange?: () => void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Page-level CRUD shell wired around <DynamicTable>. Hosts mount a single
|
|
45
|
+
* `<DynamicCRUDPage model="..." />` per route and the SDK takes care of
|
|
46
|
+
* metadata fetch, dialogs and toolbar.
|
|
47
|
+
*/
|
|
48
|
+
export declare function DynamicCRUDPage(props: DynamicCRUDPageProps): import("react/jsx-runtime").JSX.Element;
|
|
49
|
+
//# sourceMappingURL=dynamic-crud-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-crud-page.d.ts","sourceRoot":"","sources":["../src/dynamic-crud-page.tsx"],"names":[],"mappings":"AAwBA,OAAO,KAKN,MAAM,OAAO,CAAA;AAWd,MAAM,WAAW,sBAAsB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;mDAC+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAA;CACrB;AASD,MAAM,WAAW,sBAAsB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,oBAAoB;IACjC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAA;IACb,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,sBAAsB,CAAA;IAC7B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC9B,8DAA8D;IAC9D,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC/B,sDAAsD;IACtD,OAAO,CAAC,EAAE,sBAAsB,CAAA;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;CACxB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,2CAkL1D"}
|