mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-24 02:25:00 +02:00
Updated navigator layout (#5619)
Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
This commit is contained in:
+22
-20
@@ -26,14 +26,13 @@
|
||||
getDocumentName,
|
||||
DocumentState
|
||||
} from '@hcengineering/controlled-documents'
|
||||
|
||||
import DocHierarchyTreeElement from './DocHierarchyTreeElement.svelte'
|
||||
import { TreeItem } from '@hcengineering/view-resources'
|
||||
import { compareDocs } from '../../utils'
|
||||
|
||||
export let projectMeta: ProjectMeta[] = []
|
||||
export let childrenByParent: Record<Ref<DocumentMeta>, Array<ProjectMeta>>
|
||||
export let selected: Ref<Doc> | undefined
|
||||
export let level = 1
|
||||
export let level: number = 0
|
||||
export let getMoreActions: ((obj: Doc, originalEvent?: MouseEvent) => Promise<Action[]>) | undefined = undefined
|
||||
export let collapsedPrefix: string = ''
|
||||
|
||||
@@ -111,33 +110,36 @@
|
||||
|
||||
{#if doc}
|
||||
{@const children = childrenByParent[doc.attachedTo] ?? []}
|
||||
<DocHierarchyTreeElement
|
||||
docId={doc._id}
|
||||
<TreeItem
|
||||
_id={doc._id}
|
||||
icon={documents.icon.Document}
|
||||
iconProps={{
|
||||
fill: 'currentColor'
|
||||
}}
|
||||
title={getDocumentName(doc)}
|
||||
selected={selected === prjdoc._id}
|
||||
parent={children.length > 0}
|
||||
getMoreActions={getMoreActions !== undefined ? () => getDocMoreActions(prjdoc) : undefined}
|
||||
selected={selected === doc._id || selected === prjdoc._id}
|
||||
isFold
|
||||
empty={children.length === 0 || children === undefined}
|
||||
actions={getMoreActions !== undefined ? () => getDocMoreActions(prjdoc) : undefined}
|
||||
{level}
|
||||
{collapsedPrefix}
|
||||
on:click={() => {
|
||||
dispatch('selected', prjdoc)
|
||||
}}
|
||||
>
|
||||
{#if children.length}
|
||||
<svelte:self
|
||||
projectMeta={children}
|
||||
{childrenByParent}
|
||||
{selected}
|
||||
{collapsedPrefix}
|
||||
{getMoreActions}
|
||||
level={level + 1}
|
||||
on:selected
|
||||
/>
|
||||
{/if}
|
||||
</DocHierarchyTreeElement>
|
||||
<svelte:fragment slot="dropbox">
|
||||
{#if children.length}
|
||||
<svelte:self
|
||||
projectMeta={children}
|
||||
{childrenByParent}
|
||||
{selected}
|
||||
{collapsedPrefix}
|
||||
{getMoreActions}
|
||||
level={level + 1}
|
||||
on:selected
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TreeItem>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Ref } from '@hcengineering/core'
|
||||
import { type Asset, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import { type Action, type AnySvelteComponent, tooltip } from '@hcengineering/ui'
|
||||
import {
|
||||
Icon,
|
||||
IconChevronDown,
|
||||
IconMoreH,
|
||||
Menu,
|
||||
getTreeCollapsed,
|
||||
setTreeCollapsed,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { DocumentSpace } from '@hcengineering/controlled-documents'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let _id: Ref<DocumentSpace>
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let iconProps: Record<string, any> | undefined = undefined
|
||||
export let title: string
|
||||
export let selected: boolean = false
|
||||
export let getMoreActions: ((originalEvent?: MouseEvent) => Promise<Action[]>) | undefined = undefined
|
||||
export let collapsed: boolean = getTreeCollapsed(_id)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let hovered = false
|
||||
async function onMenuClick (ev: MouseEvent): Promise<void> {
|
||||
if (getMoreActions === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
hovered = true
|
||||
const actions = await getMoreActions(ev)
|
||||
showPopup(Menu, { actions, ctx: _id }, ev.target as HTMLElement, () => {
|
||||
hovered = false
|
||||
})
|
||||
}
|
||||
|
||||
$: if (_id !== undefined) collapsed = getTreeCollapsed(_id)
|
||||
$: setTreeCollapsed(_id, collapsed)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="antiNav-element tree parent"
|
||||
class:selected
|
||||
class:hovered
|
||||
class:collapsed
|
||||
on:click={() => {
|
||||
collapsed = !collapsed
|
||||
dispatch('click')
|
||||
}}
|
||||
>
|
||||
{#if icon}
|
||||
<div class="an-element__icon folder">
|
||||
<Icon {icon} {iconProps} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="an-element__label" use:tooltip={{ label: getEmbeddedLabel(title) }}>
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="an-element__tool arrow hidden"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
collapsed = !collapsed
|
||||
}}
|
||||
>
|
||||
<IconChevronDown size={'small'} />
|
||||
</div>
|
||||
|
||||
<div class="an-element__grow" />
|
||||
|
||||
<slot name="extra" />
|
||||
|
||||
<div class="w-6">
|
||||
<div class="an-element__tool" class:pressed={hovered} on:click|preventDefault|stopPropagation={onMenuClick}>
|
||||
<IconMoreH size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !collapsed}
|
||||
<div class="antiNav-element__dropbox"><slot /></div>
|
||||
{/if}
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { type Doc, type Ref } from '@hcengineering/core'
|
||||
import { type Asset, getEmbeddedLabel } from '@hcengineering/platform'
|
||||
import type { Action, AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { Icon, IconChevronDown, IconMoreH, Menu, showPopup, tooltip } from '@hcengineering/ui'
|
||||
|
||||
import { getPrefixedTreeCollapsed, setPrefixedTreeCollapsed } from '../../utils'
|
||||
|
||||
export let docId: Ref<Doc>
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let iconProps: Record<string, any> | undefined = undefined
|
||||
export let title: string
|
||||
export let parent: boolean = false
|
||||
export let selected: boolean = false
|
||||
export let folder: boolean = false
|
||||
export let level: number = 0
|
||||
export let getMoreActions: ((originalEvent?: MouseEvent) => Promise<Action[]>) | undefined = undefined
|
||||
export let collapsedPrefix: string = ''
|
||||
export let collapsed: boolean = getPrefixedTreeCollapsed(docId, collapsedPrefix)
|
||||
|
||||
let hovered = false
|
||||
async function onMenuClick (ev: MouseEvent): Promise<void> {
|
||||
if (getMoreActions === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(Menu, { actions: await getMoreActions(ev), ctx: docId }, ev.target as HTMLElement, () => {
|
||||
hovered = false
|
||||
})
|
||||
hovered = true
|
||||
}
|
||||
|
||||
$: if (docId !== undefined) collapsed = getPrefixedTreeCollapsed(docId, collapsedPrefix)
|
||||
$: setPrefixedTreeCollapsed(docId, collapsedPrefix, collapsed)
|
||||
$: hasLeftCollapser = parent && !folder
|
||||
$: hasActions = getMoreActions !== undefined
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="antiNav-element tree"
|
||||
class:selected
|
||||
class:hovered
|
||||
class:parent
|
||||
class:collapsed
|
||||
style:padding-left={`${level > 0 ? level * 1.25 + 0.125 : 0.75}rem`}
|
||||
on:click
|
||||
>
|
||||
<div
|
||||
class="an-element__tool arrow"
|
||||
class:empty={!hasLeftCollapser}
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
if (hasLeftCollapser) collapsed = !collapsed
|
||||
}}
|
||||
>
|
||||
{#if hasLeftCollapser}<IconChevronDown size={'small'} filled />{/if}
|
||||
</div>
|
||||
|
||||
{#if icon}
|
||||
<div class="an-element__icon" class:folder={level === 0}>
|
||||
<Icon {icon} {iconProps} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<span class="an-element__label" use:tooltip={{ label: getEmbeddedLabel(title) }}>
|
||||
{title}
|
||||
</span>
|
||||
{#if parent && folder}
|
||||
<div
|
||||
class="an-element__tool arrow hidden"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
if (parent) collapsed = !collapsed
|
||||
}}
|
||||
>
|
||||
<IconChevronDown size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="an-element__grow" />
|
||||
|
||||
{#if hasActions}
|
||||
<div class="an-element__tool" class:pressed={hovered} on:click|preventDefault|stopPropagation={onMenuClick}>
|
||||
<IconMoreH size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if parent && !collapsed}
|
||||
<div class="antiNav-element__dropbox"><slot /></div>
|
||||
{/if}
|
||||
+24
-34
@@ -16,19 +16,17 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { WithLookup, type Doc, type Ref } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getPlatformColorForTextDef, themeStore } from '@hcengineering/ui'
|
||||
import { getPlatformColorForTextDef, themeStore, getTreeCollapsed } from '@hcengineering/ui'
|
||||
import documents, {
|
||||
type DocumentMeta,
|
||||
type DocumentSpace,
|
||||
type Project,
|
||||
type ProjectMeta
|
||||
} from '@hcengineering/controlled-documents'
|
||||
import { TreeNode } from '@hcengineering/view-resources'
|
||||
|
||||
import FolderCollapsed from '../icons/FolderCollapsed.svelte'
|
||||
import FolderExpanded from '../icons/FolderExpanded.svelte'
|
||||
import DocHierarchyLevel from './DocHierarchyLevel.svelte'
|
||||
import DocHierarchyTreeElement from './DocHierarchyTreeElement.svelte'
|
||||
import { getPrefixedTreeCollapsed, setPrefixedTreeCollapsed, getProjectDocsHierarchy } from '../../utils'
|
||||
import { getProjectDocsHierarchy } from '../../utils'
|
||||
|
||||
export let space: DocumentSpace
|
||||
export let project: Ref<Project> | undefined
|
||||
@@ -36,9 +34,9 @@
|
||||
export let collapsedPrefix: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let collapsed: boolean = getPrefixedTreeCollapsed(space._id, collapsedPrefix)
|
||||
$: setPrefixedTreeCollapsed(space._id, collapsedPrefix, collapsed)
|
||||
$: folderIcon = collapsed ? FolderCollapsed : FolderExpanded
|
||||
// let collapsed: boolean = getPrefixedTreeCollapsed(space._id, collapsedPrefix)
|
||||
// $: setPrefixedTreeCollapsed(space._id, collapsedPrefix, collapsed)
|
||||
// $: folderIcon = collapsed ? FolderCollapsed : FolderExpanded
|
||||
|
||||
let rootDocs: Array<WithLookup<ProjectMeta>> = []
|
||||
let childrenByParent: Record<Ref<DocumentMeta>, Array<WithLookup<ProjectMeta>>> = {}
|
||||
@@ -56,29 +54,21 @@
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<DocHierarchyTreeElement
|
||||
bind:collapsed
|
||||
docId={space?._id}
|
||||
icon={folderIcon}
|
||||
iconProps={{
|
||||
fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
|
||||
}}
|
||||
title={space.name}
|
||||
folder
|
||||
selected={selected === undefined}
|
||||
parent={rootDocs.length > 0}
|
||||
{collapsedPrefix}
|
||||
on:click={() => {
|
||||
dispatch('selected', space)
|
||||
}}
|
||||
>
|
||||
<DocHierarchyLevel projectMeta={rootDocs} {childrenByParent} {selected} {collapsedPrefix} on:selected />
|
||||
</DocHierarchyTreeElement>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.root .antiNav-element) {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
<TreeNode
|
||||
_id={space?._id}
|
||||
folderIcon
|
||||
iconProps={{
|
||||
fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
|
||||
}}
|
||||
title={space.name}
|
||||
highlighted={selected !== undefined}
|
||||
selected={selected === undefined}
|
||||
empty={rootDocs.length === 0}
|
||||
{collapsedPrefix}
|
||||
type={'nested-selectable'}
|
||||
on:click={() => {
|
||||
dispatch('selected', space)
|
||||
}}
|
||||
>
|
||||
<DocHierarchyLevel projectMeta={rootDocs} {childrenByParent} {selected} {collapsedPrefix} on:selected />
|
||||
</TreeNode>
|
||||
|
||||
+69
-26
@@ -16,32 +16,22 @@
|
||||
import { WithLookup, type Doc, type Ref, type Space } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import {
|
||||
type Action,
|
||||
getTreeCollapsed,
|
||||
setTreeCollapsed,
|
||||
getPlatformColorForTextDef,
|
||||
themeStore,
|
||||
navigate,
|
||||
IconEdit,
|
||||
Label
|
||||
} from '@hcengineering/ui'
|
||||
import { getActions as getContributedActions } from '@hcengineering/view-resources'
|
||||
import { type Action, getPlatformColorForTextDef, themeStore, navigate, IconEdit, Label } from '@hcengineering/ui'
|
||||
import { getActions as getContributedActions, TreeNode, TreeItem } from '@hcengineering/view-resources'
|
||||
import { ActionGroup } from '@hcengineering/view'
|
||||
import {
|
||||
type ControlledDocument,
|
||||
type DocumentMeta,
|
||||
type DocumentSpace,
|
||||
type DocumentSpaceType,
|
||||
type Project,
|
||||
type ProjectDocument,
|
||||
type ProjectMeta
|
||||
type ProjectMeta,
|
||||
getDocumentName
|
||||
} from '@hcengineering/controlled-documents'
|
||||
|
||||
import FolderCollapsed from '../icons/FolderCollapsed.svelte'
|
||||
import FolderExpanded from '../icons/FolderExpanded.svelte'
|
||||
import ProjectSelector from '../project/ProjectSelector.svelte'
|
||||
import DocHierarchyLevel from './DocHierarchyLevel.svelte'
|
||||
import DocHierarchyRootElement from './DocHierarchyRootElement.svelte'
|
||||
import { getDocumentIdFromFragment, getProjectDocumentLink } from '../../navigation'
|
||||
import {
|
||||
getCurrentProject,
|
||||
@@ -55,12 +45,16 @@
|
||||
import documents from '../../plugin'
|
||||
|
||||
export let space: DocumentSpace
|
||||
export let currentSpace: Ref<Space> | undefined
|
||||
export let currentFragment: string | undefined
|
||||
export let getActions: (space: Space) => Promise<Action[]> = async () => []
|
||||
export let deselect: boolean = false
|
||||
export let forciblyСollapsed: boolean = false
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let spaceType: DocumentSpaceType | undefined
|
||||
let pressed: boolean = false
|
||||
|
||||
const spaceTypeQuery = createQuery()
|
||||
$: spaceTypeQuery.query(documents.class.DocumentSpaceType, { _id: space.type }, (result) => {
|
||||
@@ -69,10 +63,6 @@
|
||||
|
||||
$: selected = getDocumentIdFromFragment(currentFragment ?? '')
|
||||
|
||||
let collapsed: boolean = getTreeCollapsed(space._id)
|
||||
$: setTreeCollapsed(space._id, collapsed)
|
||||
$: folderIcon = collapsed ? FolderCollapsed : FolderExpanded
|
||||
|
||||
let project: Ref<Project> = documents.ids.NoProject
|
||||
$: void selectProject(space)
|
||||
|
||||
@@ -96,6 +86,35 @@
|
||||
}
|
||||
)
|
||||
|
||||
let selectedControlledDoc: ControlledDocument | undefined = undefined
|
||||
|
||||
$: if (selected !== undefined) {
|
||||
void client
|
||||
.findOne(
|
||||
documents.class.ProjectDocument,
|
||||
{ _id: selected },
|
||||
{
|
||||
lookup: {
|
||||
document: documents.class.ControlledDocument
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((result) => {
|
||||
if (result !== undefined) {
|
||||
selectedControlledDoc = result.$lookup?.document as ControlledDocument
|
||||
} else {
|
||||
// There's some issue with resolving which needs to be fixed later
|
||||
void client
|
||||
.findOne(documents.class.ControlledDocument, { _id: selected as unknown as Ref<ControlledDocument> })
|
||||
.then((result) => {
|
||||
selectedControlledDoc = result
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
selectedControlledDoc = undefined
|
||||
}
|
||||
|
||||
async function selectProject (space: DocumentSpace): Promise<void> {
|
||||
project = getCurrentProject(space._id) ?? (await getLatestProjectId(space._id)) ?? documents.ids.NoProject
|
||||
}
|
||||
@@ -161,17 +180,21 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<DocHierarchyRootElement
|
||||
bind:collapsed
|
||||
<TreeNode
|
||||
_id={space?._id}
|
||||
icon={folderIcon}
|
||||
folderIcon
|
||||
iconProps={{
|
||||
fill: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
|
||||
}}
|
||||
title={space.name}
|
||||
getMoreActions={() => getSpaceActions(space)}
|
||||
highlighted={space._id === currentSpace && currentFragment !== undefined && !deselect}
|
||||
visible={(space._id === currentSpace && currentFragment !== undefined && !deselect) || forciblyСollapsed}
|
||||
showMenu={pressed}
|
||||
{forciblyСollapsed}
|
||||
actions={() => getSpaceActions(space)}
|
||||
type={'nested'}
|
||||
>
|
||||
<div slot="extra">
|
||||
<svelte:fragment slot="extra">
|
||||
{#if spaceType?.projects === true}
|
||||
<ProjectSelector
|
||||
value={project}
|
||||
@@ -180,13 +203,14 @@
|
||||
kind={'ghost'}
|
||||
size={'x-small'}
|
||||
showDropdownIcon
|
||||
bind:pressed
|
||||
on:change={(evt) => {
|
||||
project = evt.detail
|
||||
setCurrentProject(space._id, project)
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
{#if rootDocs.length > 0}
|
||||
<DocHierarchyLevel
|
||||
@@ -203,7 +227,26 @@
|
||||
<Label label={documents.string.NoDocuments} />
|
||||
</div>
|
||||
{/if}
|
||||
</DocHierarchyRootElement>
|
||||
|
||||
<svelte:fragment slot="visible">
|
||||
{#if (selected || forciblyСollapsed) && selectedControlledDoc}
|
||||
{@const doc = selectedControlledDoc}
|
||||
<TreeItem
|
||||
_id={doc._id}
|
||||
icon={documents.icon.Document}
|
||||
iconProps={{
|
||||
fill: 'currentColor'
|
||||
}}
|
||||
title={getDocumentName(doc)}
|
||||
actions={() => getDocumentActions(doc)}
|
||||
selected
|
||||
isFold
|
||||
empty
|
||||
forciblyСollapsed
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TreeNode>
|
||||
|
||||
<style lang="scss">
|
||||
.pseudo-element {
|
||||
|
||||
+5
-1
@@ -26,7 +26,8 @@
|
||||
|
||||
export let value: Ref<Project> | undefined
|
||||
export let space: Ref<Space>
|
||||
export let disabled = false
|
||||
export let disabled: boolean = false
|
||||
export let pressed: boolean = false
|
||||
export let focusIndex = -1
|
||||
export let maxWidth = ''
|
||||
export let kind: ButtonKind = 'link'
|
||||
@@ -48,11 +49,13 @@
|
||||
function showVersionsPopup (): void {
|
||||
if (disabled) return
|
||||
|
||||
pressed = true
|
||||
showPopup(ProjectSelectorPopup, { space, showReadonly, selected: value }, container, (result) => {
|
||||
if (result !== undefined) {
|
||||
value = result
|
||||
dispatch('change', result)
|
||||
}
|
||||
pressed = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -68,6 +71,7 @@
|
||||
{size}
|
||||
{justify}
|
||||
{disabled}
|
||||
stopPropagation
|
||||
on:click={showVersionsPopup}
|
||||
showTooltip={{
|
||||
label: project !== undefined ? getEmbeddedLabel(project.name) : documents.string.Project
|
||||
|
||||
Reference in New Issue
Block a user