TSK-1469,-1470: added SelectAvatars, UserBoxItems components (#3176)

Signed-off-by: Alexander Platov <sas_lord@mail.ru>
This commit is contained in:
Alexander Platov 2023-05-14 21:12:16 -07:00 committed by GitHub
parent ee0a120450
commit 6423d1beeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 354 additions and 11 deletions

View File

@ -290,7 +290,7 @@
--theme-popup-hover: #F5F5F5; --theme-popup-hover: #F5F5F5;
--theme-popup-divider: rgba(0, 0, 0, .1); --theme-popup-divider: rgba(0, 0, 0, .1);
--theme-popup-header: #EBEBEB; --theme-popup-header: #EBEBEB;
--theme-popup-shadow: 0px 1px 4px 2px rgba(0, 0, 0, 0.15); --theme-popup-shadow: 0px 0px 8px rgba(0, 0, 0, 0.2);
--theme-panel-color: #FFFFFF; --theme-panel-color: #FFFFFF;
--theme-calendar-today-color: #000; --theme-calendar-today-color: #000;
--theme-calendar-holiday-color: #eb5757; --theme-calendar-holiday-color: #eb5757;

View File

@ -210,6 +210,8 @@ input.search {
.flex-row-top { .flex-row-top {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
flex-wrap: nowrap;
min-width: 0;
} }
.flex-row-reverse { .flex-row-reverse {
display: flex; display: flex;
@ -394,8 +396,8 @@ input.search {
} }
} }
.gap-1-5 { .gap-1-5 {
& > * { margin-right: .375rem; } &:not(.reverse) > *:not(:last-child) { margin-right: .375rem; }
&.reverse > :last-child { margin-right: .375rem; } &.reverse > *:not(:first-child) { margin-left: .375rem; }
} }
.gap-2 { .gap-2 {
&:not(.reverse) > *:not(:first-child) { margin-left: .5rem; } &:not(.reverse) > *:not(:first-child) { margin-left: .5rem; }
@ -688,7 +690,7 @@ input.search {
width: inherit; width: inherit;
height: inherit; height: inherit;
} }
.svg-x-small, .svg-small, .svg-medium, .svg-large { flex-shrink: 0; } .svg-card, .svg-x-small, .svg-small, .svg-medium, .svg-large { flex-shrink: 0; }
.svg-mask { .svg-mask {
position: absolute; position: absolute;
@ -716,6 +718,7 @@ a.no-line {
.cursor-inherit { cursor: inherit; } .cursor-inherit { cursor: inherit; }
.pointer-events-none { pointer-events: none; } .pointer-events-none { pointer-events: none; }
.content-pointer-events-none > * { pointer-events: none; }
.select-text { user-select: text; } .select-text { user-select: text; }
.select-text-i { user-select: text !important; } .select-text-i { user-select: text !important; }

View File

@ -23,6 +23,7 @@
export let items: Ref<Contact>[] = [] export let items: Ref<Contact>[] = []
export let size: IconSize export let size: IconSize
export let limit: number = 3 export let limit: number = 3
export let hideLimit: boolean = false
let persons: Contact[] = [] let persons: Contact[] = []
const query = createQuery() const query = createQuery()
@ -37,8 +38,13 @@
</script> </script>
<div class="avatars-container"> <div class="avatars-container">
{#each persons as person} {#each persons as person, i}
<div class="combine-avatar {size}"> <div
class="combine-avatar {size}"
data-over={i === persons.length - 1 && items.length > limit && !hideLimit
? `+${items.length - limit + 1}`
: undefined}
>
<Avatar avatar={person.avatar} {size} /> <Avatar avatar={person.avatar} {size} />
</div> </div>
{/each} {/each}
@ -52,9 +58,18 @@
.combine-avatar.inline:not(:first-child) { .combine-avatar.inline:not(:first-child) {
margin-left: calc(1px - (0.875rem / 2)); margin-left: calc(1px - (0.875rem / 2));
} }
.combine-avatar.tiny:not(:first-child) {
margin-left: calc(1px - (1.13rem / 2));
}
.combine-avatar.card:not(:first-child) {
margin-left: calc(1px - (1.25rem / 2));
}
.combine-avatar.x-small:not(:first-child) { .combine-avatar.x-small:not(:first-child) {
margin-left: calc(1px - (1.5rem / 2)); margin-left: calc(1px - (1.5rem / 2));
} }
.combine-avatar.smaller:not(:first-child) {
margin-left: calc(1px - (1.75rem / 2));
}
.combine-avatar.small:not(:first-child) { .combine-avatar.small:not(:first-child) {
margin-left: calc(1px - 1rem); margin-left: calc(1px - 1rem);
} }
@ -70,5 +85,36 @@
.combine-avatar:not(:last-child) { .combine-avatar:not(:last-child) {
mask: radial-gradient(circle at 100% 50%, rgba(0, 0, 0, 0) 48.5%, rgb(0, 0, 0) 50%); mask: radial-gradient(circle at 100% 50%, rgba(0, 0, 0, 0) 48.5%, rgb(0, 0, 0) 50%);
} }
.combine-avatar.inline,
.combine-avatar.tiny,
.combine-avatar.card,
.combine-avatar.x-small {
font-size: 0.625rem;
}
.combine-avatar[data-over^='+']:last-child {
position: relative;
&::after {
content: attr(data-over);
position: absolute;
top: 50%;
left: 50%;
color: var(--theme-caption-color);
transform: translate(-53%, -52%);
z-index: 2;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--theme-bg-color);
border: 1px solid var(--theme-divider-color);
border-radius: 50%;
opacity: 0.9;
z-index: 1;
}
}
} }
</style> </style>

View File

@ -91,7 +91,7 @@
{#if contacts.length === 1} {#if contacts.length === 1}
<ContactPresenter value={contacts[0]} disabled /> <ContactPresenter value={contacts[0]} disabled />
{:else} {:else}
<CombineAvatars {_class} bind:items size={'inline'} /> <CombineAvatars {_class} bind:items size={'inline'} hideLimit />
<span class="overflow-label ml-1-5"> <span class="overflow-label ml-1-5">
<Label label={contact.string.NumberMembers} params={{ count: contacts.length }} /> <Label label={contact.string.NumberMembers} params={{ count: contacts.length }} />
</span> </span>

View File

@ -0,0 +1,81 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import type { Class, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { showPopup } from '@hcengineering/ui'
import type { IconSize } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { employeeByIdStore } from '../utils'
import CombineAvatars from './CombineAvatars.svelte'
import AddAvatar from './icons/AddAvatar.svelte'
import UsersPopup from './UsersPopup.svelte'
export let items: Ref<Employee>[] = []
export let _class: Ref<Class<Employee>> = contact.class.Employee
export let docQuery: DocumentQuery<Employee> | undefined = {
active: true
}
export let label: IntlString | undefined = undefined
export let size: IconSize = 'small'
export let width: string | undefined = undefined
export let readonly: boolean = false
export let limit: number = 6
export let hideLimit: boolean = false
let persons: Employee[] = items.map((p) => $employeeByIdStore.get(p)).filter((p) => p !== undefined) as Employee[]
$: persons = items.map((p) => $employeeByIdStore.get(p)).filter((p) => p !== undefined) as Employee[]
const dispatch = createEventDispatcher()
async function addPerson (evt: Event): Promise<void> {
showPopup(
UsersPopup,
{
_class,
label,
docQuery,
multiSelect: true,
allowDeselect: false,
selectedUsers: items,
readonly
},
evt.target as HTMLElement,
undefined,
(result) => {
if (result != null) {
items = result
dispatch('update', items)
}
}
)
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-row-center flex-nowrap content-pointer-events-none"
class:cursor-pointer={!readonly}
style:width={width ?? 'auto'}
on:click={readonly ? () => {} : addPerson}
>
{#if persons.length > 0}
<CombineAvatars {_class} bind:items {size} {limit} {hideLimit} />
{:else}
<AddAvatar {size} />
{/if}
</div>

View File

@ -0,0 +1,124 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import type { Class, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { Label, showPopup, ActionIcon, IconClose, IconAdd, Icon } from '@hcengineering/ui'
import type { IconSize } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../plugin'
import { employeeByIdStore } from '../utils'
import UserInfo from './UserInfo.svelte'
import UsersPopup from './UsersPopup.svelte'
export let items: Ref<Employee>[] = []
export let _class: Ref<Class<Employee>> = contact.class.Employee
export let docQuery: DocumentQuery<Employee> | undefined = {
active: true
}
export let label: IntlString | undefined = undefined
export let actionLabel: IntlString = plugin.string.AddMember
export let size: IconSize = 'x-small'
export let width: string | undefined = undefined
export let readonly: boolean = false
let persons: Employee[] = items.map((p) => $employeeByIdStore.get(p)).filter((p) => p !== undefined) as Employee[]
$: persons = items.map((p) => $employeeByIdStore.get(p)).filter((p) => p !== undefined) as Employee[]
const dispatch = createEventDispatcher()
async function addPerson (evt: Event): Promise<void> {
showPopup(
UsersPopup,
{
_class,
label,
docQuery,
multiSelect: true,
allowDeselect: false,
selectedUsers: items,
readonly
},
evt.target as HTMLElement,
undefined,
(result) => {
if (result != null) {
items = result
dispatch('update', items)
}
}
)
}
const removePerson = (removed: Employee) => {
const newItems = items.filter((it) => it !== removed._id)
dispatch('update', newItems)
}
</script>
<div class="flex-col" style:width={width ?? 'auto'}>
<div class="flex-row-center flex-wrap">
{#each persons as person}
<div class="usertag-container gap-1-5">
<UserInfo value={person} {size} />
<ActionIcon
icon={IconClose}
size={size === 'inline' ? 'x-small' : 'small'}
action={() => removePerson(person)}
/>
</div>
{/each}
</div>
{#if !readonly}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="addButton {size === 'inline' ? 'small' : 'medium'} overflow-label gap-2 cursor-pointer"
class:mt-2={persons.length > 0}
on:click={addPerson}
>
<span><Label label={actionLabel} /></span>
<Icon icon={IconAdd} size={size === 'inline' ? 'x-small' : 'small'} fill={'var(--theme-dark-color)'} />
</div>
{/if}
</div>
<style lang="scss">
.usertag-container {
display: flex;
align-items: center;
margin: 0 0.5rem 0.5rem 0;
padding: 0.375rem 0.625rem 0.375rem 0.5rem;
background-color: var(--theme-button-enabled);
border: 1px solid var(--theme-button-border);
border-radius: 0.25rem;
}
.addButton {
display: flex;
align-items: center;
font-weight: 500;
color: var(--theme-dark-color);
&.small {
height: 0.875rem;
font-size: 0.75rem;
line-height: 0.75rem;
}
&.medium {
height: 1.125rem;
}
}
</style>

View File

@ -16,7 +16,8 @@
import contact, { Employee } from '@hcengineering/contact' import contact, { Employee } from '@hcengineering/contact'
import type { Class, DocumentQuery, Ref } from '@hcengineering/core' import type { Class, DocumentQuery, Ref } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import { Button, ButtonKind, ButtonSize, Label, showPopup, TooltipAlignment } from '@hcengineering/ui' import { Button, Label, showPopup } from '@hcengineering/ui'
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import plugin from '../plugin' import plugin from '../plugin'
import { employeeByIdStore } from '../utils' import { employeeByIdStore } from '../utils'
@ -86,7 +87,7 @@
{#if persons.length === 1} {#if persons.length === 1}
<UserInfo value={persons[0]} size={'inline'} /> <UserInfo value={persons[0]} size={'inline'} />
{:else} {:else}
<CombineAvatars {_class} bind:items size={'inline'} /> <CombineAvatars {_class} bind:items size={'inline'} hideLimit />
<span class="overflow-label ml-1-5"> <span class="overflow-label ml-1-5">
<Label label={plugin.string.NumberMembers} params={{ count: persons.length }} /> <Label label={plugin.string.NumberMembers} params={{ count: persons.length }} />
</span> </span>

View File

@ -0,0 +1,82 @@
<!--
// 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 { IconSize } from '@hcengineering/ui'
export let size: IconSize
export let fill: string = 'var(--theme-caption-color)'
</script>
<svg class="svg-newavatar ava-{size}" viewBox="0 0 26 24" xmlns="http://www.w3.org/2000/svg">
<circle
fill="transparent"
stroke="var(--theme-trans-color)"
stroke-width="1"
stroke-dasharray="10%"
cx="12"
cy="12"
r="12"
/>
<path
fill="var(--theme-trans-color)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.1,9.3c0,1.7-1.4,3.1-3.1,3.1c-1.7,0-3.1-1.4-3.1-3.1c0-1.7,1.4-3.1,3.1-3.1C13.7,6.2,15.1,7.5,15.1,9.3z M12,17.8c-2.5,0-4.7-0.4-4.7-2c0-1.6,2.2-2,4.7-2c2.5,0,4.7,0.4,4.7,2C16.7,17.4,14.5,17.8,12,17.8z"
/>
<circle fill="var(--theme-navpanel-color)" cx="20" cy="18" r="5.5" />
<path
fill="var(--theme-trans-color)"
d="M20,13c2.8,0,5,2.2,5,5s-2.2,5-5,5s-5-2.2-5-5S17.2,13,20,13 M20,12c-3.3,0-6,2.7-6,6s2.7,6,6,6s6-2.7,6-6S23.3,12,20,12L20,12z"
/>
<path
fill="var(--theme-trans-color)"
d="M22.8,17.5h-2.2v-2.2c0-0.3-0.2-0.5-0.5-0.5s-0.5,0.2-0.5,0.5v2.2h-2.3c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h2.3v2.2c0,0.3,0.2,0.5,0.5,0.5s0.5-0.2,0.5-0.5v-2.2h2.2c0.3,0,0.5-0.2,0.5-0.5S23,17.5,22.8,17.5z"
/>
</svg>
<style lang="scss">
.svg-newavatar {
overflow: visible;
margin-right: -8%;
}
.ava-inline {
height: 0.875rem;
}
.ava-tiny {
height: 1.13rem;
}
.ava-card {
height: 1.25rem;
}
.ava-x-small {
height: 1.5rem;
}
.ava-smaller {
height: 1.75rem;
}
.ava-small {
height: 2rem;
}
.ava-medium {
height: 2.25rem;
}
.ava-large {
height: 4.5rem;
}
.ava-x-large {
height: 7.5rem;
}
</style>

View File

@ -73,6 +73,8 @@ import IconMembers from './components/icons/Members.svelte'
import ChannelPresenter from './components/ChannelPresenter.svelte' import ChannelPresenter from './components/ChannelPresenter.svelte'
import ChannelPanel from './components/ChannelPanel.svelte' import ChannelPanel from './components/ChannelPanel.svelte'
import ActivityChannelPresenter from './components/activity/ActivityChannelPresenter.svelte' import ActivityChannelPresenter from './components/activity/ActivityChannelPresenter.svelte'
import SelectAvatars from './components/SelectAvatars.svelte'
import UserBoxItems from './components/UserBoxItems.svelte'
import contact from './plugin' import contact from './plugin'
import { import {
@ -122,7 +124,9 @@ export {
SpaceMembers, SpaceMembers,
CombineAvatars, CombineAvatars,
UserInfo, UserInfo,
IconMembers IconMembers,
SelectAvatars,
UserBoxItems
} }
const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({ const toObjectSearchResult = (e: WithLookup<Contact>): ObjectSearchResult => ({
@ -286,7 +290,9 @@ export default async (): Promise<Resources> => ({
ChannelPresenter, ChannelPresenter,
ChannelPanel, ChannelPanel,
ActivityChannelPresenter, ActivityChannelPresenter,
SpaceMembers SpaceMembers,
SelectAvatars,
UserBoxItems
}, },
completion: { completion: {
EmployeeQuery: async ( EmployeeQuery: async (