diff --git a/nx/blocks/loc/views/translate/translate.css b/nx/blocks/loc/views/translate/translate.css index 17b982f0b..45247acd2 100644 --- a/nx/blocks/loc/views/translate/translate.css +++ b/nx/blocks/loc/views/translate/translate.css @@ -2,6 +2,26 @@ svg { display: none; } +.nx-loading-spinner { + display: block; + width: 14px; + height: 14px; + border: 2px solid currentcolor; + border-top-color: transparent; + border-radius: 50%; + animation: da-spin 0.8s linear infinite; +} + +@keyframes da-spin { + to { transform: rotate(360deg); } +} + +@media (prefers-reduced-motion: reduce) { + .nx-loading-spinner { + animation-duration: 1.5s; + } +} + p { margin: 0; line-height: 1; diff --git a/nx/blocks/loc/views/translate/translate.js b/nx/blocks/loc/views/translate/translate.js index ac3c0b346..1d21ad3a8 100644 --- a/nx/blocks/loc/views/translate/translate.js +++ b/nx/blocks/loc/views/translate/translate.js @@ -24,6 +24,12 @@ class NxLocTranslate extends LitElement { _translateLangs: { state: true }, _copyLangs: { state: true }, _message: { state: true }, + _connectBusy: { state: true }, + _sendAllBusy: { state: true }, + _getStatusBusy: { state: true }, + _cancelAllBusy: { state: true }, + _copyAllBusy: { state: true }, + _cancelingLangs: { state: true }, }; connectedCallback() { @@ -92,7 +98,12 @@ class NxLocTranslate extends LitElement { async handleConnect() { const sendMessage = this.handleMessage.bind(this); - this._connected = await this._service.connector.connect(this._service, sendMessage); + this._connectBusy = true; + try { + this._connected = await this._service.connector.connect(this._service, sendMessage); + } finally { + this._connectBusy = false; + } } async fetchUrls(service, fetchContent, langs) { @@ -213,67 +224,91 @@ class NxLocTranslate extends LitElement { // return; // } - const conf = await this.getBaseTranslationConf(false); + this._getStatusBusy = true; + try { + const conf = await this.getBaseTranslationConf(false); - await this._service.connector.getStatusAll(removeWaitingLanguagesFromConf(conf)); + await this._service.connector.getStatusAll(removeWaitingLanguagesFromConf(conf)); - await this.checkAndSaveLangs(conf); + await this.checkAndSaveLangs(conf); - this.handleSaveLangs(); + this.handleSaveLangs(); + } finally { + this._getStatusBusy = false; + } } async handleCancelAll() { - const sendMessage = this.handleMessage.bind(this); + this._cancelAllBusy = true; + try { + const sendMessage = this.handleMessage.bind(this); - const { cancelTranslation } = this._service.connector; + const { cancelTranslation } = this._service.connector; - let shouldRefresh = false; - for (const lang of this._translateLangs) { - const result = await cancelTranslation({ service: this._service, lang, sendMessage }); - if (result?.ok !== false) shouldRefresh = true; - } + let shouldRefresh = false; + for (const lang of this._translateLangs) { + const result = await cancelTranslation({ service: this._service, lang, sendMessage }); + if (result?.ok !== false) shouldRefresh = true; + } - if (shouldRefresh) { - // Refresh locales GLaaS accepted; skip when every cancel was rejected. - await this.handleGetStatus(); + if (shouldRefresh) { + // Refresh locales GLaaS accepted; skip when every cancel was rejected. + await this.handleGetStatus(); + } + } finally { + this._cancelAllBusy = false; } } async handleCancelLang(lang) { - const sendMessage = this.handleMessage.bind(this); + this._cancelingLangs ??= new Set(); + this._cancelingLangs.add(lang.code); + this.requestUpdate(); + + try { + const sendMessage = this.handleMessage.bind(this); - const { cancelTranslation } = this._service.connector; + const { cancelTranslation } = this._service.connector; - const result = await cancelTranslation({ service: this._service, lang, sendMessage }); + const result = await cancelTranslation({ service: this._service, lang, sendMessage }); - if (result?.ok !== false) { - await this.handleGetStatus(); + if (result?.ok !== false) { + await this.handleGetStatus(); + } + } finally { + this._cancelingLangs.delete(lang.code); + this.requestUpdate(); } } async handleCopyAll() { - const { _copyLangs: langs } = this; + this._copyAllBusy = true; + try { + const { _copyLangs: langs } = this; - // langsWithUrls is an in-memory object that contains all URL fetches. - const { langsWithUrls, urls } = await this.fetchUrls({}, true, langs); - - langsWithUrls.forEach((lang) => { - const errors = lang.urls.filter((url) => url.error); - if (errors.length) { - // Create an errors array if it doesn't exist - this._urlErrors ??= []; - this._urlErrors.push(...errors); - } - }); + // langsWithUrls is an in-memory object that contains all URL fetches. + const { langsWithUrls, urls } = await this.fetchUrls({}, true, langs); - // Do not continue if any errors - if (this._urlErrors?.length) return; + langsWithUrls.forEach((lang) => { + const errors = lang.urls.filter((url) => url.error); + if (errors.length) { + // Create an errors array if it doesn't exist + this._urlErrors ??= []; + this._urlErrors.push(...errors); + } + }); - const { org, site, title, options } = this.project; + // Do not continue if any errors + if (this._urlErrors?.length) return; - await copySourceLangs(org, site, title, options, this._copyLangs, urls, langsWithUrls); - this.handleSaveLangs(); - this.requestUpdate(); + const { org, site, title, options } = this.project; + + await copySourceLangs(org, site, title, options, this._copyLangs, urls, langsWithUrls); + this.handleSaveLangs(); + this.requestUpdate(); + } finally { + this._copyAllBusy = false; + } } get _project() { @@ -300,11 +335,17 @@ class NxLocTranslate extends LitElement { return html`

Conflict behavior: ${this._options['translate.conflict.behavior']}

`; } + renderSpinner() { + return html``; + } + renderTranslateAction() { if (this._connected === false) { return html` ${this.renderBehavior()} - Connect + + ${this._connectBusy ? this.renderSpinner() : nothing} Connect + `; } @@ -317,14 +358,24 @@ class NxLocTranslate extends LitElement { if (sent) { return html` ${this.renderBehavior()} - ${this.canCancel ? html`Cancel project` : nothing} - ${this.incompleteLangs ? html`Get status` : nothing} + ${this.canCancel ? html` + + ${this._cancelAllBusy ? this.renderSpinner() : nothing} Cancel project + + ` : nothing} + ${this.incompleteLangs ? html` + + ${this._getStatusBusy ? this.renderSpinner() : nothing} Get status + + ` : nothing} `; } return html` ${this.renderBehavior()} - Translate all + + ${this._sendAllBusy ? this.renderSpinner() : nothing} Translate all + `; } } @@ -346,7 +397,12 @@ class NxLocTranslate extends LitElement { renderCancelLang(lang) { if (!this.canCancel || !this._connected || !lang.translation || lang.translation?.status === 'cancelled') return nothing; - return html` this.handleCancelLang(lang)} class="primary outline">Cancel`; + const busy = this._cancelingLangs?.has(lang.code); + return html` + this.handleCancelLang(lang)} class="primary outline" ?disabled=${busy}> + ${busy ? this.renderSpinner() : nothing} Cancel + + `; } renderUrlErrors() { @@ -417,7 +473,9 @@ class NxLocTranslate extends LitElement {

Copy (${this._options['source.language'].name})

Conflict behavior: ${this._options['copy.conflict.behavior']}

- Copy all + + ${this._copyAllBusy ? this.renderSpinner() : nothing} Copy all +