Skip to content

Commit

Permalink
Add "copy from seed realm" checkbox (#2077)
Browse files Browse the repository at this point in the history
  • Loading branch information
FadhlanR authored Jan 23, 2025
1 parent 163fb69 commit af16d7f
Show file tree
Hide file tree
Showing 10 changed files with 210 additions and 3 deletions.
5 changes: 4 additions & 1 deletion packages/boxel-ui/addon/src/components/input/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cn from '../../helpers/cn.ts';
import element from '../../helpers/element.ts';
import optional from '../../helpers/optional.ts';
import pick from '../../helpers/pick.ts';
import { and, eq, not } from '../../helpers/truth-helpers.ts';
import { and, bool, eq, not } from '../../helpers/truth-helpers.ts';
import FailureBordered from '../../icons/failure-bordered.gts';
import IconSearch from '../../icons/icon-search.gts';
import LoadingIndicator from '../../icons/loading-indicator.gts';
Expand Down Expand Up @@ -62,6 +62,7 @@ export interface Signature {
id?: string;
max?: string | number;
onBlur?: (ev: Event) => void;
onChange?: (ev: Event) => void;
onFocus?: (ev: Event) => void;
onInput?: (val: string) => void;
onKeyPress?: (ev: KeyboardEvent) => void;
Expand Down Expand Up @@ -155,6 +156,7 @@ export default class BoxelInput extends Component<Signature> {
id={{this.id}}
type={{this.type}}
value={{@value}}
checked={{if (and (eq @type 'checkbox') (bool @value)) @value}}
placeholder={{@placeholder}}
max={{@max}}
required={{@required}}
Expand All @@ -177,6 +179,7 @@ export default class BoxelInput extends Component<Signature> {
{{on 'blur' (optional @onBlur)}}
{{on 'keypress' (optional @onKeyPress)}}
{{on 'focus' (optional @onFocus)}}
{{on 'change' (optional @onChange)}}
...attributes
/>
{{#if this.isSearch}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export default class AddWorkspace extends Component<Signature> {
@service private declare matrixService: MatrixService;
@tracked private isModalOpen = false;
@tracked private endpoint = '';
@tracked private copyFromSeedRealm = true;
@tracked private displayName = '';
@tracked private hasUserEditedEndpoint = false;
@tracked private error: string | null = null;
Expand All @@ -54,6 +55,9 @@ export default class AddWorkspace extends Component<Signature> {
this.endpoint = cleanseString(value);
}
};
private toggleCopyFromSeedRealm = () => {
this.copyFromSeedRealm = !this.copyFromSeedRealm;
};
private closeModal = () => {
this.isModalOpen = false;
};
Expand All @@ -65,6 +69,7 @@ export default class AddWorkspace extends Component<Signature> {
name: this.displayName,
iconURL: iconURLFor(this.displayName),
backgroundURL: getRandomBackgroundURL(),
copyFromSeedRealm: this.copyFromSeedRealm,
});
this.closeModal();
} catch (e: any) {
Expand Down Expand Up @@ -135,6 +140,20 @@ export default class AddWorkspace extends Component<Signature> {
@helperText='The endpoint is the unique identifier for your workspace. Use letters, numbers, and hyphens only.'
/>
</FieldContainer>
<FieldContainer @label='Copy from Seed' @tag='label' class='field'>
<label class='copy-from-seed-label'>
<BoxelInput
data-test-copy-from-seed-field
class='copy-from-seed-checkbox'
placeholder='Workspace Endpoint'
@type='checkbox'
@value={{this.copyFromSeedRealm}}
@onChange={{this.toggleCopyFromSeedRealm}}
/>
<span>Populate new workspace with sample cards (uncheck for a
blank workspace).</span>
</label>
</FieldContainer>
{{/if}}
{{/if}}
{{#if this.error}}
Expand Down Expand Up @@ -232,6 +251,28 @@ export default class AddWorkspace extends Component<Signature> {
.spinner {
--boxel-loading-indicator-size: 2.5rem;
}
.copy-from-seed-label {
display: flex;
gap: calc(var(--boxel-sp-sm) + 1px);
padding-top: var(--boxel-sp-sm);
}
.copy-from-seed-label span {
color: var(--boxel-label-color);
font: var(--boxel-font-sm);
letter-spacing: var(--boxel-lsp-xs);
}
.copy-from-seed-label :deep(.input-container) {
grid-template-columns: 1fr;
width: auto;
height: 100%;
}
.copy-from-seed-checkbox {
--boxel-input-height: 17px;
grid-area: pre-icon;
justify-self: start;
margin: 0;
width: auto;
}
</style>
</template>
}
3 changes: 3 additions & 0 deletions packages/host/app/services/matrix-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,17 +337,20 @@ export default class MatrixService extends Service {
name,
iconURL,
backgroundURL,
copyFromSeedRealm,
}: {
endpoint: string;
name: string;
iconURL?: string;
backgroundURL?: string;
copyFromSeedRealm?: boolean;
}) {
let personalRealmURL = await this.realmServer.createRealm({
endpoint,
name,
iconURL,
backgroundURL,
copyFromSeedRealm,
});
let { realms = [] } =
(await this.client.getAccountDataFromServer<{ realms: string[] }>(
Expand Down
1 change: 1 addition & 0 deletions packages/host/app/services/realm-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export default class RealmServerService extends Service {
name: string;
iconURL?: string;
backgroundURL?: string;
copyFromSeedRealm?: boolean;
}) {
await this.login();

Expand Down
4 changes: 4 additions & 0 deletions packages/matrix/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,14 @@ export async function createRealm(
page: Page,
endpoint: string,
name = endpoint,
copyFromSeed = true,
) {
await page.locator('[data-test-add-workspace]').click();
await page.locator('[data-test-display-name-field]').fill(name);
await page.locator('[data-test-endpoint-field]').fill(endpoint);
if (!copyFromSeed) {
await page.locator('[data-test-copy-from-seed-field]').click();
}
await page.locator('[data-test-create-workspace-submit]').click();
await expect(page.locator(`[data-test-workspace="${name}"]`)).toBeVisible();
await expect(page.locator('[data-test-create-workspace-modal]')).toHaveCount(
Expand Down
39 changes: 39 additions & 0 deletions packages/matrix/tests/create-realm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,45 @@ test.describe('Create Realm via Dashboard', () => {
await expect(
page.locator(`[data-test-stack-card="${newRealmURL}index"]`),
).toBeVisible();

const filterListElements = page.locator('[data-test-boxel-filter-list-button]');
const count = await filterListElements.count();
expect(count).toBeGreaterThan(1);

await page.locator(`[data-test-workspace-chooser-toggle]`).click();
await expect(
page.locator(
`[data-test-workspace="1New Workspace"] [data-test-realm-icon-url]`,
),
'the "N" icon URL is shown',
).toHaveAttribute(
'style',
'background-image: url("https://boxel-images.boxel.ai/icons/Letter-n.png");',
);
});

test('it can create a new realm without copying from seed realm', async ({
page,
}) => {
let serverIndexUrl = new URL(appURL).origin;
await clearLocalStorage(page, serverIndexUrl);

await setupUserSubscribed('@user1:localhost', realmServer);

await login(page, 'user1', 'pass', {
url: serverIndexUrl,
skipOpeningAssistant: true,
});

await createRealm(page, 'new-workspace', '1New Workspace', false);
await page.locator('[data-test-workspace="1New Workspace"]').click();
let newRealmURL = new URL('user1/new-workspace/', serverIndexUrl).href;
await expect(
page.locator(`[data-test-stack-card="${newRealmURL}index"]`),
).toBeVisible();
await expect(
page.locator(`[data-test-boxel-filter-list-button]`),
).toHaveCount(1);

await page.locator(`[data-test-workspace-chooser-toggle]`).click();
await expect(
Expand Down
1 change: 1 addition & 0 deletions packages/realm-server/handlers/handle-create-realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface RealmCreationJSON {
name: string;
backgroundURL?: string;
iconURL?: string;
copyFromSeedRealm?: boolean;
};
};
}
Expand Down
2 changes: 2 additions & 0 deletions packages/realm-server/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export type CreateRoutesArgs = {
name,
backgroundURL,
iconURL,
copyFromSeedRealm,
}: {
ownerUserId: string;
endpoint: string;
name: string;
backgroundURL?: string;
iconURL?: string;
copyFromSeedRealm?: boolean;
}) => Promise<Realm>;
serveIndex: (ctxt: Koa.Context, next: Koa.Next) => Promise<any>;
serveFromRealm: (ctxt: Koa.Context, next: Koa.Next) => Promise<any>;
Expand Down
23 changes: 21 additions & 2 deletions packages/realm-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,12 +274,14 @@ export class RealmServer {
name,
backgroundURL,
iconURL,
copyFromSeedRealm = true,
}: {
ownerUserId: string; // note matrix userIDs look like "@mango:boxel.ai"
endpoint: string;
name: string;
backgroundURL?: string;
iconURL?: string;
copyFromSeedRealm?: boolean;
}): Promise<Realm> => {
if (
this.realms.find(
Expand Down Expand Up @@ -339,16 +341,29 @@ export class RealmServer {
...(iconURL ? { iconURL } : {}),
...(backgroundURL ? { backgroundURL } : {}),
});
if (this.seedPath) {
if (this.seedPath && copyFromSeedRealm) {
let ignoreList = IGNORE_SEED_FILES.map((file) =>
join(this.seedPath!.replace(/\/$/, ''), file),
);

copySync(this.seedPath, realmPath, {
filter: (src, _dest) => {
return !ignoreList.includes(src);
},
});
this.log.debug(`seed files for new realm ${url} copied to ${realmPath}`);
} else {
writeJSONSync(join(realmPath, 'index.json'), {
data: {
type: 'card',
meta: {
adoptsFrom: {
module: 'https://cardstack.com/base/cards-grid',
name: 'CardsGrid',
},
},
},
});
}

let realm = new Realm(
Expand All @@ -365,7 +380,11 @@ export class RealmServer {
},
},
{
...(this.seedRealmURL ? { copiedFromRealm: this.seedRealmURL } : {}),
...(this.seedRealmURL && copyFromSeedRealm
? {
copiedFromRealm: this.seedRealmURL,
}
: {}),
},
);
this.realms.push(realm);
Expand Down
94 changes: 94 additions & 0 deletions packages/realm-server/tests/server-endpoints-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,100 @@ module(basename(__filename), function () {
}
});

test('POST /_create-realm without copying seed realm', async function (assert) {
// we randomize the realm and owner names so that we can isolate matrix
// test state--there is no "delete user" matrix API
let endpoint = `test-realm-${uuidv4()}`;
let owner = 'mango';
let ownerUserId = '@mango:boxel.ai';
let response = await request2
.post('/_create-realm')
.set('Accept', 'application/vnd.api+json')
.set('Content-Type', 'application/json')
.set(
'Authorization',
`Bearer ${createRealmServerJWT(
{ user: ownerUserId, sessionRoom: 'session-room-test' },
secretSeed,
)}`,
)
.send(
JSON.stringify({
data: {
type: 'realm',
attributes: {
...testRealmInfo,
endpoint,
backgroundURL: 'http://example.com/background.jpg',
iconURL: 'http://example.com/icon.jpg',
copyFromSeedRealm: false,
},
},
}),
);

assert.strictEqual(response.status, 201, 'HTTP 201 status');
let json = response.body;
assert.deepEqual(
json,
{
data: {
type: 'realm',
id: `${testRealm2URL.origin}/${owner}/${endpoint}/`,
attributes: {
...testRealmInfo,
endpoint,
backgroundURL: 'http://example.com/background.jpg',
iconURL: 'http://example.com/icon.jpg',
copyFromSeedRealm: false,
},
},
},
'realm creation JSON is correct',
);

let realmPath = join(dir.name, 'realm_server_2', owner, endpoint);
let realmJSON = readJSONSync(join(realmPath, '.realm.json'));
assert.deepEqual(
realmJSON,
{
name: 'Test Realm',
backgroundURL: 'http://example.com/background.jpg',
iconURL: 'http://example.com/icon.jpg',
},
'.realm.json is correct',
);
assert.ok(
existsSync(join(realmPath, 'index.json')),
'seed file index.json exists',
);
assert.notOk(
existsSync(
join(
realmPath,
'HelloWorld/47c0fc54-5099-4e9c-ad0d-8a58572d05c0.json',
),
),
'seed file HelloWorld/47c0fc54-5099-4e9c-ad0d-8a58572d05c0.json exists',
);
assert.notOk(
existsSync(join(realmPath, 'package.json')),
'ignored seed file package.json does not exist',
);
assert.notOk(
existsSync(join(realmPath, 'node_modules')),
'ignored seed file node_modules/ does not exist',
);
assert.notOk(
existsSync(join(realmPath, '.gitignore')),
'ignored seed file .gitignore does not exist',
);
assert.notOk(
existsSync(join(realmPath, 'tsconfig.json')),
'ignored seed file tsconfig.json does not exist',
);
});

test('dynamically created realms are not publicly readable or writable', async function (assert) {
let endpoint = `test-realm-${uuidv4()}`;
let owner = 'mango';
Expand Down

0 comments on commit af16d7f

Please sign in to comment.