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');
});