@@ -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 ( / ( \{ f l a g \} | \{ t y p e \} | \{ o v e r r i d e \} ) / )
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 ( / ( \{ t y p e \} ) / )
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 ( / ( \{ f l a g \} | \{ t y p e \} | \{ o v e r r i d e \} ) / )
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 ( / \{ e d i t \} / ) . map ( ( part , i , arr ) => (
0 commit comments