diff --git a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts index d7fb671bd..c61852ca5 100644 --- a/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts +++ b/arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts @@ -1,9 +1,22 @@ -import { MenuModelRegistry } from '@theia/core/lib/common/menu'; +import { DialogError } from '@theia/core/lib/browser/dialogs'; import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding'; +import { LabelProvider } from '@theia/core/lib/browser/label-provider'; import { CompositeTreeNode } from '@theia/core/lib/browser/tree'; -import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Widget } from '@theia/core/lib/browser/widgets/widget'; +import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; +import { + Disposable, + DisposableCollection, +} from '@theia/core/lib/common/disposable'; +import { MenuModelRegistry } from '@theia/core/lib/common/menu'; +import { + Progress, + ProgressUpdate, +} from '@theia/core/lib/common/message-service-protocol'; import { nls } from '@theia/core/lib/common/nls'; import { inject, injectable } from '@theia/core/shared/inversify'; +import { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog'; +import { v4 } from 'uuid'; import { MainMenuManager } from '../../common/main-menu-manager'; import type { AuthenticationSession } from '../../node/auth/types'; import { AuthenticationClientService } from '../auth/authentication-client-service'; @@ -90,7 +103,7 @@ export class NewCloudSketch extends Contribution { private async createNewSketch( initialValue?: string | undefined - ): Promise { + ): Promise { const widget = await this.widgetContribution.widget; const treeModel = this.treeModelFrom(widget); if (!treeModel) { @@ -102,34 +115,50 @@ export class NewCloudSketch extends Contribution { if (!rootNode) { return undefined; } + return this.openWizard(rootNode, treeModel, initialValue); + } - const newSketchName = await this.newSketchName(rootNode, initialValue); - if (!newSketchName) { - return undefined; - } - let result: Create.Sketch | undefined | 'conflict'; - try { - result = await this.createApi.createSketch(newSketchName); - } catch (err) { - if (isConflict(err)) { - result = 'conflict'; - } else { - throw err; + private withProgress( + value: string, + treeModel: CloudSketchbookTreeModel + ): (progress: Progress) => Promise { + return async (progress: Progress) => { + let result: Create.Sketch | undefined | 'conflict'; + try { + progress.report({ + message: nls.localize( + 'arduino/cloudSketch/creating', + "Creating remote sketch '{0}'...", + value + ), + }); + result = await this.createApi.createSketch(value); + } catch (err) { + if (isConflict(err)) { + result = 'conflict'; + } else { + throw err; + } + } finally { + if (result) { + progress.report({ + message: nls.localize( + 'arduino/cloudSketch/synchronizing', + "Synchronizing sketchbook, pulling '{0}'...", + value + ), + }); + await treeModel.refresh(); + } + } + if (result === 'conflict') { + return this.createNewSketch(value); } - } finally { if (result) { - await treeModel.refresh(); + return this.open(treeModel, result); } - } - - if (result === 'conflict') { - return this.createNewSketch(newSketchName); - } - - if (result) { - return this.open(treeModel, result); - } - return undefined; + return undefined; + }; } private async open( @@ -183,14 +212,15 @@ export class NewCloudSketch extends Contribution { return undefined; } - private async newSketchName( + private async openWizard( rootNode: CompositeTreeNode, + treeModel: CloudSketchbookTreeModel, initialValue?: string | undefined - ): Promise { + ): Promise { const existingNames = rootNode.children .filter(CloudSketchbookTree.CloudSketchDirNode.is) .map(({ fileStat }) => fileStat.name); - return new WorkspaceInputDialog( + return new NewCloudSketchDialog( { title: nls.localize( 'arduino/newCloudSketch/newSketchTitle', @@ -216,7 +246,8 @@ export class NewCloudSketch extends Contribution { ); }, }, - this.labelProvider + this.labelProvider, + (value) => this.withProgress(value, treeModel) ).open(); } } @@ -245,3 +276,97 @@ function isErrorWithStatusOf( } return false; } + +@injectable() +class NewCloudSketchDialog extends WorkspaceInputDialog { + constructor( + @inject(WorkspaceInputDialogProps) + protected override readonly props: WorkspaceInputDialogProps, + @inject(LabelProvider) + protected override readonly labelProvider: LabelProvider, + private readonly withProgress: ( + value: string + ) => (progress: Progress) => Promise + ) { + super(props, labelProvider); + } + protected override async accept(): Promise { + if (!this.resolve) { + return; + } + this.acceptCancellationSource.cancel(); + this.acceptCancellationSource = new CancellationTokenSource(); + const token = this.acceptCancellationSource.token; + const value = this.value; + const error = await this.isValid(value, 'open'); + if (token.isCancellationRequested) { + return; + } + if (!DialogError.getResult(error)) { + this.setErrorMessage(error); + } else { + const spinner = document.createElement('div'); + spinner.classList.add('spinner'); + const disposables = new DisposableCollection(); + try { + this.toggleButtons(true); + disposables.push(Disposable.create(() => this.toggleButtons(false))); + + const closeParent = this.closeCrossNode.parentNode; + closeParent?.removeChild(this.closeCrossNode); + disposables.push( + Disposable.create(() => { + closeParent?.appendChild(this.closeCrossNode); + }) + ); + + this.errorMessageNode.classList.add('progress'); + disposables.push( + Disposable.create(() => + this.errorMessageNode.classList.remove('progress') + ) + ); + + const errorParent = this.errorMessageNode.parentNode; + errorParent?.insertBefore(spinner, this.errorMessageNode); + disposables.push( + Disposable.create(() => errorParent?.removeChild(spinner)) + ); + + const cancellationSource = new CancellationTokenSource(); + const progress: Progress = { + id: v4(), + cancel: () => cancellationSource.cancel(), + report: (update: ProgressUpdate) => { + this.setProgressMessage(update); + }, + result: Promise.resolve(value), + }; + await this.withProgress(value)(progress); + } finally { + disposables.dispose(); + } + this.resolve(value); + Widget.detach(this); + } + } + + private toggleButtons(disabled: boolean): void { + if (this.acceptButton) { + this.acceptButton.disabled = disabled; + } + if (this.closeButton) { + this.closeButton.disabled = disabled; + } + } + + private setProgressMessage(update: ProgressUpdate): void { + if (update.work && update.work.done === update.work.total) { + this.errorMessageNode.innerText = ''; + } else { + if (update.message) { + this.errorMessageNode.innerText = update.message; + } + } + } +} diff --git a/arduino-ide-extension/src/browser/style/dialogs.css b/arduino-ide-extension/src/browser/style/dialogs.css index 668c389b2..f48e7e25b 100644 --- a/arduino-ide-extension/src/browser/style/dialogs.css +++ b/arduino-ide-extension/src/browser/style/dialogs.css @@ -55,6 +55,7 @@ align-items: center; } +.p-Widget.dialogOverlay .dialogControl .spinner, .p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner { background: var(--theia-icon-loading) center center no-repeat; animation: theia-spin 1.25s linear infinite; @@ -63,11 +64,11 @@ } .p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child { - margin-top: 0px; + margin-top: 0px; height: 32px; } -.fl1{ +.fl1 { flex: 1; } @@ -85,3 +86,8 @@ max-height: 400px; } } + +.p-Widget.dialogOverlay .error.progress { + color: var(--theia-button-background); + align-self: center; +} diff --git a/i18n/en.json b/i18n/en.json index 418a5ae80..b5659bcf0 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -120,7 +120,9 @@ "visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches." }, "cloudSketch": { - "new": "New Remote Sketch" + "creating": "Creating remote sketch '{0}'...", + "new": "New Remote Sketch", + "synchronizing": "Synchronizing sketchbook, pulling '{0}'..." }, "common": { "all": "All",