mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-23 08:48:01 +00: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
|
// 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.
|
||||||
|
@ -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",
|
||||||
|
@ -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>
|
||||||
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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')
|
||||||
}}
|
}}
|
||||||
|
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(`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}")`)
|
||||||
|
Loading…
Reference in New Issue
Block a user