blob: a45fa33fb7bac680be065a7dc81cd305660031c4 [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 Rohlfs44f01042023-02-18 13:27:57 +010011import {fire} from '../../../utils/event-util';
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +020012import {grFormStyles} from '../../../styles/gr-form-styles';
Paladox none83c5fa62021-11-24 22:26:52 +000013import {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';
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +020020import {formStyles} from '../../../styles/form-styles';
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010021
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010022const PRIORITY_OPTIONS = [PermissionAction.BATCH, PermissionAction.INTERACTIVE];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010023
24const Action = {
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010025 ALLOW: PermissionAction.ALLOW,
26 DENY: PermissionAction.DENY,
27 BLOCK: PermissionAction.BLOCK,
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010028};
29
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010030const DROPDOWN_OPTIONS = [
31 PermissionAction.ALLOW,
32 PermissionAction.DENY,
33 PermissionAction.BLOCK,
34];
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +010035
36const ForcePushOptions = {
37 ALLOW: [
38 {name: 'Allow pushing (but not force pushing)', value: false},
39 {name: 'Allow pushing with or without force', value: true},
40 ],
41 BLOCK: [
42 {name: 'Block pushing with or without force', value: false},
43 {name: 'Block force pushing', value: true},
44 ],
45};
46
47const FORCE_EDIT_OPTIONS = [
48 {
49 name: 'No Force Edit',
50 value: false,
51 },
52 {
53 name: 'Force Edit',
54 value: true,
55 },
56];
57
Ben Rohlfsc3ce1402022-02-22 10:35:34 +010058type Rule = {value?: EditablePermissionRuleInfo};
Becky Siegel7c57cf92018-04-23 14:44:32 -070059
Milutin Kristoficccc164e2020-08-13 22:40:40 +020060interface RuleLabel {
61 values: RuleLabelValue[];
62}
Becky Siegel7198afd2017-08-15 17:49:30 -070063
Milutin Kristoficccc164e2020-08-13 22:40:40 +020064interface RuleLabelValue {
65 value: number;
66 text: string;
67}
68
69declare global {
70 interface HTMLElementTagNameMap {
71 'gr-rule-editor': GrRuleEditor;
72 }
Ben Rohlfs6bb90532023-02-17 18:55:56 +010073 interface HTMLElementEventMap {
Ben Rohlfs5b3c6552023-02-18 13:02:46 +010074 /** Fired when a rule that was previously added was removed. */
75 'added-rule-removed': CustomEvent<{}>;
Ben Rohlfs6bb90532023-02-17 18:55:56 +010076 'rule-changed': ValueChangedEvent<Rule | undefined>;
77 }
Milutin Kristoficccc164e2020-08-13 22:40:40 +020078}
79
80@customElement('gr-rule-editor')
Paladox none83c5fa62021-11-24 22:26:52 +000081export class GrRuleEditor extends LitElement {
Milutin Kristoficccc164e2020-08-13 22:40:40 +020082 @property({type: Boolean})
83 hasRange?: boolean;
84
85 @property({type: Object})
86 label?: RuleLabel;
87
Paladox none83c5fa62021-11-24 22:26:52 +000088 @property({type: Boolean})
Milutin Kristoficccc164e2020-08-13 22:40:40 +020089 editing = false;
90
91 @property({type: String})
92 groupId?: string;
93
94 @property({type: String})
95 groupName?: string;
96
97 // This is required value for this component
98 @property({type: String})
Milutin Kristoficc1137bb2020-08-27 11:23:48 +020099 permission!: AccessPermissionId;
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200100
Paladox none83c5fa62021-11-24 22:26:52 +0000101 @property({type: Object})
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200102 rule?: Rule;
103
104 @property({type: String})
105 section?: string;
106
Paladox none83c5fa62021-11-24 22:26:52 +0000107 // private but used in test
108 @state() deleted = false;
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200109
Paladox none83c5fa62021-11-24 22:26:52 +0000110 // private but used in test
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100111 @state() originalRuleValues?: EditablePermissionRuleInfo;
Wyatt Allenb38a1be2018-06-27 09:59:04 -0700112
Ben Rohlfsf7f1e8e2021-03-12 14:36:40 +0100113 constructor() {
114 super();
Paladox none83c5fa62021-11-24 22:26:52 +0000115 this.addEventListener('access-saved', () => this.handleAccessSaved());
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100116 }
Becky Siegel7198afd2017-08-15 17:49:30 -0700117
Chris Poucet59dad572021-08-20 15:25:36 +0000118 override connectedCallback() {
Ben Rohlfs5f520da2021-03-10 14:58:43 +0100119 super.connectedCallback();
Paladox none83c5fa62021-11-24 22:26:52 +0000120 if (this.rule) {
121 this.setupValues();
122 }
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200123 // Check needed for test purposes.
Paladox none83c5fa62021-11-24 22:26:52 +0000124 if (!this.originalRuleValues && this.rule) {
125 this.setOriginalRuleValues();
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100126 }
Dmitrii Filippov3fd2b102019-11-15 16:16:46 +0100127 }
128
Paladox none83c5fa62021-11-24 22:26:52 +0000129 static override get styles() {
130 return [
Milutin Kristoficaa1c08b2023-09-06 10:34:16 +0200131 grFormStyles,
Paladox none83c5fa62021-11-24 22:26:52 +0000132 formStyles,
133 sharedStyles,
134 css`
135 :host {
136 border-bottom: 1px solid var(--border-color);
137 padding: var(--spacing-m);
138 display: block;
139 }
140 #removeBtn {
141 display: none;
142 }
143 .editing #removeBtn {
144 display: flex;
145 }
146 #options {
147 align-items: baseline;
148 display: flex;
149 }
150 #options > * {
151 margin-right: var(--spacing-m);
152 }
153 #mainContainer {
154 align-items: baseline;
155 display: flex;
156 flex-wrap: nowrap;
157 justify-content: space-between;
158 }
159 #deletedContainer.deleted {
160 align-items: baseline;
161 display: flex;
162 justify-content: space-between;
163 }
164 #undoBtn,
165 #force,
166 #deletedContainer,
167 #mainContainer.deleted {
168 display: none;
169 }
170 #undoBtn.modified,
171 #force.force {
172 display: block;
173 }
174 .groupPath {
175 color: var(--deemphasized-text-color);
176 }
177 iron-autogrow-textarea {
178 width: 14em;
179 }
180 `,
181 ];
182 }
183
184 override render() {
185 return html`
186 <div
187 id="mainContainer"
188 class="gr-form-styles ${this.computeSectionClass()}"
189 >
190 <div id="options">
191 <gr-select
192 id="action"
193 .bindValue=${this.rule?.value?.action}
194 @bind-value-changed=${(e: BindValueChangeEvent) => {
195 this.handleActionBindValueChanged(e);
196 }}
197 >
198 <select ?disabled=${!this.editing}>
199 ${this.computeOptions().map(
200 item => html` <option value=${item}>${item}</option> `
201 )}
202 </select>
203 </gr-select>
204 ${this.renderMinAndMaxLabel()} ${this.renderMinAndMaxInput()}
Milutin Kristofice0503d52022-01-11 12:17:50 +0100205 <a
206 class="groupPath"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200207 href=${ifDefined(this.computeGroupPath(this.groupId))}
Milutin Kristofice0503d52022-01-11 12:17:50 +0100208 >
Paladox none83c5fa62021-11-24 22:26:52 +0000209 ${this.groupName}
210 </a>
211 <gr-select
212 id="force"
Ben Rohlfs8003bd32022-04-05 18:24:42 +0200213 class=${this.computeForce(this.rule?.value?.action) ? 'force' : ''}
Paladox none83c5fa62021-11-24 22:26:52 +0000214 .bindValue=${this.rule?.value?.force}
215 @bind-value-changed=${(e: BindValueChangeEvent) => {
216 this.handleForceBindValueChanged(e);
217 }}
218 >
219 <select ?disabled=${!this.editing}>
220 ${this.computeForceOptions(this.rule?.value?.action).map(
221 item => html`
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100222 <option value=${item.value}>${item.name}</option>
Paladox none83c5fa62021-11-24 22:26:52 +0000223 `
224 )}
225 </select>
226 </gr-select>
227 </div>
228 <gr-button
229 link
230 id="removeBtn"
231 @click=${() => {
232 this.handleRemoveRule();
233 }}
234 >Remove</gr-button
235 >
236 </div>
237 <div
238 id="deletedContainer"
239 class="gr-form-styles ${this.computeSectionClass()}"
240 >
241 ${this.groupName} was deleted
242 <gr-button
243 link
244 id="undoRemoveBtn"
245 @click=${() => {
246 this.handleUndoRemove();
247 }}
248 >Undo</gr-button
249 >
250 </div>
251 `;
252 }
253
254 private renderMinAndMaxLabel() {
255 if (!this.label) return;
256
257 return html`
258 <gr-select
259 id="labelMin"
260 .bindValue=${this.rule?.value?.min}
261 @bind-value-changed=${(e: BindValueChangeEvent) => {
262 this.handleMinBindValueChanged(e);
263 }}
264 >
265 <select ?disabled=${!this.editing}>
266 ${this.label.values.map(
267 item => html` <option value=${item.value}>${item.value}</option> `
268 )}
269 </select>
270 </gr-select>
271 <gr-select
272 id="labelMax"
273 .bindValue=${this.rule?.value?.max}
274 @bind-value-changed=${(e: BindValueChangeEvent) => {
275 this.handleMaxBindValueChanged(e);
276 }}
277 >
278 <select ?disabled=${!this.editing}>
279 ${this.label.values.map(
280 item => html` <option value=${item.value}>${item.value}</option> `
281 )}
282 </select>
283 </gr-select>
284 `;
285 }
286
287 private renderMinAndMaxInput() {
288 if (!this.hasRange) return;
289
290 return html`
291 <iron-autogrow-textarea
292 id="minInput"
293 class="min"
294 autocomplete="on"
295 placeholder="Min value"
296 .bindValue=${this.rule?.value?.min}
297 ?disabled=${!this.editing}
298 @bind-value-changed=${(e: BindValueChangeEvent) => {
299 this.handleMinBindValueChanged(e);
300 }}
301 ></iron-autogrow-textarea>
302 <iron-autogrow-textarea
303 id="maxInput"
304 class="max"
305 autocomplete="on"
306 placeholder="Max value"
307 .bindValue=${this.rule?.value?.max}
308 ?disabled=${!this.editing}
309 @bind-value-changed=${(e: BindValueChangeEvent) => {
310 this.handleMaxBindValueChanged(e);
311 }}
312 ></iron-autogrow-textarea>
313 `;
314 }
315
316 override willUpdate(changedProperties: PropertyValues) {
317 if (changedProperties.has('editing')) {
318 this.handleEditingChanged(changedProperties.get('editing') as boolean);
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100319 }
320 }
321
Paladox none83c5fa62021-11-24 22:26:52 +0000322 // private but used in test
323 setupValues() {
324 if (!this.rule?.value) {
325 this.setDefaultRuleValues();
326 }
327 }
328
329 // private but used in test
330 computeForce(action?: string) {
331 if (AccessPermissionId.PUSH === this.permission && action !== Action.DENY) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100332 return true;
333 }
334
Paladox none83c5fa62021-11-24 22:26:52 +0000335 return AccessPermissionId.EDIT_TOPIC_NAME === this.permission;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100336 }
337
Paladox none83c5fa62021-11-24 22:26:52 +0000338 // private but used in test
339 computeGroupPath(groupId?: string) {
340 if (!groupId) return;
Ben Rohlfsc02facb2023-01-27 18:46:02 +0100341 return `${getBaseUrl()}/admin/groups/${encodeURL(groupId)}`;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100342 }
343
Paladox none83c5fa62021-11-24 22:26:52 +0000344 // private but used in test
345 handleAccessSaved() {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100346 // Set a new 'original' value to keep track of after the value has been
347 // saved.
Paladox none83c5fa62021-11-24 22:26:52 +0000348 this.setOriginalRuleValues();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100349 }
350
Paladox none83c5fa62021-11-24 22:26:52 +0000351 private handleEditingChanged(editingOld: boolean) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100352 // Ignore when editing gets set initially.
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200353 if (!editingOld) {
354 return;
355 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100356 // Restore original values if no longer editing.
Paladox none83c5fa62021-11-24 22:26:52 +0000357 if (!this.editing) {
358 this.handleUndoChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100359 }
360 }
361
Paladox none83c5fa62021-11-24 22:26:52 +0000362 // private but used in test
363 computeSectionClass() {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100364 const classList = [];
Paladox none83c5fa62021-11-24 22:26:52 +0000365 if (this.editing) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100366 classList.push('editing');
367 }
Paladox none83c5fa62021-11-24 22:26:52 +0000368 if (this.deleted) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100369 classList.push('deleted');
370 }
371 return classList.join(' ');
372 }
373
Paladox none83c5fa62021-11-24 22:26:52 +0000374 // private but used in test
375 computeForceOptions(action?: string) {
376 if (this.permission === AccessPermissionId.PUSH) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100377 if (action === Action.ALLOW) {
378 return ForcePushOptions.ALLOW;
379 } else if (action === Action.BLOCK) {
380 return ForcePushOptions.BLOCK;
381 } else {
382 return [];
383 }
Paladox none83c5fa62021-11-24 22:26:52 +0000384 } else if (this.permission === AccessPermissionId.EDIT_TOPIC_NAME) {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100385 return FORCE_EDIT_OPTIONS;
386 }
387 return [];
388 }
389
Paladox none83c5fa62021-11-24 22:26:52 +0000390 // private but used in test
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100391 getDefaultRuleValues(): EditablePermissionRuleInfo {
Paladox none83c5fa62021-11-24 22:26:52 +0000392 if (this.permission === AccessPermissionId.PRIORITY) {
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100393 return {action: PRIORITY_OPTIONS[0]};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100394 }
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100395 if (this.label) {
396 return {
397 action: DROPDOWN_OPTIONS[0],
398 min: this.label.values[0].value,
399 max: this.label.values[this.label.values.length - 1].value,
400 };
401 }
402 if (this.computeForce(Action.ALLOW)) {
403 return {
404 action: DROPDOWN_OPTIONS[0],
405 force: this.computeForceOptions(Action.ALLOW)[0].value,
406 };
407 }
408 return {action: DROPDOWN_OPTIONS[0]};
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100409 }
410
Paladox none83c5fa62021-11-24 22:26:52 +0000411 // private but used in test
412 setDefaultRuleValues() {
413 this.rule!.value = this.getDefaultRuleValues();
414
415 this.handleRuleChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100416 }
417
Paladox none83c5fa62021-11-24 22:26:52 +0000418 // private but used in test
419 computeOptions() {
420 if (this.permission === 'priority') {
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100421 return PRIORITY_OPTIONS;
422 }
423 return DROPDOWN_OPTIONS;
424 }
425
Paladox none83c5fa62021-11-24 22:26:52 +0000426 private handleRemoveRule() {
Paladox none4c88fce2021-11-24 20:36:22 +0000427 if (!this.rule?.value) return;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100428 if (this.rule.value.added) {
Ben Rohlfs44f01042023-02-18 13:27:57 +0100429 fire(this, 'added-rule-removed', {});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100430 }
Paladox none83c5fa62021-11-24 22:26:52 +0000431 this.deleted = true;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100432 this.rule.value.deleted = true;
Paladox none83c5fa62021-11-24 22:26:52 +0000433
434 this.handleRuleChange();
435
Ben Rohlfs44f01042023-02-18 13:27:57 +0100436 fire(this, 'access-modified', {});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100437 }
438
Paladox none83c5fa62021-11-24 22:26:52 +0000439 private handleUndoRemove() {
Paladox none4c88fce2021-11-24 20:36:22 +0000440 if (!this.rule?.value) return;
Paladox none83c5fa62021-11-24 22:26:52 +0000441 this.deleted = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100442 delete this.rule.value.deleted;
Paladox none83c5fa62021-11-24 22:26:52 +0000443
444 this.handleRuleChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100445 }
446
Paladox none83c5fa62021-11-24 22:26:52 +0000447 private handleUndoChange() {
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100448 if (!this.originalRuleValues || !this.rule?.value) {
449 return;
450 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100451 // gr-permission will take care of removing rules that were added but
452 // unsaved. We need to keep the added bit for the filter.
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200453 if (this.rule.value.added) {
454 return;
455 }
Paladox none83c5fa62021-11-24 22:26:52 +0000456 this.rule.value = {...this.originalRuleValues};
457 this.deleted = false;
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100458 delete this.rule.value.deleted;
459 delete this.rule.value.modified;
Paladox none83c5fa62021-11-24 22:26:52 +0000460
461 this.handleRuleChange();
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100462 }
463
Paladox none83c5fa62021-11-24 22:26:52 +0000464 // private but used in test
465 handleValueChange() {
466 if (!this.originalRuleValues || !this.rule?.value) {
Milutin Kristoficccc164e2020-08-13 22:40:40 +0200467 return;
468 }
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100469 this.rule.value.modified = true;
Paladox none83c5fa62021-11-24 22:26:52 +0000470
471 this.handleRuleChange();
472
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100473 // Allows overall access page to know a change has been made.
Ben Rohlfs44f01042023-02-18 13:27:57 +0100474 fire(this, 'access-modified', {});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100475 }
476
Paladox none83c5fa62021-11-24 22:26:52 +0000477 // private but used in test
478 setOriginalRuleValues() {
479 if (!this.rule?.value) return;
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100480 this.originalRuleValues = {...this.rule.value};
Paladox none83c5fa62021-11-24 22:26:52 +0000481 }
482
483 private handleActionBindValueChanged(e: BindValueChangeEvent) {
484 if (
485 !this.rule?.value ||
486 e.detail.value === undefined ||
487 this.rule.value.action === String(e.detail.value)
488 )
489 return;
490
Ben Rohlfsc3ce1402022-02-22 10:35:34 +0100491 this.rule.value.action = String(e.detail.value) as PermissionAction;
Paladox none83c5fa62021-11-24 22:26:52 +0000492
493 this.handleValueChange();
494 }
495
496 private handleMinBindValueChanged(e: BindValueChangeEvent) {
497 if (
498 !this.rule?.value ||
499 e.detail.value === undefined ||
500 this.rule.value.min === Number(e.detail.value)
501 )
502 return;
503 this.rule.value.min = Number(e.detail.value);
504
505 this.handleValueChange();
506 }
507
508 private handleMaxBindValueChanged(e: BindValueChangeEvent) {
509 if (
510 !this.rule?.value ||
511 e.detail.value === undefined ||
512 this.rule.value.max === Number(e.detail.value)
513 )
514 return;
515 this.rule.value.max = Number(e.detail.value);
516
517 this.handleValueChange();
518 }
519
520 private handleForceBindValueChanged(e: BindValueChangeEvent) {
521 const forceValue = String(e.detail.value) === 'true' ? true : false;
522 if (
523 !this.rule?.value ||
524 e.detail.value === undefined ||
525 this.rule.value.force === forceValue
526 )
527 return;
528 this.rule.value.force = forceValue;
529
530 this.handleValueChange();
531 }
532
533 private handleRuleChange() {
534 this.requestUpdate('rule');
Ben Rohlfs6bb90532023-02-17 18:55:56 +0100535 fire(this, 'rule-changed', {value: this.rule});
Dmitrii Filippovdaf0ec92020-03-17 11:27:28 +0100536 }
537}