Show Person Already Exists for Person/Candidates (#1059)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-02-25 20:23:50 +07:00 committed by GitHub
parent bddd5f0628
commit 5d5d1a9acf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 210 additions and 34 deletions

View File

@ -44,6 +44,7 @@
"FacebookPlaceholder": "https://fb.com/jappleseed",
"Facebook": "Facebook",
"SocialLinks": "Socail links",
"ViewActivity": "View activity"
"ViewActivity": "View activity",
"PersonAlreadyExists": "Person already exists..."
}
}

View File

@ -44,6 +44,7 @@
"FacebookPlaceholder": "https://fb.com/jappleseed",
"Facebook": "Facebook",
"SocialLinks": "Контактная информация",
"ViewActivity": "Посмотреть активность"
"ViewActivity": "Посмотреть активность",
"PersonAlreadyExists": "Контакт уже существует..."
}
}

View File

@ -1,4 +1,3 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
@ -16,17 +15,18 @@
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { AttachedData, Data, generateId } from '@anticrm/core'
import { AttachedData, Data, FindResult, generateId } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { getClient, Card, EditableAvatar } from '@anticrm/presentation'
import attachment from '@anticrm/attachment'
import { EditBox } from '@anticrm/ui'
import { EditBox, IconInfo, Label } from '@anticrm/ui'
import { Channel, combineName, Person } from '@anticrm/contact'
import { Channel, combineName, findPerson, Person } from '@anticrm/contact'
import contact from '../plugin'
import Channels from './Channels.svelte'
import PersonPresenter from './PersonPresenter.svelte'
let firstName = ''
let lastName = ''
@ -52,9 +52,7 @@
async function createPerson () {
const uploadFile = await getResource(attachment.helper.UploadFile)
const avatarProp = avatar !== undefined
? { avatar: await uploadFile(avatar) }
: {}
const avatarProp = avatar !== undefined ? { avatar: await uploadFile(avatar) } : {}
const person: Data<Person> = {
name: combineName(firstName, lastName),
@ -74,30 +72,57 @@
}
let channels: AttachedData<Channel>[] = []
let matches: FindResult<Person> = []
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => {
matches = p
})
</script>
<Card
label={contact.string.CreatePerson}
okAction={createPerson}
canSave={firstName.length > 0 && lastName.length > 0}
canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0}
bind:space={contact.space.Contacts}
on:close={() => {
dispatch('close')
}}
>
{#if matches.length > 0}
<div class="flex-row update-container ERROR">
<div class="flex mb-2">
<IconInfo size={'small'} />
<div class="text-sm ml-2 overflow-label">
<Label label={contact.string.PersonAlreadyExists} />
</div>
</div>
<PersonPresenter value={matches[0]} />
</div>
{/if}
<div class="flex-row-center">
<div class="mr-4">
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
</div>
<div class="flex-col">
<div class="fs-title"><EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} /></div>
<div class="fs-title mb-1"><EditBox placeholder={contact.string.PersonLastNamePlaceholder} maxWidth="12rem" bind:value={lastName} /></div>
<div class="text-sm"><EditBox placeholder={contact.string.PersonLocationPlaceholder} maxWidth="12rem" bind:value={object.city} /></div>
<div class="fs-title">
<EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} />
</div>
<div class="fs-title mb-1">
<EditBox placeholder={contact.string.PersonLastNamePlaceholder} maxWidth="12rem" bind:value={lastName} />
</div>
<div class="text-sm">
<EditBox placeholder={contact.string.PersonLocationPlaceholder} maxWidth="12rem" bind:value={object.city} />
</div>
</div>
</div>
<div class="flex-row-center channels">
<Channels bind:channels={channels} on:change={(e) => { channels = e.detail }} />
<Channels
bind:channels
on:change={(e) => {
channels = e.detail
}}
/>
</div>
</Card>
@ -105,4 +130,23 @@
.channels {
margin-top: 1.25rem;
}
.update-container {
margin-left: -1rem;
margin-right: -1rem;
padding: 1rem;
margin-bottom: 1rem;
user-select: none;
font-size: 14px;
color: var(--theme-content-color);
&.WARNING {
color: yellow;
}
&.ERROR {
color: var(--system-error-color);
}
border: 1px dashed var(--theme-zone-border);
border-radius: 0.5rem;
backdrop-filter: blur(10px);
}
</style>

View File

@ -13,9 +13,9 @@
// limitations under the License.
//
import type { Account, AttachedData, AttachedDoc, Class, Client, Data, Doc, FindResult, Ref, Space, UXObject } from '@anticrm/core'
import type { Asset, Plugin } from '@anticrm/platform'
import { IntlString, plugin } from '@anticrm/platform'
import type { Plugin, Asset } from '@anticrm/platform'
import type { Doc, Ref, Class, UXObject, Space, Account, AttachedDoc } from '@anticrm/core'
import type { AnyComponent } from '@anticrm/ui'
/**
@ -119,7 +119,10 @@ export function formatName (name: string): string {
*/
export const contactId = 'contact' as Plugin
export default plugin(contactId, {
/**
* @public
*/
const contactPlugin = plugin(contactId, {
class: {
ChannelProvider: '' as Ref<Class<ChannelProvider>>,
Channel: '' as Ref<Class<Channel>>,
@ -166,5 +169,70 @@ export default plugin(contactId, {
},
app: {
Contacts: '' as Ref<Doc>
},
string: {
PersonAlreadyExists: '' as IntlString
}
})
export default contactPlugin
/**
* @public
*/
export async function findPerson (client: Client, person: Data<Person>, channels: AttachedData<Channel>[]): Promise<FindResult<Person>> {
if (channels.length === 0 || person.name.length === 0) {
return []
}
// 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 } })
let potentialPersonIds = Array.from(new Set(potentialChannels.map(it => it.attachedTo as Ref<Person>)).values())
if (potentialPersonIds.length === 0) {
const firstName = getFirstName(person.name).split(' ').shift() ?? ''
const lastName = getLastName(person.name)
// try match using just first/last name
potentialPersonIds = (await client.findAll(contactPlugin.class.Person, { name: { $like: `${lastName}%${firstName}%` } })).map(it => it._id)
if (potentialPersonIds.length === 0) {
return []
}
}
const potentialPersons: FindResult<Person> = await client.findAll(contactPlugin.class.Person, { _id: { $in: potentialPersonIds } }, {
lookup: {
_id: {
channels: contactPlugin.class.Channel
}
}
})
const result: FindResult<Person> = []
for (const c of potentialPersons) {
let matches = 0
if (c.name === person.name) {
matches++
}
if (c.city === person.city) {
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
matches += 2
break
}
}
}
if (matches >= 2) {
result.push(c)
}
}
return result
}

View File

@ -14,12 +14,24 @@
-->
<script lang="ts">
import attachment from '@anticrm/attachment'
import contact, { Channel, ChannelProvider, combineName, Person } from '@anticrm/contact'
import contact, { Channel, ChannelProvider, combineName, findPerson, Person } from '@anticrm/contact'
import { Channels } from '@anticrm/contact-resources'
import { Account, AttachedData, Data, Doc, generateId, MixinData, Ref, TxProcessor } from '@anticrm/core'
import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte'
import {
Account,
AttachedData,
Data,
Doc,
FindResult,
generateId,
Hierarchy,
MixinData,
Ref,
TxProcessor
} from '@anticrm/core'
import login from '@anticrm/login'
import { getMetadata, getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
import {
import presentation, {
Card,
createQuery,
EditableAvatar,
@ -34,9 +46,12 @@
import {
Component,
EditBox,
getColorNumberByText, IconFile as FileIcon,
getColorNumberByText,
IconFile as FileIcon,
IconInfo,
Label,
Link, showPopup,
Link,
showPopup,
Spinner
} from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
@ -156,7 +171,12 @@
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
// Tag elements
const skillTagElements = new Map((await client.findAll(tags.class.TagElement, { _id: { $in: skills.map(it => it.tag) } })).map(it => ([it._id, it])))
const skillTagElements = new Map(
(await client.findAll(tags.class.TagElement, { _id: { $in: skills.map((it) => it.tag) } })).map((it) => [
it._id,
it
])
)
for (const skill of skills) {
// Create update tag if missing
if (!skillTagElements.has(skill.tag)) {
@ -246,10 +266,12 @@
// Create skills
await elementsPromise
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
const categoriesMap = new Map(Array.from(categories.map(it => ([it._id, it]))))
const newSkills:TagReference[] = []
const categoriesMap = new Map(Array.from(categories.map((it) => [it._id, it])))
const newSkills: TagReference[] = []
// Create missing tag elemnts
for (const s of doc.skills ?? []) {
const title = s.trim().toLowerCase()
@ -356,29 +378,50 @@
}
]
}
</script>
<!-- <DialogHeader {space} {object} {newValue} {resume} create={true} on:save={createCandidate}/> -->
let matches: FindResult<Person> = []
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => {
matches = p
})
</script>
<Card
label={recruit.string.CreateCandidate}
okAction={createCandidate}
canSave={firstName.length > 0 && lastName.length > 0}
canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0}
space={contact.space.Contacts}
on:close={() => {
dispatch('close')
}}
>
<!-- <StatusComponent slot="error" status={{ severity: Severity.ERROR, code: 'Cant save the object because it already exists' }} /> -->
{#if matches.length > 0}
<div class="flex-row update-container ERROR">
<div class="flex mb-2">
<IconInfo size={'small'} />
<div class="text-sm ml-2 overflow-label">
<Label label={contact.string.PersonAlreadyExists} />
</div>
</div>
<PersonPresenter value={matches[0]} />
</div>
{/if}
<div class="flex-row-center">
<div class="mr-4">
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
</div>
<div class="flex-col">
<div class="fs-title"><EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} /></div>
<div class="fs-title mb-1"><EditBox placeholder={recruit.string.PersonLastNamePlaceholder} maxWidth="10rem" bind:value={lastName} /></div>
<div class="text-sm"><EditBox placeholder={recruit.string.Title} maxWidth="10rem" bind:value={object.title} /></div>
<div class="text-sm"><EditBox placeholder={recruit.string.Location} maxWidth="10rem" bind:value={object.city} /></div>
<div class="fs-title">
<EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} />
</div>
<div class="fs-title mb-1">
<EditBox placeholder={recruit.string.PersonLastNamePlaceholder} maxWidth="10rem" bind:value={lastName} />
</div>
<div class="text-sm">
<EditBox placeholder={recruit.string.Title} maxWidth="10rem" bind:value={object.title} />
</div>
<div class="text-sm">
<EditBox placeholder={recruit.string.Location} maxWidth="10rem" bind:value={object.city} />
</div>
</div>
</div>
@ -490,4 +533,23 @@
border-style: solid;
}
}
.update-container {
margin-left: -1rem;
margin-right: -1rem;
padding: 1rem;
margin-bottom: 1rem;
user-select: none;
font-size: 14px;
color: var(--theme-content-color);
&.WARNING {
color: yellow;
}
&.ERROR {
color: var(--system-error-color);
}
border: 1px dashed var(--theme-zone-border);
border-radius: 0.5rem;
backdrop-filter: blur(10px);
}
</style>