From 55b2f77c1d0f44682457e429a7cfb2844fda97d0 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 15 Jan 2026 21:45:00 +0800 Subject: [PATCH 01/14] Add canEdit prop override to BoxComponent for field editability control --- packages/base/field-component.gts | 187 +++++++++++++++--------------- 1 file changed, 95 insertions(+), 92 deletions(-) diff --git a/packages/base/field-component.gts b/packages/base/field-component.gts index c1db3eb7e6..5047c345d3 100644 --- a/packages/base/field-component.gts +++ b/packages/base/field-component.gts @@ -35,7 +35,7 @@ import { import Modifier from 'ember-modifier'; import { isEqual, flatMap } from 'lodash'; import { initSharedState } from './shared-state'; -import { and, cn, eq, not } from '@cardstack/boxel-ui/helpers'; +import { and, cn, eq, not, or } from '@cardstack/boxel-ui/helpers'; import { consume, provide } from 'ember-provide-consume-context'; import Component from '@glimmer/component'; import { concat } from '@ember/helper'; @@ -49,6 +49,7 @@ export interface BoxComponentSignature { format?: Format; displayContainer?: boolean; typeConstraint?: ResolvedCodeRef; + canEdit?: boolean; }; }; Blocks: {}; @@ -289,53 +290,96 @@ export function getBoxComponent( - - {{#let - (determineFormats @format defaultFormats) - as |effectiveFormats| - }} + {{#let + (or + @canEdit + (and + (not field.computeVia) + (not field.queryDefinition) + permissions.canWrite + ) + ) + as |canEdit| + }} + {{#let - (lookupComponents - (if - (isCard model.value) - effectiveFormats.cardDef - effectiveFormats.fieldDef - ) - ) - (if (eq @displayContainer false) false true) - as |c displayContainer| + (determineFormats @format defaultFormats) + as |effectiveFormats| }} - {{#if (isCard model.value)}} - {{#let model.value as |card|}} + {{#let + (lookupComponents + (if + (isCard model.value) + effectiveFormats.cardDef + effectiveFormats.fieldDef + ) + ) + (if (eq @displayContainer false) false true) + as |c displayContainer| + }} + {{#if (isCard model.value)}} + {{#let model.value as |card|}} + + + + + + {{/let}} + {{else if (isCompoundField model.value)}} - - + - {{/let}} - {{else if (isCompoundField model.value)}} - -
-
-
- {{else}} - - - - {{/if}} + + {{/if}} + {{/let}} {{/let}} - {{/let}} -
+
+ {{/let}}
From f042d6dfbe5ef1ac82912dc6b6adba576490bd6b Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 15 Jan 2026 21:48:54 +0800 Subject: [PATCH 02/14] implement code snippet on isolated and edit template --- .../catalog-realm/components/code-snippet.gts | 146 ++++++++++++++++++ .../components/field-spec-edit-template.gts | 31 +++- .../field-spec-isolated-template.gts | 35 ++++- 3 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 packages/catalog-realm/components/code-snippet.gts diff --git a/packages/catalog-realm/components/code-snippet.gts b/packages/catalog-realm/components/code-snippet.gts new file mode 100644 index 0000000000..499a22160d --- /dev/null +++ b/packages/catalog-realm/components/code-snippet.gts @@ -0,0 +1,146 @@ +import GlimmerComponent from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { on } from '@ember/modifier'; +import CopyIcon from '@cardstack/boxel-icons/copy'; +import CopyCheckIcon from '@cardstack/boxel-icons/copy-check'; + +interface CodeSnippetSignature { + Args: { + code: string; + label?: string; + }; + Element: HTMLDivElement; +} + +export default class CodeSnippet extends GlimmerComponent { + @tracked private isCopied = false; + private copyTimeout: ReturnType | null = null; + + get label() { + return this.args.label ?? 'Code'; + } + + @action + async copyCode() { + try { + await navigator.clipboard.writeText(this.args.code); + + // Clear any existing timeout + if (this.copyTimeout) { + clearTimeout(this.copyTimeout); + } + + this.isCopied = true; + + this.copyTimeout = setTimeout(() => { + this.isCopied = false; + this.copyTimeout = null; + }, 2000); + } catch (err) { + console.error('Failed to copy code:', err); + } + } + + willDestroy() { + super.willDestroy(); + if (this.copyTimeout) { + clearTimeout(this.copyTimeout); + } + } + + +} diff --git a/packages/catalog-realm/field-spec/components/field-spec-edit-template.gts b/packages/catalog-realm/field-spec/components/field-spec-edit-template.gts index 1f3d7e2db7..0c22cf4787 100644 --- a/packages/catalog-realm/field-spec/components/field-spec-edit-template.gts +++ b/packages/catalog-realm/field-spec/components/field-spec-edit-template.gts @@ -5,6 +5,7 @@ import { BaseDef, getCardMeta, getFields, + type Field, } from 'https://cardstack.com/base/card-api'; import { FieldContainer, @@ -14,6 +15,7 @@ import { import BookOpenText from '@cardstack/boxel-icons/book-open-text'; import GitBranch from '@cardstack/boxel-icons/git-branch'; import LayoutList from '@cardstack/boxel-icons/layout-list'; +import CodeSnippet from '../../components/code-snippet'; import { DiagonalArrowLeftUp as ExportArrow } from '@cardstack/boxel-ui/icons'; import { action } from '@ember/object'; import { on } from '@ember/modifier'; @@ -122,7 +124,7 @@ export default class FieldSpecEditTemplate extends Component { return getCardMeta(this.args.model as CardDef, 'realmInfo'); } - get allFields() { + get allFields(): Record { if (!this.args.model) return {}; return getFields(this.args.model, { includeComputeds: false, @@ -139,6 +141,25 @@ export default class FieldSpecEditTemplate extends Component { return this.args.model as CardDef; } + getFieldCodeSnippet = (fieldName: string): string => { + const field = this.allFields[fieldName]; + if (!field) return ''; + + const cardName = field.card?.name || 'Unknown'; + const fieldType = field.fieldType; + const config = field.configuration; + + if (config && typeof config !== 'function') { + const configStr = JSON.stringify(config, null, 2) + .split('\n') + .map((line, i) => (i === 0 ? line : ' ' + line)) + .join('\n'); + return `@field ${fieldName} = ${fieldType}(${cardName}, {\n configuration: ${configStr},\n});`; + } + + return `@field ${fieldName} = ${fieldType}(${cardName});`; + }; + diff --git a/packages/catalog-realm/fields/date/year.gts b/packages/catalog-realm/fields/date/year.gts index dc864b8b15..ab92d38d93 100644 --- a/packages/catalog-realm/fields/date/year.gts +++ b/packages/catalog-realm/fields/date/year.gts @@ -8,6 +8,7 @@ import NumberField from 'https://cardstack.com/base/number'; import { action } from '@ember/object'; import CalendarEventIcon from '@cardstack/boxel-icons/calendar-event'; import { BoxelSelect } from '@cardstack/boxel-ui/components'; +import { not } from '@cardstack/boxel-ui/helpers'; class YearFieldEdit extends Component { get years() { @@ -26,6 +27,7 @@ class YearFieldEdit extends Component { @onChange={{this.updateValue}} @placeholder='Select year' @dropdownClass='year-dropdown' + @disabled={{not @canEdit}} data-test-year-select as |year| > diff --git a/packages/catalog-realm/fields/image.gts b/packages/catalog-realm/fields/image.gts index f951ebc396..96cd7f8b7d 100644 --- a/packages/catalog-realm/fields/image.gts +++ b/packages/catalog-realm/fields/image.gts @@ -1,4 +1,4 @@ -import { eq } from '@cardstack/boxel-ui/helpers'; +import { eq, not } from '@cardstack/boxel-ui/helpers'; import { FieldDef, Component, @@ -354,6 +354,7 @@ class ImageFieldEdit extends Component { @hasPendingUpload={{this.hasPendingUpload}} @isReading={{this.isReading}} @readProgress={{this.readProgress}} + @disabled={{not @canEdit}} /> {{else if (eq this.variant 'dropzone')}} { @isReading={{this.isReading}} @readProgress={{this.readProgress}} @previewImageFit={{this.previewImageFit}} + @disabled={{not @canEdit}} /> {{else}} { @isReading={{this.isReading}} @readProgress={{this.readProgress}} @previewImageFit={{this.previewImageFit}} + @disabled={{not @canEdit}} /> {{/if}} @@ -404,15 +407,22 @@ class ImageFieldEdit extends Component { {{else}} {{! Upload trigger components }} {{#if (eq this.variant 'avatar')}} - + {{else if (eq this.variant 'dropzone')}} {{else}} - + {{/if}} {{/if}} diff --git a/packages/catalog-realm/fields/image/components/image-avatar-preview.gts b/packages/catalog-realm/fields/image/components/image-avatar-preview.gts index 37c0afe369..f9ee61aa55 100644 --- a/packages/catalog-realm/fields/image/components/image-avatar-preview.gts +++ b/packages/catalog-realm/fields/image/components/image-avatar-preview.gts @@ -3,6 +3,7 @@ import { on } from '@ember/modifier'; import { htmlSafe } from '@ember/template'; import CameraIcon from '@cardstack/boxel-icons/camera'; import { Button } from '@cardstack/boxel-ui/components'; +import { or } from '@cardstack/boxel-ui/helpers'; import ReadingProgress from './reading-progress'; interface ImageAvatarPreviewArgs { @@ -14,6 +15,7 @@ interface ImageAvatarPreviewArgs { hasPendingUpload?: boolean; isReading?: boolean; readProgress?: number; + disabled?: boolean; }; } @@ -29,7 +31,10 @@ export default class ImageAvatarPreview extends GlimmerComponent
{{! Avatar container }} -