Add global user status (#5526)

This commit is contained in:
Alexander Platov 2024-05-10 09:14:06 +03:00 committed by GitHub
parent 7625b73f08
commit 3e835e0c70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 410 additions and 388 deletions

View File

@ -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)}

View File

@ -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

View File

@ -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;

View File

@ -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}

View File

@ -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}

View File

@ -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

View File

@ -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}

View File

@ -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

View File

@ -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}
>

View File

@ -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>

View 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}

View File

@ -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>

View File

@ -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
/>

View File

@ -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}

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}

View File

@ -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>

View File

@ -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>