Dave Borowitz | 8cdc76b | 2018-03-26 10:04:27 -0400 | [diff] [blame] | 1 | /** |
| 2 | * @license |
Ben Rohlfs | 94fcbbc | 2022-05-27 10:45:03 +0200 | [diff] [blame] | 3 | * Copyright 2016 Google LLC |
| 4 | * SPDX-License-Identifier: Apache-2.0 |
Dave Borowitz | 8cdc76b | 2018-03-26 10:04:27 -0400 | [diff] [blame] | 5 | */ |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 6 | import '@polymer/iron-input/iron-input'; |
| 7 | import '../../shared/gr-autocomplete/gr-autocomplete'; |
| 8 | import '../../shared/gr-button/gr-button'; |
Frank Borden | 42c1a45 | 2022-08-11 16:27:20 +0200 | [diff] [blame] | 9 | import {customElement, property, query} from 'lit/decorators.js'; |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 10 | import { |
| 11 | AutocompleteQuery, |
| 12 | GrAutocomplete, |
| 13 | AutocompleteSuggestion, |
| 14 | } from '../../shared/gr-autocomplete/gr-autocomplete'; |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 15 | import {assertIsDefined} from '../../../utils/common-util'; |
| 16 | import {ProjectWatchInfo, RepoName} from '../../../types/common'; |
Chris Poucet | c6e880b | 2021-11-15 19:57:06 +0100 | [diff] [blame] | 17 | import {getAppContext} from '../../../services/app-context'; |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 18 | import {css, html, LitElement} from 'lit'; |
| 19 | import {sharedStyles} from '../../../styles/shared-styles'; |
| 20 | import {formStyles} from '../../../styles/gr-form-styles'; |
Frank Borden | 42c1a45 | 2022-08-11 16:27:20 +0200 | [diff] [blame] | 21 | import {when} from 'lit/directives/when.js'; |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 22 | import {fire} from '../../../utils/event-util'; |
Frank Borden | 7944811 | 2022-04-12 16:59:32 +0200 | [diff] [blame] | 23 | import {PropertiesOfType} from '../../../utils/type-util'; |
Kamil Musin | c39f438 | 2022-11-29 17:43:40 +0100 | [diff] [blame] | 24 | import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper'; |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 25 | |
| 26 | type NotificationKey = PropertiesOfType<Required<ProjectWatchInfo>, boolean>; |
| 27 | |
| 28 | const NOTIFICATION_TYPES: Array<{name: string; key: NotificationKey}> = [ |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 29 | {name: 'Changes', key: 'notify_new_changes'}, |
| 30 | {name: 'Patches', key: 'notify_new_patch_sets'}, |
| 31 | {name: 'Comments', key: 'notify_all_comments'}, |
| 32 | {name: 'Submits', key: 'notify_submitted_changes'}, |
| 33 | {name: 'Abandons', key: 'notify_abandoned_changes'}, |
| 34 | ]; |
Wyatt Allen | bd47217 | 2016-06-09 17:44:51 -0700 | [diff] [blame] | 35 | |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 36 | @customElement('gr-watched-projects-editor') |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 37 | export class GrWatchedProjectsEditor extends LitElement { |
| 38 | // Private but used in tests. |
| 39 | @query('#newFilter') |
| 40 | newFilter?: HTMLInputElement; |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 41 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 42 | // Private but used in tests. |
| 43 | @query('#newProject') |
| 44 | newProject?: GrAutocomplete; |
| 45 | |
| 46 | @property({type: Boolean}) |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 47 | hasUnsavedChanges = false; |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 48 | |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 49 | @property({type: Array}) |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 50 | projects?: ProjectWatchInfo[]; |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 51 | |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 52 | @property({type: Array}) |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 53 | projectsToRemove: ProjectWatchInfo[] = []; |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 54 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 55 | private readonly query: AutocompleteQuery = input => |
| 56 | this.getProjectSuggestions(input); |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 57 | |
Chris Poucet | c6e880b | 2021-11-15 19:57:06 +0100 | [diff] [blame] | 58 | private readonly restApiService = getAppContext().restApiService; |
Ben Rohlfs | 43935a4 | 2020-12-01 19:14:09 +0100 | [diff] [blame] | 59 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 60 | static override get styles() { |
| 61 | return [ |
| 62 | sharedStyles, |
| 63 | formStyles, |
| 64 | css` |
| 65 | #watchedProjects .notifType { |
| 66 | text-align: center; |
| 67 | padding: 0 var(--spacing-s); |
| 68 | } |
| 69 | .notifControl { |
| 70 | cursor: pointer; |
| 71 | text-align: center; |
| 72 | } |
| 73 | .notifControl:hover { |
| 74 | outline: 1px solid var(--border-color); |
| 75 | } |
| 76 | .projectFilter { |
| 77 | color: var(--deemphasized-text-color); |
| 78 | font-style: italic; |
| 79 | margin-left: var(--spacing-l); |
| 80 | } |
| 81 | .newFilterInput { |
| 82 | width: 100%; |
| 83 | } |
| 84 | `, |
| 85 | ]; |
| 86 | } |
| 87 | |
| 88 | override render() { |
| 89 | const types = NOTIFICATION_TYPES; |
| 90 | return html` <div class="gr-form-styles"> |
| 91 | <table id="watchedProjects"> |
| 92 | <thead> |
| 93 | <tr> |
| 94 | <th>Repo</th> |
| 95 | ${types.map(type => html`<th class="notifType">${type.name}</th>`)} |
| 96 | <th></th> |
| 97 | </tr> |
| 98 | </thead> |
| 99 | <tbody> |
| 100 | ${(this.projects ?? []).map(project => this.renderProject(project))} |
| 101 | </tbody> |
| 102 | <tfoot> |
| 103 | <tr> |
| 104 | <th> |
| 105 | <gr-autocomplete |
| 106 | id="newProject" |
Frank Borden | 1a8dbba | 2022-09-01 14:09:15 +0200 | [diff] [blame] | 107 | .query=${this.query} |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 108 | threshold="1" |
Dhruv | 80229b5 | 2022-04-08 15:46:48 +0200 | [diff] [blame] | 109 | allow-non-suggested-values |
| 110 | tab-complete |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 111 | placeholder="Repo" |
| 112 | ></gr-autocomplete> |
| 113 | </th> |
| 114 | <th colspan=${types.length}> |
| 115 | <iron-input id="newFilterInput" class="newFilterInput"> |
| 116 | <input |
| 117 | id="newFilter" |
| 118 | class="newFilterInput" |
| 119 | placeholder="branch:name, or other search expression" |
| 120 | /> |
| 121 | </iron-input> |
| 122 | </th> |
| 123 | <th> |
| 124 | <gr-button link="" @click=${this.handleAddProject}>Add</gr-button> |
| 125 | </th> |
| 126 | </tr> |
| 127 | </tfoot> |
| 128 | </table> |
| 129 | </div>`; |
| 130 | } |
| 131 | |
| 132 | private renderProject(project: ProjectWatchInfo) { |
| 133 | const types = NOTIFICATION_TYPES; |
| 134 | return html` <tr> |
| 135 | <td> |
| 136 | ${project.project} |
| 137 | ${when( |
| 138 | project.filter, |
| 139 | () => html`<div class="projectFilter">${project.filter}</div>` |
| 140 | )} |
| 141 | </td> |
| 142 | ${types.map(type => this.renderNotifyControl(project, type.key))} |
| 143 | <td> |
| 144 | <gr-button |
| 145 | link="" |
| 146 | @click=${(_e: Event) => this.handleRemoveProject(project)} |
| 147 | >Delete</gr-button |
| 148 | > |
| 149 | </td> |
| 150 | </tr>`; |
| 151 | } |
| 152 | |
| 153 | private renderNotifyControl(project: ProjectWatchInfo, key: NotificationKey) { |
| 154 | return html` <td class="notifControl" @click=${this.handleNotifCellClick}> |
| 155 | <input |
| 156 | type="checkbox" |
| 157 | data-key=${key} |
| 158 | @change=${(e: Event) => this.handleCheckboxChange(project, key, e)} |
| 159 | ?checked=${!!project[key]} |
| 160 | /> |
| 161 | </td>`; |
Dmitrii Filippov | 3fd2b10 | 2019-11-15 16:16:46 +0100 | [diff] [blame] | 162 | } |
| 163 | |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 164 | loadData() { |
Ben Rohlfs | 43935a4 | 2020-12-01 19:14:09 +0100 | [diff] [blame] | 165 | return this.restApiService.getWatchedProjects().then(projs => { |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 166 | this.projects = projs; |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 167 | }); |
| 168 | } |
| 169 | |
| 170 | save() { |
Ben Rohlfs | ed6e6f4 | 2020-12-07 10:50:04 +0100 | [diff] [blame] | 171 | let deletePromise: Promise<Response | undefined>; |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 172 | if (this.projectsToRemove.length) { |
Ben Rohlfs | 43935a4 | 2020-12-01 19:14:09 +0100 | [diff] [blame] | 173 | deletePromise = this.restApiService.deleteWatchedProjects( |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 174 | this.projectsToRemove |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 175 | ); |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 176 | } else { |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 177 | deletePromise = Promise.resolve(undefined); |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 178 | } |
| 179 | |
| 180 | return deletePromise |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 181 | .then(() => { |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 182 | if (this.projects) { |
| 183 | return this.restApiService.saveWatchedProjects(this.projects); |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 184 | } else { |
| 185 | return Promise.resolve(undefined); |
| 186 | } |
| 187 | }) |
| 188 | .then(projects => { |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 189 | this.projects = projects; |
| 190 | this.projectsToRemove = []; |
| 191 | this.setHasUnsavedChanges(false); |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 192 | }); |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 193 | } |
| 194 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 195 | // private but used in tests. |
| 196 | getProjectSuggestions(input: string) { |
Kamil Musin | c39f438 | 2022-11-29 17:43:40 +0100 | [diff] [blame] | 197 | return this.restApiService |
| 198 | .getSuggestedRepos(input, /* n=*/ undefined, throwingErrorCallback) |
| 199 | .then(response => { |
| 200 | const repos: AutocompleteSuggestion[] = []; |
| 201 | for (const [name, repo] of Object.entries(response ?? {})) { |
| 202 | repos.push({name, value: repo.id}); |
| 203 | } |
| 204 | return repos; |
| 205 | }); |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 206 | } |
| 207 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 208 | private handleRemoveProject(project: ProjectWatchInfo) { |
| 209 | if (!this.projects) return; |
| 210 | const index = this.projects.indexOf(project); |
| 211 | if (index < 0) return; |
| 212 | this.projects.splice(index, 1); |
| 213 | this.projectsToRemove.push(project); |
| 214 | this.requestUpdate(); |
| 215 | this.setHasUnsavedChanges(true); |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 216 | } |
| 217 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 218 | // private but used in tests. |
| 219 | canAddProject( |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 220 | project: string | null, |
| 221 | text: string | null, |
| 222 | filter: string | null |
| 223 | ) { |
| 224 | if (project === null && text === null) { |
| 225 | return false; |
| 226 | } |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 227 | |
| 228 | // This will only be used if not using the auto complete |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 229 | if (!project && text) { |
| 230 | return true; |
| 231 | } |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 232 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 233 | if (!this.projects) return true; |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 234 | // Check if the project with filter is already in the list. |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 235 | for (let i = 0; i < this.projects.length; i++) { |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 236 | if ( |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 237 | this.projects[i].project === project && |
| 238 | this.areFiltersEqual(this.projects[i].filter, filter) |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 239 | ) { |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 240 | return false; |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | return true; |
| 245 | } |
| 246 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 247 | // private but used in tests. |
| 248 | getNewProjectIndex(name: string, filter: string | null) { |
| 249 | if (!this.projects) return; |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 250 | let i; |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 251 | for (i = 0; i < this.projects.length; i++) { |
| 252 | const projectFilter = this.projects[i].filter; |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 253 | if ( |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 254 | this.projects[i].project > name || |
| 255 | (this.projects[i].project === name && |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 256 | this.isFilterDefined(projectFilter) && |
| 257 | this.isFilterDefined(filter) && |
| 258 | projectFilter! > filter!) |
| 259 | ) { |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 260 | break; |
| 261 | } |
| 262 | } |
| 263 | return i; |
| 264 | } |
| 265 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 266 | // Private but used in tests. |
| 267 | handleAddProject() { |
| 268 | assertIsDefined(this.newProject, 'newProject'); |
| 269 | assertIsDefined(this.newFilter, 'newFilter'); |
| 270 | const newProject = this.newProject.value; |
| 271 | const newProjectName = this.newProject.text as RepoName; |
| 272 | const filter = this.newFilter.value; |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 273 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 274 | if (!this.canAddProject(newProject, newProjectName, filter)) { |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 275 | return; |
| 276 | } |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 277 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 278 | const insertIndex = this.getNewProjectIndex(newProjectName, filter); |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 279 | |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 280 | if (insertIndex !== undefined) { |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 281 | this.projects?.splice(insertIndex, 0, { |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 282 | project: newProjectName, |
| 283 | filter, |
| 284 | _is_local: true, |
| 285 | }); |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 286 | this.requestUpdate(); |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 287 | } |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 288 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 289 | this.newProject.clear(); |
| 290 | this.newFilter.value = ''; |
| 291 | this.setHasUnsavedChanges(true); |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 292 | } |
| 293 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 294 | private handleCheckboxChange( |
| 295 | project: ProjectWatchInfo, |
| 296 | key: NotificationKey, |
| 297 | e: Event |
| 298 | ) { |
| 299 | const el = e.target as HTMLInputElement; |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 300 | const checked = el.checked; |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 301 | project[key] = !!checked; |
| 302 | this.requestUpdate(); |
| 303 | this.setHasUnsavedChanges(true); |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 304 | } |
| 305 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 306 | private handleNotifCellClick(e: Event) { |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 307 | if (e.target === null) return; |
| 308 | const checkbox = (e.target as HTMLElement).querySelector('input'); |
| 309 | if (checkbox) { |
| 310 | checkbox.click(); |
| 311 | } |
| 312 | } |
| 313 | |
Chris Poucet | 38d5343 | 2022-04-07 13:02:19 +0200 | [diff] [blame] | 314 | private setHasUnsavedChanges(value: boolean) { |
| 315 | this.hasUnsavedChanges = value; |
| 316 | fire(this, 'has-unsaved-changes-changed', {value}); |
| 317 | } |
| 318 | |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 319 | isFilterDefined(filter: string | null | undefined) { |
| 320 | return filter !== null && filter !== undefined; |
| 321 | } |
| 322 | |
| 323 | areFiltersEqual( |
| 324 | filter1: string | null | undefined, |
| 325 | filter2: string | null | undefined |
| 326 | ) { |
| 327 | // null and undefined are equal |
| 328 | if (!this.isFilterDefined(filter1) && !this.isFilterDefined(filter2)) { |
| 329 | return true; |
| 330 | } |
| 331 | return filter1 === filter2; |
Dmitrii Filippov | daf0ec9 | 2020-03-17 11:27:28 +0100 | [diff] [blame] | 332 | } |
| 333 | } |
| 334 | |
Milutin Kristofic | 77b774e | 2020-08-25 14:30:26 +0200 | [diff] [blame] | 335 | declare global { |
| 336 | interface HTMLElementTagNameMap { |
| 337 | 'gr-watched-projects-editor': GrWatchedProjectsEditor; |
| 338 | } |
| 339 | } |