UI: Move tasks to a primary tab Tasks are now rendered in a primary tab. This makes the UI compact and consistent. The API to fetch tasks has been moved from GrTaskPlugin to GrTaskSummary because the summary is registered to the commit-container which renders by default when the change page is loaded. The tasks tab content is attached to 'change-view-tab-content' which only renders when the tab is clicked or focus is changed to it. Screenshots: https://imgur.com/a/Qr2i1pQ Change-Id: I14a21fc83ef04a909576dfd796624044cbf72858
diff --git a/gr-task-plugin/gr-task-chip.js b/gr-task-plugin/gr-task-chip.js index 8e46bd0..a0c5b94 100644 --- a/gr-task-plugin/gr-task-chip.js +++ b/gr-task-plugin/gr-task-chip.js
@@ -18,7 +18,7 @@ import './gr-task-plugin.js'; import {htmlTemplate} from './gr-task-chip_html.js'; -class GrTaskChip extends Polymer.Element { +export class GrTaskChip extends Polymer.Element { static get is() { return 'gr-task-chip'; } @@ -37,23 +37,24 @@ }; } - _setTasksTabActive() { - // TODO: Identify a better way as current implementation is fragile - const endPointDecorators = document.querySelector('gr-app') + static getPrimaryTabs() { + return document.querySelector('gr-app') .shadowRoot.querySelector('gr-app-element') .shadowRoot.querySelector('main') .querySelector('gr-change-view') .shadowRoot.querySelector('#mainContent') - .getElementsByTagName('gr-endpoint-decorator'); - if (endPointDecorators) { - for (let i = 0; i <= endPointDecorators.length; i++) { - const el = endPointDecorators[i] - ?.shadowRoot?.querySelector('gr-task-plugin'); - if (el) { - el.shadowRoot.querySelector('paper-tabs') - .querySelector('paper-tab').scrollIntoView(); - break; - } + .querySelector('#primaryTabs'); + } + + _setTasksTabActive() { + // TODO: Identify a better way as current implementation is fragile + const paperTabs = GrTaskChip.getPrimaryTabs(); + const tabs = paperTabs.querySelectorAll('paper-tab'); + for (let i=0; i <= tabs.length; i++) { + if (tabs[i].dataset['name'] === 'change-view-tab-header-task') { + paperTabs.selected = i; + tabs[i].scrollIntoView({block: 'center'}); + break; } } }
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js index 2ac355d..2bff130 100644 --- a/gr-task-plugin/gr-task-plugin.js +++ b/gr-task-plugin/gr-task-plugin.js
@@ -82,30 +82,16 @@ notify: true, value: 0, }, - _invalid_count: { - type: Number, - notify: true, - value: 0, - }, - _waiting_count: { - type: Number, - notify: true, - value: 0, - }, - _duplicate_count: { - type: Number, - notify: true, - value: 0, - }, - _pass_count: { - type: Number, - notify: true, - value: 0, - }, + _isPending: { type: Boolean, value: true, }, + + _tasks_info: { + type: Object, + observer: '_tasksInfoChanged', + }, }; } @@ -113,52 +99,36 @@ return show_all === 'true'; } - connectedCallback() { - super.connectedCallback(); - + ready() { + super.ready(); + if (!this.change) { + return; + } + document.addEventListener(`response-tasks-${this.change._number}`, e => { + this._tasks_info = e.detail.tasks_info; + this._isPending = e.detail.is_loading; + }); this._getTasks(); } + _tasksInfoChanged(newValue, oldValue) { + if (this._tasks_info) { + this._tasks = this._addTasks(this._tasks_info.roots); + } + } + _is_hidden(_isPending, _tasks) { return (!_isPending && !_tasks.length); } - _getTasks() { - if (!this.change) { - return; + async _getTasks() { + while (this._isPending) { + document.dispatchEvent( + new CustomEvent(`request-tasks-${this.change._number}`, { + composed: true, bubbles: true, + })); + await new Promise(r => setTimeout(r, 100)); } - - this._isPending = true; - const endpoint = - `/changes/?q=change:${this.change._number}&--task--applicable`; - - return this.plugin.restApi().get(endpoint).then(response => { - this._isPending = false; - if (response && response.length === 1) { - const cinfo = response[0]; - if (cinfo.plugins) { - const taskPluginInfo = cinfo.plugins.find( - pluginInfo => pluginInfo.name === 'task'); - - if (taskPluginInfo) { - this._tasks = this._addTasks(taskPluginInfo.roots); - } - } - document.dispatchEvent(new CustomEvent('tasks-loaded', { - detail: { - ready_count: this._ready_count, - fail_count: this._fail_count, - invalid_count: this._invalid_count, - waiting_count: this._waiting_count, - duplicate_count: this._duplicate_count, - pass_count: this._pass_count, - }, - composed: true, bubbles: true, - })); - } - }).catch(e => { - this._isPending = false; - }); } _computeIcon(task) { @@ -223,18 +193,6 @@ case 'READY': this._ready_count++; break; - case 'INVALID': - this._invalid_count++; - break; - case 'WAITING': - this._waiting_count++; - break; - case 'DUPLICATE': - this._duplicate_count++; - break; - case 'PASS': - this._pass_count++; - break; } }
diff --git a/gr-task-plugin/gr-task-plugin_html.js b/gr-task-plugin/gr-task-plugin_html.js index 6ab5fb9..e0ea9cd 100644 --- a/gr-task-plugin/gr-task-plugin_html.js +++ b/gr-task-plugin/gr-task-plugin_html.js
@@ -80,14 +80,6 @@ </style> <div id="tasks" hidden$="[[_is_hidden(_isPending, _tasks)]]"> - <paper-tabs id="secondaryTabs" selected="0"> - <paper-tab - data-name$="Tasks" - class="Tasks" - > - Tasks - </paper-tab> - </paper-tabs> <section class="TasksList"> <div hidden$="[[!_isPending]]" class="task-list-item">Loading...</div> <div hidden$="[[_isPending]]" class="task-list-item"> @@ -119,6 +111,7 @@ <div hidden$="[[!_expand_all]]" style="padding-bottom: 12px"> <ul style="list-style-type:none;"> <gr-task-plugin-tasks + hidden$="[[_isPending]]" tasks="[[_tasks]]" show_all$="[[_show_all]]"> </gr-task-plugin-tasks> </ul>
diff --git a/gr-task-plugin/gr-task-summary.js b/gr-task-plugin/gr-task-summary.js index 49c858d..95ad3f3 100644 --- a/gr-task-plugin/gr-task-summary.js +++ b/gr-task-plugin/gr-task-summary.js
@@ -16,7 +16,7 @@ */ import {htmlTemplate} from './gr-task-summary_html.js'; -import './gr-task-chip.js'; +import {GrTaskChip} from './gr-task-chip.js'; class GrTaskSummary extends Polymer.Element { static get is() { @@ -29,6 +29,10 @@ static get properties() { return { + change: { + type: Object, + }, + ready_count: { type: Number, notify: true, @@ -69,23 +73,93 @@ type: Boolean, value: true, }, + + tasks_info: { + type: Object, + }, }; } /** @override */ ready() { super.ready(); - document.addEventListener('tasks-loaded', e => { - this.ready_count = e.detail.ready_count; - this.fail_count = e.detail.fail_count; - this.invalid_count = e.detail.invalid_count; - this.waiting_count = e.detail.waiting_count; - this.duplicate_count = e.detail.duplicate_count; - this.pass_count = e.detail.pass_count; - this.is_loading = false; + this._fetch_tasks(); + + document.addEventListener(`request-tasks-${this.change._number}`, e => { + document.dispatchEvent( + new CustomEvent(`response-tasks-${this.change._number}`, { + detail: { + tasks_info: this.tasks_info, + is_loading: this.is_loading, + }, + composed: true, bubbles: true, + })); }); } + _fetch_tasks() { + const endpoint = + `/changes/?q=change:${this.change._number}&--task--applicable`; + return this.plugin.restApi().get(endpoint).then(response => { + if (response && response.length === 1) { + const cinfo = response[0]; + if (cinfo.plugins) { + this.tasks_info = cinfo.plugins.find( + pluginInfo => pluginInfo.name === 'task'); + this._compute_counts(this.tasks_info.roots); + } + } + }).finally(e => { + this.is_loading = false; + if (!this._can_show_summary( + this.is_loading, this.ready_count, + this.fail_count, this.invalid_count, + this.waiting_count, this.duplicate_count, + this.pass_count)) { + this._hide_tasks_tab(); + } + }); + } + + _compute_counts(tasks) { + if (!tasks) return []; + tasks.forEach(task => { + switch (task.status) { + case 'FAIL': + this.fail_count++; + break; + case 'READY': + this.ready_count++; + break; + case 'INVALID': + this.invalid_count++; + break; + case 'WAITING': + this.waiting_count++; + break; + case 'DUPLICATE': + this.duplicate_count++; + break; + case 'PASS': + this.pass_count++; + break; + } + this._compute_counts(task.sub_tasks); + }); + } + + _hide_tasks_tab() { + const paperTabs = GrTaskChip.getPrimaryTabs(); + const tabs = paperTabs.querySelectorAll('paper-tab'); + for (let i=0; i <= tabs.length; i++) { + if (tabs[i].dataset['name'] === 'change-view-tab-header-task') { + tabs[i].setAttribute('hidden', true); + paperTabs.selected = 0; + break; + } + } + } + _can_show_summary(is_loading, ready_count, fail_count, invalid_count, waiting_count, duplicate_count, @@ -98,4 +172,4 @@ } } -customElements.define(GrTaskSummary.is, GrTaskSummary); \ No newline at end of file +customElements.define(GrTaskSummary.is, GrTaskSummary);
diff --git a/gr-task-plugin/gr-task-tab-header.js b/gr-task-plugin/gr-task-tab-header.js new file mode 100644 index 0000000..1bc6bbe --- /dev/null +++ b/gr-task-plugin/gr-task-tab-header.js
@@ -0,0 +1,31 @@ +/** + * @license + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {htmlTemplate} from './gr-task-tab-header_html.js'; +import './gr-task-plugin.js'; + +class GrTaskTabHeader extends Polymer.Element { + static get is() { + return 'gr-task-tab-header'; + } + + static get template() { + return htmlTemplate; + } +} + +customElements.define(GrTaskTabHeader.is, GrTaskTabHeader); \ No newline at end of file
diff --git a/gr-task-plugin/gr-task-tab-header_html.js b/gr-task-plugin/gr-task-tab-header_html.js new file mode 100644 index 0000000..66be4a6 --- /dev/null +++ b/gr-task-plugin/gr-task-tab-header_html.js
@@ -0,0 +1,20 @@ +/** + * @license + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const htmlTemplate = Polymer.html` + <div class="gr-task-tab-header">Tasks</div> +`; \ No newline at end of file
diff --git a/gr-task-plugin/plugin.js b/gr-task-plugin/plugin.js index 2219f12..2448ea1 100644 --- a/gr-task-plugin/plugin.js +++ b/gr-task-plugin/plugin.js
@@ -17,10 +17,18 @@ import './gr-task-plugin.js'; import './gr-task-summary.js'; +import './gr-task-tab-header.js'; Gerrit.install(plugin => { + plugin.registerDynamicCustomComponent( + 'change-view-tab-header', + 'gr-task-tab-header' + ); + plugin.registerDynamicCustomComponent( + 'change-view-tab-content', + 'gr-task-plugin' + ); plugin.registerCustomComponent( - 'change-view-integration', 'gr-task-plugin'); - plugin.registerCustomComponent( - 'commit-container', 'gr-task-summary'); + 'commit-container', + 'gr-task-summary'); });