mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-22 20:32:24 +02:00
flush to develop (#380)
Signed-off-by: Alexander Platov <sas_lord@mail.ru> Co-authored-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com> Co-authored-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
co-authored by
Denis Bykhov
Andrey Sobolev
parent
cd0e7da28e
commit
68ae2f2e31
@@ -20,14 +20,14 @@
|
||||
import type { Channel, ChannelProvider } from '@anticrm/contact'
|
||||
import { getClient } from '..'
|
||||
|
||||
import { AnyComponent, Icon } from '@anticrm/ui'
|
||||
import IconCopy from './icons/Copy.svelte'
|
||||
import { Tooltip, CircleButton } from '@anticrm/ui'
|
||||
import ChannelsPopup from './ChannelsPopup.svelte'
|
||||
|
||||
import contact from '@anticrm/contact'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let value: Channel[] | null
|
||||
export let size: 'small' | 'medium' = 'medium'
|
||||
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'large'
|
||||
export let reverse: boolean = false
|
||||
|
||||
interface Item {
|
||||
@@ -69,143 +69,20 @@
|
||||
$: if (value) update(value)
|
||||
|
||||
let displayItems: Item[] = []
|
||||
let divHTML: HTMLElement
|
||||
</script>
|
||||
|
||||
<div class="container" class:reverse>
|
||||
<div
|
||||
bind:this={divHTML}
|
||||
class="flex-row-center"
|
||||
class:safari-gap-1={size === 'small'}
|
||||
class:safari-gap-2={size !== 'small'}
|
||||
class:reverse
|
||||
on:click|stopPropagation={() => {}}
|
||||
>
|
||||
{#each displayItems as item}
|
||||
<div on:click|stopPropagation={() => {dispatch('click', item)}}>
|
||||
<div class="circle list list-{size}">
|
||||
<div class="icon" class:small={size === 'small'}>
|
||||
<Icon icon={item.icon} size={'small'}/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window {reverse ? 'right' : 'left'}">
|
||||
<div class="circle circle-icon">
|
||||
<div class="icon"><Icon icon={item.icon} size={'small'}/></div>
|
||||
</div>
|
||||
<div class="flex-grow flex-col caption-color">
|
||||
<div class="overflow-label label">{item.label}</div>
|
||||
<div class="overflow-label">{item.value}</div>
|
||||
</div>
|
||||
<div class="button" on:click|preventDefault={() => { alert('Copied: ' + item.value) }}>
|
||||
<IconCopy size={'small'}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip component={ChannelsPopup} props={{ value: item }} label={undefined} anchor={divHTML}>
|
||||
<CircleButton icon={item.icon} {size} />
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.circle {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--theme-bg-focused-color);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
.small {
|
||||
transform-origin: center center;
|
||||
transform: scale(.55);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
margin-right: .75rem;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
|
||||
.icon {
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
&:hover {
|
||||
border-color: var(--theme-bg-focused-border);
|
||||
z-index: 5;
|
||||
.icon { opacity: 1; }
|
||||
& + .window {
|
||||
z-index: 4;
|
||||
visibility: visible;
|
||||
}
|
||||
&::after { content: ''; }
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: -1rem;
|
||||
left: -100%;
|
||||
right: -100%;
|
||||
clip-path: polygon(0 100%, 25% 50%, 25% 0, 75% 0, 75% 50%, 100% 100%);
|
||||
// background-color: rgba(255, 255, 0, .5);
|
||||
}
|
||||
&:first-child:after {
|
||||
left: 0;
|
||||
right: -100%;
|
||||
clip-path: polygon(0 0, 0 100%, 100% 100%, 65% 50%, 65% 0);
|
||||
}
|
||||
}
|
||||
.list-small {
|
||||
margin-right: .25rem;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
.list-medium {
|
||||
margin-right: .5rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
&.reverse {
|
||||
flex-direction: row-reverse;
|
||||
.list:first-child:after {
|
||||
left: -100%;
|
||||
right: 0;
|
||||
clip-path: polygon(0 100%, 35% 50%, 35% 0, 100% 0, 100% 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.window {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
top: calc(100% + .5rem);
|
||||
min-width: 100%;
|
||||
background-color: var(--theme-button-bg-focused);
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: .75rem;
|
||||
box-shadow: 0 .75rem 1.25rem rgba(0, 0, 0, .2);
|
||||
visibility: hidden;
|
||||
|
||||
&:hover {
|
||||
z-index: 4;
|
||||
visibility: visible;
|
||||
}
|
||||
&.right { right: 0; }
|
||||
&.left { left: 0; }
|
||||
}
|
||||
}
|
||||
.label {
|
||||
font-weight: 500;
|
||||
font-size: .75rem;
|
||||
}
|
||||
.button {
|
||||
margin-left: 1.5rem;
|
||||
opacity: .4;
|
||||
cursor: pointer;
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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 { IntlString, Asset } from '@anticrm/platform'
|
||||
import { CircleButton, closeTooltip } from '@anticrm/ui'
|
||||
import IconCopy from './icons/Copy.svelte'
|
||||
|
||||
interface Item {
|
||||
label: IntlString,
|
||||
icon: Asset,
|
||||
value: string
|
||||
}
|
||||
|
||||
export let value: Item
|
||||
|
||||
const copyLink = (): void => {
|
||||
navigator.clipboard
|
||||
.writeText(value.value)
|
||||
// .then(() => {
|
||||
// console.log('Copied!', value.value)
|
||||
// })
|
||||
// .catch((err) => {
|
||||
// console.log('Something went wrong', err)
|
||||
// })
|
||||
closeTooltip()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-row-center">
|
||||
<CircleButton icon={value.icon} size={'x-large'} />
|
||||
<div class="flex-col caption-color ml-3">
|
||||
<div class="label">{value.label}</div>
|
||||
<div class="overflow-label">{value.value}</div>
|
||||
</div>
|
||||
<div class="button" on:click|preventDefault={copyLink}>
|
||||
<IconCopy size={'small'}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label {
|
||||
font-weight: 500;
|
||||
font-size: .75rem;
|
||||
}
|
||||
.button {
|
||||
margin-left: 1.5rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
cursor: pointer;
|
||||
&:hover { color: var(--theme-caption-color); }
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<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">
|
||||
|
||||
@@ -37,6 +37,14 @@
|
||||
<code><svelte:self nodes={node.childNodes}/></code>
|
||||
{:else if node.nodeName === 'BR'}
|
||||
<br/>
|
||||
{:else if node.nodeName === 'H1'}
|
||||
<h1><svelte:self nodes={node.childNodes}/></h1>
|
||||
{:else if node.nodeName === 'H2'}
|
||||
<h2><svelte:self nodes={node.childNodes}/></h2>
|
||||
{:else if node.nodeName === 'UL'}
|
||||
<ul><svelte:self nodes={node.childNodes}/></ul>
|
||||
{:else if node.nodeName === 'LI'}
|
||||
<li><svelte:self nodes={node.childNodes}/></li>
|
||||
{:else if node.nodeName === 'SPAN'}
|
||||
<Person objectId={node.getAttribute('data-id')} title={node.getAttribute('data-label')} />
|
||||
{:else}
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
--primary-button-outline: rgba(87, 132, 255, .3);
|
||||
--primary-button-border: rgba(255, 255, 255, .09);
|
||||
|
||||
--duotone-color: rgba(126, 134, 158, .25);
|
||||
|
||||
--trans-primary-button-color: #fff;
|
||||
--trans-primary-button-bg: #4474F6;
|
||||
--trans-primary-button-border: transparent;
|
||||
|
||||
@@ -131,6 +131,9 @@ table {
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
}
|
||||
.flex-reverse {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
.flex-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -146,6 +149,25 @@ table {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.safari-gap-1 {
|
||||
& > * { margin-right: .25rem; }
|
||||
& > *:last-child { margin-right: 0; }
|
||||
&.reverse {
|
||||
flex-direction: row-reverse;
|
||||
& > :last-child { margin-right: .25rem; }
|
||||
& > :first-child { margin-right: 0; }
|
||||
}
|
||||
}
|
||||
.safari-gap-2 {
|
||||
& > * { margin-right: .5rem; }
|
||||
& > *:last-child { margin-right: 0; }
|
||||
&.reverse {
|
||||
flex-direction: row-reverse;
|
||||
& > :last-child { margin-right: .5rem; }
|
||||
& > :first-child { margin-right: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
/* --------- */
|
||||
.sm-tool-icon {
|
||||
display: flex;
|
||||
@@ -170,10 +192,13 @@ table {
|
||||
.step-lr75 + .step-lr75 { margin-left: .75rem; }
|
||||
.step-tb75 + .step-tb75 { margin-top: .75rem; }
|
||||
|
||||
.ml-1 { margin-left: .25rem; }
|
||||
.ml-2 { margin-left: .5rem; }
|
||||
.ml-3 { margin-left: .75rem; }
|
||||
.mr-1 { margin-right: .25rem; }
|
||||
.mr-4 { margin-right: 1rem; }
|
||||
.mr-8 { margin-right: 2rem; }
|
||||
.mt-2 { margin-top: .5rem; }
|
||||
.mt-5 { margin-top: 1.25rem; }
|
||||
.mb-1 { margin-bottom: .25rem; }
|
||||
|
||||
|
||||
@@ -19,15 +19,16 @@
|
||||
import Icon from './Icon.svelte'
|
||||
|
||||
export let icon: Asset | AnySvelteComponent
|
||||
export let size: 'small' | 'medium' | 'large' = 'large'
|
||||
export let size: 'small' | 'medium' | 'large' | 'x-large' = 'large'
|
||||
export let transparent: boolean = false
|
||||
export let selected: boolean = false
|
||||
export let primary: boolean = false
|
||||
</script>
|
||||
|
||||
<div class="icon-button icon-{size}" class:selected class:transparent on:click>
|
||||
<div class="icon-button icon-{size}" class:selected class:transparent class:primary on:click on:mousemove>
|
||||
<div class="content">
|
||||
{#if typeof (icon) === 'string'}
|
||||
<Icon {icon} size={'small'}/>
|
||||
<Icon {icon} size={'small'} />
|
||||
{:else}
|
||||
<svelte:component this={icon} size={'small'} />
|
||||
{/if}
|
||||
@@ -49,15 +50,27 @@
|
||||
transform: scale(.75);
|
||||
pointer-events: none;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
border-color: var(--theme-bg-focused-border);
|
||||
}
|
||||
&:active {
|
||||
color: var(--theme-content-accent-color);
|
||||
background-color: var(--theme-bg-accent-color);
|
||||
}
|
||||
&.selected { background-color: var(--theme-button-bg-hovered); }
|
||||
&.transparent { background-color: rgba(31, 31, 37, .3); }
|
||||
&:hover { border-color: var(--theme-bg-focused-border); }
|
||||
&:active { background-color: var(--theme-bg-accent-color); }
|
||||
&.primary {
|
||||
background-color: var(--primary-button-enabled);
|
||||
border-color: var(--primary-button-border);
|
||||
&:hover { background-color: var(--primary-button-hovered); }
|
||||
&:active { background-color: var(--primary-button-pressed); }
|
||||
}
|
||||
}
|
||||
.icon-small {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
.content { transform: scale(.55); }
|
||||
.content { transform: scale(.6); }
|
||||
}
|
||||
.icon-medium {
|
||||
width: 1.75rem;
|
||||
@@ -66,5 +79,11 @@
|
||||
.icon-large {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
.content { transform: scale(.9); }
|
||||
}
|
||||
.icon-x-large {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
.content { transform: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
import { tooltipstore as tooltip, showTooltip } from '..'
|
||||
|
||||
export let label: IntlString | undefined
|
||||
export let direction: TooltipAligment | undefined
|
||||
export let direction: TooltipAligment | undefined = undefined
|
||||
export let component: AnySvelteComponent | AnyComponent | undefined = undefined
|
||||
export let props: any | undefined = undefined
|
||||
export let anchor: HTMLElement | undefined = undefined
|
||||
|
||||
let triggerHTML: HTMLElement
|
||||
let shown: boolean = false
|
||||
@@ -32,7 +33,7 @@
|
||||
class="tooltip-trigger"
|
||||
bind:this={triggerHTML}
|
||||
on:mousemove={() => {
|
||||
if (!shown) showTooltip(label, triggerHTML, direction, component, props)
|
||||
if (!shown) showTooltip(label, triggerHTML, direction, component, props, anchor)
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { tooltipstore as tooltip, closeTooltip } from '..'
|
||||
import type { TooltipAligment } from '..'
|
||||
@@ -20,67 +21,70 @@
|
||||
let tooltipHTML: HTMLElement
|
||||
let dir: TooltipAligment
|
||||
let rect: DOMRect
|
||||
let rectAnchor: DOMRect
|
||||
let tooltipSW: boolean // tooltipSW = true - Label; false - Component
|
||||
|
||||
$: tooltipSW = $tooltip.component ? false : true
|
||||
$: {
|
||||
if ($tooltip.label && tooltipHTML) {
|
||||
if (($tooltip.label || $tooltip.component) && tooltipHTML) {
|
||||
if ($tooltip.element) {
|
||||
rect = $tooltip.element.getBoundingClientRect()
|
||||
const doc = document.body.getBoundingClientRect()
|
||||
rect = $tooltip.element.getBoundingClientRect()
|
||||
rectAnchor = ($tooltip.anchor) ? $tooltip.anchor.getBoundingClientRect()
|
||||
: $tooltip.element.getBoundingClientRect()
|
||||
|
||||
if ($tooltip.component) {
|
||||
|
||||
if (rect.bottom + tooltipHTML.clientHeight + 28 < doc.height) {
|
||||
tooltipHTML.style.top = `calc(${rect.bottom}px + .75rem)`
|
||||
if (rectAnchor.bottom + tooltipHTML.clientHeight + 28 < doc.height) {
|
||||
tooltipHTML.style.top = `calc(${rectAnchor.bottom}px + .75rem)`
|
||||
dir = 'bottom'
|
||||
} else if (rect.top > doc.height - rect.bottom) {
|
||||
tooltipHTML.style.bottom = `calc(${doc.height - rect.y}px + .75rem)`
|
||||
if (tooltipHTML.clientHeight > rect.top - 28) {
|
||||
} else if (rectAnchor.top > doc.height - rectAnchor.bottom) {
|
||||
tooltipHTML.style.bottom = `calc(${doc.height - rectAnchor.y}px + .75rem)`
|
||||
if (tooltipHTML.clientHeight > rectAnchor.top - 28) {
|
||||
tooltipHTML.style.top = '1rem'
|
||||
tooltipHTML.style.height = rect.top - 28 + 'px'
|
||||
tooltipHTML.style.height = rectAnchor.top - 28 + 'px'
|
||||
}
|
||||
dir = 'top'
|
||||
} else {
|
||||
tooltipHTML.style.top = `calc(${rect.bottom}px + .75rem)`
|
||||
if (tooltipHTML.clientHeight > doc.height - rect.bottom - 28) {
|
||||
tooltipHTML.style.top = `calc(${rectAnchor.bottom}px + .75rem)`
|
||||
if (tooltipHTML.clientHeight > doc.height - rectAnchor.bottom - 28) {
|
||||
tooltipHTML.style.bottom = '1rem'
|
||||
tooltipHTML.style.height = doc.height - rect.bottom - 28 + 'px'
|
||||
tooltipHTML.style.height = doc.height - rectAnchor.bottom - 28 + 'px'
|
||||
}
|
||||
dir = 'bottom'
|
||||
}
|
||||
if (rect.left + tooltipHTML.clientWidth + 16 > doc.width) {
|
||||
if (rectAnchor.left + tooltipHTML.clientWidth + 16 > doc.width) {
|
||||
tooltipHTML.style.left = ''
|
||||
tooltipHTML.style.right = doc.width - rect.right + 'px'
|
||||
tooltipHTML.style.right = doc.width - rectAnchor.right + 'px'
|
||||
} else {
|
||||
tooltipHTML.style.left = rect.left + 'px'
|
||||
tooltipHTML.style.left = rectAnchor.left + 'px'
|
||||
tooltipHTML.style.right = ''
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (!$tooltip.direction) {
|
||||
if (rect.right < doc.width / 5) dir = 'right'
|
||||
else if (rect.left > doc.width - doc.width / 5) dir = 'left'
|
||||
else if (rect.top < tooltipHTML.clientHeight) dir = 'bottom'
|
||||
if (rectAnchor.right < doc.width / 5) dir = 'right'
|
||||
else if (rectAnchor.left > doc.width - doc.width / 5) dir = 'left'
|
||||
else if (rectAnchor.top < tooltipHTML.clientHeight) dir = 'bottom'
|
||||
else dir = 'top'
|
||||
} else dir = $tooltip.direction
|
||||
|
||||
if (dir === 'right') {
|
||||
tooltipHTML.style.top = rect.y + rect.height / 2 + 'px'
|
||||
tooltipHTML.style.left = `calc(${rect.right}px + .75rem)`
|
||||
tooltipHTML.style.top = rectAnchor.y + rectAnchor.height / 2 + 'px'
|
||||
tooltipHTML.style.left = `calc(${rectAnchor.right}px + .75rem)`
|
||||
tooltipHTML.style.transform = 'translateY(-50%)'
|
||||
} else if (dir === 'left') {
|
||||
tooltipHTML.style.top = rect.y + rect.height / 2 + 'px'
|
||||
tooltipHTML.style.right = `calc(${doc.width - rect.x}px + .75rem)`
|
||||
tooltipHTML.style.top = rectAnchor.y + rectAnchor.height / 2 + 'px'
|
||||
tooltipHTML.style.right = `calc(${doc.width - rectAnchor.x}px + .75rem)`
|
||||
tooltipHTML.style.transform = 'translateY(-50%)'
|
||||
} else if (dir === 'bottom') {
|
||||
tooltipHTML.style.top = `calc(${rect.bottom}px + .5rem)`
|
||||
tooltipHTML.style.left = rect.x + rect.width / 2 + 'px'
|
||||
tooltipHTML.style.top = `calc(${rectAnchor.bottom}px + .5rem)`
|
||||
tooltipHTML.style.left = rectAnchor.x + rectAnchor.width / 2 + 'px'
|
||||
tooltipHTML.style.transform = 'translateX(-50%)'
|
||||
} else if (dir === 'top') {
|
||||
tooltipHTML.style.bottom = `calc(${doc.height - rect.y}px + .75rem)`
|
||||
tooltipHTML.style.left = rect.x + rect.width / 2 + 'px'
|
||||
tooltipHTML.style.bottom = `calc(${doc.height - rectAnchor.y}px + .75rem)`
|
||||
tooltipHTML.style.left = rectAnchor.x + rectAnchor.width / 2 + 'px'
|
||||
tooltipHTML.style.transform = 'translateX(-50%)'
|
||||
}
|
||||
tooltipHTML.classList.remove('no-arrow')
|
||||
@@ -106,17 +110,16 @@
|
||||
const whileShow = (ev: MouseEvent): void => {
|
||||
if ($tooltip.element && tooltipHTML) {
|
||||
const rectP = tooltipHTML.getBoundingClientRect()
|
||||
const rectT = {
|
||||
top: (dir === 'top') ? rect.top - 16 : rect.top,
|
||||
bottom: (dir === 'bottom') ? rect.bottom + 16 : rect.bottom,
|
||||
left: (dir === 'left') ? rect.left - 16 : rect.left,
|
||||
right: (dir === 'right') ? rect.right + 16 : rect.right
|
||||
const dT: number = dir === 'bottom' ? 12 : 0
|
||||
const dB: number = dir === 'top' ? 12 : 0
|
||||
const inTrigger: boolean = ev.x >= rect.left && ev.x <= rect.right && ev.y >= rect.top && ev.y <= rect.bottom
|
||||
const inPopup: boolean =
|
||||
ev.x >= rectP.left && ev.x <= rectP.right && ev.y >= rectP.top - dT && ev.y <= rectP.bottom + dB
|
||||
if (tooltipSW) {
|
||||
if (!inTrigger) hideTooltip()
|
||||
} else {
|
||||
if (!(inTrigger || inPopup)) hideTooltip()
|
||||
}
|
||||
const inTrigger: boolean = (ev.x >= rect.left && ev.x <= rect.right && ev.y >= rect.top && ev.y <= rect.bottom)
|
||||
const inTriggerWS: boolean = (ev.x >= rectT.left && ev.x <= rectT.right && ev.y >= rectT.top && ev.y <= rectT.bottom)
|
||||
const inPopup: boolean = (ev.x >= rectP.left && ev.x <= rectP.right && ev.y >= rectP.top && ev.y <= rectP.bottom)
|
||||
if (tooltipSW) { if (!inTrigger) hideTooltip() }
|
||||
else { if (!(inTriggerWS || inPopup)) hideTooltip() }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -127,15 +127,16 @@ export const tooltipstore = writable<LabelAndProps>({
|
||||
element: undefined,
|
||||
direction: undefined,
|
||||
component: undefined,
|
||||
props: undefined
|
||||
props: undefined,
|
||||
anchor: undefined
|
||||
})
|
||||
|
||||
export function showTooltip (label: IntlString | undefined, element: HTMLElement, direction?: TooltipAligment, component?: AnySvelteComponent | AnyComponent, props?: any): void {
|
||||
tooltipstore.set({ label: label, element: element, direction: direction, component: component, props: props })
|
||||
export function showTooltip (label: IntlString | undefined, element: HTMLElement, direction?: TooltipAligment, component?: AnySvelteComponent | AnyComponent, props?: any, anchor?: HTMLElement): void {
|
||||
tooltipstore.set({ label: label, element: element, direction: direction, component: component, props: props, anchor: anchor })
|
||||
}
|
||||
|
||||
export function closeTooltip (): void {
|
||||
tooltipstore.set({ label: undefined, element: undefined, direction: undefined, component: undefined, props: undefined })
|
||||
tooltipstore.set({ label: undefined, element: undefined, direction: undefined, component: undefined, props: undefined, anchor: undefined })
|
||||
}
|
||||
|
||||
export const ticker = readable(Date.now(), set => {
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface LabelAndProps {
|
||||
direction?: TooltipAligment
|
||||
component?: AnySvelteComponent | AnyComponent
|
||||
props?: any
|
||||
anchor: HTMLElement | undefined
|
||||
}
|
||||
|
||||
export interface ListItem {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { CircleButton, IconAdd, showPopup, Spinner } from '@anticrm/ui'
|
||||
import { CircleButton, IconAdd, showPopup, Spinner, Link } from '@anticrm/ui'
|
||||
|
||||
import type { Doc, Ref, Space, Class, Bag } from '@anticrm/core'
|
||||
import { setPlatformStatus, unknownError } from '@anticrm/platform'
|
||||
@@ -24,6 +24,7 @@
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
|
||||
import { uploadFile } from '../utils'
|
||||
import Upload from './icons/Upload.svelte'
|
||||
|
||||
import chunter from '@anticrm/chunter'
|
||||
|
||||
@@ -37,12 +38,12 @@
|
||||
$: query.query(chunter.class.Attachment, { attachedTo: objectId }, result => { attachments = result })
|
||||
|
||||
let inputFile: HTMLInputElement
|
||||
let loading = false
|
||||
let loading = 0
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function createAttachment(file: File) {
|
||||
loading = true
|
||||
loading++
|
||||
try {
|
||||
const uuid = await uploadFile(space, file, objectId)
|
||||
console.log('uploaded file uuid', uuid)
|
||||
@@ -56,16 +57,30 @@
|
||||
} catch (err: any) {
|
||||
setPlatformStatus(unknownError(err))
|
||||
} finally {
|
||||
loading = false
|
||||
loading--
|
||||
}
|
||||
}
|
||||
|
||||
function fileSelected() {
|
||||
function fileSelected () {
|
||||
console.log(inputFile.files)
|
||||
const file = inputFile.files?.[0]
|
||||
if (file !== undefined) { createAttachment(file) }
|
||||
const list = inputFile.files
|
||||
if (list === null || list.length === 0) return
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const file = list.item(index)
|
||||
if (file !== null) createAttachment(file)
|
||||
}
|
||||
}
|
||||
|
||||
function fileDrop (e: DragEvent) {
|
||||
const list = e.dataTransfer?.files
|
||||
if (list === undefined || list.length === 0) return
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const file = list.item(index)
|
||||
if (file !== null) createAttachment(file)
|
||||
}
|
||||
}
|
||||
|
||||
let dragover = false
|
||||
</script>
|
||||
|
||||
<div class="attachments-container">
|
||||
@@ -76,14 +91,30 @@
|
||||
{:else}
|
||||
<CircleButton icon={IconAdd} size={'small'} on:click={ () => { inputFile.click() } } />
|
||||
{/if}
|
||||
<input bind:this={inputFile} type="file" name="file" id="file" style="display: none" on:change={fileSelected}/>
|
||||
<input bind:this={inputFile} multiple type="file" name="file" id="file" style="display: none" on:change={fileSelected}/>
|
||||
</div>
|
||||
<Table
|
||||
_class={chunter.class.Attachment}
|
||||
config={['', 'lastModified']}
|
||||
options={ {} }
|
||||
query={ { attachedTo: objectId } }
|
||||
/>
|
||||
|
||||
{#if attachments.length === 0 && !loading}
|
||||
<div class="flex-col-center mt-5 resume" class:solid={dragover}
|
||||
on:dragover|preventDefault={ () => { dragover = true } }
|
||||
on:dragleave={ () => { dragover = false } }
|
||||
on:drop|preventDefault|stopPropagation={fileDrop}
|
||||
on:click={ () => { inputFile.click() } }
|
||||
>
|
||||
<Upload size={'large'} />
|
||||
<div class="small-text content-dark-color mt-2">There are no attachments for this candidate.</div>
|
||||
<div class="small-text">
|
||||
Upload or drop files here
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<Table
|
||||
_class={chunter.class.Attachment}
|
||||
config={['', 'lastModified']}
|
||||
options={ {} }
|
||||
query={ { attachedTo: objectId } }
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -99,44 +130,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.table-body {
|
||||
margin-top: .75rem;
|
||||
|
||||
th, td {
|
||||
padding: .75rem 0;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
font-weight: 500;
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
td {
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.tr-body { border-top: 1px solid var(--theme-button-border-hovered); }
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .75rem 1rem;
|
||||
}
|
||||
.file-icon {
|
||||
margin-right: 1.25rem;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.625rem;
|
||||
line-height: 150%;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
background-color: var(--primary-button-enabled);
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
border-radius: .5rem;
|
||||
}
|
||||
.file-desc {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
.resume {
|
||||
cursor: pointer;
|
||||
padding: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
background: rgba(255, 255, 255, .03);
|
||||
border: 1px dashed rgba(255, 255, 255, .16);
|
||||
border-radius: .75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -151,7 +151,9 @@
|
||||
<span><Label label={'Add social links'} /></span>
|
||||
{:else}
|
||||
<Channels value={object.channels} size={'small'} />
|
||||
<CircleButton icon={Edit} size={'small'} transparent on:click={(ev) => showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => { object.channels = result })} />
|
||||
<div class="ml-1">
|
||||
<CircleButton icon={Edit} size={'small'} transparent on:click={(ev) => showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => { object.channels = result })} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -73,18 +73,6 @@
|
||||
</div>
|
||||
</svelte:fragment> -->
|
||||
|
||||
<!-- <div class="flex-row-center">
|
||||
<div class="avatar">
|
||||
<div class="border"/>
|
||||
<Avatar />
|
||||
</div>
|
||||
<div class="flex-col">
|
||||
<div class="name">{formatName(candidate.name)}</div>
|
||||
<div class="title">For {getVacancyName()}</div>
|
||||
<div class="city">at Cisco</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="grid-cards">
|
||||
<CandidateCard {candidate}/>
|
||||
<VacancyCard {vacancy}/>
|
||||
@@ -98,57 +86,6 @@
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../../../packages/theme/styles/mixins.scss';
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 1.5rem;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border-radius: 50%;
|
||||
filter: drop-shadow(0px 14px 44px rgba(28, 23, 22, .8));
|
||||
cursor: pointer;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
@include bg-layer(var(--theme-avatar-hover), .5);
|
||||
z-index: -1;
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
@include bg-layer(var(--theme-avatar-bg), .1);
|
||||
backdrop-filter: blur(25px);
|
||||
z-index: -2;
|
||||
}
|
||||
.border {
|
||||
@include bg-fullsize;
|
||||
border: 2px solid var(--theme-avatar-border);
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.title, .city {
|
||||
font-weight: 500;
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-color);
|
||||
}
|
||||
.title { margin-top: .75rem; }
|
||||
.resume a {
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
&:hover { color: var(--theme-content-color); }
|
||||
}
|
||||
|
||||
.attachments {
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
@@ -158,21 +95,4 @@
|
||||
grid-template-columns: 1fr 1fr;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
|
||||
// .container {
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// height: 100%;
|
||||
// background-color: var(--theme-bg-color);
|
||||
// border-radius: 1.25rem;
|
||||
// box-shadow: 0px 3.125rem 7.5rem rgba(0, 0, 0, .4);
|
||||
|
||||
// .tabs-container {
|
||||
// flex-grow: 1;
|
||||
// display: flex;
|
||||
// flex-direction: column;
|
||||
// height: fit-content;
|
||||
// padding: 0 2.5rem 2.5rem;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
import { Panel } from '@anticrm/panel'
|
||||
import type { Candidate } from '@anticrm/recruit'
|
||||
import Contact from './icons/Contact.svelte'
|
||||
import IconAvatar from './icons/Avatar.svelte'
|
||||
import Attachments from './Attachments.svelte'
|
||||
import Edit from './icons/Edit.svelte'
|
||||
import SocialEditor from './SocialEditor.svelte'
|
||||
@@ -91,7 +90,9 @@
|
||||
rightSection = ev.detail.presenter
|
||||
}
|
||||
}} />
|
||||
<CircleButton icon={Edit} size={'small'} selected on:click={(ev) => showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => { saveChannels(result) })} />
|
||||
<div class="ml-1">
|
||||
<CircleButton icon={Edit} size={'small'} selected on:click={(ev) => showPopup(SocialEditor, { values: object.channels ?? [] }, ev.target, (result) => { saveChannels(result) })} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -106,6 +106,6 @@
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
.btn { flex-grow: 1; }
|
||||
.btn + .btn { margin-left: .75rem; }
|
||||
// .btn + .btn { margin-left: .75rem; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path fill="var(--duotone-color)" d="M18,6.4c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0-0.1-0.1 c0-0.1-0.1-0.2-0.2-0.4c-0.9-1.9-2.8-3.3-5.1-3.3c-2.3,0-4.2,1.4-5.1,3.3C6.8,5.9,6.7,6,6.7,6.1c0,0.1-0.1,0.1-0.1,0.1 C6.6,6.3,6.6,6.3,6.5,6.3c0,0,0,0-0.1,0c0,0,0,0-0.1,0c-0.1,0-0.2,0-0.3,0C4,6.4,2.4,8,2.4,10S4,13.6,6,13.6h6h6 c2,0,3.6-1.6,3.6-3.6S20,6.4,18,6.4z"/>
|
||||
<g {fill}>
|
||||
<path d="M18,6.4c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0 c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1-0.1c0-0.1-0.1-0.2-0.2-0.4c-0.9-1.9-2.8-3.3-5.1-3.3c-2.3,0-4.2,1.4-5.1,3.3 C6.8,5.9,6.7,6,6.7,6.1c0,0.1-0.1,0.1-0.1,0.1c0,0,0,0,0,0C6.6,6.3,6.6,6.3,6.5,6.3c0,0,0,0-0.1,0c0,0,0,0-0.1,0 c-0.1,0-0.2,0-0.3,0C4,6.4,2.4,8,2.4,10S4,13.6,6,13.6h0.6l1.2-1.2H6c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4h0.1 c0.2,0,0.4,0,0.6,0c0.2,0,0.4-0.1,0.6-0.2c0.2-0.1,0.3-0.3,0.4-0.4c0.1-0.1,0.1-0.2,0.2-0.3C7.8,6.5,7.9,6.4,8,6.2l0,0 c0.7-1.5,2.2-2.6,4-2.6s3.3,1.1,4,2.6l0,0c0.1,0.2,0.1,0.3,0.2,0.4c0,0.1,0.1,0.2,0.2,0.3c0.1,0.2,0.2,0.3,0.4,0.4 c0.2,0.1,0.4,0.2,0.6,0.2c0.2,0,0.4,0,0.6,0H18c1.3,0,2.4,1.1,2.4,2.4c0,1.3-1.1,2.4-2.4,2.4h-1.8l1.2,1.2H18c2,0,3.6-1.6,3.6-3.6 S20,6.4,18,6.4z"/>
|
||||
<path d="M12,11.2l-4.4,4.4l0.8,0.8l3-3V21c0,0.3,0.3,0.6,0.6,0.6s0.6-0.3,0.6-0.6v-7.6l3,3l0.8-0.8L12,11.2z"/>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -15,15 +15,11 @@
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
// import type { IntlString } from '@anticrm/platform'
|
||||
import { Label } from '@anticrm/ui'
|
||||
|
||||
export let label: IntlString
|
||||
export let placeholder: IntlString
|
||||
export let value: any
|
||||
export let focus: boolean
|
||||
export let maxWidth: string
|
||||
export let maxWidth: string | undefined = undefined
|
||||
export let onChange: (value: any) => void
|
||||
|
||||
function getLabel(value: boolean | undefined) {
|
||||
@@ -31,15 +27,21 @@
|
||||
if (value === false) return 'No'
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="flex-row-center yesno-container" class:yes={value === true} class:no={value === false} class:unknown={value === undefined} on:click={() => {
|
||||
if (value === true) value = false
|
||||
else if (value === false) value = undefined
|
||||
else value = true
|
||||
onChange(value)
|
||||
<div
|
||||
class="flex-row-center yesno-container"
|
||||
class:yes={value === true}
|
||||
class:no={value === false}
|
||||
class:unknown={value === undefined}
|
||||
style={(maxWidth) ? `max-width: ${maxWidth};` : ''}
|
||||
on:click={() => {
|
||||
if (value === true) value = false
|
||||
else if (value === false) value = undefined
|
||||
else value = true
|
||||
onChange(value)
|
||||
}}
|
||||
>
|
||||
<svg class="svg-small" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<circle class:yes={value === true} class:no={value === false} cx="8" cy="8" r="6"/>
|
||||
{#if value === true}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
export let label: IntlString
|
||||
export let color: string
|
||||
export let counter: number
|
||||
// export let counter: number
|
||||
export let addAction: () => void | undefined
|
||||
</script>
|
||||
|
||||
@@ -71,10 +71,10 @@
|
||||
height: 100%;
|
||||
font-weight: 500;
|
||||
color: var(--theme-caption-color);
|
||||
span {
|
||||
font-weight: 400;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
// span {
|
||||
// font-weight: 400;
|
||||
// color: var(--theme-content-dark-color);
|
||||
// }
|
||||
}
|
||||
.tool {
|
||||
opacity: .4;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { EditBox } from '@anticrm/ui'
|
||||
|
||||
export let label: IntlString
|
||||
// export let label: IntlString
|
||||
export let placeholder: IntlString
|
||||
export let value: any
|
||||
export let focus: boolean
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
color: var(--theme-caption-color);
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
&:hover .firstCell .menuRow { visibility: visible; }
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
.fixed .menuRow { visibility: visible; }
|
||||
</style>
|
||||
|
||||
@@ -200,6 +200,7 @@
|
||||
.checkCell { visibility: visible; }
|
||||
}
|
||||
&:hover .firstCell .menuRow { visibility: visible; }
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.fixed {
|
||||
|
||||
@@ -151,6 +151,11 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
|
||||
protected override async txUpdateDoc (tx: TxUpdateDoc<Doc>): Promise<TxResult> {
|
||||
const domain = this.hierarchy.getDomain(tx.objectClass)
|
||||
const operations = {
|
||||
...tx.operations,
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
}
|
||||
if (isOperator(tx.operations)) {
|
||||
const operator = Object.keys(tx.operations)[0]
|
||||
if (operator === '$move') {
|
||||
@@ -172,6 +177,10 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
updateOne: {
|
||||
filter: { _id: tx.objectId },
|
||||
update: {
|
||||
$set: {
|
||||
modifiedBy: tx.modifiedBy,
|
||||
modifiedOn: tx.modifiedOn
|
||||
},
|
||||
$push: {
|
||||
[arr]: {
|
||||
$each: [desc.$value],
|
||||
@@ -186,18 +195,18 @@ class MongoAdapter extends MongoAdapterBase {
|
||||
return await this.db.collection(domain).bulkWrite(ops as any)
|
||||
} else {
|
||||
if (tx.retrieve === true) {
|
||||
const result = await this.db.collection(domain).findOneAndUpdate({ _id: tx.objectId }, tx.operations, { returnDocument: 'after' })
|
||||
const result = await this.db.collection(domain).findOneAndUpdate({ _id: tx.objectId }, operations, { returnDocument: 'after' })
|
||||
return { object: result.value }
|
||||
} else {
|
||||
return await this.db.collection(domain).updateOne({ _id: tx.objectId }, tx.operations)
|
||||
return await this.db.collection(domain).updateOne({ _id: tx.objectId }, operations)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tx.retrieve === true) {
|
||||
const result = await this.db.collection(domain).findOneAndUpdate({ _id: tx.objectId }, { $set: tx.operations }, { returnDocument: 'after' })
|
||||
const result = await this.db.collection(domain).findOneAndUpdate({ _id: tx.objectId }, { $set: operations }, { returnDocument: 'after' })
|
||||
return { object: result.value }
|
||||
} else {
|
||||
return await this.db.collection(domain).updateOne({ _id: tx.objectId }, { $set: tx.operations })
|
||||
return await this.db.collection(domain).updateOne({ _id: tx.objectId }, { $set: operations })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user