mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
Add TagsDropdownEditor. Fix layout. (#1422)
Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
parent
f0cca26e9b
commit
e5187e7d4a
@ -88,6 +88,7 @@ table {
|
||||
--modal-padding: 1.5rem;
|
||||
}
|
||||
|
||||
p { user-select: text; }
|
||||
p:first-child { margin-block-start: 0; } // First and last padding
|
||||
p:last-child { margin-block-end: 0; }
|
||||
|
||||
@ -109,8 +110,8 @@ p:last-child { margin-block-end: 0; }
|
||||
.inline-flex { display: inline-flex; }
|
||||
.flex-grow { flex-grow: 1; }
|
||||
.flex-no-shrink { flex-shrink: 0; }
|
||||
.flex-wrap { flex-wrap: wrap; }
|
||||
.flex-nowrap { flex-wrap: nowrap; }
|
||||
.flex-wrap { flex-wrap: wrap !important; }
|
||||
.flex-nowrap { flex-wrap: nowrap !important; }
|
||||
.flex-baseline {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
@ -446,6 +447,7 @@ a.no-line {
|
||||
.cursor-default { cursor: default; }
|
||||
|
||||
.pointer-events-none { pointer-events: none; }
|
||||
.select-text { user-select: text; }
|
||||
|
||||
/* Text */
|
||||
|
||||
|
@ -188,7 +188,7 @@
|
||||
align-items: center;
|
||||
padding: .75rem;
|
||||
height: auto;
|
||||
border-top: 1px solid var(--button-bg-color);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
|
||||
&.reverse { flex-direction: row-reverse; }
|
||||
&__error {
|
||||
|
@ -80,12 +80,6 @@
|
||||
overflow: hidden;
|
||||
|
||||
&:checked + .checkSVG {
|
||||
& .back {
|
||||
fill: var(--theme-bg-check);
|
||||
&.primary {
|
||||
fill: var(--primary-bg-color);
|
||||
}
|
||||
}
|
||||
& .check {
|
||||
visibility: visible;
|
||||
fill: var(--theme-button-bg-enabled);
|
||||
|
@ -50,16 +50,16 @@
|
||||
|
||||
.icon {
|
||||
margin-right: .25rem;
|
||||
color: var(--theme-content-color);
|
||||
color: var(--content-color);
|
||||
}
|
||||
&:hover .icon { color: var(--theme-caption-color); }
|
||||
&:active .icon { color: var(--theme-content-accent-color); }
|
||||
&:hover .icon { color: var(--accent-color); }
|
||||
&:active .icon { color: var(--caption-color); }
|
||||
}
|
||||
.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--theme-content-trans-color);
|
||||
.icon { color: var(--theme-content-trans-color); }
|
||||
&:hover .icon { color: var(--theme-content-trans-color); }
|
||||
&:active .icon { color: var(--theme-content-trans-color); }
|
||||
color: var(--dark-color);
|
||||
.icon { color: var(--dark-color); }
|
||||
&:hover .icon { color: var(--dark-color); }
|
||||
&:active .icon { color: var(--dark-color); }
|
||||
}
|
||||
</style>
|
||||
|
@ -66,7 +66,7 @@
|
||||
{/if}
|
||||
<div class="flex-col h-full min-h-0" class:background-bg-accent={!transparent}>
|
||||
<Scroller>
|
||||
<div class="p-10">
|
||||
<div class="p-10 select-text">
|
||||
{#if txes}
|
||||
<Grid column={1} rowGap={1.5}>
|
||||
{#each txes as tx (tx.tx._id)}
|
||||
|
@ -47,7 +47,7 @@
|
||||
<style lang="scss">
|
||||
.content {
|
||||
padding: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
color: var(--accent-color);
|
||||
background: var(--theme-bg-accent-color);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
|
@ -399,10 +399,7 @@
|
||||
dispatch('close')
|
||||
}}
|
||||
>
|
||||
<div class="flex-row-center">
|
||||
<div class="mr-4">
|
||||
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:remove={removeAvatar} on:done={onAvatarDone} />
|
||||
</div>
|
||||
<div class="flex-between">
|
||||
<div class="flex-col">
|
||||
<EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} bind:value={firstName} kind={'large-style'} maxWidth={'32rem'} focus />
|
||||
<EditBox placeholder={recruit.string.PersonLastNamePlaceholder} bind:value={lastName} kind={'large-style'} maxWidth={'32rem'} />
|
||||
@ -411,15 +408,28 @@
|
||||
</div>
|
||||
<EditBox placeholder={recruit.string.Location} bind:value={object.city} kind={'small-style'} maxWidth={'32rem'} />
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:remove={removeAvatar} on:done={onAvatarDone} />
|
||||
</div>
|
||||
</div>
|
||||
{#if channels.length > 0}
|
||||
<div class="ml-22"><ChannelsView value={channels} size={'small'} on:click /></div>
|
||||
<ChannelsView value={channels} size={'small'} on:click />
|
||||
{/if}
|
||||
<svelte:fragment slot="pool">
|
||||
<Button
|
||||
icon={contact.icon.SocialEdit}
|
||||
kind={'no-border'}
|
||||
size={'small'}
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => {
|
||||
if (result !== undefined) channels = result
|
||||
})
|
||||
}
|
||||
/>
|
||||
<YesNo label={recruit.string.Onsite} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.onsite} />
|
||||
<YesNo label={recruit.string.Remote} tooltip={recruit.string.WorkLocationPreferences} bind:value={object.remote} />
|
||||
<Component
|
||||
is={tags.component.TagsEditor}
|
||||
is={tags.component.TagsDropdownEditor}
|
||||
props={{
|
||||
items: skills,
|
||||
key,
|
||||
@ -437,30 +447,41 @@
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
<Button
|
||||
icon={contact.icon.SocialEdit}
|
||||
kind={'transparent'}
|
||||
on:click={(ev) =>
|
||||
showPopup(contact.component.SocialEditor, { values: channels }, ev.target, (result) => {
|
||||
if (result !== undefined) channels = result
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon={!resume.uuid && loading ? Spinner : IconAttachment}
|
||||
kind={'transparent'}
|
||||
on:click={() => { inputFile.click() }}
|
||||
/>
|
||||
<input bind:this={inputFile} type="file" name="file" id="file" style="display: none" on:change={fileSelected} />
|
||||
{#if resume.uuid}
|
||||
<Button
|
||||
icon={FileIcon}
|
||||
kind={'link-bordered'}
|
||||
on:click={() => {
|
||||
showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right')
|
||||
}}
|
||||
><svelte:fragment slot="content">{resume.name}</svelte:fragment></Button>
|
||||
{/if}
|
||||
<div
|
||||
class="flex-center resume"
|
||||
class:solid={dragover || resume.uuid}
|
||||
on:dragover|preventDefault={() => {
|
||||
dragover = true
|
||||
}}
|
||||
on:dragleave={() => {
|
||||
dragover = false
|
||||
}}
|
||||
on:drop|preventDefault|stopPropagation={drop}
|
||||
>
|
||||
{#if resume.uuid}
|
||||
<Link
|
||||
label={resume.name}
|
||||
icon={FileIcon}
|
||||
maxLenght={16}
|
||||
on:click={() => {
|
||||
showPopup(PDFViewer, { file: resume.uuid, name: resume.name }, 'right')
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
{#if loading}
|
||||
<Link label={'Uploading...'} icon={Spinner} disabled />
|
||||
{:else}
|
||||
<Link
|
||||
label={'Add or drop resume'}
|
||||
icon={FileUpload}
|
||||
on:click={() => {
|
||||
inputFile.click()
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<input bind:this={inputFile} type="file" name="file" id="file" style="display: none" on:change={fileSelected} />
|
||||
{/if}
|
||||
</div>
|
||||
{#if matches.length > 0}
|
||||
<div class="flex-row-center error-color">
|
||||
<IconInfo size={'small'} />
|
||||
@ -472,3 +493,14 @@
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
<style lang="scss">
|
||||
.resume {
|
||||
padding: .5rem .75rem;
|
||||
background: var(--accent-bg-color);
|
||||
border: 1px dashed var(--divider-color);
|
||||
border-radius: .5rem;
|
||||
|
||||
&.solid { border-style: solid; }
|
||||
}
|
||||
</style>
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'var(--theme-caption-color)'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
|
@ -41,7 +41,7 @@
|
||||
>
|
||||
{name}
|
||||
{#if action}
|
||||
<div class="ml-2">
|
||||
<div class="ml-1">
|
||||
<ActionIcon
|
||||
icon={action}
|
||||
size={'small'}
|
||||
|
@ -57,11 +57,10 @@
|
||||
</script>
|
||||
|
||||
<TagsEditor
|
||||
{elements}
|
||||
bind:elements
|
||||
{key}
|
||||
{items}
|
||||
bind:items
|
||||
targetClass={_class}
|
||||
on:open={(evt) => addRef(evt.detail)}
|
||||
on:delete={(evt) => removeTag(evt.detail)}
|
||||
countLabel={key.attr.label}
|
||||
/>
|
||||
|
@ -0,0 +1,93 @@
|
||||
<!--
|
||||
// Copyright © 2022 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 { AttachedDoc, Class, Collection, Doc, Ref } from '@anticrm/core'
|
||||
import { IntlString, translate } from '@anticrm/platform'
|
||||
import { KeyedAttribute } from '@anticrm/presentation'
|
||||
import { TagElement, TagReference } from '@anticrm/tags'
|
||||
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
|
||||
import { Button, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
import TagsPopup from './TagsPopup.svelte'
|
||||
|
||||
export let items: TagReference[] = []
|
||||
export let targetClass: Ref<Class<Doc>>
|
||||
export let key: KeyedAttribute
|
||||
export let elements: Map<Ref<TagElement>, TagElement>
|
||||
export let countLabel: IntlString
|
||||
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let keyLabel: string = ''
|
||||
|
||||
$: itemLabel = (key.attr.type as Collection<AttachedDoc>).itemLabel
|
||||
|
||||
$: translate(itemLabel ?? key.attr.label, {}).then((v) => {
|
||||
keyLabel = v
|
||||
})
|
||||
|
||||
async function addRef (tag: TagElement): Promise<void> {
|
||||
dispatch('open', tag)
|
||||
}
|
||||
async function addTag (evt: Event): Promise<void> {
|
||||
showPopup(
|
||||
TagsPopup,
|
||||
{
|
||||
targetClass,
|
||||
selected: items.map((it) => it.tag),
|
||||
keyLabel
|
||||
},
|
||||
evt.target as HTMLElement,
|
||||
() => { },
|
||||
(result) => {
|
||||
if (result != undefined) {
|
||||
if (result.action === 'add') addRef(result.tag)
|
||||
else if (result.action === 'remove') removeTag(items.filter(it => it.tag === result.tag._id)[0]._id)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function removeTag (id: Ref<TagReference>): Promise<void> {
|
||||
dispatch('delete', id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tooltip label={key.attr.label} direction={labelDirection}>
|
||||
<Button
|
||||
icon={tags.icon.Tags}
|
||||
label={items.length > 0 ? undefined : key.attr.label}
|
||||
width={width ?? 'min-content'}
|
||||
{kind} {size} {justify}
|
||||
on:click={addTag}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
{#if items.length > 0}
|
||||
<div class="flex-row-center flex-nowrap">
|
||||
{#await translate(countLabel, { count: items.length }) then text}
|
||||
{text}
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
</Tooltip>
|
@ -18,23 +18,17 @@
|
||||
import { KeyedAttribute } from '@anticrm/presentation'
|
||||
import { TagElement, TagReference } from '@anticrm/tags'
|
||||
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
|
||||
import { Button, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { ShowMore, Label, CircleButton, Button, showPopup, Tooltip, IconAdd, IconClose } from '@anticrm/ui'
|
||||
import { createEventDispatcher, afterUpdate } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
import TagsPopup from './TagsPopup.svelte'
|
||||
import TagItem from './TagItem.svelte'
|
||||
|
||||
export let items: TagReference[] = []
|
||||
export let targetClass: Ref<Class<Doc>>
|
||||
export let key: KeyedAttribute
|
||||
export let showTitle = true
|
||||
export let elements: Map<Ref<TagElement>, TagElement>
|
||||
export let countLabel: IntlString
|
||||
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let labelDirection: TooltipAlignment | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -54,12 +48,18 @@
|
||||
TagsPopup,
|
||||
{
|
||||
targetClass,
|
||||
addRef,
|
||||
removeTag,
|
||||
selected: items.map((it) => it.tag),
|
||||
keyLabel
|
||||
keyLabel,
|
||||
hideAdd: true
|
||||
},
|
||||
evt.target as HTMLElement
|
||||
evt.target as HTMLElement,
|
||||
() => { },
|
||||
(result) => {
|
||||
if (result != undefined) {
|
||||
if (result.action === 'add') addRef(result.tag)
|
||||
else if (result.action === 'remove') removeTag(items.filter(it => it.tag === result.tag._id)[0]._id)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -68,22 +68,68 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tooltip label={key.attr.label} direction={labelDirection}>
|
||||
<Button
|
||||
icon={tags.icon.Tags}
|
||||
label={items.length > 0 ? undefined : key.attr.label}
|
||||
width={width ?? 'min-content'}
|
||||
{kind} {size} {justify}
|
||||
on:click={addTag}
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
{#if items.length > 0}
|
||||
<div class="flex-row-center flex-nowrap">
|
||||
{#await translate(countLabel, { count: items.length }) then text}
|
||||
{text}
|
||||
{/await}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div class="flex-row">
|
||||
{#if showTitle}
|
||||
<div class="flex-row-center">
|
||||
<div class="title">
|
||||
<Label label={key.attr.label} />
|
||||
</div>
|
||||
<div id='add-tag'>
|
||||
<Tooltip label={tags.string.AddTagTooltip} props={{ word: keyLabel }}>
|
||||
<CircleButton icon={IconAdd} size={'small'} selected on:click={addTag} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<ShowMore ignore={!showTitle}>
|
||||
<div class:tags-container={showTitle} class:mt-4={showTitle}>
|
||||
<div class="tag-items" class:tag-items-scroll={!showTitle}>
|
||||
{#if items.length === 0}
|
||||
{#if keyLabel}
|
||||
<div class="flex flex-grow title-center">
|
||||
<Label label={tags.string.NoItems} params={{ word: keyLabel }} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#each items as tag}
|
||||
<TagItem
|
||||
{tag}
|
||||
element={elements.get(tag.tag)}
|
||||
action={IconClose}
|
||||
on:action={() => {
|
||||
removeTag(tag._id)
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</ShowMore>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.title {
|
||||
margin-right: 0.75rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.tags-container {
|
||||
padding: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
background: var(--theme-bg-accent-color);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
.tag-items {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tag-items-scroll {
|
||||
overflow-y: scroll;
|
||||
max-height: 10rem;
|
||||
}
|
||||
.title-center {
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
@ -17,7 +17,7 @@
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import presentation, { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { TagCategory, TagElement, TagReference } from '@anticrm/tags'
|
||||
import { CheckBox, Button, Icon, IconAdd, IconClose, Label, showPopup, getPlatformColor } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
@ -27,10 +27,9 @@
|
||||
|
||||
export let targetClass: Ref<Class<Doc>>
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let addRef: (tag: TagElement) => Promise<void>
|
||||
export let removeTag: (tag: TagElement) => Promise<void>
|
||||
export let selected: Ref<TagElement>[] = []
|
||||
export let keyLabel: string = ''
|
||||
export let hideAdd: boolean = false
|
||||
|
||||
let search: string = ''
|
||||
let searchElement: HTMLInputElement
|
||||
@ -60,10 +59,6 @@
|
||||
async function createTagElement (): Promise<void> {
|
||||
showPopup(CreateTagElement, { targetClass }, 'top')
|
||||
}
|
||||
async function addTag (element: TagElement): Promise<void> {
|
||||
await addRef(element)
|
||||
selected = [...selected, element._id]
|
||||
}
|
||||
|
||||
const isSelected = (element: TagElement): boolean => {
|
||||
if (selected.filter(p => p === element._id).length > 0) return true
|
||||
@ -72,14 +67,14 @@
|
||||
const checkSelected = (element: TagElement): void => {
|
||||
if (isSelected(element)) {
|
||||
selected = selected.filter(p => p !== element._id)
|
||||
removeTag(element)
|
||||
dispatch('update', { action: 'remove', tag: element })
|
||||
} else {
|
||||
selected.push(element._id)
|
||||
addTag(element)
|
||||
selected = [...selected, element._id]
|
||||
dispatch('update', { action: 'add', tag: element })
|
||||
}
|
||||
objects = objects
|
||||
categories = categories
|
||||
dispatch('update', selected)
|
||||
dispatch('update', { action: 'selected', selected: selected})
|
||||
}
|
||||
const toggleGroup = (ev: MouseEvent): void => {
|
||||
const el: HTMLElement = ev.currentTarget as HTMLElement
|
||||
@ -106,7 +101,7 @@
|
||||
{#if search !== ''}<div class="icon"><Icon icon={IconClose} size={'inline'} /></div>{/if}
|
||||
</div>
|
||||
<Button kind={'transparent'} size={'small'} icon={show ? IconView : IconViewHide} on:click={() => show = !show} />
|
||||
<Button kind={'transparent'} size={'small'} icon={IconAdd} on:click={createTagElement} />
|
||||
{#if !hideAdd}<Button kind={'transparent'} size={'small'} icon={IconAdd} on:click={createTagElement} />{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -21,6 +21,7 @@ import TagsPresenter from './components/TagsPresenter.svelte'
|
||||
import TagsItemPresenter from './components/TagsItemPresenter.svelte'
|
||||
import TagsView from './components/TagsView.svelte'
|
||||
import TagsEditor from './components/TagsEditor.svelte'
|
||||
import TagsDropdownEditor from './components/TagsDropdownEditor.svelte'
|
||||
import CategoryPresenter from './components/CategoryPresenter.svelte'
|
||||
import tags from './plugin'
|
||||
import TagsCategoryBar from './components/CategoryBar.svelte'
|
||||
@ -33,6 +34,7 @@ export default async (): Promise<Resources> => ({
|
||||
TagsPresenter,
|
||||
TagsView,
|
||||
TagsEditor,
|
||||
TagsDropdownEditor,
|
||||
TagsItemPresenter,
|
||||
CategoryPresenter,
|
||||
TagsCategoryBar
|
||||
|
@ -79,6 +79,7 @@ const tagsPlugin = plugin(tagsId, {
|
||||
component: {
|
||||
TagsView: '' as AnyComponent,
|
||||
TagsEditor: '' as AnyComponent,
|
||||
TagsDropdownEditor: '' as AnyComponent,
|
||||
TagsCategoryBar: '' as AnyComponent
|
||||
},
|
||||
category: {
|
||||
|
Loading…
Reference in New Issue
Block a user