UBERF-8520: Test management (#7154)

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko 2024-11-13 13:59:32 +07:00 committed by GitHub
parent 8e511653cc
commit 79659c4c32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
86 changed files with 4799 additions and 31 deletions

View File

@ -545,6 +545,9 @@ dependencies:
'@rush-temp/model-templates':
specifier: file:./projects/model-templates.tgz
version: file:projects/model-templates.tgz
'@rush-temp/model-test-management':
specifier: file:./projects/model-test-management.tgz
version: file:projects/model-test-management.tgz
'@rush-temp/model-text-editor':
specifier: file:./projects/model-text-editor.tgz
version: file:projects/model-text-editor.tgz
@ -1022,6 +1025,15 @@ dependencies:
'@rush-temp/templates-resources':
specifier: file:./projects/templates-resources.tgz
version: file:projects/templates-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
'@rush-temp/test-management':
specifier: file:./projects/test-management.tgz
version: file:projects/test-management.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2)
'@rush-temp/test-management-assets':
specifier: file:./projects/test-management-assets.tgz
version: file:projects/test-management-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2)
'@rush-temp/test-management-resources':
specifier: file:./projects/test-management-resources.tgz
version: file:projects/test-management-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2)
'@rush-temp/tests-sanity':
specifier: file:./projects/tests-sanity.tgz
version: file:projects/tests-sanity.tgz
@ -22727,7 +22739,7 @@ packages:
dev: false
file:projects/desktop.tgz(bufferutil@4.0.8)(sass@1.71.1)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-VjzxhhavCElN2Ak6+7CforJ3IJK0/iTE6crFhcRZrbq4RajWlGEHpD/o7O3mun0xepqA0QxpHCkTIO6SQdPsOg==, tarball: file:projects/desktop.tgz}
resolution: {integrity: sha512-rjywmlFY/JneloAn6/L8+zLke89G1qy9ROO+fB2hJwYZDK0JGaN0CE68Sq8iZX6fJ+sAIGW6IsbbfpsvhmhUag==, tarball: file:projects/desktop.tgz}
id: file:projects/desktop.tgz
name: '@rush-temp/desktop'
version: 0.0.0
@ -24471,7 +24483,7 @@ packages:
dev: false
file:projects/model-all.tgz:
resolution: {integrity: sha512-lZSE4CGfPJX/cTsbqnSoz/ge496Cwl3zeRFHM2wrpgfCFyeCNp00xpRv62LPGe38dvHo/IIzy+Uu0RSVPHUOPw==, tarball: file:projects/model-all.tgz}
resolution: {integrity: sha512-Fxv8P/cfFWJfFli1C70fOej/quvHGGyzJBLpbEixsPV2F+pqX53mwpTzAPqVkHLFVwbRkpnociSpaJQYyx0Beg==, tarball: file:projects/model-all.tgz}
name: '@rush-temp/model-all'
version: 0.0.0
dependencies:
@ -25719,6 +25731,24 @@ packages:
- supports-color
dev: false
file:projects/model-test-management.tgz:
resolution: {integrity: sha512-p0y76Msm6v7Zg8KlvMDiWV3BKCDEw8KjV9sUwC+wkMvKnJ4tOtErd+OdnJFKOyGbtX6yX8rCSp6NIJwqmMmhSA==, tarball: file:projects/model-test-management.tgz}
name: '@rush-temp/model-test-management'
version: 0.0.0
dependencies:
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
prettier: 3.2.5
typescript: 5.6.2
transitivePeerDependencies:
- supports-color
dev: false
file:projects/model-text-editor.tgz:
resolution: {integrity: sha512-z1TlVHF8VxcMeChSZ70ZGJsKdGSKisnpnPGuLmp2yYS30ckD5ohoj4KeqgbKu+qT+72FYSg4COXFoAYnszgfoA==, tarball: file:projects/model-text-editor.tgz}
name: '@rush-temp/model-text-editor'
@ -27488,7 +27518,7 @@ packages:
dev: false
file:projects/prod.tgz(bufferutil@4.0.8)(sass@1.71.1)(ts-node@10.9.2)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-LpzXVVxqqH4eEF3U7B9uOy/zBFWAZ+FGeGqIBCo9N7UzWxndKf7LZx+eYJQ8gsg5aVoqlGAbXjZJHHSVERGeJQ==, tarball: file:projects/prod.tgz}
resolution: {integrity: sha512-vNHiwvJN+OiPr+lWDdxC3+YB6vZPF0QHYXMXrmHcagQH5vEfVzRPmL9F8pTnx/MHPvAlDt4DqgQls77F8LFYgg==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz
name: '@rush-temp/prod'
version: 0.0.0
@ -29382,7 +29412,7 @@ packages:
dev: false
file:projects/server-indexer.tgz(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-H/iX6BGnUy04J35NoefNnWyoCqEd5RFve5SMXioIuA4tWqglHcAz8JqyPPLlfN+onrsh4Pz0tTVRV6lOZXSXWw==, tarball: file:projects/server-indexer.tgz}
resolution: {integrity: sha512-X0K8LN7D/inN8B9B+/ERJTAm1l0gQ7fA9nOdMMVUHEVZqmq6WoiSZ+vPxdY/dx+MQlYFTcPumxLgZiVCZET4Og==, tarball: file:projects/server-indexer.tgz}
id: file:projects/server-indexer.tgz
name: '@rush-temp/server-indexer'
version: 0.0.0
@ -31256,6 +31286,113 @@ packages:
- ts-node
dev: false
file:projects/test-management-assets.tgz(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-5goSfMWyruB2GJwVYnn8e9YYJW523dDrcME75Og3+yem8W+yUcteJ+Sy2qgMNjCzC6ADzxCM5TIHItnCpH0gjQ==, tarball: file:projects/test-management-assets.tgz}
id: file:projects/test-management-assets.tgz
name: '@rush-temp/test-management-assets'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.6.2)
typescript: 5.6.2
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- ts-node
dev: false
file:projects/test-management-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-o+axSz7dIMDinwB3h8Ntr1pP35PGsUtKtTUvEqJFQ1lA+KQdTJ9akLrijzp9T6xRSdTPa6WNG6yjtqM8yhC70w==, tarball: file:projects/test-management-resources.tgz}
id: file:projects/test-management-resources.tgz
name: '@rush-temp/test-management-resources'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.19)(ts-node@10.9.2)
fast-equals: 5.0.1
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
prettier: 3.2.5
prettier-plugin-svelte: 3.2.2(prettier@3.2.5)(svelte@4.2.19)
sass: 1.71.1
svelte: 4.2.19
svelte-check: 3.6.9(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.19)
svelte-eslint-parser: 0.33.1(svelte@4.2.19)
svelte-loader: 3.2.0(svelte@4.2.19)
svelte-preprocess: 5.1.3(postcss-load-config@4.0.2)(postcss@8.4.35)(sass@1.71.1)(svelte@4.2.19)(typescript@5.6.2)
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.6.2)
typescript: 5.6.2
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- coffeescript
- esbuild
- less
- node-notifier
- postcss
- postcss-load-config
- pug
- stylus
- sugarss
- supports-color
- ts-node
dev: false
file:projects/test-management.tgz(@types/node@20.11.19)(esbuild@0.20.1)(ts-node@10.9.2):
resolution: {integrity: sha512-zLxjXJwufmBoWJS5xoCGxG9uybC/T7UtJ+g4PlSnDv16aGSPDbnybLtQ0uorYvlOESpcSgQGrVLCgOhIB90tfw==, tarball: file:projects/test-management.tgz}
id: file:projects/test-management.tgz
name: '@rush-temp/test-management'
version: 0.0.0
dependencies:
'@types/jest': 29.5.12
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
lexorank: 1.0.5
prettier: 3.2.5
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.6.2)
typescript: 5.6.2
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@types/node'
- babel-jest
- babel-plugin-macros
- esbuild
- node-notifier
- supports-color
- ts-node
dev: false
file:projects/tests-sanity.tgz:
resolution: {integrity: sha512-noV3nlBP0OvM6i4C5YUevkhltHxG/2bct4pusP3O3AEQ7Za+A4qwzpsE08pHfA9f9pedEG677GE4EmG9x61rVQ==, tarball: file:projects/tests-sanity.tgz}
name: '@rush-temp/tests-sanity'
@ -31748,7 +31885,7 @@ packages:
dev: false
file:projects/tracker-resources.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
resolution: {integrity: sha512-6wsyMyxPpL8OKwTKistPKvRxkmmptqXJwxFZ5VOOlGuEwqXFYIPYZNR6taZd33fq1Y97L+vKeP65rWmXhbRIdQ==, tarball: file:projects/tracker-resources.tgz}
resolution: {integrity: sha512-k1Lw3hvnvA+K22hLtdZKz4+ZQbk451p8bQL0RIHZ/yS/dIBLlBpBUU279YAqH2zxlmlFt1+C1WsTsrrPRdfx7A==, tarball: file:projects/tracker-resources.tgz}
id: file:projects/tracker-resources.tgz
name: '@rush-temp/tracker-resources'
version: 0.0.0

View File

@ -204,6 +204,9 @@
"@hcengineering/analytics-collector-resources": "^0.6.0",
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/ai-bot-resources": "^0.6.0",
"@hcengineering/test-management": "^0.6.0",
"@hcengineering/test-management-assets": "^0.6.0",
"@hcengineering/test-management-resources": "^0.6.0",
"electron-squirrel-startup": "~1.0.0",
"dotenv": "~16.0.0",
"electron-context-menu": "^4.0.4",

View File

@ -53,6 +53,7 @@ import { questionsId } from '@hcengineering/questions'
import { trainingId } from '@hcengineering/training'
import { documentsId } from '@hcengineering/controlled-documents'
import aiBot, { aiBotId } from '@hcengineering/ai-bot'
import { testManagementId } from '@hcengineering/test-management'
import '@hcengineering/activity-assets'
import '@hcengineering/attachment-assets'
@ -94,6 +95,7 @@ import '@hcengineering/products-assets'
import '@hcengineering/controlled-documents-assets'
import '@hcengineering/analytics-collector-assets'
import '@hcengineering/text-editor-assets'
import '@hcengineering/test-management-assets'
import { coreId } from '@hcengineering/core'
import presentation, { parsePreviewConfig, parseUploadConfig, presentationId } from '@hcengineering/presentation'
@ -185,6 +187,7 @@ function configureI18n (): void {
addStringsLoader(loveId, async (lang: string) => await import(`@hcengineering/love-assets/lang/${lang}.json`))
addStringsLoader(printId, async (lang: string) => await import(`@hcengineering/print-assets/lang/${lang}.json`))
addStringsLoader(analyticsCollectorId, async (lang: string) => await import(`@hcengineering/analytics-collector-assets/lang/${lang}.json`))
addStringsLoader(testManagementId, async (lang: string) => await import(`@hcengineering/test-management-assets/lang/${lang}.json`))
}
export async function configurePlatform (): Promise<void> {
@ -306,6 +309,7 @@ export async function configurePlatform (): Promise<void> {
addLocation(loveId, () => import(/* webpackChunkName: "love" */ '@hcengineering/love-resources'))
addLocation(printId, () => import(/* webpackChunkName: "print" */ '@hcengineering/print-resources'))
addLocation(textEditorId, () => import(/* webpackChunkName: "text-editor" */ '@hcengineering/text-editor-resources'))
addLocation(testManagementId, () => import(/* webpackChunkName: "test-management" */ '@hcengineering/test-management-resources'))
setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])

View File

@ -233,6 +233,9 @@
"@hcengineering/products-resources": "^0.1.0",
"@hcengineering/ai-bot": "^0.6.0",
"@hcengineering/ai-bot-resources": "^0.6.0",
"@hcengineering/test-management": "^0.6.0",
"@hcengineering/test-management-assets": "^0.6.0",
"@hcengineering/test-management-resources": "^0.6.0",
"@sentry/svelte": "~7.101.0",
"posthog-js": "~1.122.0"
}

View File

@ -60,6 +60,7 @@ import textEditor, { textEditorId } from '@hcengineering/text-editor'
import analyticsCollector, {analyticsCollectorId} from '@hcengineering/analytics-collector'
import { uploaderId } from '@hcengineering/uploader'
import aiBot, { aiBotId } from '@hcengineering/ai-bot'
import { testManagementId } from '@hcengineering/test-management'
import { bitrixId } from '@hcengineering/bitrix'
@ -103,6 +104,7 @@ import '@hcengineering/controlled-documents-assets'
import '@hcengineering/analytics-collector-assets'
import '@hcengineering/text-editor-assets'
import '@hcengineering/uploader-assets'
import '@hcengineering/test-management-assets'
import github, { githubId } from '@hcengineering/github'
import '@hcengineering/github-assets'
@ -231,6 +233,7 @@ function configureI18n(): void {
addStringsLoader(loveId, async (lang: string) => await import(`@hcengineering/love-assets/lang/${lang}.json`))
addStringsLoader(printId, async (lang: string) => await import(`@hcengineering/print-assets/lang/${lang}.json`))
addStringsLoader(analyticsCollectorId, async (lang: string) => await import(`@hcengineering/analytics-collector-assets/lang/${lang}.json`))
addStringsLoader(testManagementId, async (lang: string) => await import(`@hcengineering/test-management-assets/lang/${lang}.json`))
}
export async function configurePlatform() {
@ -399,6 +402,7 @@ export async function configurePlatform() {
addLocation(printId, () => import(/* webpackChunkName: "print" */ '@hcengineering/print-resources'))
addLocation(textEditorId, () => import(/* webpackChunkName: "text-editor" */ '@hcengineering/text-editor-resources'))
addLocation(uploaderId, () => import(/* webpackChunkName: "uploader" */ '@hcengineering/uploader-resources'))
addLocation(testManagementId, () => import(/* webpackChunkName: "test-management" */ '@hcengineering/test-management-resources'))
setMetadata(client.metadata.FilterModel, 'ui')
setMetadata(client.metadata.ExtraPlugins, ['preference' as Plugin])

View File

@ -110,6 +110,7 @@
"@hcengineering/model-analytics-collector": "^0.6.0",
"@hcengineering/model-server-ai-bot": "^0.6.0",
"@hcengineering/model-ai-bot": "^0.6.0",
"@hcengineering/model-server-fulltext": "^0.6.0"
"@hcengineering/model-server-fulltext": "^0.6.0",
"@hcengineering/model-test-management": "^0.6.0"
}
}

View File

@ -95,6 +95,11 @@ import documents, { documentsId, createModel as documentsModel } from '@hcengine
import products, { productsId, createModel as productsModel } from '@hcengineering/model-products'
import { serverProductsId, createModel as serverProductsModel } from '@hcengineering/model-server-products'
import { serverTrainingId, createModel as serverTrainingModel } from '@hcengineering/model-server-training'
import testManagement, {
testManagementId,
createModel as testManagementModel
} from '@hcengineering/model-test-management'
import {
serverDocumentsId,
createModel as serverDocumentsModel
@ -404,6 +409,17 @@ export default function buildModel (enabled: string[] = ['*'], disabled: string[
classFilter: defaultFilter
}
],
[
testManagementModel,
testManagementId,
{
label: testManagement.string.ConfigLabel,
description: testManagement.string.ConfigDescription,
enabled: false,
beta: false,
classFilter: defaultFilter
}
],
[serverCoreModel, serverCoreId],
[serverAttachmentModel, serverAttachmentId],

View File

@ -52,6 +52,7 @@ import { productsOperation } from '@hcengineering/model-products'
import { requestOperation } from '@hcengineering/model-request'
import { analyticsCollectorOperation } from '@hcengineering/model-analytics-collector'
import { workbenchOperation } from '@hcengineering/model-workbench'
import { testManagementOperation } from '@hcengineering/model-test-management'
export const migrateOperations: [string, MigrateOperation][] = [
['core', coreOperation],
@ -92,5 +93,6 @@ export const migrateOperations: [string, MigrateOperation][] = [
// We should call notification migration after activityServer and chunter
['notification', notificationOperation],
['analyticsCollector', analyticsCollectorOperation],
['workbench', workbenchOperation]
['workbench', workbenchOperation],
['testManagement', testManagementOperation]
]

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/model/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "model"
}

View File

@ -0,0 +1,59 @@
{
"name": "@hcengineering/model-test-management",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Anticrm Platform Contributors",
"template": "@hcengineering/model-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"_phase:build": "compile transpile src",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/attachment": "^0.6.14",
"@hcengineering/activity": "^0.6.0",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/core": "^0.6.32",
"@hcengineering/model": "^0.6.11",
"@hcengineering/model-attachment": "^0.6.0",
"@hcengineering/model-contact": "^0.6.1",
"@hcengineering/model-core": "^0.6.0",
"@hcengineering/model-notification": "^0.6.0",
"@hcengineering/model-presentation": "^0.6.0",
"@hcengineering/model-print": "^0.6.0",
"@hcengineering/model-tracker": "^0.6.0",
"@hcengineering/model-view": "^0.6.0",
"@hcengineering/model-workbench": "^0.6.1",
"@hcengineering/model-activity": "^0.6.0",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/setting": "^0.6.17",
"@hcengineering/tags": "^0.6.16",
"@hcengineering/time": "^0.6.0",
"@hcengineering/test-management": "^0.6.0",
"@hcengineering/test-management-resources": "^0.6.0",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/workbench": "^0.6.16",
"@hcengineering/model-preference": "^0.6.0"
}
}

View File

@ -0,0 +1,51 @@
//
// 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
})
}

View File

@ -0,0 +1,422 @@
//
// 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 activity from '@hcengineering/activity'
import chunter from '@hcengineering/chunter'
import core from '@hcengineering/model-core'
import { SortingOrder } from '@hcengineering/core'
import { type Builder } from '@hcengineering/model'
import view, { createAction } from '@hcengineering/model-view'
import workbench from '@hcengineering/model-workbench'
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 {
DOMAIN_TEST_MANAGEMENT,
TTypeTestCaseType,
TTypeTestCasePriority,
TTypeTestCaseStatus,
TTestProject,
TTestSuite,
TTestCase,
TDefaultProjectTypeData,
TTestRun,
TTypeTestRunResult,
TTestRunItem
} from './types'
import testManagement from './plugin'
import { definePresenters } from './presenters'
export { testManagementId } from '@hcengineering/test-management/src/index'
function defineApplication (builder: Builder): void {
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: testManagement.string.TestManagementApplication,
icon: testManagement.icon.TestManagementApplication,
alias: testManagementId,
hidden: false,
locationResolver: testManagement.resolver.Location,
navigatorModel: {
spaces: [
{
id: 'projects',
label: testManagement.string.Projects,
spaceClass: testManagement.class.TestProject,
addSpaceLabel: testManagement.string.CreateProject,
createComponent: testManagement.component.CreateProject,
icon: testManagement.icon.Home,
specials: [
{
id: 'library',
label: testManagement.string.TestLibrary,
icon: testManagement.icon.TestLibrary,
component: workbench.component.SpecialView,
componentProps: {
_class: testManagement.class.TestCase,
icon: testManagement.icon.TestLibrary,
label: testManagement.string.TestLibrary,
createLabel: testManagement.string.CreateTestCase,
createComponent: testManagement.component.CreateTestCase
},
navigationModel: {
navigationComponent: view.component.FoldersBrowser,
navigationComponentLabel: testManagement.string.TestSuites,
navigationComponentIcon: testManagement.icon.TestSuites,
mainComponentLabel: testManagement.string.TestCases,
mainComponentIcon: testManagement.icon.TestCases,
createComponent: testManagement.component.CreateTestSuite,
navigationComponentProps: {
_class: testManagement.class.TestSuite,
icon: testManagement.icon.TestSuites,
title: testManagement.string.TestSuites,
createLabel: testManagement.string.CreateTestSuite,
createComponent: testManagement.component.CreateTestSuite,
titleKey: 'name',
parentKey: 'parent',
noParentId: testManagement.ids.NoParent,
getFolderLink: testManagement.function.GetTestSuiteLink,
allObjectsLabel: testManagement.string.AllTestCases,
allObjectsIcon: testManagement.icon.TestSuites
},
syncWithLocationQuery: true
}
}
/* TODO: UBERF-8584
{
id: opt.testRunsId,
label: testManagement.string.TestRuns,
icon: testManagement.icon.TestRuns,
component: workbench.component.SpecialView,
componentProps: {
_class: testManagement.class.TestRun,
icon: testManagement.icon.TestRuns,
title: testManagement.string.TestRuns,
createLabel: testManagement.string.NewTestRun,
createComponent: testManagement.component.CreateTestRun
}
} */
]
}
]
},
navHeaderComponent: testManagement.component.NewTestCaseHeader
},
testManagement.app.TestManagement
)
}
export function createModel (builder: Builder): void {
builder.createModel(
TTypeTestCaseType,
TTypeTestCasePriority,
TTypeTestCaseStatus,
TTestProject,
TTestSuite,
TTestCase,
TDefaultProjectTypeData,
TTestRun,
TTestRunItem,
TTypeTestRunResult
)
builder.mixin(testManagement.class.TestProject, core.class.Class, activity.mixin.ActivityDoc, {})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestProject,
components: { input: chunter.component.ChatMessageInput }
})
defineTestSuite(builder)
defineTestCase(builder)
defineTestRun(builder)
definePresenters(builder)
defineApplication(builder)
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ObjectIcon, {
component: testManagement.component.TestCaseStatusPresenter
})
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {
domain: DOMAIN_TEST_MANAGEMENT,
disabled: [
{ space: 1 },
{ attachedToClass: 1 },
{ status: 1 },
{ project: 1 },
{ priority: 1 },
{ assignee: 1 },
{ sprint: 1 },
{ component: 1 },
{ category: 1 },
{ modifiedOn: 1 },
{ modifiedBy: 1 },
{ createdBy: 1 },
{ relations: 1 },
{ milestone: 1 },
{ createdOn: -1 }
]
})
defineSpaceType(builder)
}
function defineSpaceType (builder: Builder): void {
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
},
testManagement.spaceType.DefaultProject
)
}
function defineTestSuite (builder: Builder): void {
builder.mixin(testManagement.class.TestSuite, core.class.Class, activity.mixin.ActivityDoc, {})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestSuite,
components: { input: chunter.component.ChatMessageInput }
})
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.ObjectEditor, {
editor: testManagement.component.EditTestSuite
})
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.ObjectPanel, {
component: testManagement.component.EditTestSuite
})
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.ObjectPresenter, {
presenter: testManagement.component.TestSuitePresenter
})
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: testManagement.class.TestSuite,
descriptor: view.viewlet.Table,
config: ['', 'description'],
configOptions: {
strict: true
}
},
testManagement.viewlet.TableTestSuites
)
// 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
]
})
createAction(
builder,
{
action: testManagement.actionImpl.CreateChildTestSuite,
label: testManagement.string.CreateTestSuite,
icon: testManagement.icon.TestSuite,
category: testManagement.category.TestSuite,
input: 'none',
target: testManagement.class.TestSuite,
context: {
mode: ['context', 'browser'],
application: testManagement.app.TestManagement,
group: 'create'
}
},
testManagement.action.CreateChildTestSuite
)
}
function defineTestCase (builder: Builder): void {
builder.mixin(testManagement.class.TestCase, core.class.Class, activity.mixin.ActivityDoc, {})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestCase,
components: { input: chunter.component.ChatMessageInput }
})
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ObjectEditor, {
editor: testManagement.component.EditTestCase
})
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ObjectPanel, {
component: testManagement.component.EditTestCase
})
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ObjectPresenter, {
presenter: testManagement.component.TestCasePresenter
})
builder.mixin(testManagement.class.TypeTestCaseStatus, core.class.Class, view.mixin.AttributeFilter, {
component: view.component.ValueFilter
})
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.AttributePresenter, {
presenter: testManagement.component.TestSuiteRefPresenter
})
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: testManagement.class.TestCase,
descriptor: view.viewlet.Table,
config: ['', { key: 'attachedTo', label: testManagement.string.TestSuite }, 'status', 'assignee'],
configOptions: {
strict: true
}
},
testManagement.viewlet.TableTestCase
)
const viewOptions: ViewOptionsModel = {
groupBy: ['attachedTo'],
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.TestCase,
descriptor: view.viewlet.List,
configOptions: {
strict: true,
hiddenKeys: ['title']
},
config: [
{ key: '', displayProps: { fixed: 'left', key: 'lead' } },
{
key: 'status',
props: { kind: 'list', size: 'small', shouldShowName: false }
},
{ key: 'modifiedOn', displayProps: { key: 'modified', fixed: 'right', dividerBefore: true } },
{
key: 'assignee',
props: { kind: 'list', shouldShowName: false, avatarSize: 'x-small' },
displayProps: { key: 'assignee', fixed: 'right' }
}
],
viewOptions
},
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 {
builder.mixin(testManagement.class.TestRun, core.class.Class, activity.mixin.ActivityDoc, {})
builder.createDoc(activity.class.ActivityExtension, core.space.Model, {
ofClass: testManagement.class.TestRun,
components: { input: 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
})
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.ObjectPresenter, {
presenter: testManagement.component.TestRunPresenter
})
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: testManagement.class.TestRun,
descriptor: view.viewlet.Table,
config: [''],
configOptions: {
strict: true
}
},
testManagement.viewlet.TableTestRun
)
}
export { testManagementOperation } from './migration'
export { default } from './plugin'

View File

@ -0,0 +1,21 @@
//
// 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 MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model'
export const testManagementOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {}
}

View File

@ -0,0 +1,49 @@
//
// 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 { testManagementId } from '@hcengineering/test-management'
import testManganement from '@hcengineering/test-management-resources/src/plugin'
import type { Doc, 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'
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>
},
component: {
CreateTestCase: '' as AnyComponent,
TestCasePresenter: '' as AnyComponent,
ProjectPresenter: '' as AnyComponent,
ProjectSpacePresenter: '' as AnyComponent,
TestSuitePresenter: '' as AnyComponent,
EditTestSuite: '' as AnyComponent,
EditTestCase: '' as AnyComponent,
CreateTestRun: '' as AnyComponent,
TestRunPresenter: '' as AnyComponent,
EditTestRun: '' as AnyComponent,
TestSuiteRefPresenter: '' as AnyComponent
}
})

View File

@ -0,0 +1,56 @@
//
// 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 view from '@hcengineering/model-view'
import testManagement from './plugin'
/**
* Define presenters
*/
export function definePresenters (builder: Builder): void {
//
// Project
//
builder.mixin(testManagement.class.TestProject, core.class.Class, view.mixin.ObjectPresenter, {
presenter: testManagement.component.ProjectPresenter
})
builder.mixin(testManagement.class.TestProject, core.class.Class, view.mixin.SpacePresenter, {
presenter: testManagement.component.ProjectSpacePresenter
})
//
// Test Suite
//
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.ObjectPresenter, {
presenter: testManagement.component.TestSuitePresenter
})
//
// Test Case
//
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ObjectPresenter, {
presenter: testManagement.component.TestCasePresenter
})
//
// Type Test Case Status
//
builder.mixin(testManagement.class.TypeTestCaseStatus, core.class.Class, view.mixin.AttributePresenter, {
presenter: testManagement.component.TestCaseStatusPresenter
})
}

View File

@ -0,0 +1,233 @@
//
// 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 { Employee } from '@hcengineering/contact'
import type {
TestCase,
TestSuite,
TestCaseType,
TestCasePriority,
TestCaseStatus,
TestProject,
TestRun,
TestRunResult,
TestRunItem
} from '@hcengineering/test-management'
import { type Attachment } from '@hcengineering/attachment'
import contact from '@hcengineering/contact'
import chunter from '@hcengineering/chunter'
import { getEmbeddedLabel } from '@hcengineering/platform'
import {
Account,
DateRangeMode,
IndexKind,
type RolesAssignment,
type Role,
Ref,
type Domain,
type Timestamp,
type Type,
type CollectionSize,
type CollaborativeDoc,
type Class
} from '@hcengineering/core'
import {
Mixin,
Model,
Prop,
TypeRef,
UX,
TypeMarkup,
Index,
TypeCollaborativeDoc,
TypeString,
Collection,
ReadOnly,
TypeDate,
Hidden
} from '@hcengineering/model'
import attachment from '@hcengineering/model-attachment'
import core, { TAttachedDoc, TDoc, TType, TTypedSpace } from '@hcengineering/model-core'
import testManagement from './plugin'
export { testManagementId } from '@hcengineering/test-management/src/index'
export const DOMAIN_TEST_MANAGEMENT = 'test-management' as Domain
/** @public */
export function TypeTestCaseType (): Type<TestCaseType> {
return { _class: testManagement.class.TypeTestCaseType, label: testManagement.string.TestCaseType }
}
@Model(testManagement.class.TypeTestCaseType, core.class.Type, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestCaseType)
export class TTypeTestCaseType extends TType {}
/** @public */
export function TypeTestCasePriority (): Type<TestCasePriority> {
return { _class: testManagement.class.TypeTestCasePriority, label: testManagement.string.TestCasePriority }
}
@Model(testManagement.class.TypeTestCasePriority, core.class.Type, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestCasePriority)
export class TTypeTestCasePriority extends TType {}
/** @public */
export function TypeTestCaseStatus (): Type<TestCaseStatus> {
return { _class: testManagement.class.TypeTestCaseStatus, label: testManagement.string.TestCaseStatus }
}
@Model(testManagement.class.TypeTestCaseStatus, core.class.Type, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestCaseStatus)
export class TTypeTestCaseStatus extends TType {}
@Model(testManagement.class.TestProject, core.class.TypedSpace)
@UX(testManagement.string.TestProject)
export class TTestProject extends TTypedSpace implements TestProject {
@Prop(TypeMarkup(), testManagement.string.FullDescription)
@Index(IndexKind.FullText)
fullDescription?: string
}
@Mixin(testManagement.mixin.DefaultProjectTypeData, testManagement.class.TestProject)
@UX(getEmbeddedLabel('Default project'), testManagement.icon.TestProject)
export class TDefaultProjectTypeData extends TTestProject implements RolesAssignment {
[key: Ref<Role>]: Ref<Account>[]
}
/**
* @public
*/
@Model(testManagement.class.TestSuite, core.class.Doc, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestSuite, testManagement.icon.TestSuite, testManagement.string.TestSuite)
export class TTestSuite extends TDoc implements TestSuite {
@Prop(TypeString(), testManagement.string.SuiteName)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeMarkup(), testManagement.string.SuiteDescription)
@Index(IndexKind.FullText)
description?: string
@Prop(TypeRef(testManagement.class.TestSuite), testManagement.string.TestSuite)
parent!: Ref<TestSuite>
@Prop(Collection(testManagement.class.TestCase), testManagement.string.TestCases, {
shortLabel: testManagement.string.TestCase
})
testCases?: CollectionSize<TestCase>
declare space: Ref<TestProject>
}
/**
* @public
*/
@Model(testManagement.class.TestCase, core.class.AttachedDoc, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestCase, testManagement.icon.TestCase, testManagement.string.TestCase)
export class TTestCase extends TAttachedDoc implements TestCase {
@Prop(TypeRef(testManagement.class.TestProject), core.string.Space)
@Index(IndexKind.Indexed)
@Hidden()
declare space: Ref<TestProject>
@Prop(TypeRef(testManagement.class.TestSuite), core.string.AttachedTo)
@Index(IndexKind.Indexed)
declare attachedTo: Ref<TestSuite>
@Prop(TypeRef(testManagement.class.TestSuite), core.string.AttachedToClass)
@Index(IndexKind.Indexed)
@Hidden()
declare attachedToClass: Ref<Class<TestSuite>>
@Prop(TypeString(), core.string.Collection)
@Hidden()
override collection: 'testCases' = 'testCases'
@Prop(TypeString(), testManagement.string.TestName)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeCollaborativeDoc(), testManagement.string.FullDescription)
@Index(IndexKind.FullText)
description!: CollaborativeDoc
@Prop(TypeTestCaseType(), testManagement.string.TestType)
@ReadOnly()
type!: TestCaseType
@Prop(TypeTestCasePriority(), testManagement.string.TestPriority)
@ReadOnly()
priority!: TestCasePriority
@Prop(TypeTestCaseStatus(), testManagement.string.TestStatus)
@ReadOnly()
status!: TestCaseStatus
@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
}
@Model(testManagement.class.TestRun, core.class.Doc, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestRun)
export class TTestRun extends TDoc implements TestRun {
@Prop(TypeString(), testManagement.string.TestRunName)
@Index(IndexKind.FullText)
name!: string
@Prop(TypeCollaborativeDoc(), testManagement.string.FullDescription)
@Index(IndexKind.FullText)
description!: CollaborativeDoc
@Prop(TypeDate(DateRangeMode.DATETIME), testManagement.string.DueDate)
dueDate?: Timestamp
@Prop(Collection(testManagement.class.TestRunItem), testManagement.string.TestRunItems, {
shortLabel: testManagement.string.TestRunItem
})
items?: CollectionSize<TestRunItem>
}
/** @public */
export function TypeTestRunResult (): Type<TestRunResult> {
return { _class: testManagement.class.TypeTestRunResult, label: testManagement.string.TestRunResult }
}
@Model(testManagement.class.TypeTestRunResult, core.class.Type, DOMAIN_TEST_MANAGEMENT)
@UX(testManagement.string.TestRunResult)
export class TTypeTestRunResult 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>
@Prop(TypeRef(testManagement.class.TestCase), testManagement.string.TestCase)
testCase!: Ref<TestCase>
@Prop(TypeTestRunResult(), testManagement.string.TestRunResult)
result?: TestRunResult
@Prop(Collection(chunter.class.ChatMessage), chunter.string.Comments)
comments?: number
}

View File

@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/model/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}

View File

@ -88,7 +88,8 @@ export default mergeIds(viewId, view, {
ImageViewer: '' as AnyComponent,
VideoViewer: '' as AnyComponent,
PDFViewer: '' as AnyComponent,
TextViewer: '' as AnyComponent
TextViewer: '' as AnyComponent,
FoldersBrowser: '' as AnyComponent
},
string: {
Table: '' as IntlString,

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "platform",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -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.
//
-->
<script lang="ts">
import type { IntlString } from '@hcengineering/platform'
import type { ComponentProps } from 'svelte'
import Icon from './Icon.svelte'
import Label from './Label.svelte'
export let icon: ComponentProps<Icon>['icon']
export let label: IntlString
export let labelParams: Record<string, any> = {}
</script>
<div class="antiSection-empty solid flex-col-center mt-3">
<div class="flex-center caption-color">
<Icon {icon} size="large" />
</div>
<span class="text-sm content-dark-color mt-2">
<Label {label} params={labelParams} />
</span>
<slot />
</div>

View File

@ -272,6 +272,7 @@ export { default as TimeZonesPopup } from './components/TimeZonesPopup.svelte'
export { default as CodeForm } from './components/CodeForm.svelte'
export { default as CodeInput } from './components/CodeInput.svelte'
export { default as TimeLeft } from './components/TimeLeft.svelte'
export { default as SectionEmpty } from './components/SectionEmpty.svelte'
export { default as Dock } from './components/Dock.svelte'

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/assets/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,52 @@
<!-- ALL HASHTAGs -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<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>
<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>
<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"/>
</symbol>
<symbol id="home" viewBox="0 0 16 16">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 1L13.8838 5.29644C14.271 5.5788 14.5 6.0292 14.5 6.50845V12.5C14.5 13.3284 13.8284 14 13 14H3C2.17157 14 1.5 13.3284 1.5 12.5V6.50845C1.5 6.0292 1.729 5.5788 2.11624 5.29644L8 1Z"/>
</symbol>
<symbol id="red-circle" viewBox="0 0 14 14">
<path d="M7,0C3.1,0,0,3.1,0,7c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7C14,3.1,10.9,0,7,0z M7,12c-2.8,0-5-2.2-5-5s2.2-5,5-5s5,2.2,5,5S9.8,12,7,12z" />
</symbol>
<symbol id="status-draft" viewBox="1 1 14 14" fill="#D7D8DB">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5 5.36133L8 2.73633L3.5 5.36133L3.5 10.6382L8 13.2632L12.5 10.6382L12.5 5.36133ZM8.75581 1.44066C8.28876 1.16822 7.71124 1.16822 7.24419 1.44066L2.74419 4.06566C2.28337 4.33448 2 4.82783 2 5.36133V10.6382C2 11.1717 2.28337 11.6651 2.74419 11.9339L7.24419 14.5589C7.71124 14.8313 8.28876 14.8313 8.75581 14.5589L13.2558 11.9339C13.7166 11.6651 14 11.1717 14 10.6382V5.36133C14 4.82783 13.7166 4.33448 13.2558 4.06566L8.75581 1.44066Z" />
</symbol>
<symbol id="status-review" viewBox="1 1 14 14" fill="#F2C94C">
<path d="M8.3779 4.74233C8.14438 4.60607 7.85562 4.60607 7.6221 4.74233L5.37209 6.05513C5.14168 6.18957 5 6.4363 5 6.70311V9.34216C5 9.60897 5.14168 9.85573 5.37209 9.99016L7.6221 11.303C7.85562 11.4392 8.14438 11.4392 8.3779 11.303L10.6279 9.99016C10.8583 9.85573 11 9.60897 11 9.34216V6.70311C11 6.4363 10.8583 6.18957 10.6279 6.05513L8.3779 4.74233Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.24419 1.44066C7.71124 1.16822 8.28876 1.16822 8.75581 1.44066L13.2558 4.06566C13.7166 4.33448 14 4.82783 14 5.36133V10.6382C14 11.1717 13.7166 11.6651 13.2558 11.9339L8.75581 14.5589C8.28876 14.8313 7.71124 14.8313 7.24419 14.5589L2.74419 11.9339C2.28337 11.6651 2 11.1717 2 10.6382V5.36133C2 4.82783 2.28337 4.33448 2.74419 4.06566L7.24419 1.44066ZM8 2.73633L12.5 5.36133V10.6382L8 13.2632L3.5 10.6382V5.36133L8 2.73633Z" />
</symbol>
<symbol id="status-review-comments" viewBox="1 1 14 14" fill="#8A8F98">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75581 1.21148C8.28876 0.929507 7.71124 0.929507 7.24419 1.21148L2.74419 3.92829C2.28337 4.20651 2 4.71711 2 5.26927V10.7307C2 11.2829 2.28337 11.7935 2.74419 12.0717L7.24419 14.7885C7.71124 15.0705 8.28876 15.0705 8.75581 14.7885L13.2558 12.0717C13.7166 11.7935 14 11.2829 14 10.7307V5.26927C14 4.71711 13.7166 4.20651 13.2558 3.92829L8.75581 1.21148ZM12.5 5.26928L8 2.55246L3.5 5.26927L3.5 10.7307L8 13.4475L12.5 10.7307L12.5 5.26928Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 5.75C6.91421 5.75 7.25 6.08579 7.25 6.5V9.5C7.25 9.91421 6.91421 10.25 6.5 10.25C6.08579 10.25 5.75 9.91421 5.75 9.5V6.5C5.75 6.08579 6.08579 5.75 6.5 5.75Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 5.75C9.91421 5.75 10.25 6.08579 10.25 6.5V9.5C10.25 9.91421 9.91421 10.25 9.5 10.25C9.08579 10.25 8.75 9.91421 8.75 9.5V6.5C8.75 6.08579 9.08579 5.75 9.5 5.75Z" />
</symbol>
<symbol id="status-approved" viewBox="1 1 14 14" fill="#5E6AD2">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.5 5.125L8 2.5L3.5 5.125L3.5 10.4019L8 13.0269L12.5 10.4019L12.5 5.125ZM8.75581 1.20433C8.28876 0.93189 7.71124 0.931889 7.24419 1.20433L2.74419 3.82933C2.28337 4.09815 2 4.5915 2 5.125V10.4019C2 10.9354 2.28337 11.4287 2.74419 11.6976L7.24419 14.3226C7.71124 14.595 8.28876 14.595 8.75581 14.3226L13.2558 11.6976C13.7166 11.4287 14 10.9354 14 10.4019V5.125C14 4.5915 13.7166 4.09815 13.2558 3.82933L8.75581 1.20433Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.7381 5.69424C11.0526 5.96381 11.089 6.43728 10.8194 6.75178L7.81944 10.2518C7.68349 10.4104 7.48754 10.5051 7.27878 10.5131C7.07003 10.5212 6.86739 10.4417 6.71967 10.294L5.21967 8.79402C4.92678 8.50112 4.92678 8.02625 5.21967 7.73336C5.51256 7.44046 5.98744 7.44046 6.28033 7.73336L7.20764 8.66066L9.68056 5.77559C9.95012 5.4611 10.4236 5.42468 10.7381 5.69424Z" />
</symbol>
<symbol id="status-canceled" viewBox="1 1 14 14" fill="#8A8F98">
<path d="M5.96967 5.96967C6.26256 5.67678 6.73744 5.67678 7.03033 5.96967L8 6.93934L8.96967 5.96967C9.26256 5.67678 9.73744 5.67678 10.0303 5.96967C10.3232 6.26256 10.3232 6.73744 10.0303 7.03033L9.06066 8L10.0303 8.96967C10.3232 9.26256 10.3232 9.73744 10.0303 10.0303C9.73744 10.3232 9.26256 10.3232 8.96967 10.0303L8 9.06066L7.03033 10.0303C6.73744 10.3232 6.26256 10.3232 5.96967 10.0303C5.67678 9.73744 5.67678 9.26256 5.96967 8.96967L6.93934 8L5.96967 7.03033C5.67678 6.73744 5.67678 6.26256 5.96967 5.96967Z" />
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.75581 1.21148C8.28876 0.929507 7.71124 0.929507 7.24419 1.21148L2.74419 3.92829C2.28337 4.20651 2 4.71711 2 5.26927V10.7307C2 11.2829 2.28337 11.7935 2.74419 12.0717L7.24419 14.7885C7.71124 15.0705 8.28876 15.0705 8.75581 14.7885L13.2558 12.0717C13.7166 11.7935 14 11.2829 14 10.7307V5.26927C14 4.71711 13.7166 4.20651 13.2558 3.92829L8.75581 1.21148ZM12.5 5.26928L8 2.55246L3.5 5.26927L3.5 10.7307L8 13.4475L12.5 10.7307L12.5 5.26928Z" />
</symbol>
<symbol id="document" viewBox="0 0 32 32">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 4C7.89543 4 7 4.89543 7 6V26C7 27.1046 7.89543 28 9 28H23C24.1046 28 25 27.1046 25 26V12H21C18.7909 12 17 10.2091 17 8V4H9ZM19 4.41421V8C19 9.10457 19.8954 10 21 10H24.5858L19 4.41421ZM5 6C5 3.79086 6.79086 2 9 2H18.5858C19.1162 2 19.6249 2.21071 20 2.58579L26.4142 9C26.7893 9.37507 27 9.88378 27 10.4142V26C27 28.2091 25.2091 30 23 30H9C6.79086 30 5 28.2091 5 26V6ZM10 17C10 16.4477 10.4477 16 11 16H21C21.5523 16 22 16.4477 22 17C22 17.5523 21.5523 18 21 18H11C10.4477 18 10 17.5523 10 17ZM10 23C10 22.4477 10.4477 22 11 22H21C21.5523 22 22 22.4477 22 23C22 23.5523 21.5523 24 21 24H11C10.4477 24 10 23.5523 10 23Z" />
</symbol>
<symbol id="test-library" viewBox="0 0 24 24">
<path d="M21,2.2h-5h-5c-0.4,0-0.8,0.3-0.8,0.8v18c0,0.4,0.3,0.8,0.8,0.8h5h5c0.4,0,0.8-0.3,0.8-0.8V3C21.8,2.6,21.4,2.2,21,2.2z M11.8,3.8h3.5v16.5h-3.5V3.8z M20.2,20.2h-3.5V3.8h3.5V20.2z"/>
<path d="M9.1,2.8l-4-0.5c-0.2,0-0.4,0-0.6,0.2S4.3,2.7,4.3,2.9l-2,17.5c0,0.4,0.2,0.8,0.7,0.8l4.2,0.5c0.2,0,0.4,0,0.6-0.2C7.9,21.5,8,21.3,8,21.1L9.7,3.6C9.8,3.2,9.5,2.8,9.1,2.8z M6.6,20.2l-2.7-0.3l1.8-16l2.5,0.3L6.6,20.2z"/>
<path d="M18.5,9.8c0.4,0,0.8-0.3,0.8-0.8V7.5c0-0.4-0.3-0.8-0.8-0.8s-0.8,0.3-0.8,0.8V9C17.8,9.4,18.1,9.8,18.5,9.8z"/>
<path d="M13.5,6.8c-0.4,0-0.8,0.3-0.8,0.8V9c0,0.4,0.3,0.8,0.8,0.8s0.8-0.3,0.8-0.8V7.5C14.2,7.1,13.9,6.8,13.5,6.8z"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "assets"
}

View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}

View File

@ -0,0 +1,66 @@
{
"string": {
"ConfigLabel": "Test Management",
"ConfigDescription": "Extension to manage test cases",
"TestCaseType": "Type",
"TestCasePriority": "Priority",
"TestCaseStatus": "Status",
"TestSuite": "Test Suite",
"SuiteName": "Name",
"SuiteDescription": "Description",
"Suite": "Suite",
"TestName": "Name",
"TestDescription": "Description",
"TestType": "Type",
"TestPriority": "Priority",
"TestStatus": "Status",
"TestEstimatedTime": "Estimated time",
"TestPreconditions": "Preconditions",
"TestSteps": "Steps",
"TestAssignee": "Assignee",
"TestCase": "Test case",
"TestProject": "Project",
"TestManagementApplication": "Test Management",
"AllTestCases": "All test cases",
"AllProjects": "All projects",
"Projects": "Projects",
"CreateProject": "Create project",
"EditProject": "Edit project",
"TestCases": "Test cases",
"TestManagementDescription": "Extension to manage test cases",
"CreateTestCase": "New test case",
"FullDescription": "Description",
"ProjectName": "Name",
"ProjectType": "Type",
"Members": "Members",
"RoleLabel": "Role",
"ProjectMembers": "Project members",
"TestSuites": "Test suites",
"CreateTestSuite": "Create test suite",
"NamePlaceholder": "Suite name",
"DescriptionPlaceholder": "Description (optional)",
"TestRuns": "Test runs",
"NewTestRun": "New test run",
"TestRun": "Test run",
"TestNamePlaceholder": "Test case title",
"ChooseIcon": "Choose icon",
"NoTestSuite": "No test suite",
"StatusDraft": "Draft",
"StatusReview": "Ready for review",
"StatusReviewComments": "Need to fix review comments",
"StatusApproved": "Approved",
"StatusRejected": "Rejected",
"SetStatus": "Set status",
"Assignee": "Assignee",
"Unassigned": "Unassigned",
"AssignTo": "Assign to",
"AssignedTo": "Assigneed to",
"PreviousAssigned": "Previously assigned",
"NoTestCases": "There are no test cases in this test suite",
"CreateTestRun": "Create test run",
"TestRunNamePlaceholder": "Test run name",
"SelectTestSuites": "Select test suites",
"SelectTestCases": "Select test cases",
"TestLibrary": "Test library"
}
}

View File

@ -0,0 +1,66 @@
{
"string": {
"ConfigLabel": "Gestion des tests",
"ConfigDescription": "Extension pour gérer les cas de tests",
"TestCaseType": "Taper",
"TestCasePriority": "Priorité",
"TestCaseStatus": "Statut",
"TestSuite": "Suite de tests",
"SuiteName": "Nom",
"SuiteDescription": "Description",
"Suite": "Suite",
"TestName": "Nom",
"TestDescription": "Description",
"TestType": "Taper",
"TestPriority": "Priorité",
"TestStatus": "Statut",
"TestEstimatedTime": "Temps estimé",
"TestPreconditions": "Conditions préalables",
"TestSteps": "Mesures",
"TestAssignee": "Cessionnaire",
"TestCase": "Cas de test",
"TestProject": "Projet",
"TestManagementApplication": "Gestion des tests",
"AllTestCases": "Tous les cas de tests",
"AllProjects": "Tous les projets",
"Projects": "Projets",
"CreateProject": "Créer un projet",
"EditProject": "Modifier le projet",
"TestCases": "Cas de tests",
"TestManagementDescription": "Extension pour gérer les cas de tests",
"CreateTestCase": "Nouveau cas de test",
"FullDescription": "Description",
"ProjectName": "Nom",
"ProjectType": "Taper",
"Members": "Membres",
"RoleLabel": "Rôle",
"ProjectMembers": "Membres du projet",
"TestSuites": "Suites de tests",
"CreateTestSuite": "Créer une suite de tests",
"NamePlaceholder": "Nom de la suite",
"DescriptionPlaceholder": "Description (facultatif)",
"TestRuns": "Exécutions de tests",
"NewTestRun": "Nouveau test",
"TestRun": "Exécution d'essai",
"TestNamePlaceholder": "Titre du scénario de test",
"ChooseIcon": "Choisir l'icône",
"NoTestSuite": "Pas de suite de tests",
"StatusDraft": "Brouillon",
"StatusReview": "Prêt pour l'examen",
"StatusReviewComments": "Besoin de corriger les commentaires d'évaluation",
"StatusApproved": "Approuvé",
"StatusRejected": "Rejeté",
"SetStatus": "Définir le statut",
"Assignee": "Attribué à",
"Unassigned": "Non assigné",
"AssignTo": "Assigner à...",
"AssignedTo": "Assigné à {value}",
"PreviousAssigned": "Assigné précédemment",
"NoTestCases": "Il n'y a aucun cas de test dans cette suite de tests",
"CreateTestRun": "Créer une série de tests",
"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"
}
}

View File

@ -0,0 +1,66 @@
{
"string": {
"ConfigLabel": "Управление тестированием",
"ConfigDescription": "Расширение для управления тестирование",
"TestCaseType": "Тип",
"TestCasePriority": "Приоритет",
"TestCaseStatus": "Статус",
"TestSuite": "Тестовый набор",
"SuiteName": "Имя",
"SuiteDescription": "Описание",
"Suite": "Тестовый набор",
"TestName": "Имя",
"TestDescription": "Описание",
"TestType": "Тип",
"TestPriority": "Приоритет",
"TestStatus": "Статус",
"TestEstimatedTime": "Расчетное время",
"TestPreconditions": "Предварительные условия",
"TestSteps": "Шаги",
"TestAssignee": "Исполнитель",
"TestCase": "Тест-кейс",
"TestProject": "Проект",
"TestManagementApplication": "Управление тестированием",
"AllTestCases": "Все тест-кейсы",
"AllProjects": "Все проекты",
"Projects": "Проекты",
"CreateProject": "Создать проект",
"EditProject": "Отредактировать проект",
"TestCases": "Тест-кейсы",
"TestManagementDescription": "Расширение для управления тестирование",
"CreateTestCase": "Новый тест-кейс",
"FullDescription": "Описание",
"ProjectName": "Имя",
"ProjectType": "Тип",
"Members": "Участники",
"RoleLabel": "Роль",
"ProjectMembers": "Участники проекта",
"TestSuites": "Тестовые наборы",
"CreateTestSuite": "Создать тестовый набор",
"NamePlaceholder": "Имя тестового набора",
"DescriptionPlaceholder": "Описание (опционально)",
"TestRuns": "Выполнение тестов",
"NewTestRun": "Новый тест план",
"TestRun": "Тест план",
"TestNamePlaceholder": "Имя тест кейса",
"ChooseIcon": "Выберите иконку",
"NoTestSuite": "Тестовый набор не задан",
"StatusDraft": "В прогрессе",
"StatusReview": "Готов для ревью",
"StatusReviewComments": "Требует исправлений",
"StatusApproved": "Согласован",
"StatusRejected": "Отклонен",
"SetStatus": "Выбрать статус",
"Assignee": "Исполнитель",
"Unassigned": "Не назначен",
"AssignTo": "Назначить на",
"AssignedTo": "Назначено на",
"PreviousAssigned": "Ранее назначенные",
"NoTestCases": "Тест-кейсы отсутствует",
"CreateTestRun": "Создать тест план",
"TestRunNamePlaceholder": "Название тест плана",
"SelectTestSuites": "Выбрать наборы тестов",
"SelectTestCases": "Выбрать тест-кейсы",
"TestLibrary": "Библиотека тестов"
}
}

View File

@ -0,0 +1,66 @@
{
"string": {
"ConfigLabel": "測試管理",
"ConfigDescription": "管理測試用例的擴展",
"TestCaseType": "類型",
"TestCasePriority": "優先事項",
"TestCaseStatus": "地位",
"TestSuite": "測試套件",
"SuiteName": "姓名",
"SuiteDescription": "描述",
"Suite": "Suite",
"TestName": "姓名",
"TestDescription": "描述",
"TestType": "類型",
"TestPriority": "優先事項",
"TestStatus": "地位",
"TestEstimatedTime": "預計時間",
"TestPreconditions": "前提條件",
"TestSteps": "步驟",
"TestAssignee": "受讓人",
"TestCase": "測試用例",
"TestProject": "專案",
"TestManagementApplication": "測試管理",
"AllTestCases": "所有測試用例",
"AllProjects": "所有項目",
"Projects": "專案",
"CreateProject": "創建專案",
"EditProject": "編輯項目",
"TestCases": "測試用例",
"TestManagementDescription": "管理測試用例的擴展",
"CreateTestCase": "新測試用例",
"FullDescription": "描述",
"ProjectName": "姓名",
"ProjectType": "類型",
"Members": "會員",
"RoleLabel": "角色",
"ProjectMembers": "專案成員",
"TestSuites": "測試套件",
"CreateTestSuite": "建立測試套件",
"NamePlaceholder": "套房名稱",
"DescriptionPlaceholder": "說明(可選)",
"TestRuns": "試運行",
"NewTestRun": "新試運行",
"TestRun": "試運行",
"TestNamePlaceholder": "測試用例標題",
"ChooseIcon": "選擇圖示",
"NoTestSuite": "沒有測試套件",
"StatusDraft": "草稿",
"StatusReview": "準備審查",
"StatusReviewComments": "需要修復審核意見",
"StatusApproved": "得到正式認可的",
"StatusRejected": "被拒絕",
"SetStatus": "設定狀態",
"Assignee": "受讓人",
"Unassigned": "未分配",
"AssignTo": "分配給",
"AssignedTo": "分配給",
"PreviousAssigned": "先前分配的",
"NoTestCases": "該測試套件中沒有測試案例",
"CreateTestRun": "建立測試運行",
"TestRunNamePlaceholder": "測試運行名稱",
"SelectTestSuites": "選擇測試套件",
"SelectTestCases": "選擇測試用例",
"TestLibrary": "測試庫"
}
}

View File

@ -0,0 +1,39 @@
{
"name": "@hcengineering/test-management-assets",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Anticrm Platform Contributors",
"template": "@hcengineering/assets-package",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"test": "jest --passWithNoTests --silent",
"build:docs": "",
"format": "format src",
"build:watch": "compile",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"@types/node": "~20.11.16",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"typescript": "^5.3.3"
},
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@hcengineering/test-management": "^0.6.0"
}
}

View File

@ -0,0 +1,6 @@
import { makeLocalesTest } from '@hcengineering/platform'
it(
'Locales are equale',
makeLocalesTest((lang) => import(`../../lang/${lang}.json`))
)

View File

@ -0,0 +1,40 @@
//
// 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 testManagement from '@hcengineering/test-management'
import { loadMetadata } from '@hcengineering/platform'
const icons = require('../assets/icons.svg') as string // eslint-disable-line
loadMetadata(testManagement.icon, {
TestCase: `${icons}#testCase`,
TestManagementApplication: `${icons}#testManagementApplication`,
TestManagement: `${icons}#testCase`,
TestManagementVersion: `${icons}#testCase`,
TestCases: `${icons}#testCase`,
Home: `${icons}#home`,
Estimation: `${icons}#testCase`,
TestSuite: `${icons}#testSuite`,
TestProject: `${icons}#testCase`,
TestSuites: `${icons}#testSuite`,
TestRuns: `${icons}#testRun`,
RedCircle: `${icons}#red-circle`,
Document: `${icons}#document`,
StatusDraft: `${icons}#status-draft`,
StatusReview: `${icons}#status-review`,
StatusReviewComments: `${icons}#status-review-comments`,
StatusApproved: `${icons}#status-approved`,
StatusRejected: `${icons}#status-canceled`,
TestLibrary: `${icons}#test-library`
})

View File

@ -0,0 +1,11 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/assets/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"types": ["node", "jest"],
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}

View File

@ -0,0 +1,4 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/ui/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname }
}

View File

@ -0,0 +1,22 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120,
"useTabs": false,
"bracketSpacing": true,
"proseWrap": "preserve",
"plugins": [
"prettier-plugin-svelte"
],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "ui"
}

View File

@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)']
}

View File

@ -0,0 +1,77 @@
{
"name": "@hcengineering/test-management-resources",
"version": "0.6.0",
"main": "src/index.ts",
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile ui",
"build:docs": "api-extractor run --local",
"svelte-check": "do-svelte-check",
"_phase:svelte-check": "do-svelte-check",
"format": "format src",
"build:watch": "compile ui",
"_phase:build": "compile ui",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"svelte-loader": "^3.2.0",
"sass": "^1.53.0",
"svelte-preprocess": "^5.1.3",
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-n": "^15.4.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-svelte": "^2.35.1",
"prettier-plugin-svelte": "^3.2.2",
"eslint": "^8.54.0",
"prettier": "^3.1.0",
"svelte-check": "^3.6.9",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5",
"svelte-eslint-parser": "^0.33.1"
},
"dependencies": {
"@hcengineering/activity": "^0.6.0",
"@hcengineering/activity-resources": "^0.6.1",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/attachment": "^0.6.14",
"@hcengineering/attachment-resources": "^0.6.0",
"@hcengineering/calendar": "^0.6.24",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/chunter-resources": "^0.6.0",
"@hcengineering/client": "^0.6.18",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/contact-resources": "^0.6.0",
"@hcengineering/core": "^0.6.32",
"@hcengineering/kanban": "^0.6.0",
"@hcengineering/login": "^0.6.12",
"@hcengineering/notification": "^0.6.23",
"@hcengineering/notification-resources": "^0.6.0",
"@hcengineering/panel": "^0.6.23",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/preference": "^0.6.13",
"@hcengineering/presentation": "^0.6.3",
"@hcengineering/query": "^0.6.12",
"@hcengineering/setting": "^0.6.17",
"@hcengineering/tags": "^0.6.16",
"@hcengineering/task": "^0.6.20",
"@hcengineering/task-resources": "^0.6.0",
"@hcengineering/text-editor-resources": "^0.6.0",
"@hcengineering/text": "^0.6.5",
"@hcengineering/test-management": "^0.6.0",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/view-resources": "^0.6.0",
"@hcengineering/workbench": "^0.6.16",
"@hcengineering/workbench-resources": "^0.6.1",
"fast-equals": "^5.0.1",
"svelte": "^4.2.19"
}
}

View File

@ -0,0 +1,5 @@
module.exports = {
plugins: [
require('autoprefixer')
]
}

View File

@ -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">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
fill="var(--duotone-color)"
d="M13,7V3H9C7.1,3,6.2,3,5.6,3.6C5,4.2,5,5.1,5,7v10c0,1.9,0,2.8,0.6,3.4C6.2,21,7.1,21,9,21h6 c1.9,0,2.8,0,3.4-0.6S19,18.9,19,17V9h-4c-0.9,0-1.4,0-1.7-0.3C13,8.4,13,7.9,13,7z"
/>
<g {fill}>
<path d="M9,12.4c-0.3,0-0.6,0.3-0.6,0.6s0.3,0.6,0.6,0.6h6c0.3,0,0.6-0.3,0.6-0.6s-0.3-0.6-0.6-0.6H9z" />
<path d="M13,16.4H9c-0.3,0-0.6,0.3-0.6,0.6s0.3,0.6,0.6,0.6h4c0.3,0,0.6-0.3,0.6-0.6S13.3,16.4,13,16.4z" />
<path
d="M19.5,7.8c-0.1-0.3-0.3-0.5-0.6-0.8L15,3.2c-0.3-0.3-0.5-0.5-0.8-0.6s-0.6-0.1-1-0.1H9c-2,0-3.1,0-3.8,0.8 C4.4,3.9,4.4,5,4.4,7v10c0,2,0,3.1,0.8,3.8C5.9,21.6,7,21.6,9,21.6h6c2,0,3.1,0,3.8-0.8s0.8-1.9,0.8-3.8V8.8 C19.6,8.4,19.6,8.1,19.5,7.8z M13.6,3.6c0.1,0,0.1,0,0.1,0c0.1,0,0.2,0.2,0.4,0.4L18,7.8c0.2,0.2,0.3,0.3,0.4,0.4c0,0,0,0.1,0,0.1 H15c-0.7,0-1.2,0-1.3-0.1S13.6,7.7,13.6,7V3.6z M18.4,17c0,1.8,0,2.6-0.4,3c-0.4,0.4-1.2,0.4-3,0.4H9c-1.8,0-2.6,0-3-0.4 c-0.4-0.4-0.4-1.2-0.4-3V7c0-1.8,0-2.6,0.4-3c0.4-0.4,1.2-0.4,3-0.4h3.4V7c0,1,0,1.7,0.5,2.1C13.3,9.6,14,9.6,15,9.6h3.4V17z"
/>
</g>
</svg>

View File

@ -0,0 +1,390 @@
<!--
// 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 { deepEqual } from 'fast-equals'
import { createEventDispatcher } from 'svelte'
import { AccountArrayEditor } from '@hcengineering/contact-resources'
import { Asset } from '@hcengineering/platform'
import core, {
Account,
Data,
DocumentUpdate,
RolesAssignment,
Ref,
Role,
SpaceType,
generateId,
getCurrentAccount,
WithLookup
} from '@hcengineering/core'
import view from '@hcengineering/view'
import testManagement, { TestProject } from '@hcengineering/test-management'
import presentation, { Card, getClient, reduceCalls } from '@hcengineering/presentation'
import {
Button,
EditBox,
IconWithEmoji,
Label,
Toggle,
getColorNumberByText,
showPopup,
getPlatformColorDef,
getPlatformColorForTextDef,
themeStore
} from '@hcengineering/ui'
import { IconPicker, SpaceTypeSelector } from '@hcengineering/view-resources'
import testManagementRes from '../../plugin'
export let project: TestProject | undefined = undefined
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
let name: string = project?.name ?? ''
let description: string = project?.description ?? ''
let isPrivate: boolean = project?.private ?? false
let icon: Asset | undefined = project?.icon ?? undefined
let color = project?.color ?? getColorNumberByText(name)
let isColorSelected = false
let members: Ref<Account>[] =
project?.members !== undefined ? hierarchy.clone(project.members) : [getCurrentAccount()._id]
let owners: Ref<Account>[] =
project?.owners !== undefined ? hierarchy.clone(project.owners) : [getCurrentAccount()._id]
let rolesAssignment: RolesAssignment = {}
let typeId: Ref<SpaceType> | undefined = project?.type ?? testManagementRes.spaceType.DefaultProject
let spaceType: WithLookup<SpaceType> | undefined
$: void loadSpaceType(typeId)
const loadSpaceType = reduceCalls(async (id: typeof typeId): Promise<void> => {
spaceType =
id !== undefined
? await client
.getModel()
.findOne(core.class.SpaceType, { _id: id }, { lookup: { _id: { roles: core.class.Role } } })
: undefined
if (project === undefined || spaceType?.targetClass === undefined || spaceType?.$lookup?.roles === undefined) {
return
}
rolesAssignment = getRolesAssignment()
})
function getRolesAssignment (): RolesAssignment {
if (project === undefined || spaceType?.targetClass === undefined || spaceType?.$lookup?.roles === undefined) {
return {}
}
const asMixin = hierarchy.as(project, spaceType?.targetClass)
return spaceType.$lookup.roles.reduce<RolesAssignment>((prev, { _id }) => {
prev[_id as Ref<Role>] = (asMixin as any)[_id] ?? []
return prev
}, {})
}
async function handleSave (): Promise<void> {
if (project === undefined) {
await createTestProject()
} else {
await updateTestProject()
}
}
function getTestProjectData (): Omit<Data<TestProject>, 'type'> {
return {
name,
description,
private: isPrivate,
icon,
members,
owners,
archived: false
}
}
async function updateTestProject (): Promise<void> {
if (project === undefined || spaceType?.targetClass === undefined) {
return
}
const data = getTestProjectData()
const update: DocumentUpdate<TestProject> = {}
if (data.name !== project?.name) {
update.name = data.name
}
if (data.description !== project?.description) {
update.description = data.description
}
if (data.private !== project?.private) {
update.private = data.private
}
if (data.icon !== project?.icon) {
update.icon = data.icon
}
if (data.members.length !== project?.members.length) {
update.members = data.members
} else {
for (const member of data.members) {
if (project.members.findIndex((p) => p === member) === -1) {
update.members = data.members
break
}
}
}
if (data.owners?.length !== project?.owners?.length) {
update.owners = data.owners
} else {
for (const owner of data.owners ?? []) {
if (project.owners?.findIndex((p) => p === owner) === -1) {
update.owners = data.owners
break
}
}
}
if (Object.keys(update).length > 0) {
await client.update(project, update)
}
if (!deepEqual(rolesAssignment, getRolesAssignment())) {
await client.updateMixin(
project._id,
testManagementRes.class.TestProject,
core.space.Space,
spaceType.targetClass,
rolesAssignment
)
}
close()
}
async function createTestProject (): Promise<void> {
if (typeId === undefined || spaceType?.targetClass === undefined) {
return
}
const projectId = generateId<TestProject>()
const projectData = getTestProjectData()
await client.createDoc(
testManagementRes.class.TestProject,
core.space.Space,
{ ...projectData, type: typeId },
projectId
)
// Create space type's mixin with roles assignments
await client.createMixin(
projectId,
testManagementRes.class.TestProject,
core.space.Space,
spaceType.targetClass,
rolesAssignment
)
// TODO: Analytics.handleEvent(TestProjectEvents.TestProjectCreated, { id: projectId })
close(projectId)
}
function close (id?: Ref<TestProject>): void {
dispatch('close', id)
}
function handleTypeChange (evt: CustomEvent<Ref<SpaceType>>): void {
typeId = evt.detail
}
$: roles = (spaceType?.$lookup?.roles ?? []) as Role[]
function handleOwnersChanged (newOwners: Ref<Account>[]): void {
owners = newOwners
const newMembersSet = new Set([...members, ...newOwners])
members = Array.from(newMembersSet)
}
function handleMembersChanged (newMembers: Ref<Account>[]): void {
// If a member was removed we need to remove it from any roles assignments as well
const newMembersSet = new Set(newMembers)
const removedMembersSet = new Set(members.filter((m) => !newMembersSet.has(m)))
if (removedMembersSet.size > 0 && rolesAssignment !== undefined) {
for (const [key, value] of Object.entries(rolesAssignment)) {
rolesAssignment[key as Ref<Role>] = value != null ? value.filter((m) => !removedMembersSet.has(m)) : undefined
}
}
members = newMembers
}
function chooseIcon (ev: MouseEvent): void {
const icons = [testManagement.icon.Home, testManagement.icon.RedCircle]
const update = (result: any): void => {
if (result !== undefined && result !== null) {
icon = result.icon
color = result.color
isColorSelected = true
}
}
showPopup(IconPicker, { icon, color, icons }, 'top', update, update)
}
function handleRoleAssignmentChanged (roleId: Ref<Role>, newMembers: Ref<Account>[]): void {
if (rolesAssignment === undefined) {
rolesAssignment = {}
}
rolesAssignment[roleId] = newMembers
}
$: canSave =
name.trim().length > 0 &&
!(members.length === 0 && isPrivate) &&
typeId !== undefined &&
spaceType?.targetClass !== undefined &&
owners.length > 0 &&
(!isPrivate || owners.some((o) => members.includes(o)))
</script>
<Card
label={project ? testManagementRes.string.EditProject : testManagementRes.string.CreateProject}
okLabel={project ? presentation.string.Save : presentation.string.Create}
okAction={handleSave}
{canSave}
accentHeader
width={'medium'}
gap={'gapV-6'}
onCancel={close}
on:changeContent
>
<div class="antiGrid">
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={core.string.SpaceType} />
</div>
<SpaceTypeSelector
disabled={project !== undefined}
descriptors={[testManagementRes.descriptors.ProjectType]}
type={typeId}
focusIndex={4}
kind="regular"
size="large"
on:change={handleTypeChange}
/>
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={core.string.Name} />
</div>
<div class="padding">
<EditBox id="teamspace-title" bind:value={name} placeholder={core.string.Name} kind={'large-style'} autoFocus />
</div>
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header topAlign">
<Label label={core.string.Description} />
</div>
<div class="padding">
<EditBox id="teamspace-description" bind:value={description} placeholder={core.string.Description} />
</div>
</div>
</div>
<div class="antiGrid">
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={testManagementRes.string.ChooseIcon} />
</div>
<Button
icon={icon === view.ids.IconWithEmoji ? IconWithEmoji : icon ?? testManagement.icon.Home}
iconProps={icon === view.ids.IconWithEmoji
? { icon: color }
: {
fill:
color !== undefined
? getPlatformColorDef(color, $themeStore.dark).icon
: getPlatformColorForTextDef(name, $themeStore.dark).icon
}}
size={'large'}
on:click={chooseIcon}
/>
</div>
<div class="antiGrid">
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={core.string.Owners} />
</div>
<AccountArrayEditor
value={owners}
label={core.string.Owners}
onChange={handleOwnersChanged}
kind={'regular'}
size={'large'}
/>
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header withDesciption">
<Label label={presentation.string.MakePrivate} />
<span><Label label={presentation.string.MakePrivateDescription} /></span>
</div>
<Toggle bind:on={isPrivate} disabled={!isPrivate && members.length === 0} />
</div>
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={core.string.Members} />
</div>
<AccountArrayEditor
value={members}
label={core.string.Members}
onChange={handleMembersChanged}
kind={'regular'}
size={'large'}
allowGuests
/>
</div>
{#each roles as role}
<div class="antiGrid-row">
<div class="antiGrid-row__header">
<Label label={testManagementRes.string.RoleLabel} params={{ role: role.name }} />
</div>
<AccountArrayEditor
value={rolesAssignment?.[role._id] ?? []}
label={core.string.Members}
includeItems={members}
readonly={members.length === 0}
onChange={(refs) => {
handleRoleAssignmentChanged(role._id, refs)
}}
kind={'regular'}
size={'large'}
/>
</div>
{/each}
</div>
</div></Card
>

View File

@ -0,0 +1,29 @@
<!--
// 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 { TestProject } from '@hcengineering/test-management'
export let value: TestProject | undefined
export let inline: boolean = false
export let accent: boolean = false
</script>
{#if value}
<div class="flex-presenter cursor-default" class:inline-presenter={inline}>
<span class="label no-underline nowrap" class:fs-bold={accent}>
{value.name}
</span>
</div>
{/if}

View File

@ -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 { Ref, Space } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { TestProject } from '@hcengineering/test-management'
import {
IconWithEmoji,
getPlatformColorDef,
getPlatformColorForTextDef,
themeStore,
type Action
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { NavLink, TreeNode } from '@hcengineering/view-resources'
import { SpacesNavModel, SpecialNavModel } from '@hcengineering/workbench'
import { SpecialElement } from '@hcengineering/workbench-resources'
export let space: TestProject
export let model: SpacesNavModel
export let currentSpace: Ref<Space> | undefined
export let currentSpecial: string | undefined
export let getActions: (space: TestProject) => Promise<Action[]> = async () => []
export let deselect: boolean = false
export let forciblyСollapsed: boolean = false
let specials: SpecialNavModel[] = []
async function updateSpecials (model: SpacesNavModel, space: TestProject): Promise<void> {
const newSpecials: SpecialNavModel[] = []
for (const sp of model.specials ?? []) {
let shouldAdd = true
if (sp.visibleIf !== undefined) {
const visibleIf = await getResource(sp.visibleIf)
if (visibleIf !== undefined) {
shouldAdd = await visibleIf([space])
}
}
if (shouldAdd) {
newSpecials.push(sp)
}
}
specials = newSpecials
}
$: updateSpecials(model, space)
$: visible =
(!deselect && currentSpace !== undefined && currentSpecial !== undefined && space._id === currentSpace) ||
forciblyСollapsed
</script>
{#if specials}
<TreeNode
_id={space?._id}
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
iconProps={space?.icon === view.ids.IconWithEmoji
? { icon: space.color }
: {
fill:
space.color !== undefined
? getPlatformColorDef(space.color, $themeStore.dark).icon
: getPlatformColorForTextDef(space.name, $themeStore.dark).icon
}}
title={space.name}
type={'nested'}
highlighted={space._id === currentSpace}
{visible}
actions={() => getActions(space)}
{forciblyСollapsed}
>
{#each specials as special}
<NavLink space={space._id} special={special.id}>
<SpecialElement
indent
label={special.label}
icon={special.icon}
selected={deselect ? false : currentSpace === space._id && special.id === currentSpecial}
/>
</NavLink>
{/each}
<svelte:fragment slot="visible">
{#if visible}
{@const item = specials.find((sp) => sp.id === currentSpecial && currentSpace === space._id)}
{#if item}
<NavLink space={space._id} special={item.id}>
<SpecialElement indent label={item.label} icon={item.icon} selected forciblyСollapsed />
</NavLink>
{/if}
{/if}
</svelte:fragment>
</TreeNode>
{/if}

View File

@ -0,0 +1,198 @@
<!--
// 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}

View File

@ -0,0 +1,200 @@
<!--
// 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 { Attachment } from '@hcengineering/attachment'
import { AttachmentPresenter, AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { TestCase, TestProject, TestSuite, TestCaseStatus } from '@hcengineering/test-management'
import core, { fillDefaults, generateId, makeCollaborativeDoc, Ref, TxOperations, Data } from '@hcengineering/core'
import { ObjectBox } from '@hcengineering/view-resources'
import { Card, SpaceSelector, getClient, updateMarkup } from '@hcengineering/presentation'
import { EmptyMarkup } from '@hcengineering/text'
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'
export let onCreate: ((orgId: Ref<TestCase>, client: TxOperations) => Promise<void>) | undefined = undefined
export function canClose (): boolean {
return object.name === ''
}
export let space: Ref<TestProject>
export let testSuiteId: Ref<TestSuite> | undefined
testSuiteId = testSuiteId ?? (getLocation()?.query?.attachedTo as Ref<TestSuite>)
const id: Ref<TestCase> = generateId()
const object: Data<TestCase> = {
name: '',
description: makeCollaborativeDoc(id, 'description'),
status: TestCaseStatus.Draft,
assignee: null,
attachments: 0,
attachedTo: testSuiteId
} as unknown as TestCase
let _space = space
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
let description = EmptyMarkup
fillDefaults(hierarchy, object, testManagement.class.TestCase)
async function createTestCase (): Promise<void> {
const op = client.apply()
await updateMarkup(object.description, { description })
await op.addCollection(
testManagement.class.TestCase,
_space,
testSuiteId ?? testManagement.ids.NoParent,
testManagement.class.TestSuite,
'testCases',
object,
id
)
await descriptionBox.createAttachments(id, op)
if (onCreate !== undefined) {
await onCreate?.(id, op)
}
await op.commit()
dispatch('close', id)
}
function handleTestSuiteChange (evt: CustomEvent<Ref<TestSuite>>): void {
object.attachedTo = evt.detail
}
const manager = createFocusManager()
let descriptionBox: AttachmentStyledBox
let attachments: Map<Ref<Attachment>, Attachment> = new Map<Ref<Attachment>, Attachment>()
</script>
<FocusHandler {manager} />
<Card
label={testManagement.string.CreateTestCase}
okAction={createTestCase}
hideAttachments={attachments.size === 0}
canSave={object.name.length > 0}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<svelte:fragment slot="header">
<SpaceSelector
_class={testManagement.class.TestProject}
label={testManagement.string.TestProject}
bind:space={_space}
kind={'regular'}
size={'small'}
component={ProjectPresenter}
defaultIcon={testManagement.icon.Home}
/>
<ObjectBox
_class={testManagement.class.TestSuite}
value={testSuiteId}
docQuery={{
space: _space
}}
on:change={handleTestSuiteChange}
kind={'regular'}
size={'small'}
label={testManagement.string.NoTestSuite}
icon={testManagement.icon.TestSuite}
searchField={'title'}
allowDeselect={true}
showNavigate={false}
docProps={{ disabled: true, noUnderline: true }}
focusIndex={20000}
/>
</svelte:fragment>
<div class="flex-row-center clear-mins mb-3">
<EditBox
bind:value={object.name}
placeholder={testManagement.string.TestNamePlaceholder}
kind={'large-style'}
autoFocus
/>
</div>
<AttachmentStyledBox
bind:this={descriptionBox}
objectId={id}
_class={testManagement.class.TestCase}
space={_space}
alwaysEdit
showButtons={false}
bind:content={description}
placeholder={core.string.Description}
kind="indented"
isScrollable={false}
enableBackReferences={true}
enableAttachments={false}
on:attachments={(ev) => {
if (ev.detail.size > 0) attachments = ev.detail.values
else if (ev.detail.size === 0 && ev.detail.values != null) {
attachments.clear()
attachments = attachments
}
}}
/>
<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>
<svelte:fragment slot="attachments">
{#if attachments.size > 0}
{#each Array.from(attachments.values()) as attachment}
<AttachmentPresenter
value={attachment}
showPreview
removable
on:remove={(result) => {
if (result.detail !== undefined) descriptionBox.removeAttachmentById(result.detail._id)
}}
/>
{/each}
{/if}
</svelte:fragment>
<svelte:fragment slot="footer">
<Button
icon={IconAttachment}
size="large"
on:click={() => {
descriptionBox.handleAttach()
}}
/>
</svelte:fragment>
</Card>

View File

@ -0,0 +1,103 @@
<!--
// 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 { 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 } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import testManagement from '../../plugin'
export let _id: Ref<TestCase>
export let _class: Ref<Class<TestCase>>
let object: TestCase | 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 TestCase> (field: K, value: TestCase[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.TestCase, 'description')
onMount(() => dispatch('open', { ignoreKeys: [] }))
</script>
{#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>
</Panel>
{/if}

View File

@ -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 { getClient } from '@hcengineering/presentation'
import { Button, IconAdd, showPopup } from '@hcengineering/ui'
import testManagement from '../../plugin'
import CreateTestCase from './CreateTestCase.svelte'
import { openDoc } from '@hcengineering/view-resources'
const client = getClient()
async function newTestCase (): Promise<void> {
showPopup(CreateTestCase, {}, 'top', async (id) => {
if (id != null) {
const doc = await client.findOne(testManagement.class.TestCase, { _id: id })
if (doc !== undefined) {
void openDoc(client.getHierarchy(), doc)
}
}
})
}
</script>
<div class="antiNav-subheader">
<Button
icon={IconAdd}
label={testManagement.string.CreateTestCase}
kind={'primary'}
justify={'left'}
width="100%"
on:click={newTestCase}
/>
</div>

View File

@ -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 } from 'svelte'
import { Data } from '@hcengineering/core'
import { TestCase } from '@hcengineering/test-management'
import { getClient } from '@hcengineering/presentation'
import {
Button,
ButtonKind,
ButtonSize,
Icon,
SelectPopup,
eventToHTMLElement,
showPopup,
Label
} from '@hcengineering/ui'
import { defaultTestCaseStatuses, testCaseStatusAssets } from '../../types'
import testManagement from '../../plugin'
export let value: TestCase['status'] | undefined
export let object: TestCase | Data<TestCase>
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()
function handlePopupOpen (event: MouseEvent) {
showPopup(
SelectPopup,
{ value: itemsInfo, placeholder: testManagement.string.SetStatus },
eventToHTMLElement(event),
changeStatus
)
}
async function changeStatus (newStatus: TestCase['status'] | null | undefined) {
if (disabled || newStatus == null || value === newStatus) {
return
}
value = newStatus
dispatch('change', value)
if ('_id' in object) {
await client.update(object, { status: newStatus })
}
}
$: 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>
{#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}

View File

@ -0,0 +1,43 @@
<!--
//
// 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 } from '@hcengineering/test-management'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { tooltip } from '@hcengineering/ui'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
export let value: TestCase | undefined
export let inline: boolean = false
export let disabled: boolean = false
export let accent: boolean = false
export let noUnderline: boolean = false
</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: getEmbeddedLabel(value.name) }}>
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
{value.name}
</span>
</div>
</DocNavLink>
{/if}
{/if}

View File

@ -0,0 +1,44 @@
<!--
// 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, TestCaseStatus } from '@hcengineering/test-management'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import StatusEditor from './StatusEditor.svelte'
export let value: TestCaseStatus
export let object: TestCase
export let onChange: ((value: TestCaseStatus) => 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)}
/>

View File

@ -0,0 +1,135 @@
<!--
// 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 { 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 { IntlString } from '@hcengineering/platform'
import { Card, SpaceSelector, getClient } from '@hcengineering/presentation'
import { TestRun, TestProject } from '@hcengineering/test-management'
import { EditBox } from '@hcengineering/ui'
import { EmptyMarkup } from '@hcengineering/text'
import testManagement from '../../plugin'
import ProjectPresenter from '../project/ProjectSpacePresenter.svelte'
export let space: Ref<TestProject>
const dispatch = createEventDispatcher()
const client = getClient()
const id: Ref<TestRun> = generateId()
const object: Data<TestRun> = {
name: '' as IntlString,
description: makeCollaborativeDoc(id, 'description')
}
let _space = space
let description = EmptyMarkup
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)
}
</script>
<Card
label={testManagement.string.CreateTestRun}
okAction={onSave}
canSave={object.name !== ''}
okLabel={testManagement.string.CreateTestRun}
gap={'gapV-4'}
on:close={() => dispatch('close')}
on:changeContent
>
<svelte:fragment slot="header">
<SpaceSelector
_class={testManagement.class.TestProject}
label={testManagement.string.TestProject}
bind:space={_space}
kind={'regular'}
size={'large'}
component={ProjectPresenter}
defaultIcon={testManagement.icon.Home}
/>
</svelte:fragment>
<EditBox
bind:value={object.name}
placeholder={testManagement.string.TestRunNamePlaceholder}
kind={'large-style'}
autoFocus
/>
<AttachmentStyledBox
bind:this={descriptionBox}
objectId={id}
_class={testManagement.class.TestRun}
space={_space}
alwaysEdit
showButtons={false}
bind:content={description}
placeholder={core.string.Description}
kind="indented"
isScrollable={false}
enableBackReferences={true}
enableAttachments={false}
on:attachments={(ev) => {
if (ev.detail.size > 0) attachments = ev.detail.values
else if (ev.detail.size === 0 && ev.detail.values != null) {
attachments.clear()
attachments = attachments
}
}}
/>
<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}
/>
</svelte:fragment>
</Card>

View File

@ -0,0 +1,18 @@
<!--
// 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">
</script>
<div class="antiNav-subheader"></div>

View File

@ -0,0 +1,18 @@
<!--
// 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">
</script>
<div class="antiNav-subheader"></div>

View File

@ -0,0 +1,98 @@
<!--
// 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 { Data, Ref } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Card, SpaceSelector, getClient } from '@hcengineering/presentation'
import { StyledTextArea } from '@hcengineering/text-editor-resources'
import { TestSuite, TestProject } from '@hcengineering/test-management'
import { EditBox } from '@hcengineering/ui'
import { ObjectBox } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import testManagement from '../../plugin'
import ProjectPresenter from '../project/ProjectSpacePresenter.svelte'
export let space: Ref<TestProject>
export let parentId: Ref<TestSuite> = testManagement.ids.NoParent
const dispatch = createEventDispatcher()
const client = getClient()
const object: Data<TestSuite> = {
name: '' as IntlString,
description: '',
parent: parentId
}
let _space = space
function handleTestSuiteChange (evt: CustomEvent<Ref<TestSuite>>): void {
object.parent = evt.detail
}
async function onSave () {
await client.createDoc(testManagement.class.TestSuite, _space, object)
}
</script>
<Card
label={testManagement.string.CreateTestSuite}
okAction={onSave}
canSave={object.name !== ''}
okLabel={testManagement.string.CreateTestSuite}
gap={'gapV-4'}
on:close={() => dispatch('close')}
on:changeContent
>
<svelte:fragment slot="header">
<SpaceSelector
_class={testManagement.class.TestProject}
label={testManagement.string.TestProject}
bind:space={_space}
kind={'regular'}
size={'small'}
component={ProjectPresenter}
defaultIcon={testManagement.icon.Home}
/>
<ObjectBox
_class={testManagement.class.TestSuite}
value={parentId}
docQuery={{
space: _space
}}
on:change={handleTestSuiteChange}
kind={'regular'}
size={'small'}
label={testManagement.string.NoTestSuite}
icon={testManagement.icon.TestSuite}
searchField={'title'}
allowDeselect={true}
showNavigate={false}
docProps={{ disabled: true, noUnderline: true }}
focusIndex={20000}
/>
</svelte:fragment>
<EditBox
bind:value={object.name}
placeholder={testManagement.string.NamePlaceholder}
kind={'large-style'}
autoFocus
/>
<StyledTextArea
bind:content={object.description}
placeholder={testManagement.string.DescriptionPlaceholder}
kind={'emphasized'}
showButtons={false}
/>
</Card>

View File

@ -0,0 +1,100 @@
<!--
// 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 { ActionContext, createQuery, getClient } from '@hcengineering/presentation'
import { type Class, type Ref } from '@hcengineering/core'
import { TestSuite } from '@hcengineering/test-management'
import { StyledTextArea } from '@hcengineering/text-editor-resources'
import { Panel } from '@hcengineering/panel'
import { EditBox } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import TestCasesList from './TestCasesList.svelte'
import testManagement from '../../plugin'
export let _id: Ref<TestSuite>
export let _class: Ref<Class<TestSuite>>
let object: TestSuite
const dispatch = createEventDispatcher()
const client = getClient()
let oldLabel: string | undefined = ''
let rawLabel: string | undefined = ''
const query = createQuery()
$: _id !== undefined &&
_class !== undefined &&
query.query(_class, { _id }, async (result) => {
;[object] = result
})
async function change<K extends keyof TestSuite> (field: K, value: TestSuite[K]) {
if (object !== undefined) {
await client.update(object, { [field]: value })
}
}
$: if (oldLabel !== object?.name) {
oldLabel = object?.name
rawLabel = object?.name
}
onMount(() => dispatch('open', { ignoreKeys: [] }))
</script>
{#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">
<StyledTextArea
bind:content={object.description}
placeholder={testManagement.string.DescriptionPlaceholder}
kind={'emphasized'}
showButtons={false}
/>
</div>
<div class="w-full mt-6">
<TestCasesList objectId={object._id} />
</div>
</Panel>
{/if}

View File

@ -0,0 +1,86 @@
<!--
// Copyright © 2024 Anticrm Platform Contributors.
//
// 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 { Ref } 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 { Viewlet, ViewletPreference } from '@hcengineering/view'
import { NavLink, 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>
let testCases: number
const query = createQuery()
$: query.query(testManagement.class.TestCase, { suite: objectId }, (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>
<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>
</span>
<div class="flex-row-center gap-2 reverse">
<ViewletsSettingButton
viewletQuery={{ _id: testManagement.viewlet.SuiteTestCases }}
kind={'tertiary'}
bind:viewlet
bind:preference
bind:loading
/>
<Button id="appls.add" icon={IconAdd} kind={'ghost'} on:click={createTestCase} />
</div>
</div>
{#if testCases > 0}
{#if viewlet !== undefined && !loading}
<Scroller horizontal>
<Table
_class={testManagement.class.TestCase}
config={preference?.config ?? viewlet.config}
query={{ suite: objectId }}
loadingProps={{ length: testCases }}
/>
</Scroller>
{:else}
<Loading />
{/if}
{:else}
<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}>
<Label label={testManagement.string.CreateTestCase} />
</span>
</SectionEmpty>
{/if}
</div>

View File

@ -0,0 +1,43 @@
<!--
//
// 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 { TestSuite } from '@hcengineering/test-management'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { tooltip } from '@hcengineering/ui'
import { DocNavLink, ObjectMention } from '@hcengineering/view-resources'
export let value: TestSuite | undefined
export let inline: boolean = false
export let disabled: boolean = false
export let accent: boolean = false
export let noUnderline: boolean = false
</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: getEmbeddedLabel(value.name) }}>
<span class="label nowrap" class:no-underline={noUnderline || disabled} class:fs-bold={accent}>
{value.name}
</span>
</div>
</DocNavLink>
{/if}
{/if}

View File

@ -0,0 +1,39 @@
<!--
// 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, Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { TestSuite } from '@hcengineering/test-management'
import testManagement from '../../plugin'
import TestSuitePresenter from './TestSuitePresenter.svelte'
export let value: Ref<TestSuite> | undefined
export let _class: Ref<Class<TestSuite>> = testManagement.class.TestSuite
export let inline: boolean = false
export let accent: boolean = false
let project: TestSuite | undefined
const query = createQuery()
$: value !== undefined &&
query.query(_class, { _id: value }, (res) => {
;[project] = res
})
</script>
{#if value}
<TestSuitePresenter value={project} {inline} {accent} />
{/if}

View File

@ -0,0 +1,62 @@
//
// 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 Resources } from '@hcengineering/platform'
import NewTestCaseHeader from './components/test-case/NewTestCaseHeader.svelte'
import CreateProject from './components/project/CreateProject.svelte'
import ProjectSpacePresenter from './components/project/ProjectSpacePresenter.svelte'
import CreateTestSuite from './components/test-suite/CreateTestSuite.svelte'
import EditTestSuite from './components/test-suite/EditTestSuite.svelte'
import TestSuitePresenter from './components/test-suite/TestSuitePresenter.svelte'
import TestSuiteRefPresenter from './components/test-suite/TestSuiteRefPresenter.svelte'
import EditTestCase from './components/test-case/EditTestCase.svelte'
import TestCasePresenter from './components/test-case/TestCasePresenter.svelte'
import CreateTestCase from './components/test-case/CreateTestCase.svelte'
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 { CreateChildTestSuiteAction, EditTestSuiteAction } from './utils'
import { resolveLocation, getTestSuiteLink } from './navigation'
export default async (): Promise<Resources> => ({
component: {
NewTestCaseHeader,
CreateProject,
ProjectSpacePresenter,
CreateTestSuite,
EditTestSuite,
TestSuitePresenter,
EditTestCase,
TestCasePresenter,
CreateTestRun,
CreateTestCase,
TestCaseStatusPresenter,
EditTestRun,
TestRunPresenter,
TestSuiteRefPresenter
},
function: {
GetTestSuiteLink: getTestSuiteLink
},
resolver: {
Location: resolveLocation
},
actionImpl: {
CreateChildTestSuite: CreateChildTestSuiteAction,
EditTestSuite: EditTestSuiteAction
}
})

View File

@ -0,0 +1,84 @@
//
// 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 testManagement, { testManagementId, type TestSuite, type TestProject } from '@hcengineering/test-management'
import { type Doc, type Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { getCurrentResolvedLocation, getPanelURI, type Location, type ResolvedLocation } from '@hcengineering/ui'
import view, { type ObjectPanel } from '@hcengineering/view'
import { accessDeniedStore } from '@hcengineering/view-resources'
export function getPanelFragment<T extends Doc> (object: Pick<T, '_class' | '_id'>): string {
const hierarchy = getClient().getHierarchy()
const objectPanelMixin = hierarchy.classHierarchyMixin<Doc, ObjectPanel>(object._class, view.mixin.ObjectPanel)
const component = objectPanelMixin?.component ?? view.component.EditDoc
return getPanelURI(component, object._id, object._class, 'content')
}
async function generateProjectLocation (
loc: Location,
project: Ref<TestProject>
): Promise<ResolvedLocation | undefined> {
const client = getClient()
const doc = await client.findOne(testManagement.class.TestProject, { _id: project })
if (doc === undefined) {
accessDeniedStore.set(true)
console.error(`Could not find test project ${project}.`)
return undefined
}
const appComponent = loc.path[0] ?? ''
const workspace = loc.path[1] ?? ''
return {
loc: {
...loc,
path: [appComponent, workspace, testManagementId, project, 'library']
},
defaultLocation: {
...loc,
path: [appComponent, workspace, testManagementId, project, 'library']
}
}
}
export function getTestSuiteLink (testSuite: Ref<TestSuite>): Location {
const loc = getCurrentResolvedLocation()
loc.query =
testSuite === undefined
? undefined
: {
attachedTo: testSuite
}
return loc
}
export function getTestSuiteIdFromFragment (fragment: string): Ref<TestSuite> | undefined {
const props = decodeURIComponent(fragment).split('|')
return props[6] != null ? (props[6] as Ref<TestSuite>) : undefined
}
export async function resolveLocation (loc: Location): Promise<ResolvedLocation | undefined> {
if (loc.path[2] !== testManagementId) {
return undefined
}
const projectId = loc.path[3] as Ref<TestProject>
if (projectId !== undefined) {
return await generateProjectLocation(loc, projectId)
}
return undefined
}

View File

@ -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.
//
import type { IntlString } from '@hcengineering/platform'
import { mergeIds } from '@hcengineering/platform'
import testManagement, { testManagementId } from '@hcengineering/test-management'
export default mergeIds(testManagementId, testManagement, {
string: {
Add: '' as IntlString,
Remove: '' as IntlString
}
})

View File

@ -0,0 +1,41 @@
//
// 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 Asset, type IntlString } from '@hcengineering/platform'
import testManagement, { TestCaseStatus } from '@hcengineering/test-management'
/** @public */
export const defaultTestCaseStatuses = [
TestCaseStatus.Draft,
TestCaseStatus.ReadyForReview,
TestCaseStatus.FixReviewComments,
TestCaseStatus.Approved,
TestCaseStatus.Rejected
]
/** @public */
export const testCaseStatusAssets: Record<TestCaseStatus, { icon: Asset, label: IntlString }> = {
[TestCaseStatus.Draft]: { icon: testManagement.icon.StatusDraft, label: testManagement.string.StatusDraft },
[TestCaseStatus.ReadyForReview]: {
icon: testManagement.icon.StatusReview,
label: testManagement.string.StatusReview
},
[TestCaseStatus.FixReviewComments]: {
icon: testManagement.icon.StatusReviewComments,
label: testManagement.string.StatusReviewComments
},
[TestCaseStatus.Approved]: { icon: testManagement.icon.StatusApproved, label: testManagement.string.StatusApproved },
[TestCaseStatus.Rejected]: { icon: testManagement.icon.StatusRejected, label: testManagement.string.StatusRejected }
}

View File

@ -0,0 +1,71 @@
//
// 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 Contact } from '@hcengineering/contact'
import core, { type Doc, type Ref, type TxCollectionCUD, type TxCreateDoc, type TxUpdateDoc } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { showPopup } from '@hcengineering/ui'
import { type TestProject, type TestCase, type TestSuite } from '@hcengineering/test-management'
import CreateTestSuiteComponent from './components/test-suite/CreateTestSuite.svelte'
import EditTestSuiteComponent from './components/test-suite/EditTestSuite.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<TxCollectionCUD<TestCase, TestCase>>(core.class.TxCollectionCUD, {
'tx.objectId': objectId,
'tx._class': core.class.TxCreateDoc
})
)[0]
const updateTxes = await client.findAll<TxCollectionCUD<TestCase, TestCase>>(
core.class.TxCollectionCUD,
{ 'tx.objectId': objectId, 'tx._class': core.class.TxUpdateDoc, 'tx.operations.assignee': { $exists: true } },
{ sort: { modifiedOn: -1 } }
)
const set = new Set<Ref<Contact>>()
const createAssignee = (createTx?.tx as TxCreateDoc<TestCase>)?.attributes?.assignee
for (const tx of updateTxes) {
const assignee = (tx.tx as TxUpdateDoc<TestCase>).operations.assignee
if (assignee == null) continue
set.add(assignee)
}
if (createAssignee != null) {
set.add(createAssignee)
}
return Array.from(set)
}
export async function showCreateTestSuitePopup (
space: Ref<TestProject> | undefined,
parentId: Ref<TestSuite>
): Promise<void> {
showPopup(CreateTestSuiteComponent, { space, parentId }, 'top')
}
export async function showEditTestSuitePopup (suite: Ref<TestSuite>): Promise<void> {
showPopup(EditTestSuiteComponent, { _id: suite }, 'top')
}
export async function CreateChildTestSuiteAction (doc: TestSuite): Promise<void> {
await showCreateTestSuitePopup(doc.space, doc._id)
}
export async function EditTestSuiteAction (doc: TestSuite): Promise<void> {
await showEditTestSuitePopup(doc._id)
}

View File

@ -0,0 +1,5 @@
const sveltePreprocess = require('svelte-preprocess')
module.exports = {
preprocess: sveltePreprocess()
};

View File

@ -0,0 +1,9 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/ui/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types"
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig"
}

View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}

View File

@ -0,0 +1,56 @@
{
"name": "@hcengineering/test-management",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"files": [
"lib/**/*",
"types/**/*",
"tsconfig.json"
],
"author": "Anticrm Platform Contributors",
"license": "EPL-2.0",
"scripts": {
"build": "compile",
"build:watch": "compile",
"format": "format src",
"test": "jest --passWithNoTests --silent",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"@hcengineering/platform-rig": "^0.6.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
},
"dependencies": {
"@hcengineering/core": "^0.6.32",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/ui": "^0.6.15",
"@hcengineering/view": "^0.6.13",
"@hcengineering/contact": "^0.6.24",
"@hcengineering/chunter": "^0.6.20",
"@hcengineering/attachment": "^0.6.14",
"@hcengineering/time": "^0.6.0",
"@hcengineering/tags": "^0.6.16",
"@hcengineering/preference": "^0.6.13",
"lexorank": "~1.0.4"
},
"repository": "https://github.com/hcengineering/platform",
"publishConfig": {
"registry": "https://npm.pkg.github.com"
}
}

View File

@ -0,0 +1,21 @@
//
// 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 { testManagementId, testManagementPlugin } from './plugin'
export * from './types'
export { testManagementId }
export default testManagementPlugin

View File

@ -0,0 +1,220 @@
//
// 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 {
Mixin,
type Class,
type Doc,
type Ref,
Type,
type Status,
type SpaceTypeDescriptor,
type SpaceType
} from '@hcengineering/core'
import type { Asset, IntlString, Plugin, Resource } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform'
import { type AnyComponent, type Location, type ResolvedLocation } from '@hcengineering/ui'
import { ActionCategory, Viewlet } from '@hcengineering/view'
import {
TestSuite,
TestCase,
TestProject,
TestCaseType,
TestCasePriority,
TestCaseStatus,
TestRun,
TestRunItem,
TestRunResult
} from './types'
/** @public */
export const testManagementId = 'testManagement' as Plugin
/** @public */
export const testManagementPlugin = plugin(testManagementId, {
app: {
TestManagement: '' as Ref<Doc>
},
icon: {
TestManagement: '' as Asset,
TestManagementVersion: '' as Asset,
TestManagementApplication: '' as Asset,
TestCase: '' as Asset,
TestCases: '' as Asset,
Home: '' as Asset,
Estimation: '' as Asset,
TestSuite: '' as Asset,
TestProject: '' as Asset,
TestSuites: '' as Asset,
TestRuns: '' as Asset,
RedCircle: '' as Asset,
StatusDraft: '' as Asset,
StatusReview: '' as Asset,
StatusReviewComments: '' as Asset,
StatusApproved: '' as Asset,
StatusRejected: '' as Asset,
Document: '' as Asset,
TestLibrary: '' as Asset
},
class: {
TestCase: '' as Ref<Class<TestCase>>,
TestSuite: '' as Ref<Class<TestSuite>>,
TestProject: '' as Ref<Class<TestProject>>,
TypeTestCaseType: '' as Ref<Class<Type<TestCaseType>>>,
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>>>
},
descriptors: {
ProjectType: '' as Ref<SpaceTypeDescriptor>
},
mixin: {
TestCaseTypeData: '' as Ref<Mixin<TestCase>>,
TestProject: '' as Ref<Mixin<TestProject>>,
DefaultProjectTypeData: '' as Ref<Mixin<TestProject>>
},
string: {
ConfigLabel: '' as IntlString,
ConfigDescription: '' as IntlString,
TestCaseType: '' as IntlString,
TestCasePriority: '' as IntlString,
TestCaseStatus: '' as IntlString,
TestSuite: '' as IntlString,
SuiteName: '' as IntlString,
SuiteDescription: '' as IntlString,
Suite: '' as IntlString,
TestName: '' as IntlString,
TestDescription: '' as IntlString,
TestType: '' as IntlString,
TestPriority: '' as IntlString,
TestStatus: '' as IntlString,
TestEstimatedTime: '' as IntlString,
TestPreconditions: '' as IntlString,
TestSteps: '' as IntlString,
TestAssignee: '' as IntlString,
TestCase: '' as IntlString,
TestProject: '' as IntlString,
TestManagementApplication: '' as IntlString,
AllTestCases: '' as IntlString,
AllProjects: '' as IntlString,
Projects: '' as IntlString,
CreateProject: '' as IntlString,
TestCases: '' as IntlString,
TestManagementDescription: '' as IntlString,
CreateTestCase: '' as IntlString,
FullDescription: '' as IntlString,
EditProject: '' as IntlString,
ProjectName: '' as IntlString,
ProjectType: '' as IntlString,
Members: '' as IntlString,
RoleLabel: '' as IntlString,
ProjectMembers: '' as IntlString,
ManageProjectStatuses: '' as IntlString,
TestSuites: '' as IntlString,
CreateTestSuite: '' as IntlString,
NamePlaceholder: '' as IntlString,
DescriptionPlaceholder: '' as IntlString,
TestRuns: '' as IntlString,
NewTestRun: '' as IntlString,
TestRun: '' as IntlString,
TestNamePlaceholder: '' as IntlString,
ChooseIcon: '' as IntlString,
NoTestSuite: '' as IntlString,
StatusDraft: '' as IntlString,
StatusReview: '' as IntlString,
StatusReviewComments: '' as IntlString,
StatusApproved: '' as IntlString,
StatusRejected: '' as IntlString,
SetStatus: '' as IntlString,
Assignee: '' as IntlString,
Unassigned: '' as IntlString,
AssignTo: '' as IntlString,
AssignedTo: '' as IntlString,
PreviousAssigned: '' as IntlString,
TestRunName: '' as IntlString,
NoTestCases: '' as IntlString,
DueDate: '' as IntlString,
TestRunItems: '' as IntlString,
TestRunResult: '' as IntlString,
TestRunItem: '' as IntlString,
TestRunNamePlaceholder: '' as IntlString,
SelectTestSuites: '' as IntlString,
SelectTestCases: '' as IntlString,
CreateTestRun: '' as IntlString,
TestLibrary: '' as IntlString
},
category: {
TestManagement: '' as Ref<ActionCategory>
},
component: {
TestCaseSearchIcon: '' as AnyComponent,
TestCases: '' as AnyComponent,
CreateProject: '' as AnyComponent,
NewTestCaseHeader: '' as AnyComponent,
TestCaseStatusIcon: '' as AnyComponent,
PriorityIconPresenter: '' as AnyComponent,
TestCaseStatusPresenter: '' as AnyComponent,
TestSuites: '' as AnyComponent,
CreateTestSuite: '' as AnyComponent
},
ids: {
NoParent: '' as Ref<TestSuite>,
TestCaseUpdatedActivityViewlet: '' as Ref<TestCase>
},
spaceType: {
TestCaseType: '' as Ref<SpaceType>,
DefaultProject: '' as Ref<SpaceType>
},
spaceTypeDescriptor: {
TestCaseType: '' as Ref<SpaceTypeDescriptor>
},
template: {
DefaultProject: '' as Ref<SpaceTypeDescriptor>
},
space: {
DefaultProject: '' as Ref<TestProject>
},
viewlet: {
TableTestCase: '' as Ref<Viewlet>,
TableTestSuites: '' as Ref<Viewlet>,
TableTestRun: '' as Ref<Viewlet>,
SuiteTestCases: '' as Ref<Viewlet>,
ListTestCase: '' as Ref<Viewlet>
},
testCaseTypeStatus: {
Draft: '' as Ref<Status>,
ReviewRequired: '' as Ref<Status>,
NeedFixes: '' as Ref<Status>,
Ready: '' as Ref<Status>
},
taskType: {
TestCase: '' as Ref<TestCase>
},
function: {
GetTestSuiteLink: '' as Resource<(doc: Ref<Doc>) => Location>
},
resolver: {
Location: '' as Resource<(loc: Location) => Promise<ResolvedLocation | undefined>>
}
})
/**
* @public
*/
export default testManagementPlugin

View File

@ -0,0 +1,122 @@
//
// 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 { Attachment } from '@hcengineering/attachment'
import { Employee } from '@hcengineering/contact'
import {
Doc,
type CollectionSize,
type Ref,
type Markup,
TypedSpace,
CollaborativeDoc,
AttachedDoc,
Timestamp
} from '@hcengineering/core'
import { IconProps } from '@hcengineering/view'
/** @public */
export enum TestCaseType {
Functional,
Performance,
Regression,
Security,
Smoke,
Usability
}
/** @public */
export const testCaseTypes = [
TestCaseType.Functional,
TestCaseType.Performance,
TestCaseType.Regression,
TestCaseType.Security,
TestCaseType.Smoke,
TestCaseType.Usability
]
/** @public */
export enum TestCasePriority {
Low,
Medium,
High,
Urgent
}
/** @public */
export const testCasePriorities = [
TestCasePriority.Low,
TestCasePriority.Medium,
TestCasePriority.High,
TestCasePriority.Urgent
]
/** @public */
export enum TestCaseStatus {
Draft,
ReadyForReview,
FixReviewComments,
Approved,
Rejected
}
/** @public */
export interface TestProject extends TypedSpace, IconProps {
fullDescription?: Markup
}
/** @public */
export interface TestSuite extends Doc {
space: Ref<TestProject>
name: string
description?: string
parent: Ref<TestSuite>
testCases?: CollectionSize<TestCase>
}
/** @public */
export interface TestCase extends AttachedDoc<TestSuite, 'testCases', TestProject> {
name: string
description: CollaborativeDoc
type: TestCaseType
priority: TestCasePriority
status: TestCaseStatus
assignee: Ref<Employee>
attachments?: CollectionSize<Attachment>
comments?: number
}
/** @public */
export interface TestRun extends Doc {
name: string
description: CollaborativeDoc
dueDate?: Timestamp
items?: CollectionSize<TestRunItem>
}
/** @public */
export enum TestRunResult {
Passed,
Blocked,
Failed
}
/** @public */
export interface TestRunItem extends AttachedDoc {
testRun: Ref<TestRun>
testCase: Ref<TestCase>
result?: TestRunResult
comments?: number
}

View File

@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}

View File

@ -64,7 +64,7 @@
{#if specials}
<TreeNode
_id={space?._id}
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model.icon}
icon={space?.icon === view.ids.IconWithEmoji ? IconWithEmoji : space?.icon ?? model?.icon}
iconProps={space?.icon === view.ids.IconWithEmoji
? { icon: space.color }
: {

View File

@ -0,0 +1,93 @@
<!--
// 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 { getClient } from '@hcengineering/presentation'
import { Doc, Ref } from '@hcengineering/core'
import { Action, IconEdit } from '@hcengineering/ui'
import { getResource } from '@hcengineering/platform'
import { TreeItem, getActions as getContributedActions } from '../index'
export let folders: Ref<Doc>[]
export let folderById: Map<Ref<Doc>, Doc>
export let descendants: Map<Ref<Doc>, Doc[]>
export let selected: Ref<Doc> | undefined
export let level: number = 0
export let once: boolean = false
const dispatch = createEventDispatcher()
const client = getClient()
function getTitle (doc: Doc): string {
return (doc as any)?.title || ''
}
function getDescendants (obj: Ref<Doc>): Ref<Doc>[] {
return (descendants.get(obj) ?? []).sort((a, b) => getTitle(a).localeCompare(getTitle(b))).map((p) => p._id)
}
function handleSelected (obj: Ref<Doc>): void {
dispatch('selected', obj)
}
async function getActions (obj: Doc): Promise<Action[]> {
const result: Action[] = []
const extraActions = await getContributedActions(client, obj)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(obj, evt, act.actionProps)
}
})
}
return result
}
$: _folders = folders.map((it) => folderById.get(it)).filter((it) => it !== undefined) as Doc[]
$: _descendants = new Map(_folders.map((it) => [it._id, getDescendants(it._id)]))
</script>
{#each _folders as doc}
{@const desc = _descendants.get(doc._id) ?? []}
{#if doc}
<TreeItem
_id={doc._id}
folderIcon
title={getTitle(doc)}
selected={selected === doc._id}
isFold
empty={desc.length === 0}
actions={async () => await getActions(doc)}
{level}
shouldTooltip
on:click={() => {
handleSelected(doc._id)
}}
>
<svelte:fragment slot="dropbox">
{#if desc.length > 0 && !once}
<svelte:self folders={desc} {descendants} {folderById} {selected} level={level + 1} on:selected />
{/if}
</svelte:fragment>
</TreeItem>
{/if}
{/each}

View File

@ -0,0 +1,149 @@
<!--
// 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, SortingOrder, Space } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Action, IconEdit, navigate, type Location } from '@hcengineering/ui'
import { getResource, type Resource } from '@hcengineering/platform'
import { IntlString, Asset } from '@hcengineering/platform'
import { FoldersManager, FoldersStore, FoldersState } from '../stores/folderStore'
import FolderTreeLevel from './FolderTreeLevel.svelte'
import { TreeNode, TreeItem, getActions as getContributedActions } from '../index'
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc>
export let titleKey: string = 'title'
export let parentKey: string = 'parent'
export let noParentId: Ref<Doc>
export let getFolderLink: Resource<(doc: Ref<Doc> | undefined) => Location>
export let allObjectsIcon: Asset
export let allObjectsLabel: IntlString
export let forciblyСollapsed: boolean = false
const client = getClient()
let foldersState: FoldersState = FoldersState.empty()
const foldersManager: FoldersManager = new FoldersManager(titleKey, parentKey, noParentId)
FoldersStore.subscribe((newState) => {
foldersState = newState
})
let selected: Ref<Doc> | undefined
let visibleItem: Doc | undefined
const q = createQuery()
q.query(
_class,
query ?? {},
(result) => {
foldersManager.setFolders(result)
},
{
sort: {
name: SortingOrder.Ascending
}
}
)
async function handleFolderSelected (_id: Ref<Doc>): Promise<void> {
selected = _id
visibleItem = selected !== undefined ? foldersState.folderById.get(selected) : undefined
const folder = foldersState.folderById.get(_id)
if (getFolderLink) {
const getFolderLinkFunction = await getResource(getFolderLink)
navigate(getFolderLinkFunction(_id))
}
}
async function handleAllItemsSelected (): Promise<void> {
selected = noParentId
visibleItem = undefined
const getFolderLinkFunction = await getResource(getFolderLink)
navigate(getFolderLinkFunction(undefined))
}
async function getFolderActions (obj: Doc): Promise<Action[]> {
const result: Action[] = []
const extraActions = await getContributedActions(client, obj)
for (const act of extraActions) {
result.push({
icon: act.icon ?? IconEdit,
label: act.label,
action: async (ctx: any, evt: Event) => {
const impl = await getResource(act.action)
await impl(obj, evt, act.actionProps)
}
})
}
return result
}
async function getRootActions (): Promise<Action[]> {
return []
}
</script>
<div class="folders-browser">
<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}
{@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>
</div>
<style lang="scss">
.folders-browser {
display: flex;
padding-top: 1rem;
}
</style>

View File

@ -100,6 +100,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 { blobImageMetadata, blobVideoMetadata } from './blob'
@ -295,7 +296,8 @@ export default async (): Promise<Resources> => ({
ImageViewer,
VideoViewer,
PDFViewer,
TextViewer
TextViewer,
FoldersBrowser
},
popup: {
PositionElementAlignment

View File

@ -0,0 +1,86 @@
// 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'
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 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 class FoldersManager {
titleKey: string
parentKey: string
noParentId: Ref<Doc>
constructor (titleKey: string, parentKey: string, noParentId: Ref<Doc>) {
this.titleKey = titleKey
this.parentKey = parentKey
this.noParentId = noParentId
}
public getTitle (doc: Doc): string {
return (doc as any)?.[this.titleKey] ?? ''
}
public getParent (doc: Doc): Ref<Doc> {
return (doc as any)?.[this.parentKey] ?? this.noParentId
}
public getDescendants (descendants: Map<Ref<Doc>, Doc[]>, obj: Ref<Doc>): Array<Ref<Doc>> {
return (descendants.get(obj) ?? [])
.sort((a, b) => this.getTitle(a).localeCompare(this.getTitle(b)))
.map((p) => p._id)
}
public setFolders (result: Doc[]): void {
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 current = descendants.get(this.getParent(mappedDoc)) ?? []
current.push(mappedDoc)
descendants.set(this.getParent(mappedDoc), current)
folderById.set(mappedDoc._id, mappedDoc)
}
folders = this.getDescendants(descendants, this.noParentId)
FoldersStore.set(new FoldersState(folders, folderById, descendants))
}
}

View File

@ -0,0 +1,107 @@
<!--
// 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 { onDestroy } from 'svelte'
import {
AnyComponent,
AnySvelteComponent,
Button,
Breadcrumb,
Component,
IconAdd,
Header,
Separator,
showPopup,
getLocation,
resolvedLocationStore
} from '@hcengineering/ui'
import { Doc, DocumentQuery, Ref, Space, mergeQueries } from '@hcengineering/core'
import { IntlString, Asset } from '@hcengineering/platform'
export let space: Ref<Space> | undefined = undefined
export let navigationComponent: AnyComponent
export let navigationComponentProps: Record<string, any> | undefined = undefined
export let navigationComponentLabel: IntlString
export let navigationComponentIcon: Asset | undefined = undefined
export let createComponent: AnyComponent | undefined = undefined
export let createComponentProps: Record<string, any> = {}
export let mainComponentLabel: IntlString
export let mainComponentIcon: Asset | undefined = undefined
export let query: DocumentQuery<Doc> = {}
export let syncWithLocationQuery: boolean = true
export let mainComponent: AnyComponent | AnySvelteComponent
export let mainComponentProps = {}
let locationQuery: DocumentQuery<Doc> = {}
let resultQuery: DocumentQuery<Doc> = {}
let spaceQuery: DocumentQuery<Doc> = {}
$: spaceQuery = space !== undefined ? { space } : {}
$: resultQuery = mergeQueries(query, mergeQueries(spaceQuery, locationQuery)) ?? {}
if (syncWithLocationQuery) {
locationQuery = getLocation()?.query as any
onDestroy(
resolvedLocationStore.subscribe((newLocation) => {
locationQuery = newLocation?.query ?? {}
})
)
}
function showCreateDialog (): void {
if (createComponent === undefined) return
showPopup(createComponent, { ...createComponentProps, space }, 'top')
}
</script>
<div class="hulyComponent-content__container columns">
<div class="hulyComponent-content__column">
<Header adaptive={'disabled'}>
<Breadcrumb icon={navigationComponentIcon} label={navigationComponentLabel} size={'large'} />
<svelte:fragment slot="actions">
{#if createComponent}
<Button
icon={IconAdd}
kind={'icon'}
on:click={() => {
showCreateDialog()
}}
/>
{/if}
</svelte:fragment>
</Header>
<Component
is={navigationComponent}
props={{
...navigationComponentProps,
query: spaceQuery
}}
/>
</div>
<Separator name={'navigationSection'} index={0} color={'var(--theme-divider-color)'} />
<div class="hulyComponent-content__column">
<Header adaptive={'disabled'}>
<Breadcrumb icon={mainComponentIcon} label={mainComponentLabel} size={'large'} />
</Header>
<Component
is={mainComponent}
props={{
...(mainComponentProps ?? {}),
query: resultQuery,
totalQuery: resultQuery
}}
/>
</div>
</div>

View File

@ -13,6 +13,7 @@
// limitations under the License.
-->
<script lang="ts">
import { onDestroy } from 'svelte'
import { Class, Doc, DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString, Asset, getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
@ -38,6 +39,9 @@
ViewletPreference
} from '@hcengineering/view'
import { FilterBar, FilterButton, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
import { ParentsNavigationModel } from '@hcengineering/workbench'
import ComponentNavigator from './ComponentNavigator.svelte'
export let _class: Ref<Class<Doc>>
export let space: Ref<Space> | undefined = undefined
@ -51,6 +55,7 @@
export let descriptors: Array<Ref<ViewletDescriptor>> | undefined = undefined
export let baseQuery: DocumentQuery<Doc> | undefined = undefined
export let modes: IModeSelector<any> | undefined = undefined
export let navigationModel: ParentsNavigationModel | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -104,7 +109,7 @@
function showCreateDialog (): void {
if (createComponent === undefined) return
showPopup(createComponent, createComponentProps, 'top')
showPopup(createComponent, { ...createComponentProps, space }, 'top')
}
</script>
@ -167,21 +172,44 @@
resultQuery = { ...query, ...e.detail }
}}
/>
<Component
is={viewlet.$lookup.descriptor.component}
props={{
_class,
space,
options: viewlet.options,
config: preference?.config ?? viewlet.config,
viewlet,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
createItemDialog: createComponent,
createItemLabel: createLabel,
query: resultQuery,
totalQuery: query,
...viewlet.props
}}
/>
{#if navigationModel?.navigationComponent === undefined}
<Component
is={viewlet.$lookup.descriptor.component}
props={{
_class,
space,
options: viewlet.options,
config: preference?.config ?? viewlet.config,
viewlet,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
createItemDialog: createComponent,
createItemLabel: createLabel,
query: resultQuery,
totalQuery: query,
...viewlet.props
}}
/>
{:else}
<ComponentNavigator
query={resultQuery}
{space}
mainComponent={viewlet.$lookup.descriptor.component}
mainComponentProps={{
_class,
space,
options: viewlet.options,
config: preference?.config ?? viewlet.config,
viewlet,
viewOptions,
viewOptionsConfig: viewlet.viewOptions?.other,
createItemDialog: createComponent,
createItemLabel: createLabel,
query: resultQuery,
totalQuery: query,
...viewlet.props
}}
{...navigationModel}
/>
{/if}
{/if}

View File

@ -964,7 +964,13 @@
{:else if specialComponent}
<Component
is={specialComponent.component}
props={{ model: navigatorModel, ...specialComponent.componentProps, currentSpace }}
props={{
model: navigatorModel,
...specialComponent.componentProps,
currentSpace,
space: currentSpace,
navigationModel: specialComponent?.navigationModel
}}
on:action={(e) => {
if (e?.detail) {
const loc = getCurrentLocation()

View File

@ -183,6 +183,22 @@ export interface SpecialNavModel {
notificationsCountProvider?: Resource<
(inboxNotificationsByContext: Map<Ref<DocNotifyContext>, InboxNotification[]>) => number
>
navigationModel?: ParentsNavigationModel
}
/**
* @public
*/
export interface ParentsNavigationModel {
navigationComponent: AnyComponent
navigationComponentLabel: IntlString
navigationComponentIcon?: Asset
mainComponentLabel: IntlString
mainComponentIcon?: Asset
navigationComponentProps?: Record<string, any>
syncWithLocationQuery?: boolean
createComponent?: AnyComponent
createComponentProps?: Record<string, any>
}
/**

View File

@ -1151,7 +1151,7 @@
"shouldPublish": false
},
{
"packageName": "@hcengineering/tags",
"packageName": "@hcengineering/tags",
"projectFolder": "plugins/tags",
"shouldPublish": true
},
@ -1960,7 +1960,7 @@
"packageName": "@hcengineering/diffview-resources",
"projectFolder": "plugins/diffview-resources",
"shouldPublish": false
},
},
{
"packageName": "@hcengineering/github",
"projectFolder": "services/github/github",
@ -2160,6 +2160,26 @@
"packageName": "@hcengineering/scripts",
"projectFolder": "common/scripts",
"shouldPublish": false
},
{
"packageName": "@hcengineering/model-test-management",
"projectFolder": "models/test-management",
"shouldPublish": false
},
{
"packageName": "@hcengineering/test-management",
"projectFolder": "plugins/test-management",
"shouldPublish": false
},
{
"packageName": "@hcengineering/test-management-resources",
"projectFolder": "plugins/test-management-resources",
"shouldPublish": false
},
{
"packageName": "@hcengineering/test-management-assets",
"projectFolder": "plugins/test-management-assets",
"shouldPublish": false
}
]
}