[UBER-71] Use "New issue" dialog for creating sub-issues ()

Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@icloud.com>
This commit is contained in:
Sergei Ogorelkov 2023-05-17 15:32:48 +04:00 committed by GitHub
parent a5a464a112
commit 673e7b88c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 68 additions and 464 deletions
plugins/tracker-resources/src/components
tests/sanity/tests

View File

@ -404,7 +404,6 @@
draftController.remove()
resetObject()
descriptionBox?.removeDraft(false)
subIssuesComponent.removeChildDraft()
}
async function showMoreActions (ev: Event) {
@ -518,7 +517,6 @@
if (result === true) {
dispatch('close')
resetObject()
subIssuesComponent.removeChildDraft()
draftController.remove()
descriptionBox?.removeDraft(true)
}
@ -624,11 +622,9 @@
<SubIssues
bind:this={subIssuesComponent}
projectId={_space}
parendIssueId={object._id}
project={currentProject}
milestone={object.milestone}
component={object.component}
{shouldSaveDraft}
bind:subIssues={object.subIssues}
/>
<svelte:fragment slot="pool">

View File

@ -19,27 +19,21 @@
import { DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import { Component, Issue, IssueDraft, IssueParentInfo, Project, Milestone, calcRank } from '@hcengineering/tracker'
import { Button, ExpandCollapse, IconAdd, Scroller, closeTooltip } from '@hcengineering/ui'
import { Button, ExpandCollapse, Scroller } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../plugin'
import Collapsed from './icons/Collapsed.svelte'
import Expanded from './icons/Expanded.svelte'
import DraftIssueChildEditor from './templates/DraftIssueChildEditor.svelte'
import DraftIssueChildList from './templates/DraftIssueChildList.svelte'
export let parendIssueId: Ref<Issue>
export let projectId: Ref<Project>
export let project: Project | undefined
export let milestone: Ref<Milestone> | null = null
export let component: Ref<Component> | null = null
export let subIssues: IssueDraft[] = []
export let shouldSaveDraft: boolean = false
let lastProject = project
let lastProject = project
let isCollapsed = false
$: isCreatingMode = $draftsStore[`${parendIssueId}_subIssue`] !== undefined
let isManualCreating = false
$: isCreating = isCreatingMode || isManualCreating
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
if (subIssues) {
@ -65,6 +59,7 @@
const client = getClient()
// TODO: move to utils
export async function save (parents: IssueParentInfo[], _id: Ref<Doc>) {
if (project === undefined) return
saved = true
@ -162,6 +157,7 @@
}
})
// TODO: move to utils
export async function removeDraft (_id: string, removeFiles: boolean = false): Promise<void> {
const draftAttachments = $draftsStore[`${_id}_attachments`]
DraftController.remove(`${_id}_attachments`)
@ -172,18 +168,11 @@
}
}
}
export function removeChildDraft () {
draftChild?.removeDraft()
}
$: hasSubIssues = subIssues.length > 0
let draftChild: DraftIssueChildEditor
</script>
<div class="flex-between clear-mins">
{#if hasSubIssues}
<!-- TODO: check if sub issues list is empty in a parent component -->
{#if subIssues.length > 0}
<div class="flex-between clear-mins">
<Button
width="min-content"
icon={isCollapsed ? Collapsed : Expanded}
@ -191,30 +180,10 @@
kind="transparent"
label={tracker.string.SubIssuesList}
labelParams={{ subIssues: subIssues.length }}
on:click={() => {
isCollapsed = !isCollapsed
isCreating = false
}}
on:click={() => (isCollapsed = !isCollapsed)}
/>
{/if}
</div>
<Button
id="add-sub-issue"
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 } }}
on:click={() => {
closeTooltip()
isManualCreating = true
isCollapsed = false
}}
/>
</div>
{#if hasSubIssues}
<ExpandCollapse isExpanded={!isCollapsed} on:changeContent>
<div class="flex-col flex-no-shrink max-h-30 list clear-mins" class:collapsed={isCollapsed}>
<Scroller>
@ -230,28 +199,6 @@
</div>
</ExpandCollapse>
{/if}
{#if isCreating && project}
<ExpandCollapse isExpanded={!isCollapsed} on:changeContent>
<DraftIssueChildEditor
bind:this={draftChild}
{parendIssueId}
{project}
{component}
{milestone}
{shouldSaveDraft}
on:close={() => {
isManualCreating = false
}}
on:create={(evt) => {
if (subIssues === undefined) {
subIssues = []
}
subIssues = [...subIssues, evt.detail]
}}
on:changeContent
/>
</ExpandCollapse>
{/if}
<style lang="scss">
.list {

View File

@ -1,315 +0,0 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import core, { Account, AttachedData, Doc, generateId, Ref, SortingOrder } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import presentation, { DraftController, getClient, KeyedAttribute } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { calcRank, Issue, IssueDraft, IssuePriority, Project } from '@hcengineering/tracker'
import { addNotification, Button, ButtonSize, Component, deviceOptionsStore, EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import AssigneeEditor from '../AssigneeEditor.svelte'
import IssueNotification from '../IssueNotification.svelte'
import PriorityEditor from '../PriorityEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte'
import { onDestroy } from 'svelte'
export let parentIssue: Issue
export let currentProject: Project
export let shouldSaveDraft: boolean = false
const draftController = new DraftController<IssueDraft>(`${parentIssue._id}_subIssue`)
const draft = shouldSaveDraft ? draftController.get() : undefined
const dispatch = createEventDispatcher()
const client = getClient()
onDestroy(() => draftController.destroy())
let object = draft ?? getIssueDefaults()
let thisRef: HTMLDivElement
let focusIssueTitle: () => void
let descriptionBox: AttachmentStyledBox
const key: KeyedAttribute = {
key: 'labels',
attr: client.getHierarchy().getAttribute(tracker.class.Issue, 'labels')
}
function getIssueDefaults (): IssueDraft {
return {
_id: generateId(),
space: currentProject._id,
labels: [],
subIssues: [],
status: currentProject.defaultIssueStatus,
assignee: currentProject.defaultAssignee ?? null,
title: '',
description: '',
component: parentIssue.component,
priority: IssuePriority.NoPriority,
dueDate: null,
milestone: parentIssue.milestone,
estimation: 0
}
}
const empty = {
space: currentProject._id,
status: currentProject.defaultIssueStatus,
assignee: currentProject.defaultAssignee ?? null,
component: parentIssue.component,
priority: IssuePriority.NoPriority,
milestone: parentIssue.milestone
}
function objectChange (object: IssueDraft, empty: any) {
if (shouldSaveDraft) {
draftController.save(object, empty)
}
}
$: objectChange(object, empty)
function resetToDefaults () {
object = getIssueDefaults()
focusIssueTitle?.()
}
$: objectId = object._id
function getTitle (value: string) {
return value.trim()
}
function close () {
draftController.remove()
dispatch('close')
}
async function createIssue () {
if (!canSave) {
return
}
const _id: Ref<Issue> = generateId()
loading = true
try {
const space = currentProject._id
const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.updateDoc(
tracker.class.Project,
core.space.Space,
space,
{ $inc: { sequence: 1 } },
true
)
const value: AttachedData<Issue> = {
...object,
comments: 0,
subIssues: 0,
createOn: Date.now(),
reportedTime: 0,
reports: 0,
childInfo: [],
labels: 0,
status: object.status ?? currentProject.defaultIssueStatus,
title: getTitle(object.title),
number: (incResult as any).object.sequence,
rank: calcRank(lastOne, undefined),
parents: [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
}
await client.addCollection(
tracker.class.Issue,
space,
parentIssue._id,
parentIssue._class,
'subIssues',
value,
_id
)
await descriptionBox.createAttachments(_id)
for (const label of object.labels) {
await client.addCollection(label._class, label.space, _id, tracker.class.Issue, 'labels', {
title: label.title,
color: label.color,
tag: label.tag
})
}
addNotification(await translate(tracker.string.IssueCreated, {}), getTitle(object.title), IssueNotification, {
issueId: _id,
subTitlePostfix: (await translate(tracker.string.Created, { value: 1 })).toLowerCase(),
issueUrl: currentProject && generateIssueShortLink(getIssueId(currentProject, value as Issue))
})
draftController.remove()
} finally {
resetToDefaults()
loading = false
}
}
function addTagRef (tag: TagElement): void {
object.labels = [
...object.labels,
{
_class: tags.class.TagReference,
_id: generateId() as Ref<TagReference>,
attachedTo: '' as Ref<Doc>,
attachedToClass: tracker.class.Issue,
collection: 'labels',
space: tags.space.Tags,
modifiedOn: 0,
modifiedBy: '' as Ref<Account>,
title: tag.title,
tag: tag._id,
color: tag.color
}
]
}
let loading = false
$: thisRef && thisRef.scrollIntoView({ behavior: 'smooth' })
$: canSave = getTitle(object.title ?? '').length > 0
$: if (!object.status && currentProject?.defaultIssueStatus) {
object.status = currentProject.defaultIssueStatus
}
let buttonSize: ButtonSize
$: buttonSize = $deviceOptionsStore.twoRows ? 'small' : 'large'
</script>
<div id="sub-issue-child-editor" bind:this={thisRef} class="flex-col subissue-container">
<div class="flex-row-top subissue-content">
<div id="status-editor" class="mr-1">
<StatusEditor
value={object}
kind="transparent"
size="medium"
justify="center"
tooltipAlignment="bottom"
on:change={({ detail }) => (object.status = detail)}
/>
</div>
<div class="w-full flex-col content">
<div id="sub-issue-name">
<EditBox
bind:value={object.title}
bind:focusInput={focusIssueTitle}
placeholder={tracker.string.IssueTitlePlaceholder}
focus
fullSize
/>
</div>
<div class="mt-4" id="sub-issue-description">
{#key objectId}
<AttachmentStyledBox
bind:this={descriptionBox}
objectId={object._id}
refContainer={thisRef}
_class={tracker.class.Issue}
space={currentProject._id}
{shouldSaveDraft}
alwaysEdit
showButtons
maxHeight={'20vh'}
bind:content={object.description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
on:changeSize={() => dispatch('changeContent')}
/>
{/key}
</div>
</div>
</div>
<div class="subissue-footer flex-between">
<div class="flex-row-center gap-around-2 flex-wrap">
<div id="sub-issue-priority">
<PriorityEditor
value={object}
shouldShowLabel
isEditable
kind={'secondary'}
size={buttonSize}
justify="center"
on:change={({ detail }) => (object.priority = detail)}
/>
</div>
<div id="sub-issue-assignee">
{#key object.assignee}
<AssigneeEditor
value={object}
kind={'secondary'}
size={buttonSize}
on:change={({ detail }) => (object.assignee = detail)}
/>
{/key}
</div>
<Component
is={tags.component.TagsDropdownEditor}
props={{
items: object.labels,
key,
targetClass: tracker.class.Issue,
countLabel: tracker.string.NumberLabels,
kind: 'secondary',
size: buttonSize
}}
on:open={(evt) => {
addTagRef(evt.detail)
}}
on:delete={(evt) => {
object.labels = object.labels.filter((it) => it._id !== evt.detail)
}}
/>
<EstimationEditor kind={'secondary'} size={buttonSize} value={object} />
</div>
<div class="flex-row-center gap-around-2 self-end flex-no-shrink">
<Button label={presentation.string.Cancel} kind={'secondary'} size={buttonSize} on:click={close} />
<Button
{loading}
disabled={!canSave}
label={presentation.string.Save}
kind={'primary'}
size={buttonSize}
on:click={createIssue}
/>
</div>
</div>
</div>
<style lang="scss">
.subissue-container {
background-color: var(--theme-button-enabled);
border: 1px solid var(--theme-button-border);
border-radius: 0.25rem;
overflow: hidden;
.subissue-content {
padding: 0.75rem;
.content {
padding-top: 0.3rem;
}
}
.subissue-footer {
padding: 0.25rem 0.5rem 0.5rem;
}
}
</style>

View File

@ -14,19 +14,19 @@
-->
<script lang="ts">
import { Ref, toIdMap } from '@hcengineering/core'
import { createQuery, draftsStore } from '@hcengineering/presentation'
import { createQuery } from '@hcengineering/presentation'
import { Issue, Project, trackerId } from '@hcengineering/tracker'
import {
Button,
Chevron,
ExpandCollapse,
IconAdd,
IconArrowRight,
IconScaleFull,
Label,
closeTooltip,
getCurrentResolvedLocation,
navigate
navigate,
showPopup
} from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import {
@ -37,7 +37,6 @@
viewOptionStore
} from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import CreateSubIssue from './CreateSubIssue.svelte'
import SubIssueList from './SubIssueList.svelte'
import { afterUpdate } from 'svelte'
@ -45,9 +44,7 @@
export let projects: Map<Ref<Project>, Project>
export let shouldSaveDraft: boolean = false
let subIssueEditorRef: HTMLDivElement
let isCollapsed = false
let isCreating = $draftsStore[`${issue._id}_subIssue`] !== undefined
$: hasSubIssues = issue.subIssues > 0
@ -62,6 +59,10 @@
const projectsQuery = createQuery()
function openNewIssueDialog (): void {
showPopup(tracker.component.CreateIssue, { space: issue.space, parentIssue: issue, shouldSaveDraft }, 'top')
}
$: if (projects === undefined) {
projectsQuery.query(tracker.class.Project, {}, async (result) => {
_projects = toIdMap(result)
@ -78,7 +79,6 @@
afterUpdate(() => {
if (lastIssueId !== issue._id) {
lastIssueId = issue._id
isCreating = $draftsStore[`${issue._id}_subIssue`] !== undefined
}
})
</script>
@ -91,7 +91,6 @@
kind="transparent"
on:click={() => {
isCollapsed = !isCollapsed
isCreating = false
}}
>
<svelte:fragment slot="content">
@ -130,17 +129,16 @@
<Button
id="add-sub-issue"
width="min-content"
icon={hasSubIssues ? (isCreating ? IconArrowRight : IconAdd) : undefined}
icon={hasSubIssues ? IconAdd : undefined}
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
labelParams={{ subIssues: 0 }}
kind={'transparent'}
size={'small'}
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
on:click={() => {
closeTooltip()
isCreating && subIssueEditorRef && subIssueEditorRef.scrollIntoView({ behavior: 'smooth' })
isCreating = true
isCollapsed = false
closeTooltip()
openNewIssueDialog()
}}
/>
</div>
@ -161,21 +159,6 @@
</ExpandCollapse>
{/if}
{/if}
<ExpandCollapse isExpanded={!isCollapsed}>
{#if isCreating}
{@const project = projects.get(issue.space)}
{#if project !== undefined}
<div class="pt-4" bind:this={subIssueEditorRef}>
<CreateSubIssue
parentIssue={issue}
{shouldSaveDraft}
currentProject={project}
on:close={() => (isCreating = false)}
/>
</div>
{/if}
{/if}
</ExpandCollapse>
</div>
<style lang="scss">

View File

@ -4,6 +4,7 @@ import {
DEFAULT_USER,
ViewletSelectors,
checkIssue,
checkIssueDraft,
createIssue,
createLabel,
createSubissue,
@ -206,7 +207,6 @@ test('create-issue-draft', async ({ page }) => {
await navigate(page)
const issueName = 'Draft issue'
const subIssueName = 'Sub issue draft'
// Click text=Issues >> nth=1
await page.locator('text=Issues').nth(2).click()
@ -251,53 +251,20 @@ test('create-issue-draft', async ({ page }) => {
await page.locator('button:has-text("Set due date…")').click()
// Click text=24 >> nth=0
await page.locator('.date-popup-container >> text=24').first().click()
// Click button:has-text("+ Add sub-issues")
await page.locator('button:has-text("+ Add sub-issues")').click()
// Click [placeholder="Sub-issue title"]
await page.locator('#sub-issue-name').click()
// Fill [placeholder="Sub-issue title"]
await page.locator('#sub-issue-name >> input').fill(subIssueName)
await page.locator('#sub-issue-description').click()
await page.locator('#sub-issue-description >> [contenteditable]').fill(subIssueName)
// Click button:has-text("Backlog")
await page.locator('#sub-issue-status-editor').click()
// Click button:has-text("In Progress")
await page.locator('button:has-text("In Progress")').click()
// Click button:has-text("No priority")
await page.locator('#sub-issue-priority-editor').click()
// Click button:has-text("High")
await page.locator('button:has-text("High")').click()
// Click button:has-text("Assignee")
await page.locator('#sub-issue-assignee-editor').click()
// Click button:has-text("Chen Rosamund")
await page.locator('button:has-text("Chen Rosamund")').click()
// Click button:has-text("0d")
await page.locator('#sub-issue-estimation-editor').click()
// Double click [placeholder="Type text\.\.\."]
await page.locator('[placeholder="Type text\\.\\.\\."]').dblclick()
// Fill [placeholder="Type text\.\.\."]
await page.locator('[placeholder="Type text\\.\\.\\."]').fill('2')
await page.locator('.ml-2 > .button').click()
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await page.locator('#new-issue').click()
await expect(page.locator('#issue-name')).toHaveText(issueName)
await expect(page.locator('#issue-description')).toHaveText(issueName)
await expect(page.locator('#status-editor')).toHaveText('Todo')
await expect(page.locator('#priority-editor')).toHaveText('Urgent')
await expect(page.locator('#assignee-editor')).toHaveText('Appleseed John')
await expect(page.locator('#estimation-editor')).toHaveText('1d')
await expect(page.locator('.antiCard >> .datetime-button')).toContainText('24')
await expect(page.locator('#sub-issue-name')).toHaveText(subIssueName)
await expect(page.locator('#sub-issue-description')).toHaveText(subIssueName)
await expect(page.locator('#sub-issue-status-editor')).toHaveText('In Progress')
await expect(page.locator('#sub-issue-priority-editor')).toHaveText('High')
await expect(page.locator('#sub-issue-assignee-editor')).toHaveText('Chen Rosamund')
await expect(page.locator('#sub-issue-estimation-editor')).toHaveText('2d')
await checkIssueDraft(page, {
name: issueName,
description: issueName,
status: 'Todo',
priority: 'Urgent',
assignee: 'Appleseed John',
estimation: '1d',
dueDate: '24'
})
})
test('sub-issue-draft', async ({ page }) => {
@ -310,7 +277,6 @@ test('sub-issue-draft', async ({ page }) => {
priority: 'Urgent',
assignee: DEFAULT_USER
}
const originalName = props.name
await navigate(page)
await createIssue(page, props)
await page.click('text="Issues"')
@ -321,13 +287,10 @@ test('sub-issue-draft', async ({ page }) => {
await checkIssue(page, props)
props.name = `sub${props.name}`
await page.click('button:has-text("Add sub-issue")')
await fillIssueForm(page, props, false)
await fillIssueForm(page, props)
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
await openIssue(page, originalName)
await expect(page.locator('#sub-issue-child-editor >> #sub-issue-name')).toHaveText(props.name)
await expect(page.locator('#sub-issue-child-editor >> #sub-issue-description')).toHaveText(props.description)
await expect(page.locator('#sub-issue-child-editor >> #sub-issue-priority')).toHaveText(props.priority)
await expect(page.locator('#sub-issue-child-editor >> #sub-issue-assignee')).toHaveText(props.assignee)
await page.locator('#new-issue').click()
await checkIssueDraft(page, props)
})

View File

@ -10,6 +10,8 @@ export interface IssueProps {
assignee?: string
component?: string
milestone?: string
estimation?: string
dueDate?: string
}
export enum ViewletSelectors {
@ -45,15 +47,15 @@ export async function setViewOrder (page: Page, orderName: string): Promise<void
await page.keyboard.press('Escape')
}
export async function fillIssueForm (page: Page, props: IssueProps, issue: boolean): Promise<void> {
export async function fillIssueForm (page: Page, props: IssueProps): Promise<void> {
const { name, description, status, assignee, labels, priority, component, milestone } = props
const af = issue ? 'form ' : '[id="sub-issue-child-editor"] '
const af = 'form '
const issueTitle = page.locator(af + '[placeholder="Issue\\ title"]')
await issueTitle.fill(name)
await issueTitle.evaluate((e) => e.blur())
if (description !== undefined) {
const pm = await page.locator(af + '.ProseMirror')
const pm = page.locator(af + '.ProseMirror')
await pm.fill(description)
await pm.evaluate((e) => e.blur())
}
@ -89,7 +91,7 @@ export async function fillIssueForm (page: Page, props: IssueProps, issue: boole
export async function createIssue (page: Page, props: IssueProps): Promise<void> {
await page.waitForSelector('span:has-text("Default")')
await page.click('button:has-text("New issue")')
await fillIssueForm(page, props, true)
await fillIssueForm(page, props)
await page.click('form button:has-text("Create issue")')
await page.waitForSelector('form.antiCard', { state: 'detached' })
}
@ -118,8 +120,8 @@ export async function createMilestone (page: Page, milestoneName: string): Promi
export async function createSubissue (page: Page, props: IssueProps): Promise<void> {
await page.click('button:has-text("Add sub-issue")')
await fillIssueForm(page, props, false)
await page.click('button:has-text("Save")')
await fillIssueForm(page, props)
await page.click('button:has-text("Create issue")')
}
export async function createLabel (page: Page, label: string): Promise<void> {
@ -164,6 +166,34 @@ export async function checkIssue (page: Page, props: IssueProps): Promise<void>
}
}
export async function checkIssueDraft (page: Page, props: IssueProps): Promise<void> {
await expect(page.locator('#issue-name')).toHaveText(props.name)
if (props.description !== undefined) {
await expect(page.locator('#issue-description')).toHaveText(props.description)
}
if (props.status !== undefined) {
await expect(page.locator('#status-editor')).toHaveText(props.status)
}
if (props.priority !== undefined) {
await expect(page.locator('#priority-editor')).toHaveText(props.priority)
}
if (props.assignee !== undefined) {
await expect(page.locator('#assignee-editor')).toHaveText(props.assignee)
}
if (props.estimation !== undefined) {
await expect(page.locator('#estimation-editor')).toHaveText(props.estimation)
}
if (props.dueDate !== undefined) {
await expect(page.locator('.antiCard >> .datetime-button')).toContainText(props.dueDate)
}
}
export async function checkIssueFromList (page: Page, issueName: string): Promise<void> {
await page.click(ViewletSelectors.Board)
await expect(page.locator(`.panel-container:has-text("${issueName}")`)).toContainText(issueName)