diff --git a/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte b/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte
index b15f51cc29d..430d1b5330c 100644
--- a/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte
+++ b/plugins/tracker-resources/src/components/milestones/MilestoneSelector.svelte
@@ -13,14 +13,26 @@
// limitations under the License.
-->
dispatch('close')}
+ on:close={() => dispatch('close', createdMilestone)}
on:changeContent
>
diff --git a/plugins/tracker-resources/src/components/milestones/__tests__/milestonePermissions.test.ts b/plugins/tracker-resources/src/components/milestones/__tests__/milestonePermissions.test.ts
new file mode 100644
index 00000000000..f0b69fca2cb
--- /dev/null
+++ b/plugins/tracker-resources/src/components/milestones/__tests__/milestonePermissions.test.ts
@@ -0,0 +1,115 @@
+//
+// Copyright © 2026 Hardcore Engineering Inc.
+// SPDX-License-Identifier: EPL-2.0
+//
+
+import type { PermissionsStore } from '@hcengineering/contact'
+import type { Permission, Ref } from '@hcengineering/core'
+import type { Project } from '@hcengineering/tracker'
+import { canCreateMilestone, canSaveMilestone } from '../milestonePermissions'
+
+jest.mock('@hcengineering/view-resources', () => jest.requireActual('../../../../../view-resources/src/permissions'))
+jest.mock('@hcengineering/platform', () => ({
+ ...jest.requireActual('@hcengineering/platform'),
+ getMetadata: jest.fn()
+}))
+jest.mock('@hcengineering/presentation', () => ({
+ getClient: () => ({
+ getHierarchy: () => ({ getAncestors: (objectClass: string) => [objectClass] }),
+ getModel: () => ({
+ findAllSync: (_class: string, query: { txClass: string }) =>
+ query.txClass === 'core:class:TxCreateDoc'
+ ? [
+ {
+ _id: 'allow-milestone',
+ objectClass: 'tracker:class:Milestone'
+ },
+ {
+ _id: 'forbid-milestone',
+ objectClass: 'tracker:class:Milestone',
+ forbid: true
+ }
+ ]
+ : []
+ })
+ })
+}))
+jest.mock('../../../plugin', () => ({
+ __esModule: true,
+ default: { class: { Milestone: 'tracker:class:Milestone' } }
+}))
+
+const project = 'project' as Ref
+const otherProject = 'other-project' as Ref
+
+function permissions (permission?: string, restricted: boolean = false): PermissionsStore {
+ return {
+ ps: permission === undefined ? {} : { [project]: new Set([permission as Ref]) },
+ ap: {},
+ ms: {},
+ whitelist: new Set(),
+ restrictedSpaces: new Set(restricted ? [project] : [])
+ }
+}
+
+describe('milestone creation permissions', () => {
+ it('offers creation to a permitted user in a restricted project', () => {
+ expect(canCreateMilestone(project, true, false, permissions('allow-milestone', true))).toBe(true)
+ })
+
+ it('hides creation from a forbidden user even in an unrestricted project', () => {
+ expect(canCreateMilestone(project, true, false, permissions('forbid-milestone'))).toBe(false)
+ })
+
+ it('hides creation in a restricted project without a creation grant', () => {
+ const store = permissions(undefined, true)
+ store.ps[project] = new Set()
+ expect(canCreateMilestone(project, true, false, store)).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)
+ expect(canCreateMilestone(project, true, false, permissions('forbid-milestone', true))).toBe(false)
+ })
+
+ it('does not carry permission over to another project', () => {
+ expect(canCreateMilestone(otherProject, true, false, permissions('allow-milestone'))).toBe(false)
+ })
+
+ it('allows creation in a whitelisted project', () => {
+ const store = permissions()
+ store.whitelist.add(project)
+ expect(canCreateMilestone(project, true, false, store)).toBe(true)
+ })
+
+ it('requires an editable selector bound to a single project outside action mode', () => {
+ const store = permissions('allow-milestone')
+ expect(canCreateMilestone(project, false, false, store)).toBe(false)
+ expect(canCreateMilestone(project, true, true, store)).toBe(false)
+ expect(canCreateMilestone(undefined, true, false, store)).toBe(false)
+ 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
new file mode 100644
index 00000000000..bef1c5ffeee
--- /dev/null
+++ b/plugins/tracker-resources/src/components/milestones/milestonePermissions.ts
@@ -0,0 +1,30 @@
+//
+// Copyright © 2026 Hardcore Engineering Inc.
+// SPDX-License-Identifier: EPL-2.0
+//
+
+import type { PermissionsStore } from '@hcengineering/contact'
+import type { DocumentQuery } from '@hcengineering/core'
+import type { Milestone } from '@hcengineering/tracker'
+import { canCreateObject } from '@hcengineering/view-resources'
+import tracker from '../../plugin'
+
+export function canCreateMilestone (
+ space: DocumentQuery['space'] | undefined,
+ isEditable: boolean,
+ isAction: boolean,
+ permissions: PermissionsStore
+): boolean {
+ if (!isEditable || isAction || typeof space !== 'string') return false
+ // The permissions query loads asynchronously. Do not allow creation before this project is known.
+ 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)
+}