diff --git a/plugins/task-resources/package.json b/plugins/task-resources/package.json index c0f894786c5..a6fe0108705 100644 --- a/plugins/task-resources/package.json +++ b/plugins/task-resources/package.json @@ -8,12 +8,14 @@ "build": "compile ui", "build:docs": "api-extractor run --local", "format": "format src", + "test": "jest --passWithNoTests --silent", "svelte-check": "do-svelte-check", "_phase:svelte-check": "do-svelte-check", "build:watch": "compile ui", "_phase:build": "compile ui", "_phase:format": "format src", - "_phase:validate": "compile validate" + "_phase:validate": "compile validate", + "_phase:test": "jest --passWithNoTests --silent" }, "devDependencies": { "svelte-loader": "^3.2.0", diff --git a/plugins/task-resources/src/__tests__/statusProjectType.test.ts b/plugins/task-resources/src/__tests__/statusProjectType.test.ts new file mode 100644 index 00000000000..bbddeeb469c --- /dev/null +++ b/plugins/task-resources/src/__tests__/statusProjectType.test.ts @@ -0,0 +1,57 @@ +// +// Copyright © 2026 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 { IdMap, Ref, Status } from '@hcengineering/core' +import type { ProjectStatus, ProjectType, TaskType } from '@hcengineering/task' +import { getStatusProjectType } from '../statusProjectType' + +const status = 'status-a' as Ref +const other = 'status-b' as Ref + +function projectType (id: string, statuses: Array>): ProjectType { + return { + _id: id as Ref, + statuses: statuses.map((it) => ({ taskType: 'tt' as Ref, ...it })) + } as unknown as ProjectType +} + +function typesMap (...types: ProjectType[]): IdMap { + return new Map(types.map((it) => [it._id, it])) +} + +describe('getStatusProjectType', () => { + const plain = projectType('plain', [{ _id: status }]) + const colored = projectType('colored', [{ _id: status, color: 5 }]) + const unrelated = projectType('unrelated', [{ _id: other, color: 3 }]) + const types = typesMap(plain, colored, unrelated) + + it('uses the selected project type when present', () => { + expect(getStatusProjectType(status, unrelated._id, types, [plain._id, colored._id])).toBe(unrelated._id) + }) + + it('prefers a joined project type that configures the status color', () => { + expect(getStatusProjectType(status, undefined, types, [unrelated._id, plain._id, colored._id])).toBe(colored._id) + }) + + it('falls back to the first joined project type containing the status', () => { + expect(getStatusProjectType(status, undefined, types, [unrelated._id, plain._id])).toBe(plain._id) + }) + + it('returns undefined when no joined project type contains the status', () => { + expect(getStatusProjectType(status, undefined, types, [unrelated._id, 'missing' as Ref])).toBe( + undefined + ) + }) +}) diff --git a/plugins/task-resources/src/components/StatusFilter.svelte b/plugins/task-resources/src/components/StatusFilter.svelte index 1c1fd557918..f1c032e4c65 100644 --- a/plugins/task-resources/src/components/StatusFilter.svelte +++ b/plugins/task-resources/src/components/StatusFilter.svelte @@ -40,6 +40,7 @@ import { buildConfigLookup, getPresenter } from '@hcengineering/view-resources/src/utils' import { createEventDispatcher } from 'svelte' import { typesOfJoinedProjectsStore, selectedTaskTypeStore, selectedTypeStore, taskTypeStore, typeStore } from '..' + import { getStatusProjectType } from '../statusProjectType' export let filter: Filter export let space: Ref | undefined = undefined @@ -183,6 +184,19 @@ }, FILTER_DEBOUNCE_MS) } + // Without a space (e.g. All Issues) pick a project type per status, so its own color is shown, not the category one. + $: getPresenterProps = (status: Status): Record => + space === undefined + ? { + projectType: getStatusProjectType( + status._id, + $selectedTypeStore, + $typeStore, + $typesOfJoinedProjectsStore ?? [] + ) + } + : {} + let search: string = '' const dispatch = createEventDispatcher() @@ -237,6 +251,7 @@ {value} {...attribute.props} {space} + {...getPresenterProps(value)} disabled oneLine /> diff --git a/plugins/task-resources/src/statusProjectType.ts b/plugins/task-resources/src/statusProjectType.ts new file mode 100644 index 00000000000..23643275523 --- /dev/null +++ b/plugins/task-resources/src/statusProjectType.ts @@ -0,0 +1,41 @@ +// +// Copyright © 2026 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 { IdMap, Ref, Status } from '@hcengineering/core' +import type { ProjectType } from '@hcengineering/task' + +/** + * Resolve the project type used to present a status when no space is known (e.g. All Issues). + * The selected project type wins; otherwise a joined project type that configures a color or icon + * for the status is preferred, falling back to the first joined project type that contains it. + * + * @public + */ +export function getStatusProjectType ( + status: Ref, + selectedProjectType: Ref | undefined, + projectTypes: IdMap, + joinedProjectTypes: Array> +): Ref | undefined { + if (selectedProjectType !== undefined) return selectedProjectType + let fallback: Ref | undefined + for (const typeId of joinedProjectTypes) { + const projectStatus = projectTypes.get(typeId)?.statuses?.find((it) => it._id === status) + if (projectStatus === undefined) continue + if (projectStatus.color !== undefined || projectStatus.icon !== undefined) return typeId + fallback = fallback ?? typeId + } + return fallback +}