blob: 28882296e71048fc12e77b9c30a59284a06928af [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 */
Milutin Kristoficccc164e2020-08-13 22:40:40 +02006import '@polymer/iron-autogrow-textarea/iron-autogrow-textarea';
Milutin Kristoficccc164e2020-08-13 22:40:40 +02007import '../../shared/gr-button/gr-button';
8import '../../shared/gr-select/gr-select';
Milutin Kristoficccc164e2020-08-13 22:40:40 +02009import {encodeURL, getBaseUrl} from '../../../utils/url-util';
Milutin Kristoficc1137bb2020-08-27 11:23:48 +020010import {AccessPermissionId} from '../../../utils/access-util';
Ben Rohlfs6bb90532023-02-17 18:55:56 +010011import {fire, fireEvent} from '../../../utils/event-util';
Paladox none83c5fa62021-11-24 22:26:52 +000012import {formStyles} from '../../../styles/gr-form-styles';
13import {sharedStyles} from '../../../styles/shared-styles';
14import {LitElement, PropertyValues, html, css} from 'lit';
Frank Borden42c1a452022-08-11 16:27:20 +020015import {customElement, property, state} from 'lit/decorators.js';
Ben Rohlfs6bb90532023-02-17 18:55:56 +010016import {BindValueChangeEvent, ValueChangedEvent} from '../../../types/events';
Frank Borden42c1a452022-08-11 16:27:20 +020017import {ifDefined} from 'lit/directives/if-defined.js';
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010018import {EditablePermissionRuleInfo} from '../gr-repo-access/gr-repo-access-interfaces';
19import {PermissionAction} from '../../../constants/constants';
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010020
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010021const PRIORITY_OPTIONS = [PermissionAction.BATCH, PermissionAction.INTERACTIVE];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010022
23const Action = {
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010024 ALLOW: PermissionAction.ALLOW,
25 DENY: PermissionAction.DENY,
26 BLOCK: PermissionAction.BLOCK,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010027};
28
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010029const DROPDOWN_OPTIONS = [
30 PermissionAction.ALLOW,
31 PermissionAction.DENY,
32 PermissionAction.BLOCK,
33];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010034
35const ForcePushOptions = {
36 ALLOW: [
37 {name: 'Allow pushing (but not force pushing)', value: false},
38 {name: 'Allow pushing with or without force', value: true},
39 ],
40 BLOCK: [
41 {name: 'Block pushing with or without force', value: false},
42 {name: 'Block force pushing', value: true},
43 ],
44};
45
46const FORCE_EDIT_OPTIONS = [
47 {
48 name: 'No Force Edit',
49 value: false,
50 },
51 {
52 name: 'Force Edit',
53 value: true,
54 },
55];
56
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010057type Rule = {value?: EditablePermissionRuleInfo};
Becky Siegel7c57cf92018-04-23 14:44:32 -070058
Milutin Kristoficccc164e2020-08-13 22:40:40 +020059interface RuleLabel {
60 values: RuleLabelValue[];
61}
Becky Siegel7198afd2017-08-15 17:49:30 -070062
Milutin Kristoficccc164e2020-08-13 22:40:40 +020063interface RuleLabelValue {
64 value: number;
65 text: string;
66}
67
68declare global {
69 interface HTMLElementTagNameMap {
70 'gr-rule-editor': GrRuleEditor;
71 }
Ben Rohlfs6bb90532023-02-17 18:55:56 +010072 interface HTMLElementEventMap {
Ben Rohlfs5b3c6552023-02-18 13:02:46 +010073 /** Fired when a rule that was previously added was removed. */
74 'added-rule-removed': CustomEvent<{}>;
Ben Rohlfs6bb90532023-02-17 18:55:56 +010075 'rule-changed': ValueChangedEvent<Rule | undefined>;
76 }
Milutin Kristoficccc164e2020-08-13 22:40:40 +020077}
78
79@customElement('gr-rule-editor')
Paladox none83c5fa62021-11-24 22:26:52 +000080export class GrRuleEditor extends LitElement {
Milutin Kristoficccc164e2020-08-13 22:40:40 +020081 @property({type: Boolean})
82 hasRange?: boolean;
83
84 @property({type: Object})
85 label?: RuleLabel;
86
Paladox none83c5fa62021-11-24 22:26:52 +000087 @property({type: Boolean})
Milutin Kristoficccc164e2020-08-13 22:40:40 +020088 editing = false;
89
90 @property({type: String})
91 groupId?: string;
92
93 @property({type: String})
94 groupName?: string;
95
96 // This is required value for this component
97 @property({type: String})
Milutin Kristoficc1137bb2020-08-27 11:23:48 +020098 permission!: AccessPermissionId;
Milutin Kristoficccc164e2020-08-13 22:40:40 +020099
Paladox none83c5fa62021-11-24 22:26:52 +0000100 @property({type: Object})
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200101 rule?: Rule;
102
103 @property({type: String})
104 section?: string;
105
Paladox none83c5fa62021-11-24 22:26:52 +0000106 // private but used in test
107 @state() deleted = false;
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200108
Paladox none83c5fa62021-11-24 22:26:52 +0000109 // private but used in test
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100110 @state() originalRuleValues?: EditablePermissionRuleInfo;
Wyatt Allenb38a1be2018-06-27 09:59:04 -0700111
Ben Rohlfsf7f1e8e2021-03-12 14:36:40 +0100112 constructor() {
113 super();
Paladox none83c5fa62021-11-24 22:26:52 +0000114 this.addEventListener('access-saved', () => this.handleAccessSaved());
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100115 }
Becky Siegel7198afd2017-08-15 17:49:30 -0700116
Chris Poucet59dad572021-08-20 15:25:36 +0000117 override connectedCallback() {
Ben Rohlfs5f520da2021-03-10 14:58:43 +0100118 super.connectedCallback();
Paladox none83c5fa62021-11-24 22:26:52 +0000119 if (this.rule) {
120 this.setupValues();
121 }
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200122 // Check needed for test purposes.
Paladox none83c5fa62021-11-24 22:26:52 +0000123 if (!this.originalRuleValues && this.rule) {
124 this.setOriginalRuleValues();
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100125 }
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100126 }
127
Paladox none83c5fa62021-11-24 22:26:52 +0000128 static override get styles() {
129 return [
130 formStyles,
131 sharedStyles,
132 css`
133 :host {
134 border-bottom: 1px solid var(--border-color);
135 padding: var(--spacing-m);
136 display: block;
137 }
138 #removeBtn {
139 display: none;
140 }
141 .editing #removeBtn {
142 display: flex;
143 }
144 #options {
145 align-items: baseline;
146 display: flex;
147 }
148 #options > * {
149 margin-right: var(--spacing-m);
150 }
151 #mainContainer {
152 align-items: baseline;
153 display: flex;
154 flex-wrap: nowrap;
155 justify-content: space-between;
156 }
157 #deletedContainer.deleted {
158 align-items: baseline;
159 display: flex;
160 justify-content: space-between;
161 }
162 #undoBtn,
163 #force,
164 #deletedContainer,
165 #mainContainer.deleted {
166 display: none;
167 }
168 #undoBtn.modified,
169 #force.force {
170 display: block;
171 }
172 .groupPath {
173 color: var(--deemphasized-text-color);
174 }
175 iron-autogrow-textarea {
176 width: 14em;
177 }
178 `,
179 ];
180 }
181
182 override render() {
183 return html`
184 <div
185 id="mainContainer"
186 class="gr-form-styles ${this.computeSectionClass()}"
187 >
188 <div id="options">
189 <gr-select
190 id="action"
191 .bindValue=${this.rule?.value?.action}
192 @bind-value-changed=${(e: BindValueChangeEvent) => {
193 this.handleActionBindValueChanged(e);
194 }}
195 >
196 <select ?disabled=${!this.editing}>
197 ${this.computeOptions().map(
198 item => html` <option value=${item}>${item}</option> `
199 )}
200 </select>
201 </gr-select>
202 ${this.renderMinAndMaxLabel()} ${this.renderMinAndMaxInput()}
Milutin Kristofice0503d52022-01-11 12:17:50 +0100203 <a
204 class="groupPath"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200205 href=${ifDefined(this.computeGroupPath(this.groupId))}
Milutin Kristofice0503d52022-01-11 12:17:50 +0100206 >
Paladox none83c5fa62021-11-24 22:26:52 +0000207 ${this.groupName}
208 </a>
209 <gr-select
210 id="force"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200211 class=${this.computeForce(this.rule?.value?.action) ? 'force' : ''}
Paladox none83c5fa62021-11-24 22:26:52 +0000212 .bindValue=${this.rule?.value?.force}
213 @bind-value-changed=${(e: BindValueChangeEvent) => {
214 this.handleForceBindValueChanged(e);
215 }}
216 >
217 <select ?disabled=${!this.editing}>
218 ${this.computeForceOptions(this.rule?.value?.action).map(
219 item => html`
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100220 <option value=${item.value}>${item.name}</option>
Paladox none83c5fa62021-11-24 22:26:52 +0000221 `
222 )}
223 </select>
224 </gr-select>
225 </div>
226 <gr-button
227 link
228 id="removeBtn"
229 @click=${() => {
230 this.handleRemoveRule();
231 }}
232 >Remove</gr-button
233 >
234 </div>
235 <div
236 id="deletedContainer"
237 class="gr-form-styles ${this.computeSectionClass()}"
238 >
239 ${this.groupName} was deleted
240 <gr-button
241 link
242 id="undoRemoveBtn"
243 @click=${() => {
244 this.handleUndoRemove();
245 }}
246 >Undo</gr-button
247 >
248 </div>
249 `;
250 }
251
252 private renderMinAndMaxLabel() {
253 if (!this.label) return;
254
255 return html`
256 <gr-select
257 id="labelMin"
258 .bindValue=${this.rule?.value?.min}
259 @bind-value-changed=${(e: BindValueChangeEvent) => {
260 this.handleMinBindValueChanged(e);
261 }}
262 >
263 <select ?disabled=${!this.editing}>
264 ${this.label.values.map(
265 item => html` <option value=${item.value}>${item.value}</option> `
266 )}
267 </select>
268 </gr-select>
269 <gr-select
270 id="labelMax"
271 .bindValue=${this.rule?.value?.max}
272 @bind-value-changed=${(e: BindValueChangeEvent) => {
273 this.handleMaxBindValueChanged(e);
274 }}
275 >
276 <select ?disabled=${!this.editing}>
277 ${this.label.values.map(
278 item => html` <option value=${item.value}>${item.value}</option> `
279 )}
280 </select>
281 </gr-select>
282 `;
283 }
284
285 private renderMinAndMaxInput() {
286 if (!this.hasRange) return;
287
288 return html`
289 <iron-autogrow-textarea
290 id="minInput"
291 class="min"
292 autocomplete="on"
293 placeholder="Min value"
294 .bindValue=${this.rule?.value?.min}
295 ?disabled=${!this.editing}
296 @bind-value-changed=${(e: BindValueChangeEvent) => {
297 this.handleMinBindValueChanged(e);
298 }}
299 ></iron-autogrow-textarea>
300 <iron-autogrow-textarea
301 id="maxInput"
302 class="max"
303 autocomplete="on"
304 placeholder="Max value"
305 .bindValue=${this.rule?.value?.max}
306 ?disabled=${!this.editing}
307 @bind-value-changed=${(e: BindValueChangeEvent) => {
308 this.handleMaxBindValueChanged(e);
309 }}
310 ></iron-autogrow-textarea>
311 `;
312 }
313
314 override willUpdate(changedProperties: PropertyValues) {
315 if (changedProperties.has('editing')) {
316 this.handleEditingChanged(changedProperties.get('editing') as boolean);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100317 }
318 }
319
Paladox none83c5fa62021-11-24 22:26:52 +0000320 // private but used in test
321 setupValues() {
322 if (!this.rule?.value) {
323 this.setDefaultRuleValues();
324 }
325 }
326
327 // private but used in test
328 computeForce(action?: string) {
329 if (AccessPermissionId.PUSH === this.permission && action !== Action.DENY) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100330 return true;
331 }
332
Paladox none83c5fa62021-11-24 22:26:52 +0000333 return AccessPermissionId.EDIT_TOPIC_NAME === this.permission;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100334 }
335
Paladox none83c5fa62021-11-24 22:26:52 +0000336 // private but used in test
337 computeGroupPath(groupId?: string) {
338 if (!groupId) return;
Ben Rohlfsc02facb2023-01-27 18:46:02 +0100339 return `${getBaseUrl()}/admin/groups/${encodeURL(groupId)}`;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100340 }
341
Paladox none83c5fa62021-11-24 22:26:52 +0000342 // private but used in test
343 handleAccessSaved() {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100344 // Set a new 'original' value to keep track of after the value has been
345 // saved.
Paladox none83c5fa62021-11-24 22:26:52 +0000346 this.setOriginalRuleValues();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100347 }
348
Paladox none83c5fa62021-11-24 22:26:52 +0000349 private handleEditingChanged(editingOld: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100350 // Ignore when editing gets set initially.
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200351 if (!editingOld) {
352 return;
353 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100354 // Restore original values if no longer editing.
Paladox none83c5fa62021-11-24 22:26:52 +0000355 if (!this.editing) {
356 this.handleUndoChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100357 }
358 }
359
Paladox none83c5fa62021-11-24 22:26:52 +0000360 // private but used in test
361 computeSectionClass() {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100362 const classList = [];
Paladox none83c5fa62021-11-24 22:26:52 +0000363 if (this.editing) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100364 classList.push('editing');
365 }
Paladox none83c5fa62021-11-24 22:26:52 +0000366 if (this.deleted) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100367 classList.push('deleted');
368 }
369 return classList.join(' ');
370 }
371
Paladox none83c5fa62021-11-24 22:26:52 +0000372 // private but used in test
373 computeForceOptions(action?: string) {
374 if (this.permission === AccessPermissionId.PUSH) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100375 if (action === Action.ALLOW) {
376 return ForcePushOptions.ALLOW;
377 } else if (action === Action.BLOCK) {
378 return ForcePushOptions.BLOCK;
379 } else {
380 return [];
381 }
Paladox none83c5fa62021-11-24 22:26:52 +0000382 } else if (this.permission === AccessPermissionId.EDIT_TOPIC_NAME) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100383 return FORCE_EDIT_OPTIONS;
384 }
385 return [];
386 }
387
Paladox none83c5fa62021-11-24 22:26:52 +0000388 // private but used in test
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100389 getDefaultRuleValues(): EditablePermissionRuleInfo {
Paladox none83c5fa62021-11-24 22:26:52 +0000390 if (this.permission === AccessPermissionId.PRIORITY) {
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100391 return {action: PRIORITY_OPTIONS[0]};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100392 }
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100393 if (this.label) {
394 return {
395 action: DROPDOWN_OPTIONS[0],
396 min: this.label.values[0].value,
397 max: this.label.values[this.label.values.length - 1].value,
398 };
399 }
400 if (this.computeForce(Action.ALLOW)) {
401 return {
402 action: DROPDOWN_OPTIONS[0],
403 force: this.computeForceOptions(Action.ALLOW)[0].value,
404 };
405 }
406 return {action: DROPDOWN_OPTIONS[0]};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100407 }
408
Paladox none83c5fa62021-11-24 22:26:52 +0000409 // private but used in test
410 setDefaultRuleValues() {
411 this.rule!.value = this.getDefaultRuleValues();
412
413 this.handleRuleChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100414 }
415
Paladox none83c5fa62021-11-24 22:26:52 +0000416 // private but used in test
417 computeOptions() {
418 if (this.permission === 'priority') {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100419 return PRIORITY_OPTIONS;
420 }
421 return DROPDOWN_OPTIONS;
422 }
423
Paladox none83c5fa62021-11-24 22:26:52 +0000424 private handleRemoveRule() {
Paladox none4c88fce2021-11-24 20:36:22 +0000425 if (!this.rule?.value) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100426 if (this.rule.value.added) {
Ben Rohlfse07f8182020-12-07 10:09:20 +0100427 fireEvent(this, 'added-rule-removed');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100428 }
Paladox none83c5fa62021-11-24 22:26:52 +0000429 this.deleted = true;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100430 this.rule.value.deleted = true;
Paladox none83c5fa62021-11-24 22:26:52 +0000431
432 this.handleRuleChange();
433
Ben Rohlfse07f8182020-12-07 10:09:20 +0100434 fireEvent(this, 'access-modified');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100435 }
436
Paladox none83c5fa62021-11-24 22:26:52 +0000437 private handleUndoRemove() {
Paladox none4c88fce2021-11-24 20:36:22 +0000438 if (!this.rule?.value) return;
Paladox none83c5fa62021-11-24 22:26:52 +0000439 this.deleted = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100440 delete this.rule.value.deleted;
Paladox none83c5fa62021-11-24 22:26:52 +0000441
442 this.handleRuleChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100443 }
444
Paladox none83c5fa62021-11-24 22:26:52 +0000445 private handleUndoChange() {
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100446 if (!this.originalRuleValues || !this.rule?.value) {
447 return;
448 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100449 // gr-permission will take care of removing rules that were added but
450 // unsaved. We need to keep the added bit for the filter.
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200451 if (this.rule.value.added) {
452 return;
453 }
Paladox none83c5fa62021-11-24 22:26:52 +0000454 this.rule.value = {...this.originalRuleValues};
455 this.deleted = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100456 delete this.rule.value.deleted;
457 delete this.rule.value.modified;
Paladox none83c5fa62021-11-24 22:26:52 +0000458
459 this.handleRuleChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100460 }
461
Paladox none83c5fa62021-11-24 22:26:52 +0000462 // private but used in test
463 handleValueChange() {
464 if (!this.originalRuleValues || !this.rule?.value) {
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200465 return;
466 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100467 this.rule.value.modified = true;
Paladox none83c5fa62021-11-24 22:26:52 +0000468
469 this.handleRuleChange();
470
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100471 // Allows overall access page to know a change has been made.
Ben Rohlfse07f8182020-12-07 10:09:20 +0100472 fireEvent(this, 'access-modified');
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100473 }
474
Paladox none83c5fa62021-11-24 22:26:52 +0000475 // private but used in test
476 setOriginalRuleValues() {
477 if (!this.rule?.value) return;
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100478 this.originalRuleValues = {...this.rule.value};
Paladox none83c5fa62021-11-24 22:26:52 +0000479 }
480
481 private handleActionBindValueChanged(e: BindValueChangeEvent) {
482 if (
483 !this.rule?.value ||
484 e.detail.value === undefined ||
485 this.rule.value.action === String(e.detail.value)
486 )
487 return;
488
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100489 this.rule.value.action = String(e.detail.value) as PermissionAction;
Paladox none83c5fa62021-11-24 22:26:52 +0000490
491 this.handleValueChange();
492 }
493
494 private handleMinBindValueChanged(e: BindValueChangeEvent) {
495 if (
496 !this.rule?.value ||
497 e.detail.value === undefined ||
498 this.rule.value.min === Number(e.detail.value)
499 )
500 return;
501 this.rule.value.min = Number(e.detail.value);
502
503 this.handleValueChange();
504 }
505
506 private handleMaxBindValueChanged(e: BindValueChangeEvent) {
507 if (
508 !this.rule?.value ||
509 e.detail.value === undefined ||
510 this.rule.value.max === Number(e.detail.value)
511 )
512 return;
513 this.rule.value.max = Number(e.detail.value);
514
515 this.handleValueChange();
516 }
517
518 private handleForceBindValueChanged(e: BindValueChangeEvent) {
519 const forceValue = String(e.detail.value) === 'true' ? true : false;
520 if (
521 !this.rule?.value ||
522 e.detail.value === undefined ||
523 this.rule.value.force === forceValue
524 )
525 return;
526 this.rule.value.force = forceValue;
527
528 this.handleValueChange();
529 }
530
531 private handleRuleChange() {
532 this.requestUpdate('rule');
Ben Rohlfs6bb90532023-02-17 18:55:56 +0100533 fire(this, 'rule-changed', {value: this.rule});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100534 }
535}