diff --git a/docs/wiki/2.07-Manage-Task-Integrations.md b/docs/wiki/2.07-Manage-Task-Integrations.md index 7acbf65585..da11594c15 100755 --- a/docs/wiki/2.07-Manage-Task-Integrations.md +++ b/docs/wiki/2.07-Manage-Task-Integrations.md @@ -28,6 +28,10 @@ If the integration “doesn’t work,” check that you used the right link and After saving, a new **tab** appears in the panel for that provider. You can have multiple instances of the same type (e.g. two Jira configurations) and reorder tabs by dragging. +### Azure DevOps and Self-Hosted TFS URLs + +For **Azure DevOps**, enter the full organization or collection URL in **Host**. For Azure DevOps Services this looks like `https://dev.azure.com/`. For self-hosted Azure DevOps Server/TFS this can include the collection path, for example `https://server:8080/tfs/DefaultCollection`. The separate **Project** field is still required. + ## Add an Issue or Calendar Item as a Task 1. Open the issue provider panel and select the **tab** for the provider (e.g. Jira or GitHub). diff --git a/src/app/features/issue/providers/azure-devops/azure-devops-api.service.spec.ts b/src/app/features/issue/providers/azure-devops/azure-devops-api.service.spec.ts index 2b18d57074..94cd6bd726 100644 --- a/src/app/features/issue/providers/azure-devops/azure-devops-api.service.spec.ts +++ b/src/app/features/issue/providers/azure-devops/azure-devops-api.service.spec.ts @@ -63,6 +63,22 @@ describe('AzureDevOpsApiService', () => { req.flush({ workItems: [] }); }); + it('should use the configured host directly for self-hosted Azure DevOps Server URLs (#7672)', () => { + const cfg: AzureDevOpsCfg = { + ...mockCfg, + host: 'https://ado.example.com/tfs/DefaultCollection', + organization: null, + }; + + service.searchIssues$('test', cfg).subscribe(); + + const req = httpMock.expectOne( + `${cfg.host}/${cfg.project}/_apis/wit/wiql?api-version=6.0`, + ); + + req.flush({ workItems: [] }); + }); + it('should map search results correctly including ticket type', () => { const searchTerm = 'test'; service.searchIssues$(searchTerm, mockCfg).subscribe((issues) => { @@ -108,5 +124,22 @@ describe('AzureDevOpsApiService', () => { req.flush({ authenticatedUser: { providerDisplayName: 'Test User' } }); }); + + it('should not require organization when host already contains the collection path (#7672)', () => { + const cfg: AzureDevOpsCfg = { + ...mockCfg, + host: 'https://ado.example.com/tfs/DefaultCollection', + organization: null, + }; + + service.getCurrentUser$(cfg).subscribe(); + + const req = httpMock.expectOne( + `${cfg.host}/_apis/connectionData?api-version=5.1-preview`, + ); + expect(req.request.method).toBe('GET'); + + req.flush({ authenticatedUser: { providerDisplayName: 'Test User' } }); + }); }); }); diff --git a/src/app/features/issue/providers/azure-devops/azure-devops-cfg-form.const.spec.ts b/src/app/features/issue/providers/azure-devops/azure-devops-cfg-form.const.spec.ts new file mode 100644 index 0000000000..8a0bca8f4a --- /dev/null +++ b/src/app/features/issue/providers/azure-devops/azure-devops-cfg-form.const.spec.ts @@ -0,0 +1,17 @@ +import { AZURE_DEVOPS_CONFIG_FORM_SECTION } from './azure-devops-cfg-form.const'; + +describe('AZURE_DEVOPS_CONFIG_FORM_SECTION', () => { + it('does not require a separate organization field when host is the full Azure DevOps URL (#7672)', () => { + const fields = AZURE_DEVOPS_CONFIG_FORM_SECTION.items ?? []; + const keys = fields.map((field) => field.key); + const hostField = fields.find((field) => field.key === 'host'); + + expect(keys).toContain('host'); + expect(keys).not.toContain('organization'); + expect(hostField?.templateOptions?.required).toBeTrue(); + expect(hostField?.templateOptions?.label).toBe('F.AZURE_DEVOPS.FORM.HOST'); + expect(hostField?.templateOptions?.description).toBe( + 'F.AZURE_DEVOPS.FORM.HOST_DESCRIPTION', + ); + }); +}); diff --git a/src/app/features/issue/providers/azure-devops/azure-devops-cfg-form.const.ts b/src/app/features/issue/providers/azure-devops/azure-devops-cfg-form.const.ts index 745d13b321..6b7e03169d 100644 --- a/src/app/features/issue/providers/azure-devops/azure-devops-cfg-form.const.ts +++ b/src/app/features/issue/providers/azure-devops/azure-devops-cfg-form.const.ts @@ -11,25 +11,18 @@ export const AZURE_DEVOPS_CONFIG_FORM_SECTION: ConfigFormSection key: 'host', type: 'input', templateOptions: { - label: 'Host (Organization URL)', + label: T.F.AZURE_DEVOPS.FORM.HOST, placeholder: 'https://dev.azure.com/your-org', + description: T.F.AZURE_DEVOPS.FORM.HOST_DESCRIPTION, required: true, type: 'url', }, }, - { - key: 'organization', - type: 'input', - templateOptions: { - label: 'Organization', - required: true, - }, - }, { key: 'project', type: 'input', templateOptions: { - label: 'Project', + label: T.F.AZURE_DEVOPS.FORM.PROJECT, required: true, }, }, @@ -37,7 +30,7 @@ export const AZURE_DEVOPS_CONFIG_FORM_SECTION: ConfigFormSection key: 'token', type: 'input', templateOptions: { - label: 'Personal Access Token', + label: T.F.AZURE_DEVOPS.FORM.TOKEN, required: true, type: 'password', }, @@ -53,11 +46,14 @@ export const AZURE_DEVOPS_CONFIG_FORM_SECTION: ConfigFormSection defaultValue: 'assigned-to-me', templateOptions: { required: true, - label: 'Scope (Auto Import only)', + label: T.F.AZURE_DEVOPS.FORM.SCOPE, options: [ - { value: 'all', label: 'All' }, - { value: 'created-by-me', label: 'Created by me' }, - { value: 'assigned-to-me', label: 'Assigned to me' }, + { value: 'all', label: T.F.AZURE_DEVOPS.FORM.SCOPE_ALL }, + { value: 'created-by-me', label: T.F.AZURE_DEVOPS.FORM.SCOPE_CREATED }, + { + value: 'assigned-to-me', + label: T.F.AZURE_DEVOPS.FORM.SCOPE_ASSIGNED, + }, ], }, }, diff --git a/src/app/t.const.ts b/src/app/t.const.ts index 6ad7ada90c..f0fd1c2850 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -77,6 +77,18 @@ const T = { 'F.ANDROID.FOREGROUND_SERVICE_START_FAILED_TRACKING', OPEN_NOTIFICATION_SETTINGS: 'F.ANDROID.OPEN_NOTIFICATION_SETTINGS', }, + AZURE_DEVOPS: { + FORM: { + HOST: 'F.AZURE_DEVOPS.FORM.HOST', + HOST_DESCRIPTION: 'F.AZURE_DEVOPS.FORM.HOST_DESCRIPTION', + PROJECT: 'F.AZURE_DEVOPS.FORM.PROJECT', + SCOPE: 'F.AZURE_DEVOPS.FORM.SCOPE', + SCOPE_ALL: 'F.AZURE_DEVOPS.FORM.SCOPE_ALL', + SCOPE_ASSIGNED: 'F.AZURE_DEVOPS.FORM.SCOPE_ASSIGNED', + SCOPE_CREATED: 'F.AZURE_DEVOPS.FORM.SCOPE_CREATED', + TOKEN: 'F.AZURE_DEVOPS.FORM.TOKEN', + }, + }, ATTACHMENT: { LOCAL_FILE_UNAVAILABLE: 'F.ATTACHMENT.LOCAL_FILE_UNAVAILABLE', DIALOG_EDIT: { diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 25d75937ef..229623cdb5 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -75,6 +75,18 @@ "FOREGROUND_SERVICE_START_FAILED_TRACKING": "Android blocked the persistent time tracking notification. Time is still tracked in the app, but background recovery may be unavailable. Check notification permission and battery/background restrictions.", "OPEN_NOTIFICATION_SETTINGS": "Open settings" }, + "AZURE_DEVOPS": { + "FORM": { + "HOST": "Host (organization or collection URL)", + "HOST_DESCRIPTION": "Use the full Azure DevOps URL, for example https://dev.azure.com/your-org or https://server:8080/tfs/DefaultCollection.", + "PROJECT": "Project", + "SCOPE": "Scope (Auto Import only)", + "SCOPE_ALL": "All", + "SCOPE_ASSIGNED": "Assigned to me", + "SCOPE_CREATED": "Created by me", + "TOKEN": "Personal Access Token" + } + }, "ATTACHMENT": { "LOCAL_FILE_UNAVAILABLE": "This attachment is only available on the device where it was added.", "DIALOG_EDIT": {