From 5eb5942dd82e20053648e62c8485661b94a1aa8e Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Sun, 18 Oct 2026 04:18:48 +0000 Subject: [PATCH 1/3] feat(tracker): create a milestone from the milestone selector The milestone selector used when creating or editing an issue only listed existing milestones, so a milestone that did not exist yet could not be added without leaving the issue dialog. NewMilestone also did not report the created milestone back to its caller. The selector now offers a "New Milestone" entry when it is bound to a single project. It opens the existing NewMilestone dialog for that project and selects the created milestone afterwards (only if it was created in the same project, since the dialog allows changing the project). NewMilestone now passes the id of the created milestone when it closes. Fixes #4858 Signed-off-by: Michael Uray --- .../milestones/MilestoneSelector.svelte | 44 ++++++++++++++++--- .../components/milestones/NewMilestone.svelte | 6 ++- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte b/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte index b15f51cc29d..744e51d7ba6 100644 --- a/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte +++ b/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte @@ -15,12 +15,22 @@ @@ -49,7 +51,7 @@ canSave={object.label !== ''} okLabel={tracker.string.CreateMilestone} gap={'gapV-4'} - on:close={() => dispatch('close')} + on:close={() => dispatch('close', createdMilestone)} on:changeContent > From 397a61160935d27d231f73fd94120121428362b9 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Thu, 29 Oct 2026 06:00:52 +0000 Subject: [PATCH 2/3] fix(tracker): offer milestone creation only to users who may create milestones Signed-off-by: Michael Uray --- .../milestones/MilestoneSelector.svelte | 6 +- .../__tests__/milestonePermissions.test.ts | 98 +++++++++++++++++++ .../milestones/milestonePermissions.ts | 22 +++++ 3 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 plugins/tracker-resources/src/components/milestones/__tests__/milestonePermissions.test.ts create mode 100644 plugins/tracker-resources/src/components/milestones/milestonePermissions.ts diff --git a/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte b/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte index 744e51d7ba6..32cc417bb98 100644 --- a/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte +++ b/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte @@ -13,6 +13,7 @@ // limitations under the License. --> @@ -48,7 +53,7 @@ dispatch('close', createdMilestone)} diff --git a/plugins/tracker-resources/src/components/milestones/__tests__/milestonePermissions.test.ts b/plugins/tracker-resources/src/components/milestones/__tests__/milestonePermissions.test.ts index 8b78ca62b3f..f0b69fca2cb 100644 --- a/plugins/tracker-resources/src/components/milestones/__tests__/milestonePermissions.test.ts +++ b/plugins/tracker-resources/src/components/milestones/__tests__/milestonePermissions.test.ts @@ -6,7 +6,7 @@ import type { PermissionsStore } from '@hcengineering/contact' import type { Permission, Ref } from '@hcengineering/core' import type { Project } from '@hcengineering/tracker' -import { canCreateMilestone } from '../milestonePermissions' +import { canCreateMilestone, canSaveMilestone } from '../milestonePermissions' jest.mock('@hcengineering/view-resources', () => jest.requireActual('../../../../../view-resources/src/permissions')) jest.mock('@hcengineering/platform', () => ({ @@ -67,11 +67,6 @@ describe('milestone creation permissions', () => { expect(canCreateMilestone(project, true, false, store)).toBe(false) }) - it('stays hidden while permissions load and when a forbidden result arrives', () => { - expect(canCreateMilestone(project, true, false, permissions())).toBe(false) - expect(canCreateMilestone(project, true, false, permissions('forbid-milestone'))).toBe(false) - }) - it('appears after permissions load and disappears when permission is revoked', () => { expect(canCreateMilestone(project, true, false, permissions())).toBe(false) expect(canCreateMilestone(project, true, false, permissions('allow-milestone', true))).toBe(true) @@ -96,3 +91,25 @@ describe('milestone creation permissions', () => { expect(canCreateMilestone({ $in: [project, otherProject] }, true, false, store)).toBe(false) }) }) + +describe('milestone dialog save permissions', () => { + it('requires a label even when the user may create milestones', () => { + const store = permissions('allow-milestone', true) + expect(canSaveMilestone(project, '', store)).toBe(false) + expect(canSaveMilestone(project, 'Release', store)).toBe(true) + }) + + it('disables saving when the dialog switches to a forbidden project and enables it on return', () => { + const store = permissions('allow-milestone', true) + store.ps[otherProject] = new Set(['forbid-milestone' as Ref]) + expect(canSaveMilestone(project, 'Release', store)).toBe(true) + expect(canSaveMilestone(otherProject, 'Release', store)).toBe(false) + expect(canSaveMilestone(project, 'Release', store)).toBe(true) + }) + + it('waits for permissions and disables saving when permission is revoked', () => { + expect(canSaveMilestone(project, 'Release', permissions())).toBe(false) + expect(canSaveMilestone(project, 'Release', permissions('allow-milestone', true))).toBe(true) + expect(canSaveMilestone(project, 'Release', permissions('forbid-milestone', true))).toBe(false) + }) +}) diff --git a/plugins/tracker-resources/src/components/milestones/milestonePermissions.ts b/plugins/tracker-resources/src/components/milestones/milestonePermissions.ts index c77beb8090b..bef1c5ffeee 100644 --- a/plugins/tracker-resources/src/components/milestones/milestonePermissions.ts +++ b/plugins/tracker-resources/src/components/milestones/milestonePermissions.ts @@ -20,3 +20,11 @@ export function canCreateMilestone ( if (!permissions.whitelist.has(space) && permissions.ps[space] === undefined) return false return canCreateObject(tracker.class.Milestone, space, permissions) } + +export function canSaveMilestone ( + space: DocumentQuery['space'] | undefined, + label: string, + permissions: PermissionsStore +): boolean { + return label !== '' && canCreateMilestone(space, true, false, permissions) +}