blob: 52e0b3fd9a9e94512e31733abdc620cce52cc81f [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';
42import {ValueChangedEvent} from '../../../types/events';
Frank Borden42c1a452022-08-11 16:27:20 +020043import {ifDefined} from 'lit/directives/if-defined.js';
Ben Rohlfsaa533902022-09-22 09:07:12 +020044import {resolve} from '../../../models/dependency';
45import {createChangeUrl} from '../../../models/views/change';
Kamil Musin12755c42022-11-29 17:11:43 +010046import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
Ben Rohlfs678e19d2023-01-13 14:26:14 +000047import {createRepoUrl, RepoDetailView} from '../../../models/views/repo';
Becky Siegel148c7b22018-01-16 15:01:58 -080048
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010049const NOTHING_TO_SAVE = 'No changes to save.';
Becky Siegel8d7b6272018-03-28 09:38:29 -070050
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010051const MAX_AUTOCOMPLETE_RESULTS = 50;
Becky Siegel148c7b22018-01-16 15:01:58 -080052
Paladox nonea6c05892021-11-23 22:19:27 +000053declare global {
Paladox nonea6c05892021-11-23 22:19:27 +000054 interface HTMLElementTagNameMap {
55 'gr-repo-access': GrRepoAccess;
56 }
57}
58
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010059/**
60 * Fired when save is a no-op
61 *
62 * @event show-alert
63 */
Dmitrii Filippov460685c22020-08-21 11:12:49 +020064@customElement('gr-repo-access')
Paladox nonea6c05892021-11-23 22:19:27 +000065export class GrRepoAccess extends LitElement {
66 @query('gr-access-section:last-of-type') accessSection?: GrAccessSection;
Becky Siegel9640eb22017-12-11 15:58:57 -080067
Paladox nonea6c05892021-11-23 22:19:27 +000068 @property({type: String})
Dmitrii Filippov460685c22020-08-21 11:12:49 +020069 repo?: RepoName;
Becky Siegel9640eb22017-12-11 15:58:57 -080070
Paladox nonea6c05892021-11-23 22:19:27 +000071 // private but used in test
72 @state() canUpload?: boolean = false; // restAPI can return undefined
Becky Siegel9640eb22017-12-11 15:58:57 -080073
Paladox nonea6c05892021-11-23 22:19:27 +000074 // private but used in test
75 @state() inheritFromFilter?: RepoName;
Becky Siegel9640eb22017-12-11 15:58:57 -080076
Paladox nonea6c05892021-11-23 22:19:27 +000077 // private but used in test
78 @state() ownerOf?: GitRef[];
Becky Siegel6db432f2017-08-25 09:17:42 -070079
Paladox nonea6c05892021-11-23 22:19:27 +000080 // private but used in test
81 @state() capabilities?: CapabilityInfoMap;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010082
Paladox nonea6c05892021-11-23 22:19:27 +000083 // private but used in test
Ben Rohlfsbfc688b2022-10-21 12:38:37 +020084 @state() groups?: RepoAccessGroups;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010085
Paladox nonea6c05892021-11-23 22:19:27 +000086 // private but used in test
87 @state() inheritsFrom?: ProjectInfo;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010088
Paladox nonea6c05892021-11-23 22:19:27 +000089 // private but used in test
90 @state() labels?: LabelNameToLabelTypeInfoMap;
Dmitrii Filippov460685c22020-08-21 11:12:49 +020091
Paladox nonea6c05892021-11-23 22:19:27 +000092 // private but used in test
93 @state() local?: EditableLocalAccessSectionInfo;
Dmitrii Filippov460685c22020-08-21 11:12:49 +020094
Paladox nonea6c05892021-11-23 22:19:27 +000095 // private but used in test
96 @state() editing = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +020097
Paladox nonea6c05892021-11-23 22:19:27 +000098 // private but used in test
99 @state() modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200100
Paladox nonea6c05892021-11-23 22:19:27 +0000101 // private but used in test
102 @state() sections?: PermissionAccessSection[];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200103
Paladox nonea6c05892021-11-23 22:19:27 +0000104 @state() private weblinks?: WebLinkInfo[];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200105
Paladox nonea6c05892021-11-23 22:19:27 +0000106 // private but used in test
107 @state() loading = true;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200108
Paladox noneeb72adf2021-11-23 16:14:51 +0000109 // private but used in the tests
110 originalInheritsFrom?: ProjectInfo;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200111
Paladox nonea6c05892021-11-23 22:19:27 +0000112 private readonly query: AutocompleteQuery;
113
Chris Poucetc6e880b2021-11-15 19:57:06 +0100114 private readonly restApiService = getAppContext().restApiService;
Ben Rohlfs43935a42020-12-01 19:14:09 +0100115
Ben Rohlfsaa533902022-09-22 09:07:12 +0200116 private readonly getNavigation = resolve(this, navigationToken);
117
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200118 constructor() {
119 super();
Paladox nonea6c05892021-11-23 22:19:27 +0000120 this.query = () => this.getInheritFromSuggestions();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200121 this.addEventListener('access-modified', () =>
122 this._handleAccessModified()
123 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100124 }
Becky Siegel6db432f2017-08-25 09:17:42 -0700125
Paladox nonea6c05892021-11-23 22:19:27 +0000126 static override get styles() {
127 return [
128 fontStyles,
129 menuPageStyles,
130 subpageStyles,
131 sharedStyles,
132 css`
133 gr-button,
134 #inheritsFrom,
135 #editInheritFromInput,
136 .editing #inheritFromName,
137 .weblinks,
138 .editing .invisible {
139 display: none;
140 }
141 #inheritsFrom.show {
142 display: flex;
143 min-height: 2em;
144 align-items: center;
145 }
146 .weblink {
147 margin-right: var(--spacing-xs);
148 }
149 gr-access-section {
150 margin-top: var(--spacing-l);
151 }
152 .weblinks.show,
153 .referenceContainer {
154 display: block;
155 }
156 .rightsText {
157 margin-right: var(--spacing-s);
158 }
159
160 .editing gr-button,
161 .admin #editBtn {
162 display: inline-block;
163 margin: var(--spacing-l) 0;
164 }
165 .editing #editInheritFromInput {
166 display: inline-block;
167 }
168 `,
169 ];
170 }
171
172 override render() {
173 return html`
174 <div class="main ${this.computeMainClass()}">
175 <div id="loading" class=${this.loading ? 'loading' : ''}>
176 Loading...
177 </div>
178 <div id="loadedContent" class=${this.loading ? 'loading' : ''}>
179 <h3
180 id="inheritsFrom"
181 class="heading-3 ${this.editing || this.inheritsFrom?.id?.length
182 ? 'show'
183 : ''}"
184 >
185 <span class="rightsText">Rights Inherit From</span>
186 <a
187 id="inheritFromName"
188 href=${this.computeParentHref()}
189 rel="noopener"
190 >
191 ${this.inheritsFrom?.name}</a
192 >
193 <gr-autocomplete
194 id="editInheritFromInput"
195 .text=${this.inheritFromFilter}
196 .query=${this.query}
197 @commit=${(e: ValueChangedEvent) => {
198 this.handleUpdateInheritFrom(e);
199 }}
200 @bind-value-changed=${(e: ValueChangedEvent) => {
201 this.handleUpdateInheritFrom(e);
202 }}
203 @text-changed=${(e: ValueChangedEvent) => {
204 this.handleEditInheritFromTextChanged(e);
205 }}
206 ></gr-autocomplete>
207 </h3>
208 <div class="weblinks ${this.weblinks?.length ? 'show' : ''}">
209 History:
210 ${this.weblinks?.map(webLink => this.renderWebLinks(webLink))}
211 </div>
212 ${this.sections?.map((section, index) =>
213 this.renderPermissionSections(section, index)
214 )}
215 <div class="referenceContainer">
216 <gr-button
217 id="addReferenceBtn"
218 @click=${() => this.handleCreateSection()}
219 >Add Reference</gr-button
220 >
221 </div>
222 <div>
223 <gr-button
224 id="editBtn"
225 @click=${() => {
226 this.handleEdit();
227 }}
228 >${this.editing ? 'Cancel' : 'Edit'}</gr-button
229 >
230 <gr-button
231 id="saveBtn"
232 class=${this.ownerOf && this.ownerOf.length === 0
233 ? 'invisible'
234 : ''}
235 primary
236 ?disabled=${!this.modified}
237 @click=${this.handleSave}
238 >Save</gr-button
239 >
240 <gr-button
241 id="saveReviewBtn"
242 class=${!this.canUpload ? 'invisible' : ''}
243 primary
244 ?disabled=${!this.modified}
245 @click=${this.handleSaveForReview}
246 >Save for review</gr-button
247 >
248 </div>
249 </div>
250 </div>
251 `;
252 }
253
254 private renderWebLinks(webLink: WebLinkInfo) {
255 return html`
256 <a
257 class="weblink"
258 href=${webLink.url}
259 rel="noopener"
260 target=${ifDefined(webLink.target)}
261 >
262 ${webLink.name}
263 </a>
264 `;
265 }
266
267 private renderPermissionSections(
268 section: PermissionAccessSection,
269 index: number
270 ) {
271 return html`
272 <gr-access-section
273 .capabilities=${this.capabilities}
274 .section=${section}
275 .labels=${this.labels}
276 .canUpload=${this.canUpload}
277 .editing=${this.editing}
278 .ownerOf=${this.ownerOf}
279 .groups=${this.groups}
280 .repo=${this.repo}
281 @added-section-removed=${() => {
282 this.handleAddedSectionRemoved(index);
283 }}
284 @section-changed=${(e: ValueChangedEvent<PermissionAccessSection>) => {
285 this.handleAccessSectionChanged(e, index);
286 }}
287 ></gr-access-section>
288 `;
289 }
290
291 override willUpdate(changedProperties: PropertyValues) {
292 if (changedProperties.has('repo')) {
293 this._repoChanged(this.repo);
294 }
295
296 if (changedProperties.has('editing')) {
297 this.handleEditingChanged(changedProperties.get('editing') as boolean);
298 this.requestUpdate();
299 }
300 }
301
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100302 _handleAccessModified() {
Paladox nonea6c05892021-11-23 22:19:27 +0000303 this.modified = true;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100304 }
Becky Siegel9640eb22017-12-11 15:58:57 -0800305
Paladox noneeb72adf2021-11-23 16:14:51 +0000306 _repoChanged(repo?: RepoName) {
Paladox nonea6c05892021-11-23 22:19:27 +0000307 this.loading = true;
Becky Siegel9640eb22017-12-11 15:58:57 -0800308
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200309 if (!repo) {
310 return Promise.resolve();
311 }
Paladox none685117922018-03-17 20:05:21 +0000312
Paladox nonea6c05892021-11-23 22:19:27 +0000313 return this.reload(repo);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100314 }
Paladox none70cb10c2018-02-17 19:12:09 +0000315
Paladox nonea6c05892021-11-23 22:19:27 +0000316 private reload(repo: RepoName) {
Dhruv Srivastavab0131f92020-11-24 09:31:54 +0100317 const errFn = (response?: Response | null) => {
Ben Rohlfsa76c82f2021-01-22 22:22:32 +0100318 firePageError(response);
Dhruv Srivastavab0131f92020-11-24 09:31:54 +0100319 };
Paladox none70cb10c2018-02-17 19:12:09 +0000320
Paladox nonea6c05892021-11-23 22:19:27 +0000321 this.editing = false;
Paladox none70cb10c2018-02-17 19:12:09 +0000322
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200323 // Always reset sections when a repo changes.
Paladox nonea6c05892021-11-23 22:19:27 +0000324 this.sections = [];
Ben Rohlfs43935a42020-12-01 19:14:09 +0100325 const sectionsPromises = this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200326 .getRepoAccessRights(repo, errFn)
327 .then(res => {
328 if (!res) {
329 return Promise.resolve(undefined);
330 }
Becky Siegel6af63252018-04-26 14:02:36 -0700331
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200332 // Keep a copy of the original inherit from values separate from
333 // the ones data bound to gr-autocomplete, so the original value
334 // can be restored if the user cancels.
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200335 if (res.inherits_from) {
Paladox nonea6c05892021-11-23 22:19:27 +0000336 this.inheritsFrom = {...res.inherits_from};
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200337 this.originalInheritsFrom = {...res.inherits_from};
338 } else {
Paladox nonea6c05892021-11-23 22:19:27 +0000339 this.inheritsFrom = undefined;
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200340 this.originalInheritsFrom = undefined;
341 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200342 // Initialize the filter value so when the user clicks edit, the
343 // current value appears. If there is no parent repo, it is
344 // initialized as an empty string.
Paladox nonea6c05892021-11-23 22:19:27 +0000345 this.inheritFromFilter = res.inherits_from
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200346 ? res.inherits_from.name
347 : ('' as RepoName);
348 // 'as EditableLocalAccessSectionInfo' is required because res.local
349 // type doesn't have index signature
Paladox nonea6c05892021-11-23 22:19:27 +0000350 this.local = res.local as EditableLocalAccessSectionInfo;
351 this.groups = res.groups;
352 this.weblinks = res.config_web_links || [];
353 this.canUpload = res.can_upload;
354 this.ownerOf = res.owner_of || [];
355 return toSortedPermissionsArray(this.local);
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200356 });
Becky Siegel6af63252018-04-26 14:02:36 -0700357
Ben Rohlfs43935a42020-12-01 19:14:09 +0100358 const capabilitiesPromises = this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200359 .getCapabilities(errFn)
360 .then(res => {
361 if (!res) {
362 return Promise.resolve(undefined);
363 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100364
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200365 return res;
366 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100367
Ben Rohlfs43935a42020-12-01 19:14:09 +0100368 const labelsPromises = this.restApiService
369 .getRepo(repo, errFn)
370 .then(res => {
371 if (!res) {
372 return Promise.resolve(undefined);
373 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100374
Ben Rohlfs43935a42020-12-01 19:14:09 +0100375 return res.labels;
376 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100377
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200378 return Promise.all([
379 sectionsPromises,
380 capabilitiesPromises,
381 labelsPromises,
382 ]).then(([sections, capabilities, labels]) => {
Paladox nonea6c05892021-11-23 22:19:27 +0000383 this.capabilities = capabilities;
384 this.labels = labels;
385 this.sections = sections;
386 this.loading = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100387 });
388 }
389
Paladox nonea6c05892021-11-23 22:19:27 +0000390 // private but used in test
391 handleUpdateInheritFrom(e: ValueChangedEvent) {
392 this.inheritsFrom = {
393 ...(this.inheritsFrom ?? {}),
Dhruv Srivastava71768182021-06-18 15:59:08 +0200394 id: e.detail.value as UrlEncodedRepoName,
Paladox nonea6c05892021-11-23 22:19:27 +0000395 name: this.inheritFromFilter,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200396 };
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100397 this._handleAccessModified();
398 }
399
Paladox nonea6c05892021-11-23 22:19:27 +0000400 private getInheritFromSuggestions(): Promise<AutocompleteSuggestion[]> {
Ben Rohlfs43935a42020-12-01 19:14:09 +0100401 return this.restApiService
Kamil Musin12755c42022-11-29 17:11:43 +0100402 .getRepos(
403 this.inheritFromFilter,
404 MAX_AUTOCOMPLETE_RESULTS,
405 /* offset=*/ undefined,
406 throwingErrorCallback
407 )
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200408 .then(response => {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200409 const repos: AutocompleteSuggestion[] = [];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200410 if (!response) {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200411 return repos;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200412 }
413 for (const item of response) {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200414 repos.push({
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200415 name: item.name,
416 value: item.id,
417 });
418 }
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200419 return repos;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200420 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100421 }
422
Paladox nonea6c05892021-11-23 22:19:27 +0000423 private handleEdit() {
424 this.editing = !this.editing;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100425 }
426
Dhruv Srivastava0d133952022-08-23 10:41:45 +0200427 // private but used in tests
428 handleAddedSectionRemoved(index: number) {
Paladox nonea6c05892021-11-23 22:19:27 +0000429 if (!this.sections) return;
Dhruv Srivastava0d133952022-08-23 10:41:45 +0200430 assertIsDefined(this.local, 'local');
431 delete this.local[this.sections[index].id];
Paladox nonea6c05892021-11-23 22:19:27 +0000432 this.sections = this.sections
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200433 .slice(0, index)
Paladox nonea6c05892021-11-23 22:19:27 +0000434 .concat(this.sections.slice(index + 1, this.sections.length));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100435 }
436
Paladox nonea6c05892021-11-23 22:19:27 +0000437 private handleEditingChanged(editingOld: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100438 // Ignore when editing gets set initially.
Paladox nonea6c05892021-11-23 22:19:27 +0000439 if (!editingOld || this.editing) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200440 return;
441 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100442 // Remove any unsaved but added refs.
Paladox nonea6c05892021-11-23 22:19:27 +0000443 if (this.sections) {
444 this.sections = this.sections.filter(p => !p.value.added);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100445 }
446 // Restore inheritFrom.
Paladox nonea6c05892021-11-23 22:19:27 +0000447 if (this.inheritsFrom) {
448 this.inheritsFrom = this.originalInheritsFrom
Dmitrii Filippovaab98252021-04-06 19:04:11 +0200449 ? {...this.originalInheritsFrom}
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200450 : undefined;
Paladox nonea6c05892021-11-23 22:19:27 +0000451 this.inheritFromFilter = this.originalInheritsFrom?.name;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200452 }
Paladox nonea6c05892021-11-23 22:19:27 +0000453 if (!this.local) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200454 return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100455 }
Paladox nonea6c05892021-11-23 22:19:27 +0000456 for (const key of Object.keys(this.local)) {
457 if (this.local[key].added) {
458 delete this.local[key];
Tao Zhou704cfe732020-03-12 08:32:46 +0100459 }
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100460 }
461 }
462
Paladox nonea6c05892021-11-23 22:19:27 +0000463 private updateRemoveObj(
464 addRemoveObj: {remove: PropertyTreeNode},
465 path: string[]
466 ) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200467 let curPos: PropertyTreeNode = addRemoveObj.remove;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100468 for (const item of path) {
469 if (!curPos[item]) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200470 if (item === path[path.length - 1]) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100471 if (path[path.length - 2] === 'permissions') {
472 curPos[item] = {rules: {}};
473 } else if (path.length === 1) {
474 curPos[item] = {permissions: {}};
475 } else {
476 curPos[item] = {};
477 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100478 } else {
479 curPos[item] = {};
480 }
481 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200482 // The last item can be a PrimitiveValue, but we don't use it
483 // All intermediate items are PropertyTreeNode
484 // TODO(TS): rewrite this loop and process the last item explicitly
485 curPos = curPos[item] as PropertyTreeNode;
486 }
487 return addRemoveObj;
488 }
489
Paladox nonea6c05892021-11-23 22:19:27 +0000490 private updateAddObj(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200491 addRemoveObj: {add: PropertyTreeNode},
492 path: string[],
493 value: PropertyTreeNode | PrimitiveValue
494 ) {
495 let curPos: PropertyTreeNode = addRemoveObj.add;
496 for (const item of path) {
497 if (!curPos[item]) {
498 if (item === path[path.length - 1]) {
499 curPos[item] = value;
500 } else {
501 curPos[item] = {};
502 }
503 }
504 // The last item can be a PrimitiveValue, but we don't use it
505 // All intermediate items are PropertyTreeNode
506 // TODO(TS): rewrite this loop and process the last item explicitly
507 curPos = curPos[item] as PropertyTreeNode;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100508 }
509 return addRemoveObj;
510 }
511
512 /**
513 * Used to recursively remove any objects with a 'deleted' bit.
Paladox nonea6c05892021-11-23 22:19:27 +0000514 *
515 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100516 */
Paladox nonea6c05892021-11-23 22:19:27 +0000517 recursivelyRemoveDeleted(obj?: PropertyTreeNode) {
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100518 if (!obj) return;
519 for (const k of Object.keys(obj)) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200520 const node = obj[k];
521 if (typeof node === 'object') {
522 if (node.deleted) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100523 delete obj[k];
524 return;
525 }
Paladox nonea6c05892021-11-23 22:19:27 +0000526 this.recursivelyRemoveDeleted(node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100527 }
528 }
529 }
530
Paladox nonea6c05892021-11-23 22:19:27 +0000531 // private but used in test
532 recursivelyUpdateAddRemoveObj(
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100533 obj: PropertyTreeNode | undefined,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200534 addRemoveObj: {
535 add: PropertyTreeNode;
536 remove: PropertyTreeNode;
537 },
538 path: string[] = []
539 ) {
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100540 if (!obj) return;
541 for (const k of Object.keys(obj)) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200542 const node = obj[k];
543 if (typeof node === 'object') {
544 const updatedId = node.updatedId;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100545 const ref = updatedId ? updatedId : k;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200546 if (node.deleted) {
Paladox nonea6c05892021-11-23 22:19:27 +0000547 this.updateRemoveObj(addRemoveObj, path.concat(k));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100548 continue;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200549 } else if (node.modified) {
Paladox nonea6c05892021-11-23 22:19:27 +0000550 this.updateRemoveObj(addRemoveObj, path.concat(k));
551 this.updateAddObj(addRemoveObj, path.concat(ref), node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100552 /* Special case for ref changes because they need to be added and
Paladox nonea6c05892021-11-23 22:19:27 +0000553 removed in a different way. The new ref needs to include all
554 changes but also the initial state. To do this, instead of
555 continuing with the same recursion, just remove anything that is
556 deleted in the current state. */
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100557 if (updatedId && updatedId !== k) {
Paladox nonea6c05892021-11-23 22:19:27 +0000558 this.recursivelyRemoveDeleted(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200559 addRemoveObj.add[updatedId] as PropertyTreeNode
560 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100561 }
562 continue;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200563 } else if (node.added) {
Paladox nonea6c05892021-11-23 22:19:27 +0000564 this.updateAddObj(addRemoveObj, path.concat(ref), node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100565 /**
566 * As add / delete both can happen in the new section,
567 * so here to make sure it will remove the deleted ones.
568 *
569 * @see Issue 11339
570 */
Paladox nonea6c05892021-11-23 22:19:27 +0000571 this.recursivelyRemoveDeleted(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200572 addRemoveObj.add[k] as PropertyTreeNode
573 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100574 continue;
575 }
Paladox nonea6c05892021-11-23 22:19:27 +0000576 this.recursivelyUpdateAddRemoveObj(node, addRemoveObj, path.concat(k));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100577 }
578 }
579 }
580
581 /**
582 * Returns an object formatted for saving or submitting access changes for
583 * review
Paladox nonea6c05892021-11-23 22:19:27 +0000584 *
585 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100586 */
Paladox nonea6c05892021-11-23 22:19:27 +0000587 computeAddAndRemove() {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200588 const addRemoveObj: {
589 add: PropertyTreeNode;
590 remove: PropertyTreeNode;
591 parent?: string | null;
592 } = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100593 add: {},
594 remove: {},
595 };
596
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100597 const originalInheritsFromId = this.originalInheritsFrom
598 ? singleDecodeURL(this.originalInheritsFrom.id)
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200599 : undefined;
Paladox nonea6c05892021-11-23 22:19:27 +0000600 const inheritsFromId = this.inheritsFrom
601 ? singleDecodeURL(this.inheritsFrom.id)
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200602 : undefined;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100603
604 const inheritFromChanged =
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200605 // Inherit from changed
606 (originalInheritsFromId && originalInheritsFromId !== inheritsFromId) ||
607 // Inherit from added (did not have one initially);
608 (!originalInheritsFromId && inheritsFromId);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100609
Paladox nonea6c05892021-11-23 22:19:27 +0000610 if (!this.local) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200611 return addRemoveObj;
612 }
613
Paladox nonea6c05892021-11-23 22:19:27 +0000614 this.recursivelyUpdateAddRemoveObj(
615 this.local as unknown as PropertyTreeNode,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200616 addRemoveObj
617 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100618
619 if (inheritFromChanged) {
620 addRemoveObj.parent = inheritsFromId;
621 }
622 return addRemoveObj;
623 }
624
Ben Rohlfs53953e12022-05-04 11:35:46 +0200625 private handleCreateSection() {
Paladox nonea6c05892021-11-23 22:19:27 +0000626 if (!this.local) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100627 let newRef = 'refs/for/*';
628 // Avoid using an already used key for the placeholder, since it
629 // immediately gets added to an object.
Paladox nonea6c05892021-11-23 22:19:27 +0000630 while (this.local[newRef]) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100631 newRef = `${newRef}*`;
632 }
633 const section = {permissions: {}, added: true};
Paladox nonea6c05892021-11-23 22:19:27 +0000634 this.sections!.push({id: newRef as GitRef, value: section});
635 this.local[newRef] = section;
636 this.requestUpdate();
637 assertIsDefined(this.accessSection, 'accessSection');
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200638 // Template already instantiated at this point
Paladox nonea6c05892021-11-23 22:19:27 +0000639 this.accessSection.editReference();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100640 }
641
Paladox nonea6c05892021-11-23 22:19:27 +0000642 private getObjforSave(): ProjectAccessInput | undefined {
643 const addRemoveObj = this.computeAddAndRemove();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100644 // If there are no changes, don't actually save.
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200645 if (
646 !Object.keys(addRemoveObj.add).length &&
647 !Object.keys(addRemoveObj.remove).length &&
648 !addRemoveObj.parent
649 ) {
Milutin Kristofic860fe4d2020-11-23 16:13:45 +0100650 fireAlert(this, NOTHING_TO_SAVE);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100651 return;
652 }
Chris Poucetcaeea1b2021-08-19 22:12:56 +0000653 const obj: ProjectAccessInput = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100654 add: addRemoveObj.add,
655 remove: addRemoveObj.remove,
Chris Poucetcaeea1b2021-08-19 22:12:56 +0000656 } as unknown as ProjectAccessInput;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100657 if (addRemoveObj.parent) {
658 obj.parent = addRemoveObj.parent;
659 }
660 return obj;
661 }
662
Paladox nonea6c05892021-11-23 22:19:27 +0000663 // private but used in test
664 handleSave(e: Event) {
665 const obj = this.getObjforSave();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200666 if (!obj) {
667 return;
668 }
669 const button = e && (e.target as GrButton);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100670 if (button) {
671 button.loading = true;
672 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200673 const repo = this.repo;
674 if (!repo) {
675 return Promise.resolve();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100676 }
Ben Rohlfs43935a42020-12-01 19:14:09 +0100677 return this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200678 .setRepoAccessRights(repo, obj)
679 .then(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000680 this.reload(repo);
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200681 })
682 .finally(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000683 this.modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200684 if (button) {
685 button.loading = false;
686 }
687 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100688 }
689
Paladox nonea6c05892021-11-23 22:19:27 +0000690 // private but used in test
691 handleSaveForReview(e: Event) {
692 const obj = this.getObjforSave();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200693 if (!obj) {
694 return;
695 }
696 const button = e && (e.target as GrButton);
697 if (button) {
698 button.loading = true;
699 }
700 if (!this.repo) {
701 return;
702 }
Ben Rohlfs43935a42020-12-01 19:14:09 +0100703 return this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200704 .setRepoAccessRightsForReview(this.repo, obj)
705 .then(change => {
Ben Rohlfsaa533902022-09-22 09:07:12 +0200706 this.getNavigation().setUrl(createChangeUrl({change}));
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200707 })
708 .finally(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000709 this.modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200710 if (button) {
711 button.loading = false;
712 }
713 });
714 }
715
Paladox nonea6c05892021-11-23 22:19:27 +0000716 // private but used in test
717 computeMainClass() {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100718 const classList = [];
Paladox nonea6c05892021-11-23 22:19:27 +0000719 if ((this.ownerOf && this.ownerOf.length > 0) || this.canUpload) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100720 classList.push('admin');
721 }
Paladox nonea6c05892021-11-23 22:19:27 +0000722 if (this.editing) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100723 classList.push('editing');
724 }
725 return classList.join(' ');
726 }
727
Paladox nonea6c05892021-11-23 22:19:27 +0000728 computeParentHref() {
729 if (!this.inheritsFrom?.name) return '';
Ben Rohlfs678e19d2023-01-13 14:26:14 +0000730 return createRepoUrl({
731 repo: this.inheritsFrom.name,
732 detail: RepoDetailView.ACCESS,
733 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100734 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100735
Paladox nonea6c05892021-11-23 22:19:27 +0000736 private handleEditInheritFromTextChanged(e: ValueChangedEvent) {
737 this.inheritFromFilter = e.detail.value as RepoName;
738 }
739
740 private handleAccessSectionChanged(
741 e: ValueChangedEvent<PermissionAccessSection>,
742 index: number
743 ) {
744 this.sections![index] = e.detail.value;
745 this.requestUpdate();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200746 }
747}