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

Adds project save/load functionality to :play #37

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@
"gzip": "3.5 KB"
},
"dist/play-pen.js": {
"none": "31900 KB",
"gzip": "5150 KB"
"none": "32100 KB",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a pretty big bump. does this PR really add 200 KB?

"gzip": "5190 KB"
}
},
"typesVersions": {
Expand Down
1 change: 1 addition & 0 deletions src/elements/play-icon/icons/upload-outline.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion src/elements/play-icon/play-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import resizeHorizontalOutline from './icons/resize-horizontal-outline.svg'
import restartOutline from './icons/restart-outline.svg'
import shareNewOutline from './icons/share-new-outline.svg'
import unmountOutline from './icons/unmount-outline.svg'
import uploadOutline from './icons/upload-outline.svg'

export type PlayIconSVG = keyof typeof icons

Expand Down Expand Up @@ -62,7 +63,8 @@ const icons = {
'resize-horizontal-outline': resizeHorizontalOutline,
'restart-outline': restartOutline,
'share-ios-outline': unmountOutline,
'share-new-outline': shareNewOutline
'share-new-outline': shareNewOutline,
'upload-outline': uploadOutline
}

declare global {
Expand Down
7 changes: 5 additions & 2 deletions src/elements/play-new-pen-button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ export class PlayNewPenButton extends LitElement {
<div class="container">
<button
class="new-pen"
@click=${() =>
@click=${() => {
this.dispatchEvent(Bubble<string>('new-project', ''))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you mind updating the HTMLElementEventMap at the top of the file? same thing for other events in the patch.

this.dispatchEvent(
Bubble<string>('edit-src', this.srcByLabel?.Default || '')
)}
)
}}
title="New pen"
>
<play-icon
Expand All @@ -180,6 +182,7 @@ export class PlayNewPenButton extends LitElement {
<play-list-item
label=${label}
@click=${() => {
this.dispatchEvent(Bubble<string>('new-project', label))
this.dispatchEvent(Bubble<string>('edit-src', src))
}}
>${label}</play-list-item
Expand Down
85 changes: 72 additions & 13 deletions src/elements/play-pen-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@ import {
type TemplateResult
} from 'lit'
import {customElement, property, query} from 'lit/decorators.js'
import {ifDefined} from 'lit/directives/if-defined.js'
import {ProjectManager} from '../storage/project-manager.js'
import {defaultSettings} from '../storage/settings-save.js'
import {Bubble} from '../utils/bubble.js'
import {cssReset} from '../utils/css-reset.js'
import {openURL} from '../utils/open-url.js'
import {type AssetsState, emptyAssetsState} from './play-assets/play-assets.js'
import type {PlayAssetsDialog} from './play-assets-dialog.js'
import type {PlayExportDialog} from './play-export-dialog.js'
import type {PlayProjectSaveDialog} from './play-project-save-dialog.js'
import type {PlaySettingsDialog} from './play-settings-dialog.js'
import type {PlayAssetsDialog} from './play-assets-dialog.js'
import type {PlayToast} from './play-toast.js'
import type {PlayProjectLoadDialog} from './play-project-load-dialog.js'

import './play-assets-dialog.js'
import './play-button.js'
import './play-export-dialog.js'
import './play-icon/play-icon.js'
import './play-logo/play-logo.js'
import './play-new-pen-button.js'
import './play-project-button.js'
import './play-project-load-dialog.js'
import './play-project-save-dialog.js'
import './play-resizable-text-input.js'
import './play-settings-dialog.js'
import './play-assets-dialog.js'
import {type AssetsState, emptyAssetsState} from './play-assets/play-assets.js'

declare global {
interface HTMLElementEventMap {
Expand Down Expand Up @@ -97,6 +105,9 @@ export class PlayPenHeader extends LitElement {
@property({attribute: 'sandbox-app', type: Boolean})
sandboxApp: boolean = false

@property()
src: string = ''

@property()
url: string = ''

Expand All @@ -121,8 +132,49 @@ export class PlayPenHeader extends LitElement {
@query('play-export-dialog')
private _export!: PlayExportDialog

@query('play-project-save-dialog')
private _saveDialog!: PlayProjectSaveDialog

@query('play-project-load-dialog')
private _loadDialog!: PlayProjectLoadDialog

@query('play-settings-dialog')
private _settings!: PlaySettingsDialog
private _settingsDialog!: PlaySettingsDialog

@query('play-toast')
private _toast!: PlayToast

@query('.toast-content')
private _toastContent!: HTMLDivElement

@property({attribute: 'project-manager', type: ProjectManager})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this type works? suggest attribute: false.

projectManager!: ProjectManager
Comment on lines +150 to +151
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you mind moving this near the other publics properties at the top? also, can we make it optional? here's the thinking: it's ok to assert what we do inside the class but safer not to assert what's done outside.


private openSaveDialog(): void {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the current repo convention is to use ES private fields like #openSaveDialog() where possible. it's not possible on decorators.

let projectName = this.name
if (this.projectManager.getCurrentProject() === undefined) {
this._saveDialog.open(this.name)
return
}
this.saveProject(projectName)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we await this?

}

private async saveProject(projectName: string): Promise<void> {
this._saveDialog.close()
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you mind shrinking the try block to just what is expected to throw?

await this.projectManager.saveProject(projectName, this.src)
this._toastContent.textContent = 'Project saved'
this._toast.open()
this.dispatchEvent(Bubble<string>('edit-name', projectName))
} catch (e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔕 suggest err to distinguish from events, a common variable.

this._toastContent.textContent = `Error: ${e}`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will likely print Error twice. you can check with:

$ node
> `Error: ${Error('banana')}`
'Error: Error: banana'

this._toast.open()
}
}

private loadProject(): void {
this._loadDialog.open()
}

protected override render(): TemplateResult {
return html`
Expand All @@ -140,6 +192,7 @@ export class PlayPenHeader extends LitElement {
<play-new-pen-button
size="small"
.srcByLabel=${this.srcByLabel}
@new-project=${() => this.projectManager.clearCurrentProject()}
></play-new-pen-button
><play-button
appearance="bordered"
Expand All @@ -149,14 +202,14 @@ export class PlayPenHeader extends LitElement {
label="Docs"
@click=${() => openURL('https://developers.reddit.com/docs')}
></play-button
><play-button
appearance="bordered"
><play-project-button
size="small"
icon="download-outline"
title="Export Pen"
label="Export"
@click=${() => this._export.open()}
></play-button
.srcByLabel=${this.srcByLabel}
@open-export-dialog=${() => this._export.open()}
@save-project=${() => this.openSaveDialog()}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔕 fat arrow is optional here due to automatic this binding.

@load-project=${() => this.loadProject()}
></play-project-button
></play-button
><play-button
appearance="bordered"
size="small"
Expand All @@ -170,7 +223,7 @@ export class PlayPenHeader extends LitElement {
size="small"
title=":play Settings"
label="Settings"
@click=${() => this._settings.open()}
@click=${() => this._settingsDialog.open()}
></play-button
><play-button
appearance="orangered"
Expand All @@ -180,14 +233,19 @@ export class PlayPenHeader extends LitElement {
label="Share"
@click=${() =>
this.dispatchEvent(Bubble<undefined>('share', undefined))}
></play-button>
></play-button
><slot name="account-button"></slot>
</div>
</header>
<play-assets-dialog
.assetsState=${this.assetsState}
?enable-local-assets=${this.enableLocalAssets}
></play-assets-dialog>
<play-export-dialog url=${this.url}></play-export-dialog>
<play-project-save-dialog
src=${ifDefined(this.src)}
@save-dialog-submit=${(ev: CustomEvent<string>) => this.saveProject(ev.detail)}></play-project-save-dialog>
<play-project-load-dialog .projectManager=${this.projectManager}></play-project-load-dialog>
<play-settings-dialog
?allow-storage=${this.allowStorage}
remote-runtime-origin=${this.remoteRuntimeOrigin}
Expand All @@ -200,6 +258,7 @@ export class PlayPenHeader extends LitElement {
?use-remote-runtime=${this.useRemoteRuntime}
?enable-local-assets=${this.enableLocalAssets}
></play-settings-dialog>
<play-toast><div class="toast-content"></div></play-toast>
`
}
}
20 changes: 17 additions & 3 deletions src/elements/play-pen/play-pen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ import '../play-pen-header.js'
import '../play-preview-controls.js'
import '../play-preview.js'
import '../play-toast.js'
import type {ProjectStorageClient} from '../../storage/project-storage-client.js'
import {LocalProjectStorageClient} from '../../storage/local-project-storage-client.js'
import {ProjectManager} from '../../storage/project-manager.js'

declare global {
interface HTMLElementTagNameMap {
Expand Down Expand Up @@ -147,6 +150,9 @@ export class PlayPen extends LitElement {
SVG: svg
}

@property()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this need to be a property?

projectStorageClient: ProjectStorageClient = new LocalProjectStorageClient()

/** Program executable. */
@state() private _assetsFilesystemType: AssetsFilesystemType = 'virtual'
@state() private _assetsState: AssetsState = emptyAssetsState()
Expand All @@ -169,6 +175,7 @@ export class PlayPen extends LitElement {
@query('play-editor') private _editor!: PlayEditor
@query('play-toast') private _toast!: PlayToast
#bundleStore?: BundleStore | undefined
_projectManager?: ProjectManager | undefined
readonly #env: VirtualTypeScriptEnvironment = newTSEnv()
@state() _uploaded: Promise<Empty> = Promise.resolve({})
/** Try to ensure the bundle hostname is unique. See compute-util. */
Expand Down Expand Up @@ -206,6 +213,10 @@ export class PlayPen extends LitElement {
// bundle is loaded.
}

if (!this._projectManager) {
this._projectManager = new ProjectManager(this.projectStorageClient)
}

let pen
if (this.allowURL) pen = loadPen(location)
if (this.allowStorage) pen ??= loadPen(localStorage)
Expand Down Expand Up @@ -235,9 +246,11 @@ export class PlayPen extends LitElement {
><play-pen-header
name=${this._name}
remote-runtime-origin=${this._remoteRuntimeOrigin}
src=${ifDefined(this._src)}
url=${this.#shareURL().toString()}
.assetsState=${this._assetsState}
.srcByLabel=${this.srcByLabel}
.projectManager=${this._projectManager}
?allow-storage=${this.allowStorage}
?runtime-debug-logging=${this._runtimeDebugLogging}
?sandbox-app=${this._sandboxApp}
Expand Down Expand Up @@ -285,6 +298,7 @@ export class PlayPen extends LitElement {
ev: CustomEvent<AssetsVirtualFileChange>
) => this._assets.onVirtualFileChange(ev.detail)}
@share=${this.#onShare}
><div slot="account-button"><slot name="account-button"></slot></div
></play-pen-header>
<main>
<play-editor
Expand Down Expand Up @@ -418,7 +432,7 @@ export class PlayPen extends LitElement {
}

/** Save to LocalStorage and URL as allowed. */
#save(): void {
#autosave(): void {
savePen(
this.allowURL ? location : undefined,
this.allowStorage ? localStorage : undefined,
Expand All @@ -428,7 +442,7 @@ export class PlayPen extends LitElement {

#setName(name: string, save: boolean): void {
this._name = name
if (save) this.#save()
if (save) this.#autosave()
}

#setSrc(src: string, save: boolean): void {
Expand Down Expand Up @@ -456,7 +470,7 @@ export class PlayPen extends LitElement {
webviewAssets: this._assetsState.map
}
)
if (save) this.#save()
if (save) this.#autosave()
this.#upload()
}, 500)

Expand Down
7 changes: 7 additions & 0 deletions src/elements/play-project-button.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {assert} from '@esm-bundle/chai'
import {PlayProjectButton} from './play-project-button.js'

test('tag is defined', () => {
const el = document.createElement('play-project-button')
assert.instanceOf(el, PlayProjectButton)
})
Loading
Loading