TSK-852 List perform individual requests (#2765)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-03-17 22:05:22 +06:00 committed by GitHub
parent 892e7b1dbc
commit f0bbd4d443
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 405 additions and 467 deletions

View File

@ -1,19 +1,17 @@
<script lang="ts">
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import core, { IdMap, Ref, Space, toIdMap } from '@hcengineering/core'
import core, { IdMap, Ref, Space } from '@hcengineering/core'
import { ActionIcon, Button, IconClose, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import presentation from '../plugin'
import { createQuery, getClient } from '../utils'
import { getClient } from '../utils'
import UsersPopup from './UsersPopup.svelte'
export let value: Space
const dispatch = createEventDispatcher()
const client = getClient()
const query = createQuery()
let employees: IdMap<Employee> = new Map()
query.query(contact.class.Employee, {}, (res) => (employees = toIdMap(res)))
const employees: IdMap<Employee> = new Map()
let membersToAdd: EmployeeAccount[] = []
let channelMembers: Ref<Employee>[] = []

View File

@ -18,6 +18,7 @@
import type { Comment } from '@hcengineering/chunter'
import chunter from '@hcengineering/chunter'
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import { Avatar, getClient, MessageViewer } from '@hcengineering/presentation'
import { Icon, ShowMore, TimeSince } from '@hcengineering/ui'
@ -35,7 +36,7 @@
async function getEmployee (value: Comment): Promise<Employee | undefined> {
const acc = await client.findOne(contact.class.EmployeeAccount, { _id: value.modifiedBy as Ref<EmployeeAccount> })
if (acc !== undefined) {
const emp = await client.findOne(contact.class.Employee, { _id: acc.employee })
const emp = $employeeByIdStore.get(acc.employee)
return emp
}
}

View File

@ -16,12 +16,12 @@
import { Attachment } from '@hcengineering/attachment'
import { AttachmentList, AttachmentRefInput } from '@hcengineering/attachment-resources'
import type { ChunterMessage, Message, Reaction } from '@hcengineering/chunter'
import contact, { Employee, EmployeeAccount } from '@hcengineering/contact'
import { EmployeePresenter } from '@hcengineering/contact-resources'
import { EmployeeAccount } from '@hcengineering/contact'
import { employeeByIdStore, EmployeePresenter } from '@hcengineering/contact-resources'
import { getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
import { getResource } from '@hcengineering/platform'
import { Avatar, createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import { Avatar, getClient, MessageViewer } from '@hcengineering/presentation'
import { EmojiPopup } from '@hcengineering/text-editor'
import ui, { ActionIcon, Button, IconMoreH, Label, showPopup, tooltip } from '@hcengineering/ui'
import { Action } from '@hcengineering/view'
@ -46,16 +46,8 @@
let refInput: AttachmentRefInput
let employee: Employee | undefined
const employeeQuery = createQuery()
$: employeeQuery.query(
contact.class.Employee,
{
_id: (message.$lookup?.createBy as EmployeeAccount)?.employee
},
(res) => ([employee] = res)
)
$: empRef = (message.$lookup?.createBy as EmployeeAccount)?.employee
$: employee = empRef !== undefined ? $employeeByIdStore.get(empRef) : undefined
$: attachments = (message.$lookup?.attachments ?? []) as Attachment[]
const client = getClient()

View File

@ -1,6 +1,7 @@
<script lang="ts">
import chunter, { ChunterMessage } from '@hcengineering/chunter'
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref, Space, toIdMap } from '@hcengineering/core'
import { Avatar, createQuery, MessageViewer } from '@hcengineering/presentation'
import { IconClose } from '@hcengineering/ui'
@ -34,11 +35,6 @@
employeeAccoutsQuery.query(contact.class.EmployeeAccount, {}, (res) => (employeeAcounts = toIdMap(res)))
const employeeQuery = createQuery()
let employees: IdMap<Employee> = new Map()
employeeQuery.query(contact.class.Employee, {}, (res) => (employees = toIdMap(res)))
const dispatch = createEventDispatcher()
function getEmployee (
@ -55,7 +51,7 @@
<div class="antiPopup vScroll popup">
{#each pinnedMessages as message}
{@const employee = getEmployee(message, employeeAcounts, employees)}
{@const employee = getEmployee(message, employeeAcounts, $employeeByIdStore)}
<div class="message">
<div class="header">
<div class="avatar">

View File

@ -1,20 +1,17 @@
<script lang="ts">
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { Account, IdMap, Ref, toIdMap } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
export let reactionAccounts: Ref<Account>[]
let accounts: IdMap<EmployeeAccount> = new Map()
let employees: IdMap<Employee> = new Map()
const query = createQuery()
const empQ = createQuery()
$: query.query(contact.class.EmployeeAccount, {}, (res) => {
accounts = toIdMap(res)
})
empQ.query(contact.class.Employee, {}, (res) => (employees = toIdMap(res)))
function getAccName (acc: Ref<Account>, accounts: IdMap<EmployeeAccount>, employees: IdMap<Employee>): string {
const account = accounts.get(acc as Ref<EmployeeAccount>)
if (account !== undefined) {
@ -27,6 +24,6 @@
{#each reactionAccounts as acc}
<div>
{getAccName(acc, accounts, employees)}
{getAccName(acc, accounts, $employeeByIdStore)}
</div>
{/each}

View File

@ -14,9 +14,10 @@
-->
<script lang="ts">
import { Message } from '@hcengineering/chunter'
import contact, { Employee } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { Avatar, createQuery } from '@hcengineering/presentation'
import { Employee } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref } from '@hcengineering/core'
import { Avatar } from '@hcengineering/presentation'
import { Label, TimeSince } from '@hcengineering/ui'
import chunter from '../plugin'
@ -27,23 +28,17 @@
const shown: number = 4
let showReplies: Employee[] = []
const query = createQuery()
$: updateQuery(employees, $employeeByIdStore)
$: updateQuery(employees)
function updateQuery (employees: Set<Ref<Employee>>) {
query.query(
contact.class.Employee,
{
_id: { $in: Array.from(employees) }
},
(res) => {
showReplies = res
},
{
limit: shown
function updateQuery (employees: Set<Ref<Employee>>, map: IdMap<Employee>) {
showReplies = []
for (const employee of employees) {
const emp = map.get(employee)
if (emp !== undefined) {
showReplies.push(emp)
}
)
}
showReplies = showReplies
}
</script>

View File

@ -2,7 +2,8 @@
import attachment, { Attachment } from '@hcengineering/attachment'
import AttachmentPreview from '@hcengineering/attachment-resources/src/components/AttachmentPreview.svelte'
import { ChunterMessage } from '@hcengineering/chunter'
import contact, { Employee, EmployeeAccount, getName as getContactName } from '@hcengineering/contact'
import contact, { EmployeeAccount, getName as getContactName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import core, { IdMap, Ref, toIdMap, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Label, Scroller } from '@hcengineering/ui'
@ -17,17 +18,14 @@
let savedAttachmentsIds: Ref<Attachment>[] = []
let savedAttachments: WithLookup<Attachment>[] = []
let accounts: IdMap<EmployeeAccount> = new Map()
let employees: IdMap<Employee> = new Map()
const messagesQuery = createQuery()
const attachmentsQuery = createQuery()
const savedMessagesQuery = createQuery()
const savedAttachmentsQuery = createQuery()
const accQ = createQuery()
const empQ = createQuery()
accQ.query(contact.class.EmployeeAccount, {}, (res) => (accounts = toIdMap(res)))
empQ.query(contact.class.Employee, {}, (res) => (employees = toIdMap(res)))
savedMessagesQuery.query(chunter.class.SavedMessages, {}, (res) => {
savedMessagesIds = res.map((r) => r.attachedTo)
@ -83,7 +81,7 @@
function getName (a: Attachment): string | undefined {
const acc = accounts.get(a.modifiedBy as Ref<EmployeeAccount>)
if (acc !== undefined) {
const emp = employees.get(acc?.employee)
const emp = $employeeByIdStore.get(acc?.employee)
if (emp !== undefined) {
return getContactName(emp)
}

View File

@ -17,6 +17,7 @@
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import type { ChunterSpace, Message, ThreadMessage } from '@hcengineering/chunter'
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import core, {
FindOptions,
generateId,
@ -24,7 +25,6 @@
IdMap,
Ref,
SortingOrder,
toIdMap,
TxFactory
} from '@hcengineering/core'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
@ -102,11 +102,6 @@
)
}
let employees: IdMap<Employee> = new Map()
const employeeQuery = createQuery()
employeeQuery.query(contact.class.Employee, {}, (res) => (employees = toIdMap(res)))
async function getParticipants (
comments: ThreadMessage[],
parent: Message | undefined,
@ -176,7 +171,7 @@
<DmPresenter value={channel} />
{/if}
{/await}
{#await getParticipants(comments, parent, employees) then participants}
{#await getParticipants(comments, parent, $employeeByIdStore) then participants}
{participants.join(', ')}
<Label label={chunter.string.AndYou} params={{ participants: participants.length }} />
{/await}

View File

@ -1,5 +1,6 @@
import { chunterId, ChunterMessage, Comment, ThreadMessage } from '@hcengineering/chunter'
import contact, { EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { Class, Client, Doc, getCurrentAccount, Obj, Ref, Space, Timestamp } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
@ -48,11 +49,16 @@ export async function getDmName (client: Client, dm: Space): Promise<string> {
employeeAccounts = employeeAccounts.filter((p) => p._id !== myAccId)
}
const emloyees = await client.findAll(contact.class.Employee, {
_id: { $in: employeeAccounts.map((p) => p.employee) }
})
const map = get(employeeByIdStore)
const names: string[] = []
const name = emloyees.map((a) => getName(a)).join(', ')
for (const acc of employeeAccounts) {
const employee = map.get(acc.employee)
if (employee !== undefined) {
names.push(getName(employee))
}
}
const name = names.join(', ')
return name
}

View File

@ -14,28 +14,23 @@
// limitations under the License.
-->
<script lang="ts">
import { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import { EmployeeAccount, getName } from '@hcengineering/contact'
import { Account } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Avatar, createQuery } from '@hcengineering/presentation'
import { Avatar } from '@hcengineering/presentation'
import { showPopup, tooltip } from '@hcengineering/ui'
import { EditDoc } from '@hcengineering/view-resources'
import contact from '../plugin'
import { employeeByIdStore } from '../utils'
export let value: Account
let employee: Employee | undefined
$: employee = $employeeByIdStore.get((value as EmployeeAccount).employee)
async function onClick () {
if (employee !== undefined) {
showPopup(EditDoc, { _id: employee._id, _class: employee._class }, 'content')
}
}
const query = createQuery()
$: if (value && value._class === contact.class.EmployeeAccount) {
query.query(contact.class.Employee, { _id: (value as EmployeeAccount).employee }, (r) => ([employee] = r))
}
</script>
{#if value}

View File

@ -1,11 +1,12 @@
<script lang="ts">
import { Employee, EmployeeAccount, getName, Status } from '@hcengineering/contact'
import { getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
import { getCurrentAccount, Ref } from '@hcengineering/core'
import { Avatar, createQuery, getClient } from '@hcengineering/presentation'
import { Button, Label, resizeObserver, showPopup } from '@hcengineering/ui'
import { DocNavLink } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import contact from '../plugin'
import { employeeByIdStore } from '../utils'
import EmployeeSetStatusPopup from './EmployeeSetStatusPopup.svelte'
import EmployeeStatusPresenter from './EmployeeStatusPresenter.svelte'
import Edit from './icons/Edit.svelte'
@ -16,14 +17,10 @@
const me = (getCurrentAccount() as EmployeeAccount).employee
$: editable = employeeId === me
const employeeQuery = createQuery()
$: status = employee?.$lookup?.statuses?.[0]
let employee: WithLookup<Employee> | undefined
employeeQuery.query(contact.class.Employee, { _id: employeeId }, (res) => (employee = res[0]), {
lookup: {
_id: { statuses: contact.class.Status }
}
})
const statusesQuery = createQuery()
let status: Status | undefined = undefined
$: employee = $employeeByIdStore.get(employeeId)
statusesQuery.query(contact.class.Status, { attachedTo: employeeId }, (res) => (status = res[0]))
const dispatch = createEventDispatcher()

View File

@ -1,10 +1,11 @@
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import { Ref } from '@hcengineering/core'
import { AssigneeBox, createQuery } from '@hcengineering/presentation'
import { AssigneeBox } from '@hcengineering/presentation'
import { ButtonKind } from '@hcengineering/ui'
import { PersonLabelTooltip } from '..'
import contact from '../plugin'
import { employeeByIdStore } from '../utils'
import EmployeePresenter from './EmployeePresenter.svelte'
export let value: Ref<Employee> | null | undefined
@ -12,9 +13,7 @@
export let tooltipLabels: PersonLabelTooltip | undefined = undefined
export let onChange: ((value: Ref<Employee>) => void) | undefined = undefined
let employee: Employee | undefined
const query = createQuery()
$: value && query.query(contact.class.Employee, { _id: value }, (res) => ([employee] = res), { limit: 1 })
$: employee = value ? $employeeByIdStore.get(value) : undefined
function getValue (
employee: Employee | undefined,

View File

@ -70,6 +70,7 @@ import {
resolveLocation
} from './utils'
export { employeeByIdStore, employeesStore } from './utils'
export {
Channels,
ChannelsEditor,

View File

@ -23,12 +23,13 @@ import {
formatName,
getName
} from '@hcengineering/contact'
import { Doc, getCurrentAccount, ObjQueryType, Ref, Timestamp, toIdMap } from '@hcengineering/core'
import { Doc, getCurrentAccount, IdMap, ObjQueryType, Ref, Timestamp, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { TemplateDataProvider } from '@hcengineering/templates'
import { getPanelURI, Location } from '@hcengineering/ui'
import view, { Filter } from '@hcengineering/view'
import { FilterQuery } from '@hcengineering/view-resources'
import { get, writable } from 'svelte/store'
import contact from './plugin'
const client = getClient()
@ -53,36 +54,28 @@ export function formatDate (dueDateMs: Timestamp): string {
}
export async function employeeSort (value: Array<Ref<Employee>>): Promise<Array<Ref<Employee>>> {
return await new Promise((resolve) => {
const query = createQuery(true)
query.query(contact.class.Employee, { _id: { $in: value } }, (res) => {
const employees = toIdMap(res)
value.sort((a, b) => {
const employeeId1 = a as Ref<Employee> | null | undefined
const employeeId2 = b as Ref<Employee> | null | undefined
return value.sort((a, b) => {
const employeeId1 = a as Ref<Employee> | null | undefined
const employeeId2 = b as Ref<Employee> | null | undefined
if (employeeId1 == null && employeeId2 != null) {
return 1
}
if (employeeId1 == null && employeeId2 != null) {
return 1
}
if (employeeId1 != null && employeeId2 == null) {
return -1
}
if (employeeId1 != null && employeeId2 == null) {
return -1
}
if (employeeId1 != null && employeeId2 != null) {
const employee1 = employees.get(employeeId1)
const employee2 = employees.get(employeeId2)
const name1 = employee1 != null ? getName(employee1) : ''
const name2 = employee2 != null ? getName(employee2) : ''
if (employeeId1 != null && employeeId2 != null) {
const employee1 = get(employeeByIdStore).get(employeeId1)
const employee2 = get(employeeByIdStore).get(employeeId2)
const name1 = employee1 != null ? getName(employee1) : ''
const name2 = employee2 != null ? getName(employee2) : ''
return name1.localeCompare(name2)
}
return name1.localeCompare(name2)
}
return 0
})
resolve(value)
query.unsubscribe()
})
return 0
})
}
@ -208,3 +201,11 @@ async function generateLocation (loc: Location, shortLink: string): Promise<Loca
fragment: getPanelURI(view.component.EditDoc, doc._id, doc._class, 'content')
}
}
export const employeeByIdStore = writable<IdMap<Employee>>(new Map())
export const employeesStore = writable<Employee[]>([])
const query = createQuery(true)
query.query(contact.class.Employee, {}, (res) => {
employeesStore.set(res)
employeeByIdStore.set(toIdMap(res))
})

View File

@ -1,5 +1,6 @@
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2023 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
@ -13,27 +14,12 @@
// limitations under the License.
//
import MD5 from 'crypto-js/md5'
import {
Account,
AttachedData,
AttachedDoc,
Class,
Client,
Data,
Doc,
FindResult,
Ref,
Space,
Timestamp,
UXObject
} from '@hcengineering/core'
import { Account, AttachedDoc, Class, Doc, Ref, Space, Timestamp, UXObject } from '@hcengineering/core'
import type { Asset, Plugin, Resource } from '@hcengineering/platform'
import { IntlString, plugin } from '@hcengineering/platform'
import { TemplateField, TemplateFieldCategory } from '@hcengineering/templates'
import type { AnyComponent, IconSize } from '@hcengineering/ui'
import { FilterMode, ViewAction, Viewlet } from '@hcengineering/view'
import { TemplateFieldCategory, TemplateField } from '@hcengineering/templates'
/**
* @public
@ -164,57 +150,6 @@ export interface ContactsTab extends Doc {
index: number
}
const SEP = ','
/**
* @public
*/
export function combineName (first: string, last: string): string {
return last + SEP + first
}
/**
* @public
*/
export function getFirstName (name: string): string {
return name !== undefined ? name.substring(name.indexOf(SEP) + 1) : ''
}
/**
* @public
*/
export function getLastName (name: string): string {
return name !== undefined ? name.substring(0, name.indexOf(SEP)) : ''
}
/**
* @public
*/
export function formatName (name: string): string {
return getLastName(name) + ' ' + getFirstName(name)
}
/**
* @public
*/
export function getName (value: Contact): string {
if (isEmployee(value)) {
return value.displayName ?? formatName(value.name)
}
if (isPerson(value)) {
return formatName(value.name)
}
return value.name
}
function isEmployee (value: Contact): value is Employee {
return value._class === contactPlugin.class.Employee
}
function isPerson (value: Contact): value is Person {
return value._class === contactPlugin.class.Person
}
/**
* @public
*/
@ -223,7 +158,7 @@ export const contactId = 'contact' as Plugin
/**
* @public
*/
const contactPlugin = plugin(contactId, {
export const contactPlugin = plugin(contactId, {
class: {
AvatarProvider: '' as Ref<Class<AvatarProvider>>,
ChannelProvider: '' as Ref<Class<ChannelProvider>>,
@ -329,188 +264,5 @@ const contactPlugin = plugin(contactId, {
})
export default contactPlugin
/**
* @public
*/
export async function findContacts (
client: Client,
_class: Ref<Class<Doc>>,
person: Data<Contact>,
channels: AttachedData<Channel>[]
): Promise<{ contacts: Contact[], channels: AttachedData<Channel>[] }> {
if (channels.length === 0 && person.name.length === 0) {
return { contacts: [], channels: [] }
}
// Take only first part of first name for match.
const values = channels.map((it) => it.value)
// Same name persons
const potentialChannels = await client.findAll(
contactPlugin.class.Channel,
{ value: { $in: values } },
{ limit: 1000 }
)
let potentialContactIds = Array.from(new Set(potentialChannels.map((it) => it.attachedTo as Ref<Contact>)).values())
if (potentialContactIds.length === 0) {
if (client.getHierarchy().isDerived(_class, contactPlugin.class.Person)) {
const firstName = getFirstName(person.name).split(' ').shift() ?? ''
const lastName = getLastName(person.name)
// try match using just first/last name
potentialContactIds = (
await client.findAll(
contactPlugin.class.Contact,
{ name: { $like: `${lastName}%${firstName}%` } },
{ limit: 100 }
)
).map((it) => it._id)
if (potentialContactIds.length === 0) {
return { contacts: [], channels: [] }
}
} else if (client.getHierarchy().isDerived(_class, contactPlugin.class.Organization)) {
// try match using just first/last name
potentialContactIds = (
await client.findAll(contactPlugin.class.Contact, { name: { $like: `${person.name}` } }, { limit: 100 })
).map((it) => it._id)
if (potentialContactIds.length === 0) {
return { contacts: [], channels: [] }
}
}
}
const potentialPersons: FindResult<Contact> = await client.findAll(
contactPlugin.class.Contact,
{ _id: { $in: potentialContactIds } },
{
lookup: {
_id: {
channels: contactPlugin.class.Channel
}
}
}
)
const result: Contact[] = []
const resChannels: AttachedData<Channel>[] = []
for (const c of potentialPersons) {
let matches = 0
if (c.name === person.name) {
matches++
}
for (const ch of (c.$lookup?.channels as Channel[]) ?? []) {
for (const chc of channels) {
if (chc.provider === ch.provider && chc.value === ch.value.trim()) {
// We have matched value
resChannels.push(chc)
matches += 2
break
}
}
}
if (matches > 0) {
result.push(c)
}
}
return { contacts: result, channels: resChannels }
}
/**
* @public
*/
export async function findPerson (
client: Client,
person: Data<Person>,
channels: AttachedData<Channel>[]
): Promise<Person[]> {
const result = await findContacts(client, contactPlugin.class.Person, person, channels)
return result.contacts as Person[]
}
/**
* @public
*/
export type GravatarPlaceholderType =
| '404'
| 'mp'
| 'identicon'
| 'monsterid'
| 'wavatar'
| 'retro'
| 'robohash'
| 'blank'
/**
* @public
*/
export function buildGravatarId (email: string): string {
return MD5(email.trim().toLowerCase()).toString()
}
/**
* @public
*/
export function getGravatarUrl (
gravatarId: string,
size: IconSize = 'full',
placeholder: GravatarPlaceholderType = 'identicon'
): string {
let width = 64
switch (size) {
case 'inline':
case 'tiny':
case 'x-small':
case 'small':
case 'medium':
width = 64
break
case 'large':
width = 256
break
case 'x-large':
width = 512
break
}
return `https://gravatar.com/avatar/${gravatarId}?s=${width}&d=${placeholder}`
}
/**
* @public
*/
export async function checkHasGravatar (gravatarId: string, fetch?: typeof window.fetch): Promise<boolean> {
try {
return (await (fetch ?? window.fetch)(getGravatarUrl(gravatarId, 'full', '404'))).ok
} catch {
return false
}
}
const AVATAR_COLORS = [
'#4674ca', // blue
'#315cac', // blue_dark
'#57be8c', // green
'#3fa372', // green_dark
'#f9a66d', // yellow_orange
'#ec5e44', // red
'#e63717', // red_dark
'#f868bc', // pink
'#6c5fc7', // purple
'#4e3fb4', // purple_dark
'#57b1be', // teal
'#847a8c' // gray
]
/**
* @public
*/
export function getAvatarColorForId (id: string): string {
let hash = 0
for (let i = 0; i < id.length; i++) {
hash += id.charCodeAt(i)
}
return AVATAR_COLORS[hash % AVATAR_COLORS.length]
}
export * from './types'
export * from './utils'

View File

@ -0,0 +1,45 @@
//
// Copyright © 2023 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.
//
/**
* @public
*/
export type GravatarPlaceholderType =
| '404'
| 'mp'
| 'identicon'
| 'monsterid'
| 'wavatar'
| 'retro'
| 'robohash'
| 'blank'
/**
* @public
*/
export const AVATAR_COLORS = [
'#4674ca', // blue
'#315cac', // blue_dark
'#57be8c', // green
'#3fa372', // green_dark
'#f9a66d', // yellow_orange
'#ec5e44', // red
'#e63717', // red_dark
'#f868bc', // pink
'#6c5fc7', // purple
'#4e3fb4', // purple_dark
'#57b1be', // teal
'#847a8c' // gray
]

View File

@ -0,0 +1,228 @@
//
// Copyright © 2023 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.
//
import { AttachedData, Class, Client, Data, Doc, FindResult, Ref } from '@hcengineering/core'
import { IconSize } from '@hcengineering/ui'
import { MD5 } from 'crypto-js'
import { Channel, Contact, contactPlugin, Employee, Person } from '.'
import { AVATAR_COLORS, GravatarPlaceholderType } from './types'
/**
* @public
*/
export function getAvatarColorForId (id: string): string {
let hash = 0
for (let i = 0; i < id.length; i++) {
hash += id.charCodeAt(i)
}
return AVATAR_COLORS[hash % AVATAR_COLORS.length]
}
/**
* @public
*/
export function buildGravatarId (email: string): string {
return MD5(email.trim().toLowerCase()).toString()
}
/**
* @public
*/
export function getGravatarUrl (
gravatarId: string,
size: IconSize = 'full',
placeholder: GravatarPlaceholderType = 'identicon'
): string {
let width = 64
switch (size) {
case 'inline':
case 'tiny':
case 'x-small':
case 'small':
case 'medium':
width = 64
break
case 'large':
width = 256
break
case 'x-large':
width = 512
break
}
return `https://gravatar.com/avatar/${gravatarId}?s=${width}&d=${placeholder}`
}
/**
* @public
*/
export async function checkHasGravatar (gravatarId: string, fetch?: typeof window.fetch): Promise<boolean> {
try {
return (await (fetch ?? window.fetch)(getGravatarUrl(gravatarId, 'full', '404'))).ok
} catch {
return false
}
}
/**
* @public
*/
export async function findContacts (
client: Client,
_class: Ref<Class<Doc>>,
person: Data<Contact>,
channels: AttachedData<Channel>[]
): Promise<{ contacts: Contact[], channels: AttachedData<Channel>[] }> {
if (channels.length === 0 && person.name.length === 0) {
return { contacts: [], channels: [] }
}
// Take only first part of first name for match.
const values = channels.map((it) => it.value)
// Same name persons
const potentialChannels = await client.findAll(
contactPlugin.class.Channel,
{ value: { $in: values } },
{ limit: 1000 }
)
let potentialContactIds = Array.from(new Set(potentialChannels.map((it) => it.attachedTo as Ref<Contact>)).values())
if (potentialContactIds.length === 0) {
if (client.getHierarchy().isDerived(_class, contactPlugin.class.Person)) {
const firstName = getFirstName(person.name).split(' ').shift() ?? ''
const lastName = getLastName(person.name)
// try match using just first/last name
potentialContactIds = (
await client.findAll(
contactPlugin.class.Contact,
{ name: { $like: `${lastName}%${firstName}%` } },
{ limit: 100 }
)
).map((it) => it._id)
if (potentialContactIds.length === 0) {
return { contacts: [], channels: [] }
}
} else if (client.getHierarchy().isDerived(_class, contactPlugin.class.Organization)) {
// try match using just first/last name
potentialContactIds = (
await client.findAll(contactPlugin.class.Contact, { name: { $like: `${person.name}` } }, { limit: 100 })
).map((it) => it._id)
if (potentialContactIds.length === 0) {
return { contacts: [], channels: [] }
}
}
}
const potentialPersons: FindResult<Contact> = await client.findAll(
contactPlugin.class.Contact,
{ _id: { $in: potentialContactIds } },
{
lookup: {
_id: {
channels: contactPlugin.class.Channel
}
}
}
)
const result: Contact[] = []
const resChannels: AttachedData<Channel>[] = []
for (const c of potentialPersons) {
let matches = 0
if (c.name === person.name) {
matches++
}
for (const ch of (c.$lookup?.channels as Channel[]) ?? []) {
for (const chc of channels) {
if (chc.provider === ch.provider && chc.value === ch.value.trim()) {
// We have matched value
resChannels.push(chc)
matches += 2
break
}
}
}
if (matches > 0) {
result.push(c)
}
}
return { contacts: result, channels: resChannels }
}
/**
* @public
*/
export async function findPerson (
client: Client,
person: Data<Person>,
channels: AttachedData<Channel>[]
): Promise<Person[]> {
const result = await findContacts(client, contactPlugin.class.Person, person, channels)
return result.contacts as Person[]
}
const SEP = ','
/**
* @public
*/
export function combineName (first: string, last: string): string {
return last + SEP + first
}
/**
* @public
*/
export function getFirstName (name: string): string {
return name !== undefined ? name.substring(name.indexOf(SEP) + 1) : ''
}
/**
* @public
*/
export function getLastName (name: string): string {
return name !== undefined ? name.substring(0, name.indexOf(SEP)) : ''
}
/**
* @public
*/
export function formatName (name: string): string {
return getLastName(name) + ' ' + getFirstName(name)
}
/**
* @public
*/
export function getName (value: Contact): string {
if (isEmployee(value)) {
return value.displayName ?? formatName(value.name)
}
if (isPerson(value)) {
return formatName(value.name)
}
return value.name
}
function isEmployee (value: Contact): value is Employee {
return value._class === contactPlugin.class.Employee
}
function isPerson (value: Contact): value is Person {
return value._class === contactPlugin.class.Person
}

View File

@ -21,6 +21,7 @@
EmployeeAccount,
getName as getContactName
} from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { IdMap, Ref, SortingOrder, toIdMap } from '@hcengineering/core'
import { Message, SharedMessage } from '@hcengineering/gmail'
import { NotificationClientImpl } from '@hcengineering/notification-resources'
@ -40,16 +41,13 @@
let messages: Message[] = []
let accounts: IdMap<EmployeeAccount> = new Map()
let employees: IdMap<Employee> = new Map()
let selected: Set<Ref<SharedMessage>> = new Set<Ref<SharedMessage>>()
let selectable = false
const messagesQuery = createQuery()
const accountsQuery = createQuery()
const employeesQuery = createQuery()
accountsQuery.query(contact.class.EmployeeAccount, {}, (res) => (accounts = toIdMap(res)))
employeesQuery.query(contact.class.Employee, {}, (res) => (employees = toIdMap(res)))
const notificationClient = NotificationClientImpl.getClient()
@ -78,7 +76,7 @@
object._class,
'gmailSharedMessages',
{
messages: convertMessages(selectedMessages, accounts, employees)
messages: convertMessages(selectedMessages, accounts, $employeeByIdStore)
}
)
await notificationClient.updateLastView(channel._id, channel._class, undefined, true)
@ -168,7 +166,12 @@
<Scroller>
<div class="popupPanel-body__main-content py-4 clear-mins flex-no-shrink">
{#if messages && messages.length > 0}
<Messages messages={convertMessages(messages, accounts, employees)} {selectable} bind:selected on:select />
<Messages
messages={convertMessages(messages, accounts, $employeeByIdStore)}
{selectable}
bind:selected
on:select
/>
<div class="clear-mins h-4 flex-no-shrink" />
{:else}
<div class="flex-col-center justify-center h-full">

View File

@ -13,7 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee, EmployeeAccount, getName } from '@hcengineering/contact'
import contact, { EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { Request } from '@hcengineering/request'
@ -26,7 +27,7 @@
export let value: Request
let account: EmployeeAccount | undefined
let employee: Employee | undefined
$: employee = account && $employeeByIdStore.get(account.employee)
const query = createQuery()
@ -38,18 +39,6 @@
},
{ limit: 1 }
)
const employeeQuery = createQuery()
$: account &&
employeeQuery.query(
contact.class.Employee,
{ _id: account.employee },
(res) => {
;[employee] = res
},
{ limit: 1 }
)
</script>
<div class="container">

View File

@ -13,9 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee, EmployeeAccount, formatName } from '@hcengineering/contact'
import { EmployeePresenter } from '@hcengineering/contact-resources'
import { AccountRole, getCurrentAccount, IdMap, SortingOrder, toIdMap } from '@hcengineering/core'
import contact, { EmployeeAccount, formatName } from '@hcengineering/contact'
import { employeeByIdStore, EmployeePresenter } from '@hcengineering/contact-resources'
import { AccountRole, getCurrentAccount, SortingOrder } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { DropdownIntlItem, DropdownLabelsIntl, Icon, Label } from '@hcengineering/ui'
import setting from '../plugin'
@ -23,7 +23,6 @@
const client = getClient()
const query = createQuery()
const employeeQuery = createQuery()
const currentRole = getCurrentAccount().role
@ -35,7 +34,6 @@
let accounts: EmployeeAccount[] = []
$: owners = accounts.filter((p) => p.role === AccountRole.Owner)
let employees: IdMap<Employee> = new Map()
query.query(
contact.class.EmployeeAccount,
@ -48,10 +46,6 @@
}
)
employeeQuery.query(contact.class.Employee, {}, (res) => {
employees = toIdMap(res)
})
async function change (account: EmployeeAccount, value: AccountRole): Promise<void> {
await client.update(account, {
role: value
@ -67,7 +61,7 @@
<div class="ac-body columns">
<div class="ac-column max">
{#each accounts as account (account._id)}
{@const employee = employees.get(account.employee)}
{@const employee = $employeeByIdStore.get(account.employee)}
<div class="flex-between">
{#if employee}
<EmployeePresenter value={employee} isInteractive={false} />

View File

@ -13,41 +13,25 @@
// limitations under the License.
-->
<script lang="ts">
import { AttributeEditor, createQuery, EditableAvatar, getClient } from '@hcengineering/presentation'
import { AttributeEditor, EditableAvatar, getClient } from '@hcengineering/presentation'
import setting from '../plugin'
import { EditBox, Icon, Label, createFocusManager, FocusHandler, Button, showPopup } from '@hcengineering/ui'
import contact, { Employee, EmployeeAccount, getFirstName, getLastName } from '@hcengineering/contact'
import contact, { EmployeeAccount, getFirstName, getLastName } from '@hcengineering/contact'
import { ChannelsEditor, employeeByIdStore } from '@hcengineering/contact-resources'
import contactRes from '@hcengineering/contact-resources/src/plugin'
import { getCurrentAccount } from '@hcengineering/core'
import { changeName, leaveWorkspace } from '@hcengineering/login-resources'
import { ChannelsEditor } from '@hcengineering/contact-resources'
import MessageBox from '@hcengineering/presentation/src/components/MessageBox.svelte'
import { Button, createFocusManager, EditBox, FocusHandler, Icon, Label, showPopup } from '@hcengineering/ui'
import setting from '../plugin'
const client = getClient()
let avatarEditor: EditableAvatar
let employee: Employee | undefined
let firstName: string
let lastName: string
let displayName: string = ''
const employeeQ = createQuery()
const account = getCurrentAccount() as EmployeeAccount
employeeQ.query(
contact.class.Employee,
{
_id: account.employee
},
(res) => {
employee = res[0]
firstName = getFirstName(employee.name)
lastName = getLastName(employee.name)
displayName = employee.displayName ?? ''
},
{ limit: 1 }
)
const employee = $employeeByIdStore.get(account.employee)
let firstName: string = employee ? getFirstName(employee.name) : ''
let lastName: string = employee ? getLastName(employee.name) : ''
let displayName = employee?.displayName ?? ''
async function onAvatarDone (e: any) {
if (employee === undefined) return

View File

@ -1,8 +1,10 @@
import contact, { EmployeeAccount, getName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { Class, Doc, Hierarchy, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import { TemplateDataProvider } from '@hcengineering/templates'
import { get } from 'svelte/store'
function isEditable (hierarchy: Hierarchy, p: Class<Doc>): boolean {
let ancestors = [p._id]
@ -63,9 +65,7 @@ export async function getOwnerName (provider: TemplateDataProvider): Promise<str
_id: value.modifiedBy as Ref<EmployeeAccount>
})
if (employeeAccount !== undefined) {
const employee = await client.findOne(contact.class.Employee, {
_id: employeeAccount.employee
})
const employee = get(employeeByIdStore).get(employeeAccount.employee)
return employee != null ? getName(employee) : undefined
}
}

View File

@ -13,11 +13,12 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee, EmployeeAccount, formatName } from '@hcengineering/contact'
import { EmployeeAccount, formatName } from '@hcengineering/contact'
import { employeeByIdStore } from '@hcengineering/contact-resources'
import { getCurrentAccount } from '@hcengineering/core'
import login, { loginId } from '@hcengineering/login'
import { setMetadata } from '@hcengineering/platform'
import { Avatar, createQuery } from '@hcengineering/presentation'
import { Avatar } from '@hcengineering/presentation'
import setting, { settingId, SettingsCategory } from '@hcengineering/setting'
import {
closePopup,
@ -35,19 +36,7 @@
}
const account = getCurrentAccount() as EmployeeAccount
let employee: Employee | undefined
const employeeQ = createQuery()
employeeQ.query(
contact.class.Employee,
{
_id: account.employee
},
(res) => {
employee = res[0]
},
{ limit: 1 }
)
$: employee = $employeeByIdStore.get(account.employee)
function selectCategory (sp: SettingsCategory): void {
closePopup()

View File

@ -14,6 +14,7 @@
-->
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { employeeByIdStore, employeesStore } from '@hcengineering/contact-resources'
import { Class, Doc, DocumentQuery, generateId, IdMap, Lookup, Ref, toIdMap, WithLookup } from '@hcengineering/core'
import { Kanban, TypeState } from '@hcengineering/kanban'
import notification from '@hcengineering/notification'
@ -21,13 +22,13 @@
import { createQuery, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import {
Component as ComponentType,
Issue,
IssuesGrouping,
IssuesOrdering,
IssueStatus,
Component as ComponentType,
Sprint,
Project
Project,
Sprint
} from '@hcengineering/tracker'
import {
Button,
@ -54,8 +55,8 @@
import { onMount } from 'svelte'
import tracker from '../../plugin'
import { issuesGroupBySorting, mapKanbanCategories } from '../../utils'
import CreateIssue from '../CreateIssue.svelte'
import ComponentEditor from '../components/ComponentEditor.svelte'
import CreateIssue from '../CreateIssue.svelte'
import AssigneePresenter from './AssigneePresenter.svelte'
import SubIssuesSelector from './edit/SubIssuesSelector.svelte'
import IssuePresenter from './IssuePresenter.svelte'
@ -137,8 +138,7 @@
const lookupIssue: Lookup<Issue> = {
status: tracker.class.IssueStatus,
component: tracker.class.Component,
sprint: tracker.class.Sprint,
assignee: contact.class.Employee
sprint: tracker.class.Sprint
}
$: issuesQuery.query(
tracker.class.Issue,
@ -152,12 +152,6 @@
}
)
const assigneeQuery = createQuery()
let assignee: Employee[] = []
$: assigneeQuery.query(contact.class.Employee, {}, (result) => {
assignee = result
})
const statusesQuery = createQuery()
let statuses: WithLookup<IssueStatus>[] = []
let statusesMap: IdMap<IssueStatus> = new Map()
@ -212,7 +206,7 @@
statuses,
components,
sprints,
assignee
$employeesStore
)
function update () {
@ -225,7 +219,7 @@
statuses,
components,
sprints,
assignee
$employeesStore
)
}
@ -354,7 +348,7 @@
</div>
<div class="abs-rt-content">
<AssigneePresenter
value={issue.$lookup?.assignee}
value={issue.assignee ? $employeeByIdStore.get(issue.assignee) : null}
defaultClass={contact.class.Employee}
object={issue}
isEditable={true}

View File

@ -13,8 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee, EmployeeAccount } from '@hcengineering/contact'
import { EmployeePresenter } from '@hcengineering/contact-resources'
import contact, { EmployeeAccount } from '@hcengineering/contact'
import { employeeByIdStore, EmployeePresenter } from '@hcengineering/contact-resources'
import core, { ClassifierKind, Doc, Mixin, Ref, WithLookup } from '@hcengineering/core'
import { AttributeBarEditor, createQuery, getClient, KeyedAttribute } from '@hcengineering/presentation'
@ -78,10 +78,9 @@
$: updateKeys(['title', 'description', 'priority', 'status', 'number', 'assignee', 'component', 'dueDate', 'sprint'])
const employeeAccountQuery = createQuery()
const employeeQuery = createQuery()
let account: EmployeeAccount | undefined
let employee: Employee | undefined
$: employee = account && $employeeByIdStore.get(account.employee)
$: employeeAccountQuery.query(
contact.class.EmployeeAccount,
@ -91,16 +90,6 @@
},
{ limit: 1 }
)
$: account &&
employeeQuery.query(
contact.class.Employee,
{ _id: account.employee },
(res) => {
;[employee] = res
},
{ limit: 1 }
)
</script>
<div class="content">