Skip to content

Commit 6943854

Browse files
committed
feat(metadata-admin): support allowRuntimeCreate for db-only items
1 parent d8f7c43 commit 6943854

7 files changed

Lines changed: 94 additions & 25 deletions

File tree

packages/app-shell/src/views/metadata-admin/DirectoryPage.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,13 @@ export function MetadataDirectoryPage() {
8989
return c;
9090
}, [entries]);
9191

92-
const writableCount = entries.filter((e) => !HIDDEN_TYPES.has(e.type) && e.allowOrgOverride).length;
92+
const writableCount = entries.filter(
93+
(e) => !HIDDEN_TYPES.has(e.type) && (e.allowOrgOverride || e.allowRuntimeCreate),
94+
).length;
9395

9496
const filtered = entries.filter((e) => {
9597
if (HIDDEN_TYPES.has(e.type)) return false;
96-
if (writableOnly && !e.allowOrgOverride) return false;
98+
if (writableOnly && !(e.allowOrgOverride || e.allowRuntimeCreate)) return false;
9799
if (domainFilter !== 'all' && (e.domain ?? 'other') !== domainFilter) return false;
98100
if (query) {
99101
const q = query.toLowerCase();
@@ -282,6 +284,13 @@ function TypeTile({ entry, locale }: { entry: RichMetadataTypeEntry; locale?: st
282284
>
283285
{t('engine.badge.writable', locale)}
284286
</Badge>
287+
) : entry.allowRuntimeCreate ? (
288+
<Badge
289+
className="text-[10px] shrink-0 bg-sky-100 text-sky-800 hover:bg-sky-100"
290+
title="Code-shipped items are locked; new items can be created at runtime"
291+
>
292+
{t('engine.badge.createOnly', locale)}
293+
</Badge>
285294
) : (
286295
<Badge variant="outline" className="text-[10px] shrink-0 text-muted-foreground">
287296
{t('engine.badge.readOnly', locale)}

packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function EmbeddedItemEditor({
7373
}, [initialRaw, itemName, parentType, parentName]);
7474

7575
const readOnly =
76-
subEntry != null && !subEntry.allowOrgOverride;
76+
subEntry != null && !subEntry.allowOrgOverride && !subEntry.allowRuntimeCreate;
7777

7878
async function doSave() {
7979
if (!embeddedPath) {

packages/app-shell/src/views/metadata-admin/PageShell.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ export function PageShell({
112112
>
113113
{t('engine.badge.writable', locale)}
114114
</Badge>
115+
) : entry?.allowRuntimeCreate ? (
116+
<Badge
117+
className="text-[10px] bg-sky-100 text-sky-800 hover:bg-sky-100"
118+
title="Code-shipped items are locked; new items can be created at runtime"
119+
>
120+
{t('engine.badge.createOnly', locale)}
121+
</Badge>
115122
) : (
116123
<Badge variant="outline" className="text-[10px] text-muted-foreground">
117124
{t('engine.badge.readOnly', locale)}

packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,23 @@ export function MetadataResourceEditPage({
434434
const schema =
435435
(entry?.schema as Record<string, unknown> | undefined) ??
436436
(config.defaultSchema as Record<string, unknown> | undefined);
437-
const readOnly = !entry?.allowOrgOverride && !createMode;
437+
438+
// Two-tier authorization (PR-10d.7):
439+
// - artifact-backed items (layered.code != null) need allowOrgOverride
440+
// - DB-only items (no artifact) need allowOrgOverride OR allowRuntimeCreate
441+
// - createMode is always writable (the server will gate on intent)
442+
const isArtifactItem = !createMode && layered?.code != null;
443+
const canWrite = createMode
444+
? !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate)
445+
: isArtifactItem
446+
? !!entry?.allowOrgOverride
447+
: !!(entry?.allowOrgOverride || entry?.allowRuntimeCreate);
448+
const readOnly = !canWrite && !createMode;
449+
// Banner variant: when type ships with allowRuntimeCreate but this
450+
// specific item is locked because it comes from a code package, we
451+
// show a different message inviting the user to create their own.
452+
const showArtifactLockedBanner =
453+
readOnly && isArtifactItem && !!entry?.allowRuntimeCreate;
438454

439455
// Preview tab — opt-in via `registerMetadataPreview()`. Hidden in
440456
// create mode (nothing to preview yet) and inside the embedded
@@ -494,7 +510,7 @@ export function MetadataResourceEditPage({
494510
: t('engine.edit.hideInspector', locale)}
495511
</Button>
496512
)}
497-
{!createMode && entry?.allowOrgOverride && (
513+
{!createMode && canWrite && layered?.overlay && (
498514
<Button variant="ghost" size="sm" onClick={doReset} disabled={saving}>
499515
<RotateCcw className="h-4 w-4 mr-1" />
500516
{t('engine.edit.reset', locale)}
@@ -517,19 +533,19 @@ export function MetadataResourceEditPage({
517533
discard the whole create flow which is awkward; users can
518534
navigate away to cancel).
519535
Truly read-only types (no allowOrgOverride) skip all of this. */}
520-
{entry?.allowOrgOverride && !createMode && !editing && (
536+
{canWrite && !createMode && !editing && (
521537
<Button size="sm" onClick={() => setEditing(true)}>
522538
<Pencil className="h-4 w-4 mr-1" />
523539
{t('engine.edit.edit', locale)}
524540
</Button>
525541
)}
526-
{entry?.allowOrgOverride && !createMode && editing && (
542+
{canWrite && !createMode && editing && (
527543
<Button variant="ghost" size="sm" onClick={doCancelEdit} disabled={saving}>
528544
<X className="h-4 w-4 mr-1" />
529545
{t('engine.cancel', locale)}
530546
</Button>
531547
)}
532-
{entry?.allowOrgOverride && (editing || createMode) && (
548+
{canWrite && (editing || createMode) && (
533549
<Button
534550
size="sm"
535551
onClick={() => doSave(false)}
@@ -578,20 +594,44 @@ export function MetadataResourceEditPage({
578594
</div>
579595
)}
580596
{readOnly && (
581-
<div className="text-xs text-amber-800 border border-amber-300 bg-amber-50 rounded p-3 dark:text-amber-200 dark:border-amber-700/50 dark:bg-amber-950/30">
582-
{/* The platform i18n bundle ships `engine.edit.readOnlyTypeBanner`
583-
with `{flag} / {type} / {override}` placeholders so the
584-
monospace tokens are inlined inside the translated sentence
585-
in any locale. Splitting on the three tokens preserves the
586-
sentence order across translations. */}
587-
{t('engine.edit.readOnlyTypeBanner', locale)
588-
.split(/(\{flag\}|\{type\}|\{override\})/)
589-
.map((part, i) => {
590-
if (part === '{flag}') return <code key={i} className="font-mono">OBJECTSTACK_METADATA_WRITABLE</code>;
591-
if (part === '{type}') return <code key={i} className="font-mono">{type}</code>;
592-
if (part === '{override}') return <code key={i} className="font-mono">allowOrgOverride</code>;
593-
return <React.Fragment key={i}>{part}</React.Fragment>;
594-
})}
597+
<div className="text-xs text-amber-800 border border-amber-300 bg-amber-50 rounded p-3 dark:text-amber-200 dark:border-amber-700/50 dark:bg-amber-950/30 flex items-start gap-3">
598+
<div className="flex-1">
599+
{showArtifactLockedBanner ? (
600+
/* Type allows runtime-create but THIS item ships from
601+
a code package. Tell the user clearly and provide
602+
a CTA to author their own. */
603+
t('engine.edit.artifactLockedBanner', locale)
604+
.split(/(\{type\})/)
605+
.map((part, i) => {
606+
if (part === '{type}') return <code key={i} className="font-mono">{type}</code>;
607+
return <React.Fragment key={i}>{part}</React.Fragment>;
608+
})
609+
) : (
610+
/* The platform i18n bundle ships `engine.edit.readOnlyTypeBanner`
611+
with `{flag} / {type} / {override}` placeholders so the
612+
monospace tokens are inlined inside the translated sentence
613+
in any locale. Splitting on the three tokens preserves the
614+
sentence order across translations. */
615+
t('engine.edit.readOnlyTypeBanner', locale)
616+
.split(/(\{flag\}|\{type\}|\{override\})/)
617+
.map((part, i) => {
618+
if (part === '{flag}') return <code key={i} className="font-mono">OBJECTSTACK_METADATA_WRITABLE</code>;
619+
if (part === '{type}') return <code key={i} className="font-mono">{type}</code>;
620+
if (part === '{override}') return <code key={i} className="font-mono">allowOrgOverride</code>;
621+
return <React.Fragment key={i}>{part}</React.Fragment>;
622+
})
623+
)}
624+
</div>
625+
{showArtifactLockedBanner && (
626+
<Button
627+
size="sm"
628+
variant="outline"
629+
className="shrink-0"
630+
onClick={() => navigate(`../new`)}
631+
>
632+
{t('engine.list.create', locale)}
633+
</Button>
634+
)}
595635
</div>
596636
)}
597637
</div>
@@ -660,7 +700,7 @@ export function MetadataResourceEditPage({
660700
"可写" badge in the header plus the Edit button in the
661701
action bar already convey both signal and call-to-action,
662702
and saving every vertical pixel for the canvas matters. */}
663-
{!PreviewComponent && formReadOnly && !readOnly && entry?.allowOrgOverride && !createMode && (
703+
{!PreviewComponent && formReadOnly && !readOnly && canWrite && !createMode && (
664704
<div className="flex items-center justify-between gap-3 text-xs text-muted-foreground border rounded p-2.5 bg-muted/30">
665705
<span>
666706
{t('engine.edit.readOnlyBanner', locale).split(/\{edit\}/).map((part, i, arr) => (

packages/app-shell/src/views/metadata-admin/ResourceListPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ function DefaultMetadataList({ type }: { type: string }) {
157157
>
158158
<RefreshCw className="h-4 w-4" />
159159
</Button>
160-
{entry?.allowOrgOverride && (
160+
{(entry?.allowOrgOverride || entry?.allowRuntimeCreate) && (
161161
<Button
162162
size="sm"
163163
onClick={() => navigate(`./new`)}
@@ -212,7 +212,7 @@ function DefaultMetadataList({ type }: { type: string }) {
212212
</EmptyTitle>
213213
<EmptyDescription>
214214
{config.emptyStateHint ??
215-
(entry?.allowOrgOverride
215+
(entry?.allowOrgOverride || entry?.allowRuntimeCreate
216216
? tFormat('engine.list.createHint', locale, { type: typeLabel })
217217
: t('engine.list.readOnlyHint', locale))}
218218
</EmptyDescription>

packages/app-shell/src/views/metadata-admin/i18n.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ const ENGINE_STRINGS_EN: Record<string, string> = {
171171
'engine.edit.readOnlyBanner': 'Viewing in read-only mode. Click {edit} to make changes.',
172172
'engine.edit.readOnlyTypeBanner':
173173
'This metadata type is read-only for safety: it ships executable code or sensitive configuration and cannot be overlaid at runtime. Edit the source in your package and redeploy. To override this lock (use with caution), set {flag} to include {type}, or flip {override} in the registry.',
174+
'engine.edit.artifactLockedBanner':
175+
'This item is shipped by a code package and cannot be modified at runtime. You can still create your own {type} from scratch and freely edit or delete it.',
176+
'engine.badge.createOnly': 'create-only',
174177
'engine.repeater.empty': 'No items. Click + to add.',
175178
'engine.badge.writable': 'writable',
176179
'engine.badge.readOnly': 'read-only',
@@ -285,6 +288,9 @@ const ENGINE_STRINGS_ZH: Record<string, string> = {
285288
'engine.edit.readOnlyBanner': '当前为只读模式。点击 {edit} 进行修改。',
286289
'engine.edit.readOnlyTypeBanner':
287290
'出于安全考虑,此元数据类型为只读:它包含可执行代码或敏感配置,不允许在运行时通过覆盖层修改。请编辑包内源代码后重新部署。如需临时解除限制(请谨慎使用),可将 {flag} 设置为包含 {type},或在注册表中开启 {override}。',
291+
'engine.edit.artifactLockedBanner':
292+
'此条目由代码包提供,无法在运行时修改。但您可以新建自己的 {type},并自由地编辑或删除。',
293+
'engine.badge.createOnly': '仅可新建',
288294
'engine.repeater.empty': '暂无条目。点击 + 添加。',
289295
'engine.badge.writable': '可写',
290296
'engine.badge.readOnly': '只读',

packages/app-shell/src/views/metadata-admin/useMetadata.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ export interface RichMetadataTypeEntry {
2424
description?: string;
2525
domain?: string;
2626
allowOrgOverride?: boolean;
27+
/**
28+
* Two-tier model (PR-10d.7): brand-new items of this type can be
29+
* authored at runtime even when `allowOrgOverride` is false. UI
30+
* affordances ("+ New", Save, Delete on DB-only items) should
31+
* activate when either flag is true.
32+
*/
33+
allowRuntimeCreate?: boolean;
2734
/** 'registry' = ADR opt-in; 'env' = unlocked via OBJECTSTACK_METADATA_WRITABLE. */
2835
overrideSource?: 'registry' | 'env';
2936
supportsOverlay?: boolean;

0 commit comments

Comments
 (0)