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", "FacebookPlaceholder": "https://fb.com/jappleseed",
"Facebook": "Facebook", "Facebook": "Facebook",
"SocialLinks": "Socail links", "SocialLinks": "Socail links",
"ViewActivity": "View activity" "ViewActivity": "View activity",
"PersonAlreadyExists": "Person already exists..."
} }
} }

View File

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

View File

@ -1,4 +1,3 @@
<!-- <!--
// Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc. // Copyright © 2021 Hardcore Engineering Inc.
@ -16,17 +15,18 @@
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' 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 { getResource } from '@anticrm/platform'
import { getClient, Card, EditableAvatar } from '@anticrm/presentation' import { getClient, Card, EditableAvatar } from '@anticrm/presentation'
import attachment from '@anticrm/attachment' 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 contact from '../plugin'
import Channels from './Channels.svelte' import Channels from './Channels.svelte'
import PersonPresenter from './PersonPresenter.svelte'
let firstName = '' let firstName = ''
let lastName = '' let lastName = ''
@ -52,9 +52,7 @@
async function createPerson () { async function createPerson () {
const uploadFile = await getResource(attachment.helper.UploadFile) const uploadFile = await getResource(attachment.helper.UploadFile)
const avatarProp = avatar !== undefined const avatarProp = avatar !== undefined ? { avatar: await uploadFile(avatar) } : {}
? { avatar: await uploadFile(avatar) }
: {}
const person: Data<Person> = { const person: Data<Person> = {
name: combineName(firstName, lastName), name: combineName(firstName, lastName),
@ -74,30 +72,57 @@
} }
let channels: AttachedData<Channel>[] = [] let channels: AttachedData<Channel>[] = []
let matches: FindResult<Person> = []
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => {
matches = p
})
</script> </script>
<Card <Card
label={contact.string.CreatePerson} label={contact.string.CreatePerson}
okAction={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} bind:space={contact.space.Contacts}
on:close={() => { on:close={() => {
dispatch('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="flex-row-center">
<div class="mr-4"> <div class="mr-4">
<EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} /> <EditableAvatar avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
</div> </div>
<div class="flex-col"> <div class="flex-col">
<div class="fs-title"><EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} /></div> <div class="fs-title">
<div class="fs-title mb-1"><EditBox placeholder={contact.string.PersonLastNamePlaceholder} maxWidth="12rem" bind:value={lastName} /></div> <EditBox placeholder={contact.string.PersonFirstNamePlaceholder} maxWidth="12rem" bind:value={firstName} />
<div class="text-sm"><EditBox placeholder={contact.string.PersonLocationPlaceholder} maxWidth="12rem" bind:value={object.city} /></div> </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> </div>
<div class="flex-row-center channels"> <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> </div>
</Card> </Card>
@ -105,4 +130,23 @@
.channels { .channels {
margin-top: 1.25rem; 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> </style>

View File

@ -13,9 +13,9 @@
// limitations under the License. // 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 { 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' import type { AnyComponent } from '@anticrm/ui'
/** /**
@ -119,7 +119,10 @@ export function formatName (name: string): string {
*/ */
export const contactId = 'contact' as Plugin export const contactId = 'contact' as Plugin
export default plugin(contactId, { /**
* @public
*/
const contactPlugin = plugin(contactId, {
class: { class: {
ChannelProvider: '' as Ref<Class<ChannelProvider>>, ChannelProvider: '' as Ref<Class<ChannelProvider>>,
Channel: '' as Ref<Class<Channel>>, Channel: '' as Ref<Class<Channel>>,
@ -166,5 +169,70 @@ export default plugin(contactId, {
}, },
app: { app: {
Contacts: '' as Ref<Doc> 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"> <script lang="ts">
import attachment from '@anticrm/attachment' 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 { 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 login from '@anticrm/login'
import { getMetadata, getResource, setPlatformStatus, unknownError } from '@anticrm/platform' import { getMetadata, getResource, setPlatformStatus, unknownError } from '@anticrm/platform'
import { import presentation, {
Card, Card,
createQuery, createQuery,
EditableAvatar, EditableAvatar,
@ -34,9 +46,12 @@
import { import {
Component, Component,
EditBox, EditBox,
getColorNumberByText, IconFile as FileIcon, getColorNumberByText,
IconFile as FileIcon,
IconInfo,
Label, Label,
Link, showPopup, Link,
showPopup,
Spinner Spinner
} from '@anticrm/ui' } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
@ -156,7 +171,12 @@
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate }) const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
// Tag elements // 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) { for (const skill of skills) {
// Create update tag if missing // Create update tag if missing
if (!skillTagElements.has(skill.tag)) { if (!skillTagElements.has(skill.tag)) {
@ -246,10 +266,12 @@
// Create skills // Create skills
await elementsPromise await elementsPromise
const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate }) 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 categoriesMap = new Map(Array.from(categories.map((it) => [it._id, it])))
const newSkills:TagReference[] = [] const newSkills: TagReference[] = []
// Create missing tag elemnts // Create missing tag elemnts
for (const s of doc.skills ?? []) { for (const s of doc.skills ?? []) {
const title = s.trim().toLowerCase() 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 <Card
label={recruit.string.CreateCandidate} label={recruit.string.CreateCandidate}
okAction={createCandidate} okAction={createCandidate}
canSave={firstName.length > 0 && lastName.length > 0} canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0}
space={contact.space.Contacts} space={contact.space.Contacts}
on:close={() => { on:close={() => {
dispatch('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="flex-row-center">
<div class="mr-4"> <div class="mr-4">
<EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:done={onAvatarDone} /> <EditableAvatar bind:direct={avatar} avatar={object.avatar} size={'large'} on:done={onAvatarDone} />
</div> </div>
<div class="flex-col"> <div class="flex-col">
<div class="fs-title"><EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} /></div> <div class="fs-title">
<div class="fs-title mb-1"><EditBox placeholder={recruit.string.PersonLastNamePlaceholder} maxWidth="10rem" bind:value={lastName} /></div> <EditBox placeholder={recruit.string.PersonFirstNamePlaceholder} maxWidth="10rem" bind:value={firstName} />
<div class="text-sm"><EditBox placeholder={recruit.string.Title} maxWidth="10rem" bind:value={object.title} /></div> </div>
<div class="text-sm"><EditBox placeholder={recruit.string.Location} maxWidth="10rem" bind:value={object.city} /></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>
</div> </div>
@ -490,4 +533,23 @@
border-style: solid; 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> </style>