mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-20 01:21:55 +03:00
Add next test button (#7247)
This commit is contained in:
parent
3775132777
commit
abdf2572e2
@ -160,6 +160,7 @@ export function createModel (builder: Builder): void {
|
|||||||
defineTestSuite(builder)
|
defineTestSuite(builder)
|
||||||
defineTestCase(builder)
|
defineTestCase(builder)
|
||||||
defineTestRun(builder)
|
defineTestRun(builder)
|
||||||
|
defineTestResult(builder)
|
||||||
|
|
||||||
definePresenters(builder)
|
definePresenters(builder)
|
||||||
|
|
||||||
@ -261,7 +262,7 @@ function defineTestSuite (builder: Builder): void {
|
|||||||
// Actions
|
// Actions
|
||||||
|
|
||||||
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.IgnoreActions, {
|
builder.mixin(testManagement.class.TestSuite, core.class.Class, view.mixin.IgnoreActions, {
|
||||||
actions: [print.action.Print, tracker.action.EditRelatedTargets]
|
actions: [print.action.Print, tracker.action.EditRelatedTargets, tracker.action.NewRelatedIssue]
|
||||||
})
|
})
|
||||||
|
|
||||||
createAction(
|
createAction(
|
||||||
@ -331,7 +332,7 @@ function defineTestCase (builder: Builder): void {
|
|||||||
|
|
||||||
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ClassFilters, {
|
builder.mixin(testManagement.class.TestCase, core.class.Class, view.mixin.ClassFilters, {
|
||||||
filters: ['priority', 'status'],
|
filters: ['priority', 'status'],
|
||||||
ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn']
|
ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn', 'name']
|
||||||
})
|
})
|
||||||
|
|
||||||
builder.createDoc(
|
builder.createDoc(
|
||||||
@ -419,6 +420,12 @@ function defineTestRun (builder: Builder): void {
|
|||||||
component: testManagement.component.TestResultStatusPresenter
|
component: testManagement.component.TestResultStatusPresenter
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(testManagement.class.TestRun, core.class.Class, view.mixin.IgnoreActions, {
|
||||||
|
actions: [print.action.Print, tracker.action.EditRelatedTargets, tracker.action.NewRelatedIssue]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function defineTestResult (builder: Builder): void {
|
||||||
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectPresenter, {
|
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ObjectPresenter, {
|
||||||
presenter: testManagement.component.TestResultPresenter
|
presenter: testManagement.component.TestResultPresenter
|
||||||
})
|
})
|
||||||
@ -448,7 +455,7 @@ function defineTestRun (builder: Builder): void {
|
|||||||
|
|
||||||
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ClassFilters, {
|
builder.mixin(testManagement.class.TestResult, core.class.Class, view.mixin.ClassFilters, {
|
||||||
filters: ['assignee', 'status', 'testSuite'],
|
filters: ['assignee', 'status', 'testSuite'],
|
||||||
ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn']
|
ignoreKeys: ['createdBy', 'modifiedBy', 'createdOn', 'modifiedOn', 'name', 'attachedTo']
|
||||||
})
|
})
|
||||||
|
|
||||||
const viewOptions: ViewOptionsModel = {
|
const viewOptions: ViewOptionsModel = {
|
||||||
|
@ -23,7 +23,8 @@ import type { ActionCategory } from '@hcengineering/view'
|
|||||||
export default mergeIds(testManagementId, testManganement, {
|
export default mergeIds(testManagementId, testManganement, {
|
||||||
category: {
|
category: {
|
||||||
TestSuite: '' as Ref<ActionCategory>,
|
TestSuite: '' as Ref<ActionCategory>,
|
||||||
TestCase: '' as Ref<ActionCategory>
|
TestCase: '' as Ref<ActionCategory>,
|
||||||
|
TestResult: '' as Ref<ActionCategory>
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
CreateTestCase: '' as AnyComponent,
|
CreateTestCase: '' as AnyComponent,
|
||||||
|
@ -250,12 +250,15 @@ export class TTestResult extends TAttachedDoc implements TestResult {
|
|||||||
testCase!: Ref<TestCase>
|
testCase!: Ref<TestCase>
|
||||||
|
|
||||||
@Prop(TypeRef(testManagement.class.TestSuite), testManagement.string.TestSuite)
|
@Prop(TypeRef(testManagement.class.TestSuite), testManagement.string.TestSuite)
|
||||||
|
@Index(IndexKind.Indexed)
|
||||||
testSuite?: Ref<TestSuite>
|
testSuite?: Ref<TestSuite>
|
||||||
|
|
||||||
@Prop(TypeTestRunStatus(), testManagement.string.TestRunStatus)
|
@Prop(TypeTestRunStatus(), testManagement.string.TestRunStatus)
|
||||||
|
@Index(IndexKind.Indexed)
|
||||||
status?: TestRunStatus
|
status?: TestRunStatus
|
||||||
|
|
||||||
@Prop(TypeRef(contact.mixin.Employee), testManagement.string.TestAssignee)
|
@Prop(TypeRef(contact.mixin.Employee), testManagement.string.TestAssignee)
|
||||||
|
@Index(IndexKind.Indexed)
|
||||||
assignee?: Ref<Employee>
|
assignee?: Ref<Employee>
|
||||||
|
|
||||||
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
"DueDate": "Termín",
|
"DueDate": "Termín",
|
||||||
"RunTestCases": "Spustit",
|
"RunTestCases": "Spustit",
|
||||||
"TestCaseDescription": "Popis testovacího případu",
|
"TestCaseDescription": "Popis testovacího případu",
|
||||||
"TestResultAttributes": "Výsledek"
|
"TestResultAttributes": "Výsledek",
|
||||||
|
"GoToNextTest": "Další",
|
||||||
|
"GoToNextTestTooltip": "Přejít na další test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
"DueDate": "Due date",
|
"DueDate": "Due date",
|
||||||
"RunTestCases": "Run",
|
"RunTestCases": "Run",
|
||||||
"TestCaseDescription": "Test case description",
|
"TestCaseDescription": "Test case description",
|
||||||
"TestResultAttributes": "Result"
|
"TestResultAttributes": "Result",
|
||||||
|
"GoToNextTest": "Next",
|
||||||
|
"GoToNextTestTooltip": "Go to next test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
"DueDate": "Fecha de vencimiento",
|
"DueDate": "Fecha de vencimiento",
|
||||||
"RunTestCases": "Ejecutar",
|
"RunTestCases": "Ejecutar",
|
||||||
"TestCaseDescription": "Descripción del caso de prueba",
|
"TestCaseDescription": "Descripción del caso de prueba",
|
||||||
"TestResultAttributes": "Resultado"
|
"TestResultAttributes": "Resultado",
|
||||||
|
"GoToNextTest": "Siguiente",
|
||||||
|
"GoToNextTestTooltip": "Ir al siguiente test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
"DueDate": "Date d'échéance",
|
"DueDate": "Date d'échéance",
|
||||||
"RunTestCases": "Exécuter",
|
"RunTestCases": "Exécuter",
|
||||||
"TestCaseDescription": "Description du cas de test",
|
"TestCaseDescription": "Description du cas de test",
|
||||||
"TestResultAttributes": "Résultat"
|
"TestResultAttributes": "Résultat",
|
||||||
|
"GoToNextTest": "Suivant",
|
||||||
|
"GoToNextTestTooltip": "Aller au test suivant"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
"DueDate": "Data di scadenza",
|
"DueDate": "Data di scadenza",
|
||||||
"RunTestCases": "Esegui",
|
"RunTestCases": "Esegui",
|
||||||
"TestCaseDescription": "Descrizione del caso di test",
|
"TestCaseDescription": "Descrizione del caso di test",
|
||||||
"TestResultAttributes": "Risultato"
|
"TestResultAttributes": "Risultato",
|
||||||
|
"GoToNextTest": "Successivo",
|
||||||
|
"GoToNextTestTooltip": "Vai al test successivo"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
"DueDate": "Data de vencimento",
|
"DueDate": "Data de vencimento",
|
||||||
"RunTestCases": "Executar",
|
"RunTestCases": "Executar",
|
||||||
"TestCaseDescription": "Descrição do caso de teste",
|
"TestCaseDescription": "Descrição do caso de teste",
|
||||||
"TestResultAttributes": "Resultado"
|
"TestResultAttributes": "Resultado",
|
||||||
|
"GoToNextTest": "Próximo",
|
||||||
|
"GoToNextTestTooltip": "Ir para o próximo teste"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
"DueDate": "Выполнить до",
|
"DueDate": "Выполнить до",
|
||||||
"RunTestCases": "Выполнить",
|
"RunTestCases": "Выполнить",
|
||||||
"TestCaseDescription": "Описание тест-кейса",
|
"TestCaseDescription": "Описание тест-кейса",
|
||||||
"TestResultAttributes": "Результат"
|
"TestResultAttributes": "Результат",
|
||||||
|
"GoToNextTest": "Следующий",
|
||||||
|
"GoToNextTestTooltip": "Перейти к следующему тесту"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,8 @@
|
|||||||
"DueDate": "到期日",
|
"DueDate": "到期日",
|
||||||
"RunTestCases": "运行",
|
"RunTestCases": "运行",
|
||||||
"TestCaseDescription": "測試用例描述",
|
"TestCaseDescription": "測試用例描述",
|
||||||
"TestResultAttributes": "結果"
|
"TestResultAttributes": "結果",
|
||||||
|
"GoToNextTest": "下一個",
|
||||||
|
"GoToNextTestTooltip": "轉到下一個測試"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
import { DocAttributeBar, getDocMixins } from '@hcengineering/view-resources'
|
import { DocAttributeBar, getDocMixins } from '@hcengineering/view-resources'
|
||||||
|
|
||||||
import RightHeader from './RightHeader.svelte'
|
import RightHeader from './RightHeader.svelte'
|
||||||
|
import NextButton from './NextButton.svelte'
|
||||||
import TestCaseDetails from '../test-case/TestCaseDetails.svelte'
|
import TestCaseDetails from '../test-case/TestCaseDetails.svelte'
|
||||||
import testManagement from '../../plugin'
|
import testManagement from '../../plugin'
|
||||||
|
|
||||||
@ -92,6 +93,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<svelte:fragment slot="extra">
|
||||||
|
<NextButton {object} />
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
<svelte:fragment slot="aside">
|
<svelte:fragment slot="aside">
|
||||||
<DocAttributeBar {object} {mixins} ignoreKeys={['name']} />
|
<DocAttributeBar {object} {mixins} ignoreKeys={['name']} />
|
||||||
<RightHeader>
|
<RightHeader>
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
<!--
|
||||||
|
// 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 { onMount, onDestroy } from 'svelte'
|
||||||
|
import { Button, Loading, Location, navigate } from '@hcengineering/ui'
|
||||||
|
import { initializeIterator, testResultIteratorProvider, testIteratorStore } from './store/testIteratorStore'
|
||||||
|
import testManagement, { TestResult } from '@hcengineering/test-management'
|
||||||
|
import { Doc, type DocumentQuery, WithLookup } from '@hcengineering/core'
|
||||||
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
import { getObjectLinkFragment } from '@hcengineering/view-resources'
|
||||||
|
import view from '@hcengineering/view'
|
||||||
|
|
||||||
|
export let object: WithLookup<TestResult> | undefined
|
||||||
|
let isLoading = true
|
||||||
|
let hasNext = false
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
|
const unsubscribe = testIteratorStore.subscribe(() => {
|
||||||
|
hasNext = testResultIteratorProvider.getIterator()?.hasNext() ?? false
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const query: DocumentQuery<TestResult> = { attachedTo: object?.attachedTo } as any
|
||||||
|
await initializeIterator(query, object?._id)
|
||||||
|
hasNext = testResultIteratorProvider.getIterator()?.hasNext() ?? false
|
||||||
|
isLoading = false
|
||||||
|
})
|
||||||
|
onDestroy(() => {
|
||||||
|
testResultIteratorProvider.reset()
|
||||||
|
unsubscribe()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function goToNextItem (): Promise<void> {
|
||||||
|
const iterator = testResultIteratorProvider.getIterator()
|
||||||
|
if (iterator !== undefined) {
|
||||||
|
const nextItem = iterator.next()
|
||||||
|
if (nextItem === undefined) {
|
||||||
|
console.error('No next item')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const link = await getLink(nextItem)
|
||||||
|
if (link !== undefined) {
|
||||||
|
navigate(link)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Next item:', nextItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLink (object: Doc): Promise<Location> {
|
||||||
|
const { component } = hierarchy.classHierarchyMixin(testManagement.class.TestResult, view.mixin.ObjectPanel) as any
|
||||||
|
return await getObjectLinkFragment(hierarchy, object, {}, component)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isLoading}
|
||||||
|
<Loading />
|
||||||
|
{:else}
|
||||||
|
<Button
|
||||||
|
label={testManagement.string.GoToNextTest}
|
||||||
|
kind={'primary'}
|
||||||
|
icon={view.icon.ArrowRight}
|
||||||
|
disabled={!hasNext}
|
||||||
|
on:click={goToNextItem}
|
||||||
|
showTooltip={{ label: testManagement.string.GoToNextTestTooltip }}
|
||||||
|
/>
|
||||||
|
{/if}
|
@ -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 { writable, get } from 'svelte/store'
|
||||||
|
import {
|
||||||
|
type IteratorState,
|
||||||
|
type StoreAdapter,
|
||||||
|
ObjectIteratorProvider,
|
||||||
|
getDefaultIteratorState
|
||||||
|
} from '@hcengineering/view-resources'
|
||||||
|
import testManagement, { type TestResult } from '@hcengineering/test-management'
|
||||||
|
import type { DocumentQuery, Ref } from '@hcengineering/core'
|
||||||
|
|
||||||
|
export const testIteratorStore = writable<IteratorState<TestResult>>(getDefaultIteratorState<TestResult>({}))
|
||||||
|
|
||||||
|
const adapter: StoreAdapter<TestResult> = {
|
||||||
|
set (value: IteratorState<TestResult>) {
|
||||||
|
testIteratorStore.set(value)
|
||||||
|
},
|
||||||
|
update (updater: (value: IteratorState<TestResult>) => IteratorState<TestResult>) {
|
||||||
|
testIteratorStore.update(updater)
|
||||||
|
},
|
||||||
|
get () {
|
||||||
|
return get(testIteratorStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testResultIteratorProvider = new ObjectIteratorProvider<TestResult>(adapter)
|
||||||
|
|
||||||
|
export async function initializeIterator (
|
||||||
|
query: DocumentQuery<TestResult>,
|
||||||
|
currentObject: Ref<TestResult> | undefined
|
||||||
|
): Promise<void> {
|
||||||
|
await testResultIteratorProvider.initialize(testManagement.class.TestResult, query, currentObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetTestObjectIterator (): void {
|
||||||
|
testResultIteratorProvider.reset()
|
||||||
|
}
|
@ -185,7 +185,9 @@ export const testManagementPlugin = plugin(testManagementId, {
|
|||||||
TestResults: '' as IntlString,
|
TestResults: '' as IntlString,
|
||||||
RunTestCases: '' as IntlString,
|
RunTestCases: '' as IntlString,
|
||||||
TestCaseDescription: '' as IntlString,
|
TestCaseDescription: '' as IntlString,
|
||||||
TestResultAttributes: '' as IntlString
|
TestResultAttributes: '' as IntlString,
|
||||||
|
GoToNextTest: '' as IntlString,
|
||||||
|
GoToNextTestTooltip: '' as IntlString
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
TestManagement: '' as Ref<ActionCategory>
|
TestManagement: '' as Ref<ActionCategory>
|
||||||
|
142
plugins/view-resources/src/__tests__/objectIterator.test.ts
Normal file
142
plugins/view-resources/src/__tests__/objectIterator.test.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// 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 { ObjectIterator, type StoreAdapter, type IteratorState, ObjectIteratorProvider } from '../objectIterator'
|
||||||
|
import { type DocumentQuery, type Doc, type Ref, type Class } from '@hcengineering/core'
|
||||||
|
|
||||||
|
let mockObjects: Doc[] = []
|
||||||
|
const findAll = jest.fn(() => mockObjects)
|
||||||
|
jest.mock('@hcengineering/presentation', () => ({
|
||||||
|
getClient: jest.fn(() => ({
|
||||||
|
findAll
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('ObjectIterator', () => {
|
||||||
|
let storeAdapter: StoreAdapter<Doc>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
let state: IteratorState<Doc> = {
|
||||||
|
query: {},
|
||||||
|
currentObjects: [],
|
||||||
|
iteratorIndex: 0,
|
||||||
|
limit: 0
|
||||||
|
}
|
||||||
|
storeAdapter = {
|
||||||
|
set: jest.fn((newState) => {
|
||||||
|
state = newState
|
||||||
|
}),
|
||||||
|
update: jest.fn((updater) => {
|
||||||
|
state = updater(state)
|
||||||
|
}),
|
||||||
|
get: jest.fn(() => state)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should initialize the store with the given query', async () => {
|
||||||
|
const query: DocumentQuery<Doc> = { key: 'value' }
|
||||||
|
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-new
|
||||||
|
new ObjectIterator(_class, query, storeAdapter)
|
||||||
|
expect(storeAdapter.set).toHaveBeenCalledWith({
|
||||||
|
query,
|
||||||
|
currentObjects: [],
|
||||||
|
iteratorIndex: 0,
|
||||||
|
limit: 100
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load objects and update the store', async () => {
|
||||||
|
mockObjects = [{ id: '1' }, { id: '2' }] as any
|
||||||
|
|
||||||
|
const query: DocumentQuery<Doc> = { key: 'value' }
|
||||||
|
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
|
||||||
|
|
||||||
|
const iterator = new ObjectIterator(_class, query, storeAdapter)
|
||||||
|
await iterator.loadObjects(undefined)
|
||||||
|
|
||||||
|
expect(findAll).toHaveBeenCalledWith(_class, query, {
|
||||||
|
limit: 100,
|
||||||
|
total: true
|
||||||
|
})
|
||||||
|
expect(storeAdapter.update).toHaveBeenCalledWith(expect.any(Function))
|
||||||
|
const state = storeAdapter.get()
|
||||||
|
expect(state?.currentObjects.length).toBe(2)
|
||||||
|
expect(state?.iteratorIndex).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the next object and update the iterator', async () => {
|
||||||
|
mockObjects = [{ id: '1' }, { id: '2' }, { id: '3' }] as any
|
||||||
|
const query: DocumentQuery<Doc> = { key: 'value' }
|
||||||
|
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
|
||||||
|
const iterator = new ObjectIterator(_class, query, storeAdapter)
|
||||||
|
await iterator.loadObjects(undefined)
|
||||||
|
let nextObject = iterator.next()
|
||||||
|
|
||||||
|
expect(nextObject).toEqual(mockObjects[0])
|
||||||
|
expect(storeAdapter.update).toHaveBeenCalledWith(expect.any(Function))
|
||||||
|
|
||||||
|
nextObject = iterator.next()
|
||||||
|
expect(nextObject).toEqual(mockObjects[1])
|
||||||
|
|
||||||
|
nextObject = iterator.next()
|
||||||
|
expect(nextObject).toEqual(mockObjects[2])
|
||||||
|
|
||||||
|
nextObject = iterator.next()
|
||||||
|
expect(nextObject).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined for empty array', async () => {
|
||||||
|
mockObjects = [] as any
|
||||||
|
const query: DocumentQuery<Doc> = { key: 'value' }
|
||||||
|
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
|
||||||
|
const iterator = new ObjectIterator(_class, query, storeAdapter)
|
||||||
|
await iterator.loadObjects(undefined)
|
||||||
|
const nextObject = iterator.next()
|
||||||
|
|
||||||
|
expect(nextObject).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not create a new ObjectIterator if already defined', async () => {
|
||||||
|
const query: DocumentQuery<Doc> = { key: 'value' }
|
||||||
|
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
|
||||||
|
const provider = new ObjectIteratorProvider(storeAdapter)
|
||||||
|
|
||||||
|
await provider.initialize(_class, query, undefined)
|
||||||
|
const firstIterator = provider.getIterator()
|
||||||
|
|
||||||
|
await provider.initialize(_class, query, undefined)
|
||||||
|
const secondIterator = provider.getIterator()
|
||||||
|
|
||||||
|
expect(firstIterator).toBe(secondIterator)
|
||||||
|
expect(firstIterator).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reset the state of ObjectIterator', async () => {
|
||||||
|
const query: DocumentQuery<Doc> = { key: 'value' }
|
||||||
|
const _class: Ref<Class<Doc>> = { id: 'class1' } as any
|
||||||
|
const provider = new ObjectIteratorProvider(storeAdapter)
|
||||||
|
|
||||||
|
await provider.initialize(_class, query, undefined)
|
||||||
|
provider.reset()
|
||||||
|
|
||||||
|
expect(storeAdapter.set).toHaveBeenCalledWith({
|
||||||
|
query: {},
|
||||||
|
currentObjects: [],
|
||||||
|
iteratorIndex: 0,
|
||||||
|
limit: 100
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -175,6 +175,7 @@ export * from './middleware'
|
|||||||
export * from './selection'
|
export * from './selection'
|
||||||
export * from './status'
|
export * from './status'
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
|
export * from './objectIterator'
|
||||||
export {
|
export {
|
||||||
buildModel,
|
buildModel,
|
||||||
getActiveViewletId,
|
getActiveViewletId,
|
||||||
|
113
plugins/view-resources/src/objectIterator.ts
Normal file
113
plugins/view-resources/src/objectIterator.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// 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 { DocumentQuery, Doc, Ref, Class } from '@hcengineering/core'
|
||||||
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
|
||||||
|
export interface IteratorState<T extends Doc> {
|
||||||
|
query: DocumentQuery<T>
|
||||||
|
currentObjects: T[]
|
||||||
|
iteratorIndex: number
|
||||||
|
limit: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StoreAdapter<T extends Doc> {
|
||||||
|
set: (value: IteratorState<T>) => void
|
||||||
|
update: (updater: (value: IteratorState<T>) => IteratorState<T>) => void
|
||||||
|
get: () => IteratorState<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultIteratorState<T extends Doc> (query: DocumentQuery<T>): IteratorState<T> {
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
currentObjects: [],
|
||||||
|
iteratorIndex: 0,
|
||||||
|
limit: 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ObjectIterator<T extends Doc> {
|
||||||
|
private readonly storeAdapter: StoreAdapter<T>
|
||||||
|
private readonly class: Ref<Class<T>>
|
||||||
|
|
||||||
|
constructor (_class: Ref<Class<T>>, query: DocumentQuery<T>, storeAdapter: StoreAdapter<T>) {
|
||||||
|
this.class = _class
|
||||||
|
this.storeAdapter = storeAdapter
|
||||||
|
this.storeAdapter.set(getDefaultIteratorState<T>(query))
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadObjects (currentObject: Ref<Doc> | undefined): Promise<void> {
|
||||||
|
const client = getClient()
|
||||||
|
const { query, limit } = this.storeAdapter.get()
|
||||||
|
const testResults = await client.findAll(this.class, query, {
|
||||||
|
limit,
|
||||||
|
total: true
|
||||||
|
})
|
||||||
|
this.storeAdapter.update((store) => {
|
||||||
|
store.currentObjects = [...store.currentObjects, ...testResults]
|
||||||
|
store.limit = testResults.total
|
||||||
|
if (currentObject !== undefined) {
|
||||||
|
store.iteratorIndex = store.currentObjects.findIndex((obj) => obj._id === currentObject) ?? 0
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
next (): T | undefined {
|
||||||
|
let nextObject
|
||||||
|
this.storeAdapter.update((store) => {
|
||||||
|
if (store.iteratorIndex < store.currentObjects.length) {
|
||||||
|
store.iteratorIndex += 1
|
||||||
|
nextObject = store.currentObjects[store.iteratorIndex]
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
})
|
||||||
|
return nextObject
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNext (): boolean {
|
||||||
|
const { currentObjects, iteratorIndex } = this.storeAdapter.get()
|
||||||
|
return iteratorIndex < currentObjects.length - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ObjectIteratorProvider<T extends Doc> {
|
||||||
|
private objectIterator: ObjectIterator<T> | undefined = undefined
|
||||||
|
|
||||||
|
constructor (private readonly storeAdapter: StoreAdapter<T>) {}
|
||||||
|
|
||||||
|
async initialize (_class: Ref<Class<T>>, query: DocumentQuery<T>, currentObject: Ref<Doc> | undefined): Promise<void> {
|
||||||
|
if (this.objectIterator === undefined) {
|
||||||
|
this.objectIterator = new ObjectIterator(_class, query, this.storeAdapter)
|
||||||
|
await this.objectIterator.loadObjects(currentObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset (): void {
|
||||||
|
this.objectIterator = undefined
|
||||||
|
this.storeAdapter.set(getDefaultIteratorState<T>({}))
|
||||||
|
}
|
||||||
|
|
||||||
|
getObject (): T | undefined {
|
||||||
|
if (this.objectIterator === undefined) {
|
||||||
|
console.error('ObjectIterator is not initialized')
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return this.objectIterator?.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
getIterator (): ObjectIterator<T> | undefined {
|
||||||
|
return this.objectIterator
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user