UBERF-8584: Add test runs (#7235)

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2024-11-29 01:36:41 +07:00
committed by GitHub
parent 61f7770a02
commit da85d6dd5f
60 changed files with 2379 additions and 554 deletions
@@ -1,198 +0,0 @@
<!--
// Copyright © 2024 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 contact, { Employee, Person, PersonAccount } from '@hcengineering/contact'
import { AssigneeBox, AssigneePopup, personAccountByIdStore } from '@hcengineering/contact-resources'
import { AssigneeCategory } from '@hcengineering/contact-resources/src/assignee'
import { Account, Doc, DocumentQuery, Ref, Space, generateId } from '@hcengineering/core'
import { RuleApplyResult, getClient, getDocRules } from '@hcengineering/presentation'
import { TestCase } from '@hcengineering/test-management'
import { ButtonKind, ButtonSize, IconSize, TooltipAlignment } from '@hcengineering/ui'
import { Analytics } from '@hcengineering/analytics'
import { createEventDispatcher } from 'svelte'
import { get } from 'svelte/store'
import testManagement from '../../plugin'
import { getPreviousAssignees } from '../../utils'
type AssigneeObject = (Doc | any) & Pick<TestCase, 'assignee'>
export let object: AssigneeObject | AssigneeObject[] | undefined = undefined
export let value: AssigneeObject | AssigneeObject[] | undefined = undefined
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let avatarSize: IconSize = 'card'
export let tooltipAlignment: TooltipAlignment | undefined = undefined
export let width: string = 'min-content'
export let focusIndex: number | undefined = undefined
export let short: boolean = false
export let shouldShowName = true
export let shrink: number = 0
export let isAction: boolean = false
export let readonly: boolean = false
export let showStatus = true
$: _object =
(typeof object !== 'string' ? object : undefined) ?? (typeof value !== 'string' ? value : undefined) ?? []
$: docs = Array.isArray(_object) ? _object : [_object]
$: cdocs = docs.filter((d) => '_class' in d) as Doc[]
const client = getClient()
const dispatch = createEventDispatcher()
let progress = false
const handleAssigneeChanged = async (newAssignee: Ref<Person> | undefined | null) => {
if (newAssignee === undefined || (!Array.isArray(_object) && _object?.assignee === newAssignee)) {
return
}
progress = true
const ops = client.apply()
if (Array.isArray(_object)) {
for (const p of _object) {
if ('_class' in p) {
// Analytics.handleEvent(TrackerEvents.IssueSetAssignee, { issue: p.identifier ?? p._id })
await ops.update(p, { assignee: newAssignee })
}
}
} else {
if ('_class' in _object) {
// Analytics.handleEvent(TrackerEvents.IssueSetAssignee, { issue: _object.identifier ?? _object._id })
await ops.update(_object, { assignee: newAssignee })
}
}
await ops.commit()
progress = false
dispatch('change', newAssignee)
if (isAction) dispatch('close')
}
let categories: AssigneeCategory[] = []
function getCategories (object: AssigneeObject | AssigneeObject[]): void {
categories = []
if (cdocs.length > 0) {
categories.push({
label: testManagement.string.PreviousAssigned,
func: async () => {
const r: Ref<Person>[] = []
for (const d of cdocs) {
r.push(...(await getPreviousAssignees(d._id)))
}
return r
}
})
}
categories.push({
label: testManagement.string.Members,
func: async () => {
const spaces = Array.from(docs.map((it) => it.space).filter((it) => it)) as Ref<Space>[]
if (spaces.length === 0) {
return []
}
const projects = await client.findAll(testManagement.class.TestProject, {
_id: !Array.isArray(object) ? object.space : { $in: Array.from(object.map((it) => it.space)) }
})
if (projects === undefined) {
return []
}
const store = get(personAccountByIdStore)
const allMembers = projects.reduce((arr, p) => arr.concat(p.members), [] as Ref<Account>[])
const accounts = allMembers
.map((p) => store.get(p as Ref<PersonAccount>))
.filter((p) => p !== undefined) as PersonAccount[]
return accounts.map((p) => p.person as Ref<Employee>)
}
})
}
$: getCategories(_object)
$: sel =
(!Array.isArray(_object)
? _object.assignee
: _object.reduce((v, it) => (v != null && v === it.assignee ? it.assignee : null), _object[0]?.assignee) ??
undefined) ?? undefined
let rulesQuery: RuleApplyResult<Employee> | undefined
let query: DocumentQuery<Employee>
$: if (cdocs.length > 0) {
rulesQuery = getDocRules<Employee>(cdocs, 'assignee')
if (rulesQuery !== undefined) {
query = { ...(rulesQuery?.fieldQuery ?? {}), active: true }
} else {
query = { _id: 'none' as Ref<Employee>, active: true }
rulesQuery = {
disableEdit: true,
disableUnset: true,
fieldQuery: {}
}
}
}
</script>
{#if _object}
{#if isAction}
<AssigneePopup
docQuery={query}
{categories}
icon={contact.icon.Person}
selected={sel}
allowDeselect={true}
titleDeselect={undefined}
loading={progress}
on:close={(evt) => {
const result = evt.detail
if (result === null) {
handleAssigneeChanged(null)
} else if (result !== undefined && result._id !== value) {
value = result._id
handleAssigneeChanged(result._id)
}
}}
/>
{:else}
<AssigneeBox
docQuery={query}
{focusIndex}
label={testManagement.string.Assignee}
placeholder={testManagement.string.Assignee}
value={sel}
{categories}
titleDeselect={testManagement.string.Unassigned}
{size}
{kind}
{avatarSize}
{width}
{short}
{shrink}
{readonly}
{shouldShowName}
{showStatus}
showNavigate={false}
justify={'left'}
showTooltip={{
label: testManagement.string.AssignTo,
personLabel: testManagement.string.AssignedTo,
placeholderLabel: testManagement.string.Unassigned,
direction: tooltipAlignment
}}
on:change={({ detail }) => handleAssigneeChanged(detail)}
/>
{/if}
{/if}
@@ -24,7 +24,6 @@
import { Button, createFocusManager, EditBox, FocusHandler, IconAttachment, getLocation } from '@hcengineering/ui'
import StatusEditor from './StatusEditor.svelte'
import AssigneeEditor from './AssigneeEditor.svelte'
import ProjectPresenter from '../project/ProjectPresenter.svelte'
import testManagement from '../../plugin'
@@ -164,12 +163,6 @@
/>
<svelte:fragment slot="pool">
<AssigneeEditor
object={{ ...object, space }}
kind={'regular'}
size={'large'}
on:change={({ detail }) => (object.assignee = detail)}
/>
<StatusEditor bind:value={object.status} {object} kind="regular" />
</svelte:fragment>
@@ -13,13 +13,16 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { type Class, type Ref } from '@hcengineering/core'
import { TestCase } from '@hcengineering/test-management'
import { Panel } from '@hcengineering/panel'
import { EditBox, Breadcrumb } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import { DocAttributeBar } from '@hcengineering/view-resources'
import testManagement from '../../plugin'
export let _id: Ref<TestCase>
@@ -102,5 +105,9 @@
boundary={content}
/>
</div>
<svelte:fragment slot="aside">
<DocAttributeBar {object} ignoreKeys={['name']} />
</svelte:fragment>
</Panel>
{/if}
@@ -0,0 +1,88 @@
<!--
// Copyright © 2024 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 { Doc, DocumentQuery, Ref, Space } from '@hcengineering/core'
import { ButtonWithDropdown, IconDropdown, SelectPopupValueType } from '@hcengineering/ui'
import { selectionStore } from '@hcengineering/view-resources'
import type { TestProject } from '@hcengineering/test-management'
import testManagement from '../../plugin'
import { showCreateTestRunPopup } from '../../utils'
export let query: DocumentQuery<Doc> = {}
export let space: Ref<Space>
const project: Ref<TestProject> = space as any
const commonDropdownItems = [
{
id: testManagement.string.RunAllTestCases,
label: testManagement.string.RunAllTestCases,
icon: testManagement.icon.TestRuns
},
{
id: testManagement.string.RunFilteredTestCases,
label: testManagement.string.RunFilteredTestCases,
icon: testManagement.icon.Filter
}
]
let dropdownItems: SelectPopupValueType[] = []
$: dropdownItems =
$selectionStore?.docs?.length > 0
? [
...commonDropdownItems,
{
id: testManagement.string.RunSelectedTestCases,
label: testManagement.string.RunSelectedTestCases,
icon: testManagement.icon.Check
}
]
: commonDropdownItems
async function handleDropdownItemSelected (res?: SelectPopupValueType['id']): Promise<void> {
switch (res) {
case testManagement.string.RunAllTestCases: {
await showCreateTestRunPopup({ space: project })
return
}
case testManagement.string.RunFilteredTestCases: {
await showCreateTestRunPopup({ query, space: project })
return
}
case testManagement.string.RunSelectedTestCases: {
await showCreateTestRunPopup({ space: project, testCases: $selectionStore?.docs as any })
}
}
}
const handleRunAllTestCases = async (): Promise<void> => {
await showCreateTestRunPopup({ space: project })
}
</script>
<ButtonWithDropdown
icon={testManagement.icon.TestRuns}
justify={'left'}
kind={'primary'}
label={testManagement.string.RunAllTestCases}
dropdownIcon={IconDropdown}
{dropdownItems}
on:click={handleRunAllTestCases}
on:dropdown-selected={(ev) => {
void handleDropdownItemSelected(ev.detail)
}}
/>
@@ -43,7 +43,13 @@
const dispatch = createEventDispatcher()
const client = getClient()
function handlePopupOpen (event: MouseEvent) {
$: itemsInfo = defaultTestCaseStatuses.map((status) => ({
id: status,
isSelected: value === status,
...testCaseStatusAssets[status]
}))
function handlePopupOpen (event: MouseEvent): void {
showPopup(
SelectPopup,
{ value: itemsInfo, placeholder: testManagement.string.SetStatus },
@@ -52,7 +58,7 @@
)
}
async function changeStatus (newStatus: TestCase['status'] | null | undefined) {
async function changeStatus (newStatus: TestCase['status'] | null | undefined): Promise<void> {
if (disabled || newStatus == null || value === newStatus) {
return
}
@@ -65,12 +71,6 @@
}
}
$: itemsInfo = defaultTestCaseStatuses.map((status) => ({
id: status,
isSelected: value === status,
...testCaseStatusAssets[status]
}))
$: icon = value === undefined ? testManagement.icon.StatusDraft : testCaseStatusAssets[value].icon
$: label = value === undefined ? testManagement.string.StatusDraft : testCaseStatusAssets[value].label
</script>
@@ -0,0 +1,58 @@
<!--
// Copyright © 2024 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 { createEventDispatcher, onMount } from 'svelte'
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import { createQuery, getClient } from '@hcengineering/presentation'
import { type Class, type Ref } from '@hcengineering/core'
import { TestCase } from '@hcengineering/test-management'
import testManagement from '../../plugin'
export let object: TestCase | undefined = undefined
export let _id: Ref<TestCase>
export let _class: Ref<Class<TestCase>>
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const query = createQuery()
$: _id !== undefined &&
_class !== undefined &&
(object === undefined || object._id !== _id) &&
query.query(_class, { _id }, async (result) => {
;[object] = result
})
$: descriptionKey = hierarchy.getAttribute(testManagement.class.TestCase, 'description')
onMount(() => dispatch('open', { ignoreKeys: [] }))
</script>
{#if object}
<div class="w-full h-full">
<AttachmentStyleBoxCollabEditor
focusIndex={30}
{object}
key={{ key: 'description', attr: descriptionKey }}
identifier={object?._id}
placeholder={testManagement.string.DescriptionPlaceholder}
readonly
/>
</div>
{/if}
@@ -0,0 +1,47 @@
<!--
// Copyright © 2024 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 type { Class, Ref } from '@hcengineering/core'
import { DocPopup } from '@hcengineering/presentation'
import { TestCase } from '@hcengineering/test-management'
import TestCasePresenter from './TestCasePresenter.svelte'
export let _class: Ref<Class<TestCase>>
export let objects: TestCase[] = []
export let allowDeselect = false
export let closeAfterSelect: boolean = false
export let shadows = true
export let readonly = false
export let width: 'medium' | 'large' | 'full' = 'medium'
</script>
<DocPopup
{_class}
{objects}
multiSelect={true}
{allowDeselect}
{closeAfterSelect}
{shadows}
{width}
{readonly}
on:update
on:close
groupBy={'attachedTo'}
>
<svelte:fragment slot="item" let:item={testCase}>
<TestCasePresenter value={testCase} disabled={readonly} />
</svelte:fragment>
</DocPopup>
@@ -26,13 +26,14 @@
export let disabled: boolean = false
export let accent: boolean = false
export let noUnderline: boolean = false
export let onClick: ((event: MouseEvent) => void) | undefined = undefined
</script>
{#if value}
{#if inline}
<ObjectMention object={value} {disabled} {accent} {noUnderline} />
{:else}
<DocNavLink object={value} {disabled} {accent} {noUnderline}>
<DocNavLink object={value} {disabled} {accent} {noUnderline} {onClick}>
<div class="flex-presenter" use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
{value.name}
@@ -0,0 +1,67 @@
<!--
// Copyright © 2024 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 { IntlString } from '@hcengineering/platform'
import { Button, ButtonKind, ButtonShape, ButtonSize, Label, TooltipAlignment, showPopup } from '@hcengineering/ui'
import { TestCase } from '@hcengineering/test-management'
import TestCasePopup from './TestCasePopup.svelte'
import testManagement from '../../plugin'
export let objects: TestCase[]
export let selectedObjects: TestCase[]
export let label: IntlString = testManagement.string.TestCases
export let focusIndex = -1
export let focus = false
export let labelDirection: TooltipAlignment | undefined = undefined
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'center'
export let shape: ButtonShape = undefined
export let width: string | undefined = undefined
export let readonly = false
const showSpacesPopup = (ev: MouseEvent): void => {
showPopup(
TestCasePopup,
{
objects,
readonly
},
ev.target as HTMLElement,
() => {}
)
}
</script>
<Button
id="testcase.selector"
{focus}
{shape}
{focusIndex}
icon={testManagement.icon.TestCase}
{size}
{kind}
{justify}
{width}
showTooltip={{ label, direction: labelDirection }}
on:click={showSpacesPopup}
>
<span slot="content" class="overflow-label disabled text">
{selectedObjects?.length ?? 0}
<Label {label} />
</span>
</Button>
@@ -0,0 +1,105 @@
<!--
// Copyright © 2024 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 { createEventDispatcher, onMount } from 'svelte'
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { type Class, type Ref, Doc, Mixin, WithLookup } from '@hcengineering/core'
import { TestCase, TestResult } from '@hcengineering/test-management'
import { Panel } from '@hcengineering/panel'
import { Label, Scroller } from '@hcengineering/ui'
import { DocAttributeBar, getDocMixins } from '@hcengineering/view-resources'
import RightHeader from './RightHeader.svelte'
import TestCaseDetails from '../test-case/TestCaseDetails.svelte'
import testManagement from '../../plugin'
export let _id: Ref<TestResult>
export let _class: Ref<Class<TestResult>>
let object: WithLookup<TestResult> | undefined
const testCase = object?.$lookup?.testCase as TestCase | undefined
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
let mixins: Mixin<Doc>[] = []
$: mixins = object ? getDocMixins(object, false) : []
let descriptionBox: AttachmentStyleBoxCollabEditor
const query = createQuery()
$: _id !== undefined &&
_class !== undefined &&
query.query(
_class,
{ _id },
async (result) => {
;[object] = result
},
{
lookup: {
testCase: testManagement.class.TestCase
}
}
)
let content: HTMLElement
$: descriptionKey = hierarchy.getAttribute(testManagement.class.TestResult, 'description')
onMount(() => dispatch('open', { ignoreKeys: [] }))
</script>
{#if object}
<ActionContext context={{ mode: 'editor' }} />
<Panel
{object}
title={testCase?.name ?? object?.name}
isHeader={false}
isAside={true}
isSub={false}
adaptive={'default'}
on:open
on:close={() => dispatch('close')}
>
<div class="space-divider" />
<div class="w-full mt-6">
<AttachmentStyleBoxCollabEditor
focusIndex={30}
{object}
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
identifier={object?._id}
placeholder={testManagement.string.DescriptionPlaceholder}
boundary={content}
/>
</div>
<svelte:fragment slot="aside">
<DocAttributeBar {object} {mixins} ignoreKeys={['name']} />
<RightHeader>
<Label label={testManagement.string.TestCaseDescription} />
</RightHeader>
<Scroller padding={'0.5rem 2rem'}>
<TestCaseDetails _id={object.testCase} object={testCase} _class={testManagement.class.TestCase} />
</Scroller>
</svelte:fragment>
</Panel>
{/if}
@@ -0,0 +1,24 @@
<!--
// Copyright © 2024 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.
-->
<div class="header flex-between min-h-8 pl-4 pr-4 font-medium text-md bottom-divider top-divider">
<slot />
</div>
<style lang="scss">
.header {
font-size: 0.875rem;
}
</style>
@@ -0,0 +1,31 @@
<!--
// Copyright © 2024 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 { Button } from '@hcengineering/ui'
import testManagement from '../../plugin'
</script>
<div class="flex-grow flex-shrink">
<Button
label={testManagement.string.Save}
kind={'primary'}
on:click={() => {
// TODO: Add next test result logic
}}
showTooltip={{ label: testManagement.string.Save }}
/>
</div>
@@ -0,0 +1,25 @@
<!--
// Copyright © 2024 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 { TestResult } from '@hcengineering/test-management'
import TestResultPresenter from './TestResultPresenter.svelte'
export let object: TestResult
</script>
<div class="antiHSpacer x2" />
<div class="fs-title flex-row-center">
<TestResultPresenter value={object} shouldShowAvatar={false} disabled noUnderline />
</div>
@@ -0,0 +1,55 @@
<!--
//
// Copyright © 2024 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 { TestCase, TestResult } from '@hcengineering/test-management'
import { WithLookup } from '@hcengineering/core'
import { Icon, tooltip } from '@hcengineering/ui'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
import testManagement from '../../plugin'
export let value: WithLookup<TestResult> | undefined
export let inline: boolean = false
export let disabled: boolean = false
export let accent: boolean = false
export let noUnderline: boolean = false
export let shouldShowAvatar = true
let testCase: TestCase | undefined = undefined
$: testCase = value?.$lookup?.testCase as TestCase | undefined
$: title = testCase?.name ?? value?.name
</script>
{#if value}
{#if inline}
<ObjectMention object={value} {disabled} {accent} {noUnderline} />
{:else}
<DocNavLink object={value} {disabled} {accent} {noUnderline}>
<div class="flex-presenter" use:tooltip={{ label: testManagement.string.TestResult }}>
{#if shouldShowAvatar}
<div class="icon">
<Icon icon={testManagement.icon.TestResult} size="small" />
</div>
{/if}
<span {title} class="overflow-label label" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
{title}
</span>
</div>
</DocNavLink>
{/if}
{/if}
@@ -0,0 +1,106 @@
<!--
// Copyright © 2024 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 { createEventDispatcher } from 'svelte'
import { Data } from '@hcengineering/core'
import { TestResult } from '@hcengineering/test-management'
import { getClient } from '@hcengineering/presentation'
import {
Button,
ButtonKind,
ButtonSize,
Icon,
SelectPopup,
eventToHTMLElement,
showPopup,
Label
} from '@hcengineering/ui'
import { defaultTestRunStatuses, testRunStatusAssets } from '../../types'
import testManagement from '../../plugin'
export let value: TestResult['status'] | undefined
export let object: TestResult | Data<TestResult>
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = undefined
export let disabled = false
export let shouldShowAvatar: boolean = true
export let accent: boolean = false
const dispatch = createEventDispatcher()
const client = getClient()
$: itemsInfo = defaultTestRunStatuses.map((status) => ({
id: status,
isSelected: value === status,
...testRunStatusAssets[status]
}))
function handlePopupOpen (event: MouseEvent): void {
showPopup(
SelectPopup,
{ value: itemsInfo, placeholder: testManagement.string.SetStatus },
eventToHTMLElement(event),
changeStatus
)
}
async function changeStatus (newStatus: TestResult['status'] | null | undefined): Promise<void> {
if (disabled || newStatus == null || value === newStatus) {
return
}
value = newStatus
dispatch('change', value)
if (object !== undefined && '_id' in object) {
await client.update(object, { status: newStatus })
}
}
$: icon = value === undefined ? testManagement.icon.StatusNonTested : testRunStatusAssets[value].icon
$: label = value === undefined ? testManagement.string.StatusNonTested : testRunStatusAssets[value].label
</script>
{#if kind === 'list'}
<button
class="flex-no-shrink clear-mins cursor-pointer content-pointer-events-none"
{disabled}
on:click={handlePopupOpen}
>
<Icon {icon} {size} />
</button>
{:else if kind === 'list-header'}
<div class="flex-row-center pl-0-5">
{#if shouldShowAvatar}
<Icon {icon} {size} />
{/if}
<span class="overflow-label" class:ml-1-5={shouldShowAvatar} class:fs-bold={accent}><Label {label} /></span>
</div>
{:else}
<Button
{label}
{kind}
{icon}
{justify}
{size}
{width}
{disabled}
showTooltip={{ label: testManagement.string.SetStatus }}
on:click={handlePopupOpen}
/>
{/if}
@@ -0,0 +1,45 @@
<!--
// Copyright © 2024 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 { TestResult, TestRunStatus } from '@hcengineering/test-management'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import StatusEditor from './TestResultStatusEditor.svelte'
export let value: TestRunStatus
export let object: TestResult
export let onChange: ((value: TestRunStatus) => void) | undefined = undefined
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
export let justify: 'left' | 'center' = 'left'
export let width: string | undefined = '100%'
export let shouldShowAvatar: boolean = true
export let accent: boolean = false
$: disabled = onChange === undefined
</script>
<StatusEditor
{value}
{object}
{kind}
{size}
{width}
{justify}
{disabled}
{accent}
{shouldShowAvatar}
on:change={({ detail }) => onChange?.(detail)}
/>
@@ -17,26 +17,49 @@
import { Attachment } from '@hcengineering/attachment'
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { ObjectBox } from '@hcengineering/view-resources'
import core, { Data, Ref, generateId, makeCollaborativeDoc } from '@hcengineering/core'
import core, { Data, DocumentQuery, Ref, generateId, makeCollaborativeDoc } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Card, SpaceSelector, getClient } from '@hcengineering/presentation'
import { TestRun, TestProject } from '@hcengineering/test-management'
import { EditBox } from '@hcengineering/ui'
import { Card, SpaceSelector, createQuery, getClient } from '@hcengineering/presentation'
import {
TestCase,
TestRun,
TestProject,
TestResult,
TestRunStatus,
TestManagementEvents
} from '@hcengineering/test-management'
import { DatePresenter, EditBox, Loading, navigate } from '@hcengineering/ui'
import { EmptyMarkup } from '@hcengineering/text'
import { Analytics } from '@hcengineering/analytics'
import { getTestRunsLink } from '../../navigation'
import testManagement from '../../plugin'
import ProjectPresenter from '../project/ProjectSpacePresenter.svelte'
import TestCaseSelector from '../test-case/TestCaseSelector.svelte'
export let space: Ref<TestProject>
export let query: DocumentQuery<TestCase> = {}
export let testCases: TestCase[]
const dispatch = createEventDispatcher()
const client = getClient()
let isLoading = testCases === undefined
if (testCases === undefined) {
const client = createQuery()
const spaceQuery = space !== undefined ? { space } : {}
client.query(testManagement.class.TestCase, { ...spaceQuery, ...(query ?? {}) }, (result) => {
testCases = result
isLoading = false
})
}
const id: Ref<TestRun> = generateId()
const object: Data<TestRun> = {
name: '' as IntlString,
description: makeCollaborativeDoc(id, 'description')
description: makeCollaborativeDoc(id, 'description'),
dueDate: undefined
}
let _space = space
@@ -46,15 +69,52 @@
let descriptionBox: AttachmentStyledBox
let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>()
async function onSave () {
await client.createDoc(testManagement.class.TestRun, _space, object)
async function onSave (): Promise<void> {
try {
const applyOp = client.apply()
await applyOp.createDoc(testManagement.class.TestRun, _space, object, id)
const testCasesArray = testCases instanceof Array ? testCases : [testCases]
const createPromises = testCasesArray.map((testCase) => {
const testResultId: Ref<TestResult> = generateId()
const testResultData: Data<TestResult> = {
attachedTo: id,
attachedToClass: testManagement.class.TestRun,
name: testCase.name,
testCase: testCase._id,
testSuite: testCase.attachedTo,
collection: 'results',
description: makeCollaborativeDoc(testResultId, 'description'),
status: TestRunStatus.Untested
}
return applyOp.addCollection(
testManagement.class.TestResult,
_space,
id,
testManagement.class.TestRun,
'results',
testResultData,
testResultId
)
})
await Promise.all(createPromises)
const opResult = await applyOp.commit()
if (!opResult.result) {
throw new Error('Failed to create test run')
} else {
Analytics.handleEvent(TestManagementEvents.TestRunCreated, { id })
navigate(getTestRunsLink(id))
}
} catch (err: any) {
console.error(err)
Analytics.handleError(err)
}
}
</script>
<Card
label={testManagement.string.CreateTestRun}
okAction={onSave}
canSave={object.name !== ''}
canSave={object.name !== '' && !isLoading}
okLabel={testManagement.string.CreateTestRun}
gap={'gapV-4'}
on:close={() => dispatch('close')}
@@ -99,37 +159,22 @@
}}
/>
<svelte:fragment slot="pool">
<ObjectBox
_class={testManagement.class.TestSuite}
value={null}
docQuery={{
space: _space
}}
kind={'regular'}
size={'small'}
label={testManagement.string.SelectTestSuites}
icon={testManagement.icon.TestSuite}
searchField={'title'}
allowDeselect={true}
showNavigate={false}
docProps={{ disabled: true, noUnderline: true }}
focusIndex={20000}
/>
<ObjectBox
_class={testManagement.class.TestCase}
value={null}
docQuery={{
space: _space
}}
kind={'regular'}
size={'small'}
label={testManagement.string.SelectTestCases}
icon={testManagement.icon.TestCase}
searchField={'title'}
allowDeselect={true}
showNavigate={false}
docProps={{ disabled: true, noUnderline: true }}
focusIndex={20000}
/>
<div id="duedate-editor">
<DatePresenter
focusIndex={10}
bind:value={object.dueDate}
labelNull={testManagement.string.DueDate}
kind={'regular'}
size={'large'}
editable
/>
</div>
<div id="test-cases-selector">
{#if isLoading}
<Loading />
{:else}
<TestCaseSelector objects={testCases} selectedObjects={testCases} readonly={true} />
{/if}
</div>
</svelte:fragment>
</Card>
@@ -13,6 +13,97 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachmentStyleBoxCollabEditor } from '@hcengineering/attachment-resources'
import { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { type Class, type Ref } from '@hcengineering/core'
import { TestRun } from '@hcengineering/test-management'
import { Panel } from '@hcengineering/panel'
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import testManagement from '../../plugin'
import TestRunAside from './TestRunAside.svelte'
export let _id: Ref<TestRun>
export let _class: Ref<Class<TestRun>>
let object: TestRun | undefined
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
let oldLabel: string | undefined = ''
let rawLabel: string | undefined = ''
let descriptionBox: AttachmentStyleBoxCollabEditor
const query = createQuery()
$: _id !== undefined &&
_class !== undefined &&
query.query(_class, { _id }, async (result) => {
;[object] = result
})
async function change<K extends keyof TestRun> (field: K, value: TestRun[K]) {
if (object !== undefined) {
await client.update(object, { [field]: value })
}
}
let content: HTMLElement
$: if (oldLabel !== object?.name) {
oldLabel = object?.name
rawLabel = object?.name
}
$: descriptionKey = hierarchy.getAttribute(testManagement.class.TestRun, 'description')
onMount(() => dispatch('open', { ignoreKeys: [] }))
</script>
<div class="antiNav-subheader"></div>
{#if object}
<ActionContext context={{ mode: 'editor' }} />
<Panel
{object}
title={object.name}
isHeader={false}
isAside={true}
isSub={false}
adaptive={'default'}
on:open
on:close={() => dispatch('close')}
>
<EditBox
bind:value={rawLabel}
placeholder={testManagement.string.NamePlaceholder}
kind="large-style"
on:blur={async () => {
const trimmedLabel = rawLabel?.trim()
if (trimmedLabel?.length === 0) {
rawLabel = oldLabel
} else if (trimmedLabel !== object?.name) {
await change('name', trimmedLabel ?? '')
}
}}
/>
<div class="w-full mt-6">
<AttachmentStyleBoxCollabEditor
focusIndex={30}
{object}
key={{ key: 'description', attr: descriptionKey }}
bind:this={descriptionBox}
identifier={object?._id}
placeholder={testManagement.string.DescriptionPlaceholder}
boundary={content}
/>
</div>
<svelte:fragment slot="aside">
<TestRunAside {object} />
</svelte:fragment>
</Panel>
{/if}
@@ -0,0 +1,106 @@
<!--
// Copyright © 2024 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 { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { ActionContext } from '@hcengineering/presentation'
import { TestCase } from '@hcengineering/test-management'
import { AnyComponent, AnySvelteComponent, registerFocus } from '@hcengineering/ui'
import { ViewOptions, Viewlet, ViewletPreference } from '@hcengineering/view'
import { List, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import testManagement from '../../plugin'
export let query: DocumentQuery<TestCase> | undefined = undefined
export let viewlet: Viewlet
export let viewOptions: ViewOptions
export let disableHeader: boolean = false
export let compactMode: boolean = false
export let configurations: Record<Ref<Class<Doc>>, Viewlet['config']> = {}
export let preference: ViewletPreference[] = []
export let createItemDialog: AnySvelteComponent | AnyComponent | undefined = undefined
export let createItemLabel: IntlString | undefined = undefined
export let createItemDialogProps: Record<string, any> | undefined = undefined
let list: List
const listProvider = new ListSelectionProvider(
(offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection, noScroll?: boolean) => {
if (dir === 'vertical') {
// Select next
list?.select(offset, of, noScroll)
}
}
)
let docs: Doc[] = []
function select () {
listProvider.update(docs)
listProvider.updateFocus(docs[0])
list?.select(0, undefined)
}
const selection = listProvider.selection
// Focusable control with index
let focused = false
export let focusIndex = -1
registerFocus(focusIndex, {
focus: () => {
;(window.document.activeElement as HTMLElement).blur()
focused = true
select()
return true
},
isFocus: () => focused
})
const dispatch = createEventDispatcher()
</script>
<ActionContext
context={{
mode: 'browser'
}}
/>
{#if viewlet}
<List
bind:this={list}
_class={testManagement.class.TestCase}
{viewOptions}
viewOptionsConfig={viewlet.viewOptions?.other}
config={preference.find((it) => it.attachedTo === viewlet._id)?.config ?? viewlet.config}
{configurations}
{query}
flatHeaders={true}
{disableHeader}
{createItemDialog}
{createItemDialogProps}
{createItemLabel}
{listProvider}
selectedObjectIds={$selection ?? []}
{compactMode}
on:row-focus={(event) => {
listProvider.updateFocus(event.detail ?? undefined)
}}
on:check={(event) => {
listProvider.updateSelection(event.detail.docs, event.detail.value)
}}
on:content={(evt) => {
docs = evt.detail
listProvider.update(evt.detail)
dispatch('docs', docs)
}}
/>
{/if}
@@ -13,52 +13,45 @@
// limitations under the License.
-->
<script lang="ts">
import type { Ref } from '@hcengineering/core'
import { Doc, DocumentQuery } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { testManagementId, TestSuite } from '@hcengineering/test-management'
import { Button, Icon, IconAdd, Label, Loading, Scroller, showPopup, SectionEmpty } from '@hcengineering/ui'
import { Button, Icon, IconAdd, Label, Loading, Scroller, SectionEmpty } from '@hcengineering/ui'
import { Viewlet, ViewletPreference } from '@hcengineering/view'
import { NavLink, Table, ViewletsSettingButton } from '@hcengineering/view-resources'
import { Table, ViewletsSettingButton } from '@hcengineering/view-resources'
import testManagement from '../../plugin'
import CreateTestCase from '../test-case/CreateTestCase.svelte'
import FileDuo from '../icons/FileDuo.svelte'
export let objectId: Ref<TestSuite>
export let baseQuery: DocumentQuery<Doc> = {}
let testCases: number
const query = createQuery()
$: query.query(testManagement.class.TestCase, { suite: objectId }, (res) => {
$: query.query(testManagement.class.TestCase, baseQuery, (res) => {
testCases = res.length
})
const createTestCase = (ev: MouseEvent): void => {
showPopup(CreateTestCase, { testSuiteId: objectId }, ev.target as HTMLElement)
}
let viewlet: Viewlet | undefined
let preference: ViewletPreference | undefined
let loading = true
</script>
<!--TODO: Finish implementation-->
<div class="antiSection max-h-125 clear-mins">
<div class="antiSection-header">
<div class="antiSection-header__icon">
<Icon icon={testManagement.icon.TestCase} size={'small'} />
</div>
<span class="antiSection-header__title">
<NavLink app={testManagementId} space={objectId}>
<Label label={testManagement.string.TestCases} />
</NavLink>
<Label label={testManagement.string.TestCases} />
</span>
<div class="flex-row-center gap-2 reverse">
<ViewletsSettingButton
viewletQuery={{ _id: testManagement.viewlet.SuiteTestCases }}
viewletQuery={{ _id: testManagement.viewlet.ListTestCase }}
kind={'tertiary'}
bind:viewlet
bind:preference
bind:loading
/>
<Button id="appls.add" icon={IconAdd} kind={'ghost'} on:click={createTestCase} />
<Button id="appls.add" icon={IconAdd} kind={'ghost'} on:click={() => {}} />
</div>
</div>
{#if testCases > 0}
@@ -67,7 +60,7 @@
<Table
_class={testManagement.class.TestCase}
config={preference?.config ?? viewlet.config}
query={{ suite: objectId }}
query={baseQuery}
loadingProps={{ length: testCases }}
/>
</Scroller>
@@ -78,7 +71,7 @@
<SectionEmpty icon={FileDuo} label={testManagement.string.NoTestCases}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<span class="over-underline content-color" on:click={createTestCase}>
<span class="over-underline content-color">
<Label label={testManagement.string.CreateTestCase} />
</span>
</SectionEmpty>
@@ -0,0 +1,32 @@
<!--
// Copyright © 2024 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 { WithLookup } from '@hcengineering/core'
import type { TestRun } from '@hcengineering/test-management'
import { Scroller } from '@hcengineering/ui'
import { DocAttributeBar } from '@hcengineering/view-resources'
import TestRunStats from './TestRunStats.svelte'
export let object: WithLookup<TestRun>
export let readonly: boolean = false
</script>
<Scroller>
<TestRunStats _id={object._id} />
<div class="space-divider" />
<DocAttributeBar {object} {readonly} ignoreKeys={['name']} />
<div class="space-divider bottom" />
</Scroller>
@@ -13,6 +13,28 @@
// limitations under the License.
-->
<script lang="ts">
import { TestRun } from '@hcengineering/test-management'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { tooltip } from '@hcengineering/ui'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
export let value: TestRun | undefined
export let inline: boolean = false
export let disabled: boolean = false
export let accent: boolean = false
export let noUnderline: boolean = false
</script>
<div class="antiNav-subheader"></div>
{#if value}
{#if inline}
<ObjectMention object={value} {disabled} {accent} {noUnderline} />
{:else}
<DocNavLink object={value} {disabled} {accent} {noUnderline}>
<div class="flex-presenter" use:tooltip={{ label: getEmbeddedLabel(value.name) }}>
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
{value.name}
</span>
</div>
</DocNavLink>
{/if}
{/if}
@@ -0,0 +1,64 @@
<!--
// Copyright © 2024 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 { BreadcrumbsElement } from '@hcengineering/presentation'
import { ScrollerBar } from '@hcengineering/ui'
import { type TestRunStats } from '../../testRunUtils'
export let value: TestRunStats
let divScroll: HTMLElement
</script>
<!--TODO: Refactor and get rid of harcoded values-->
<ScrollerBar gap="none" bind:scroller={divScroll}>
<BreadcrumbsElement
noGap
label={value.untested.toString()}
position={'start'}
color={'#4CA6EE'}
fontColor="white"
title="Untested"
selected
/>
<BreadcrumbsElement
label={value.blocked.toString()}
noGap
position={'middle'}
color={'#D27540'}
selected
fontColor="white"
title="Failed"
/>
<BreadcrumbsElement
label={value.failed.toString()}
noGap
position={'middle'}
color={'#D15045'}
selected
fontColor="white"
title="Failed"
/>
<BreadcrumbsElement
noGap
label={value.completed.toString()}
position={'end'}
color={'#46A44F'}
fontColor="white"
title="Passed"
selected
/>
</ScrollerBar>
@@ -0,0 +1,55 @@
<!--
// Copyright © 2024 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 { Ref } from '@hcengineering/core'
import type { TestRun } from '@hcengineering/test-management'
import { Label, ProgressCircle, Loading } from '@hcengineering/ui'
import TestRunResult from './TestRunResult.svelte'
import { type TestRunStats, getTestRunStats } from '../../testRunUtils'
import testManagement from '../../plugin'
export let _id: Ref<TestRun>
let stats: TestRunStats | undefined = undefined
let isLoading = true
getTestRunStats(_id).then((newStats) => {
stats = newStats
isLoading = false
})
</script>
<div class="popupPanel-body__aside-grid">
<span class="labelOnPanel"><Label label={testManagement.string.TestResults} /> </span>
{#if !isLoading && stats !== undefined}
<TestRunResult value={stats} />
{:else}
<Loading />
{/if}
<span class="labelOnPanel">
<Label label={testManagement.string.DonePercent} />
</span>
{#if !isLoading}
<div class="flex-row-center content-color text-sm pointer-events-none">
<div class="mr-1">
<ProgressCircle value={stats?.done ?? 0} />
</div>
{stats?.done ?? 0}
</div>
{:else}
<Loading />
{/if}
</div>
@@ -0,0 +1,25 @@
<!--
// Copyright © 2024 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 { Breadcrumb, Header } from '@hcengineering/ui'
import { IntlString } from '@hcengineering/platform'
export let header: IntlString
</script>
<Header adaptive={'disabled'}>
<Breadcrumb label={header} size={'large'} />
</Header>
<slot />
@@ -0,0 +1,22 @@
//
// Copyright © 2024 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.
//
import type { TestCase, TestResult } from '@hcengineering/test-management'
import { type Ref } from '@hcengineering/core'
import { type Writable, writable } from 'svelte/store'
export const currentTestCase: Writable<Ref<TestCase> | undefined> = writable(undefined)
export const selectedTestRun: Writable<TestResult | undefined> = writable(undefined)
@@ -21,7 +21,6 @@
import { EditBox, Breadcrumb } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import TestCasesList from './TestCasesList.svelte'
import testManagement from '../../plugin'
export let _id: Ref<TestSuite>
@@ -95,9 +94,5 @@
showButtons={false}
/>
</div>
<div class="w-full mt-6">
<TestCasesList objectId={object._id} />
</div>
</Panel>
{/if}