blob: 36b85073a129c0a8c62a48503c1d051bd6d87322 [file] [log] [blame]
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04001/**
2 * @license
Ben Rohlfs94fcbbc2022-05-27 10:45:03 +02003 * Copyright 2017 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
Dave Borowitz8cdc76b2018-03-26 10:04:27 -04005 */
Dmitrii Filippov460685c22020-08-21 11:12:49 +02006import '../gr-access-section/gr-access-section';
Ben Rohlfs678e19d2023-01-13 14:26:14 +00007import {singleDecodeURL} from '../../../utils/url-util';
Ben Rohlfsaa533902022-09-22 09:07:12 +02008import {navigationToken} from '../../core/gr-navigation/gr-navigation';
Dmitrii Filippov460685c22020-08-21 11:12:49 +02009import {toSortedPermissionsArray} from '../../../utils/access-util';
Dmitrii Filippove3c09ae2020-07-10 11:39:50 +020010import {
Dmitrii Filippov460685c22020-08-21 11:12:49 +020011 RepoName,
12 ProjectInfo,
13 CapabilityInfoMap,
14 LabelNameToLabelTypeInfoMap,
15 ProjectAccessInput,
16 GitRef,
17 UrlEncodedRepoName,
Ben Rohlfsbfc688b2022-10-21 12:38:37 +020018 RepoAccessGroups,
Dmitrii Filippov460685c22020-08-21 11:12:49 +020019} from '../../../types/common';
Dmitrii Filippov460685c22020-08-21 11:12:49 +020020import {GrButton} from '../../shared/gr-button/gr-button';
21import {GrAccessSection} from '../gr-access-section/gr-access-section';
22import {
23 AutocompleteQuery,
24 AutocompleteSuggestion,
25} from '../../shared/gr-autocomplete/gr-autocomplete';
26import {
27 EditableLocalAccessSectionInfo,
28 PermissionAccessSection,
29 PropertyTreeNode,
30 PrimitiveValue,
31} from './gr-repo-access-interfaces';
Milutin Kristofic860fe4d2020-11-23 16:13:45 +010032import {firePageError, fireAlert} from '../../../utils/event-util';
Chris Poucetc6e880b2021-11-15 19:57:06 +010033import {getAppContext} from '../../../services/app-context';
Dhruv Srivastavad4880e32021-01-29 13:42:58 +010034import {WebLinkInfo} from '../../../types/diff';
Paladox nonea6c05892021-11-23 22:19:27 +000035import {fontStyles} from '../../../styles/gr-font-styles';
36import {menuPageStyles} from '../../../styles/gr-menu-page-styles';
37import {subpageStyles} from '../../../styles/gr-subpage-styles';
38import {sharedStyles} from '../../../styles/shared-styles';
39import {LitElement, PropertyValues, css, html} from 'lit';
Frank Borden42c1a452022-08-11 16:27:20 +020040import {customElement, property, query, state} from 'lit/decorators.js';
Paladox nonea6c05892021-11-23 22:19:27 +000041import {assertIsDefined} from '../../../utils/common-util';
Dhruv Srivastava25e53d82023-02-28 19:13:19 +010042import {
43 AutocompleteCommitEvent,
44 ValueChangedEvent,
45} from '../../../types/events';
Frank Borden42c1a452022-08-11 16:27:20 +020046import {ifDefined} from 'lit/directives/if-defined.js';
Ben Rohlfsaa533902022-09-22 09:07:12 +020047import {resolve} from '../../../models/dependency';
48import {createChangeUrl} from '../../../models/views/change';
Kamil Musin12755c42022-11-29 17:11:43 +010049import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
Ben Rohlfs678e19d2023-01-13 14:26:14 +000050import {createRepoUrl, RepoDetailView} from '../../../models/views/repo';
Becky Siegel148c7b22018-01-16 15:01:58 -080051
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010052const NOTHING_TO_SAVE = 'No changes to save.';
Becky Siegel8d7b6272018-03-28 09:38:29 -070053
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010054const MAX_AUTOCOMPLETE_RESULTS = 50;
Becky Siegel148c7b22018-01-16 15:01:58 -080055
Paladox nonea6c05892021-11-23 22:19:27 +000056declare global {
Paladox nonea6c05892021-11-23 22:19:27 +000057 interface HTMLElementTagNameMap {
58 'gr-repo-access': GrRepoAccess;
59 }
60}
61
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010062/**
63 * Fired when save is a no-op
64 *
65 * @event show-alert
66 */
Dmitrii Filippov460685c22020-08-21 11:12:49 +020067@customElement('gr-repo-access')
Paladox nonea6c05892021-11-23 22:19:27 +000068export class GrRepoAccess extends LitElement {
69 @query('gr-access-section:last-of-type') accessSection?: GrAccessSection;
Becky Siegel9640eb22017-12-11 15:58:57 -080070
Paladox nonea6c05892021-11-23 22:19:27 +000071 @property({type: String})
Dmitrii Filippov460685c22020-08-21 11:12:49 +020072 repo?: RepoName;
Becky Siegel9640eb22017-12-11 15:58:57 -080073
Paladox nonea6c05892021-11-23 22:19:27 +000074 // private but used in test
75 @state() canUpload?: boolean = false; // restAPI can return undefined
Becky Siegel9640eb22017-12-11 15:58:57 -080076
Paladox nonea6c05892021-11-23 22:19:27 +000077 // private but used in test
78 @state() inheritFromFilter?: RepoName;
Becky Siegel9640eb22017-12-11 15:58:57 -080079
Paladox nonea6c05892021-11-23 22:19:27 +000080 // private but used in test
81 @state() ownerOf?: GitRef[];
Becky Siegel6db432f2017-08-25 09:17:42 -070082
Paladox nonea6c05892021-11-23 22:19:27 +000083 // private but used in test
84 @state() capabilities?: CapabilityInfoMap;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010085
Paladox nonea6c05892021-11-23 22:19:27 +000086 // private but used in test
Ben Rohlfsbfc688b2022-10-21 12:38:37 +020087 @state() groups?: RepoAccessGroups;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010088
Paladox nonea6c05892021-11-23 22:19:27 +000089 // private but used in test
90 @state() inheritsFrom?: ProjectInfo;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010091
Paladox nonea6c05892021-11-23 22:19:27 +000092 // private but used in test
93 @state() labels?: LabelNameToLabelTypeInfoMap;
Dmitrii Filippov460685c22020-08-21 11:12:49 +020094
Paladox nonea6c05892021-11-23 22:19:27 +000095 // private but used in test
96 @state() local?: EditableLocalAccessSectionInfo;
Dmitrii Filippov460685c22020-08-21 11:12:49 +020097
Paladox nonea6c05892021-11-23 22:19:27 +000098 // private but used in test
99 @state() editing = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200100
Paladox nonea6c05892021-11-23 22:19:27 +0000101 // private but used in test
102 @state() modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200103
Paladox nonea6c05892021-11-23 22:19:27 +0000104 // private but used in test
105 @state() sections?: PermissionAccessSection[];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200106
Paladox nonea6c05892021-11-23 22:19:27 +0000107 @state() private weblinks?: WebLinkInfo[];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200108
Paladox nonea6c05892021-11-23 22:19:27 +0000109 // private but used in test
110 @state() loading = true;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200111
Paladox noneeb72adf2021-11-23 16:14:51 +0000112 // private but used in the tests
113 originalInheritsFrom?: ProjectInfo;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200114
Paladox nonea6c05892021-11-23 22:19:27 +0000115 private readonly query: AutocompleteQuery;
116
Chris Poucetc6e880b2021-11-15 19:57:06 +0100117 private readonly restApiService = getAppContext().restApiService;
Ben Rohlfs43935a42020-12-01 19:14:09 +0100118
Ben Rohlfsaa533902022-09-22 09:07:12 +0200119 private readonly getNavigation = resolve(this, navigationToken);
120
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200121 constructor() {
122 super();
Paladox nonea6c05892021-11-23 22:19:27 +0000123 this.query = () => this.getInheritFromSuggestions();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200124 this.addEventListener('access-modified', () =>
125 this._handleAccessModified()
126 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100127 }
Becky Siegel6db432f2017-08-25 09:17:42 -0700128
Paladox nonea6c05892021-11-23 22:19:27 +0000129 static override get styles() {
130 return [
131 fontStyles,
132 menuPageStyles,
133 subpageStyles,
134 sharedStyles,
135 css`
136 gr-button,
137 #inheritsFrom,
138 #editInheritFromInput,
139 .editing #inheritFromName,
140 .weblinks,
141 .editing .invisible {
142 display: none;
143 }
144 #inheritsFrom.show {
145 display: flex;
146 min-height: 2em;
147 align-items: center;
148 }
149 .weblink {
150 margin-right: var(--spacing-xs);
151 }
152 gr-access-section {
153 margin-top: var(--spacing-l);
154 }
155 .weblinks.show,
156 .referenceContainer {
157 display: block;
158 }
159 .rightsText {
160 margin-right: var(--spacing-s);
161 }
162
163 .editing gr-button,
164 .admin #editBtn {
165 display: inline-block;
166 margin: var(--spacing-l) 0;
167 }
168 .editing #editInheritFromInput {
169 display: inline-block;
170 }
171 `,
172 ];
173 }
174
175 override render() {
176 return html`
177 <div class="main ${this.computeMainClass()}">
178 <div id="loading" class=${this.loading ? 'loading' : ''}>
179 Loading...
180 </div>
181 <div id="loadedContent" class=${this.loading ? 'loading' : ''}>
182 <h3
183 id="inheritsFrom"
184 class="heading-3 ${this.editing || this.inheritsFrom?.id?.length
185 ? 'show'
186 : ''}"
187 >
188 <span class="rightsText">Rights Inherit From</span>
189 <a
190 id="inheritFromName"
191 href=${this.computeParentHref()}
192 rel="noopener"
193 >
194 ${this.inheritsFrom?.name}</a
195 >
196 <gr-autocomplete
197 id="editInheritFromInput"
198 .text=${this.inheritFromFilter}
199 .query=${this.query}
Dhruv Srivastava46db10c2023-02-17 18:37:37 +0100200 @commit=${(e: AutocompleteCommitEvent) => {
Paladox nonea6c05892021-11-23 22:19:27 +0000201 this.handleUpdateInheritFrom(e);
202 }}
203 @bind-value-changed=${(e: ValueChangedEvent) => {
204 this.handleUpdateInheritFrom(e);
205 }}
206 @text-changed=${(e: ValueChangedEvent) => {
207 this.handleEditInheritFromTextChanged(e);
208 }}
209 ></gr-autocomplete>
210 </h3>
211 <div class="weblinks ${this.weblinks?.length ? 'show' : ''}">
212 History:
213 ${this.weblinks?.map(webLink => this.renderWebLinks(webLink))}
214 </div>
215 ${this.sections?.map((section, index) =>
216 this.renderPermissionSections(section, index)
217 )}
218 <div class="referenceContainer">
219 <gr-button
220 id="addReferenceBtn"
221 @click=${() => this.handleCreateSection()}
222 >Add Reference</gr-button
223 >
224 </div>
225 <div>
226 <gr-button
227 id="editBtn"
228 @click=${() => {
229 this.handleEdit();
230 }}
231 >${this.editing ? 'Cancel' : 'Edit'}</gr-button
232 >
233 <gr-button
234 id="saveBtn"
235 class=${this.ownerOf && this.ownerOf.length === 0
236 ? 'invisible'
237 : ''}
238 primary
239 ?disabled=${!this.modified}
240 @click=${this.handleSave}
241 >Save</gr-button
242 >
243 <gr-button
244 id="saveReviewBtn"
245 class=${!this.canUpload ? 'invisible' : ''}
246 primary
247 ?disabled=${!this.modified}
248 @click=${this.handleSaveForReview}
249 >Save for review</gr-button
250 >
251 </div>
252 </div>
253 </div>
254 `;
255 }
256
257 private renderWebLinks(webLink: WebLinkInfo) {
258 return html`
259 <a
260 class="weblink"
261 href=${webLink.url}
262 rel="noopener"
263 target=${ifDefined(webLink.target)}
264 >
265 ${webLink.name}
266 </a>
267 `;
268 }
269
270 private renderPermissionSections(
271 section: PermissionAccessSection,
272 index: number
273 ) {
274 return html`
275 <gr-access-section
276 .capabilities=${this.capabilities}
277 .section=${section}
278 .labels=${this.labels}
279 .canUpload=${this.canUpload}
280 .editing=${this.editing}
281 .ownerOf=${this.ownerOf}
282 .groups=${this.groups}
283 .repo=${this.repo}
284 @added-section-removed=${() => {
285 this.handleAddedSectionRemoved(index);
286 }}
287 @section-changed=${(e: ValueChangedEvent<PermissionAccessSection>) => {
288 this.handleAccessSectionChanged(e, index);
289 }}
290 ></gr-access-section>
291 `;
292 }
293
294 override willUpdate(changedProperties: PropertyValues) {
295 if (changedProperties.has('repo')) {
296 this._repoChanged(this.repo);
297 }
298
299 if (changedProperties.has('editing')) {
300 this.handleEditingChanged(changedProperties.get('editing') as boolean);
301 this.requestUpdate();
302 }
303 }
304
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100305 _handleAccessModified() {
Paladox nonea6c05892021-11-23 22:19:27 +0000306 this.modified = true;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100307 }
Becky Siegel9640eb22017-12-11 15:58:57 -0800308
Paladox noneeb72adf2021-11-23 16:14:51 +0000309 _repoChanged(repo?: RepoName) {
Paladox nonea6c05892021-11-23 22:19:27 +0000310 this.loading = true;
Becky Siegel9640eb22017-12-11 15:58:57 -0800311
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200312 if (!repo) {
313 return Promise.resolve();
314 }
Paladox none685117922018-03-17 20:05:21 +0000315
Paladox nonea6c05892021-11-23 22:19:27 +0000316 return this.reload(repo);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100317 }
Paladox none70cb10c2018-02-17 19:12:09 +0000318
Paladox nonea6c05892021-11-23 22:19:27 +0000319 private reload(repo: RepoName) {
Dhruv Srivastavab0131f92020-11-24 09:31:54 +0100320 const errFn = (response?: Response | null) => {
Ben Rohlfsa76c82f2021-01-22 22:22:32 +0100321 firePageError(response);
Dhruv Srivastavab0131f92020-11-24 09:31:54 +0100322 };
Paladox none70cb10c2018-02-17 19:12:09 +0000323
Paladox nonea6c05892021-11-23 22:19:27 +0000324 this.editing = false;
Paladox none70cb10c2018-02-17 19:12:09 +0000325
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200326 // Always reset sections when a repo changes.
Paladox nonea6c05892021-11-23 22:19:27 +0000327 this.sections = [];
Ben Rohlfs43935a42020-12-01 19:14:09 +0100328 const sectionsPromises = this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200329 .getRepoAccessRights(repo, errFn)
330 .then(res => {
331 if (!res) {
332 return Promise.resolve(undefined);
333 }
Becky Siegel6af63252018-04-26 14:02:36 -0700334
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200335 // Keep a copy of the original inherit from values separate from
336 // the ones data bound to gr-autocomplete, so the original value
337 // can be restored if the user cancels.
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200338 if (res.inherits_from) {
Paladox nonea6c05892021-11-23 22:19:27 +0000339 this.inheritsFrom = {...res.inherits_from};
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200340 this.originalInheritsFrom = {...res.inherits_from};
341 } else {
Paladox nonea6c05892021-11-23 22:19:27 +0000342 this.inheritsFrom = undefined;
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200343 this.originalInheritsFrom = undefined;
344 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200345 // Initialize the filter value so when the user clicks edit, the
346 // current value appears. If there is no parent repo, it is
347 // initialized as an empty string.
Paladox nonea6c05892021-11-23 22:19:27 +0000348 this.inheritFromFilter = res.inherits_from
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200349 ? res.inherits_from.name
350 : ('' as RepoName);
351 // 'as EditableLocalAccessSectionInfo' is required because res.local
352 // type doesn't have index signature
Paladox nonea6c05892021-11-23 22:19:27 +0000353 this.local = res.local as EditableLocalAccessSectionInfo;
354 this.groups = res.groups;
355 this.weblinks = res.config_web_links || [];
356 this.canUpload = res.can_upload;
357 this.ownerOf = res.owner_of || [];
358 return toSortedPermissionsArray(this.local);
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200359 });
Becky Siegel6af63252018-04-26 14:02:36 -0700360
Ben Rohlfs43935a42020-12-01 19:14:09 +0100361 const capabilitiesPromises = this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200362 .getCapabilities(errFn)
363 .then(res => {
364 if (!res) {
365 return Promise.resolve(undefined);
366 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100367
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200368 return res;
369 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100370
Ben Rohlfs43935a42020-12-01 19:14:09 +0100371 const labelsPromises = this.restApiService
372 .getRepo(repo, errFn)
373 .then(res => {
374 if (!res) {
375 return Promise.resolve(undefined);
376 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100377
Ben Rohlfs43935a42020-12-01 19:14:09 +0100378 return res.labels;
379 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100380
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200381 return Promise.all([
382 sectionsPromises,
383 capabilitiesPromises,
384 labelsPromises,
385 ]).then(([sections, capabilities, labels]) => {
Paladox nonea6c05892021-11-23 22:19:27 +0000386 this.capabilities = capabilities;
387 this.labels = labels;
388 this.sections = sections;
389 this.loading = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100390 });
391 }
392
Paladox nonea6c05892021-11-23 22:19:27 +0000393 // private but used in test
Dhruv Srivastava46db10c2023-02-17 18:37:37 +0100394 handleUpdateInheritFrom(e: AutocompleteCommitEvent) {
Paladox nonea6c05892021-11-23 22:19:27 +0000395 this.inheritsFrom = {
396 ...(this.inheritsFrom ?? {}),
Dhruv Srivastava71768182021-06-18 15:59:08 +0200397 id: e.detail.value as UrlEncodedRepoName,
Paladox nonea6c05892021-11-23 22:19:27 +0000398 name: this.inheritFromFilter,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200399 };
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100400 this._handleAccessModified();
401 }
402
Paladox nonea6c05892021-11-23 22:19:27 +0000403 private getInheritFromSuggestions(): Promise<AutocompleteSuggestion[]> {
Ben Rohlfs43935a42020-12-01 19:14:09 +0100404 return this.restApiService
Kamil Musin12755c42022-11-29 17:11:43 +0100405 .getRepos(
406 this.inheritFromFilter,
407 MAX_AUTOCOMPLETE_RESULTS,
408 /* offset=*/ undefined,
409 throwingErrorCallback
410 )
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200411 .then(response => {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200412 const repos: AutocompleteSuggestion[] = [];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200413 if (!response) {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200414 return repos;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200415 }
416 for (const item of response) {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200417 repos.push({
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200418 name: item.name,
419 value: item.id,
420 });
421 }
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200422 return repos;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200423 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100424 }
425
Paladox nonea6c05892021-11-23 22:19:27 +0000426 private handleEdit() {
427 this.editing = !this.editing;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100428 }
429
Dhruv Srivastava0d133952022-08-23 10:41:45 +0200430 // private but used in tests
431 handleAddedSectionRemoved(index: number) {
Paladox nonea6c05892021-11-23 22:19:27 +0000432 if (!this.sections) return;
Dhruv Srivastava0d133952022-08-23 10:41:45 +0200433 assertIsDefined(this.local, 'local');
434 delete this.local[this.sections[index].id];
Paladox nonea6c05892021-11-23 22:19:27 +0000435 this.sections = this.sections
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200436 .slice(0, index)
Paladox nonea6c05892021-11-23 22:19:27 +0000437 .concat(this.sections.slice(index + 1, this.sections.length));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100438 }
439
Paladox nonea6c05892021-11-23 22:19:27 +0000440 private handleEditingChanged(editingOld: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100441 // Ignore when editing gets set initially.
Paladox nonea6c05892021-11-23 22:19:27 +0000442 if (!editingOld || this.editing) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200443 return;
444 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100445 // Remove any unsaved but added refs.
Paladox nonea6c05892021-11-23 22:19:27 +0000446 if (this.sections) {
447 this.sections = this.sections.filter(p => !p.value.added);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100448 }
449 // Restore inheritFrom.
Paladox nonea6c05892021-11-23 22:19:27 +0000450 if (this.inheritsFrom) {
451 this.inheritsFrom = this.originalInheritsFrom
Dmitrii Filippovaab98252021-04-06 19:04:11 +0200452 ? {...this.originalInheritsFrom}
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200453 : undefined;
Paladox nonea6c05892021-11-23 22:19:27 +0000454 this.inheritFromFilter = this.originalInheritsFrom?.name;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200455 }
Paladox nonea6c05892021-11-23 22:19:27 +0000456 if (!this.local) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200457 return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100458 }
Paladox nonea6c05892021-11-23 22:19:27 +0000459 for (const key of Object.keys(this.local)) {
460 if (this.local[key].added) {
461 delete this.local[key];
Tao Zhou704cfe732020-03-12 08:32:46 +0100462 }
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100463 }
464 }
465
Paladox nonea6c05892021-11-23 22:19:27 +0000466 private updateRemoveObj(
467 addRemoveObj: {remove: PropertyTreeNode},
468 path: string[]
469 ) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200470 let curPos: PropertyTreeNode = addRemoveObj.remove;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100471 for (const item of path) {
472 if (!curPos[item]) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200473 if (item === path[path.length - 1]) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100474 if (path[path.length - 2] === 'permissions') {
475 curPos[item] = {rules: {}};
476 } else if (path.length === 1) {
477 curPos[item] = {permissions: {}};
478 } else {
479 curPos[item] = {};
480 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100481 } else {
482 curPos[item] = {};
483 }
484 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200485 // The last item can be a PrimitiveValue, but we don't use it
486 // All intermediate items are PropertyTreeNode
487 // TODO(TS): rewrite this loop and process the last item explicitly
488 curPos = curPos[item] as PropertyTreeNode;
489 }
490 return addRemoveObj;
491 }
492
Paladox nonea6c05892021-11-23 22:19:27 +0000493 private updateAddObj(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200494 addRemoveObj: {add: PropertyTreeNode},
495 path: string[],
496 value: PropertyTreeNode | PrimitiveValue
497 ) {
498 let curPos: PropertyTreeNode = addRemoveObj.add;
499 for (const item of path) {
500 if (!curPos[item]) {
501 if (item === path[path.length - 1]) {
502 curPos[item] = value;
503 } else {
504 curPos[item] = {};
505 }
506 }
507 // The last item can be a PrimitiveValue, but we don't use it
508 // All intermediate items are PropertyTreeNode
509 // TODO(TS): rewrite this loop and process the last item explicitly
510 curPos = curPos[item] as PropertyTreeNode;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100511 }
512 return addRemoveObj;
513 }
514
515 /**
516 * Used to recursively remove any objects with a 'deleted' bit.
Paladox nonea6c05892021-11-23 22:19:27 +0000517 *
518 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100519 */
Paladox nonea6c05892021-11-23 22:19:27 +0000520 recursivelyRemoveDeleted(obj?: PropertyTreeNode) {
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100521 if (!obj) return;
522 for (const k of Object.keys(obj)) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200523 const node = obj[k];
524 if (typeof node === 'object') {
525 if (node.deleted) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100526 delete obj[k];
527 return;
528 }
Paladox nonea6c05892021-11-23 22:19:27 +0000529 this.recursivelyRemoveDeleted(node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100530 }
531 }
532 }
533
Paladox nonea6c05892021-11-23 22:19:27 +0000534 // private but used in test
535 recursivelyUpdateAddRemoveObj(
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100536 obj: PropertyTreeNode | undefined,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200537 addRemoveObj: {
538 add: PropertyTreeNode;
539 remove: PropertyTreeNode;
540 },
541 path: string[] = []
542 ) {
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100543 if (!obj) return;
544 for (const k of Object.keys(obj)) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200545 const node = obj[k];
546 if (typeof node === 'object') {
547 const updatedId = node.updatedId;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100548 const ref = updatedId ? updatedId : k;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200549 if (node.deleted) {
Paladox nonea6c05892021-11-23 22:19:27 +0000550 this.updateRemoveObj(addRemoveObj, path.concat(k));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100551 continue;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200552 } else if (node.modified) {
Paladox nonea6c05892021-11-23 22:19:27 +0000553 this.updateRemoveObj(addRemoveObj, path.concat(k));
554 this.updateAddObj(addRemoveObj, path.concat(ref), node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100555 /* Special case for ref changes because they need to be added and
Paladox nonea6c05892021-11-23 22:19:27 +0000556 removed in a different way. The new ref needs to include all
557 changes but also the initial state. To do this, instead of
558 continuing with the same recursion, just remove anything that is
559 deleted in the current state. */
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100560 if (updatedId && updatedId !== k) {
Paladox nonea6c05892021-11-23 22:19:27 +0000561 this.recursivelyRemoveDeleted(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200562 addRemoveObj.add[updatedId] as PropertyTreeNode
563 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100564 }
565 continue;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200566 } else if (node.added) {
Paladox nonea6c05892021-11-23 22:19:27 +0000567 this.updateAddObj(addRemoveObj, path.concat(ref), node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100568 /**
569 * As add / delete both can happen in the new section,
570 * so here to make sure it will remove the deleted ones.
571 *
572 * @see Issue 11339
573 */
Paladox nonea6c05892021-11-23 22:19:27 +0000574 this.recursivelyRemoveDeleted(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200575 addRemoveObj.add[k] as PropertyTreeNode
576 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100577 continue;
578 }
Paladox nonea6c05892021-11-23 22:19:27 +0000579 this.recursivelyUpdateAddRemoveObj(node, addRemoveObj, path.concat(k));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100580 }
581 }
582 }
583
584 /**
585 * Returns an object formatted for saving or submitting access changes for
586 * review
Paladox nonea6c05892021-11-23 22:19:27 +0000587 *
588 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100589 */
Paladox nonea6c05892021-11-23 22:19:27 +0000590 computeAddAndRemove() {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200591 const addRemoveObj: {
592 add: PropertyTreeNode;
593 remove: PropertyTreeNode;
594 parent?: string | null;
595 } = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100596 add: {},
597 remove: {},
598 };
599
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100600 const originalInheritsFromId = this.originalInheritsFrom
601 ? singleDecodeURL(this.originalInheritsFrom.id)
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200602 : undefined;
Paladox nonea6c05892021-11-23 22:19:27 +0000603 const inheritsFromId = this.inheritsFrom
604 ? singleDecodeURL(this.inheritsFrom.id)
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200605 : undefined;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100606
607 const inheritFromChanged =
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200608 // Inherit from changed
609 (originalInheritsFromId && originalInheritsFromId !== inheritsFromId) ||
610 // Inherit from added (did not have one initially);
611 (!originalInheritsFromId && inheritsFromId);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100612
Paladox nonea6c05892021-11-23 22:19:27 +0000613 if (!this.local) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200614 return addRemoveObj;
615 }
616
Paladox nonea6c05892021-11-23 22:19:27 +0000617 this.recursivelyUpdateAddRemoveObj(
618 this.local as unknown as PropertyTreeNode,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200619 addRemoveObj
620 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100621
622 if (inheritFromChanged) {
623 addRemoveObj.parent = inheritsFromId;
624 }
625 return addRemoveObj;
626 }
627
Ben Rohlfs53953e12022-05-04 11:35:46 +0200628 private handleCreateSection() {
Paladox nonea6c05892021-11-23 22:19:27 +0000629 if (!this.local) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100630 let newRef = 'refs/for/*';
631 // Avoid using an already used key for the placeholder, since it
632 // immediately gets added to an object.
Paladox nonea6c05892021-11-23 22:19:27 +0000633 while (this.local[newRef]) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100634 newRef = `${newRef}*`;
635 }
636 const section = {permissions: {}, added: true};
Paladox nonea6c05892021-11-23 22:19:27 +0000637 this.sections!.push({id: newRef as GitRef, value: section});
638 this.local[newRef] = section;
639 this.requestUpdate();
640 assertIsDefined(this.accessSection, 'accessSection');
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200641 // Template already instantiated at this point
Paladox nonea6c05892021-11-23 22:19:27 +0000642 this.accessSection.editReference();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100643 }
644
Paladox nonea6c05892021-11-23 22:19:27 +0000645 private getObjforSave(): ProjectAccessInput | undefined {
646 const addRemoveObj = this.computeAddAndRemove();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100647 // If there are no changes, don't actually save.
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200648 if (
649 !Object.keys(addRemoveObj.add).length &&
650 !Object.keys(addRemoveObj.remove).length &&
651 !addRemoveObj.parent
652 ) {
Milutin Kristofic860fe4d2020-11-23 16:13:45 +0100653 fireAlert(this, NOTHING_TO_SAVE);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100654 return;
655 }
Chris Poucetcaeea1b2021-08-19 22:12:56 +0000656 const obj: ProjectAccessInput = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100657 add: addRemoveObj.add,
658 remove: addRemoveObj.remove,
Chris Poucetcaeea1b2021-08-19 22:12:56 +0000659 } as unknown as ProjectAccessInput;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100660 if (addRemoveObj.parent) {
661 obj.parent = addRemoveObj.parent;
662 }
663 return obj;
664 }
665
Paladox nonea6c05892021-11-23 22:19:27 +0000666 // private but used in test
667 handleSave(e: Event) {
668 const obj = this.getObjforSave();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200669 if (!obj) {
670 return;
671 }
672 const button = e && (e.target as GrButton);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100673 if (button) {
674 button.loading = true;
675 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200676 const repo = this.repo;
677 if (!repo) {
678 return Promise.resolve();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100679 }
Ben Rohlfs43935a42020-12-01 19:14:09 +0100680 return this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200681 .setRepoAccessRights(repo, obj)
682 .then(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000683 this.reload(repo);
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200684 })
685 .finally(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000686 this.modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200687 if (button) {
688 button.loading = false;
689 }
690 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100691 }
692
Paladox nonea6c05892021-11-23 22:19:27 +0000693 // private but used in test
694 handleSaveForReview(e: Event) {
695 const obj = this.getObjforSave();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200696 if (!obj) {
697 return;
698 }
699 const button = e && (e.target as GrButton);
700 if (button) {
701 button.loading = true;
702 }
703 if (!this.repo) {
704 return;
705 }
Ben Rohlfs43935a42020-12-01 19:14:09 +0100706 return this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200707 .setRepoAccessRightsForReview(this.repo, obj)
708 .then(change => {
Ben Rohlfsaa533902022-09-22 09:07:12 +0200709 this.getNavigation().setUrl(createChangeUrl({change}));
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200710 })
711 .finally(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000712 this.modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200713 if (button) {
714 button.loading = false;
715 }
716 });
717 }
718
Paladox nonea6c05892021-11-23 22:19:27 +0000719 // private but used in test
720 computeMainClass() {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100721 const classList = [];
Paladox nonea6c05892021-11-23 22:19:27 +0000722 if ((this.ownerOf && this.ownerOf.length > 0) || this.canUpload) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100723 classList.push('admin');
724 }
Paladox nonea6c05892021-11-23 22:19:27 +0000725 if (this.editing) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100726 classList.push('editing');
727 }
728 return classList.join(' ');
729 }
730
Paladox nonea6c05892021-11-23 22:19:27 +0000731 computeParentHref() {
732 if (!this.inheritsFrom?.name) return '';
Ben Rohlfs678e19d2023-01-13 14:26:14 +0000733 return createRepoUrl({
734 repo: this.inheritsFrom.name,
735 detail: RepoDetailView.ACCESS,
736 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100737 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100738
Paladox nonea6c05892021-11-23 22:19:27 +0000739 private handleEditInheritFromTextChanged(e: ValueChangedEvent) {
740 this.inheritFromFilter = e.detail.value as RepoName;
741 }
742
743 private handleAccessSectionChanged(
744 e: ValueChangedEvent<PermissionAccessSection>,
745 index: number
746 ) {
747 this.sections![index] = e.detail.value;
748 this.requestUpdate();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200749 }
750}