blob: f178094a90499b4ec72411cd9ce2d2eadee20c66 [file] [log] [blame]
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04001/**
2 * @license
3 * Copyright (C) 2016 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
Milutin Kristofic77b774e2020-08-25 14:30:26 +020017import '@polymer/iron-input/iron-input';
18import '../../shared/gr-autocomplete/gr-autocomplete';
19import '../../shared/gr-button/gr-button';
20import '../../shared/gr-rest-api-interface/gr-rest-api-interface';
21import '../../../styles/gr-form-styles';
22import '../../../styles/shared-styles';
23import {dom, EventApi} from '@polymer/polymer/lib/legacy/polymer.dom';
24import {GestureEventListeners} from '@polymer/polymer/lib/mixins/gesture-event-listeners';
25import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin';
26import {PolymerElement} from '@polymer/polymer/polymer-element';
27import {htmlTemplate} from './gr-watched-projects-editor_html';
28import {customElement, property} from '@polymer/decorators';
29import {
30 AutocompleteQuery,
31 GrAutocomplete,
32 AutocompleteSuggestion,
33} from '../../shared/gr-autocomplete/gr-autocomplete';
34import {GrRestApiInterface} from '../../shared/gr-rest-api-interface/gr-rest-api-interface';
35import {hasOwnProperty} from '../../../utils/common-util';
36import {ProjectWatchInfo} from '../../../types/common';
Wyatt Allenbd472172016-06-09 17:44:51 -070037
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010038const NOTIFICATION_TYPES = [
39 {name: 'Changes', key: 'notify_new_changes'},
40 {name: 'Patches', key: 'notify_new_patch_sets'},
41 {name: 'Comments', key: 'notify_all_comments'},
42 {name: 'Submits', key: 'notify_submitted_changes'},
43 {name: 'Abandons', key: 'notify_abandoned_changes'},
44];
Wyatt Allenbd472172016-06-09 17:44:51 -070045
Milutin Kristofic77b774e2020-08-25 14:30:26 +020046export interface GrWatchedProjectsEditor {
47 $: {
48 restAPI: GrRestApiInterface;
49 newFilter: HTMLInputElement;
50 newProject: GrAutocomplete;
51 };
52}
53@customElement('gr-watched-projects-editor')
54export class GrWatchedProjectsEditor extends GestureEventListeners(
55 LegacyElementMixin(PolymerElement)
56) {
57 static get template() {
58 return htmlTemplate;
59 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010060
Milutin Kristofic77b774e2020-08-25 14:30:26 +020061 @property({type: Boolean, notify: true})
62 hasUnsavedChanges = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010063
Milutin Kristofic77b774e2020-08-25 14:30:26 +020064 @property({type: Array})
65 _projects?: ProjectWatchInfo[];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010066
Milutin Kristofic77b774e2020-08-25 14:30:26 +020067 @property({type: Array})
68 _projectsToRemove: ProjectWatchInfo[] = [];
69
70 @property({type: Object})
71 _query?: AutocompleteQuery;
72
73 constructor() {
74 super();
75 this._query = input => this._getProjectSuggestions(input);
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +010076 }
77
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010078 loadData() {
79 return this.$.restAPI.getWatchedProjects().then(projs => {
80 this._projects = projs;
81 });
82 }
83
84 save() {
85 let deletePromise;
86 if (this._projectsToRemove.length) {
87 deletePromise = this.$.restAPI.deleteWatchedProjects(
Milutin Kristofic77b774e2020-08-25 14:30:26 +020088 this._projectsToRemove
89 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010090 } else {
Milutin Kristofic77b774e2020-08-25 14:30:26 +020091 deletePromise = Promise.resolve(undefined);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010092 }
93
94 return deletePromise
Milutin Kristofic77b774e2020-08-25 14:30:26 +020095 .then(() => {
96 if (this._projects) {
97 return this.$.restAPI.saveWatchedProjects(this._projects);
98 } else {
99 return Promise.resolve(undefined);
100 }
101 })
102 .then(projects => {
103 this._projects = projects;
104 this._projectsToRemove = [];
105 this.hasUnsavedChanges = false;
106 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100107 }
108
109 _getTypes() {
110 return NOTIFICATION_TYPES;
111 }
112
113 _getTypeCount() {
114 return this._getTypes().length;
115 }
116
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200117 _computeCheckboxChecked(project: ProjectWatchInfo, key: string) {
118 return hasOwnProperty(project, key);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100119 }
120
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200121 _getProjectSuggestions(input: string) {
122 return this.$.restAPI.getSuggestedProjects(input).then(response => {
123 const projects: AutocompleteSuggestion[] = [];
124 for (const key in response) {
125 if (!hasOwnProperty(response, key)) {
126 continue;
127 }
128 projects.push({
129 name: key,
130 value: response[key].id,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100131 });
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200132 }
133 return projects;
134 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100135 }
136
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200137 _handleRemoveProject(e: Event) {
138 const el = (dom(e) as EventApi).localTarget as HTMLInputElement;
139 const dataIndex = el.getAttribute('data-index');
140 if (dataIndex === null || !this._projects) return;
141 const index = parseInt(dataIndex, 10);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100142 const project = this._projects[index];
143 this.splice('_projects', index, 1);
144 this.push('_projectsToRemove', project);
145 this.hasUnsavedChanges = true;
146 }
147
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200148 _canAddProject(
149 project: string | null,
150 text: string | null,
151 filter: string | null
152 ) {
153 if (project === null && text === null) {
154 return false;
155 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100156
157 // This will only be used if not using the auto complete
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200158 if (!project && text) {
159 return true;
160 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100161
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200162 if (!this._projects) return true;
163 // Check if the project with filter is already in the list.
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100164 for (let i = 0; i < this._projects.length; i++) {
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200165 if (
166 this._projects[i].project === project &&
167 this.areFiltersEqual(this._projects[i].filter, filter)
168 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100169 return false;
170 }
171 }
172
173 return true;
174 }
175
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200176 _getNewProjectIndex(name: string, filter: string | null) {
177 if (!this._projects) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100178 let i;
179 for (i = 0; i < this._projects.length; i++) {
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200180 const projectFilter = this._projects[i].filter;
181 if (
182 this._projects[i].project > name ||
183 (this._projects[i].project === name &&
184 this.isFilterDefined(projectFilter) &&
185 this.isFilterDefined(filter) &&
186 projectFilter! > filter!)
187 ) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100188 break;
189 }
190 }
191 return i;
192 }
193
194 _handleAddProject() {
195 const newProject = this.$.newProject.value;
196 const newProjectName = this.$.newProject.text;
197 const filter = this.$.newFilter.value || null;
198
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200199 if (!this._canAddProject(newProject, newProjectName, filter)) {
200 return;
201 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100202
203 const insertIndex = this._getNewProjectIndex(newProjectName, filter);
204
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200205 if (insertIndex !== undefined) {
206 this.splice('_projects', insertIndex, 0, {
207 project: newProjectName,
208 filter,
209 _is_local: true,
210 });
211 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100212
213 this.$.newProject.clear();
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200214 this.$.newFilter.value = '';
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100215 this.hasUnsavedChanges = true;
216 }
217
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200218 _handleCheckboxChange(e: Event) {
219 const el = (dom(e) as EventApi).localTarget as HTMLInputElement;
220 if (el === null) return;
221 const dataIndex = el.getAttribute('data-index');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100222 const key = el.getAttribute('data-key');
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200223 if (dataIndex === null || key === null) return;
224 const index = parseInt(dataIndex, 10);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100225 const checked = el.checked;
226 this.set(['_projects', index, key], !!checked);
227 this.hasUnsavedChanges = true;
228 }
229
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200230 _handleNotifCellClick(e: Event) {
231 if (e.target === null) return;
232 const checkbox = (e.target as HTMLElement).querySelector('input');
233 if (checkbox) {
234 checkbox.click();
235 }
236 }
237
238 isFilterDefined(filter: string | null | undefined) {
239 return filter !== null && filter !== undefined;
240 }
241
242 areFiltersEqual(
243 filter1: string | null | undefined,
244 filter2: string | null | undefined
245 ) {
246 // null and undefined are equal
247 if (!this.isFilterDefined(filter1) && !this.isFilterDefined(filter2)) {
248 return true;
249 }
250 return filter1 === filter2;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100251 }
252}
253
Milutin Kristofic77b774e2020-08-25 14:30:26 +0200254declare global {
255 interface HTMLElementTagNameMap {
256 'gr-watched-projects-editor': GrWatchedProjectsEditor;
257 }
258}