Add the ability to add a new section in gr-repo-access
Bug: Issue 8038 Change-Id: I23f7922614244cd1833602385c4cfa1657c7e8b9
This commit is contained in:
@@ -42,7 +42,6 @@ limitations under the License.
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.header,
|
.header,
|
||||||
.editingRef .editContainer,
|
|
||||||
#deletedContainer {
|
#deletedContainer {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f6f6f6;
|
background: #f6f6f6;
|
||||||
@@ -55,9 +54,6 @@ limitations under the License.
|
|||||||
#deletedContainer {
|
#deletedContainer {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
#editRefInput {
|
|
||||||
width: 70%;
|
|
||||||
}
|
|
||||||
.sectionContent {
|
.sectionContent {
|
||||||
padding: .7em;
|
padding: .7em;
|
||||||
}
|
}
|
||||||
@@ -67,11 +63,12 @@ limitations under the License.
|
|||||||
.deleted #mainContainer,
|
.deleted #mainContainer,
|
||||||
#addPermission,
|
#addPermission,
|
||||||
#deleteBtn,
|
#deleteBtn,
|
||||||
.editingRef .header,
|
.editingRef .name,
|
||||||
.editContainer {
|
#editRefInput {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.editing #editBtn {
|
.editing #editBtn,
|
||||||
|
.editingRef #editRefInput {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
.deleted #deletedContainer {
|
.deleted #deletedContainer {
|
||||||
@@ -86,9 +83,6 @@ limitations under the License.
|
|||||||
#undoRemoveBtn {
|
#undoRemoveBtn {
|
||||||
padding-right: .7em;
|
padding-right: .7em;
|
||||||
}
|
}
|
||||||
.editingRef .editContainer {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<style include="gr-form-styles"></style>
|
<style include="gr-form-styles"></style>
|
||||||
<fieldset id="section"
|
<fieldset id="section"
|
||||||
@@ -101,23 +95,21 @@ limitations under the License.
|
|||||||
id="editBtn"
|
id="editBtn"
|
||||||
link
|
link
|
||||||
class$="[[_computeEditBtnClass(section.id)]]"
|
class$="[[_computeEditBtnClass(section.id)]]"
|
||||||
on-tap="_handleEditReference">
|
on-tap="editReference">
|
||||||
<iron-icon id="icon" icon="gr-icons:create"></iron-icon>
|
<iron-icon id="icon" icon="gr-icons:create"></iron-icon>
|
||||||
</gr-button>
|
</gr-button>
|
||||||
</div>
|
</div>
|
||||||
<gr-button
|
|
||||||
link
|
|
||||||
id="deleteBtn"
|
|
||||||
on-tap="_handleRemoveReference">Remove</gr-button>
|
|
||||||
</div><!-- end header -->
|
|
||||||
<div class="editContainer">
|
|
||||||
<input
|
<input
|
||||||
id="editRefInput"
|
id="editRefInput"
|
||||||
bind-value="{{section.id}}"
|
bind-value="{{section.id}}"
|
||||||
is="iron-input"
|
is="iron-input"
|
||||||
type="text"
|
type="text"
|
||||||
on-input="_handleValueChange">
|
on-input="_handleValueChange">
|
||||||
</div><!-- end editContainer -->
|
<gr-button
|
||||||
|
link
|
||||||
|
id="deleteBtn"
|
||||||
|
on-tap="_handleRemoveReference">Remove</gr-button>
|
||||||
|
</div><!-- end header -->
|
||||||
<div class="sectionContent">
|
<div class="sectionContent">
|
||||||
<template
|
<template
|
||||||
is="dom-repeat"
|
is="dom-repeat"
|
||||||
|
@@ -78,11 +78,15 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
_handleValueChange() {
|
_handleValueChange() {
|
||||||
|
if (!this.section.value.added) {
|
||||||
this.section.value.modified = this.section.id !== this._originalId;
|
this.section.value.modified = this.section.id !== this._originalId;
|
||||||
this.section.value.updatedId = this.section.id;
|
|
||||||
|
|
||||||
// Allows overall access page to know a change has been made.
|
// Allows overall access page to know a change has been made.
|
||||||
|
// For a new section, this is not fired because new permissions and
|
||||||
|
// rules have to be added in order to save, modifying the ref is not
|
||||||
|
// enough.
|
||||||
this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
|
this.dispatchEvent(new CustomEvent('access-modified', {bubbles: true}));
|
||||||
|
}
|
||||||
|
this.section.value.updatedId = this.section.id;
|
||||||
},
|
},
|
||||||
|
|
||||||
_handleEditingChanged(editing, editingOld) {
|
_handleEditingChanged(editing, editingOld) {
|
||||||
@@ -187,8 +191,9 @@
|
|||||||
delete this.section.value.deleted;
|
delete this.section.value.deleted;
|
||||||
},
|
},
|
||||||
|
|
||||||
_handleEditReference() {
|
editReference() {
|
||||||
this._editingRef = true;
|
this._editingRef = true;
|
||||||
|
this.$.editRefInput.focus();
|
||||||
},
|
},
|
||||||
|
|
||||||
_computeSectionClass(editing, editingRef, deleted) {
|
_computeSectionClass(editing, editingRef, deleted) {
|
||||||
|
@@ -264,8 +264,8 @@ limitations under the License.
|
|||||||
assert.isFalse(element._editingRef);
|
assert.isFalse(element._editingRef);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('_handleEditReference', () => {
|
test('editReference', () => {
|
||||||
element._handleEditReference();
|
element.editReference();
|
||||||
assert.isTrue(element._editingRef);
|
assert.isTrue(element._editingRef);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -456,6 +456,7 @@ limitations under the License.
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('_handleValueChange', () => {
|
test('_handleValueChange', () => {
|
||||||
|
// For an exising section.
|
||||||
const modifiedHandler = sandbox.stub();
|
const modifiedHandler = sandbox.stub();
|
||||||
element.section = {id: 'refs/for/bar', value: {permissions: {}}};
|
element.section = {id: 'refs/for/bar', value: {permissions: {}}};
|
||||||
assert.notOk(element.section.value.updatedId);
|
assert.notOk(element.section.value.updatedId);
|
||||||
@@ -465,10 +466,21 @@ limitations under the License.
|
|||||||
element._handleValueChange();
|
element._handleValueChange();
|
||||||
assert.equal(element.section.value.updatedId, 'refs/for/baz');
|
assert.equal(element.section.value.updatedId, 'refs/for/baz');
|
||||||
assert.isTrue(element.section.value.modified);
|
assert.isTrue(element.section.value.modified);
|
||||||
assert.isTrue(modifiedHandler.called);
|
assert.equal(modifiedHandler.callCount, 1);
|
||||||
element.section.id = 'refs/for/bar';
|
element.section.id = 'refs/for/bar';
|
||||||
element._handleValueChange();
|
element._handleValueChange();
|
||||||
assert.isFalse(element.section.value.modified);
|
assert.isFalse(element.section.value.modified);
|
||||||
|
assert.equal(modifiedHandler.callCount, 2);
|
||||||
|
|
||||||
|
// For a new section.
|
||||||
|
element.section.value.added = true;
|
||||||
|
element._handleValueChange();
|
||||||
|
assert.isFalse(element.section.value.modified);
|
||||||
|
assert.equal(modifiedHandler.callCount, 2);
|
||||||
|
element.section.id = 'refs/for/bar';
|
||||||
|
element._handleValueChange();
|
||||||
|
assert.isFalse(element.section.value.modified);
|
||||||
|
assert.equal(modifiedHandler.callCount, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('remove section', () => {
|
test('remove section', () => {
|
||||||
|
@@ -80,6 +80,9 @@ limitations under the License.
|
|||||||
editing="[[_editing]]"
|
editing="[[_editing]]"
|
||||||
groups="[[_groups]]"></gr-access-section>
|
groups="[[_groups]]"></gr-access-section>
|
||||||
</template>
|
</template>
|
||||||
|
<gr-button id="addReferenceBtn"
|
||||||
|
class$="[[_computeShowSaveClass(_editing)]]"
|
||||||
|
on-tap="_handleCreateSection">Add Reference</gr-button>
|
||||||
</main>
|
</main>
|
||||||
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
|
||||||
</template>
|
</template>
|
||||||
|
@@ -87,6 +87,7 @@
|
|||||||
_editing: {
|
_editing: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
|
observer: '_handleEditingChanged',
|
||||||
},
|
},
|
||||||
_modified: {
|
_modified: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -152,6 +153,18 @@
|
|||||||
return editing ? 'Cancel' : 'Edit';
|
return editing ? 'Cancel' : 'Edit';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_handleEditingChanged(editing, editingOld) {
|
||||||
|
// Ignore when editing gets set initially.
|
||||||
|
if (!editingOld || editing) { return; }
|
||||||
|
// Remove any unsaved but added refs.
|
||||||
|
this._sections = this._sections.filter(p => !p.value.added);
|
||||||
|
for (const key of Object.keys(this._local)) {
|
||||||
|
if (this._local[key].added) {
|
||||||
|
delete this._local[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {!Defs.projectAccessInput} addRemoveObj
|
* @param {!Defs.projectAccessInput} addRemoveObj
|
||||||
* @param {!Array} path
|
* @param {!Array} path
|
||||||
@@ -202,6 +215,8 @@
|
|||||||
for (const k in obj) {
|
for (const k in obj) {
|
||||||
if (!obj.hasOwnProperty(k)) { return; }
|
if (!obj.hasOwnProperty(k)) { return; }
|
||||||
if (typeof obj[k] == 'object') {
|
if (typeof obj[k] == 'object') {
|
||||||
|
const updatedId = obj[k].updatedId;
|
||||||
|
const ref = updatedId ? updatedId : k;
|
||||||
if (obj[k].deleted) {
|
if (obj[k].deleted) {
|
||||||
this._updateAddRemoveObj(addRemoveObj,
|
this._updateAddRemoveObj(addRemoveObj,
|
||||||
path.concat(k), 'remove');
|
path.concat(k), 'remove');
|
||||||
@@ -209,9 +224,6 @@
|
|||||||
} else if (obj[k].modified) {
|
} else if (obj[k].modified) {
|
||||||
this._updateAddRemoveObj(addRemoveObj,
|
this._updateAddRemoveObj(addRemoveObj,
|
||||||
path.concat(k), 'remove');
|
path.concat(k), 'remove');
|
||||||
|
|
||||||
const updatedId = obj[k].updatedId;
|
|
||||||
const ref = updatedId ? updatedId : k;
|
|
||||||
this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
|
this._updateAddRemoveObj(addRemoveObj, path.concat(ref), 'add',
|
||||||
obj[k]);
|
obj[k]);
|
||||||
/* Special case for ref changes because they need to be added and
|
/* Special case for ref changes because they need to be added and
|
||||||
@@ -225,7 +237,7 @@
|
|||||||
continue;
|
continue;
|
||||||
} else if (obj[k].added) {
|
} else if (obj[k].added) {
|
||||||
this._updateAddRemoveObj(addRemoveObj,
|
this._updateAddRemoveObj(addRemoveObj,
|
||||||
path.concat(k), 'add', obj[k]);
|
path.concat(ref), 'add', obj[k]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
|
this._recursivelyUpdateAddRemoveObj(obj[k], addRemoveObj,
|
||||||
@@ -250,6 +262,21 @@
|
|||||||
return addRemoveObj;
|
return addRemoveObj;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_handleCreateSection() {
|
||||||
|
let newRef = 'refs/for/*';
|
||||||
|
// Avoid using an already used key for the placeholder, since it
|
||||||
|
// immediately gets added to an object.
|
||||||
|
while (this._local[newRef]) {
|
||||||
|
newRef = `${newRef}*`;
|
||||||
|
}
|
||||||
|
const section = {permissions: {}, added: true};
|
||||||
|
this.push('_sections', {id: newRef, value: section});
|
||||||
|
this.set(['_local', newRef], section);
|
||||||
|
Polymer.dom.flush();
|
||||||
|
Polymer.dom(this.root).querySelector('gr-access-section:last-of-type')
|
||||||
|
.editReference();
|
||||||
|
},
|
||||||
|
|
||||||
_handleSaveForReview() {
|
_handleSaveForReview() {
|
||||||
const addRemoveObj = this._computeAddAndRemove();
|
const addRemoveObj = this._computeAddAndRemove();
|
||||||
|
|
||||||
|
@@ -523,6 +523,99 @@ limitations under the License.
|
|||||||
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('_computeAddAndRemove new section', () => {
|
||||||
|
// Add a new permission to a section
|
||||||
|
expectedInput = {
|
||||||
|
add: {
|
||||||
|
'refs/for/*': {
|
||||||
|
added: true,
|
||||||
|
permissions: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {},
|
||||||
|
};
|
||||||
|
MockInteractions.tap(element.$.addReferenceBtn);
|
||||||
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
|
|
||||||
|
expectedInput = {
|
||||||
|
add: {
|
||||||
|
'refs/for/*': {
|
||||||
|
added: true,
|
||||||
|
permissions: {
|
||||||
|
'label-Code-Review': {
|
||||||
|
added: true,
|
||||||
|
rules: {},
|
||||||
|
label: 'Code-Review',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {},
|
||||||
|
};
|
||||||
|
const newSection = Polymer.dom(element.root)
|
||||||
|
.querySelectorAll('gr-access-section')[1];
|
||||||
|
newSection._handleAddPermission();
|
||||||
|
flushAsynchronousOperations();
|
||||||
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
|
|
||||||
|
// Add rule to the new permission.
|
||||||
|
expectedInput = {
|
||||||
|
add: {
|
||||||
|
'refs/for/*': {
|
||||||
|
added: true,
|
||||||
|
permissions: {
|
||||||
|
'label-Code-Review': {
|
||||||
|
added: true,
|
||||||
|
rules: {
|
||||||
|
Maintainers: {
|
||||||
|
action: 'ALLOW',
|
||||||
|
added: true,
|
||||||
|
max: 2,
|
||||||
|
min: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: 'Code-Review',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
newSection.$$('gr-permission')._handleAddRuleItem(
|
||||||
|
{detail: {value: {id: 'Maintainers'}}});
|
||||||
|
|
||||||
|
flushAsynchronousOperations();
|
||||||
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
|
|
||||||
|
// Modify a the reference from the default value.
|
||||||
|
element._local['refs/for/*'].updatedId = 'refs/for/new';
|
||||||
|
expectedInput = {
|
||||||
|
add: {
|
||||||
|
'refs/for/new': {
|
||||||
|
added: true,
|
||||||
|
updatedId: 'refs/for/new',
|
||||||
|
permissions: {
|
||||||
|
'label-Code-Review': {
|
||||||
|
added: true,
|
||||||
|
rules: {
|
||||||
|
Maintainers: {
|
||||||
|
action: 'ALLOW',
|
||||||
|
added: true,
|
||||||
|
max: 2,
|
||||||
|
min: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: 'Code-Review',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {},
|
||||||
|
};
|
||||||
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
|
});
|
||||||
|
|
||||||
test('_computeAddAndRemove combinations', () => {
|
test('_computeAddAndRemove combinations', () => {
|
||||||
// Modify rule and delete permission that it is inside of.
|
// Modify rule and delete permission that it is inside of.
|
||||||
element._local['refs/*'].permissions.owner.rules[123].modified = true;
|
element._local['refs/*'].permissions.owner.rules[123].modified = true;
|
||||||
@@ -671,6 +764,150 @@ limitations under the License.
|
|||||||
};
|
};
|
||||||
element._local['refs/*'].deleted = true;
|
element._local['refs/*'].deleted = true;
|
||||||
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
|
|
||||||
|
// Add a new section.
|
||||||
|
MockInteractions.tap(element.$.addReferenceBtn);
|
||||||
|
let newSection = Polymer.dom(element.root)
|
||||||
|
.querySelectorAll('gr-access-section')[1];
|
||||||
|
newSection._handleAddPermission();
|
||||||
|
flushAsynchronousOperations();
|
||||||
|
newSection.$$('gr-permission')._handleAddRuleItem(
|
||||||
|
{detail: {value: {id: 'Maintainers'}}});
|
||||||
|
// Modify a the reference from the default value.
|
||||||
|
element._local['refs/for/*'].updatedId = 'refs/for/new';
|
||||||
|
|
||||||
|
expectedInput = {
|
||||||
|
add: {
|
||||||
|
'refs/for/new': {
|
||||||
|
added: true,
|
||||||
|
updatedId: 'refs/for/new',
|
||||||
|
permissions: {
|
||||||
|
'label-Code-Review': {
|
||||||
|
added: true,
|
||||||
|
rules: {
|
||||||
|
Maintainers: {
|
||||||
|
action: 'ALLOW',
|
||||||
|
added: true,
|
||||||
|
max: 2,
|
||||||
|
min: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: 'Code-Review',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
'refs/*': {
|
||||||
|
permissions: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
|
|
||||||
|
// Modify newly added rule inside new ref.
|
||||||
|
element._local['refs/for/*'].permissions['label-Code-Review'].
|
||||||
|
rules['Maintainers'].modified = true;
|
||||||
|
expectedInput = {
|
||||||
|
add: {
|
||||||
|
'refs/for/new': {
|
||||||
|
added: true,
|
||||||
|
updatedId: 'refs/for/new',
|
||||||
|
permissions: {
|
||||||
|
'label-Code-Review': {
|
||||||
|
added: true,
|
||||||
|
rules: {
|
||||||
|
Maintainers: {
|
||||||
|
action: 'ALLOW',
|
||||||
|
added: true,
|
||||||
|
modified: true,
|
||||||
|
max: 2,
|
||||||
|
min: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: 'Code-Review',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
'refs/*': {
|
||||||
|
permissions: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
|
|
||||||
|
// Add a second new section.
|
||||||
|
MockInteractions.tap(element.$.addReferenceBtn);
|
||||||
|
newSection = Polymer.dom(element.root)
|
||||||
|
.querySelectorAll('gr-access-section')[2];
|
||||||
|
newSection._handleAddPermission();
|
||||||
|
flushAsynchronousOperations();
|
||||||
|
newSection.$$('gr-permission')._handleAddRuleItem(
|
||||||
|
{detail: {value: {id: 'Maintainers'}}});
|
||||||
|
// Modify a the reference from the default value.
|
||||||
|
element._local['refs/for/**'].updatedId = 'refs/for/new2';
|
||||||
|
expectedInput = {
|
||||||
|
add: {
|
||||||
|
'refs/for/new': {
|
||||||
|
added: true,
|
||||||
|
updatedId: 'refs/for/new',
|
||||||
|
permissions: {
|
||||||
|
'label-Code-Review': {
|
||||||
|
added: true,
|
||||||
|
rules: {
|
||||||
|
Maintainers: {
|
||||||
|
action: 'ALLOW',
|
||||||
|
added: true,
|
||||||
|
modified: true,
|
||||||
|
max: 2,
|
||||||
|
min: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: 'Code-Review',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'refs/for/new2': {
|
||||||
|
added: true,
|
||||||
|
updatedId: 'refs/for/new2',
|
||||||
|
permissions: {
|
||||||
|
'label-Code-Review': {
|
||||||
|
added: true,
|
||||||
|
rules: {
|
||||||
|
Maintainers: {
|
||||||
|
action: 'ALLOW',
|
||||||
|
added: true,
|
||||||
|
max: 2,
|
||||||
|
min: -2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
label: 'Code-Review',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
'refs/*': {
|
||||||
|
permissions: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.deepEqual(element._computeAddAndRemove(), expectedInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Unsaved added refs are discarded when edit cancelled', () => {
|
||||||
|
// Unsaved changes are discarded when editing is cancelled.
|
||||||
|
MockInteractions.tap(element.$.editBtn);
|
||||||
|
assert.equal(element._sections.length, 1);
|
||||||
|
assert.equal(Object.keys(element._local).length, 1);
|
||||||
|
MockInteractions.tap(element.$.addReferenceBtn);
|
||||||
|
assert.equal(element._sections.length, 2);
|
||||||
|
assert.equal(Object.keys(element._local).length, 2);
|
||||||
|
MockInteractions.tap(element.$.editBtn);
|
||||||
|
assert.equal(element._sections.length, 1);
|
||||||
|
assert.equal(Object.keys(element._local).length, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('_handleSaveForReview', done => {
|
test('_handleSaveForReview', done => {
|
||||||
|
Reference in New Issue
Block a user