Skip to content

Commit 1a7784a

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
feat: progress for the remote sketch creation
Closes #1668 Signed-off-by: Akos Kitta <[email protected]>
1 parent d24a391 commit 1a7784a

File tree

3 files changed

+167
-34
lines changed

3 files changed

+167
-34
lines changed

Diff for: arduino-ide-extension/src/browser/contributions/new-cloud-sketch.ts

+156-31
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1-
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
1+
import { DialogError } from '@theia/core/lib/browser/dialogs';
22
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
3+
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
34
import { CompositeTreeNode } from '@theia/core/lib/browser/tree';
4-
import { DisposableCollection } from '@theia/core/lib/common/disposable';
5+
import { Widget } from '@theia/core/lib/browser/widgets/widget';
6+
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
7+
import {
8+
Disposable,
9+
DisposableCollection,
10+
} from '@theia/core/lib/common/disposable';
11+
import { MenuModelRegistry } from '@theia/core/lib/common/menu';
12+
import {
13+
Progress,
14+
ProgressUpdate,
15+
} from '@theia/core/lib/common/message-service-protocol';
516
import { nls } from '@theia/core/lib/common/nls';
617
import { inject, injectable } from '@theia/core/shared/inversify';
18+
import { WorkspaceInputDialogProps } from '@theia/workspace/lib/browser/workspace-input-dialog';
19+
import { v4 } from 'uuid';
720
import { MainMenuManager } from '../../common/main-menu-manager';
821
import type { AuthenticationSession } from '../../node/auth/types';
922
import { AuthenticationClientService } from '../auth/authentication-client-service';
@@ -90,7 +103,7 @@ export class NewCloudSketch extends Contribution {
90103

91104
private async createNewSketch(
92105
initialValue?: string | undefined
93-
): Promise<URI | undefined> {
106+
): Promise<unknown> {
94107
const widget = await this.widgetContribution.widget;
95108
const treeModel = this.treeModelFrom(widget);
96109
if (!treeModel) {
@@ -102,34 +115,50 @@ export class NewCloudSketch extends Contribution {
102115
if (!rootNode) {
103116
return undefined;
104117
}
118+
return this.openWizard(rootNode, treeModel, initialValue);
119+
}
105120

106-
const newSketchName = await this.newSketchName(rootNode, initialValue);
107-
if (!newSketchName) {
108-
return undefined;
109-
}
110-
let result: Create.Sketch | undefined | 'conflict';
111-
try {
112-
result = await this.createApi.createSketch(newSketchName);
113-
} catch (err) {
114-
if (isConflict(err)) {
115-
result = 'conflict';
116-
} else {
117-
throw err;
121+
private withProgress(
122+
value: string,
123+
treeModel: CloudSketchbookTreeModel
124+
): (progress: Progress) => Promise<unknown> {
125+
return async (progress: Progress) => {
126+
let result: Create.Sketch | undefined | 'conflict';
127+
try {
128+
progress.report({
129+
message: nls.localize(
130+
'arduino/cloudSketch/creating',
131+
"Creating remote sketch '{0}'...",
132+
value
133+
),
134+
});
135+
result = await this.createApi.createSketch(value);
136+
} catch (err) {
137+
if (isConflict(err)) {
138+
result = 'conflict';
139+
} else {
140+
throw err;
141+
}
142+
} finally {
143+
if (result) {
144+
progress.report({
145+
message: nls.localize(
146+
'arduino/cloudSketch/synchronizing',
147+
"Synchronizing sketchbook, pulling '{0}'...",
148+
value
149+
),
150+
});
151+
await treeModel.refresh();
152+
}
153+
}
154+
if (result === 'conflict') {
155+
return this.createNewSketch(value);
118156
}
119-
} finally {
120157
if (result) {
121-
await treeModel.refresh();
158+
return this.open(treeModel, result);
122159
}
123-
}
124-
125-
if (result === 'conflict') {
126-
return this.createNewSketch(newSketchName);
127-
}
128-
129-
if (result) {
130-
return this.open(treeModel, result);
131-
}
132-
return undefined;
160+
return undefined;
161+
};
133162
}
134163

135164
private async open(
@@ -183,14 +212,15 @@ export class NewCloudSketch extends Contribution {
183212
return undefined;
184213
}
185214

186-
private async newSketchName(
215+
private async openWizard(
187216
rootNode: CompositeTreeNode,
217+
treeModel: CloudSketchbookTreeModel,
188218
initialValue?: string | undefined
189-
): Promise<string | undefined> {
219+
): Promise<unknown> {
190220
const existingNames = rootNode.children
191221
.filter(CloudSketchbookTree.CloudSketchDirNode.is)
192222
.map(({ fileStat }) => fileStat.name);
193-
return new WorkspaceInputDialog(
223+
return new NewCloudSketchDialog(
194224
{
195225
title: nls.localize(
196226
'arduino/newCloudSketch/newSketchTitle',
@@ -216,7 +246,8 @@ export class NewCloudSketch extends Contribution {
216246
);
217247
},
218248
},
219-
this.labelProvider
249+
this.labelProvider,
250+
(value) => this.withProgress(value, treeModel)
220251
).open();
221252
}
222253
}
@@ -245,3 +276,97 @@ function isErrorWithStatusOf(
245276
}
246277
return false;
247278
}
279+
280+
@injectable()
281+
class NewCloudSketchDialog extends WorkspaceInputDialog {
282+
constructor(
283+
@inject(WorkspaceInputDialogProps)
284+
protected override readonly props: WorkspaceInputDialogProps,
285+
@inject(LabelProvider)
286+
protected override readonly labelProvider: LabelProvider,
287+
private readonly withProgress: (
288+
value: string
289+
) => (progress: Progress) => Promise<unknown>
290+
) {
291+
super(props, labelProvider);
292+
}
293+
protected override async accept(): Promise<void> {
294+
if (!this.resolve) {
295+
return;
296+
}
297+
this.acceptCancellationSource.cancel();
298+
this.acceptCancellationSource = new CancellationTokenSource();
299+
const token = this.acceptCancellationSource.token;
300+
const value = this.value;
301+
const error = await this.isValid(value, 'open');
302+
if (token.isCancellationRequested) {
303+
return;
304+
}
305+
if (!DialogError.getResult(error)) {
306+
this.setErrorMessage(error);
307+
} else {
308+
const spinner = document.createElement('div');
309+
spinner.classList.add('spinner');
310+
const disposables = new DisposableCollection();
311+
try {
312+
this.toggleButtons(true);
313+
disposables.push(Disposable.create(() => this.toggleButtons(false)));
314+
315+
const closeParent = this.closeCrossNode.parentNode;
316+
closeParent?.removeChild(this.closeCrossNode);
317+
disposables.push(
318+
Disposable.create(() => {
319+
closeParent?.appendChild(this.closeCrossNode);
320+
})
321+
);
322+
323+
this.errorMessageNode.classList.add('progress');
324+
disposables.push(
325+
Disposable.create(() =>
326+
this.errorMessageNode.classList.remove('progress')
327+
)
328+
);
329+
330+
const errorParent = this.errorMessageNode.parentNode;
331+
errorParent?.insertBefore(spinner, this.errorMessageNode);
332+
disposables.push(
333+
Disposable.create(() => errorParent?.removeChild(spinner))
334+
);
335+
336+
const cancellationSource = new CancellationTokenSource();
337+
const progress: Progress = {
338+
id: v4(),
339+
cancel: () => cancellationSource.cancel(),
340+
report: (update: ProgressUpdate) => {
341+
this.setProgressMessage(update);
342+
},
343+
result: Promise.resolve(value),
344+
};
345+
await this.withProgress(value)(progress);
346+
} finally {
347+
disposables.dispose();
348+
}
349+
this.resolve(value);
350+
Widget.detach(this);
351+
}
352+
}
353+
354+
private toggleButtons(disabled: boolean): void {
355+
if (this.acceptButton) {
356+
this.acceptButton.disabled = disabled;
357+
}
358+
if (this.closeButton) {
359+
this.closeButton.disabled = disabled;
360+
}
361+
}
362+
363+
private setProgressMessage(update: ProgressUpdate): void {
364+
if (update.work && update.work.done === update.work.total) {
365+
this.errorMessageNode.innerText = '';
366+
} else {
367+
if (update.message) {
368+
this.errorMessageNode.innerText = update.message;
369+
}
370+
}
371+
}
372+
}

Diff for: arduino-ide-extension/src/browser/style/dialogs.css

+8-2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
align-items: center;
5656
}
5757

58+
.p-Widget.dialogOverlay .dialogControl .spinner,
5859
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow .spinner {
5960
background: var(--theia-icon-loading) center center no-repeat;
6061
animation: theia-spin 1.25s linear infinite;
@@ -63,11 +64,11 @@
6364
}
6465

6566
.p-Widget.dialogOverlay .dialogBlock .dialogContent .dialogSection .dialogRow:first-child {
66-
margin-top: 0px;
67+
margin-top: 0px;
6768
height: 32px;
6869
}
6970

70-
.fl1{
71+
.fl1 {
7172
flex: 1;
7273
}
7374

@@ -85,3 +86,8 @@
8586
max-height: 400px;
8687
}
8788
}
89+
90+
.p-Widget.dialogOverlay .error.progress {
91+
color: var(--theia-button-background);
92+
align-self: center;
93+
}

Diff for: i18n/en.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@
120120
"visitArduinoCloud": "Visit Arduino Cloud to create Cloud Sketches."
121121
},
122122
"cloudSketch": {
123-
"new": "New Remote Sketch"
123+
"creating": "Creating remote sketch '{0}'...",
124+
"new": "New Remote Sketch",
125+
"synchronizing": "Synchronizing sketchbook, pulling '{0}'..."
124126
},
125127
"common": {
126128
"all": "All",

0 commit comments

Comments
 (0)