Merge branch 'main' into bugfix/composition-interfaces
This commit is contained in:
@@ -31,6 +31,9 @@ export class UmbDefaultCollectionContext<
|
||||
#manifest?: ManifestCollection;
|
||||
#repository?: UmbCollectionRepository;
|
||||
|
||||
#loading = new UmbObjectState<boolean>(false);
|
||||
public readonly loading = this.#loading.asObservable();
|
||||
|
||||
#items = new UmbArrayState<CollectionItemType>([], (x) => x);
|
||||
public readonly items = this.#items.asObservable();
|
||||
|
||||
@@ -176,6 +179,8 @@ export class UmbDefaultCollectionContext<
|
||||
|
||||
if (!this.#repository) throw new Error(`Missing repository for ${this.#manifest}`);
|
||||
|
||||
this.#loading.setValue(true);
|
||||
|
||||
const filter = this.#filter.getValue();
|
||||
const { data } = await this.#repository.requestCollection(filter);
|
||||
|
||||
@@ -184,6 +189,8 @@ export class UmbDefaultCollectionContext<
|
||||
this.#totalItems.setValue(data.total);
|
||||
this.pagination.setTotalItems(data.total);
|
||||
}
|
||||
|
||||
this.#loading.setValue(false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -57,7 +57,7 @@ export class UmbCollectionDefaultElement extends UmbLitElement {
|
||||
return html`
|
||||
<umb-body-layout header-transparent>
|
||||
${this.renderToolbar()}
|
||||
<umb-router-slot id="router-slot" .routes="${this._routes}"></umb-router-slot>
|
||||
<umb-router-slot id="router-slot" .routes=${this._routes}></umb-router-slot>
|
||||
${this.renderPagination()} ${this.renderSelectionActions()}
|
||||
</umb-body-layout>
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface UmbDocumentCollectionFilterModel extends UmbCollectionFilterMod
|
||||
orderBy?: string;
|
||||
orderCulture?: string;
|
||||
orderDirection?: 'asc' | 'desc';
|
||||
userDefinedProperties: Array<{alias: string, header: string, isSystem: boolean}>;
|
||||
userDefinedProperties: Array<{ alias: string; header: string; isSystem: boolean }>;
|
||||
}
|
||||
|
||||
export interface UmbDocumentCollectionItemModel {
|
||||
@@ -23,3 +23,8 @@ export interface UmbDocumentCollectionItemModel {
|
||||
updater?: string | null;
|
||||
values: Array<{ alias: string; value: string }>;
|
||||
}
|
||||
|
||||
export interface UmbEditableDocumentCollectionItemModel {
|
||||
item: UmbDocumentCollectionItemModel;
|
||||
editPath: string;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { getPropertyValueByAlias } from '../index.js';
|
||||
import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../paths.js';
|
||||
import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js';
|
||||
import type { UmbDocumentCollectionFilterModel, UmbDocumentCollectionItemModel } from '../../types.js';
|
||||
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, customElement, html, nothing, repeat, state, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { fromCamelCase } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
|
||||
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbDefaultCollectionContext } from '@umbraco-cms/backoffice/collection';
|
||||
import type { UUIInterfaceColor } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
@customElement('umb-document-grid-collection-view')
|
||||
export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
|
||||
@state()
|
||||
private _editDocumentPath = '';
|
||||
|
||||
@state()
|
||||
private _items: Array<UmbDocumentCollectionItemModel> = [];
|
||||
|
||||
@@ -19,9 +25,6 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
|
||||
@state()
|
||||
private _selection: Array<string | null> = [];
|
||||
|
||||
@state()
|
||||
private _skip: number = 0;
|
||||
|
||||
@state()
|
||||
private _userDefinedProperties?: Array<UmbCollectionColumnConfiguration>;
|
||||
|
||||
@@ -34,40 +37,51 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
|
||||
this.#collectionContext = collectionContext;
|
||||
this.#observeCollectionContext();
|
||||
});
|
||||
|
||||
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
|
||||
.addAdditionalPath('document')
|
||||
.onSetup(() => {
|
||||
return { data: { entityType: 'document', preset: {} } };
|
||||
})
|
||||
.onReject(() => {
|
||||
this.#collectionContext?.requestCollection();
|
||||
})
|
||||
.onSubmit(() => {
|
||||
this.#collectionContext?.requestCollection();
|
||||
})
|
||||
.observeRouteBuilder((routeBuilder) => {
|
||||
this._editDocumentPath = routeBuilder({});
|
||||
});
|
||||
}
|
||||
|
||||
#observeCollectionContext() {
|
||||
if (!this.#collectionContext) return;
|
||||
|
||||
this.observe(this.#collectionContext.loading, (loading) => (this._loading = loading), '_observeLoading');
|
||||
|
||||
this.observe(
|
||||
this.#collectionContext.userDefinedProperties,
|
||||
(userDefinedProperties) => {
|
||||
this._userDefinedProperties = userDefinedProperties;
|
||||
},
|
||||
'umbCollectionUserDefinedPropertiesObserver',
|
||||
'_observeUserDefinedProperties',
|
||||
);
|
||||
|
||||
this.observe(this.#collectionContext.items, (items) => (this._items = items), 'umbCollectionItemsObserver');
|
||||
this.observe(this.#collectionContext.items, (items) => (this._items = items), '_observeItems');
|
||||
|
||||
this.observe(
|
||||
this.#collectionContext.selection.selection,
|
||||
(selection) => (this._selection = selection),
|
||||
'umbCollectionSelectionObserver',
|
||||
);
|
||||
|
||||
this.observe(
|
||||
this.#collectionContext.pagination.skip,
|
||||
(skip) => {
|
||||
this._skip = skip;
|
||||
},
|
||||
'umbCollectionSkipObserver',
|
||||
'_observeSelection',
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: How should we handle url stuff? [?]
|
||||
#onOpen(id: string) {
|
||||
// TODO: this will not be needed when cards works as links with href [?]
|
||||
history.pushState(null, '', 'section/content/workspace/document/edit/' + id);
|
||||
#onOpen(event: Event, unique: string) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const url = this._editDocumentPath + UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({ unique });
|
||||
window.history.pushState(null, '', url);
|
||||
}
|
||||
|
||||
#onSelect(item: UmbDocumentCollectionItemModel) {
|
||||
@@ -83,33 +97,44 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this._loading) {
|
||||
return html`<div class="container"><uui-loader></uui-loader></div>`;
|
||||
}
|
||||
|
||||
if (this._items.length === 0) {
|
||||
return html`<div class="container"><p>${this.localize.term('content_listViewNoItems')}</p></div>`;
|
||||
}
|
||||
return this._items.length === 0 ? this.#renderEmpty() : this.#renderItems();
|
||||
}
|
||||
|
||||
#renderEmpty() {
|
||||
if (this._items.length > 0) return nothing;
|
||||
return html`
|
||||
<div id="document-grid">
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item.unique,
|
||||
(item) => this.#renderCard(item),
|
||||
<div class="container">
|
||||
${when(
|
||||
this._loading,
|
||||
() => html`<uui-loader></uui-loader>`,
|
||||
() => html`<p>${this.localize.term('content_listViewNoItems')}</p>`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderCard(item: UmbDocumentCollectionItemModel) {
|
||||
#renderItems() {
|
||||
if (this._items.length === 0) return nothing;
|
||||
return html`
|
||||
<div id="document-grid">
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item.unique,
|
||||
(item) => this.#renderItem(item),
|
||||
)}
|
||||
</div>
|
||||
${when(this._loading, () => html`<uui-loader-bar></uui-loader-bar>`)}
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItem(item: UmbDocumentCollectionItemModel) {
|
||||
return html`
|
||||
<uui-card-content-node
|
||||
.name=${item.name ?? 'Unnamed Document'}
|
||||
selectable
|
||||
?select-only=${this._selection.length > 0}
|
||||
?selected=${this.#isSelected(item)}
|
||||
@open=${() => this.#onOpen(item.unique ?? '')}
|
||||
@open=${(event: Event) => this.#onOpen(event, item.unique)}
|
||||
@selected=${() => this.#onSelect(item)}
|
||||
@deselected=${() => this.#onDeselect(item)}>
|
||||
<umb-icon slot="icon" name=${item.icon}></umb-icon>
|
||||
@@ -118,29 +143,26 @@ export class UmbDocumentGridCollectionViewElement extends UmbLitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
#renderState(item: UmbDocumentCollectionItemModel) {
|
||||
switch (item.state) {
|
||||
#getStateTagConfig(state: string): { color: UUIInterfaceColor; label: string } {
|
||||
switch (state) {
|
||||
case 'Published':
|
||||
return html`<uui-tag slot="tag" color="positive" look="secondary"
|
||||
>${this.localize.term('content_published')}</uui-tag
|
||||
>`;
|
||||
return { color: 'positive', label: this.localize.term('content_published') };
|
||||
case 'PublishedPendingChanges':
|
||||
return html`<uui-tag slot="tag" color="warning" look="secondary"
|
||||
>${this.localize.term('content_publishedPendingChanges')}</uui-tag
|
||||
>`;
|
||||
return { color: 'warning', label: this.localize.term('content_publishedPendingChanges') };
|
||||
case 'Draft':
|
||||
return html`<uui-tag slot="tag" color="default" look="secondary"
|
||||
>${this.localize.term('content_unpublished')}</uui-tag
|
||||
>`;
|
||||
return { color: 'default', label: this.localize.term('content_unpublished') };
|
||||
case 'NotCreated':
|
||||
return html`<uui-tag slot="tag" color="danger" look="secondary"
|
||||
>${this.localize.term('content_notCreated')}</uui-tag
|
||||
>`;
|
||||
return { color: 'danger', label: this.localize.term('content_notCreated') };
|
||||
default:
|
||||
return html`<uui-tag slot="tag" color="danger" look="secondary">${fromCamelCase(item.state)}</uui-tag>`;
|
||||
return { color: 'danger', label: fromCamelCase(state) };
|
||||
}
|
||||
}
|
||||
|
||||
#renderState(item: UmbDocumentCollectionItemModel) {
|
||||
const tagConfig = this.#getStateTagConfig(item.state);
|
||||
return html`<uui-tag slot="tag" color=${tagConfig.color} look="secondary">${tagConfig.label}</uui-tag>`;
|
||||
}
|
||||
|
||||
#renderProperties(item: UmbDocumentCollectionItemModel) {
|
||||
if (!this._userDefinedProperties) return;
|
||||
return html`
|
||||
|
||||
@@ -1,49 +1,30 @@
|
||||
import type { UmbDocumentCollectionItemModel } from '../../../types.js';
|
||||
import { css, customElement, html, property, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import type { UmbEditableDocumentCollectionItemModel } from '../../../types.js';
|
||||
import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbTableColumn, UmbTableColumnLayoutElement, UmbTableItem } from '@umbraco-cms/backoffice/components';
|
||||
import type { UUIButtonElement } from '@umbraco-cms/backoffice/external/uui';
|
||||
|
||||
@customElement('umb-document-table-column-name')
|
||||
export class UmbDocumentTableColumnNameElement extends UmbLitElement implements UmbTableColumnLayoutElement {
|
||||
@state()
|
||||
private _editDocumentPath = '';
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
column!: UmbTableColumn;
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
item!: UmbTableItem;
|
||||
|
||||
@property({ attribute: false })
|
||||
value!: UmbDocumentCollectionItemModel;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
|
||||
.addAdditionalPath('document')
|
||||
.onSetup(() => {
|
||||
return { data: { entityType: 'document', preset: {} } };
|
||||
})
|
||||
.observeRouteBuilder((routeBuilder) => {
|
||||
this._editDocumentPath = routeBuilder({});
|
||||
});
|
||||
}
|
||||
value!: UmbEditableDocumentCollectionItemModel;
|
||||
|
||||
#onClick(event: Event & { target: UUIButtonElement }) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
window.history.pushState({}, '', event.target.href);
|
||||
window.history.pushState(null, '', event.target.href);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.value) return nothing;
|
||||
return html`
|
||||
<uui-button
|
||||
compact
|
||||
href="${this._editDocumentPath}edit/${this.value.unique}"
|
||||
label=${this.value.name}
|
||||
href=${this.value.editPath}
|
||||
label=${this.value.item.name}
|
||||
@click=${this.#onClick}></uui-button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UmbDocumentCollectionItemModel } from '../../../types.js';
|
||||
import type { UmbEditableDocumentCollectionItemModel } from '../../../types.js';
|
||||
import { customElement, html, property } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { fromCamelCase } from '@umbraco-cms/backoffice/utils';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
@@ -6,17 +6,14 @@ import type { UmbTableColumn, UmbTableColumnLayoutElement, UmbTableItem } from '
|
||||
|
||||
@customElement('umb-document-table-column-state')
|
||||
export class UmbDocumentTableColumnStateElement extends UmbLitElement implements UmbTableColumnLayoutElement {
|
||||
@property({ type: Object, attribute: false })
|
||||
column!: UmbTableColumn;
|
||||
|
||||
@property({ type: Object, attribute: false })
|
||||
item!: UmbTableItem;
|
||||
|
||||
@property({ attribute: false })
|
||||
value!: UmbDocumentCollectionItemModel;
|
||||
value!: UmbEditableDocumentCollectionItemModel;
|
||||
|
||||
render() {
|
||||
switch (this.value.state) {
|
||||
switch (this.value.item.state) {
|
||||
case 'Published':
|
||||
return html`<uui-tag color="positive" look="secondary">${this.localize.term('content_published')}</uui-tag>`;
|
||||
case 'PublishedPendingChanges':
|
||||
@@ -26,7 +23,7 @@ export class UmbDocumentTableColumnStateElement extends UmbLitElement implements
|
||||
case 'NotCreated':
|
||||
return html`<uui-tag color="danger" look="secondary">${this.localize.term('content_notCreated')}</uui-tag>`;
|
||||
default:
|
||||
return html`<uui-tag color="danger" look="secondary">${fromCamelCase(this.value.state)}</uui-tag>`;
|
||||
return html`<uui-tag color="danger" look="secondary">${fromCamelCase(this.value.item.state)}</uui-tag>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { getPropertyValueByAlias } from '../index.js';
|
||||
import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from '../../../paths.js';
|
||||
import type { UmbCollectionColumnConfiguration } from '../../../../../core/collection/types.js';
|
||||
import type { UmbDocumentCollectionItemModel } from '../../types.js';
|
||||
import type { UmbDocumentCollectionContext } from '../../document-collection.context.js';
|
||||
import { css, html, customElement, state } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { css, customElement, html, nothing, state, when } from '@umbraco-cms/backoffice/external/lit';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
import { UMB_COLLECTION_CONTEXT } from '@umbraco-cms/backoffice/collection';
|
||||
import { UMB_WORKSPACE_MODAL, UmbModalRouteRegistrationController } from '@umbraco-cms/backoffice/modal';
|
||||
import type { UmbModalRouteBuilder } from '@umbraco-cms/backoffice/modal';
|
||||
import type {
|
||||
UmbTableColumn,
|
||||
UmbTableConfig,
|
||||
@@ -59,29 +62,52 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
|
||||
@state()
|
||||
private _selection: Array<string> = [];
|
||||
|
||||
@state()
|
||||
private _skip: number = 0;
|
||||
|
||||
#collectionContext?: UmbDocumentCollectionContext;
|
||||
|
||||
#routeBuilder?: UmbModalRouteBuilder;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_COLLECTION_CONTEXT, (collectionContext) => {
|
||||
this.#collectionContext = collectionContext;
|
||||
this.#observeCollectionContext();
|
||||
});
|
||||
|
||||
this.#registerModalRoute();
|
||||
}
|
||||
|
||||
#registerModalRoute() {
|
||||
new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
|
||||
.addAdditionalPath(':entityType')
|
||||
.onSetup((params) => {
|
||||
return { data: { entityType: params.entityType, preset: {} } };
|
||||
})
|
||||
.onReject(() => {
|
||||
this.#collectionContext?.requestCollection();
|
||||
})
|
||||
.onSubmit(() => {
|
||||
this.#collectionContext?.requestCollection();
|
||||
})
|
||||
.observeRouteBuilder((routeBuilder) => {
|
||||
this.#routeBuilder = routeBuilder;
|
||||
|
||||
// NOTE: Configuring the observations AFTER the route builder is ready,
|
||||
// otherwise there is a race condition and `#collectionContext.items` tends to win. [LK]
|
||||
this.#observeCollectionContext();
|
||||
});
|
||||
}
|
||||
|
||||
#observeCollectionContext() {
|
||||
if (!this.#collectionContext) return;
|
||||
|
||||
this.observe(this.#collectionContext.loading, (loading) => (this._loading = loading), '_observeLoading');
|
||||
|
||||
this.observe(
|
||||
this.#collectionContext.userDefinedProperties,
|
||||
(userDefinedProperties) => {
|
||||
this._userDefinedProperties = userDefinedProperties;
|
||||
this.#createTableHeadings();
|
||||
},
|
||||
'umbCollectionUserDefinedPropertiesObserver',
|
||||
'_observeUserDefinedProperties',
|
||||
);
|
||||
|
||||
this.observe(
|
||||
@@ -90,7 +116,7 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
|
||||
this._items = items;
|
||||
this.#createTableItems(this._items);
|
||||
},
|
||||
'umbCollectionItemsObserver',
|
||||
'_observeItems',
|
||||
);
|
||||
|
||||
this.observe(
|
||||
@@ -98,15 +124,7 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
|
||||
(selection) => {
|
||||
this._selection = selection as string[];
|
||||
},
|
||||
'umbCollectionSelectionObserver',
|
||||
);
|
||||
|
||||
this.observe(
|
||||
this.#collectionContext.pagination.skip,
|
||||
(skip) => {
|
||||
this._skip = skip;
|
||||
},
|
||||
'umbCollectionSkipObserver',
|
||||
'_observeSelection',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,15 +149,21 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
|
||||
|
||||
const data =
|
||||
this._tableColumns?.map((column) => {
|
||||
const editPath = this.#routeBuilder
|
||||
? this.#routeBuilder({ entityType: item.entityType }) +
|
||||
UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({ unique: item.unique })
|
||||
: '';
|
||||
|
||||
return {
|
||||
columnAlias: column.alias,
|
||||
value: column.elementName ? item : getPropertyValueByAlias(item, column.alias),
|
||||
value: column.elementName ? { item, editPath } : getPropertyValueByAlias(item, column.alias),
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
return {
|
||||
id: item.unique,
|
||||
icon: item.icon,
|
||||
entityType: 'document',
|
||||
data: data,
|
||||
};
|
||||
});
|
||||
@@ -170,23 +194,34 @@ export class UmbDocumentTableCollectionViewElement extends UmbLitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this._loading) {
|
||||
return html`<div class="container"><uui-loader></uui-loader></div>`;
|
||||
}
|
||||
return this._tableItems.length === 0 ? this.#renderEmpty() : this.#renderItems();
|
||||
}
|
||||
|
||||
if (this._tableItems.length === 0) {
|
||||
return html`<div class="container"><p>${this.localize.term('content_listViewNoItems')}</p></div>`;
|
||||
}
|
||||
#renderEmpty() {
|
||||
if (this._tableItems.length > 0) return nothing;
|
||||
return html`
|
||||
<div class="container">
|
||||
${when(
|
||||
this._loading,
|
||||
() => html`<uui-loader></uui-loader>`,
|
||||
() => html`<p>${this.localize.term('content_listViewNoItems')}</p>`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
#renderItems() {
|
||||
if (this._tableItems.length === 0) return nothing;
|
||||
return html`
|
||||
<umb-table
|
||||
.config=${this._tableConfig}
|
||||
.columns=${this._tableColumns}
|
||||
.items=${this._tableItems}
|
||||
.selection=${this._selection}
|
||||
@selected="${this.#handleSelect}"
|
||||
@deselected="${this.#handleDeselect}"
|
||||
@ordered="${this.#handleOrdering}"></umb-table>
|
||||
@selected=${this.#handleSelect}
|
||||
@deselected=${this.#handleDeselect}
|
||||
@ordered=${this.#handleOrdering}></umb-table>
|
||||
${when(this._loading, () => html`<uui-loader-bar></uui-loader-bar>`)}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user