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,51 +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.
//
import { type Builder } from '@hcengineering/model'
import core from '@hcengineering/model-core'
import { TDefaultProjectTypeData } from './types'
import testManagement from './plugin'
export function defineDefaultSpace (builder: Builder): void {
defineDefaultProject(builder)
}
function defineDefaultProject (builder: Builder): void {
builder.createModel(TDefaultProjectTypeData)
builder.createDoc(
core.class.SpaceTypeDescriptor,
core.space.Model,
{
name: testManagement.string.TestProject,
description: testManagement.string.FullDescription,
icon: testManagement.icon.TestProject,
baseClass: testManagement.class.TestProject,
availablePermissions: [
core.permission.UpdateSpace,
core.permission.ArchiveSpace,
core.permission.ForbidDeleteObject
]
},
testManagement.descriptors.ProjectType
)
builder.createDoc(core.class.SpaceType, core.space.Model, {
name: 'Default project type',
descriptor: testManagement.descriptors.ProjectType,
roles: 0,
targetClass: testManagement.mixin.DefaultProjectTypeData
})
}
+151 -46
View File
@@ -16,7 +16,7 @@
import activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import core from '@hcengineering/model-core'
import { SortingOrder } from '@hcengineering/core'
import { SortingOrder, type FindOptions } from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import view, { createAction } from '@hcengineering/model-view'
@@ -25,7 +25,7 @@ import print from '@hcengineering/model-print'
import tracker from '@hcengineering/model-tracker'
import { type ViewOptionsModel } from '@hcengineering/view'
import { testManagementId } from '@hcengineering/test-management'
import { testManagementId, type TestResult } from '@hcengineering/test-management'
import {
DOMAIN_TEST_MANAGEMENT,
@@ -37,8 +37,8 @@ import {
TTestCase,
TDefaultProjectTypeData,
TTestRun,
TTypeTestRunResult,
TTestRunItem
TTypeTestRunStatus,
TTestResult
} from './types'
import testManagement from './plugin'
@@ -55,7 +55,6 @@ function defineApplication (builder: Builder): void {
icon: testManagement.icon.TestManagementApplication,
alias: testManagementId,
hidden: false,
locationResolver: testManagement.resolver.Location,
navigatorModel: {
spaces: [
{
@@ -85,6 +84,7 @@ function defineApplication (builder: Builder): void {
mainComponentLabel: testManagement.string.TestCases,
mainComponentIcon: testManagement.icon.TestCases,
createComponent: testManagement.component.CreateTestSuite,
mainHeaderComponent: testManagement.component.RunSelectedTestsButton,
navigationComponentProps: {
_class: testManagement.class.TestSuite,
icon: testManagement.icon.TestSuites,
@@ -100,21 +100,34 @@ function defineApplication (builder: Builder): void {
},
syncWithLocationQuery: true
}
}
/* TODO: UBERF-8584
},
{
id: opt.testRunsId,
id: 'testRuns',
label: testManagement.string.TestRuns,
icon: testManagement.icon.TestRuns,
component: workbench.component.SpecialView,
componentProps: {
_class: testManagement.class.TestRun,
_class: testManagement.class.TestResult,
icon: testManagement.icon.TestRuns,
title: testManagement.string.TestRuns,
createLabel: testManagement.string.NewTestRun,
createComponent: testManagement.component.CreateTestRun
label: testManagement.string.TestRuns
},
navigationModel: {
navigationComponent: view.component.FoldersBrowser,
navigationComponentLabel: testManagement.string.TestRun,
navigationComponentIcon: testManagement.icon.TestRuns,
mainComponentLabel: testManagement.string.TestResults,
mainComponentIcon: testManagement.icon.TestResult,
navigationComponentProps: {
_class: testManagement.class.TestRun,
icon: testManagement.icon.TestRuns,
title: testManagement.string.TestSuites,
titleKey: 'name',
getFolderLink: testManagement.function.GetTestRunLink,
plainList: true
},
syncWithLocationQuery: true
}
} */
}
]
}
]
@@ -135,8 +148,8 @@ export function createModel (builder: Builder): void {
TTestCase,
TDefaultProjectTypeData,
TTestRun,
TTestRunItem,
TTypeTestRunResult
TTypeTestRunStatus,
TTestResult
)
builder.mixin(testManagement.class.TestProject, core.class.Class, activity.mixin.ActivityDoc, {})
@@ -204,7 +217,7 @@ function defineSpaceType (builder: Builder): void {
core.class.SpaceType,
core.space.Model,
{
name: 'Default project type',
name: 'Default Test Management',
descriptor: testManagement.descriptors.ProjectType,
roles: 0,
targetClass: testManagement.mixin.DefaultProjectTypeData
@@ -250,13 +263,7 @@ function defineTestSuite (builder: Builder): void {
// Actions
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.IgnoreActions, {
actions: [
view.action.Open,
view.action.OpenInNewTab,
print.action.Print,
tracker.action.EditRelatedTargets,
tracker.action.NewRelatedIssue
]
actions: [print.action.Print, tracker.action.EditRelatedTargets]
})
createAction(
@@ -276,6 +283,24 @@ function defineTestSuite (builder: Builder): void {
},
testManagement.action.CreateChildTestSuite
)
createAction(
builder,
{
action: testManagement.actionImpl.RunSelectedTests,
label: testManagement.string.CreateTestRun,
icon: testManagement.icon.TestRuns,
category: testManagement.category.TestCase,
input: 'selection',
target: testManagement.class.TestCase,
context: {
mode: ['context'],
application: testManagement.app.TestManagement,
group: 'create'
}
},
testManagement.action.RunSelectedTests
)
}
function defineTestCase (builder: Builder): void {
@@ -306,6 +331,11 @@ function defineTestCase (builder: Builder): void {
presenter: testManagement.component.TestSuiteRefPresenter
})
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ClassFilters, {
filters: ['priority', 'status'],
ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn']
})
builder.createDoc(
view.class.Viewlet,
core.space.Model,
@@ -350,6 +380,7 @@ function defineTestCase (builder: Builder): void {
hiddenKeys: ['title']
},
config: [
{ key: '', displayProps: { fixed: 'left' } },
{
key: 'status',
props: { kind: 'list', size: 'small', shouldShowName: false },
@@ -368,21 +399,6 @@ function defineTestCase (builder: Builder): void {
},
testManagement.viewlet.ListTestCase
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: testManagement.class.TestCase,
descriptor: view.viewlet.Table,
config: ['', 'assignee', 'modifiedOn'],
configOptions: {
sortable: true
},
variant: 'short'
},
testManagement.viewlet.SuiteTestCases
)
}
function defineTestRun (builder: Builder): void {
@@ -393,10 +409,6 @@ function defineTestRun (builder: Builder): void {
components: { input: { component: chunter.component.ChatMessageInput } }
})
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectEditor, {
editor: testManagement.component.EditTestRun
})
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectPanel, {
component: testManagement.component.EditTestRun
})
@@ -405,18 +417,111 @@ function defineTestRun (builder: Builder): void {
presenter: testManagement.component.TestRunPresenter
})
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectIcon, {
component: testManagement.component.TestResultStatusPresenter
})
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectPresenter, {
presenter: testManagement.component.TestResultPresenter
})
builder.mixin(testManagement.class.TestResult, core.class.Class, activity.mixin.ActivityDoc, {})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestResult,
components: { input: { component: chunter.component.ChatMessageInput } }
})
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectEditor, {
editor: testManagement.component.EditTestResult
})
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectEditorHeader, {
editor: testManagement.component.TestResultHeader
})
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectPanel, {
component: testManagement.component.EditTestResult
})
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectPanelFooter, {
editor: testManagement.component.TestResultFooter
})
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ClassFilters, {
filters: ['assignee', 'status', 'testSuite'],
ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn']
})
const viewOptions: ViewOptionsModel = {
groupBy: ['testSuite'],
orderBy: [
['status', SortingOrder.Ascending],
['modifiedOn', SortingOrder.Descending],
['createdOn', SortingOrder.Descending]
],
other: [
{
key: 'shouldShowAll',
type: 'toggle',
defaultValue: false,
actionTarget: 'category',
action: view.function.ShowEmptyGroups,
label: view.string.ShowEmptyGroups
}
]
}
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: testManagement.class.TestRun,
attachTo: testManagement.class.TestResult,
descriptor: view.viewlet.List,
configOptions: {
strict: true,
hiddenKeys: ['title', 'status', 'modifiedOn']
},
config: [
{ key: '', displayProps: { fixed: 'left' } },
{
key: 'status',
props: { kind: 'list', size: 'small', shouldShowName: false }
},
{
key: 'assignee',
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' },
displayProps: { key: 'assignee', fixed: 'right' }
}
],
viewOptions,
/* eslint-disable @typescript-eslint/consistent-type-assertions */
options: {
lookup: {
testCase: testManagement.class.TestCase
}
} as FindOptions<TestResult>
},
testManagement.viewlet.TestResultList
)
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: testManagement.class.TestResult,
descriptor: view.viewlet.Table,
config: [''],
config: ['', 'testSuite', 'status', 'assignee'],
configOptions: {
strict: true
}
},
options: {
lookup: {
testCase: testManagement.class.TestCase
}
} as FindOptions<TestResult>
},
testManagement.viewlet.TableTestRun
testManagement.viewlet.TableTestResult
)
}
+9 -13
View File
@@ -15,23 +15,15 @@
import { testManagementId } from '@hcengineering/test-management'
import testManganement from '@hcengineering/test-management-resources/src/plugin'
import type { Doc, Ref } from '@hcengineering/core'
import type { Ref } from '@hcengineering/core'
import { mergeIds } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui/src/types'
import type { Action, ActionCategory, ViewAction } from '@hcengineering/view'
import type { ActionCategory } from '@hcengineering/view'
export default mergeIds(testManagementId, testManganement, {
action: {
DeleteTestCase: '' as Ref<Action<Doc, any>>,
CreateChildTestSuite: '' as Ref<Action>,
EditTestSuite: '' as Ref<Action>
},
actionImpl: {
CreateChildTestSuite: '' as ViewAction,
EditTestSuite: '' as ViewAction
},
category: {
TestSuite: '' as Ref<ActionCategory>
TestSuite: '' as Ref<ActionCategory>,
TestCase: '' as Ref<ActionCategory>
},
component: {
CreateTestCase: '' as AnyComponent,
@@ -44,6 +36,10 @@ export default mergeIds(testManagementId, testManganement, {
CreateTestRun: '' as AnyComponent,
TestRunPresenter: '' as AnyComponent,
EditTestRun: '' as AnyComponent,
TestSuiteRefPresenter: '' as AnyComponent
TestSuiteRefPresenter: '' as AnyComponent,
RunSelectedTestsButton: '' as AnyComponent,
TestResultPresenter: '' as AnyComponent,
EditTestResult: '' as AnyComponent,
TestResultFooter: '' as AnyComponent
}
})
+8
View File
@@ -53,4 +53,12 @@ export function definePresenters (builder: Builder): void {
builder.mixin(testManagement.class.TypeTestCaseStatus, core.class.Class, view.mixin.AttributePresenter, {
presenter: testManagement.component.TestCaseStatusPresenter
})
builder.mixin(testManagement.class.TypeTestRunStatus, core.class.Class, view.mixin.AttributePresenter, {
presenter: testManagement.component.TestResultStatusPresenter
})
builder.mixin(testManagement.class.TypeTestRunStatus, core.class.Class, view.mixin.AttributeEditor, {
inlineEditor: testManagement.component.TestResultStatusEditor
})
}
+50 -17
View File
@@ -22,8 +22,8 @@ import type {
TestCaseStatus,
TestProject,
TestRun,
TestRunResult,
TestRunItem
TestRunStatus,
TestResult
} from '@hcengineering/test-management'
import { type Attachment } from '@hcengineering/attachment'
import contact from '@hcengineering/contact'
@@ -201,32 +201,65 @@ export class TTestRun extends TDoc implements TestRun {
@Prop(TypeDate(DateRangeMode.DATETIME), testManagement.string.DueDate)
dueDate?: Timestamp
@Prop(Collection(testManagement.class.TestRunItem), testManagement.string.TestRunItems, {
shortLabel: testManagement.string.TestRunItem
@Prop(Collection(testManagement.class.TestResult), testManagement.string.TestResult, {
shortLabel: testManagement.string.TestResult
})
items?: CollectionSize<TestRunItem>
results?: CollectionSize<TestResult>
}
/** @public */
export function TypeTestRunResult (): Type<TestRunResult> {
return { _class: testManagement.class.TypeTestRunResult, label: testManagement.string.TestRunResult }
export function TypeTestRunStatus (): Type<TestRunStatus> {
return { _class: testManagement.class.TypeTestRunStatus, label: testManagement.string.TestRunStatus }
}
@Model(testManagement.class.TypeTestRunResult, core.class.Type, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestRunResult)
export class TTypeTestRunResult extends TType {}
@Model(testManagement.class.TypeTestRunStatus, core.class.Type, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestRunStatus)
export class TTypeTestRunStatus extends TType {}
@Model(testManagement.class.TestRunItem, core.class.AttachedDoc, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestRunItem)
export class TTestRunItem extends TAttachedDoc implements TestRunItem {
@Prop(TypeRef(testManagement.class.TestRun), testManagement.string.TestRun)
testRun!: Ref<TestRun>
// TODO: Refactor to associations
@Model(testManagement.class.TestResult, core.class.AttachedDoc, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestResult)
export class TTestResult extends TAttachedDoc implements TestResult {
@Prop(TypeRef(testManagement.class.TestRun), core.string.AttachedTo)
@Index(IndexKind.Indexed)
declare attachedTo: Ref<TestRun>
@Prop(TypeRef(testManagement.class.TestRun), core.string.AttachedToClass)
@Index(IndexKind.Indexed)
@Hidden()
declare attachedToClass: Ref<Class<TestRun>>
@Prop(TypeRef(testManagement.class.TestProject), core.string.Space)
@Index(IndexKind.Indexed)
@Hidden()
declare space: Ref<TestProject>
@Prop(TypeString(), core.string.Collection)
@Hidden()
override collection: 'results' = 'results'
@Prop(TypeString(), testManagement.string.TestRunName)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeCollaborativeDoc(), testManagement.string.FullDescription)
@Index(IndexKind.FullText)
description!: CollaborativeDoc
@Prop(TypeRef(testManagement.class.TestCase), testManagement.string.TestCase)
testCase!: Ref<TestCase>
@Prop(TypeTestRunResult(), testManagement.string.TestRunResult)
result?: TestRunResult
@Prop(TypeRef(testManagement.class.TestSuite), testManagement.string.TestSuite)
testSuite?: Ref<TestSuite>
@Prop(TypeTestRunStatus(), testManagement.string.TestRunStatus)
status?: TestRunStatus
@Prop(TypeRef(contact.mixin.Employee), testManagement.string.TestAssignee)
assignee?: Ref<Employee>
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: CollectionSize<Attachment>
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
comments?: number
@@ -19,6 +19,8 @@
export let position: 'start' | 'middle' | 'end' | undefined = undefined
export let selected = false
export let color = 'var(--theme-bg-color)'
export let noGap = false
export let fontColor: string | undefined = undefined
let clientWidth = 0
@@ -43,6 +45,7 @@
<div
class="asb-bar"
class:selected
class:noGap
class:cursor-pointer={!selected && !$$slots.default}
class:cursor-default={selected || $$slots.default}
{title}
@@ -91,7 +94,7 @@
{/if}
</svg>
<div class="asb-label__container" class:selected class:disabled={!$$slots.default}>
<div class="overflow-label">
<div class="overflow-label" style={fontColor ? `color: ${fontColor};` : ''}>
{#if $$slots.default}
<slot />
{:else}
+4
View File
@@ -1493,6 +1493,10 @@
min-width: 0;
width: auto;
&.noGap {
flex: none;
}
&__back {
padding: 1px 0.5px;
height: calc(1.5rem + 2px);
@@ -3,12 +3,11 @@
<symbol id="testManagementApplication" viewBox="0 0 24 24">
<path d="M17.2,7.8h3.6c1.1,0,2-0.9,2-2V4.2c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0H9.8V4c0-1.5-1.2-2.8-2.8-2.8H4 C2.5,1.2,1.2,2.5,1.2,4v2c0,1.5,1.2,2.8,2.8,2.8h3c1.5,0,2.8-1.2,2.8-2.8V5.8h2V18c0,1.5,1.2,2.8,2.8,2.8h0.8v0c0,1.1,0.9,2,2,2h3.6 c1.1,0,2-0.9,2-2v-1.6c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0h-0.8c-0.7,0-1.2-0.6-1.2-1.2v-4.8h2v0c0,1.1,0.9,2,2,2h3.6 c1.1,0,2-0.9,2-2v-1.6c0-1.1-0.9-2-2-2h-3.6c-1.1,0-2,0.9-2,2v0h-2v-6h2v0C15.2,6.9,16.1,7.8,17.2,7.8z M8.2,6 c0,0.7-0.6,1.2-1.2,1.2H4C3.3,7.2,2.8,6.7,2.8,6V4c0-0.7,0.6-1.2,1.2-1.2h3c0.7,0,1.2,0.6,1.2,1.2V6z M16.8,19.2 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V19.2z M16.8,11.7 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V11.7z M16.8,4.2 c0-0.2,0.2-0.5,0.5-0.5h3.6c0.2,0,0.5,0.2,0.5,0.5v1.6c0,0.2-0.2,0.5-0.5,0.5h-3.6c-0.2,0-0.5-0.2-0.5-0.5V4.2z" />
</symbol>
<symbol id="testCase" viewBox="0 0 24 24">
<path xmlns:default="http://www.w3.org/2000/svg" d="M3 9H1v11c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2H3V9zm15-4V3c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H5v11c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2V5h-5zm-6-2h4v2h-4V3zm0 12V8l5.5 3-5.5 4z" vector-effect="non-scaling-stroke"/>
<symbol id="testCase" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 4C8.89543 4 8 4.89543 8 6V26C8 27.1046 8.89543 28 10 28H22C23.1046 28 24 27.1046 24 26V12H20C17.7909 12 16 10.2091 16 8V4H10ZM18 4.41421V8C18 9.10457 18.8954 10 20 10H23.5858L18 4.41421ZM6 6C6 3.79086 7.79086 2 10 2H17.5858C18.1162 2 18.6249 2.21071 19 2.58579L25.4142 9C25.7893 9.37507 26 9.88378 26 10.4142V26C26 28.2091 24.2091 30 22 30H10C7.79086 30 6 28.2091 6 26V6ZM10 17C10 16.4477 10.4477 16 11 16H15C15.5523 16 16 16.4477 16 17C16 17.5523 15.5523 18 15 18H11C10.4477 18 10 17.5523 10 17ZM10 21C10 20.4477 10.4477 20 11 20H21C21.5523 20 22 20.4477 22 21C22 21.5523 21.5523 22 21 22H11C10.4477 22 10 21.5523 10 21Z" />
</symbol>
<symbol id="testRun" viewBox="0 0 20 20" fill="none">
<path d="M10 1.24988C8.26942 1.24988 6.57769 1.76306 5.13876 2.72452C3.69983 3.68598 2.57832 5.05254 1.91606 6.6514C1.25379 8.25025 1.08051 10.0096 1.41813 11.7069C1.75575 13.4043 2.58911 14.9634 3.81282 16.1871C5.03653 17.4108 6.59563 18.2441 8.29296 18.5817C9.9903 18.9194 11.7496 18.7461 13.3485 18.0838C14.9473 17.4216 16.3139 16.3 17.2754 14.8611C18.2368 13.4222 18.75 11.7305 18.75 9.99988C18.75 7.67923 17.8281 5.45364 16.1872 3.81269C14.5462 2.17175 12.3206 1.24988 10 1.24988ZM10 17.4999C8.51664 17.4999 7.0666 17.06 5.83323 16.2359C4.59986 15.4118 3.63856 14.2404 3.07091 12.87C2.50325 11.4996 2.35473 9.99156 2.64411 8.5367C2.9335 7.08184 3.64781 5.74547 4.6967 4.69658C5.7456 3.64768 7.08197 2.93338 8.53683 2.64399C9.99168 2.3546 11.4997 2.50312 12.8701 3.07078C14.2406 3.63844 15.4119 4.59973 16.236 5.8331C17.0601 7.06647 17.5 8.51652 17.5 9.99988C17.5 11.989 16.7098 13.8967 15.3033 15.3032C13.8968 16.7097 11.9891 17.4999 10 17.4999Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1919 7.05809C14.436 7.30217 14.436 7.6979 14.1919 7.94197L9.19194 12.942C8.94786 13.1861 8.55214 13.1861 8.30806 12.942L5.80806 10.442C5.56398 10.1979 5.56398 9.80217 5.80806 9.55809C6.05214 9.31401 6.44786 9.31401 6.69194 9.55809L8.75 11.6161L13.3081 7.05809C13.5521 6.81401 13.9479 6.81401 14.1919 7.05809Z" fill="currentColor"/>
<symbol id="testRun" viewBox="0 0 24 24">
<path xmlns:default="http://www.w3.org/2000/svg" d="M3 9H1v11c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2H3V9zm15-4V3c0-1.11-.89-2-2-2h-4c-1.11 0-2 .89-2 2v2H5v11c0 1.11.89 2 2 2h14c1.11 0 2-.89 2-2V5h-5zm-6-2h4v2h-4V3zm0 12V8l5.5 3-5.5 4z" vector-effect="non-scaling-stroke"/>
</symbol>
<symbol id="testSuite" viewBox="0 0 16 16">
<path d="M14.5,6.7c0-2,0-3-0.7-3.8C13,2.2,12,2.2,10,2.2H6c-0.2,0-0.4,0-0.6,0l-0.1,0v0c-1.5,0-2.5,0.1-3.1,0.7 C1.5,3.6,1.5,4.7,1.5,6.7v2.7c0,2,0,3,0.7,3.8C3,13.8,4,13.8,6,13.8h4c2,0,3,0,3.8-0.7c0.5-0.5,0.7-1.3,0.7-2.4h0V10 c0-0.1,0-0.3,0-0.4c0-0.1,0-0.2,0-0.3V6.7z M13.1,3.6c0.4,0.4,0.4,1.3,0.4,3.1v0c-0.1-0.1-0.2-0.2-0.3-0.2c-0.6-0.3-1.2-0.3-2.5-0.3 c-0.7,0-1.1,0-1.3-0.1C9.2,6,9,5.9,8.9,5.8C8.7,5.6,8.5,5.3,8.2,4.6L7.6,3.5C7.6,3.3,7.5,3.3,7.4,3.2H10C11.7,3.2,12.6,3.2,13.1,3.6z M10,12.8H6c-1.7,0-2.6,0-3.1-0.4C2.5,12,2.5,11.1,2.5,9.3V6.7c0-1.7,0-2.6,0.4-3.1c0.4-0.4,1.1-0.4,2.5-0.4l0.1,0 c0.5,0,1,0.3,1.2,0.8l0.6,1.1c0.4,0.7,0.6,1.1,0.9,1.4C8.4,6.7,8.7,6.9,9,7c0.4,0.2,0.9,0.2,1.7,0.2c1.1,0,1.7,0,2,0.2 c0.2,0.1,0.4,0.3,0.6,0.6c0.2,0.3,0.2,0.9,0.2,1.9c0,1.4,0,2.2-0.4,2.6C12.6,12.8,11.7,12.8,10,12.8z"/>
@@ -52,4 +51,21 @@
<symbol id="project" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 6C13.4477 6 13 6.44772 13 7V8H19V7C19 6.44772 18.5523 6 18 6H14ZM21 8V7C21 5.34315 19.6569 4 18 4H14C12.3431 4 11 5.34315 11 7V8H6C3.79086 8 2 9.79086 2 12V24C2 26.2091 3.79086 28 6 28H26C28.2091 28 30 26.2091 30 24V12C30 9.79086 28.2091 8 26 8H21ZM6 10C4.89543 10 4 10.8954 4 12V16H28V12C28 10.8954 27.1046 10 26 10H6ZM28 18H17V19C17 19.5523 16.5523 20 16 20C15.4477 20 15 19.5523 15 19V18H4V24C4 25.1046 4.89543 26 6 26H26C27.1046 26 28 25.1046 28 24V18Z" />
</symbol>
<symbol id="testResult" viewBox="0 0 32 32">
<path d="M8 4.00012H22C23.1046 4.00012 24 4.89555 24 6.00012V19.0001C24 19.5524 24.4477 20.0001 25 20.0001C25.5523 20.0001 26 19.5524 26 19.0001V6.00012C26 3.79098 24.2091 2.00012 22 2.00012H8C5.79086 2.00012 4 3.79098 4 6.00012V8.00012H3C2.44772 8.00012 2 8.44784 2 9.00012C2 9.55241 2.44772 10.0001 3 10.0001H4V15.0001H3C2.44772 15.0001 2 15.4478 2 16.0001C2 16.5524 2.44772 17.0001 3 17.0001H4V22.0001H3C2.44772 22.0001 2 22.4478 2 23.0001C2 23.5524 2.44772 24.0001 3 24.0001H4V26.0001C4 28.2093 5.79086 30.0001 8 30.0001H15C15.5523 30.0001 16 29.5524 16 29.0001C16 28.4478 15.5523 28.0001 15 28.0001H8C6.89543 28.0001 6 27.1047 6 26.0001V24.0001H7C7.55228 24.0001 8 23.5524 8 23.0001C8 22.4478 7.55228 22.0001 7 22.0001H6V17.0001H7C7.55228 17.0001 8 16.5524 8 16.0001C8 15.4478 7.55228 15.0001 7 15.0001H6V10.0001H7C7.55228 10.0001 8 9.55241 8 9.00012C8 8.44784 7.55228 8.00012 7 8.00012H6V6.00012C6 4.89555 6.89543 4.00012 8 4.00012Z" />
<path d="M19 8.00049H11C10.4477 8.00049 10 8.4482 10 9.00049C10 9.55277 10.4477 10.0005 11 10.0005H19C19.5523 10.0005 20 9.55277 20 9.00049C20 8.4482 19.5523 8.00049 19 8.00049Z" />
<path d="M11 13.0005H19C19.5523 13.0005 20 13.4482 20 14.0005C20 14.5528 19.5523 15.0005 19 15.0005H11C10.4477 15.0005 10 14.5528 10 14.0005C10 13.4482 10.4477 13.0005 11 13.0005Z" />
<path d="M11 18.0005H19C19.5523 18.0005 20 18.4482 20 19.0005C20 19.5528 19.5523 20.0005 19 20.0005H11C10.4477 20.0005 10 19.5528 10 19.0005C10 18.4482 10.4477 18.0005 11 18.0005Z" />
<path d="M23 27.2005L20.4 24.6005L19 26.0005L23 30.0005L30 23.0005L28.6 21.6005L23 27.2005Z" />
</symbol>
<symbol id="testCase" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 4C8.89543 4 8 4.89543 8 6V26C8 27.1046 8.89543 28 10 28H22C23.1046 28 24 27.1046 24 26V12H20C17.7909 12 16 10.2091 16 8V4H10ZM18 4.41421V8C18 9.10457 18.8954 10 20 10H23.5858L18 4.41421ZM6 6C6 3.79086 7.79086 2 10 2H17.5858C18.1162 2 18.6249 2.21071 19 2.58579L25.4142 9C25.7893 9.37507 26 9.88378 26 10.4142V26C26 28.2091 24.2091 30 22 30H10C7.79086 30 6 28.2091 6 26V6ZM10 17C10 16.4477 10.4477 16 11 16H15C15.5523 16 16 16.4477 16 17C16 17.5523 15.5523 18 15 18H11C10.4477 18 10 17.5523 10 17ZM10 21C10 20.4477 10.4477 20 11 20H21C21.5523 20 22 20.4477 22 21C22 21.5523 21.5523 22 21 22H11C10.4477 22 10 21.5523 10 21Z" />
</symbol>
<symbol id="check" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 6H8C6.89543 6 6 6.89543 6 8V24C6 25.1046 6.89543 26 8 26H24C25.1046 26 26 25.1046 26 24V8C26 6.89543 25.1046 6 24 6ZM8 4C5.79086 4 4 5.79086 4 8V24C4 26.2091 5.79086 28 8 28H24C26.2091 28 28 26.2091 28 24V8C28 5.79086 26.2091 4 24 4H8Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.7071 10.2929C24.0976 10.6834 24.0976 11.3166 23.7071 11.7071L14.7071 20.7071C14.3166 21.0976 13.6834 21.0976 13.2929 20.7071L9.29289 16.7071C8.90237 16.3166 8.90237 15.6834 9.29289 15.2929C9.68342 14.9024 10.3166 14.9024 10.7071 15.2929L14 18.5858L22.2929 10.2929C22.6834 9.90237 23.3166 9.90237 23.7071 10.2929Z" />
</symbol>
<symbol id="filter" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.3,4.2C2.4,3.9,2.7,3.8,3,3.8h18c0.3,0,0.6,0.2,0.7,0.4c0.1,0.3,0.1,0.6-0.1,0.8l-7,8.2V21c0,0.3-0.1,0.5-0.4,0.6c-0.2,0.1-0.5,0.2-0.7,0l-3.6-1.8c-0.3-0.1-0.4-0.4-0.4-0.7v-6L2.4,5C2.2,4.8,2.2,4.5,2.3,4.2z M4.6,5.2l6.1,7.2c0.1,0.1,0.2,0.3,0.2,0.5v5.8l2.1,1v-6.9c0-0.2,0.1-0.4,0.2-0.5l6.1-7.2H4.6z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

+21 -2
View File
@@ -61,6 +61,25 @@
"TestRunNamePlaceholder": "Název testovacího běhu",
"SelectTestSuites": "Vyberte testovací sady",
"SelectTestCases": "Vyberte testovací případy",
"TestLibrary": "Testovací knihovna"
"TestLibrary": "Testovací knihovna",
"TestResult": "Výsledek",
"TestRunResult": "Výsledek",
"TestRunStatus": "Stav",
"StatusNonTested": "Neprovedeno",
"StatusBlocked": "Blokováno",
"StatusPassed": "Prošlo",
"StatusFailed": "Neprošlo",
"SelectTestCase": "Vyberte testovací případ",
"Save": "Uložit",
"SaveAndNext": "Uložit a další",
"DonePercent": "% Hotovo",
"TestResults": "Výsledky",
"TestRunName": "Název",
"DueDate": "Termín",
"RunAllTestCases": "Spustit všechny testovací případy",
"RunSelectedTestCases": "Spustit vybrané testovací případy",
"RunFilteredTestCases": "Spustit testovací případy odpovídající filtru",
"TestCaseDescription": "Popis testovacího případu",
"TestResultAttributes": "Výsledek"
}
}
}
+21 -2
View File
@@ -57,10 +57,29 @@
"AssignedTo": "Assigneed to",
"PreviousAssigned": "Previously assigned",
"NoTestCases": "There are no test cases in this test suite",
"CreateTestRun": "Create test run",
"CreateTestRun": "New test run",
"TestRunNamePlaceholder": "Test run name",
"SelectTestSuites": "Select test suites",
"SelectTestCases": "Select test cases",
"TestLibrary": "Test library"
"TestLibrary": "Test library",
"TestResult": "Result",
"TestRunResult": "Result",
"TestRunStatus": "Status",
"StatusNonTested": "Untested",
"StatusBlocked": "Blocked",
"StatusPassed": "Passed",
"StatusFailed": "Failed",
"SelectTestCase": "Select test case",
"Save": "Save",
"SaveAndNext": "Save and next",
"DonePercent": "% Done",
"TestResults": "Results",
"TestRunName": "Name",
"DueDate": "Due date",
"RunAllTestCases": "Run all test cases",
"RunSelectedTestCases": "Run selected test cases",
"RunFilteredTestCases": "Run test cases matching a filter",
"TestCaseDescription": "Test case description",
"TestResultAttributes": "Result"
}
}
@@ -0,0 +1,85 @@
{
"string": {
"ConfigLabel": "Gestión de Pruebas",
"ConfigDescription": "Extensión para gestionar casos de prueba",
"TestCaseType": "Tipo",
"TestCasePriority": "Prioridad",
"TestCaseStatus": "Estado",
"TestSuite": "Suite de Pruebas",
"SuiteName": "Nombre",
"SuiteDescription": "Descripción",
"Suite": "Suite",
"TestName": "Nombre",
"TestDescription": "Descripción",
"TestType": "Tipo",
"TestPriority": "Prioridad",
"TestStatus": "Estado",
"TestEstimatedTime": "Tiempo estimado",
"TestPreconditions": "Precondiciones",
"TestSteps": "Pasos",
"TestAssignee": "Asignado a",
"TestCase": "Caso de prueba",
"TestProject": "Proyecto",
"TestManagementApplication": "Gestión de Pruebas",
"AllTestCases": "Todos los casos de prueba",
"AllProjects": "Todos los proyectos",
"Projects": "Proyectos",
"CreateProject": "Crear proyecto",
"EditProject": "Editar proyecto",
"TestCases": "Casos de prueba",
"TestManagementDescription": "Extensión para gestionar casos de prueba",
"CreateTestCase": "Nuevo caso de prueba",
"FullDescription": "Descripción completa",
"ProjectName": "Nombre del proyecto",
"ProjectType": "Tipo de proyecto",
"Members": "Miembros",
"RoleLabel": "Rol",
"ProjectMembers": "Miembros del proyecto",
"TestSuites": "Suites de pruebas",
"CreateTestSuite": "Crear suite de pruebas",
"NamePlaceholder": "Nombre de la suite",
"DescriptionPlaceholder": "Descripción (opcional)",
"TestRuns": "Ejecuciones de prueba",
"NewTestRun": "Nueva ejecución de prueba",
"TestRun": "Ejecución de prueba",
"TestNamePlaceholder": "Título del caso de prueba",
"ChooseIcon": "Elegir ícono",
"NoTestSuite": "No hay suite de pruebas",
"StatusDraft": "Borrador",
"StatusReview": "Listo para revisión",
"StatusReviewComments": "Necesita corregir comentarios de revisión",
"StatusApproved": "Aprobado",
"StatusRejected": "Rechazado",
"SetStatus": "Establecer estado",
"Assignee": "Asignado a",
"Unassigned": "No asignado",
"AssignTo": "Asignar a",
"AssignedTo": "Asignado a",
"PreviousAssigned": "Asignado previamente",
"NoTestCases": "No hay casos de prueba en esta suite de pruebas",
"CreateTestRun": "Nueva ejecución de prueba",
"TestRunNamePlaceholder": "Nombre de la ejecución de prueba",
"SelectTestSuites": "Seleccionar suites de prueba",
"SelectTestCases": "Seleccionar casos de prueba",
"TestLibrary": "Biblioteca de pruebas",
"TestResult": "Resultado",
"TestRunResult": "Resultado",
"TestRunStatus": "Estado",
"StatusNonTested": "No probado",
"StatusBlocked": "Bloqueado",
"StatusPassed": "Aprobado",
"StatusFailed": "Fallido",
"SelectTestCase": "Seleccionar caso de prueba",
"Save": "Guardar",
"SaveAndNext": "Guardar y siguiente",
"DonePercent": "% Completado",
"TestResults": "Resultados",
"TestRunName": "Nombre",
"DueDate": "Fecha de vencimiento",
"RunAllTestCases": "Ejecutar todos los casos de prueba",
"RunSelectedTestCases": "Ejecutar casos de prueba seleccionados",
"RunFilteredTestCases": "Ejecutar casos de prueba que coincidan con un filtro",
"TestCaseDescription": "Descripción del caso de prueba",
"TestResultAttributes": "Resultado"
}
}
+20 -1
View File
@@ -61,6 +61,25 @@
"TestRunNamePlaceholder": "Nom de l'exécution du test",
"SelectTestSuites": "Sélectionnez les suites de tests",
"SelectTestCases": "Sélectionnez des cas de test",
"TestLibrary": "Bibliothèque de tests"
"TestLibrary": "Bibliothèque de tests",
"TestResult": "Résultat",
"TestRunResult": "Résultat",
"TestRunStatus": "Statut",
"StatusNonTested": "Non testé",
"StatusBlocked": "Bloqué",
"StatusPassed": "Réussi",
"StatusFailed": "Échoué",
"SelectTestCase": "Sélectionnez le cas de test",
"Save": "Sauvegarder",
"SaveAndNext": "Sauvegarder et suivant",
"DonePercent": "% Fait",
"TestResults": "Résultats",
"TestRunName": "Nom",
"DueDate": "Date d'échéance",
"RunAllTestCases": "Exécuter tous les cas de test",
"RunSelectedTestCases": "Exécuter des scénarios de test sélectionnés",
"RunFilteredTestCases": "Exécuter des cas de test correspondant à un filtre",
"TestCaseDescription": "Description du cas de test",
"TestResultAttributes": "Résultat"
}
}
@@ -0,0 +1,85 @@
{
"string": {
"ConfigLabel": "Gestione Test",
"ConfigDescription": "Estensione per gestire i casi di test",
"TestCaseType": "Tipo",
"TestCasePriority": "Priorità",
"TestCaseStatus": "Stato",
"TestSuite": "Suite di Test",
"SuiteName": "Nome",
"SuiteDescription": "Descrizione",
"Suite": "Suite",
"TestName": "Nome",
"TestDescription": "Descrizione",
"TestType": "Tipo",
"TestPriority": "Priorità",
"TestStatus": "Stato",
"TestEstimatedTime": "Tempo stimato",
"TestPreconditions": "Precondizioni",
"TestSteps": "Passaggi",
"TestAssignee": "Assegnatario",
"TestCase": "Caso di test",
"TestProject": "Progetto",
"TestManagementApplication": "Gestione Test",
"AllTestCases": "Tutti i casi di test",
"AllProjects": "Tutti i progetti",
"Projects": "Progetti",
"CreateProject": "Crea progetto",
"EditProject": "Modifica progetto",
"TestCases": "Casi di test",
"TestManagementDescription": "Estensione per gestire i casi di test",
"CreateTestCase": "Nuovo caso di test",
"FullDescription": "Descrizione completa",
"ProjectName": "Nome",
"ProjectType": "Tipo",
"Members": "Membri",
"RoleLabel": "Ruolo",
"ProjectMembers": "Membri del progetto",
"TestSuites": "Suite di test",
"CreateTestSuite": "Crea suite di test",
"NamePlaceholder": "Nome della suite",
"DescriptionPlaceholder": "Descrizione (opzionale)",
"TestRuns": "Esecuzioni di test",
"NewTestRun": "Nuova esecuzione di test",
"TestRun": "Esecuzione di test",
"TestNamePlaceholder": "Titolo del caso di test",
"ChooseIcon": "Scegli icona",
"NoTestSuite": "Nessuna suite di test",
"StatusDraft": "Bozza",
"StatusReview": "Pronto per la revisione",
"StatusReviewComments": "Necessita di correggere i commenti della revisione",
"StatusApproved": "Approvato",
"StatusRejected": "Respinto",
"SetStatus": "Imposta stato",
"Assignee": "Assegnatario",
"Unassigned": "Non assegnato",
"AssignTo": "Assegna a",
"AssignedTo": "Assegnato a",
"PreviousAssigned": "Assegnato in precedenza",
"NoTestCases": "Non ci sono casi di test in questa suite di test",
"CreateTestRun": "Nuova esecuzione di test",
"TestRunNamePlaceholder": "Nome dell'esecuzione di test",
"SelectTestSuites": "Seleziona suite di test",
"SelectTestCases": "Seleziona casi di test",
"TestLibrary": "Libreria di test",
"TestResult": "Risultato",
"TestRunResult": "Risultato",
"TestRunStatus": "Stato",
"StatusNonTested": "Non testato",
"StatusBlocked": "Bloccato",
"StatusPassed": "Superato",
"StatusFailed": "Fallito",
"SelectTestCase": "Seleziona caso di test",
"Save": "Salva",
"SaveAndNext": "Salva e successivo",
"DonePercent": "% Completato",
"TestResults": "Risultati",
"TestRunName": "Nome",
"DueDate": "Data di scadenza",
"RunAllTestCases": "Esegui tutti i casi di test",
"RunSelectedTestCases": "Esegui i casi di test selezionati",
"RunFilteredTestCases": "Esegui i casi di test che corrispondono a un filtro",
"TestCaseDescription": "Descrizione del caso di test",
"TestResultAttributes": "Risultato"
}
}
@@ -0,0 +1,85 @@
{
"string": {
"ConfigLabel": "Gestão de Testes",
"ConfigDescription": "Extensão para gerenciar casos de teste",
"TestCaseType": "Tipo",
"TestCasePriority": "Prioridade",
"TestCaseStatus": "Status",
"TestSuite": "Suite de Testes",
"SuiteName": "Nome",
"SuiteDescription": "Descrição",
"Suite": "Suite",
"TestName": "Nome",
"TestDescription": "Descrição",
"TestType": "Tipo",
"TestPriority": "Prioridade",
"TestStatus": "Status",
"TestEstimatedTime": "Tempo estimado",
"TestPreconditions": "Precondições",
"TestSteps": "Passos",
"TestAssignee": "Responsável",
"TestCase": "Caso de teste",
"TestProject": "Projeto",
"TestManagementApplication": "Gestão de Testes",
"AllTestCases": "Todos os casos de teste",
"AllProjects": "Todos os projetos",
"Projects": "Projetos",
"CreateProject": "Criar projeto",
"EditProject": "Editar projeto",
"TestCases": "Casos de teste",
"TestManagementDescription": "Extensão para gerenciar casos de teste",
"CreateTestCase": "Novo caso de teste",
"FullDescription": "Descrição completa",
"ProjectName": "Nome",
"ProjectType": "Tipo",
"Members": "Membros",
"RoleLabel": "Função",
"ProjectMembers": "Membros do projeto",
"TestSuites": "Suites de teste",
"CreateTestSuite": "Criar suite de teste",
"NamePlaceholder": "Nome da suite",
"DescriptionPlaceholder": "Descrição (opcional)",
"TestRuns": "Execuções de teste",
"NewTestRun": "Nova execução de teste",
"TestRun": "Execução de teste",
"TestNamePlaceholder": "Título do caso de teste",
"ChooseIcon": "Escolher ícone",
"NoTestSuite": "Nenhuma suite de teste",
"StatusDraft": "Rascunho",
"StatusReview": "Pronto para revisão",
"StatusReviewComments": "Precisa corrigir comentários da revisão",
"StatusApproved": "Aprovado",
"StatusRejected": "Rejeitado",
"SetStatus": "Definir status",
"Assignee": "Responsável",
"Unassigned": "Não atribuído",
"AssignTo": "Atribuir a",
"AssignedTo": "Atribuído a",
"PreviousAssigned": "Atribuído anteriormente",
"NoTestCases": "Não há casos de teste nesta suite de teste",
"CreateTestRun": "Nova execução de teste",
"TestRunNamePlaceholder": "Nome da execução de teste",
"SelectTestSuites": "Selecionar suites de teste",
"SelectTestCases": "Selecionar casos de teste",
"TestLibrary": "Biblioteca de testes",
"TestResult": "Resultado",
"TestRunResult": "Resultado",
"TestRunStatus": "Status",
"StatusNonTested": "Não testado",
"StatusBlocked": "Bloqueado",
"StatusPassed": "Aprovado",
"StatusFailed": "Falhou",
"SelectTestCase": "Selecionar caso de teste",
"Save": "Salvar",
"SaveAndNext": "Salvar e próximo",
"DonePercent": "% Concluído",
"TestResults": "Resultados",
"TestRunName": "Nome",
"DueDate": "Data de vencimento",
"RunAllTestCases": "Executar todos os casos de teste",
"RunSelectedTestCases": "Executar casos de teste selecionados",
"RunFilteredTestCases": "Executar casos de teste que correspondem a um filtro",
"TestCaseDescription": "Descrição do caso de teste",
"TestResultAttributes": "Resultado"
}
}
+20 -1
View File
@@ -61,6 +61,25 @@
"TestRunNamePlaceholder": "Название тест плана",
"SelectTestSuites": "Выбрать наборы тестов",
"SelectTestCases": "Выбрать тест-кейсы",
"TestLibrary": "Библиотека тестов"
"TestLibrary": "Библиотека тестов",
"TestResult": "Результат",
"TestRunResult": "Результат",
"TestRunStatus": "Статус",
"StatusNonTested": "Не протестировано",
"StatusBlocked": "Заблокировано",
"StatusPassed": "Пройдено",
"StatusFailed": "Не пройдено",
"SelectTestCase": "Выбрать тест-кейс",
"Save": "Сохранить",
"SaveAndNext": "Сохранить и перейти к следующему",
"DonePercent": "% Выполнено",
"TestResults": "Результаты",
"TestRunName": "Название",
"DueDate": "Выполнить до",
"RunAllTestCases": "Выполнить все тест-кейсы",
"RunSelectedTestCases": "Выполнить выбранные тест-кейсы",
"RunFilteredTestCases": "Выполнить тест-кейсы, соответствующие фильтру",
"TestCaseDescription": "Описание тест-кейса",
"TestResultAttributes": "Результат"
}
}
+20 -1
View File
@@ -61,6 +61,25 @@
"TestRunNamePlaceholder": "測試運行名稱",
"SelectTestSuites": "選擇測試套件",
"SelectTestCases": "選擇測試用例",
"TestLibrary": "測試庫"
"TestLibrary": "測試庫",
"TestResult": "結果",
"TestRunResult": "結果",
"TestRunStatus": "狀態",
"StatusNonTested": "未測試",
"StatusBlocked": "被阻止",
"StatusPassed": "通過",
"StatusFailed": "失敗",
"SelectTestCase": "選擇測試用例",
"Save": "保存",
"SaveAndNext": "保存並繼續",
"DonePercent": "%完成",
"TestResults": "結果",
"TestRunName": "姓名",
"DueDate": "到期日",
"RunAllTestCases": "運行所有測試用例",
"RunSelectedTestCases": "運行選定的測試用例",
"RunFilteredTestCases": "運行與篩選器匹配的測試用例",
"TestCaseDescription": "測試用例描述",
"TestResultAttributes": "結果"
}
}
+8 -1
View File
@@ -36,5 +36,12 @@ loadMetadata(testManagement.icon, {
StatusReviewComments: `${icons}#status-review-comments`,
StatusApproved: `${icons}#status-approved`,
StatusRejected: `${icons}#status-canceled`,
TestLibrary: `${icons}#test-library`
TestLibrary: `${icons}#test-library`,
TestResult: `${icons}#testResult`,
StatusNonTested: `${icons}#status-draft`,
StatusBlocked: `${icons}#status-review-comments`,
StatusPassed: `${icons}#status-approved`,
StatusFailed: `${icons}#status-canceled`,
Filter: `${icons}#filter`,
Check: `${icons}#check`
})
@@ -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}
+23 -5
View File
@@ -28,9 +28,17 @@ import CreateTestRun from './components/test-run/CreateTestRun.svelte'
import TestCaseStatusPresenter from './components/test-case/TestCaseStatusPresenter.svelte'
import EditTestRun from './components/test-run/EditTestRun.svelte'
import TestRunPresenter from './components/test-run/TestRunPresenter.svelte'
import RunSelectedTestsButton from './components/test-case/RunSelectedTestsButton.svelte'
import TestResultStatusPresenter from './components/test-result/TestResultStatusPresenter.svelte'
import TestResultStatusEditor from './components/test-result/TestResultStatusEditor.svelte'
import TestRunResult from './components/test-run/TestRunResult.svelte'
import TestResultPresenter from './components/test-result/TestResultPresenter.svelte'
import EditTestResult from './components/test-result/EditTestResult.svelte'
import TestResultHeader from './components/test-result/TestResultHeader.svelte'
import TestResultFooter from './components/test-result/TestResultFooter.svelte'
import { CreateChildTestSuiteAction, EditTestSuiteAction } from './utils'
import { resolveLocation, getTestSuiteLink } from './navigation'
import { CreateChildTestSuiteAction, EditTestSuiteAction, RunSelectedTestsAction } from './utils'
import { resolveLocation, getAttachedObjectLink } from './navigation'
export default async (): Promise<Resources> => ({
component: {
@@ -47,16 +55,26 @@ export default async (): Promise<Resources> => ({
TestCaseStatusPresenter,
EditTestRun,
TestRunPresenter,
TestSuiteRefPresenter
TestSuiteRefPresenter,
RunSelectedTestsButton,
TestResultStatusPresenter,
TestResultStatusEditor,
TestRunResult,
TestResultPresenter,
EditTestResult,
TestResultHeader,
TestResultFooter
},
function: {
GetTestSuiteLink: getTestSuiteLink
GetTestSuiteLink: getAttachedObjectLink,
GetTestRunLink: getAttachedObjectLink
},
resolver: {
Location: resolveLocation
},
actionImpl: {
CreateChildTestSuite: CreateChildTestSuiteAction,
EditTestSuite: EditTestSuiteAction
EditTestSuite: EditTestSuiteAction,
RunSelectedTests: RunSelectedTestsAction
}
})
@@ -61,14 +61,9 @@ async function generateProjectLocation (
}
}
export function getTestSuiteLink (testSuite: Ref<TestSuite>): Location {
export function getAttachedObjectLink (parentDoc: Ref<Doc>): Location {
const loc = getCurrentResolvedLocation()
loc.query =
testSuite === undefined
? undefined
: {
[SUITE_KEY]: testSuite
}
loc.query = parentDoc === undefined ? undefined : { attachedTo: parentDoc }
return loc
}
@@ -84,6 +79,16 @@ export function getTestSuiteIdFromLocation (): Ref<TestSuite> {
return (location?.query?.[SUITE_KEY] as Ref<TestSuite>) ?? testManagement.ids.NoParent
}
export function getTestRunsLink (parentDoc: Ref<Doc>): Location {
const loc = getCurrentResolvedLocation()
loc.path.length = 4
loc.path[3] = 'testRuns'
loc.fragment = undefined
loc.query = parentDoc === undefined ? undefined : { attachedTo: parentDoc }
return loc
}
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
if (loc.path[2] !== testManagementId) {
return undefined
@@ -0,0 +1,65 @@
//
// 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 Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import testManagement, { type TestRun, type TestCase, TestRunStatus } from '@hcengineering/test-management'
export async function getTestCases (objectId: Ref<TestRun>): Promise<TestCase[]> {
if (objectId === undefined) {
return []
}
const client = getClient()
const testResults = await client.findAll(testManagement.class.TestResult, { attachedTo: objectId })
const testCaseIds = testResults.map((testResult) => testResult.testCase)
return await client.findAll(testManagement.class.TestCase, { _id: { $in: testCaseIds } })
}
export interface TestRunStats {
readonly done: number
readonly untested: number
readonly blocked: number
readonly completed: number
readonly failed: number
}
async function getTestResultCount (objectId: Ref<TestRun>, status: TestRunStatus): Promise<number> {
const client = getClient()
const testResults = await client.findAll(
testManagement.class.TestResult,
{
attachedTo: objectId,
status
},
{ limit: 0, total: true }
)
return testResults.total > 0 ? testResults.total : 0
}
export async function getTestRunStats (objectId: Ref<TestRun>): Promise<TestRunStats> {
const untested = await getTestResultCount(objectId, TestRunStatus.Untested)
const blocked = await getTestResultCount(objectId, TestRunStatus.Blocked)
const completed = await getTestResultCount(objectId, TestRunStatus.Passed)
const failed = await getTestResultCount(objectId, TestRunStatus.Failed)
const total = untested + blocked + completed + failed
const done = total > 0 ? ((total - untested) * 100) / total : 0
return {
done,
untested,
blocked,
completed,
failed
}
}
+29 -1
View File
@@ -14,7 +14,7 @@
//
import { type Asset, type IntlString } from '@hcengineering/platform'
import testManagement, { TestCaseStatus } from '@hcengineering/test-management'
import testManagement, { TestCaseStatus, TestRunStatus } from '@hcengineering/test-management'
/** @public */
export const defaultTestCaseStatuses = [
@@ -39,3 +39,31 @@ export const testCaseStatusAssets: Record<TestCaseStatus, { icon: Asset, label:
[TestCaseStatus.Approved]: { icon: testManagement.icon.StatusApproved, label: testManagement.string.StatusApproved },
[TestCaseStatus.Rejected]: { icon: testManagement.icon.StatusRejected, label: testManagement.string.StatusRejected }
}
/** @public */
export const defaultTestRunStatuses = [
TestRunStatus.Untested,
TestRunStatus.Blocked,
TestRunStatus.Passed,
TestRunStatus.Failed
]
/** @public */
export const testRunStatusAssets: Record<TestRunStatus, { icon: Asset, label: IntlString }> = {
[TestRunStatus.Untested]: {
icon: testManagement.icon.StatusNonTested,
label: testManagement.string.StatusNonTested
},
[TestRunStatus.Blocked]: {
icon: testManagement.icon.StatusBlocked,
label: testManagement.string.StatusBlocked
},
[TestRunStatus.Passed]: {
icon: testManagement.icon.StatusPassed,
label: testManagement.string.StatusPassed
},
[TestRunStatus.Failed]: {
icon: testManagement.icon.StatusFailed,
label: testManagement.string.StatusFailed
}
}
+19 -31
View File
@@ -13,9 +13,7 @@
// limitations under the License.
//
import { type Contact } from '@hcengineering/contact'
import core, { type Doc, type Ref, type TxCreateDoc, type TxUpdateDoc } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type { Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { showPopup } from '@hcengineering/ui'
import { type TestProject, type TestCase, type TestSuite } from '@hcengineering/test-management'
@@ -23,34 +21,7 @@ import CreateTestSuiteComponent from './components/test-suite/CreateTestSuite.sv
import EditTestSuiteComponent from './components/test-suite/EditTestSuite.svelte'
import CreateTestCase from './components/test-case/CreateTestCase.svelte'
import CreateProject from './components/project/CreateProject.svelte'
export async function getPreviousAssignees (objectId: Ref<Doc> | undefined): Promise<Array<Ref<Contact>>> {
if (objectId === undefined) {
return []
}
const client = getClient()
const createTx = (
await client.findAll<TxCreateDoc<TestCase>>(core.class.TxCreateDoc, {
objectId: objectId as Ref<TestCase>
})
)[0]
const updateTxes = await client.findAll<TxUpdateDoc<TestCase>>(
core.class.TxUpdateDoc,
{ objectId: objectId as Ref<TestCase>, 'operations.assignee': { $exists: true } },
{ sort: { modifiedOn: -1 } }
)
const set = new Set<Ref<Contact>>()
const createAssignee = createTx?.attributes?.assignee
for (const tx of updateTxes) {
const assignee = tx.operations.assignee
if (assignee == null) continue
set.add(assignee)
}
if (createAssignee != null) {
set.add(createAssignee)
}
return Array.from(set)
}
import CreateTestRun from './components/test-run/CreateTestRun.svelte'
export async function showCreateTestSuitePopup (
space: Ref<TestProject> | undefined,
@@ -71,6 +42,14 @@ export async function showCreateProjectPopup (): Promise<void> {
showPopup(CreateProject, {}, 'top')
}
export async function showCreateTestRunPopup (options: {
testCases?: TestCase[]
query?: DocumentQuery<Doc>
space: Ref<TestProject>
}): Promise<void> {
showPopup(CreateTestRun, options, 'top')
}
export async function CreateChildTestSuiteAction (doc: TestSuite): Promise<void> {
await showCreateTestSuitePopup(doc.space, doc._id)
}
@@ -78,3 +57,12 @@ export async function CreateChildTestSuiteAction (doc: TestSuite): Promise<void>
export async function EditTestSuiteAction (doc: TestSuite): Promise<void> {
await showEditTestSuitePopup(doc._id)
}
export async function RunSelectedTestsAction (testCases: TestCase[]): Promise<void> {
if (testCases?.length > 0) {
const space = testCases[0].space
await showCreateTestRunPopup({ testCases, space })
} else {
console.error('No test cases selected')
}
}
+26
View File
@@ -0,0 +1,26 @@
//
// 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.
//
export enum TestManagementEvents {
TestCaseCreated = 'testManagement.TestCaseCreated',
TestCaseEdited = 'testManagement.TestCaseEdited',
TestCaseDeleted = 'testManagement.TestCaseDeleted',
TestSuiteCreated = 'testManagement.TestSuiteCreated',
TestSuiteEdited = 'testManagement.TestSuiteEdited',
TestSuiteDeleted = 'testManagement.TestSuiteDeleted',
TestRunCreated = 'testManagement.TestRunCreated',
TestRunEdited = 'testManagement.TestRunEdited',
TestRunDeleted = 'testManagement.TestRunDeleted'
}
+1
View File
@@ -16,6 +16,7 @@
import { testManagementId, testManagementPlugin } from './plugin'
export * from './types'
export * from './analytics'
export { testManagementId }
export default testManagementPlugin
+53 -15
View File
@@ -28,7 +28,7 @@ import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platfor
import { plugin } from '@hcengineering/platform'
import { type AnyComponent, type Location, type ResolvedLocation } from '@hcengineering/ui'
import { ActionCategory, Viewlet } from '@hcengineering/view'
import { Action, ActionCategory, ViewAction, Viewlet } from '@hcengineering/view'
import {
TestSuite,
TestCase,
@@ -37,8 +37,8 @@ import {
TestCasePriority,
TestCaseStatus,
TestRun,
TestRunItem,
TestRunResult
TestRunStatus,
TestResult
} from './types'
/** @public */
@@ -49,6 +49,17 @@ export const testManagementPlugin = plugin(testManagementId, {
app: {
TestManagement: '' as Ref<Doc>
},
action: {
DeleteTestCase: '' as Ref<Action<Doc, any>>,
CreateChildTestSuite: '' as Ref<Action>,
EditTestSuite: '' as Ref<Action>,
RunSelectedTests: '' as Ref<Action>
},
actionImpl: {
CreateChildTestSuite: '' as ViewAction,
EditTestSuite: '' as ViewAction,
RunSelectedTests: '' as ViewAction
},
icon: {
TestManagement: '' as Asset,
TestManagementVersion: '' as Asset,
@@ -68,7 +79,14 @@ export const testManagementPlugin = plugin(testManagementId, {
StatusApproved: '' as Asset,
StatusRejected: '' as Asset,
Document: '' as Asset,
TestLibrary: '' as Asset
TestLibrary: '' as Asset,
TestResult: '' as Asset,
StatusNonTested: '' as Asset,
StatusBlocked: '' as Asset,
StatusPassed: '' as Asset,
StatusFailed: '' as Asset,
Filter: '' as Asset,
Check: '' as Asset
},
class: {
TestCase: '' as Ref<Class<TestCase>>,
@@ -78,8 +96,8 @@ export const testManagementPlugin = plugin(testManagementId, {
TypeTestCasePriority: '' as Ref<Class<Type<TestCasePriority>>>,
TypeTestCaseStatus: '' as Ref<Class<Type<TestCaseStatus>>>,
TestRun: '' as Ref<Class<TestRun>>,
TestRunItem: '' as Ref<Class<TestRunItem>>,
TypeTestRunResult: '' as Ref<Class<Type<TestRunResult>>>
TypeTestRunStatus: '' as Ref<Class<Type<TestRunStatus>>>,
TestResult: '' as Ref<Class<TestResult>>
},
descriptors: {
ProjectType: '' as Ref<SpaceTypeDescriptor>
@@ -150,14 +168,28 @@ export const testManagementPlugin = plugin(testManagementId, {
TestRunName: '' as IntlString,
NoTestCases: '' as IntlString,
DueDate: '' as IntlString,
TestRunItems: '' as IntlString,
TestRunResult: '' as IntlString,
TestRunItem: '' as IntlString,
TestRunStatus: '' as IntlString,
TestRunNamePlaceholder: '' as IntlString,
TestRunResult: '' as IntlString,
SelectTestSuites: '' as IntlString,
SelectTestCases: '' as IntlString,
CreateTestRun: '' as IntlString,
TestLibrary: '' as IntlString
TestLibrary: '' as IntlString,
TestResult: '' as IntlString,
StatusNonTested: '' as IntlString,
StatusBlocked: '' as IntlString,
StatusPassed: '' as IntlString,
StatusFailed: '' as IntlString,
SelectTestCase: '' as IntlString,
Save: '' as IntlString,
SaveAndNext: '' as IntlString,
DonePercent: '' as IntlString,
TestResults: '' as IntlString,
RunAllTestCases: '' as IntlString,
RunSelectedTestCases: '' as IntlString,
RunFilteredTestCases: '' as IntlString,
TestCaseDescription: '' as IntlString,
TestResultAttributes: '' as IntlString
},
category: {
TestManagement: '' as Ref<ActionCategory>
@@ -171,7 +203,12 @@ export const testManagementPlugin = plugin(testManagementId, {
PriorityIconPresenter: '' as AnyComponent,
TestCaseStatusPresenter: '' as AnyComponent,
TestSuites: '' as AnyComponent,
CreateTestSuite: '' as AnyComponent
CreateTestSuite: '' as AnyComponent,
TestRunFromSelection: '' as AnyComponent,
TestResultStatusPresenter: '' as AnyComponent,
TestResultStatusEditor: '' as AnyComponent,
TestRunResult: '' as AnyComponent,
TestResultHeader: '' as AnyComponent
},
ids: {
NoParent: '' as Ref<TestSuite>,
@@ -193,9 +230,9 @@ export const testManagementPlugin = plugin(testManagementId, {
viewlet: {
TableTestCase: '' as Ref<Viewlet>,
TableTestSuites: '' as Ref<Viewlet>,
TableTestRun: '' as Ref<Viewlet>,
SuiteTestCases: '' as Ref<Viewlet>,
ListTestCase: '' as Ref<Viewlet>
ListTestCase: '' as Ref<Viewlet>,
TestResultList: '' as Ref<Viewlet>,
TableTestResult: '' as Ref<Viewlet>
},
testCaseTypeStatus: {
Draft: '' as Ref<Status>,
@@ -207,7 +244,8 @@ export const testManagementPlugin = plugin(testManagementId, {
TestCase: '' as Ref<TestCase>
},
function: {
GetTestSuiteLink: '' as Resource<(doc: Ref<Doc>) => Location>
GetTestSuiteLink: '' as Resource<(doc: Ref<Doc>) => Location>,
GetTestRunLink: '' as Resource<(doc: Ref<Doc>) => Location>
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
+12 -6
View File
@@ -103,20 +103,26 @@ export interface TestRun extends Doc {
name: string
description: CollaborativeDoc
dueDate?: Timestamp
items?: CollectionSize<TestRunItem>
results?: CollectionSize<TestResult>
}
/** @public */
export enum TestRunResult {
Passed,
export enum TestRunStatus {
Untested,
Blocked,
Passed,
Failed
}
// TODO: Refactor to associations
/** @public */
export interface TestRunItem extends AttachedDoc {
testRun: Ref<TestRun>
export interface TestResult extends AttachedDoc<TestRun, 'results', TestProject> {
name: string
testCase: Ref<TestCase>
result?: TestRunResult
testSuite?: Ref<TestSuite>
status?: TestRunStatus
description: CollaborativeDoc
assignee?: Ref<Employee>
attachments?: CollectionSize<Attachment>
comments?: number
}
@@ -0,0 +1,183 @@
// 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 Doc } from '@hcengineering/core'
import { FoldersManager, emptyFoldersState, type FoldersState } from '../components/folders/store/folderUtils'
describe('foldersManager', () => {
test('folders tree', () => {
const docs: Doc[] = [
{
name: 'Child 1',
description: '',
parent: '673ef344595d5369f68ced98',
_class: 'testManagement:class:TestSuite',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1732178763949,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1732178763949,
docUpdateMessages: 1,
_id: '673ef34b595d5369f68ceda2',
space: '673434ed74993781b4195e02'
},
{
name: 'Child 2',
description: '',
parent: '673ef344595d5369f68ced98',
_class: 'testManagement:class:TestSuite',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1732178770252,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1732178770252,
docUpdateMessages: 1,
_id: '673ef352595d5369f68cedac',
space: '673434ed74993781b4195e02'
},
{
_id: '673434f674993781b4195e1b',
name: 'New suite',
description: '',
parent: 'testManagement:ids:NoParent',
_class: 'testManagement:class:TestSuite',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1731495620083,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1731474678014,
docUpdateMessages: 2,
testCases: 1,
space: '673434ed74993781b4195e02'
},
{
name: 'Parent',
description: '',
parent: 'testManagement:ids:NoParent',
_class: 'testManagement:class:TestSuite',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1732178756247,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1732178756247,
docUpdateMessages: 1,
_id: '673ef344595d5369f68ced98',
space: '673434ed74993781b4195e02'
},
{
_id: '6734863403ae9eca5b01e4fd',
name: 'test',
description: '',
parent: 'testManagement:ids:NoParent',
_class: 'testManagement:class:TestSuite',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1731495529218,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1731495476678,
docUpdateMessages: 4,
testCases: 3,
space: '673434ed74993781b4195e02'
}
] as any
const foldersManager = new FoldersManager('name', 'parent', 'testManagement:ids:NoParent' as any, false, () => {})
const foldersState = foldersManager.getNestedStructure(docs)
expect(foldersState.folders.length).toBe(3)
expect(foldersState.folderById.size).toBe(5)
expect(foldersState.descendants.get('673ef344595d5369f68ced98' as any)?.length).toBe(2)
})
test('plain list', () => {
const docs: Doc[] = [
{
_id: '6739541d04aed0096599e03a',
name: '3 items',
description: '6739541704aed0096599e032%description:HEAD:0',
_class: 'testManagement:class:TestRun',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1731810333365,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1731810333307,
docUpdateMessages: 4,
testCases: 3,
space: '673434ed74993781b4195e02'
},
{
name: 'Test run 1',
description: '673ef510595d5369f68cee0a%description:HEAD:0',
_class: 'testManagement:class:TestRun',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1732179220778,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1732179220778,
testCases: 2,
docUpdateMessages: 3,
_id: '673ef514595d5369f68cee12',
space: '673434ed74993781b4195e02'
},
{
name: 'Test run 2',
description: '673ef516595d5369f68cee18%description:HEAD:0',
_class: 'testManagement:class:TestRun',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1732179227645,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1732179227611,
testCases: 2,
docUpdateMessages: 3,
_id: '673ef51b595d5369f68cee20',
space: '673434ed74993781b4195e02'
}
] as any
const foldersManager = new FoldersManager('name', 'parent', 'noParent' as any, true, () => {})
const foldersState = foldersManager.getPlainList(docs)
expect(foldersState.folders.length).toBe(3)
expect(foldersState.folderById.size).toBe(3)
expect(foldersState.descendants.size).toBe(0)
})
test('test state update', () => {
const docs: Doc[] = [
{
_id: '673434f674993781b4195e1b',
name: 'New suite',
description: '',
parent: 'testManagement:ids:NoParent',
_class: 'testManagement:class:TestSuite',
modifiedBy: '67286f072788ebca822ea1b2',
modifiedOn: 1731495620083,
createdBy: '67286f072788ebca822ea1b2',
createdOn: 1731474678014,
docUpdateMessages: 2,
testCases: 1,
space: '673434ed74993781b4195e02'
}
] as any
let folderState = emptyFoldersState()
expect(folderState.folders.length).toBe(0)
expect(folderState.descendants.size).toBe(0)
expect(folderState.folderById.size).toBe(0)
const foldersManager = new FoldersManager(
'name',
'parent',
'testManagement:ids:NoParent' as any,
false,
(state: FoldersState) => {
folderState = state
}
)
foldersManager.setFolders(docs)
expect(folderState.folders.length).toBe(1)
expect(folderState.folderById.size).toBe(1)
})
})
@@ -19,7 +19,7 @@
import { Action, IconEdit } from '@hcengineering/ui'
import { getResource } from '@hcengineering/platform'
import { TreeItem, getActions as getContributedActions } from '../index'
import { TreeItem, getActions as getContributedActions } from '../../index'
export let folders: Ref<Doc>[]
export let folderById: Map<Ref<Doc>, Doc>
@@ -20,9 +20,9 @@
import { getResource, type Resource } from '@hcengineering/platform'
import { IntlString, Asset } from '@hcengineering/platform'
import { FoldersManager, FoldersStore, FoldersState } from '../stores/folderStore'
import { FoldersStore, FoldersState, emptyFoldersState, getFoldersManager } from './store/folderStore'
import FolderTreeLevel from './FolderTreeLevel.svelte'
import { TreeNode, TreeItem, getActions as getContributedActions } from '../index'
import { TreeNode, TreeItem, getActions as getContributedActions } from '../../index'
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc>
@@ -32,14 +32,15 @@
export let getFolderLink: Resource<(doc: Ref<Doc> | undefined) => Location>
export let allObjectsIcon: Asset
export let allObjectsLabel: IntlString
export let plainList: boolean = false
export let forciblyСollapsed: boolean = false
const client = getClient()
let foldersState: FoldersState = FoldersState.empty()
let foldersState: FoldersState = emptyFoldersState()
const foldersManager: FoldersManager = new FoldersManager(titleKey, parentKey, noParentId)
const foldersManager = getFoldersManager(titleKey, parentKey, noParentId, plainList)
FoldersStore.subscribe((newState) => {
foldersState = newState
@@ -54,6 +55,9 @@
query ?? {},
(result) => {
foldersManager.setFolders(result)
if (plainList && foldersState.folders?.length > 0) {
handleFolderSelected(foldersState.folders[0])
}
},
{
sort: {
@@ -65,10 +69,8 @@
async function handleFolderSelected (_id: Ref<Doc>): Promise<void> {
selected = _id
visibleItem = selected !== undefined ? foldersState.folderById.get(selected) : undefined
if (getFolderLink) {
const getFolderLinkFunction = await getResource(getFolderLink)
navigate(getFolderLinkFunction(_id))
}
const getFolderLinkFunction = await getResource(getFolderLink)
navigate(getFolderLinkFunction(_id))
}
async function handleAllItemsSelected (): Promise<void> {
@@ -100,17 +102,46 @@
</script>
<Scroller padding={'1rem 0'}>
<TreeNode
_id={noParentId}
icon={allObjectsIcon}
label={allObjectsLabel}
selected={selected === noParentId}
type={'nested-selectable'}
empty={foldersState?.folders?.length === 0}
actions={() => getRootActions()}
{forciblyСollapsed}
on:click={handleAllItemsSelected}
>
{#if noParentId !== undefined}
<TreeNode
_id={noParentId}
icon={allObjectsIcon}
label={allObjectsLabel}
selected={selected === noParentId}
type={'nested-selectable'}
empty={foldersState?.folders?.length === 0}
actions={() => getRootActions()}
{forciblyСollapsed}
on:click={handleAllItemsSelected}
>
<FolderTreeLevel
folders={foldersState.folders}
descendants={foldersState.descendants}
folderById={foldersState.folderById}
{selected}
on:selected={(ev) => {
handleFolderSelected(ev.detail)
}}
/>
<svelte:fragment slot="visible">
{#if (selected || forciblyСollapsed) && visibleItem !== undefined}
{@const folder = visibleItem}
<TreeItem
_id={folder._id}
folderIcon
iconProps={{ fill: 'var(--global-accent-IconColor)' }}
title={foldersManager.getTitle(folder)}
selected
isFold
empty
actions={async () => await getFolderActions(folder)}
shouldTooltip
forciblyСollapsed
/>
{/if}
</svelte:fragment>
</TreeNode>
{:else}
<FolderTreeLevel
folders={foldersState.folders}
descendants={foldersState.descendants}
@@ -120,22 +151,5 @@
handleFolderSelected(ev.detail)
}}
/>
<svelte:fragment slot="visible">
{#if (selected || forciblyСollapsed) && visibleItem}
{@const folder = visibleItem}
<TreeItem
_id={folder._id}
folderIcon
iconProps={{ fill: 'var(--global-accent-IconColor)' }}
title={foldersManager.getTitle(folder)}
selected
isFold
empty
actions={async () => await getFolderActions(folder)}
shouldTooltip
forciblyСollapsed
/>
{/if}
</svelte:fragment>
</TreeNode>
{/if}
</Scroller>
@@ -0,0 +1,38 @@
// 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 Doc, type Ref } from '@hcengineering/core'
import { writable, type Writable } from 'svelte/store'
import { type FoldersState, FoldersManager, emptyFoldersState } from './folderUtils'
export { type FoldersState, emptyFoldersState }
export const FoldersStore: Writable<FoldersState> = writable(emptyFoldersState())
export const SelectedFolderStore: Writable<Ref<Doc> | undefined> = writable(undefined)
export function setSelectedFolder (_id: Ref<Doc> | undefined): void {
SelectedFolderStore.set(_id)
}
export function getFoldersManager (
titleKey: string,
parentKey: string,
noParentId: Ref<Doc>,
plainList: boolean
): FoldersManager {
return new FoldersManager(titleKey, parentKey, noParentId, plainList, (state: FoldersState) => {
FoldersStore.set(state)
})
}
@@ -12,41 +12,40 @@
// limitations under the License.
import { type Doc, type Ref } from '@hcengineering/core'
import { writable, type Writable } from 'svelte/store'
export class FoldersState {
folders: Array<Ref<Doc>>
folderById: Map<Ref<Doc>, Doc>
descendants: Map<Ref<Doc>, Doc[]>
constructor (folders: Array<Ref<Doc>>, folderById: Map<Ref<Doc>, Doc>, descendants: Map<Ref<Doc>, Doc[]>) {
this.folders = folders
this.folderById = folderById
this.descendants = descendants
}
static empty (): FoldersState {
return new FoldersState([], new Map<Ref<Doc>, Doc>(), new Map<Ref<Doc>, Doc[]>())
}
export interface FoldersState {
readonly folders: Array<Ref<Doc>>
readonly folderById: Map<Ref<Doc>, Doc>
readonly descendants: Map<Ref<Doc>, Doc[]>
}
export const FoldersStore: Writable<FoldersState> = writable(FoldersState.empty())
export const SelectedFolderStore: Writable<Ref<Doc> | undefined> = writable(undefined)
export function setSelectedFolder (_id: Ref<Doc> | undefined): void {
SelectedFolderStore.set(_id)
export function emptyFoldersState (): FoldersState {
return {
folders: [],
folderById: new Map<Ref<Doc>, Doc>(),
descendants: new Map<Ref<Doc>, Doc[]>()
}
}
export class FoldersManager {
titleKey: string
parentKey: string
noParentId: Ref<Doc>
plainList: boolean
updateStore: (state: FoldersState) => void
constructor (titleKey: string, parentKey: string, noParentId: Ref<Doc>) {
constructor (
titleKey: string,
parentKey: string,
noParentId: Ref<Doc>,
plainList: boolean,
updateStore: (state: FoldersState) => void
) {
this.titleKey = titleKey
this.parentKey = parentKey
this.noParentId = noParentId
this.plainList = plainList
this.updateStore = updateStore
}
public getTitle (doc: Doc): string {
@@ -64,16 +63,17 @@ export class FoldersManager {
}
public setFolders (result: Doc[]): void {
const newState: FoldersState = this.plainList ? this.getPlainList(result) : this.getNestedStructure(result)
this.updateStore(newState)
}
public getNestedStructure (result: Doc[]): FoldersState {
let folders: Array<Ref<Doc>> = []
const folderById: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
const descendants: Map<Ref<Doc>, Doc[]> = new Map<Ref<Doc>, Doc[]>()
for (const doc of result) {
const mappedDoc = {
title: this.getTitle(doc),
parent: this.getParent(doc),
...doc
}
const mappedDoc = this.mapDoc(doc)
const current = descendants.get(this.getParent(mappedDoc)) ?? []
current.push(mappedDoc)
descendants.set(this.getParent(mappedDoc), current)
@@ -81,6 +81,31 @@ export class FoldersManager {
}
folders = this.getDescendants(descendants, this.noParentId)
FoldersStore.set(new FoldersState(folders, folderById, descendants))
return { folders, folderById, descendants }
}
public getPlainList (result: Doc[]): FoldersState {
const folderById: Map<Ref<Doc>, Doc> = new Map<Ref<Doc>, Doc>()
for (const doc of result) {
const mappedDoc = this.mapDoc(doc)
folderById.set(mappedDoc._id, mappedDoc)
}
const folders: Array<Ref<Doc>> = result.map((doc) => doc._id)
return {
folders,
folderById,
descendants: new Map<Ref<Doc>, Doc[]>()
}
}
private mapDoc (doc: Doc): Doc {
const mappedDoc = {
title: this.getTitle(doc),
parent: this.getParent(doc),
...doc
}
return mappedDoc
}
}
+4 -1
View File
@@ -101,7 +101,7 @@ import ImageViewer from './components/viewer/ImageViewer.svelte'
import VideoViewer from './components/viewer/VideoViewer.svelte'
import PDFViewer from './components/viewer/PDFViewer.svelte'
import TextViewer from './components/viewer/TextViewer.svelte'
import FoldersBrowser from './components/FoldersBrowser.svelte'
import FoldersBrowser from './components/folders/FoldersBrowser.svelte'
import { blobImageMetadata, blobVideoMetadata } from './blob'
@@ -167,6 +167,8 @@ export { default as List } from './components/list/List.svelte'
export { default as NavLink } from './components/navigator/NavLink.svelte'
export { default as StatusPresenter } from './components/status/StatusPresenter.svelte'
export { default as StatusRefPresenter } from './components/status/StatusRefPresenter.svelte'
export { default as FoldersBrowser } from './components/folders/FoldersBrowser.svelte'
export { default as ListView } from './components/list/ListView.svelte'
export * from './filter'
export * from './middleware'
@@ -189,6 +191,7 @@ export {
} from './utils'
export * from './viewOptions'
export {
ArrayEditor,
BooleanEditor,
BooleanPresenter,
ClassAttributeBar,
@@ -47,6 +47,7 @@
export let createComponentProps: Record<string, any> = {}
export let mainComponentLabel: IntlString
export let mainComponentIcon: Asset | undefined = undefined
export let mainHeaderComponent: AnyComponent | undefined = undefined
export let query: DocumentQuery<Doc> = {}
export let syncWithLocationQuery: boolean = true
export let mainComponent: AnyComponent | AnySvelteComponent
@@ -165,6 +166,17 @@
</svelte:fragment>
<Breadcrumb icon={mainComponentIcon} label={mainComponentLabel} size={'large'} isCurrent />
<svelte:fragment slot="actions">
{#if mainHeaderComponent}
<Component
is={mainHeaderComponent}
props={{
query: resultQuery,
space
}}
/>
{/if}
</svelte:fragment>
</Header>
<Component
is={mainComponent}