Fix TSK-152 (#2110)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-06-20 14:57:45 +07:00 committed by GitHub
parent ce3ef44592
commit c38bf48d75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 175 additions and 76 deletions

View File

@ -277,7 +277,7 @@ export class LiveQuery extends TxProcessor implements Client {
// Mixin potentially added to object we doesn't have in out results // Mixin potentially added to object we doesn't have in out results
const doc = await this.findOne(q._class, { _id: tx.objectId }, q.options) const doc = await this.findOne(q._class, { _id: tx.objectId }, q.options)
if (doc !== undefined) { 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 {} 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 (this.match(q, doc)) {
if (q.result instanceof Promise) { if (q.result instanceof Promise) {
q.result = await q.result 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) 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. // We could already have document inside results, if query is created during processing of document create transaction and not yet handled on client.

View File

@ -47,7 +47,7 @@
"Homepage": "Home page", "Homepage": "Home page",
"SocialLinks": "Socail links", "SocialLinks": "Socail links",
"ViewActivity": "View activity", "ViewActivity": "View activity",
"PersonAlreadyExists": "Person already exists...", "PersonAlreadyExists": "Contact already exists...",
"Status": "Status", "Status": "Status",
"SetStatus": "Set status", "SetStatus": "Set status",
"ClearStatus": "Clear status", "ClearStatus": "Clear status",

View File

@ -13,14 +13,15 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Channel, Organization } from '@anticrm/contact' import { Channel, findContacts, Organization } from '@anticrm/contact'
import { AttachedData, generateId } from '@anticrm/core' import { AttachedData, generateId, WithLookup } from '@anticrm/core'
import { Card, getClient } from '@anticrm/presentation' 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 { createEventDispatcher } from 'svelte'
import contact from '../plugin' import contact from '../plugin'
import ChannelsDropdown from './ChannelsDropdown.svelte' import ChannelsDropdown from './ChannelsDropdown.svelte'
import Company from './icons/Company.svelte' import Company from './icons/Company.svelte'
import OrganizationPresenter from './OrganizationPresenter.svelte'
export function canClose (): boolean { export function canClose (): boolean {
return object.name === '' return object.name === ''
@ -57,6 +58,13 @@
let channels: AttachedData<Channel>[] = [] let channels: AttachedData<Channel>[] = []
const manager = createFocusManager() 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> </script>
<FocusHandler {manager} /> <FocusHandler {manager} />
@ -83,6 +91,22 @@
/> />
</div> </div>
<svelte:fragment slot="pool"> <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> </svelte:fragment>
</Card> </Card>

View File

@ -226,13 +226,14 @@ export default contactPlugin
/** /**
* @public * @public
*/ */
export async function findPerson ( export async function findContacts (
client: Client, client: Client,
person: Data<Person>, _class: Ref<Class<Doc>>,
person: Data<Contact>,
channels: AttachedData<Channel>[] channels: AttachedData<Channel>[]
): Promise<Person[]> { ): Promise<{ contacts: Contact[], channels: AttachedData<Channel>[] }> {
if (channels.length === 0 || person.name.length === 0) { if (channels.length === 0 && person.name.length === 0) {
return [] return { contacts: [], channels: [] }
} }
// Take only first part of first name for match. // Take only first part of first name for match.
const values = channels.map((it) => it.value) const values = channels.map((it) => it.value)
@ -240,23 +241,33 @@ export async function findPerson (
// Same name persons // Same name persons
const potentialChannels = await client.findAll(contactPlugin.class.Channel, { value: { $in: values } }) 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) { if (potentialContactIds.length === 0) {
const firstName = getFirstName(person.name).split(' ').shift() ?? '' if (client.getHierarchy().isDerived(_class, contactPlugin.class.Person)) {
const lastName = getLastName(person.name) const firstName = getFirstName(person.name).split(' ').shift() ?? ''
// try match using just first/last name const lastName = getLastName(person.name)
potentialPersonIds = ( // try match using just first/last name
await client.findAll(contactPlugin.class.Person, { name: { $like: `${lastName}%${firstName}%` } }) potentialContactIds = (
).map((it) => it._id) await client.findAll(contactPlugin.class.Contact, { name: { $like: `${lastName}%${firstName}%` } })
if (potentialPersonIds.length === 0) { ).map((it) => it._id)
return [] 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( const potentialPersons: FindResult<Contact> = await client.findAll(
contactPlugin.class.Person, contactPlugin.class.Contact,
{ _id: { $in: potentialPersonIds } }, { _id: { $in: potentialContactIds } },
{ {
lookup: { lookup: {
_id: { _id: {
@ -266,29 +277,40 @@ export async function findPerson (
} }
) )
const result: Person[] = [] const result: Contact[] = []
const resChannels: AttachedData<Channel>[] = []
for (const c of potentialPersons) { for (const c of potentialPersons) {
let matches = 0 let matches = 0
if (c.name === person.name) { if (c.name === person.name) {
matches++ matches++
} }
if (c.city === person.city) {
matches++
}
for (const ch of (c.$lookup?.channels as Channel[]) ?? []) { for (const ch of (c.$lookup?.channels as Channel[]) ?? []) {
for (const chc of channels) { for (const chc of channels) {
if (chc.provider === ch.provider && chc.value === ch.value.trim()) { if (chc.provider === ch.provider && chc.value === ch.value.trim()) {
// We have matched value // We have matched value
resChannels.push(chc)
matches += 2 matches += 2
break break
} }
} }
} }
if (matches >= 2) { if (matches > 0) {
result.push(c) 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[]
} }

View File

@ -14,24 +14,24 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment from '@anticrm/attachment' 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 { ChannelsDropdown } from '@anticrm/contact-resources'
import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte' import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte'
import contact from '@anticrm/contact-resources/src/plugin' 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 type { Customer } from '@anticrm/lead'
import { getResource } from '@anticrm/platform' import { getResource } from '@anticrm/platform'
import { Card, EditableAvatar, getClient } from '@anticrm/presentation' import { Card, EditableAvatar, getClient } from '@anticrm/presentation'
import { import {
Button, Button,
createFocusManager,
EditBox, EditBox,
eventToHTMLElement, eventToHTMLElement,
FocusHandler,
IconInfo, IconInfo,
Label, Label,
SelectPopup, SelectPopup,
showPopup, showPopup
createFocusManager,
FocusHandler
} from '@anticrm/ui' } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import lead from '../plugin' import lead from '../plugin'
@ -56,7 +56,7 @@
let avatar: File | undefined let avatar: File | undefined
function formatName (targetClass: Ref<Class<Doc>>, firstName: string, lastName: string, objectName: string): string { 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 () { async function createCustomer () {
@ -114,15 +114,6 @@
avatar = file avatar = file
} }
let matches: Contact[] = []
$: findPerson(
client,
{ ...object, name: formatName(targetClass._id, firstName, lastName, object.name) },
channels
).then((p) => {
matches = p
})
function removeAvatar (): void { function removeAvatar (): void {
avatar = undefined avatar = undefined
} }
@ -161,6 +152,20 @@
$: canSave = formatName(targetClass._id, firstName, lastName, object.name).length > 0 $: canSave = formatName(targetClass._id, firstName, lastName, object.name).length > 0
const manager = createFocusManager() 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> </script>
<FocusHandler {manager} /> <FocusHandler {manager} />
@ -245,7 +250,12 @@
</div> </div>
{/if} {/if}
<svelte:fragment slot="pool"> <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>
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
{#if matches.length > 0} {#if matches.length > 0}

View File

@ -14,7 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment from '@anticrm/attachment' 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 { ChannelsDropdown } from '@anticrm/contact-resources'
import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte' import PersonPresenter from '@anticrm/contact-resources/src/components/PersonPresenter.svelte'
import { Account, AttachedData, Data, Doc, generateId, MixinData, Ref, TxProcessor, WithLookup } from '@anticrm/core' import { Account, AttachedData, Data, Doc, generateId, MixinData, Ref, TxProcessor, WithLookup } from '@anticrm/core'
@ -79,7 +79,9 @@
let avatar: File | undefined let avatar: File | undefined
let channels: AttachedData<Channel>[] = [] let channels: AttachedData<Channel>[] = []
let matchedChannels: Channel[] = []
let matches: WithLookup<Person>[] = []
let matchedChannels: AttachedData<Channel>[] = []
let skills: TagReference[] = [] let skills: TagReference[] = []
const key: KeyedAttribute = { const key: KeyedAttribute = {
@ -379,31 +381,16 @@
] ]
} }
let matches: WithLookup<Person>[] = [] $: findContacts(
$: findPerson(client, { ...object, name: combineName(firstName, lastName) }, channels).then((p) => { client,
matches = p 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 { function removeAvatar (): void {
avatar = undefined avatar = undefined
} }
@ -416,7 +403,7 @@
<Card <Card
label={recruit.string.CreateTalent} label={recruit.string.CreateTalent}
okAction={createCandidate} okAction={createCandidate}
canSave={firstName.length > 0 && lastName.length > 0 && matches.length === 0} canSave={firstName.length > 0 && lastName.length > 0}
on:close={() => { on:close={() => {
dispatch('close') dispatch('close')
}} }}

View 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...')
})
})

View File

@ -28,7 +28,7 @@ test.describe('project tests', () => {
await page.click(`text=${prjId}`) await page.click(`text=${prjId}`)
await page.click('button:has-text("New issue")') await page.click('button:has-text("New issue")')
await page.fill('[placeholder="Issue\\ title"]', '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("${prjId}")`)
await page.click('button:has-text("Save issue")') await page.click('button:has-text("Save issue")')
await page.click(`button:has-text("${prjId}")`) await page.click(`button:has-text("${prjId}")`)