@object-ui/app-shell 6.1.0 → 6.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +129 -0
- package/README.md +10 -1
- package/dist/console/AppContent.js +53 -2
- package/dist/console/ai/AiChatPage.d.ts +8 -0
- package/dist/console/ai/AiChatPage.js +188 -0
- package/dist/console/ai/ConversationsSidebar.d.ts +7 -0
- package/dist/console/ai/ConversationsSidebar.js +111 -0
- package/dist/console/auth/LoginPage.js +19 -2
- package/dist/console/auth/RegisterPage.js +30 -1
- package/dist/console/marketplace/MarketplaceAccessDenied.js +3 -1
- package/dist/console/marketplace/MarketplaceInstalledPage.js +11 -3
- package/dist/console/marketplace/MarketplacePackagePage.js +57 -19
- package/dist/console/marketplace/MarketplacePage.js +55 -18
- package/dist/console/marketplace/marketplaceApi.d.ts +20 -0
- package/dist/console/marketplace/usePackageL10n.d.ts +38 -0
- package/dist/console/marketplace/usePackageL10n.js +110 -0
- package/dist/console/organizations/CreateWorkspaceDialog.js +29 -1
- package/dist/console/organizations/OrganizationsPage.js +24 -3
- package/dist/context/FavoritesProvider.d.ts +40 -2
- package/dist/context/FavoritesProvider.js +201 -20
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useChatConversation.d.ts +7 -0
- package/dist/hooks/useChatConversation.js +37 -5
- package/dist/hooks/useConversationList.d.ts +25 -0
- package/dist/hooks/useConversationList.js +131 -0
- package/dist/hooks/useNavPins.d.ts +11 -4
- package/dist/hooks/useNavPins.js +104 -53
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/layout/AppHeader.js +2 -2
- package/dist/layout/AppSidebar.js +20 -1
- package/dist/layout/UnifiedSidebar.js +1 -1
- package/dist/providers/ExpressionProvider.d.ts +11 -1
- package/dist/providers/ExpressionProvider.js +11 -6
- package/dist/services/builtinComponents.d.ts +1 -0
- package/dist/services/builtinComponents.js +169 -0
- package/dist/services/componentRegistry.d.ts +63 -0
- package/dist/services/componentRegistry.js +36 -0
- package/dist/views/ComponentNavView.d.ts +6 -0
- package/dist/views/ComponentNavView.js +26 -0
- package/dist/views/RecordDetailView.js +66 -6
- package/dist/views/RecordFormPage.js +15 -3
- package/dist/views/SearchResultsPage.js +4 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.d.ts +68 -0
- package/dist/views/metadata-admin/DesignerEditorWrapper.js +158 -0
- package/dist/views/metadata-admin/DirectoryPage.d.ts +1 -0
- package/dist/views/metadata-admin/DirectoryPage.js +135 -0
- package/dist/views/metadata-admin/LayeredDiff.d.ts +6 -0
- package/dist/views/metadata-admin/LayeredDiff.js +26 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.d.ts +13 -0
- package/dist/views/metadata-admin/MetadataDetailDrawer.js +52 -0
- package/dist/views/metadata-admin/PageShell.d.ts +34 -0
- package/dist/views/metadata-admin/PageShell.js +40 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.d.ts +5 -0
- package/dist/views/metadata-admin/PermissionMatrixEditor.js +288 -0
- package/dist/views/metadata-admin/QuickFind.d.ts +5 -0
- package/dist/views/metadata-admin/QuickFind.js +152 -0
- package/dist/views/metadata-admin/RelatedPanel.d.ts +33 -0
- package/dist/views/metadata-admin/RelatedPanel.js +171 -0
- package/dist/views/metadata-admin/ResourceEditPage.d.ts +13 -0
- package/dist/views/metadata-admin/ResourceEditPage.js +302 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.d.ts +5 -0
- package/dist/views/metadata-admin/ResourceHistoryPage.js +100 -0
- package/dist/views/metadata-admin/ResourceListPage.d.ts +4 -0
- package/dist/views/metadata-admin/ResourceListPage.js +146 -0
- package/dist/views/metadata-admin/ResourceRouter.d.ts +10 -0
- package/dist/views/metadata-admin/ResourceRouter.js +47 -0
- package/dist/views/metadata-admin/SchemaForm.d.ts +99 -0
- package/dist/views/metadata-admin/SchemaForm.js +565 -0
- package/dist/views/metadata-admin/anchors.d.ts +1 -0
- package/dist/views/metadata-admin/anchors.js +229 -0
- package/dist/views/metadata-admin/default-schemas.d.ts +6 -0
- package/dist/views/metadata-admin/default-schemas.js +207 -0
- package/dist/views/metadata-admin/i18n.d.ts +33 -0
- package/dist/views/metadata-admin/i18n.js +303 -0
- package/dist/views/metadata-admin/index.d.ts +33 -0
- package/dist/views/metadata-admin/index.js +39 -0
- package/dist/views/metadata-admin/predicate.d.ts +31 -0
- package/dist/views/metadata-admin/predicate.js +150 -0
- package/dist/views/metadata-admin/registry.d.ts +232 -0
- package/dist/views/metadata-admin/registry.js +106 -0
- package/dist/views/metadata-admin/useMetadata.d.ts +37 -0
- package/dist/views/metadata-admin/useMetadata.js +96 -0
- package/dist/views/metadata-admin/widgets.d.ts +68 -0
- package/dist/views/metadata-admin/widgets.js +287 -0
- package/package.json +27 -26
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetadataResourceRegistry — per-type overrides for the generic metadata
|
|
3
|
+
* admin engine (Phase 3c).
|
|
4
|
+
*
|
|
5
|
+
* The engine drives all 27 metadata types from a single ListPage /
|
|
6
|
+
* EditPage / HistoryPage shell. By default everything is rendered via
|
|
7
|
+
* a JSONSchema-driven AutoForm (using the rich `/meta/types` registry
|
|
8
|
+
* row's `schema` field). Specialised editors (ObjectManager,
|
|
9
|
+
* FieldDesigner, ObjectViewConfigurator, PermissionMatrix, …) opt in
|
|
10
|
+
* by calling `registerMetadataResource()` to override the default
|
|
11
|
+
* components for their type.
|
|
12
|
+
*
|
|
13
|
+
* This keeps the contract for "add a new metadata type" trivial:
|
|
14
|
+
* 1. Define its Zod schema in `packages/spec/src/<domain>/`.
|
|
15
|
+
* 2. Add it to `DEFAULT_METADATA_TYPE_REGISTRY` (framework).
|
|
16
|
+
* 3. Done. It shows up in the Setup app's Metadata Directory with
|
|
17
|
+
* a working list / create / edit / history out of the box.
|
|
18
|
+
*
|
|
19
|
+
* Conventions:
|
|
20
|
+
* • `primaryKey` defaults to `'name'` (the universal metadata
|
|
21
|
+
* short-name; ADR-0006).
|
|
22
|
+
* • `searchableFields` defaults to `['name','label','description']`.
|
|
23
|
+
* • `listColumns` defaults to inferring from primary + label.
|
|
24
|
+
* • `supportsHistory` defaults to true (every overlay goes through
|
|
25
|
+
* `sys_metadata_history`).
|
|
26
|
+
*/
|
|
27
|
+
import type { ComponentType, ReactNode } from 'react';
|
|
28
|
+
export type MetadataDomain = 'data' | 'ui' | 'automation' | 'ai' | 'system' | 'platform' | 'identity' | 'security' | 'other';
|
|
29
|
+
/**
|
|
30
|
+
* One row in the registry — describes how the generic engine should
|
|
31
|
+
* render and edit a metadata type. All fields are optional; sensible
|
|
32
|
+
* defaults apply if a type isn't registered at all.
|
|
33
|
+
*/
|
|
34
|
+
export interface MetadataResourceConfig {
|
|
35
|
+
/** Metadata type, e.g. 'view', 'flow', 'agent'. */
|
|
36
|
+
type: string;
|
|
37
|
+
/** Display label. Falls back to server-side label or the type id. */
|
|
38
|
+
label?: string;
|
|
39
|
+
/** Long-form description shown in the page hero. */
|
|
40
|
+
description?: string;
|
|
41
|
+
/** Lucide icon name (e.g. 'database', 'workflow'). */
|
|
42
|
+
iconName?: string;
|
|
43
|
+
/** Coarse grouping for the directory page. */
|
|
44
|
+
domain?: MetadataDomain;
|
|
45
|
+
/** Primary key field, default `'name'`. */
|
|
46
|
+
primaryKey?: string;
|
|
47
|
+
/** Fields searched by the free-text filter. Default `['name','label','description']`. */
|
|
48
|
+
searchableFields?: string[];
|
|
49
|
+
/** Columns rendered in the list page. */
|
|
50
|
+
listColumns?: Array<{
|
|
51
|
+
key: string;
|
|
52
|
+
label: string;
|
|
53
|
+
/** Optional cell renderer. `value` is `item[key]`. */
|
|
54
|
+
render?: (value: unknown, item: Record<string, unknown>) => ReactNode;
|
|
55
|
+
/** Column width hint in CSS (e.g. `'180px'`, `'30%'`). */
|
|
56
|
+
width?: string;
|
|
57
|
+
}>;
|
|
58
|
+
/**
|
|
59
|
+
* Fully custom list page. When provided, the generic shell is bypassed.
|
|
60
|
+
* Receives `{ type }`. Use this for ObjectManager, etc.
|
|
61
|
+
*/
|
|
62
|
+
ListPage?: ComponentType<{
|
|
63
|
+
type: string;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Fully custom edit page. Receives `{ type, name }`.
|
|
67
|
+
*
|
|
68
|
+
* Use this when the bespoke editor replaces the JSONSchema form entirely
|
|
69
|
+
* (e.g. PermissionMatrix grid). For visual designers that should coexist
|
|
70
|
+
* with the generic Form/Layers/References tabs, prefer `DesignerTab`.
|
|
71
|
+
*/
|
|
72
|
+
EditPage?: ComponentType<{
|
|
73
|
+
type: string;
|
|
74
|
+
name: string;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* Optional visual designer that renders inside its own dedicated tab on
|
|
78
|
+
* the generic edit page, alongside Form / Layers / References. Receives
|
|
79
|
+
* `{ type, name }` and should render without an outer `PageShell` — the
|
|
80
|
+
* generic engine owns the chrome.
|
|
81
|
+
*
|
|
82
|
+
* When both `EditPage` and `DesignerTab` are provided, `EditPage` wins
|
|
83
|
+
* (full takeover); the tab variant is only used when `EditPage` is unset.
|
|
84
|
+
*/
|
|
85
|
+
DesignerTab?: ComponentType<{
|
|
86
|
+
type: string;
|
|
87
|
+
name: string;
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Optional label for the Designer tab. Defaults to "Designer".
|
|
91
|
+
*/
|
|
92
|
+
designerTabLabel?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Fully custom create page. Receives `{ type }`.
|
|
95
|
+
*/
|
|
96
|
+
CreatePage?: ComponentType<{
|
|
97
|
+
type: string;
|
|
98
|
+
}>;
|
|
99
|
+
/** Fields hidden from the AutoForm (still serialised on save). */
|
|
100
|
+
hiddenFields?: string[];
|
|
101
|
+
/** Suggested form field order (top to bottom). */
|
|
102
|
+
fieldOrder?: string[];
|
|
103
|
+
/** Whether the type opts in to the history tab. Default true. */
|
|
104
|
+
supportsHistory?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Override the empty-state copy on the list page (e.g. "No views yet —
|
|
107
|
+
* Studio's Page Designer creates these for you").
|
|
108
|
+
*/
|
|
109
|
+
emptyStateHint?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Fallback JSONSchema used by the generic SchemaForm when the
|
|
112
|
+
* framework's `/meta/types` endpoint doesn't expose a `schema` field
|
|
113
|
+
* for this type (most types today). This lets us deliver a usable
|
|
114
|
+
* form-driven create/edit experience without first wiring full
|
|
115
|
+
* Zod→JSONSchema generation in the framework registry.
|
|
116
|
+
*
|
|
117
|
+
* If the server registry DOES return a `schema`, this is ignored.
|
|
118
|
+
*/
|
|
119
|
+
defaultSchema?: Record<string, unknown>;
|
|
120
|
+
/**
|
|
121
|
+
* Declares that items of this type are "anchored" to one or more
|
|
122
|
+
* parent metadata types. The Related tab on the parent's edit page
|
|
123
|
+
* lists every item that matches the anchor predicate.
|
|
124
|
+
*
|
|
125
|
+
* For example, registering `hook` with
|
|
126
|
+
* `{ anchorType: 'object', match: (item, name) => item.object === name }`
|
|
127
|
+
* makes every `hook` whose `object` field equals `account` show up in
|
|
128
|
+
* the Account object's Related tab under a "Hooks" group.
|
|
129
|
+
*
|
|
130
|
+
* Multiple anchors are allowed (e.g. a field could anchor to both
|
|
131
|
+
* `object` and `view`). The Related panel groups by anchored type.
|
|
132
|
+
*/
|
|
133
|
+
anchors?: MetadataAnchor[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Anchor declaration — "items of type X are children of items of type Y
|
|
137
|
+
* when this predicate matches". Used to power the Related tab.
|
|
138
|
+
*
|
|
139
|
+
* The predicate runs in the client against the unwrapped item shape
|
|
140
|
+
* returned by `client.list(type)`. Keep it cheap (just property reads);
|
|
141
|
+
* we evaluate it once per item per render.
|
|
142
|
+
*/
|
|
143
|
+
export interface MetadataAnchor {
|
|
144
|
+
/** The parent metadata type whose Related tab should surface this. */
|
|
145
|
+
anchorType: string;
|
|
146
|
+
/**
|
|
147
|
+
* Where the anchored items live.
|
|
148
|
+
* - 'list' (default): query `client.list(childType)` and filter with
|
|
149
|
+
* `match()`. Used for first-class metadata types (hooks, views, …).
|
|
150
|
+
* - 'embedded': items are stored inside the parent body itself
|
|
151
|
+
* (e.g. `object.fields[]`). `extract()` plucks the array; no
|
|
152
|
+
* network call is made.
|
|
153
|
+
*/
|
|
154
|
+
source?: 'list' | 'embedded';
|
|
155
|
+
/**
|
|
156
|
+
* Returns true when the child `item` belongs to the parent identified
|
|
157
|
+
* by `anchorName`. The default helper `byField('object')` covers the
|
|
158
|
+
* common case of an explicit `object: 'foo'` reference. Required when
|
|
159
|
+
* `source === 'list'` (the default); ignored for `'embedded'`.
|
|
160
|
+
*/
|
|
161
|
+
match?: (item: Record<string, unknown>, anchorName: string) => boolean;
|
|
162
|
+
/**
|
|
163
|
+
* For `source: 'embedded'` only. Returns the embedded items array
|
|
164
|
+
* given the fully-loaded parent body. Items should already carry a
|
|
165
|
+
* `name` (or be normalised by the renderer).
|
|
166
|
+
*/
|
|
167
|
+
extract?: (parentItem: Record<string, unknown>) => Array<Record<string, unknown>>;
|
|
168
|
+
/**
|
|
169
|
+
* Optional override for the group label shown on the Related panel.
|
|
170
|
+
* Defaults to the child type's resolved label.
|
|
171
|
+
*/
|
|
172
|
+
groupLabel?: string;
|
|
173
|
+
/**
|
|
174
|
+
* Optional icon override for the group header. Falls back to the
|
|
175
|
+
* child type's registered icon.
|
|
176
|
+
*/
|
|
177
|
+
iconName?: string;
|
|
178
|
+
/**
|
|
179
|
+
* Optional explicit ordering hint inside the Related panel. Lower
|
|
180
|
+
* numbers float to the top. Defaults to 100 if unset.
|
|
181
|
+
*/
|
|
182
|
+
order?: number;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Helper that returns a `match` reading a (possibly dotted) field path
|
|
186
|
+
* from the item and comparing it to the anchor name.
|
|
187
|
+
*
|
|
188
|
+
* anchorByField('object') // item.object === name
|
|
189
|
+
* anchorByField('data.object') // item.data?.object === name
|
|
190
|
+
* anchorByField(['list.data.object',
|
|
191
|
+
* 'form.data.object']) // either path matches
|
|
192
|
+
*/
|
|
193
|
+
export declare function anchorByField(paths: string | string[]): MetadataAnchor['match'];
|
|
194
|
+
/**
|
|
195
|
+
* Register (or merge) an entry. Idempotent — re-registering with the
|
|
196
|
+
* same `type` merges the new fields with any existing entry so that
|
|
197
|
+
* bespoke editors (e.g. PermissionMatrixEditor) and generic-engine
|
|
198
|
+
* defaults (e.g. `defaultSchema`) can be registered independently and
|
|
199
|
+
* coexist. Explicit `undefined` values do not overwrite.
|
|
200
|
+
*/
|
|
201
|
+
export declare function registerMetadataResource(config: MetadataResourceConfig): void;
|
|
202
|
+
/** Look up an entry. Returns `undefined` when the type isn't registered. */
|
|
203
|
+
export declare function getMetadataResource(type: string): MetadataResourceConfig | undefined;
|
|
204
|
+
/** Snapshot of all registered entries (diagnostics; directory page). */
|
|
205
|
+
export declare function listMetadataResources(): MetadataResourceConfig[];
|
|
206
|
+
/**
|
|
207
|
+
* Build the list of child types whose items can be "anchored to" a
|
|
208
|
+
* parent type. Used by the Related tab to decide which `client.list`
|
|
209
|
+
* calls to make.
|
|
210
|
+
*
|
|
211
|
+
* Returns an array of `{ type, anchor }` pairs, sorted by `anchor.order`
|
|
212
|
+
* (lower first) and then by child type label for stable rendering.
|
|
213
|
+
*/
|
|
214
|
+
export declare function listAnchorsFor(parentType: string): Array<{
|
|
215
|
+
type: string;
|
|
216
|
+
config: MetadataResourceConfig;
|
|
217
|
+
anchor: MetadataAnchor;
|
|
218
|
+
}>;
|
|
219
|
+
/**
|
|
220
|
+
* Merge a registered config (if any) with server-side defaults from
|
|
221
|
+
* `/meta/types`. Server fields win for label/description/domain
|
|
222
|
+
* (the spec is source of truth); user-registered fields win for
|
|
223
|
+
* everything else (UI behaviour).
|
|
224
|
+
*/
|
|
225
|
+
export declare function resolveResourceConfig(type: string, serverEntry?: {
|
|
226
|
+
label?: string;
|
|
227
|
+
description?: string;
|
|
228
|
+
domain?: string;
|
|
229
|
+
allowOrgOverride?: boolean;
|
|
230
|
+
}): MetadataResourceConfig & {
|
|
231
|
+
allowOrgOverride?: boolean;
|
|
232
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Helper that returns a `match` reading a (possibly dotted) field path
|
|
4
|
+
* from the item and comparing it to the anchor name.
|
|
5
|
+
*
|
|
6
|
+
* anchorByField('object') // item.object === name
|
|
7
|
+
* anchorByField('data.object') // item.data?.object === name
|
|
8
|
+
* anchorByField(['list.data.object',
|
|
9
|
+
* 'form.data.object']) // either path matches
|
|
10
|
+
*/
|
|
11
|
+
export function anchorByField(paths) {
|
|
12
|
+
const list = (Array.isArray(paths) ? paths : [paths]).map((p) => p.split('.'));
|
|
13
|
+
return (item, name) => {
|
|
14
|
+
for (const segs of list) {
|
|
15
|
+
let cur = item;
|
|
16
|
+
for (const s of segs) {
|
|
17
|
+
if (cur && typeof cur === 'object' && s in cur) {
|
|
18
|
+
cur = cur[s];
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
cur = undefined;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (cur === name)
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const REGISTRY = new Map();
|
|
32
|
+
/**
|
|
33
|
+
* Register (or merge) an entry. Idempotent — re-registering with the
|
|
34
|
+
* same `type` merges the new fields with any existing entry so that
|
|
35
|
+
* bespoke editors (e.g. PermissionMatrixEditor) and generic-engine
|
|
36
|
+
* defaults (e.g. `defaultSchema`) can be registered independently and
|
|
37
|
+
* coexist. Explicit `undefined` values do not overwrite.
|
|
38
|
+
*/
|
|
39
|
+
export function registerMetadataResource(config) {
|
|
40
|
+
const prev = REGISTRY.get(config.type);
|
|
41
|
+
if (!prev) {
|
|
42
|
+
REGISTRY.set(config.type, config);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const merged = { ...prev };
|
|
46
|
+
for (const [k, v] of Object.entries(config)) {
|
|
47
|
+
if (v !== undefined)
|
|
48
|
+
merged[k] = v;
|
|
49
|
+
}
|
|
50
|
+
REGISTRY.set(config.type, merged);
|
|
51
|
+
}
|
|
52
|
+
/** Look up an entry. Returns `undefined` when the type isn't registered. */
|
|
53
|
+
export function getMetadataResource(type) {
|
|
54
|
+
return REGISTRY.get(type);
|
|
55
|
+
}
|
|
56
|
+
/** Snapshot of all registered entries (diagnostics; directory page). */
|
|
57
|
+
export function listMetadataResources() {
|
|
58
|
+
return Array.from(REGISTRY.values());
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build the list of child types whose items can be "anchored to" a
|
|
62
|
+
* parent type. Used by the Related tab to decide which `client.list`
|
|
63
|
+
* calls to make.
|
|
64
|
+
*
|
|
65
|
+
* Returns an array of `{ type, anchor }` pairs, sorted by `anchor.order`
|
|
66
|
+
* (lower first) and then by child type label for stable rendering.
|
|
67
|
+
*/
|
|
68
|
+
export function listAnchorsFor(parentType) {
|
|
69
|
+
const hits = [];
|
|
70
|
+
for (const cfg of REGISTRY.values()) {
|
|
71
|
+
if (!cfg.anchors?.length)
|
|
72
|
+
continue;
|
|
73
|
+
for (const a of cfg.anchors) {
|
|
74
|
+
if (a.anchorType === parentType)
|
|
75
|
+
hits.push({ type: cfg.type, config: cfg, anchor: a });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
hits.sort((a, b) => {
|
|
79
|
+
const ao = a.anchor.order ?? 100;
|
|
80
|
+
const bo = b.anchor.order ?? 100;
|
|
81
|
+
if (ao !== bo)
|
|
82
|
+
return ao - bo;
|
|
83
|
+
const al = a.anchor.groupLabel ?? a.config.label ?? a.type;
|
|
84
|
+
const bl = b.anchor.groupLabel ?? b.config.label ?? b.type;
|
|
85
|
+
return al.localeCompare(bl);
|
|
86
|
+
});
|
|
87
|
+
return hits;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Merge a registered config (if any) with server-side defaults from
|
|
91
|
+
* `/meta/types`. Server fields win for label/description/domain
|
|
92
|
+
* (the spec is source of truth); user-registered fields win for
|
|
93
|
+
* everything else (UI behaviour).
|
|
94
|
+
*/
|
|
95
|
+
export function resolveResourceConfig(type, serverEntry) {
|
|
96
|
+
const registered = REGISTRY.get(type) ?? { type };
|
|
97
|
+
return {
|
|
98
|
+
...registered,
|
|
99
|
+
type,
|
|
100
|
+
label: registered.label ?? serverEntry?.label ?? type,
|
|
101
|
+
description: registered.description ?? serverEntry?.description,
|
|
102
|
+
domain: (registered.domain ?? serverEntry?.domain ?? 'other'),
|
|
103
|
+
allowOrgOverride: serverEntry?.allowOrgOverride,
|
|
104
|
+
defaultSchema: registered.defaultSchema,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { MetadataClient } from '@object-ui/data-objectstack';
|
|
2
|
+
export interface RichMetadataTypeEntry {
|
|
3
|
+
type: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
domain?: string;
|
|
7
|
+
allowOrgOverride?: boolean;
|
|
8
|
+
/** 'registry' = ADR opt-in; 'env' = unlocked via OBJECTSTACK_METADATA_WRITABLE. */
|
|
9
|
+
overrideSource?: 'registry' | 'env';
|
|
10
|
+
supportsOverlay?: boolean;
|
|
11
|
+
loadOrder?: number;
|
|
12
|
+
/** JSONSchema for the type's item shape (Phase 3a addition). */
|
|
13
|
+
schema?: Record<string, unknown>;
|
|
14
|
+
/** Canonical FormView layout for the type's editor (Phase 3c+). */
|
|
15
|
+
form?: Record<string, unknown>;
|
|
16
|
+
/** UI hints (icon, color, etc.) the framework may include. */
|
|
17
|
+
ui?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
/** Use a single MetadataClient for the whole admin engine. */
|
|
20
|
+
export declare function useMetadataClient(environmentId?: string): MetadataClient;
|
|
21
|
+
/**
|
|
22
|
+
* Fetch and cache the rich `/meta/types` registry response. Most pages
|
|
23
|
+
* only need it once per session, so we memoise per (client) instance.
|
|
24
|
+
*/
|
|
25
|
+
export declare function useMetadataTypes(client: MetadataClient): {
|
|
26
|
+
loading: boolean;
|
|
27
|
+
error: string | null;
|
|
28
|
+
entries: RichMetadataTypeEntry[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Build a stable lookup map by type id from the entries array. Most
|
|
32
|
+
* pages need O(1) access to "what's the label / writable status for
|
|
33
|
+
* type X?".
|
|
34
|
+
*/
|
|
35
|
+
export declare function useTypesIndex(entries: RichMetadataTypeEntry[]): Record<string, RichMetadataTypeEntry>;
|
|
36
|
+
/** Free-text filter helper used by list pages + QuickFind. */
|
|
37
|
+
export declare function matchesQuery(item: Record<string, unknown>, query: string, fields?: string[]): boolean;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
/**
|
|
3
|
+
* Shared hooks + types for the metadata-admin engine (Phase 3c).
|
|
4
|
+
*
|
|
5
|
+
* Centralises the MetadataClient creation and the rich `/meta/types`
|
|
6
|
+
* row shape so every page reads from the same source of truth.
|
|
7
|
+
*
|
|
8
|
+
* Why a tiny barrel?
|
|
9
|
+
* • DirectoryPage, ListPage, EditPage, HistoryPage and QuickFind
|
|
10
|
+
* all need (client, typesRegistry). Letting each page re-fetch
|
|
11
|
+
* would be wasteful and produce flickering badges.
|
|
12
|
+
* • The hook caches the client per `baseUrl + environmentId` pair
|
|
13
|
+
* so `client.withEnvironment(...)` swaps without remounting
|
|
14
|
+
* consumers.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
17
|
+
import { MetadataClient } from '@object-ui/data-objectstack';
|
|
18
|
+
/** Use a single MetadataClient for the whole admin engine. */
|
|
19
|
+
export function useMetadataClient(environmentId) {
|
|
20
|
+
return useMemo(() => {
|
|
21
|
+
const c = new MetadataClient({ baseUrl: '' });
|
|
22
|
+
return environmentId ? c.withEnvironment(environmentId) : c;
|
|
23
|
+
}, [environmentId]);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fetch and cache the rich `/meta/types` registry response. Most pages
|
|
27
|
+
* only need it once per session, so we memoise per (client) instance.
|
|
28
|
+
*/
|
|
29
|
+
export function useMetadataTypes(client) {
|
|
30
|
+
const [loading, setLoading] = useState(true);
|
|
31
|
+
const [error, setError] = useState(null);
|
|
32
|
+
const [entries, setEntries] = useState([]);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
let cancelled = false;
|
|
35
|
+
setLoading(true);
|
|
36
|
+
setError(null);
|
|
37
|
+
(async () => {
|
|
38
|
+
try {
|
|
39
|
+
const result = await client.listTypes();
|
|
40
|
+
let list;
|
|
41
|
+
if (Array.isArray(result)) {
|
|
42
|
+
list = result.map((t) => typeof t === 'string' ? { type: t } : t);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const rich = result?.entries;
|
|
46
|
+
if (Array.isArray(rich) && rich.length > 0) {
|
|
47
|
+
list = rich;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
const names = result?.types ?? [];
|
|
51
|
+
list = names.map((t) => ({ type: t }));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!cancelled) {
|
|
55
|
+
setEntries(list);
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (!cancelled) {
|
|
61
|
+
setError(err?.message ?? String(err));
|
|
62
|
+
setLoading(false);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})();
|
|
66
|
+
return () => {
|
|
67
|
+
cancelled = true;
|
|
68
|
+
};
|
|
69
|
+
}, [client]);
|
|
70
|
+
return { loading, error, entries };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a stable lookup map by type id from the entries array. Most
|
|
74
|
+
* pages need O(1) access to "what's the label / writable status for
|
|
75
|
+
* type X?".
|
|
76
|
+
*/
|
|
77
|
+
export function useTypesIndex(entries) {
|
|
78
|
+
return useMemo(() => {
|
|
79
|
+
const idx = {};
|
|
80
|
+
for (const e of entries)
|
|
81
|
+
idx[e.type] = e;
|
|
82
|
+
return idx;
|
|
83
|
+
}, [entries]);
|
|
84
|
+
}
|
|
85
|
+
/** Free-text filter helper used by list pages + QuickFind. */
|
|
86
|
+
export function matchesQuery(item, query, fields = ['name', 'label', 'description']) {
|
|
87
|
+
if (!query)
|
|
88
|
+
return true;
|
|
89
|
+
const q = query.toLowerCase();
|
|
90
|
+
for (const f of fields) {
|
|
91
|
+
const v = item[f];
|
|
92
|
+
if (typeof v === 'string' && v.toLowerCase().includes(q))
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in widget renderers for SchemaForm.
|
|
3
|
+
*
|
|
4
|
+
* These cover the common widget hints declared in spec `*.form.ts`
|
|
5
|
+
* files (e.g. `widget: 'ref:object'`, `widget: 'master-detail'`).
|
|
6
|
+
*
|
|
7
|
+
* Each widget receives `WidgetProps`:
|
|
8
|
+
* - schema — the JSONSchema fragment for THIS field (so widgets
|
|
9
|
+
* for nested arrays can read items.properties)
|
|
10
|
+
* - value — the current value
|
|
11
|
+
* - onChange — write-back callback
|
|
12
|
+
* - readOnly — disable
|
|
13
|
+
* - context — out-of-band data (object list, ObjectQL fields, …)
|
|
14
|
+
*
|
|
15
|
+
* To register a new widget, add an entry to `WIDGETS` below. To wire
|
|
16
|
+
* extra context (e.g. a fields list), extend `WidgetContext` in
|
|
17
|
+
* SchemaForm.tsx and prefetch it in ResourceEditPage.
|
|
18
|
+
*/
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
export interface WidgetContext {
|
|
21
|
+
/** Names of all object metadata records (for `ref:object`). */
|
|
22
|
+
objectNames?: string[];
|
|
23
|
+
/** Loading flag for the object list. */
|
|
24
|
+
objectsLoading?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface WidgetProps {
|
|
27
|
+
id?: string;
|
|
28
|
+
schema: Record<string, any>;
|
|
29
|
+
value: unknown;
|
|
30
|
+
onChange: (v: unknown) => void;
|
|
31
|
+
readOnly?: boolean;
|
|
32
|
+
context?: WidgetContext;
|
|
33
|
+
/** Optional FormFieldSpec with type/options/reference/constraints */
|
|
34
|
+
fieldSpec?: {
|
|
35
|
+
field: string;
|
|
36
|
+
type?: string;
|
|
37
|
+
options?: Array<{
|
|
38
|
+
label: string;
|
|
39
|
+
value: string;
|
|
40
|
+
color?: string;
|
|
41
|
+
}>;
|
|
42
|
+
reference?: string;
|
|
43
|
+
maxLength?: number;
|
|
44
|
+
minLength?: number;
|
|
45
|
+
min?: number;
|
|
46
|
+
max?: number;
|
|
47
|
+
multiple?: boolean;
|
|
48
|
+
dependsOn?: string | string[];
|
|
49
|
+
/** Sub-fields for `composite` / `repeater` types */
|
|
50
|
+
fields?: Array<any>;
|
|
51
|
+
/** Code editor language (for type=code) */
|
|
52
|
+
language?: string;
|
|
53
|
+
/** Form-level helpers passed through from FormField */
|
|
54
|
+
label?: string;
|
|
55
|
+
placeholder?: string;
|
|
56
|
+
helpText?: string;
|
|
57
|
+
widget?: string;
|
|
58
|
+
colSpan?: number;
|
|
59
|
+
immutable?: boolean;
|
|
60
|
+
readonly?: boolean;
|
|
61
|
+
required?: boolean;
|
|
62
|
+
};
|
|
63
|
+
/** All form data (for reading dependency values) */
|
|
64
|
+
formData?: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
export type WidgetRenderer = (props: WidgetProps) => React.ReactElement;
|
|
67
|
+
export declare const WIDGETS: Record<string, WidgetRenderer>;
|
|
68
|
+
export declare function CodeWidget({ schema, value, onChange, readOnly, fieldSpec, }: WidgetProps): import("react/jsx-runtime").JSX.Element;
|