mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-25 01:39:53 +00:00
Add global user status (#5526)
This commit is contained in:
parent
7625b73f08
commit
3e835e0c70
packages
plugins
activity-resources/src/components
chunter-resources/src/components
contact-resources/src/components
AssigneeBox.svelteAvatar.svelteAvatarInstance.svelteCombineAvatars.svelteEmployeePresenter.sveltePersonContent.sveltePersonElement.sveltePersonPresenter.svelteSelectUsersPopup.svelteUserBox.svelteUserDetails.svelteUserInfo.svelteUserStatus.svelteUsersList.svelteUsersPopup.svelte
workbench-resources/src/components
@ -58,7 +58,8 @@
|
||||
export let disallowDeselect: Ref<Doc>[] | undefined = undefined
|
||||
export let created: Doc[] = []
|
||||
export let embedded: boolean = false
|
||||
export let loading = false
|
||||
export let loading: boolean = false
|
||||
export let type: 'text' | 'object' = 'text'
|
||||
|
||||
let search: string = ''
|
||||
|
||||
@ -220,9 +221,13 @@
|
||||
void handleSelection(undefined, objects, item)
|
||||
}}
|
||||
>
|
||||
<span class="label" class:disabled={readonly || isDeselectDisabled || loading}>
|
||||
{#if type === 'text'}
|
||||
<span class="label" class:disabled={readonly || isDeselectDisabled || loading}>
|
||||
<slot name="item" item={obj} />
|
||||
</span>
|
||||
{:else}
|
||||
<slot name="item" item={obj} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if (allowDeselect && selected) || multiSelect || selected}
|
||||
<div class="check" class:disabled={readonly}>
|
||||
{#if obj._id === selected || selectedElements.has(obj._id)}
|
||||
|
@ -54,7 +54,8 @@
|
||||
export let readonly = false
|
||||
export let disallowDeselect: Ref<Doc>[] | undefined = undefined
|
||||
export let embedded: boolean = false
|
||||
export let loading = false
|
||||
export let loading: boolean = false
|
||||
export let type: 'text' | 'object' = 'text'
|
||||
|
||||
export let filter: (it: Doc) => boolean = () => {
|
||||
return true
|
||||
@ -119,6 +120,7 @@
|
||||
{disallowDeselect}
|
||||
{embedded}
|
||||
{loading}
|
||||
{type}
|
||||
on:update
|
||||
on:close
|
||||
on:changeContent
|
||||
|
@ -107,6 +107,208 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Avatar */
|
||||
.hulyAvatar-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
flex-shrink: 0;
|
||||
aspect-ratio: 1;
|
||||
background-color: var(--theme-button-default);
|
||||
pointer-events: none;
|
||||
|
||||
&.withStatus {
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: cover;
|
||||
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M10,13.5c0-1.9,1.6-3.5,3.5-3.5c1,0,1.9,0.4,2.5,1.1V0H0v16h11.1C10.4,15.4,10,14.5,10,13.5z' /%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
&.circle,
|
||||
&.circle img.ava-image { border-radius: 50%; }
|
||||
&.roundedRect,
|
||||
&.roundedRect img.ava-image { border-radius: 20%; }
|
||||
&.standby {
|
||||
opacity: .5;
|
||||
transition: opacity .5s ease-in-out;
|
||||
pointer-events: all;
|
||||
|
||||
&:hover,
|
||||
&.standbyOn { opacity: 1; }
|
||||
&:hover { transition-duration: .1s; }
|
||||
}
|
||||
|
||||
&.no-img {
|
||||
color: var(--primary-button-color);
|
||||
border-color: transparent;
|
||||
}
|
||||
&.bordered {
|
||||
color: var(--theme-dark-color);
|
||||
border: 1px solid var(--theme-button-border);
|
||||
}
|
||||
&.border {
|
||||
border: 1px solid var(--theme-bg-color);
|
||||
outline: 2px solid var(--border-color);
|
||||
|
||||
&.hulyAvatarSize-xx-small,
|
||||
&.hulyAvatarSize-inline,
|
||||
&.hulyAvatarSize-tiny,
|
||||
&.hulyAvatarSize-card,
|
||||
&.hulyAvatarSize-x-small { outline-width: 1px; }
|
||||
&.hulyAvatarSize-large,
|
||||
&.hulyAvatarSize-x-large,
|
||||
&.hulyAvatarSize-2x-large { border-width: 2px; }
|
||||
}
|
||||
img { object-fit: cover; }
|
||||
.icon,
|
||||
.ava-text::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: inherit;
|
||||
transform-origin: center;
|
||||
transform: translate(-50%, -50%) scale(.6);
|
||||
}
|
||||
.ava-text {
|
||||
font-weight: 500;
|
||||
letter-spacing: -.05em;
|
||||
|
||||
&::after {
|
||||
content: attr(data-name);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Avatar sizes */
|
||||
.hulyAvatarSize-xx-small {
|
||||
width: .75rem; // 12 - 10
|
||||
.small-font & { width: 10px; }
|
||||
|
||||
.ava-text { font-size: .375rem; }
|
||||
}
|
||||
.hulyAvatarSize-inline {
|
||||
width: .875rem; // 14 - 12
|
||||
.small-font & { width: 12px; }
|
||||
|
||||
.ava-text { font-size: .525rem; }
|
||||
}
|
||||
.hulyAvatarSize-tiny {
|
||||
width: 1.125rem; // 18 - 16
|
||||
.small-font & { width: 16px; }
|
||||
|
||||
.ava-text { font-size: .625rem; }
|
||||
}
|
||||
.hulyAvatarSize-card {
|
||||
width: 1.25rem; // 20 - 18
|
||||
.small-font & { width: 18px; }
|
||||
|
||||
.ava-text { font-size: .75rem; }
|
||||
}
|
||||
.hulyAvatarSize-x-small {
|
||||
width: 1.5rem; // 24 - 22
|
||||
.small-font & { width: 22px; }
|
||||
|
||||
.ava-text { font-size: .875rem; }
|
||||
}
|
||||
.hulyAvatarSize-smaller {
|
||||
width: 1.75rem; // 28 - 25
|
||||
.small-font & { width: 25px; }
|
||||
|
||||
.ava-text { font-size: 1rem; }
|
||||
}
|
||||
.hulyAvatarSize-small {
|
||||
width: 2rem; // 32 - 28
|
||||
|
||||
.ava-text { font-size: 1.125rem; }
|
||||
}
|
||||
.hulyAvatarSize-medium {
|
||||
width: 2.5rem; // 40 - 35
|
||||
|
||||
.ava-text { font-size: 1.375rem; }
|
||||
}
|
||||
.hulyAvatarSize-large {
|
||||
width: 4.5rem; // 72 - 63
|
||||
|
||||
.ava-text { font-size: 2.75rem; }
|
||||
}
|
||||
.hulyAvatarSize-x-large {
|
||||
width: 7.5rem; // 120 - 105
|
||||
|
||||
.ava-text { font-size: 4.5rem; }
|
||||
}
|
||||
.hulyAvatarSize-2x-large {
|
||||
width: 10rem; // 160 - 140
|
||||
|
||||
.ava-text { font-size: 6rem; }
|
||||
}
|
||||
.hulyAvatarSize-full {
|
||||
width: 100%;
|
||||
|
||||
.ava-text { font-size: inherit; }
|
||||
}
|
||||
|
||||
/* Avatar status marker */
|
||||
.hulyAvatar-statusMarker {
|
||||
position: absolute;
|
||||
right: -4%;
|
||||
bottom: -4%;
|
||||
width: 39%;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
|
||||
&.xx-small,
|
||||
&.inline,
|
||||
&.tiny,
|
||||
&.card,
|
||||
&.x-small,
|
||||
&.smaller,
|
||||
&.small,
|
||||
&.medium {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.small-font &.xx-small { width: 3px; }
|
||||
&.xx-small,
|
||||
&.inline,
|
||||
.small-font &.inline { width: 4px; }
|
||||
.small-font &.tiny { width: 5px; }
|
||||
&.tiny,
|
||||
&.card,
|
||||
.small-font &.card { width: 6px; }
|
||||
.small-font &.x-small { width: 7px; }
|
||||
&.x-small,
|
||||
.small-font &.smaller { width: 8px; }
|
||||
&.smaller,
|
||||
.small-font &.small { width: 9px; }
|
||||
&.small { width: 10px; }
|
||||
.small-font &.medium { width: 11px; }
|
||||
&.medium { width: 13px; }
|
||||
&.large {
|
||||
right: -.125rem;
|
||||
bottom: -.125rem;
|
||||
width: 36.5%;
|
||||
|
||||
.small-font & {
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
width: 37%;
|
||||
}
|
||||
}
|
||||
&.online { background-color: var(--global-online-color); }
|
||||
&.offline {
|
||||
border: 1px solid var(--global-offline-color);
|
||||
|
||||
&:not(.xx-small, .inline, .tiny, .card, .x-small, .smaller, .small, .medium) { border-width: 2px; }
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.hulyHeader-container {
|
||||
display: flex;
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import { Person, type PersonAccount } from '@hcengineering/contact'
|
||||
import contact, { Person, type PersonAccount } from '@hcengineering/contact'
|
||||
import {
|
||||
Avatar,
|
||||
EmployeePresenter,
|
||||
@ -43,6 +43,7 @@
|
||||
export let header: IntlString | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const limit = 300
|
||||
|
||||
let isActionsOpened = false
|
||||
@ -88,6 +89,8 @@
|
||||
} else {
|
||||
tooltipLabel = core.string.System
|
||||
}
|
||||
|
||||
$: showStatus = !!person && hierarchy.hasMixin(person, contact.mixin.Employee)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
@ -115,7 +118,7 @@
|
||||
{#if headerObject}
|
||||
<Icon icon={headerIcon ?? classIcon(client, headerObject._class) ?? activity.icon.Activity} size="small" />
|
||||
{:else if person}
|
||||
<Avatar size="card" avatar={person.avatar} name={person.name} />
|
||||
<Avatar size="card" avatar={person.avatar} name={person.name} {showStatus} {account} />
|
||||
{:else}
|
||||
<SystemAvatar size="card" />
|
||||
{/if}
|
||||
|
@ -65,7 +65,7 @@
|
||||
bind:filters
|
||||
{object}
|
||||
icon={getObjectIcon(_class)}
|
||||
iconProps={{ value: object, showStatus: true, background: 'var(--theme-comp-header-color)' }}
|
||||
iconProps={{ value: object, showStatus: true }}
|
||||
label={title}
|
||||
intlLabel={chunter.string.Channel}
|
||||
{description}
|
||||
|
@ -50,7 +50,7 @@
|
||||
{#each persons as person, index}
|
||||
<div class="item" class:withoutBorder={index === persons.length - 1}>
|
||||
<div class="item__content" class:disabled={disableRemoveFor.includes(person._id)}>
|
||||
<UserDetails {person} showStatus background="var(--global-surface-01-BackgroundColor)" />
|
||||
<UserDetails {person} showStatus />
|
||||
{#if !disableRemoveFor.includes(person._id)}
|
||||
<div class="item__action">
|
||||
<ButtonIcon
|
||||
|
@ -27,7 +27,6 @@
|
||||
export let value: DirectMessage | undefined
|
||||
export let size: IconSize = 'small'
|
||||
export let showStatus = false
|
||||
export let background: string = 'var(--global-ui-BackgroundColor)'
|
||||
|
||||
const visiblePersons = 4
|
||||
const client = getClient()
|
||||
@ -61,7 +60,6 @@
|
||||
name={persons[0].name}
|
||||
{showStatus}
|
||||
account={getAccountByPerson($personAccountByIdStore, persons[0])?._id}
|
||||
{background}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -83,7 +83,7 @@
|
||||
title: (await getChannelName(object._id, object._class, object)) ?? (await translate(titleIntl, {})),
|
||||
description: isDocChat && !isPerson ? await getDocTitle(client, object._id, object._class, object) : undefined,
|
||||
icon: icon ?? getObjectIcon(_class),
|
||||
iconProps: { showStatus: true, background: 'var(--global-surface-01-BackgroundColor)' },
|
||||
iconProps: { showStatus: true },
|
||||
iconSize,
|
||||
withIconBackground: !isDirect && !isPerson,
|
||||
isSecondary: isDocChat && !isPerson
|
||||
|
@ -161,7 +161,7 @@
|
||||
>
|
||||
<span
|
||||
slot="content"
|
||||
class="overflow-label flex-grow"
|
||||
class="overflow-label flex-grow h-full flex-center"
|
||||
class:flex-between={showNavigate && selected}
|
||||
class:dark-color={value == null}
|
||||
>
|
||||
|
@ -37,19 +37,16 @@
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
ColorDefinition,
|
||||
Icon,
|
||||
IconSize,
|
||||
getPlatformAvatarColorByName,
|
||||
getPlatformAvatarColorForTextDef,
|
||||
getPlatformColor,
|
||||
resizeObserver,
|
||||
themeStore
|
||||
} from '@hcengineering/ui'
|
||||
import { onMount } from 'svelte'
|
||||
import { Account } from '@hcengineering/core'
|
||||
|
||||
import AvatarIcon from './icons/Avatar.svelte'
|
||||
import UserStatus from './UserStatus.svelte'
|
||||
import AvatarInstance from './AvatarInstance.svelte'
|
||||
import { loadUsersStatus, statusByUserStore } from '../utils'
|
||||
|
||||
export let avatar: string | null | undefined = undefined
|
||||
export let name: string | null | undefined = undefined
|
||||
@ -59,32 +56,18 @@
|
||||
export let variant: 'circle' | 'roundedRect' | 'none' = 'roundedRect'
|
||||
export let borderColor: number | undefined = undefined
|
||||
export let standby: boolean = false
|
||||
export let showStatus = false
|
||||
export let showStatus: boolean = true
|
||||
export let account: Ref<Account> | undefined = undefined
|
||||
export let background: string | undefined = undefined
|
||||
|
||||
export function pulse (): void {
|
||||
if (element) element.animate(pulsating, { duration: 150, easing: 'ease-out' })
|
||||
if (standby) {
|
||||
standbyMode = false
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
standbyMode = true
|
||||
}, 2000)
|
||||
}
|
||||
avatarInst.pulse()
|
||||
}
|
||||
|
||||
let url: string[] | undefined
|
||||
let avatarProvider: AvatarProvider | undefined
|
||||
let color: ColorDefinition | undefined = undefined
|
||||
let standbyMode: boolean = standby
|
||||
let timer: any | undefined = undefined
|
||||
let fontSize: number = 16
|
||||
let element: HTMLElement
|
||||
const pulsating: Keyframe[] = [
|
||||
{ boxShadow: '0 0 .125rem 0 var(--theme-bg-color), 0 0 0 .125rem var(--border-color)' },
|
||||
{ boxShadow: '0 0 .375rem .375rem var(--theme-bg-color), 0 0 0 .25rem var(--border-color)' }
|
||||
]
|
||||
let avatarInst: AvatarInstance
|
||||
|
||||
$: displayName = getDisplayName(name)
|
||||
$: bColor = borderColor !== undefined ? getPlatformColor(borderColor, $themeStore.dark) : undefined
|
||||
@ -138,264 +121,48 @@
|
||||
$: srcset = url?.slice(1)?.join(', ')
|
||||
|
||||
onMount(() => {
|
||||
if (size === 'full' && !url && name && displayName && displayName !== '' && element) {
|
||||
fontSize = element.clientWidth * 0.6
|
||||
}
|
||||
loadUsersStatus()
|
||||
})
|
||||
|
||||
function getStatusSize (avaSize: IconSize): 'small' | 'medium' {
|
||||
switch (avaSize) {
|
||||
case 'inline':
|
||||
case 'tiny':
|
||||
case 'card':
|
||||
case 'x-small':
|
||||
return 'small'
|
||||
case 'small':
|
||||
case 'medium':
|
||||
case 'large':
|
||||
case 'x-large':
|
||||
case '2x-large':
|
||||
return 'medium'
|
||||
default:
|
||||
return 'small'
|
||||
}
|
||||
}
|
||||
$: userStatus = account ? $statusByUserStore.get(account) : undefined
|
||||
</script>
|
||||
|
||||
{#if size === 'full' && !url && name && displayName && displayName !== ''}
|
||||
<div
|
||||
bind:this={element}
|
||||
class="ava-{size} flex-center avatar-container {variant}"
|
||||
class:no-img={!url && color}
|
||||
class:bordered={!url && color === undefined}
|
||||
class:border={bColor !== undefined}
|
||||
class:standby
|
||||
class:standbyOn={standby && !standbyMode}
|
||||
style:--border-color={bColor ?? 'var(--primary-button-default)'}
|
||||
style:background-color={color && !url ? color.icon : 'var(--theme-button-default)'}
|
||||
use:resizeObserver={(element) => {
|
||||
fontSize = element.clientWidth * 0.6
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="ava-text"
|
||||
style:color={color ? color.iconText : 'var(--primary-button-color)'}
|
||||
style:font-size={`${fontSize}px`}
|
||||
data-name={displayName.toLocaleUpperCase()}
|
||||
{#if showStatus && account}
|
||||
<div class="relative">
|
||||
<AvatarInstance
|
||||
bind:this={avatarInst}
|
||||
{url}
|
||||
{srcset}
|
||||
{displayName}
|
||||
{size}
|
||||
{icon}
|
||||
{variant}
|
||||
{color}
|
||||
{bColor}
|
||||
{standby}
|
||||
bind:element
|
||||
withStatus
|
||||
/>
|
||||
{#if showStatus && account}
|
||||
<div
|
||||
class="hulyAvatar-statusMarker {size}"
|
||||
class:online={userStatus?.online}
|
||||
class:offline={!userStatus?.online}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
bind:this={element}
|
||||
class="ava-{size} flex-center avatar-container {variant}"
|
||||
class:no-img={!url && color}
|
||||
class:bordered={!url && color === undefined}
|
||||
class:border={bColor !== undefined}
|
||||
class:standby
|
||||
class:standbyOn={standby && !standbyMode}
|
||||
style:--border-color={bColor ?? 'var(--primary-button-default)'}
|
||||
style:background-color={color && !url ? color.icon : 'var(--theme-button-default)'}
|
||||
>
|
||||
{#if url}
|
||||
<img class="ava-{size} ava-image" src={url[0]} {srcset} alt={''} />
|
||||
{:else if name && displayName && displayName !== ''}
|
||||
<div
|
||||
class="ava-text"
|
||||
style:color={color ? color.iconText : 'var(--primary-button-color)'}
|
||||
data-name={displayName.toLocaleUpperCase()}
|
||||
/>
|
||||
{:else}
|
||||
<div class="icon">
|
||||
<Icon icon={icon ?? AvatarIcon} size={'full'} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if showStatus && account}
|
||||
<span class="status">
|
||||
<UserStatus user={account} size={getStatusSize(size)} {background} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<AvatarInstance
|
||||
bind:this={avatarInst}
|
||||
{url}
|
||||
{srcset}
|
||||
{displayName}
|
||||
{size}
|
||||
{icon}
|
||||
{variant}
|
||||
{color}
|
||||
{bColor}
|
||||
{standby}
|
||||
bind:element
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.avatar-container {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
background-color: var(--theme-button-default);
|
||||
pointer-events: none;
|
||||
|
||||
&.circle,
|
||||
&.circle img.ava-image {
|
||||
border-radius: 50%;
|
||||
}
|
||||
&.roundedRect,
|
||||
&.roundedRect img.ava-image {
|
||||
border-radius: 20%;
|
||||
}
|
||||
&.standby {
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
pointer-events: all;
|
||||
|
||||
&:hover,
|
||||
&.standbyOn {
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover {
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-img {
|
||||
color: var(--primary-button-color);
|
||||
border-color: transparent;
|
||||
}
|
||||
&.bordered {
|
||||
color: var(--theme-dark-color);
|
||||
border: 1px solid var(--theme-button-border);
|
||||
}
|
||||
&.border {
|
||||
border: 1px solid var(--theme-bg-color);
|
||||
outline: 2px solid var(--border-color);
|
||||
|
||||
&.ava-inline,
|
||||
&.ava-tiny,
|
||||
&.ava-card,
|
||||
&.ava-x-small {
|
||||
outline-width: 1px;
|
||||
}
|
||||
&.ava-large,
|
||||
&.ava-x-large,
|
||||
&.ava-2x-large {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
img {
|
||||
object-fit: cover;
|
||||
}
|
||||
.icon,
|
||||
.ava-text::after {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
.icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: inherit;
|
||||
transform-origin: center;
|
||||
transform: translate(-50%, -50%) scale(0.6);
|
||||
}
|
||||
.ava-text {
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.05em;
|
||||
|
||||
&::after {
|
||||
content: attr(data-name);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ava-inline {
|
||||
width: 0.875rem; // 24
|
||||
height: 0.875rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 0.525rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ava-tiny {
|
||||
width: 1.13rem; // ~18
|
||||
height: 1.13rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 0.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ava-card {
|
||||
width: 1.25rem; // 20
|
||||
height: 1.25rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ava-x-small {
|
||||
width: 1.5rem; // 24
|
||||
height: 1.5rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
.ava-smaller {
|
||||
width: 1.75rem; // 28
|
||||
height: 1.75rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
.ava-small {
|
||||
width: 2rem; // 32
|
||||
height: 2rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
}
|
||||
.ava-medium {
|
||||
width: 2.5rem; // 40
|
||||
height: 2.5rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 1.375rem;
|
||||
}
|
||||
}
|
||||
.ava-large {
|
||||
width: 4.5rem; // 72
|
||||
height: 4.5rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 2.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ava-x-large {
|
||||
width: 7.5rem; // 120
|
||||
height: 7.5rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ava-2x-large {
|
||||
width: 10rem; // 120
|
||||
height: 10rem;
|
||||
|
||||
.ava-text {
|
||||
font-size: 6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ava-full {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 1;
|
||||
|
||||
.ava-text {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
bottom: -0.125rem;
|
||||
right: -0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
102
plugins/contact-resources/src/components/AvatarInstance.svelte
Normal file
102
plugins/contact-resources/src/components/AvatarInstance.svelte
Normal file
@ -0,0 +1,102 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { Asset } from '@hcengineering/platform'
|
||||
import { AnySvelteComponent, ColorDefinition, Icon, IconSize, resizeObserver } from '@hcengineering/ui'
|
||||
import AvatarIcon from './icons/Avatar.svelte'
|
||||
|
||||
export let url: string[] | undefined
|
||||
export let srcset: string | undefined
|
||||
export let displayName: string
|
||||
export let size: IconSize
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let variant: 'circle' | 'roundedRect' | 'none' = 'roundedRect'
|
||||
export let color: ColorDefinition | undefined = undefined
|
||||
export let bColor: string | undefined = undefined
|
||||
export let standby: boolean = false
|
||||
export let withStatus: boolean = false
|
||||
export let element: HTMLElement
|
||||
|
||||
export function pulse (): void {
|
||||
if (element) element.animate(pulsating, { duration: 150, easing: 'ease-out' })
|
||||
if (standby) {
|
||||
standbyMode = false
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
standbyMode = true
|
||||
}, 2000)
|
||||
}
|
||||
}
|
||||
|
||||
let standbyMode: boolean = standby
|
||||
let timer: any | undefined = undefined
|
||||
let fontSize: number = 16
|
||||
const pulsating: Keyframe[] = [
|
||||
{ boxShadow: '0 0 .125rem 0 var(--theme-bg-color), 0 0 0 .125rem var(--border-color)' },
|
||||
{ boxShadow: '0 0 .375rem .375rem var(--theme-bg-color), 0 0 0 .25rem var(--border-color)' }
|
||||
]
|
||||
</script>
|
||||
|
||||
{#if size === 'full' && !url && displayName && displayName !== ''}
|
||||
<div
|
||||
bind:this={element}
|
||||
class="hulyAvatar-container hulyAvatarSize-{size} {variant}"
|
||||
class:no-img={!url && color}
|
||||
class:bordered={!url && color === undefined}
|
||||
class:border={bColor !== undefined}
|
||||
class:standby
|
||||
class:standbyOn={standby && !standbyMode}
|
||||
class:withStatus
|
||||
style:--border-color={bColor ?? 'var(--primary-button-default)'}
|
||||
style:background-color={color && !url ? color.icon : 'var(--theme-button-default)'}
|
||||
use:resizeObserver={(element) => {
|
||||
fontSize = element.clientWidth * 0.6
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="ava-text"
|
||||
style:color={color ? color.iconText : 'var(--primary-button-color)'}
|
||||
style:font-size={`${fontSize}px`}
|
||||
data-name={displayName.toLocaleUpperCase()}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
bind:this={element}
|
||||
class="hulyAvatar-container hulyAvatarSize-{size} stat {variant}"
|
||||
class:no-img={!url && color}
|
||||
class:bordered={!url && color === undefined}
|
||||
class:border={bColor !== undefined}
|
||||
class:standby
|
||||
class:standbyOn={standby && !standbyMode}
|
||||
class:withStatus
|
||||
style:--border-color={bColor ?? 'var(--primary-button-default)'}
|
||||
style:background-color={color && !url ? color.icon : 'var(--theme-button-default)'}
|
||||
>
|
||||
{#if url}
|
||||
<img class="hulyAvatarSize-{size} ava-image" src={url[0]} {srcset} alt={''} />
|
||||
{:else if displayName && displayName !== ''}
|
||||
<div
|
||||
class="ava-text"
|
||||
style:color={color ? color.iconText : 'var(--primary-button-color)'}
|
||||
data-name={displayName.toLocaleUpperCase()}
|
||||
/>
|
||||
{:else}
|
||||
<div class="icon">
|
||||
<Icon icon={icon ?? AvatarIcon} size={'full'} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
@ -57,7 +57,7 @@
|
||||
{/if}
|
||||
{#each persons as person, i}
|
||||
<div class="combine-avatar {size}" data-over={getDataOver(persons.length === i + 1, items)}>
|
||||
<Avatar avatar={person.avatar} {size} name={person.name} />
|
||||
<Avatar avatar={person.avatar} {size} name={person.name} showStatus={false} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -21,8 +21,8 @@
|
||||
export let defaultName: IntlString | undefined = ui.string.NotSelected
|
||||
// export let element: HTMLElement | undefined = undefined
|
||||
export let noUnderline: boolean = false
|
||||
export let compact = false
|
||||
export let showStatus = true
|
||||
export let compact: boolean = false
|
||||
export let showStatus: boolean = true
|
||||
|
||||
$: employeeValue = typeof value === 'string' ? $personByIdStore.get(value) : value
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
{defaultName}
|
||||
{noUnderline}
|
||||
{compact}
|
||||
{showStatus}
|
||||
statusLabel={!active && shouldShowName && showStatus ? contact.string.Inactive : undefined}
|
||||
on:accent-color
|
||||
/>
|
||||
|
@ -47,7 +47,8 @@
|
||||
export let colorInherit: boolean = false
|
||||
export let accent: boolean = false
|
||||
export let maxWidth = ''
|
||||
export let compact = false
|
||||
export let compact: boolean = false
|
||||
export let showStatus: boolean = false
|
||||
export let type: ObjectPresenterType = 'link'
|
||||
|
||||
const client = getClient()
|
||||
@ -91,6 +92,7 @@
|
||||
{accent}
|
||||
{type}
|
||||
{maxWidth}
|
||||
{showStatus}
|
||||
/>
|
||||
<span class="status">
|
||||
<Label label={statusLabel} />
|
||||
@ -113,6 +115,7 @@
|
||||
{accent}
|
||||
{type}
|
||||
{maxWidth}
|
||||
{showStatus}
|
||||
/>
|
||||
{/if}
|
||||
{:else if shouldShowPlaceholder}
|
||||
@ -127,7 +130,7 @@
|
||||
>
|
||||
{#if !inline && shouldShowAvatar}
|
||||
<span class="ap-icon" class:mr-2={shouldShowName && !enlargedText} class:mr-3={shouldShowName && enlargedText}>
|
||||
<Avatar size={avatarSize} on:accent-color />
|
||||
<Avatar size={avatarSize} {showStatus} on:accent-color />
|
||||
</span>
|
||||
{/if}
|
||||
{#if shouldShowName && defaultName}
|
||||
|
@ -13,11 +13,14 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Employee, Person } from '@hcengineering/contact'
|
||||
import contact, { Employee, Person } from '@hcengineering/contact'
|
||||
import { IconSize, LabelAndProps, tooltip } from '@hcengineering/ui'
|
||||
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
|
||||
import { ObjectPresenterType } from '@hcengineering/view'
|
||||
|
||||
import Avatar from './Avatar.svelte'
|
||||
import { personAccountByIdStore } from '../utils'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
|
||||
export let value: Person | Employee | undefined | null
|
||||
export let name: string
|
||||
@ -34,6 +37,13 @@
|
||||
export let accent: boolean = false
|
||||
export let maxWidth: string = ''
|
||||
export let type: ObjectPresenterType = 'link'
|
||||
export let showStatus = true
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: showStatus = showStatus && !!value && hierarchy.hasMixin(value, contact.mixin.Employee)
|
||||
$: account = value && Array.from($personAccountByIdStore.values()).find((account) => account.person === value?._id)
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
@ -53,7 +63,7 @@
|
||||
class:mr-2={shouldShowName && !enlargedText}
|
||||
class:mr-3={shouldShowName && enlargedText}
|
||||
>
|
||||
<Avatar size={avatarSize} avatar={value.avatar} name={value.name} />
|
||||
<Avatar size={avatarSize} avatar={value.avatar} name={value.name} {showStatus} account={account?._id} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if shouldShowName}
|
||||
|
@ -42,6 +42,7 @@
|
||||
export let maxWidth = ''
|
||||
export let compact = false
|
||||
export let type: ObjectPresenterType = 'link'
|
||||
export let showStatus: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
$: personValue = typeof value === 'string' ? $personByIdStore.get(value) : value
|
||||
@ -97,6 +98,7 @@
|
||||
{maxWidth}
|
||||
{compact}
|
||||
{type}
|
||||
{showStatus}
|
||||
on:accent-color
|
||||
/>
|
||||
{/if}
|
||||
|
@ -32,7 +32,7 @@
|
||||
export let selected: Ref<Employee>[] = []
|
||||
export let skipCurrentAccount = false
|
||||
export let disableDeselectFor: Ref<Employee>[] = []
|
||||
export let showStatus = false
|
||||
export let showStatus = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@ -90,7 +90,6 @@
|
||||
{showStatus}
|
||||
{disableDeselectFor}
|
||||
{skipCurrentAccount}
|
||||
background="var(--theme-popup-color)"
|
||||
on:select={handleSelectionChanged}
|
||||
/>
|
||||
</Scroller>
|
||||
|
@ -145,7 +145,7 @@
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
class="overflow-label flex-row-center"
|
||||
class="overflow-label flex-row-center h-full"
|
||||
class:w-full={width === '100%'}
|
||||
class:flex-between={showNavigate && selected}
|
||||
class:content-color={value == null}
|
||||
|
@ -23,8 +23,7 @@
|
||||
|
||||
export let person: Person
|
||||
export let avatarSize: IconSize = 'x-small'
|
||||
export let showStatus = false
|
||||
export let background: string | undefined = undefined
|
||||
export let showStatus = true
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
@ -44,7 +43,6 @@
|
||||
on:accent-color
|
||||
{showStatus}
|
||||
account={getAccountByPerson($personAccountByIdStore, person)?._id}
|
||||
{background}
|
||||
/>
|
||||
<div class="flex-col min-w-0 {avatarSize === 'tiny' || avatarSize === 'inline' ? 'ml-1' : 'ml-3'}">
|
||||
<div class="label overflow-label text-left">{getName(hierarchy, person)}</div>
|
||||
|
@ -15,26 +15,32 @@
|
||||
<script lang="ts">
|
||||
import Avatar from './Avatar.svelte'
|
||||
|
||||
import { getName, Person } from '@hcengineering/contact'
|
||||
import contact, { getName, Person } from '@hcengineering/contact'
|
||||
import { Asset } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { AnySvelteComponent, IconSize } from '@hcengineering/ui'
|
||||
import { personAccountByIdStore } from '../utils'
|
||||
|
||||
export let value: Person
|
||||
export let subtitle: string | undefined = undefined
|
||||
export let size: IconSize
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let short: boolean = false
|
||||
export let showStatus = true
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
$: showStatus = showStatus && !!value && hierarchy.hasMixin(value, contact.mixin.Employee)
|
||||
$: account = value && Array.from($personAccountByIdStore.values()).find((account) => account.person === value?._id)
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="flex-row-center" on:click>
|
||||
<Avatar avatar={value.avatar} {size} {icon} name={value.name} on:accent-color />
|
||||
<Avatar avatar={value.avatar} {size} {icon} name={value.name} on:accent-color {showStatus} account={account?._id} />
|
||||
<div class="flex-col min-w-0 {size === 'tiny' || size === 'inline' ? 'ml-1' : 'ml-2'}" class:max-w-20={short}>
|
||||
{#if subtitle}<div class="content-dark-color text-sm">{subtitle}</div>{/if}
|
||||
<div class="label overflow-label text-left">{getName(client.getHierarchy(), value)}</div>
|
||||
<div class="label text-left">{getName(client.getHierarchy(), value)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,79 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { Account, Ref } from '@hcengineering/core'
|
||||
import { onMount } from 'svelte'
|
||||
import { loadUsersStatus, statusByUserStore } from '../utils'
|
||||
|
||||
export let user: Ref<Account>
|
||||
export let size: 'small' | 'medium' = 'small'
|
||||
export let background: string = 'red'
|
||||
|
||||
onMount(() => {
|
||||
loadUsersStatus()
|
||||
})
|
||||
|
||||
$: userStatus = $statusByUserStore.get(user)
|
||||
</script>
|
||||
|
||||
<div class="container {size}" style="background-color: {background}">
|
||||
<div
|
||||
class="status {size}"
|
||||
style="background-color: {background}"
|
||||
class:online={userStatus?.online}
|
||||
class:offline={!userStatus?.online}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
|
||||
&.small {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
width: 0.875rem;
|
||||
height: 0.875rem;
|
||||
}
|
||||
}
|
||||
.status {
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
border-radius: 50%;
|
||||
|
||||
&.online {
|
||||
background-color: var(--global-online-color) !important;
|
||||
}
|
||||
|
||||
&.offline {
|
||||
border: 1px solid var(--global-offline-color);
|
||||
}
|
||||
&.small {
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -30,8 +30,7 @@
|
||||
export let selected: Ref<Employee>[] = []
|
||||
export let skipCurrentAccount = false
|
||||
export let disableDeselectFor: Ref<Employee>[] = []
|
||||
export let showStatus = false
|
||||
export let background: string | undefined = undefined
|
||||
export let showStatus = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
@ -116,7 +115,7 @@
|
||||
handleSelection(persons, index)
|
||||
}}
|
||||
>
|
||||
<UserDetails avatarSize="small" {person} {showStatus} {background} />
|
||||
<UserDetails avatarSize="small" {person} {showStatus} />
|
||||
<CheckBox
|
||||
checked={selectedItems.has(person._id)}
|
||||
readonly={disabled}
|
||||
|
@ -69,6 +69,7 @@
|
||||
{allowDeselect}
|
||||
{titleDeselect}
|
||||
{placeholder}
|
||||
type={'object'}
|
||||
docQuery={readonly ? { ...docQuery, _id: { $in: selectedUsers } } : docQuery}
|
||||
{filter}
|
||||
groupBy={'_class'}
|
||||
@ -83,7 +84,7 @@
|
||||
{readonly}
|
||||
>
|
||||
<svelte:fragment slot="item" let:item={person}>
|
||||
<div class="flex flex-grow overflow-label">
|
||||
<div class="flex-row-center flex-grow">
|
||||
<UserInfo size={'smaller'} value={person} {icon} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
|
||||
import contact, { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import core, { AccountRole, Class, Doc, Ref, Space, getCurrentAccount, hasAccountRole } from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import notification, { DocNotifyContext, InboxNotification, notificationId } from '@hcengineering/notification'
|
||||
@ -727,7 +727,10 @@
|
||||
class="cursor-pointer"
|
||||
on:click|stopPropagation={() => showPopup(AccountPopup, {}, popupPosition)}
|
||||
>
|
||||
<Component is={contact.component.Avatar} props={{ avatar: person?.avatar, size: 'small' }} />
|
||||
<Component
|
||||
is={contact.component.Avatar}
|
||||
props={{ avatar: person?.avatar, size: 'small', account: account._id }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user