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
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.

View File

@ -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",

View File

@ -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>

View File

@ -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[]
}

View File

@ -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}

View File

@ -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')
}}

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('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}")`)