mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 11:01:54 +03:00
parent
ce3ef44592
commit
c38bf48d75
@ -277,7 +277,7 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
// Mixin potentially added to object we doesn't have in out results
|
||||
const doc = await this.findOne(q._class, { _id: tx.objectId }, q.options)
|
||||
if (doc !== undefined) {
|
||||
await this.handleDocAdd(q, doc)
|
||||
await this.handleDocAdd(q, doc, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -547,13 +547,13 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
return {}
|
||||
}
|
||||
|
||||
private async handleDocAdd (q: Query, doc: Doc): Promise<void> {
|
||||
private async handleDocAdd (q: Query, doc: Doc, handleLookup = true): Promise<void> {
|
||||
if (this.match(q, doc)) {
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
|
||||
if (q.options?.lookup !== undefined) {
|
||||
if (q.options?.lookup !== undefined && handleLookup) {
|
||||
await this.lookup(q._class, doc, q.options.lookup)
|
||||
}
|
||||
// We could already have document inside results, if query is created during processing of document create transaction and not yet handled on client.
|
||||
|
@ -47,7 +47,7 @@
|
||||
"Homepage": "Home page",
|
||||
"SocialLinks": "Socail links",
|
||||
"ViewActivity": "View activity",
|
||||
"PersonAlreadyExists": "Person already exists...",
|
||||
"PersonAlreadyExists": "Contact already exists...",
|
||||
"Status": "Status",
|
||||
"SetStatus": "Set status",
|
||||
"ClearStatus": "Clear status",
|
||||
|
@ -13,14 +13,15 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Channel, Organization } from '@anticrm/contact'
|
||||
import { AttachedData, generateId } from '@anticrm/core'
|
||||
import { Channel, findContacts, Organization } from '@anticrm/contact'
|
||||
import { AttachedData, generateId, WithLookup } from '@anticrm/core'
|
||||
import { Card, getClient } from '@anticrm/presentation'
|
||||
import { Button, EditBox, createFocusManager, FocusHandler } from '@anticrm/ui'
|
||||
import { Button, createFocusManager, EditBox, FocusHandler, IconInfo, Label } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import contact from '../plugin'
|
||||
import ChannelsDropdown from './ChannelsDropdown.svelte'
|
||||
import Company from './icons/Company.svelte'
|
||||
import OrganizationPresenter from './OrganizationPresenter.svelte'
|
||||
|
||||
export function canClose (): boolean {
|
||||
return object.name === ''
|
||||
@ -57,6 +58,13 @@
|
||||
let channels: AttachedData<Channel>[] = []
|
||||
|
||||
const manager = createFocusManager()
|
||||
|
||||
let matches: WithLookup<Organization>[] = []
|
||||
let matchedChannels: AttachedData<Channel>[] = []
|
||||
$: findContacts(client, contact.class.Organization, { ...object, name: object.name }, channels).then((p) => {
|
||||
matches = p.contacts as Organization[]
|
||||
matchedChannels = p.channels
|
||||
})
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -83,6 +91,22 @@
|
||||
/>
|
||||
</div>
|
||||
<svelte:fragment slot="pool">
|
||||
<ChannelsDropdown bind:value={channels} focusIndex={10} editable />
|
||||
<ChannelsDropdown
|
||||
bind:value={channels}
|
||||
focusIndex={10}
|
||||
editable
|
||||
highlighted={matchedChannels.map((it) => it.provider)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
{#if matches.length > 0}
|
||||
<div class="flex-row-center error-color">
|
||||
<IconInfo size={'small'} />
|
||||
<span class="text-sm overflow-label ml-2">
|
||||
<Label label={contact.string.PersonAlreadyExists} />
|
||||
</span>
|
||||
<div class="ml-4"><OrganizationPresenter value={matches[0]} /></div>
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
@ -226,13 +226,14 @@ export default contactPlugin
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function findPerson (
|
||||
export async function findContacts (
|
||||
client: Client,
|
||||
person: Data<Person>,
|
||||
_class: Ref<Class<Doc>>,
|
||||
person: Data<Contact>,
|
||||
channels: AttachedData<Channel>[]
|
||||
): Promise<Person[]> {
|
||||
if (channels.length === 0 || person.name.length === 0) {
|
||||
return []
|
||||
): 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)
|
||||
@ -240,23 +241,33 @@ export async function findPerson (
|
||||
// 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())
|
||||
let potentialContactIds = Array.from(new Set(potentialChannels.map((it) => it.attachedTo as Ref<Contact>)).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 []
|
||||
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}%` } })
|
||||
).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}` } })
|
||||
).map((it) => it._id)
|
||||
if (potentialContactIds.length === 0) {
|
||||
return { contacts: [], channels: [] }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const potentialPersons: FindResult<Person> = await client.findAll(
|
||||
contactPlugin.class.Person,
|
||||
{ _id: { $in: potentialPersonIds } },
|
||||
const potentialPersons: FindResult<Contact> = await client.findAll(
|
||||
contactPlugin.class.Contact,
|
||||
{ _id: { $in: potentialContactIds } },
|
||||
{
|
||||
lookup: {
|
||||
_id: {
|
||||
@ -266,29 +277,40 @@ export async function findPerson (
|
||||
}
|
||||
)
|
||||
|
||||
const result: Person[] = []
|
||||
|
||||
const result: Contact[] = []
|
||||
const resChannels: AttachedData<Channel>[] = []
|
||||
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
|
||||
resChannels.push(chc)
|
||||
matches += 2
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matches >= 2) {
|
||||
if (matches > 0) {
|
||||
result.push(c)
|
||||
}
|
||||
}
|
||||
return result
|
||||
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[]
|
||||
}
|
||||
|
@ -14,24 +14,24 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment from '@anticrm/attachment'
|
||||
import { Channel, combineName, Contact, findPerson } from '@anticrm/contact'
|
||||
import { Channel, combineName, Contact, findContacts } from '@anticrm/contact'
|
||||
import { ChannelsDropdown } from '@anticrm/contact-resources'
|
||||
import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte'
|
||||
import contact from '@anticrm/contact-resources/src/plugin'
|
||||
import { AttachedData, Class, Data, Doc, generateId, MixinData, Ref } from '@anticrm/core'
|
||||
import { AttachedData, Class, Data, Doc, generateId, MixinData, Ref, WithLookup } from '@anticrm/core'
|
||||
import type { Customer } from '@anticrm/lead'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { Card, EditableAvatar, getClient } from '@anticrm/presentation'
|
||||
import {
|
||||
Button,
|
||||
createFocusManager,
|
||||
EditBox,
|
||||
eventToHTMLElement,
|
||||
FocusHandler,
|
||||
IconInfo,
|
||||
Label,
|
||||
SelectPopup,
|
||||
showPopup,
|
||||
createFocusManager,
|
||||
FocusHandler
|
||||
showPopup
|
||||
} from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import lead from '../plugin'
|
||||
@ -56,7 +56,7 @@
|
||||
let avatar: File | undefined
|
||||
|
||||
function formatName (targetClass: Ref<Class<Doc>>, firstName: string, lastName: string, objectName: string): string {
|
||||
return targetClass === contact.class.Person ? combineName(firstName, lastName) : objectName
|
||||
return targetClass === contact.class.Person ? combineName(firstName.trim(), lastName.trim()) : objectName
|
||||
}
|
||||
|
||||
async function createCustomer () {
|
||||
@ -114,15 +114,6 @@
|
||||
avatar = file
|
||||
}
|
||||
|
||||
let matches: Contact[] = []
|
||||
$: findPerson(
|
||||
client,
|
||||
{ ...object, name: formatName(targetClass._id, firstName, lastName, object.name) },
|
||||
channels
|
||||
).then((p) => {
|
||||
matches = p
|
||||
})
|
||||
|
||||
function removeAvatar (): void {
|
||||
avatar = undefined
|
||||
}
|
||||
@ -161,6 +152,20 @@
|
||||
$: canSave = formatName(targetClass._id, firstName, lastName, object.name).length > 0
|
||||
|
||||
const manager = createFocusManager()
|
||||
|
||||
let matches: WithLookup<Contact>[] = []
|
||||
let matchedChannels: AttachedData<Channel>[] = []
|
||||
$: if (targetClass !== undefined) {
|
||||
findContacts(
|
||||
client,
|
||||
targetClass._id,
|
||||
{ ...object, name: formatName(targetClass._id, firstName, lastName, object.name) },
|
||||
channels
|
||||
).then((p) => {
|
||||
matches = p.contacts
|
||||
matchedChannels = p.channels
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -245,7 +250,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
<svelte:fragment slot="pool">
|
||||
<ChannelsDropdown bind:value={channels} focusIndex={10} editable />
|
||||
<ChannelsDropdown
|
||||
bind:value={channels}
|
||||
focusIndex={10}
|
||||
editable
|
||||
highlighted={matchedChannels.map((it) => it.provider)}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="footer">
|
||||
{#if matches.length > 0}
|
||||
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment from '@anticrm/attachment'
|
||||
import contact, { Channel, ChannelProvider, combineName, findPerson, Person } from '@anticrm/contact'
|
||||
import contact, { Channel, ChannelProvider, combineName, findContacts, Person } from '@anticrm/contact'
|
||||
import { ChannelsDropdown } from '@anticrm/contact-resources'
|
||||
import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte'
|
||||
import { Account, AttachedData, Data, Doc, generateId, MixinData, Ref, TxProcessor, WithLookup } from '@anticrm/core'
|
||||
@ -79,7 +79,9 @@
|
||||
|
||||
let avatar: File | undefined
|
||||
let channels: AttachedData<Channel>[] = []
|
||||
let matchedChannels: Channel[] = []
|
||||
|
||||
let matches: WithLookup<Person>[] = []
|
||||
let matchedChannels: AttachedData<Channel>[] = []
|
||||
|
||||
let skills: TagReference[] = []
|
||||
const key: KeyedAttribute = {
|
||||
@ -379,31 +381,16 @@
|
||||
]
|
||||
}
|
||||
|
||||
let matches: WithLookup<Person>[] = []
|
||||
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => {
|
||||
matches = p
|
||||
$: findContacts(
|
||||
client,
|
||||
contact.class.Person,
|
||||
{ ...object, name: combineName(firstName.trim(), lastName.trim()) },
|
||||
channels
|
||||
).then((p) => {
|
||||
matches = p.contacts
|
||||
matchedChannels = p.channels
|
||||
})
|
||||
|
||||
$: if (matches.length > 0) {
|
||||
const res: Channel[] = []
|
||||
for (const ci in channels) {
|
||||
let matched = false
|
||||
for (const m of matches) {
|
||||
for (const c of (m.$lookup?.channels as Channel[]) ?? []) {
|
||||
if (c.provider === channels[ci].provider && c.value === channels[ci].value) {
|
||||
res.push(c)
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
matchedChannels = res
|
||||
}
|
||||
|
||||
function removeAvatar (): void {
|
||||
avatar = undefined
|
||||
}
|
||||
@ -416,7 +403,7 @@
|
||||
<Card
|
||||
label={recruit.string.CreateTalent}
|
||||
okAction={createCandidate}
|
||||
canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0}
|
||||
canSave={firstName.length > 0 && lastName.length > 0}
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
|
56
tests/sanity/tests/contact.duplicate.spec.ts
Normal file
56
tests/sanity/tests/contact.duplicate.spec.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { test } from '@playwright/test'
|
||||
import { generateId, PlatformSetting, PlatformURI } from './utils'
|
||||
|
||||
test.use({
|
||||
storageState: PlatformSetting
|
||||
})
|
||||
|
||||
test.describe('duplicate-org-test', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Create user and workspace
|
||||
await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp`)
|
||||
})
|
||||
test('test', async ({ page }) => {
|
||||
await page.click('[id="app-lead\\:string\\:LeadApplication"]')
|
||||
|
||||
// Click text=Customers
|
||||
await page.click('text=Customers')
|
||||
|
||||
// Click button:has-text("New Customer")
|
||||
await page.click('button:has-text("New Customer")')
|
||||
|
||||
// Click button:has-text("Person")
|
||||
await page.click('button:has-text("Person")')
|
||||
|
||||
// Click button:has-text("Organization")
|
||||
await page.click('button:has-text("Organization")')
|
||||
|
||||
// Click [placeholder="Apple"]
|
||||
await page.click('[placeholder="Apple"]')
|
||||
|
||||
const genId = 'Asoft-' + generateId(4)
|
||||
// Fill [placeholder="Apple"]
|
||||
await page.fill('[placeholder="Apple"]', genId)
|
||||
|
||||
// Click button:has-text("Create")
|
||||
await page.click('button:has-text("Create")')
|
||||
|
||||
// Click button:has-text("New Customer")
|
||||
await page.click('button:has-text("New Customer")')
|
||||
|
||||
// Click button:has-text("Person")
|
||||
await page.click('button:has-text("Person")')
|
||||
|
||||
// Click button:has-text("Organization")
|
||||
await page.click('button:has-text("Organization")')
|
||||
|
||||
// Click [placeholder="Apple"]
|
||||
await page.click('[placeholder="Apple"]')
|
||||
|
||||
// Fill [placeholder="Apple"]
|
||||
await page.fill('[placeholder="Apple"]', genId)
|
||||
|
||||
// Click text=Person already exists...
|
||||
await page.click('text=Contact already exists...')
|
||||
})
|
||||
})
|
@ -28,7 +28,7 @@ test.describe('project tests', () => {
|
||||
await page.click(`text=${prjId}`)
|
||||
await page.click('button:has-text("New issue")')
|
||||
await page.fill('[placeholder="Issue\\ title"]', 'issue')
|
||||
await page.click('button:has-text("Project")')
|
||||
await page.click('form button:has-text("Project")')
|
||||
await page.click(`button:has-text("${prjId}")`)
|
||||
await page.click('button:has-text("Save issue")')
|
||||
await page.click(`button:has-text("${prjId}")`)
|
||||
|
Loading…
Reference in New Issue
Block a user