Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remember playground format via local storage #2155

Merged
merged 4 commits into from
Feb 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { action } from '@ember/object';
import type Owner from '@ember/owner';
import { service } from '@ember/service';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import Folder from '@cardstack/boxel-icons/folder';
import { restartableTask, task } from 'ember-concurrency';
Expand Down Expand Up @@ -39,6 +38,8 @@ import type RealmService from '@cardstack/host/services/realm';
import type RealmServerService from '@cardstack/host/services/realm-server';
import type RecentFilesService from '@cardstack/host/services/recent-files-service';

import { PlaygroundSelections } from '@cardstack/host/utils/local-storage-keys';

import type { CardDef, Format } from 'https://cardstack.com/base/card-api';

import PrerenderedCardSearch, {
Expand Down Expand Up @@ -248,11 +249,13 @@ class PlaygroundPanelContent extends Component<PlaygroundContentSignature> {
{{/if}}
{{/if}}
</div>
<FormatChooser
class='format-chooser'
@format={{this.format}}
@setFormat={{this.setFormat}}
/>
{{#if this.card}}
<FormatChooser
class='format-chooser'
@format={{this.format}}
@setFormat={{this.setFormat}}
/>
{{/if}}
</div>
<style scoped>
.playground-panel-content {
Expand Down Expand Up @@ -355,12 +358,14 @@ class PlaygroundPanelContent extends Component<PlaygroundContentSignature> {
@service private declare realm: RealmService;
@service private declare realmServer: RealmServerService;
@service declare recentFilesService: RecentFilesService;
@tracked private format: Format = 'isolated';
private playgroundSelections: Record<string, string>;
private playgroundSelections: Record<
string, // moduleId
{ cardId: string; format: Format }
>; // TrackedObject

constructor(owner: Owner, args: PlaygroundContentSignature['Args']) {
super(owner, args);
let selections = window.localStorage.getItem('playground-selections');
let selections = window.localStorage.getItem(PlaygroundSelections);

this.playgroundSelections = new TrackedObject(
selections?.length ? JSON.parse(selections) : {},
Expand Down Expand Up @@ -403,22 +408,27 @@ class PlaygroundPanelContent extends Component<PlaygroundContentSignature> {
};
}

private cardResource = getCard(this, () =>
this.playgroundSelections[this.args.moduleId]?.replace(/\.json$/, ''),
private cardResource = getCard(
this,
() => this.playgroundSelections[this.args.moduleId]?.cardId,
);

private get card(): CardDef | undefined {
return this.cardResource.card;
}

private get format(): Format {
return this.playgroundSelections[this.args.moduleId]?.format ?? 'isolated';
}

private copyToClipboard = task(async (id: string) => {
await navigator.clipboard.writeText(id);
});

private openInInteractMode = task(async (id: string, format: Format) => {
private openInInteractMode = task(async (id: string) => {
await this.operatorModeStateService.openCardInInteractMode(
new URL(id),
format,
this.format === 'edit' ? 'edit' : 'isolated',
);
});

Expand All @@ -438,28 +448,31 @@ class PlaygroundPanelContent extends Component<PlaygroundContentSignature> {
icon: IconCode,
}),
new MenuItem('Open in Interact Mode', 'action', {
action: () => this.openInInteractMode.perform(cardId, this.format),
action: () => this.openInInteractMode.perform(cardId),
icon: Eye,
}),
];
return menuItems;
}

private persistSelections = (cardId: string) => {
this.playgroundSelections[this.args.moduleId] = cardId;
private persistSelections = (cardId: string, format = this.format) => {
this.playgroundSelections[this.args.moduleId] = { cardId, format };
window.localStorage.setItem(
'playground-selections',
PlaygroundSelections,
JSON.stringify(this.playgroundSelections),
);
};

@action private onSelect(card: PrerenderedCard) {
this.persistSelections(card.url);
this.persistSelections(card.url.replace(/\.json$/, ''));
}

@action
private setFormat(format: Format) {
this.format = format;
if (!this.card?.id) {
return;
}
this.persistSelections(this.card.id, format);
}

private chooseCard = restartableTask(async () => {
Expand Down
1 change: 1 addition & 0 deletions packages/host/app/utils/local-storage-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export const CodeModePanelWidths = 'code-mode-panel-widths';
export const CodeModePanelHeights = 'code-mode-panel-heights';
export const CodeModePanelSelections = 'code-mode-panel-selections';
export const SessionLocalStorageKey = 'boxel-session';
export const PlaygroundSelections = 'playground-selections';
132 changes: 126 additions & 6 deletions packages/host/tests/acceptance/code-submode/playground-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import { module, test } from 'qunit';

import type { Realm } from '@cardstack/runtime-common';

import { PlaygroundSelections } from '@cardstack/host/utils/local-storage-keys';

import type { Format } from 'https://cardstack.com/base/card-api';

import {
percySnapshot,
setupAcceptanceTestRealm,
Expand Down Expand Up @@ -230,7 +234,7 @@ export class BlogPost extends CardDef {
[testRealmURL, 'Author/jane-doe.json'],
]),
);
window.localStorage.setItem('playground-selections', '');
window.localStorage.setItem(PlaygroundSelections, '');
});

test('can render playground panel when a card def is selected', async function (assert) {
Expand Down Expand Up @@ -262,7 +266,7 @@ export class BlogPost extends CardDef {

test('can populate instance chooser dropdown options from recent files', async function (assert) {
window.localStorage.setItem('recent-files', '');
window.localStorage.setItem('playground-selections', '');
window.localStorage.setItem(PlaygroundSelections, '');

await visitOperatorMode({
submode: 'code',
Expand Down Expand Up @@ -339,12 +343,18 @@ export class BlogPost extends CardDef {

test('can populate playground preview with previous choices saved in local storage', async function (assert) {
let selections = {
[`${testRealmURL}author/Author`]: `${testRealmURL}Author/jane-doe.json`,
[`${testRealmURL}blog-post/BlogPost`]: `${testRealmURL}BlogPost/remote-work.json`,
[`${testRealmURL}blog-post/Category`]: `${testRealmURL}Category/city-design.json`,
[`${testRealmURL}author/Author`]: {
cardId: `${testRealmURL}Author/jane-doe`,
},
[`${testRealmURL}blog-post/BlogPost`]: {
cardId: `${testRealmURL}BlogPost/remote-work`,
},
[`${testRealmURL}blog-post/Category`]: {
cardId: `${testRealmURL}Category/city-design`,
},
};
window.localStorage.setItem(
'playground-selections',
PlaygroundSelections,
JSON.stringify(selections),
);
const assertCardExists = (fileName: string) => {
Expand Down Expand Up @@ -719,4 +729,114 @@ export class BlogPost extends CardDef {
});
assert.dom('[data-test-post-title]').includesText('Hello Mad As a Hatter');
});

test('can remember format choice via local storage', async function (assert) {
const authorModuleId = `${testRealmURL}author/Author`;
const categoryModuleId = `${testRealmURL}blog-post/Category`;
const blogPostModuleId = `${testRealmURL}blog-post/BlogPost`;
const authorId = `${testRealmURL}Author/jane-doe`;
const categoryId1 = `${testRealmURL}Category/city-design`;
const categoryId2 = `${testRealmURL}Category/future-tech`;
const blogPostId1 = `${testRealmURL}BlogPost/mad-hatter`;
const blogPostId2 = `${testRealmURL}BlogPost/remote-work`;

window.localStorage.setItem(
PlaygroundSelections,
JSON.stringify({
[`${authorModuleId}`]: {
cardId: authorId,
format: 'edit',
},
[`${categoryModuleId}`]: {
cardId: categoryId1,
format: 'embedded',
},
[`${blogPostModuleId}`]: {
cardId: blogPostId1,
},
}),
);
const getSelection = (moduleId: string) => {
let selections = window.localStorage.getItem(PlaygroundSelections);
if (!selections) {
throw new Error('No selections found in mock local storage');
}
return JSON.parse(selections)[moduleId];
};
const assertCorrectFormat = (
cardId: string,
format: Format,
message?: string,
) => {
const dataAttr = `[data-test-playground-panel] [data-test-card="${cardId}"][data-test-card-format="${format}"]`;
assert.dom(dataAttr).exists(message);
};
await visitOperatorMode({
stacks: [],
submode: 'code',
codePath: `${testRealmURL}author.gts`,
});
assertCorrectFormat(authorId, 'edit');
await click('[data-test-format-chooser-atom]'); // change selected format
assertCorrectFormat(authorId, 'atom');
assert.deepEqual(
getSelection(authorModuleId),
{
cardId: authorId,
format: 'atom',
},
'local storage is updated',
);

await click('[data-test-file-browser-toggle]');
await click('[data-test-file="blog-post.gts"]'); // change open file
assertCorrectFormat(categoryId1, 'embedded');

await click('[data-test-instance-chooser]');
await click('[data-option-index="1"]'); // change selected instance
assertCorrectFormat(categoryId2, 'embedded');
assert.deepEqual(
getSelection(categoryModuleId),
{
cardId: categoryId2,
format: 'embedded',
},
'local storage is updated',
);

await click('[data-test-inspector-toggle]');
await click('[data-test-boxel-selector-item-text="BlogPost"]'); // change selected module
assertCorrectFormat(blogPostId1, 'isolated', 'default format is correct');
await click('[data-test-format-chooser-fitted]'); // change selected format
assert.deepEqual(getSelection(blogPostModuleId), {
cardId: blogPostId1,
format: 'fitted',
});
await click('[data-test-instance-chooser]');
await click('[data-option-index="1"]'); // change selected instance
assertCorrectFormat(blogPostId2, 'fitted');
assert.deepEqual(getSelection(blogPostModuleId), {
cardId: blogPostId2,
format: 'fitted',
});

let selections = window.localStorage.getItem(PlaygroundSelections);
assert.strictEqual(
selections,
JSON.stringify({
[`${authorModuleId}`]: {
cardId: authorId,
format: 'atom',
},
[`${categoryModuleId}`]: {
cardId: categoryId2,
format: 'embedded',
},
[`${blogPostModuleId}`]: {
cardId: blogPostId2,
format: 'fitted',
},
}),
);
});
});