diff --git a/packages/host/app/components/operator-mode/create-file-modal.gts b/packages/host/app/components/operator-mode/create-file-modal.gts index 0c9887a5d8..7ca6ab85d4 100644 --- a/packages/host/app/components/operator-mode/create-file-modal.gts +++ b/packages/host/app/components/operator-mode/create-file-modal.gts @@ -32,6 +32,7 @@ import { } from '@cardstack/boxel-ui/icons'; import { + cardIdToURL, specRef, chooseCard, baseRealm, @@ -826,7 +827,16 @@ export default class CreateFileModal extends Component { ref: { name: exportName, module }, } = (this.definitionClass ?? spec)!; // we just checked above to make sure one of these exists let className = convertToClassName(this.displayName); - let absoluteModule = new URL(module, spec?.id); + const absoluteModuleHref = ( + codeRefWithAbsoluteURL( + { + module: spec?.moduleHref ?? module, + name: exportName, + }, + new URL(this.selectedRealmURL), + ) as ResolvedCodeRef + ).module; + const absoluteModule = new URL(absoluteModuleHref); let moduleURL = maybeRelativeURL( absoluteModule, url, @@ -929,9 +939,7 @@ export class ${className} extends ${exportName} { let { ref } = (this.definitionClass ? this.definitionClass : spec)!; // we just checked above to make sure one of these exist - let relativeTo = spec - ? new URL(spec.id!) // only new cards are missing urls - : undefined; + let relativeTo = spec?.id ? cardIdToURL(spec.id) : undefined; // we make the code ref use an absolute URL for safety in // the case it's being created in a different realm than where the card // definition comes from. The server will make relative URL if appropriate after creation diff --git a/packages/host/tests/acceptance/code-submode/create-file-test.gts b/packages/host/tests/acceptance/code-submode/create-file-test.gts index 295e34792d..3bff0c60e3 100644 --- a/packages/host/tests/acceptance/code-submode/create-file-test.gts +++ b/packages/host/tests/acceptance/code-submode/create-file-test.gts @@ -9,7 +9,12 @@ import { import { getService } from '@universal-ember/test-support'; import { module, test } from 'qunit'; -import { baseRealm, Deferred } from '@cardstack/runtime-common'; +import { + baseRealm, + Deferred, + registerCardReferencePrefix, + unregisterCardReferencePrefix, +} from '@cardstack/runtime-common'; import type FileUploadService from '@cardstack/host/services/file-upload'; @@ -37,6 +42,8 @@ import type { TestRealmAdapter } from '../../helpers/adapter'; const testRealmURL2 = 'http://test-realm/test2/'; const testRealmAIconURL = 'https://i.postimg.cc/L8yXRvws/icon.png'; +const testPrefixRealmURL2 = `@test-realm/test2/`; + const files: Record = { '.realm.json': { name: 'Test Workspace A', @@ -183,6 +190,32 @@ const filesB: Record = { }, }, }, + 'animal.gts': ` + import { contains, field, CardDef } from "https://cardstack.com/base/card-api"; + import StringField from "https://cardstack.com/base/string"; + + export class Animal extends CardDef { + static displayName = 'Animal'; + @field name = contains(StringField); + } + `, + 'spec/animal.json': { + data: { + type: 'card', + attributes: { + cardTitle: 'Animal', + cardDescription: 'Spec for Animal', + specType: 'card', + ref: { module: '@test-realm/test2/animal', name: 'Animal' }, + }, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/spec', + name: 'Spec', + }, + }, + }, + }, }; module('Acceptance | code submode | create-file tests', function (hooks) { @@ -1234,6 +1267,66 @@ export class TestCard extends CardDef { }); }); + module('when a selected spec uses a prefix-form ref', function (hooks) { + hooks.before(function () { + registerCardReferencePrefix(testPrefixRealmURL2, testRealmURL2); + }); + + hooks.after(function () { + unregisterCardReferencePrefix(testPrefixRealmURL2); + }); + + test('can create new card definition in workspace A that extends a card from workspace B via prefix-form ref', async function (assert) { + assert.expect(2); + await visitOperatorMode(`${baseRealm.url}card-api.gts`); + await openNewFileModal('Card Definition'); + await click('[data-test-select-card-type]'); + await waitFor('[data-test-card-catalog-modal]'); + await waitFor( + `[data-test-card-catalog-item="${testRealmURL2}spec/animal"]`, + ); + await click( + `[data-test-card-catalog-item="${testRealmURL2}spec/animal"]`, + ); + await click('[data-test-card-catalog-go-button]'); + await waitFor(`[data-test-selected-type="Animal"]`); + + await fillIn('[data-test-display-name-field]', 'Test Card'); + await fillIn('[data-test-file-name-field]', 'test-card'); + + let deferred = new Deferred(); + this.onSave((url, content) => { + if (typeof content !== 'string') { + throw new Error(`expected string save data`); + } + assert.strictEqual( + content, + ` +import { Animal } from '${testRealmURL2}animal'; +import { Component } from 'https://cardstack.com/base/card-api'; +export class TestCard extends Animal { + static displayName = "Test Card"; +}`.trim(), + 'The source uses the resolved absolute module URL', + ); + assert.strictEqual( + url.href, + `${testRealmURL}test-card.gts`, + [ + 'Saved file URL should point to Test Workspace A', + `Expected: ${testRealmURL}test-card.gts`, + `Actual: ${url.href}`, + ].join('\n'), + ); + deferred.fulfill(); + }); + + await click('[data-test-create-definition]'); + await waitFor('[data-test-create-file-modal]', { count: 0 }); + await deferred.promise; + }); + }); + module( 'when the user lacks write permissions in remote realm', function (hooks) {