blob: ff87849a977b8596f7fa914630527af9e5b8d5a6 [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';
Ben Rohlfsaa533902022-09-22 09:07:12 +020046import {resolve} from '../../../models/dependency';
47import {createChangeUrl} from '../../../models/views/change';
Kamil Musin12755c42022-11-29 17:11:43 +010048import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
Ben Rohlfs678e19d2023-01-13 14:26:14 +000049import {createRepoUrl, RepoDetailView} from '../../../models/views/repo';
Ben Rohlfs59ee7a22023-03-31 10:09:42 +020050import '../../shared/gr-weblink/gr-weblink';
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
Dmitrii Filippov625fd7e2024-05-17 16:06:23 +020077 @state() disableSaveWithoutReview = true;
78
Paladox nonea6c05892021-11-23 22:19:27 +000079 // private but used in test
80 @state() inheritFromFilter?: RepoName;
Becky Siegel9640eb22017-12-11 15:58:57 -080081
Paladox nonea6c05892021-11-23 22:19:27 +000082 // private but used in test
83 @state() ownerOf?: GitRef[];
Becky Siegel6db432f2017-08-25 09:17:42 -070084
Paladox nonea6c05892021-11-23 22:19:27 +000085 // private but used in test
86 @state() capabilities?: CapabilityInfoMap;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010087
Paladox nonea6c05892021-11-23 22:19:27 +000088 // private but used in test
Ben Rohlfsbfc688b2022-10-21 12:38:37 +020089 @state() groups?: RepoAccessGroups;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010090
Paladox nonea6c05892021-11-23 22:19:27 +000091 // private but used in test
92 @state() inheritsFrom?: ProjectInfo;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010093
Paladox nonea6c05892021-11-23 22:19:27 +000094 // private but used in test
95 @state() labels?: LabelNameToLabelTypeInfoMap;
Dmitrii Filippov460685c22020-08-21 11:12:49 +020096
Paladox nonea6c05892021-11-23 22:19:27 +000097 // private but used in test
98 @state() local?: EditableLocalAccessSectionInfo;
Dmitrii Filippov460685c22020-08-21 11:12:49 +020099
Paladox nonea6c05892021-11-23 22:19:27 +0000100 // private but used in test
101 @state() editing = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200102
Paladox nonea6c05892021-11-23 22:19:27 +0000103 // private but used in test
104 @state() modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200105
Paladox nonea6c05892021-11-23 22:19:27 +0000106 // private but used in test
107 @state() sections?: PermissionAccessSection[];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200108
Paladox nonea6c05892021-11-23 22:19:27 +0000109 @state() private weblinks?: WebLinkInfo[];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200110
Paladox nonea6c05892021-11-23 22:19:27 +0000111 // private but used in test
112 @state() loading = true;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200113
Paladox noneeb72adf2021-11-23 16:14:51 +0000114 // private but used in the tests
115 originalInheritsFrom?: ProjectInfo;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200116
Paladox nonea6c05892021-11-23 22:19:27 +0000117 private readonly query: AutocompleteQuery;
118
Chris Poucetc6e880b2021-11-15 19:57:06 +0100119 private readonly restApiService = getAppContext().restApiService;
Ben Rohlfs43935a42020-12-01 19:14:09 +0100120
Ben Rohlfsaa533902022-09-22 09:07:12 +0200121 private readonly getNavigation = resolve(this, navigationToken);
122
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200123 constructor() {
124 super();
Paladox nonea6c05892021-11-23 22:19:27 +0000125 this.query = () => this.getInheritFromSuggestions();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200126 this.addEventListener('access-modified', () =>
127 this._handleAccessModified()
128 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100129 }
Becky Siegel6db432f2017-08-25 09:17:42 -0700130
Paladox nonea6c05892021-11-23 22:19:27 +0000131 static override get styles() {
132 return [
133 fontStyles,
134 menuPageStyles,
135 subpageStyles,
136 sharedStyles,
137 css`
138 gr-button,
139 #inheritsFrom,
140 #editInheritFromInput,
141 .editing #inheritFromName,
142 .weblinks,
143 .editing .invisible {
144 display: none;
145 }
146 #inheritsFrom.show {
147 display: flex;
148 min-height: 2em;
149 align-items: center;
150 }
Ben Rohlfs59ee7a22023-03-31 10:09:42 +0200151 gr-weblink {
Paladox nonea6c05892021-11-23 22:19:27 +0000152 margin-right: var(--spacing-xs);
153 }
154 gr-access-section {
155 margin-top: var(--spacing-l);
156 }
157 .weblinks.show,
158 .referenceContainer {
159 display: block;
160 }
161 .rightsText {
162 margin-right: var(--spacing-s);
163 }
164
165 .editing gr-button,
166 .admin #editBtn {
167 display: inline-block;
168 margin: var(--spacing-l) 0;
169 }
170 .editing #editInheritFromInput {
171 display: inline-block;
172 }
173 `,
174 ];
175 }
176
177 override render() {
178 return html`
179 <div class="main ${this.computeMainClass()}">
180 <div id="loading" class=${this.loading ? 'loading' : ''}>
181 Loading...
182 </div>
183 <div id="loadedContent" class=${this.loading ? 'loading' : ''}>
184 <h3
185 id="inheritsFrom"
186 class="heading-3 ${this.editing || this.inheritsFrom?.id?.length
187 ? 'show'
188 : ''}"
189 >
190 <span class="rightsText">Rights Inherit From</span>
191 <a
192 id="inheritFromName"
193 href=${this.computeParentHref()}
194 rel="noopener"
195 >
196 ${this.inheritsFrom?.name}</a
197 >
198 <gr-autocomplete
199 id="editInheritFromInput"
200 .text=${this.inheritFromFilter}
201 .query=${this.query}
Dhruv Srivastava46db10c2023-02-17 18:37:37 +0100202 @commit=${(e: AutocompleteCommitEvent) => {
Paladox nonea6c05892021-11-23 22:19:27 +0000203 this.handleUpdateInheritFrom(e);
204 }}
205 @bind-value-changed=${(e: ValueChangedEvent) => {
206 this.handleUpdateInheritFrom(e);
207 }}
208 @text-changed=${(e: ValueChangedEvent) => {
209 this.handleEditInheritFromTextChanged(e);
210 }}
211 ></gr-autocomplete>
212 </h3>
213 <div class="weblinks ${this.weblinks?.length ? 'show' : ''}">
214 History:
Ben Rohlfs59ee7a22023-03-31 10:09:42 +0200215 ${this.weblinks?.map(
216 info => html`<gr-weblink .info=${info}></gr-weblink>`
217 )}
Paladox nonea6c05892021-11-23 22:19:27 +0000218 </div>
219 ${this.sections?.map((section, index) =>
220 this.renderPermissionSections(section, index)
221 )}
222 <div class="referenceContainer">
223 <gr-button
224 id="addReferenceBtn"
225 @click=${() => this.handleCreateSection()}
226 >Add Reference</gr-button
227 >
228 </div>
229 <div>
230 <gr-button
231 id="editBtn"
232 @click=${() => {
233 this.handleEdit();
234 }}
235 >${this.editing ? 'Cancel' : 'Edit'}</gr-button
236 >
237 <gr-button
238 id="saveBtn"
239 class=${this.ownerOf && this.ownerOf.length === 0
240 ? 'invisible'
241 : ''}
242 primary
Dmitrii Filippov625fd7e2024-05-17 16:06:23 +0200243 ?disabled=${!this.modified || this.disableSaveWithoutReview}
Paladox nonea6c05892021-11-23 22:19:27 +0000244 @click=${this.handleSave}
245 >Save</gr-button
246 >
247 <gr-button
248 id="saveReviewBtn"
249 class=${!this.canUpload ? 'invisible' : ''}
250 primary
251 ?disabled=${!this.modified}
252 @click=${this.handleSaveForReview}
253 >Save for review</gr-button
254 >
255 </div>
256 </div>
257 </div>
258 `;
259 }
260
Paladox nonea6c05892021-11-23 22:19:27 +0000261 private renderPermissionSections(
262 section: PermissionAccessSection,
263 index: number
264 ) {
265 return html`
266 <gr-access-section
267 .capabilities=${this.capabilities}
268 .section=${section}
269 .labels=${this.labels}
270 .canUpload=${this.canUpload}
271 .editing=${this.editing}
272 .ownerOf=${this.ownerOf}
273 .groups=${this.groups}
274 .repo=${this.repo}
275 @added-section-removed=${() => {
276 this.handleAddedSectionRemoved(index);
277 }}
278 @section-changed=${(e: ValueChangedEvent<PermissionAccessSection>) => {
279 this.handleAccessSectionChanged(e, index);
280 }}
281 ></gr-access-section>
282 `;
283 }
284
285 override willUpdate(changedProperties: PropertyValues) {
286 if (changedProperties.has('repo')) {
287 this._repoChanged(this.repo);
288 }
289
290 if (changedProperties.has('editing')) {
291 this.handleEditingChanged(changedProperties.get('editing') as boolean);
292 this.requestUpdate();
293 }
294 }
295
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100296 _handleAccessModified() {
Paladox nonea6c05892021-11-23 22:19:27 +0000297 this.modified = true;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100298 }
Becky Siegel9640eb22017-12-11 15:58:57 -0800299
Paladox noneeb72adf2021-11-23 16:14:51 +0000300 _repoChanged(repo?: RepoName) {
Paladox nonea6c05892021-11-23 22:19:27 +0000301 this.loading = true;
Becky Siegel9640eb22017-12-11 15:58:57 -0800302
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200303 if (!repo) {
304 return Promise.resolve();
305 }
Paladox none685117922018-03-17 20:05:21 +0000306
Paladox nonea6c05892021-11-23 22:19:27 +0000307 return this.reload(repo);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100308 }
Paladox none70cb10c2018-02-17 19:12:09 +0000309
Paladox nonea6c05892021-11-23 22:19:27 +0000310 private reload(repo: RepoName) {
Dhruv Srivastavab0131f92020-11-24 09:31:54 +0100311 const errFn = (response?: Response | null) => {
Ben Rohlfsa76c82f2021-01-22 22:22:32 +0100312 firePageError(response);
Dhruv Srivastavab0131f92020-11-24 09:31:54 +0100313 };
Paladox none70cb10c2018-02-17 19:12:09 +0000314
Paladox nonea6c05892021-11-23 22:19:27 +0000315 this.editing = false;
Paladox none70cb10c2018-02-17 19:12:09 +0000316
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200317 // Always reset sections when a repo changes.
Paladox nonea6c05892021-11-23 22:19:27 +0000318 this.sections = [];
Ben Rohlfs43935a42020-12-01 19:14:09 +0100319 const sectionsPromises = this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200320 .getRepoAccessRights(repo, errFn)
321 .then(res => {
322 if (!res) {
323 return Promise.resolve(undefined);
324 }
Becky Siegel6af63252018-04-26 14:02:36 -0700325
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200326 // Keep a copy of the original inherit from values separate from
327 // the ones data bound to gr-autocomplete, so the original value
328 // can be restored if the user cancels.
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200329 if (res.inherits_from) {
Paladox nonea6c05892021-11-23 22:19:27 +0000330 this.inheritsFrom = {...res.inherits_from};
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200331 this.originalInheritsFrom = {...res.inherits_from};
332 } else {
Paladox nonea6c05892021-11-23 22:19:27 +0000333 this.inheritsFrom = undefined;
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200334 this.originalInheritsFrom = undefined;
335 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200336 // Initialize the filter value so when the user clicks edit, the
337 // current value appears. If there is no parent repo, it is
338 // initialized as an empty string.
Paladox nonea6c05892021-11-23 22:19:27 +0000339 this.inheritFromFilter = res.inherits_from
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200340 ? res.inherits_from.name
341 : ('' as RepoName);
342 // 'as EditableLocalAccessSectionInfo' is required because res.local
343 // type doesn't have index signature
Paladox nonea6c05892021-11-23 22:19:27 +0000344 this.local = res.local as EditableLocalAccessSectionInfo;
345 this.groups = res.groups;
346 this.weblinks = res.config_web_links || [];
347 this.canUpload = res.can_upload;
Dmitrii Filippov625fd7e2024-05-17 16:06:23 +0200348 this.disableSaveWithoutReview = !!res.require_change_for_config_update;
Paladox nonea6c05892021-11-23 22:19:27 +0000349 this.ownerOf = res.owner_of || [];
350 return toSortedPermissionsArray(this.local);
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200351 });
Becky Siegel6af63252018-04-26 14:02:36 -0700352
Ben Rohlfs43935a42020-12-01 19:14:09 +0100353 const capabilitiesPromises = this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200354 .getCapabilities(errFn)
355 .then(res => {
356 if (!res) {
357 return Promise.resolve(undefined);
358 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100359
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200360 return res;
361 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100362
Ben Rohlfs43935a42020-12-01 19:14:09 +0100363 const labelsPromises = this.restApiService
364 .getRepo(repo, errFn)
365 .then(res => {
366 if (!res) {
367 return Promise.resolve(undefined);
368 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100369
Ben Rohlfs43935a42020-12-01 19:14:09 +0100370 return res.labels;
371 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100372
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200373 return Promise.all([
374 sectionsPromises,
375 capabilitiesPromises,
376 labelsPromises,
377 ]).then(([sections, capabilities, labels]) => {
Paladox nonea6c05892021-11-23 22:19:27 +0000378 this.capabilities = capabilities;
379 this.labels = labels;
380 this.sections = sections;
381 this.loading = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100382 });
383 }
384
Paladox nonea6c05892021-11-23 22:19:27 +0000385 // private but used in test
Dhruv Srivastava46db10c2023-02-17 18:37:37 +0100386 handleUpdateInheritFrom(e: AutocompleteCommitEvent) {
Paladox nonea6c05892021-11-23 22:19:27 +0000387 this.inheritsFrom = {
388 ...(this.inheritsFrom ?? {}),
Dhruv Srivastava71768182021-06-18 15:59:08 +0200389 id: e.detail.value as UrlEncodedRepoName,
Paladox nonea6c05892021-11-23 22:19:27 +0000390 name: this.inheritFromFilter,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200391 };
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100392 this._handleAccessModified();
393 }
394
Paladox nonea6c05892021-11-23 22:19:27 +0000395 private getInheritFromSuggestions(): Promise<AutocompleteSuggestion[]> {
Ben Rohlfs43935a42020-12-01 19:14:09 +0100396 return this.restApiService
Kamil Musin12755c42022-11-29 17:11:43 +0100397 .getRepos(
398 this.inheritFromFilter,
399 MAX_AUTOCOMPLETE_RESULTS,
400 /* offset=*/ undefined,
401 throwingErrorCallback
402 )
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200403 .then(response => {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200404 const repos: AutocompleteSuggestion[] = [];
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200405 if (!response) {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200406 return repos;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200407 }
408 for (const item of response) {
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200409 repos.push({
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200410 name: item.name,
411 value: item.id,
412 });
413 }
Ben Rohlfsbfc688b2022-10-21 12:38:37 +0200414 return repos;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200415 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100416 }
417
Paladox nonea6c05892021-11-23 22:19:27 +0000418 private handleEdit() {
419 this.editing = !this.editing;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100420 }
421
Dhruv Srivastava0d133952022-08-23 10:41:45 +0200422 // private but used in tests
423 handleAddedSectionRemoved(index: number) {
Paladox nonea6c05892021-11-23 22:19:27 +0000424 if (!this.sections) return;
Dhruv Srivastava0d133952022-08-23 10:41:45 +0200425 assertIsDefined(this.local, 'local');
426 delete this.local[this.sections[index].id];
Paladox nonea6c05892021-11-23 22:19:27 +0000427 this.sections = this.sections
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200428 .slice(0, index)
Paladox nonea6c05892021-11-23 22:19:27 +0000429 .concat(this.sections.slice(index + 1, this.sections.length));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100430 }
431
Paladox nonea6c05892021-11-23 22:19:27 +0000432 private handleEditingChanged(editingOld: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100433 // Ignore when editing gets set initially.
Paladox nonea6c05892021-11-23 22:19:27 +0000434 if (!editingOld || this.editing) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200435 return;
436 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100437 // Remove any unsaved but added refs.
Paladox nonea6c05892021-11-23 22:19:27 +0000438 if (this.sections) {
439 this.sections = this.sections.filter(p => !p.value.added);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100440 }
441 // Restore inheritFrom.
Paladox nonea6c05892021-11-23 22:19:27 +0000442 if (this.inheritsFrom) {
443 this.inheritsFrom = this.originalInheritsFrom
Dmitrii Filippovaab98252021-04-06 19:04:11 +0200444 ? {...this.originalInheritsFrom}
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200445 : undefined;
Paladox nonea6c05892021-11-23 22:19:27 +0000446 this.inheritFromFilter = this.originalInheritsFrom?.name;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200447 }
Paladox nonea6c05892021-11-23 22:19:27 +0000448 if (!this.local) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200449 return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100450 }
Paladox nonea6c05892021-11-23 22:19:27 +0000451 for (const key of Object.keys(this.local)) {
452 if (this.local[key].added) {
453 delete this.local[key];
Tao Zhou704cfe732020-03-12 08:32:46 +0100454 }
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100455 }
456 }
457
Paladox nonea6c05892021-11-23 22:19:27 +0000458 private updateRemoveObj(
459 addRemoveObj: {remove: PropertyTreeNode},
460 path: string[]
461 ) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200462 let curPos: PropertyTreeNode = addRemoveObj.remove;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100463 for (const item of path) {
464 if (!curPos[item]) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200465 if (item === path[path.length - 1]) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100466 if (path[path.length - 2] === 'permissions') {
467 curPos[item] = {rules: {}};
468 } else if (path.length === 1) {
469 curPos[item] = {permissions: {}};
470 } else {
471 curPos[item] = {};
472 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100473 } else {
474 curPos[item] = {};
475 }
476 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200477 // The last item can be a PrimitiveValue, but we don't use it
478 // All intermediate items are PropertyTreeNode
479 // TODO(TS): rewrite this loop and process the last item explicitly
480 curPos = curPos[item] as PropertyTreeNode;
481 }
482 return addRemoveObj;
483 }
484
Paladox nonea6c05892021-11-23 22:19:27 +0000485 private updateAddObj(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200486 addRemoveObj: {add: PropertyTreeNode},
487 path: string[],
488 value: PropertyTreeNode | PrimitiveValue
489 ) {
490 let curPos: PropertyTreeNode = addRemoveObj.add;
491 for (const item of path) {
492 if (!curPos[item]) {
493 if (item === path[path.length - 1]) {
494 curPos[item] = value;
495 } else {
496 curPos[item] = {};
497 }
498 }
499 // The last item can be a PrimitiveValue, but we don't use it
500 // All intermediate items are PropertyTreeNode
501 // TODO(TS): rewrite this loop and process the last item explicitly
502 curPos = curPos[item] as PropertyTreeNode;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100503 }
504 return addRemoveObj;
505 }
506
507 /**
508 * Used to recursively remove any objects with a 'deleted' bit.
Paladox nonea6c05892021-11-23 22:19:27 +0000509 *
510 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100511 */
Paladox nonea6c05892021-11-23 22:19:27 +0000512 recursivelyRemoveDeleted(obj?: PropertyTreeNode) {
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100513 if (!obj) return;
514 for (const k of Object.keys(obj)) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200515 const node = obj[k];
516 if (typeof node === 'object') {
517 if (node.deleted) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100518 delete obj[k];
519 return;
520 }
Paladox nonea6c05892021-11-23 22:19:27 +0000521 this.recursivelyRemoveDeleted(node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100522 }
523 }
524 }
525
Paladox nonea6c05892021-11-23 22:19:27 +0000526 // private but used in test
527 recursivelyUpdateAddRemoveObj(
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100528 obj: PropertyTreeNode | undefined,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200529 addRemoveObj: {
530 add: PropertyTreeNode;
531 remove: PropertyTreeNode;
532 },
533 path: string[] = []
534 ) {
Ben Rohlfs7b71b112021-02-12 10:36:08 +0100535 if (!obj) return;
536 for (const k of Object.keys(obj)) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200537 const node = obj[k];
538 if (typeof node === 'object') {
539 const updatedId = node.updatedId;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100540 const ref = updatedId ? updatedId : k;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200541 if (node.deleted) {
Paladox nonea6c05892021-11-23 22:19:27 +0000542 this.updateRemoveObj(addRemoveObj, path.concat(k));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100543 continue;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200544 } else if (node.modified) {
Paladox nonea6c05892021-11-23 22:19:27 +0000545 this.updateRemoveObj(addRemoveObj, path.concat(k));
546 this.updateAddObj(addRemoveObj, path.concat(ref), node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100547 /* Special case for ref changes because they need to be added and
Paladox nonea6c05892021-11-23 22:19:27 +0000548 removed in a different way. The new ref needs to include all
549 changes but also the initial state. To do this, instead of
550 continuing with the same recursion, just remove anything that is
551 deleted in the current state. */
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100552 if (updatedId && updatedId !== k) {
Paladox nonea6c05892021-11-23 22:19:27 +0000553 this.recursivelyRemoveDeleted(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200554 addRemoveObj.add[updatedId] as PropertyTreeNode
555 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100556 }
557 continue;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200558 } else if (node.added) {
Paladox nonea6c05892021-11-23 22:19:27 +0000559 this.updateAddObj(addRemoveObj, path.concat(ref), node);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100560 /**
561 * As add / delete both can happen in the new section,
562 * so here to make sure it will remove the deleted ones.
563 *
564 * @see Issue 11339
565 */
Paladox nonea6c05892021-11-23 22:19:27 +0000566 this.recursivelyRemoveDeleted(
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200567 addRemoveObj.add[k] as PropertyTreeNode
568 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100569 continue;
570 }
Paladox nonea6c05892021-11-23 22:19:27 +0000571 this.recursivelyUpdateAddRemoveObj(node, addRemoveObj, path.concat(k));
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100572 }
573 }
574 }
575
576 /**
577 * Returns an object formatted for saving or submitting access changes for
578 * review
Paladox nonea6c05892021-11-23 22:19:27 +0000579 *
580 * private but used in test
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100581 */
Paladox nonea6c05892021-11-23 22:19:27 +0000582 computeAddAndRemove() {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200583 const addRemoveObj: {
584 add: PropertyTreeNode;
585 remove: PropertyTreeNode;
586 parent?: string | null;
587 } = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100588 add: {},
589 remove: {},
590 };
591
Dhruv Srivastava7e454cd2021-02-22 15:06:58 +0100592 const originalInheritsFromId = this.originalInheritsFrom
593 ? singleDecodeURL(this.originalInheritsFrom.id)
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200594 : undefined;
Paladox nonea6c05892021-11-23 22:19:27 +0000595 const inheritsFromId = this.inheritsFrom
596 ? singleDecodeURL(this.inheritsFrom.id)
frankborden2@gmail.com4c610db2021-08-12 17:56:01 +0200597 : undefined;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100598
599 const inheritFromChanged =
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200600 // Inherit from changed
601 (originalInheritsFromId && originalInheritsFromId !== inheritsFromId) ||
602 // Inherit from added (did not have one initially);
603 (!originalInheritsFromId && inheritsFromId);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100604
Paladox nonea6c05892021-11-23 22:19:27 +0000605 if (!this.local) {
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200606 return addRemoveObj;
607 }
608
Paladox nonea6c05892021-11-23 22:19:27 +0000609 this.recursivelyUpdateAddRemoveObj(
610 this.local as unknown as PropertyTreeNode,
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200611 addRemoveObj
612 );
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100613
614 if (inheritFromChanged) {
615 addRemoveObj.parent = inheritsFromId;
616 }
617 return addRemoveObj;
618 }
619
Ben Rohlfs53953e12022-05-04 11:35:46 +0200620 private handleCreateSection() {
Paladox nonea6c05892021-11-23 22:19:27 +0000621 if (!this.local) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100622 let newRef = 'refs/for/*';
623 // Avoid using an already used key for the placeholder, since it
624 // immediately gets added to an object.
Paladox nonea6c05892021-11-23 22:19:27 +0000625 while (this.local[newRef]) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100626 newRef = `${newRef}*`;
627 }
628 const section = {permissions: {}, added: true};
Paladox nonea6c05892021-11-23 22:19:27 +0000629 this.sections!.push({id: newRef as GitRef, value: section});
630 this.local[newRef] = section;
631 this.requestUpdate();
632 assertIsDefined(this.accessSection, 'accessSection');
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200633 // Template already instantiated at this point
Paladox nonea6c05892021-11-23 22:19:27 +0000634 this.accessSection.editReference();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100635 }
636
Paladox nonea6c05892021-11-23 22:19:27 +0000637 private getObjforSave(): ProjectAccessInput | undefined {
638 const addRemoveObj = this.computeAddAndRemove();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100639 // If there are no changes, don't actually save.
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200640 if (
641 !Object.keys(addRemoveObj.add).length &&
642 !Object.keys(addRemoveObj.remove).length &&
643 !addRemoveObj.parent
644 ) {
Milutin Kristofic860fe4d2020-11-23 16:13:45 +0100645 fireAlert(this, NOTHING_TO_SAVE);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100646 return;
647 }
Chris Poucetcaeea1b2021-08-19 22:12:56 +0000648 const obj: ProjectAccessInput = {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100649 add: addRemoveObj.add,
650 remove: addRemoveObj.remove,
Chris Poucetcaeea1b2021-08-19 22:12:56 +0000651 } as unknown as ProjectAccessInput;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100652 if (addRemoveObj.parent) {
653 obj.parent = addRemoveObj.parent;
654 }
655 return obj;
656 }
657
Paladox nonea6c05892021-11-23 22:19:27 +0000658 // private but used in test
659 handleSave(e: Event) {
660 const obj = this.getObjforSave();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200661 if (!obj) {
662 return;
663 }
664 const button = e && (e.target as GrButton);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100665 if (button) {
666 button.loading = true;
667 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200668 const repo = this.repo;
669 if (!repo) {
670 return Promise.resolve();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100671 }
Ben Rohlfs43935a42020-12-01 19:14:09 +0100672 return this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200673 .setRepoAccessRights(repo, obj)
674 .then(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000675 this.reload(repo);
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200676 })
677 .finally(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000678 this.modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200679 if (button) {
680 button.loading = false;
681 }
682 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100683 }
684
Paladox nonea6c05892021-11-23 22:19:27 +0000685 // private but used in test
686 handleSaveForReview(e: Event) {
687 const obj = this.getObjforSave();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200688 if (!obj) {
689 return;
690 }
691 const button = e && (e.target as GrButton);
692 if (button) {
693 button.loading = true;
694 }
695 if (!this.repo) {
696 return;
697 }
Ben Rohlfs43935a42020-12-01 19:14:09 +0100698 return this.restApiService
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200699 .setRepoAccessRightsForReview(this.repo, obj)
700 .then(change => {
Kamil Musin10c31de2024-02-08 16:49:38 +0100701 // Don't navigate on server error.
702 if (change) {
703 this.getNavigation().setUrl(createChangeUrl({change}));
704 }
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200705 })
706 .finally(() => {
Paladox nonea6c05892021-11-23 22:19:27 +0000707 this.modified = false;
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200708 if (button) {
709 button.loading = false;
710 }
711 });
712 }
713
Paladox nonea6c05892021-11-23 22:19:27 +0000714 // private but used in test
715 computeMainClass() {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100716 const classList = [];
Paladox nonea6c05892021-11-23 22:19:27 +0000717 if ((this.ownerOf && this.ownerOf.length > 0) || this.canUpload) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100718 classList.push('admin');
719 }
Paladox nonea6c05892021-11-23 22:19:27 +0000720 if (this.editing) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100721 classList.push('editing');
722 }
723 return classList.join(' ');
724 }
725
Paladox nonea6c05892021-11-23 22:19:27 +0000726 computeParentHref() {
727 if (!this.inheritsFrom?.name) return '';
Ben Rohlfs678e19d2023-01-13 14:26:14 +0000728 return createRepoUrl({
729 repo: this.inheritsFrom.name,
730 detail: RepoDetailView.ACCESS,
731 });
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100732 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100733
Paladox nonea6c05892021-11-23 22:19:27 +0000734 private handleEditInheritFromTextChanged(e: ValueChangedEvent) {
735 this.inheritFromFilter = e.detail.value as RepoName;
736 }
737
738 private handleAccessSectionChanged(
739 e: ValueChangedEvent<PermissionAccessSection>,
740 index: number
741 ) {
742 this.sections![index] = e.detail.value;
743 this.requestUpdate();
Dmitrii Filippov460685c22020-08-21 11:12:49 +0200744 }
745}