fix(azure-devops): simplify host setup #7672 (#8086)

Co-authored-by: cocojojo5213 <cocojojo5213@users.noreply.github.com>
This commit is contained in:
felix bear 2026-06-08 18:40:19 +09:00 committed by GitHub
parent c0146e2307
commit 667e8c3aa0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 89 additions and 15 deletions

View file

@ -28,6 +28,10 @@ If the integration “doesnt 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/<organization>`. 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).

View file

@ -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' } });
});
});
});

View file

@ -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',
);
});
});

View file

@ -11,25 +11,18 @@ export const AZURE_DEVOPS_CONFIG_FORM_SECTION: ConfigFormSection<AzureDevOpsCfg>
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<AzureDevOpsCfg>
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<AzureDevOpsCfg>
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,
},
],
},
},

View file

@ -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: {

View file

@ -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": {