@gitkraken/core-gitlens 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/dist/git/models/issue.d.ts +3 -1
  3. package/dist/git/models/issue.js.map +1 -1
  4. package/dist/git/utils/issue.utils.d.ts +3 -1
  5. package/dist/git/utils/issue.utils.js +5 -1
  6. package/dist/git/utils/issue.utils.js.map +1 -1
  7. package/dist/plus/integrations/models/gitHostIntegration.js +3 -1
  8. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  9. package/dist/plus/integrations/models/integration.js +1 -1
  10. package/dist/plus/integrations/providers/azure/azure.js +3 -2
  11. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  12. package/dist/plus/integrations/providers/azure/models.d.ts +27 -10
  13. package/dist/plus/integrations/providers/azure/models.js +39 -19
  14. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  15. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +19 -0
  16. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +32 -0
  17. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -0
  18. package/dist/plus/integrations/providers/azureDevOps.js +5 -5
  19. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  20. package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
  21. package/dist/plus/integrations/providers/jira-server.js +11 -5
  22. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  23. package/dist/plus/integrations/providers/jira.d.ts +5 -2
  24. package/dist/plus/integrations/providers/jira.js +45 -30
  25. package/dist/plus/integrations/providers/jira.js.map +1 -1
  26. package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +3 -4
  27. package/dist/plus/integrations/providers/jiraIssueByKey.js +3 -4
  28. package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
  29. package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +5 -5
  30. package/dist/plus/integrations/providers/linearIssuesEtag.js +6 -6
  31. package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -1
  32. package/dist/plus/integrations/providers/models.d.ts +11 -1
  33. package/dist/plus/integrations/providers/models.js +9 -9
  34. package/dist/plus/integrations/providers/models.js.map +1 -1
  35. package/dist/plus/integrations/providers/providersApi.d.ts +30 -32
  36. package/dist/plus/integrations/providers/providersApi.js +149 -46
  37. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  38. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +27 -15
  39. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +72 -10
  40. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
  41. package/dist/plus/integrations/reads/cursors.js +1 -1
  42. package/dist/plus/integrations/reads/drains.d.ts +0 -1
  43. package/dist/plus/integrations/reads/hierarchy.js +2 -2
  44. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  45. package/dist/plus/integrations/reads/hierarchy.utils.d.ts +13 -0
  46. package/dist/plus/integrations/reads/hierarchy.utils.js +26 -1
  47. package/dist/plus/integrations/reads/hierarchy.utils.js.map +1 -1
  48. package/dist/plus/integrations/reads/issueTracker.js +4 -4
  49. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  50. package/dist/plus/integrations/reads/issues.js +1 -1
  51. package/dist/plus/integrations/results.d.ts +12 -2
  52. package/dist/plus/integrations/results.js.map +1 -1
  53. package/dist/plus/integrations/utils/integration.utils.d.ts +6 -1
  54. package/dist/plus/integrations/utils/integration.utils.js +8 -1
  55. package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
  56. package/docs/integrations.md +49 -19
  57. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"jiraIssueByKey.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jiraIssueByKey.ts"],"sourcesContent":["import type { IssueEtagFields } from '../models/integration.js';\nimport type { ProviderAccount, ProviderIssue, ProviderRequestFunction } from './models.js';\n\nconst jiraCloudApiUrl = 'https://api.atlassian.com/ex/jira';\nconst jiraIssueByKeyFields = [\n\t'assignee',\n\t'comment',\n\t'created',\n\t'creator',\n\t'description',\n\t'issuetype',\n\t'labels',\n\t'project',\n\t'status',\n\t'summary',\n\t'updated',\n\t'votes',\n];\n\ntype JiraIssueMemberResponse = {\n\taccountId?: string;\n\tdisplayName?: string | null;\n\temailAddress?: string | null;\n\tavatarUrls?: Record<string, string | undefined>;\n};\n\ntype JiraIssueStatusResponse = {\n\tid: string;\n\tname: string;\n\tstatusCategory: {\n\t\tcolorName: string;\n\t\tkey: string;\n\t\tname: string;\n\t};\n};\n\ntype JiraIssueByKeyResponse = {\n\tid: string;\n\tkey: string;\n\tfields: {\n\t\tassignee?: JiraIssueMemberResponse | null;\n\t\tcomment?: { comments?: unknown[]; total?: number };\n\t\tcreated: string;\n\t\tcreator?: JiraIssueMemberResponse | null;\n\t\tdescription?: string | null;\n\t\tissuetype?: { name?: string | null };\n\t\tlabels?: string[];\n\t\tproject?: { id?: string | null; key?: string | null; name?: string | null };\n\t\tstatus: JiraIssueStatusResponse;\n\t\tsummary: string;\n\t\tupdated: string;\n\t\tvotes?: { votes?: number | null };\n\t};\n};\n\nfunction toAccount(member: JiraIssueMemberResponse | null | undefined): ProviderAccount | null {\n\tif (member == null) return null;\n\n\treturn {\n\t\tid: member.accountId ?? '',\n\t\tname: member.displayName ?? null,\n\t\tusername: member.displayName ?? null,\n\t\temail: member.emailAddress ?? null,\n\t\tavatarUrl: member.avatarUrls?.['48x48'] ?? null,\n\t\turl: null,\n\t};\n}\n\nfunction toStatusCategory(key: string): 'TO_DO' | 'IN_PROGRESS' | 'DONE' {\n\tswitch (key.toLowerCase()) {\n\t\tcase 'new':\n\t\t\treturn 'TO_DO';\n\t\tcase 'indeterminate':\n\t\t\treturn 'IN_PROGRESS';\n\t\tcase 'done':\n\t\t\treturn 'DONE';\n\t\tdefault:\n\t\t\treturn 'TO_DO';\n\t}\n}\n\nfunction toJiraIssueState(status: JiraIssueStatusResponse): NonNullable<ProviderIssue['state']> {\n\treturn {\n\t\tid: status.id,\n\t\tname: status.name,\n\t\tcolor: status.statusCategory.colorName,\n\t\tcategory: toStatusCategory(status.statusCategory.key),\n\t};\n}\n\nfunction toIssueWebUrl(resourceUrl: string, key: string): string {\n\tconst url = new URL(resourceUrl);\n\turl.pathname = `${url.pathname.replace(/\\/+$/, '')}/browse/${encodeURIComponent(key)}`;\n\turl.search = '';\n\turl.hash = '';\n\treturn url.toString();\n}\n\nfunction fromJiraIssueByKey(issue: JiraIssueByKeyResponse, resourceId: string, resourceUrl: string): ProviderIssue {\n\tconst assignee = toAccount(issue.fields.assignee);\n\tconst project = issue.fields.project;\n\tconst status = issue.fields.status;\n\n\treturn {\n\t\tid: issue.id,\n\t\tcommentCount: issue.fields.comment?.total ?? issue.fields.comment?.comments?.length ?? null,\n\t\tnumber: issue.key,\n\t\ttitle: issue.fields.summary,\n\t\turl: toIssueWebUrl(resourceUrl, issue.key),\n\t\tclosedDate: null,\n\t\tcreatedDate: new Date(issue.fields.created),\n\t\tauthor: toAccount(issue.fields.creator),\n\t\tupdatedDate: new Date(issue.fields.updated),\n\t\tassignees: assignee != null ? [assignee] : [],\n\t\tdescription: issue.fields.description ?? null,\n\t\trepository: null,\n\t\tproject:\n\t\t\tproject != null\n\t\t\t\t? {\n\t\t\t\t\t\tname: project.name ?? '',\n\t\t\t\t\t\tresourceId: resourceId,\n\t\t\t\t\t\tkey: project.key ?? null,\n\t\t\t\t\t\tnamespace: null,\n\t\t\t\t\t\tid: project.id ?? null,\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\tstate: toJiraIssueState(status),\n\t\ttype: issue.fields.issuetype?.name ?? null,\n\t\tupvoteCount: issue.fields.votes?.votes ?? null,\n\t\tlabels: (issue.fields.labels ?? []).map(label => ({\n\t\t\tcolor: null,\n\t\t\tdescription: null,\n\t\t\tid: null,\n\t\t\tname: label,\n\t\t})),\n\t};\n}\n\n/**\n * The SDK's Jira point read performs resource and field discovery before the issue GET. This focused path uses\n * caller-supplied resource identity so a cold lookup remains one upstream request.\n */\nexport async function requestJiraIssueByKey(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tresourceId: string,\n\tresourceUrl: string,\n\tkey: string,\n): Promise<ProviderIssue> {\n\tconst url = new URL(\n\t\t`${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/2/issue/${encodeURIComponent(key)}`,\n\t);\n\turl.searchParams.set('fields', jiraIssueByKeyFields.join(','));\n\n\tconst response = await request<JiraIssueByKeyResponse>({\n\t\turl: url.toString(),\n\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t});\n\treturn fromJiraIssueByKey(response.body, resourceId, resourceUrl);\n}\n\n/**\n * The most keys one bulk fetch takes. Atlassian allows up to 1000 for a request that names its fields explicitly, but\n * documents 100 as the default limit.\n */\nexport const jiraBulkFetchMaxKeys = 100;\n\n/**\n * One issue of a bulk fetch that asks for `status` and `updated` only. The bulk fetch is API v3 while the full read is\n * v2, but these two fields have the same shape in both: the v2 and v3 OpenAPI specs define the same `StatusDetails`\n * and `StatusCategory`, and a live comparison found both fields byte-identical. v3 differs in rich-text fields, which\n * it returns as ADF, so this request must never widen to `description`. `self` differs too, and is never read.\n */\nexport type JiraIssueEtagResponse = {\n\tid: string;\n\tkey: string;\n\tfields: Partial<Pick<JiraIssueByKeyResponse['fields'], 'status' | 'updated'>>;\n};\n\ntype JiraBulkFetchResponse = {\n\tissues?: JiraIssueEtagResponse[];\n\t/**\n\t * Only issues Jira could not return for a retriable error or a payload constraint, by numeric issue id rather than\n\t * the key asked for. Issues that don't exist or aren't visible are NOT listed here, nor in `issues`: they are\n\t * silently omitted, so a bulk fetch never proves an absence.\n\t */\n\tissueErrors?: { id?: string; errorMessage?: string }[];\n};\n\n/**\n * The cheap check behind the batch issue read's etags: `status` and `updated` of up to {@link jiraBulkFetchMaxKeys}\n * issues of one site in ONE request, where {@link requestJiraIssueByKey} reads one issue per request. Jira returns the\n * issues in its own order, each under its CURRENT key (a lowercase or moved key resolves to the issue it names now), and\n * omits the ones it can't find, so a caller matches them to its keys by key, never by position.\n */\nexport async function requestJiraIssuesEtagFields(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tresourceId: string,\n\tkeys: readonly string[],\n): Promise<{ issues: JiraIssueEtagResponse[]; errorCount: number }> {\n\tconst response = await request<JiraBulkFetchResponse | null>({\n\t\turl: `${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/3/issue/bulkfetch`,\n\t\tmethod: 'POST',\n\t\theaders: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ issueIdsOrKeys: keys, fields: ['status', 'updated'] }),\n\t});\n\n\tconst issues = response.body?.issues;\n\tif (!Array.isArray(issues)) throw new Error('Jira bulk fetch returned no issues');\n\n\treturn { issues: issues, errorCount: response.body?.issueErrors?.length ?? 0 };\n}\n\n/**\n * A bulk-fetched issue's change state, as the fields the full row's etag reads. {@link fromJiraIssueByKey} maps the\n * status with the same {@link toJiraIssueState} and never sets `closedDate`, so `toIssueShape` (with\n * `reliableStateCategory`) closes exactly the issues whose status category is done. Throws on a missing status or an\n * unparseable `updated`, so the target falls through to the full read rather than matching an etag that would ignore\n * its update time.\n */\nexport function toJiraIssueEtagFields(issue: JiraIssueEtagResponse): IssueEtagFields {\n\tconst { status, updated } = issue.fields;\n\tif (status?.statusCategory == null) throw new Error(`Jira bulk fetch returned no status for ${issue.key}`);\n\n\tconst updatedDate = new Date(updated ?? Number.NaN);\n\tif (Number.isNaN(updatedDate.getTime())) {\n\t\tthrow new Error(`Jira bulk fetch returned no update time for ${issue.key}`);\n\t}\n\n\treturn { state: toJiraIssueState(status).category === 'DONE' ? 'closed' : 'opened', updatedDate: updatedDate };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"jiraIssueByKey.js","names":[],"sources":["../../../../../plus/integrations/src/providers/jiraIssueByKey.ts"],"sourcesContent":["import type { IssueEtagFields } from '../models/integration.js';\nimport type { ProviderAccount, ProviderIssue, ProviderRequestFunction } from './models.js';\n\nconst jiraCloudApiUrl = 'https://api.atlassian.com/ex/jira';\nconst jiraIssueByKeyFields = [\n\t'assignee',\n\t'comment',\n\t'created',\n\t'creator',\n\t'description',\n\t'issuetype',\n\t'labels',\n\t'project',\n\t'status',\n\t'summary',\n\t'updated',\n\t'votes',\n];\n\ntype JiraIssueMemberResponse = {\n\taccountId?: string;\n\tdisplayName?: string | null;\n\temailAddress?: string | null;\n\tavatarUrls?: Record<string, string | undefined>;\n};\n\ntype JiraIssueStatusResponse = {\n\tid: string;\n\tname: string;\n\tstatusCategory: {\n\t\tcolorName: string;\n\t\tkey: string;\n\t\tname: string;\n\t};\n};\n\ntype JiraIssueByKeyResponse = {\n\tid: string;\n\tkey: string;\n\tfields: {\n\t\tassignee?: JiraIssueMemberResponse | null;\n\t\tcomment?: { comments?: unknown[]; total?: number };\n\t\tcreated: string;\n\t\tcreator?: JiraIssueMemberResponse | null;\n\t\tdescription?: string | null;\n\t\tissuetype?: { name?: string | null };\n\t\tlabels?: string[];\n\t\tproject?: { id?: string | null; key?: string | null; name?: string | null };\n\t\tstatus: JiraIssueStatusResponse;\n\t\tsummary: string;\n\t\tupdated: string;\n\t\tvotes?: { votes?: number | null };\n\t};\n};\n\nfunction toAccount(member: JiraIssueMemberResponse | null | undefined): ProviderAccount | null {\n\tif (member == null) return null;\n\n\treturn {\n\t\tid: member.accountId ?? '',\n\t\tname: member.displayName ?? null,\n\t\tusername: member.displayName ?? null,\n\t\temail: member.emailAddress ?? null,\n\t\tavatarUrl: member.avatarUrls?.['48x48'] ?? null,\n\t\turl: null,\n\t};\n}\n\nfunction toStatusCategory(key: string): 'TO_DO' | 'IN_PROGRESS' | 'DONE' {\n\tswitch (key.toLowerCase()) {\n\t\tcase 'new':\n\t\t\treturn 'TO_DO';\n\t\tcase 'indeterminate':\n\t\t\treturn 'IN_PROGRESS';\n\t\tcase 'done':\n\t\t\treturn 'DONE';\n\t\tdefault:\n\t\t\treturn 'TO_DO';\n\t}\n}\n\nfunction toJiraIssueState(status: JiraIssueStatusResponse): NonNullable<ProviderIssue['state']> {\n\treturn {\n\t\tid: status.id,\n\t\tname: status.name,\n\t\tcolor: status.statusCategory.colorName,\n\t\tcategory: toStatusCategory(status.statusCategory.key),\n\t};\n}\n\nfunction toIssueWebUrl(resourceUrl: string, key: string): string {\n\tconst url = new URL(resourceUrl);\n\turl.pathname = `${url.pathname.replace(/\\/+$/, '')}/browse/${encodeURIComponent(key)}`;\n\turl.search = '';\n\turl.hash = '';\n\treturn url.toString();\n}\n\nfunction fromJiraIssueByKey(issue: JiraIssueByKeyResponse, resourceId: string, resourceUrl: string): ProviderIssue {\n\tconst assignee = toAccount(issue.fields.assignee);\n\tconst project = issue.fields.project;\n\tconst status = issue.fields.status;\n\n\treturn {\n\t\tid: issue.id,\n\t\tcommentCount: issue.fields.comment?.total ?? issue.fields.comment?.comments?.length ?? null,\n\t\tnumber: issue.key,\n\t\ttitle: issue.fields.summary,\n\t\turl: toIssueWebUrl(resourceUrl, issue.key),\n\t\tclosedDate: null,\n\t\tcreatedDate: new Date(issue.fields.created),\n\t\tauthor: toAccount(issue.fields.creator),\n\t\tupdatedDate: new Date(issue.fields.updated),\n\t\tassignees: assignee != null ? [assignee] : [],\n\t\tdescription: issue.fields.description ?? null,\n\t\trepository: null,\n\t\tproject:\n\t\t\tproject != null\n\t\t\t\t? {\n\t\t\t\t\t\tname: project.name ?? '',\n\t\t\t\t\t\tresourceId: resourceId,\n\t\t\t\t\t\tkey: project.key ?? null,\n\t\t\t\t\t\tnamespace: null,\n\t\t\t\t\t\tid: project.id ?? null,\n\t\t\t\t\t}\n\t\t\t\t: undefined,\n\t\tstate: toJiraIssueState(status),\n\t\ttype: issue.fields.issuetype?.name ?? null,\n\t\tupvoteCount: issue.fields.votes?.votes ?? null,\n\t\tlabels: (issue.fields.labels ?? []).map(label => ({\n\t\t\tcolor: null,\n\t\t\tdescription: null,\n\t\t\tid: null,\n\t\t\tname: label,\n\t\t})),\n\t};\n}\n\n/**\n * The SDK's Jira point read performs resource and field discovery before the issue GET. This focused path uses\n * caller-supplied resource identity so a cold lookup remains one upstream request.\n */\nexport async function requestJiraIssueByKey(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tresourceId: string,\n\tresourceUrl: string,\n\tkey: string,\n): Promise<ProviderIssue> {\n\tconst url = new URL(\n\t\t`${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/2/issue/${encodeURIComponent(key)}`,\n\t);\n\turl.searchParams.set('fields', jiraIssueByKeyFields.join(','));\n\n\tconst response = await request<JiraIssueByKeyResponse>({\n\t\turl: url.toString(),\n\t\theaders: { Authorization: `Bearer ${accessToken}` },\n\t});\n\treturn fromJiraIssueByKey(response.body, resourceId, resourceUrl);\n}\n\n/**\n * The most keys one bulk fetch takes. Atlassian allows up to 1000 for a request that names its fields explicitly, but\n * documents 100 as the default limit.\n */\nexport const jiraBulkFetchMaxKeys = 100;\n\n/**\n * One issue of a bulk fetch that asks for `status` and `updated` only. The bulk fetch is API v3 while the full read is\n * v2, but these two fields have the same shape in both: the v2 and v3 OpenAPI specs define the same `StatusDetails`\n * and `StatusCategory`, and a live comparison found both fields byte-identical. v3 differs in rich-text fields, which\n * it returns as ADF, so this request must never widen to `description`. `self` differs too, and is never read.\n */\nexport type JiraIssueEtagResponse = {\n\tid: string;\n\tkey: string;\n\tfields: Partial<Pick<JiraIssueByKeyResponse['fields'], 'status' | 'updated'>>;\n};\n\ntype JiraBulkFetchResponse = {\n\tissues?: JiraIssueEtagResponse[];\n\t/**\n\t * Only issues Jira could not return for a retriable error or a payload constraint, by numeric issue id rather than\n\t * the key asked for. Issues that don't exist or aren't visible are NOT listed here, nor in `issues`: they are\n\t * silently omitted, so a bulk fetch never proves an absence.\n\t */\n\tissueErrors?: { id?: string; errorMessage?: string }[];\n};\n\n/**\n * The cheap check behind the batch issue read's etags: `status` and `updated` of up to {@link jiraBulkFetchMaxKeys}\n * issues of one site in ONE request, where {@link requestJiraIssueByKey} reads one issue per request. Jira returns the\n * issues in its own order, each under its CURRENT key (a lowercase or moved key resolves to the issue it names now), and\n * omits the ones it can't find, so a caller matches them to its keys by key, never by position.\n */\nexport async function requestJiraIssuesEtagFields(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tresourceId: string,\n\tkeys: readonly string[],\n): Promise<{ issues: JiraIssueEtagResponse[]; errorCount: number }> {\n\tconst response = await request<JiraBulkFetchResponse | null>({\n\t\turl: `${jiraCloudApiUrl}/${encodeURIComponent(resourceId)}/rest/api/3/issue/bulkfetch`,\n\t\tmethod: 'POST',\n\t\theaders: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ issueIdsOrKeys: keys, fields: ['status', 'updated'] }),\n\t});\n\n\tconst issues = response.body?.issues;\n\tif (!Array.isArray(issues)) throw new Error('Jira bulk fetch returned no issues');\n\n\treturn { issues: issues, errorCount: response.body?.issueErrors?.length ?? 0 };\n}\n\n/**\n * A bulk-fetched issue's change state, as the fields the full row's etag reads. {@link fromJiraIssueByKey} maps the\n * status with the same {@link toJiraIssueState} and never sets `closedDate`, so `toIssueShape` closes exactly the\n * issues whose status category is done. Throws on a missing status or an unparseable `updated`, so the target falls\n * through to the full read rather than matching an etag that would ignore its update time.\n */\nexport function toJiraIssueEtagFields(issue: JiraIssueEtagResponse): IssueEtagFields {\n\tconst { status, updated } = issue.fields;\n\tif (status?.statusCategory == null) throw new Error(`Jira bulk fetch returned no status for ${issue.key}`);\n\n\tconst updatedDate = new Date(updated ?? Number.NaN);\n\tif (Number.isNaN(updatedDate.getTime())) {\n\t\tthrow new Error(`Jira bulk fetch returned no update time for ${issue.key}`);\n\t}\n\n\treturn { state: toJiraIssueState(status).category === 'DONE' ? 'closed' : 'opened', updatedDate: updatedDate };\n}\n"],"mappings":"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"}
@@ -25,12 +25,12 @@ export type LinearIssueEtagNode = {
25
25
  export declare function requestLinearIssuesEtagFields(request: ProviderRequestFunction, accessToken: string, teamKey: string, numbers: readonly number[]): Promise<LinearIssueEtagNode[]>;
26
26
  /**
27
27
  * A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'
28
- * Linear issue normalizer (0.61.0) then `fromProviderIssue`, so this mirrors both:
28
+ * Linear issue normalizer (0.65.0) then `fromProviderIssue`, so this mirrors both:
29
29
  *
30
- * - provider-apis maps `state.type` to a category — `completed` and `canceled` to `DONE`, `started` to `IN_PROGRESS`,
31
- * anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none — and sets `closedDate` from
32
- * `archivedAt` (a falsy value reads as none);
33
- * - `fromProviderIssue` closes an issue that has a `closedDate` or a `DONE` category, so an archived issue is closed
30
+ * - provider-apis maps `state.type` to a category — `completed` to `DONE`, `canceled` and `duplicate` to `CANCELED`,
31
+ * `started` to `IN_PROGRESS`, anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none —
32
+ * and sets `closedDate` from `archivedAt` (a falsy value reads as none);
33
+ * - `fromProviderIssue` closes an issue that has a `closedDate` or a terminal category, so an archived issue is closed
34
34
  * whatever its state.
35
35
  *
36
36
  * Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an
@@ -67,12 +67,12 @@ async function requestLinearIssuesEtagFields(request, accessToken, teamKey, numb
67
67
  }
68
68
  /**
69
69
  * A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'
70
- * Linear issue normalizer (0.61.0) then `fromProviderIssue`, so this mirrors both:
70
+ * Linear issue normalizer (0.65.0) then `fromProviderIssue`, so this mirrors both:
71
71
  *
72
- * - provider-apis maps `state.type` to a category — `completed` and `canceled` to `DONE`, `started` to `IN_PROGRESS`,
73
- * anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none — and sets `closedDate` from
74
- * `archivedAt` (a falsy value reads as none);
75
- * - `fromProviderIssue` closes an issue that has a `closedDate` or a `DONE` category, so an archived issue is closed
72
+ * - provider-apis maps `state.type` to a category — `completed` to `DONE`, `canceled` and `duplicate` to `CANCELED`,
73
+ * `started` to `IN_PROGRESS`, anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none —
74
+ * and sets `closedDate` from `archivedAt` (a falsy value reads as none);
75
+ * - `fromProviderIssue` closes an issue that has a `closedDate` or a terminal category, so an archived issue is closed
76
76
  * whatever its state.
77
77
  *
78
78
  * Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an
@@ -84,7 +84,7 @@ function toLinearIssueEtagFields(issue) {
84
84
  if (Number.isNaN(updatedDate.getTime())) throw new Error(`Linear returned no update time for ${issue.identifier}`);
85
85
  const type = issue.state?.type;
86
86
  return {
87
- state: Boolean(issue.archivedAt) || type === "completed" || type === "canceled" ? "closed" : "opened",
87
+ state: Boolean(issue.archivedAt) || type === "completed" || type === "canceled" || type === "duplicate" ? "closed" : "opened",
88
88
  updatedDate
89
89
  };
90
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"linearIssuesEtag.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linearIssuesEtag.ts"],"sourcesContent":["import type { IssueEtagFields } from '../models/integration.js';\nimport type { ProviderRequestFunction } from './models.js';\n\nconst linearGraphQLUrl = 'https://api.linear.app/graphql';\n\n/** The most issue numbers one cheap check asks of one team. */\nexport const linearIssuesEtagMaxNumbers = 50;\n\n/**\n * Archived issues are left out of Linear's `issues` connection unless asked for, so `includeArchived` keeps an archived\n * issue answerable here as the full read's `issue(id:)` answers it.\n */\nconst linearIssuesEtagQuery = `\n\tquery GetIssuesEtagFields($teamKey: String!, $numbers: [Float!]!) {\n\t\tissues(\n\t\t\tfirst: ${linearIssuesEtagMaxNumbers}\n\t\t\tincludeArchived: true\n\t\t\tfilter: { team: { key: { eq: $teamKey } }, number: { in: $numbers } }\n\t\t) {\n\t\t\tpageInfo {\n\t\t\t\thasNextPage\n\t\t\t}\n\t\t\tnodes {\n\t\t\t\tidentifier\n\t\t\t\tnumber\n\t\t\t\tupdatedAt\n\t\t\t\tarchivedAt\n\t\t\t\tstate {\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n`;\n\nexport type LinearIssueEtagNode = {\n\tidentifier: string;\n\tnumber: number;\n\tupdatedAt?: string | null;\n\tarchivedAt?: string | null;\n\tstate?: { type?: string | null } | null;\n};\n\ntype LinearIssuesEtagResponse = {\n\tdata?: {\n\t\tissues?: { pageInfo?: { hasNextPage?: boolean }; nodes?: LinearIssueEtagNode[] } | null;\n\t} | null;\n\terrors?: unknown[];\n};\n\n/**\n * The cheap check behind the batch issue read's etags: the change state of up to {@link linearIssuesEtagMaxNumbers}\n * issues of one team in ONE GraphQL request, where the full read sends provider-apis' `issue(id:)` once per issue.\n * Linear returns only the issues it found, in its own order, so a caller matches them by number, never by position.\n *\n * Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.\n * GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling\n * code is still classified as one. A next page also rejects it: with every number unique within a team, more\n * issues than were asked for means the filter was not applied as written.\n */\nexport async function requestLinearIssuesEtagFields(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tteamKey: string,\n\tnumbers: readonly number[],\n): Promise<LinearIssueEtagNode[]> {\n\tconst response = await request<LinearIssuesEtagResponse | null>({\n\t\turl: linearGraphQLUrl,\n\t\tmethod: 'POST',\n\t\theaders: { Authorization: accessToken, 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ query: linearIssuesEtagQuery, variables: { teamKey: teamKey, numbers: numbers } }),\n\t});\n\n\tconst body = response.body;\n\tif (body?.errors != null) {\n\t\tconst error = new Error(`Linear GraphQL errors: ${JSON.stringify(body.errors)}`);\n\t\tObject.assign(error, { response: response });\n\t\tthrow error;\n\t}\n\n\tconst issues = body?.data?.issues;\n\tif (!Array.isArray(issues?.nodes)) throw new Error('Linear returned no issues');\n\tif (issues.pageInfo?.hasNextPage !== false) throw new Error('Linear returned more issues than were asked for');\n\n\treturn issues.nodes;\n}\n\n/**\n * A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'\n * Linear issue normalizer (0.61.0) then `fromProviderIssue`, so this mirrors both:\n *\n * - provider-apis maps `state.type` to a category — `completed` and `canceled` to `DONE`, `started` to `IN_PROGRESS`,\n * anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none — and sets `closedDate` from\n * `archivedAt` (a falsy value reads as none);\n * - `fromProviderIssue` closes an issue that has a `closedDate` or a `DONE` category, so an archived issue is closed\n * whatever its state.\n *\n * Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an\n * etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then\n * `createdDate`).\n */\nexport function toLinearIssueEtagFields(issue: LinearIssueEtagNode): IssueEtagFields {\n\tconst updatedDate = new Date(issue.updatedAt || Number.NaN);\n\tif (Number.isNaN(updatedDate.getTime())) {\n\t\tthrow new Error(`Linear returned no update time for ${issue.identifier}`);\n\t}\n\n\tconst type = issue.state?.type;\n\tconst closed = Boolean(issue.archivedAt) || type === 'completed' || type === 'canceled';\n\treturn { state: closed ? 'closed' : 'opened', updatedDate: updatedDate };\n}\n"],"mappings":";AAGA,MAAM,mBAAmB;;AAGzB,MAAa,6BAA6B;;;;;AAM1C,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgD9B,eAAsB,8BACrB,SACA,aACA,SACA,SACiC;CACjC,MAAM,WAAW,MAAM,QAAyC;EAC/D,KAAK;EACL,QAAQ;EACR,SAAS;GAAE,eAAe;GAAa,gBAAgB;EAAmB;EAC1E,MAAM,KAAK,UAAU;GAAE,OAAO;GAAuB,WAAW;IAAW;IAAkB;GAAQ;EAAE,CAAC;CACzG,CAAC;CAED,MAAM,OAAO,SAAS;CACtB,IAAI,MAAM,UAAU,MAAM;EACzB,MAAM,wBAAQ,IAAI,MAAM,0BAA0B,KAAK,UAAU,KAAK,MAAM,GAAG;EAC/E,OAAO,OAAO,OAAO,EAAY,SAAS,CAAC;EAC3C,MAAM;CACP;CAEA,MAAM,SAAS,MAAM,MAAM;CAC3B,IAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,GAAG,MAAM,IAAI,MAAM,2BAA2B;CAC9E,IAAI,OAAO,UAAU,gBAAgB,OAAO,MAAM,IAAI,MAAM,iDAAiD;CAE7G,OAAO,OAAO;AACf;;;;;;;;;;;;;;;AAgBA,SAAgB,wBAAwB,OAA6C;CACpF,MAAM,cAAc,IAAI,KAAK,MAAM,aAAa,GAAU;CAC1D,IAAI,OAAO,MAAM,YAAY,QAAQ,CAAC,GACrC,MAAM,IAAI,MAAM,sCAAsC,MAAM,YAAY;CAGzE,MAAM,OAAO,MAAM,OAAO;CAE1B,OAAO;EAAE,OADM,QAAQ,MAAM,UAAU,KAAK,SAAS,eAAe,SAAS,aACpD,WAAW;EAAuB;CAAY;AACxE"}
1
+ {"version":3,"file":"linearIssuesEtag.js","names":[],"sources":["../../../../../plus/integrations/src/providers/linearIssuesEtag.ts"],"sourcesContent":["import type { IssueEtagFields } from '../models/integration.js';\nimport type { ProviderRequestFunction } from './models.js';\n\nconst linearGraphQLUrl = 'https://api.linear.app/graphql';\n\n/** The most issue numbers one cheap check asks of one team. */\nexport const linearIssuesEtagMaxNumbers = 50;\n\n/**\n * Archived issues are left out of Linear's `issues` connection unless asked for, so `includeArchived` keeps an archived\n * issue answerable here as the full read's `issue(id:)` answers it.\n */\nconst linearIssuesEtagQuery = `\n\tquery GetIssuesEtagFields($teamKey: String!, $numbers: [Float!]!) {\n\t\tissues(\n\t\t\tfirst: ${linearIssuesEtagMaxNumbers}\n\t\t\tincludeArchived: true\n\t\t\tfilter: { team: { key: { eq: $teamKey } }, number: { in: $numbers } }\n\t\t) {\n\t\t\tpageInfo {\n\t\t\t\thasNextPage\n\t\t\t}\n\t\t\tnodes {\n\t\t\t\tidentifier\n\t\t\t\tnumber\n\t\t\t\tupdatedAt\n\t\t\t\tarchivedAt\n\t\t\t\tstate {\n\t\t\t\t\ttype\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n`;\n\nexport type LinearIssueEtagNode = {\n\tidentifier: string;\n\tnumber: number;\n\tupdatedAt?: string | null;\n\tarchivedAt?: string | null;\n\tstate?: { type?: string | null } | null;\n};\n\ntype LinearIssuesEtagResponse = {\n\tdata?: {\n\t\tissues?: { pageInfo?: { hasNextPage?: boolean }; nodes?: LinearIssueEtagNode[] } | null;\n\t} | null;\n\terrors?: unknown[];\n};\n\n/**\n * The cheap check behind the batch issue read's etags: the change state of up to {@link linearIssuesEtagMaxNumbers}\n * issues of one team in ONE GraphQL request, where the full read sends provider-apis' `issue(id:)` once per issue.\n * Linear returns only the issues it found, in its own order, so a caller matches them by number, never by position.\n *\n * Sent as provider-apis sends every Linear request: the token as the whole `Authorization` header, with no scheme.\n * GraphQL `errors` reject the whole request, as provider-apis rejects them, but keep the response so a throttling\n * code is still classified as one. A next page also rejects it: with every number unique within a team, more\n * issues than were asked for means the filter was not applied as written.\n */\nexport async function requestLinearIssuesEtagFields(\n\trequest: ProviderRequestFunction,\n\taccessToken: string,\n\tteamKey: string,\n\tnumbers: readonly number[],\n): Promise<LinearIssueEtagNode[]> {\n\tconst response = await request<LinearIssuesEtagResponse | null>({\n\t\turl: linearGraphQLUrl,\n\t\tmethod: 'POST',\n\t\theaders: { Authorization: accessToken, 'Content-Type': 'application/json' },\n\t\tbody: JSON.stringify({ query: linearIssuesEtagQuery, variables: { teamKey: teamKey, numbers: numbers } }),\n\t});\n\n\tconst body = response.body;\n\tif (body?.errors != null) {\n\t\tconst error = new Error(`Linear GraphQL errors: ${JSON.stringify(body.errors)}`);\n\t\tObject.assign(error, { response: response });\n\t\tthrow error;\n\t}\n\n\tconst issues = body?.data?.issues;\n\tif (!Array.isArray(issues?.nodes)) throw new Error('Linear returned no issues');\n\tif (issues.pageInfo?.hasNextPage !== false) throw new Error('Linear returned more issues than were asked for');\n\n\treturn issues.nodes;\n}\n\n/**\n * A cheap-checked issue's change state, as the fields the full row's etag reads. The full read's row is provider-apis'\n * Linear issue normalizer (0.65.0) then `fromProviderIssue`, so this mirrors both:\n *\n * - provider-apis maps `state.type` to a category — `completed` to `DONE`, `canceled` and `duplicate` to `CANCELED`,\n * `started` to `IN_PROGRESS`, anything else (`backlog`, `unstarted`, `triage`) to `TO_DO`, and no state to none —\n * and sets `closedDate` from `archivedAt` (a falsy value reads as none);\n * - `fromProviderIssue` closes an issue that has a `closedDate` or a terminal category, so an archived issue is closed\n * whatever its state.\n *\n * Throws on a missing or unparseable `updatedAt`, so the target falls through to the full read rather than matching an\n * etag the full row would compute from a fallback date (`fromProviderIssue` falls back to `closedDate`, then\n * `createdDate`).\n */\nexport function toLinearIssueEtagFields(issue: LinearIssueEtagNode): IssueEtagFields {\n\tconst updatedDate = new Date(issue.updatedAt || Number.NaN);\n\tif (Number.isNaN(updatedDate.getTime())) {\n\t\tthrow new Error(`Linear returned no update time for ${issue.identifier}`);\n\t}\n\n\tconst type = issue.state?.type;\n\tconst closed = Boolean(issue.archivedAt) || type === 'completed' || type === 'canceled' || type === 'duplicate';\n\treturn { state: closed ? 'closed' : 'opened', updatedDate: updatedDate };\n}\n"],"mappings":";AAGA,MAAM,mBAAmB;;AAGzB,MAAa,6BAA6B;;;;;AAM1C,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgD9B,eAAsB,8BACrB,SACA,aACA,SACA,SACiC;CACjC,MAAM,WAAW,MAAM,QAAyC;EAC/D,KAAK;EACL,QAAQ;EACR,SAAS;GAAE,eAAe;GAAa,gBAAgB;EAAmB;EAC1E,MAAM,KAAK,UAAU;GAAE,OAAO;GAAuB,WAAW;IAAW;IAAkB;GAAQ;EAAE,CAAC;CACzG,CAAC;CAED,MAAM,OAAO,SAAS;CACtB,IAAI,MAAM,UAAU,MAAM;EACzB,MAAM,wBAAQ,IAAI,MAAM,0BAA0B,KAAK,UAAU,KAAK,MAAM,GAAG;EAC/E,OAAO,OAAO,OAAO,EAAY,SAAS,CAAC;EAC3C,MAAM;CACP;CAEA,MAAM,SAAS,MAAM,MAAM;CAC3B,IAAI,CAAC,MAAM,QAAQ,QAAQ,KAAK,GAAG,MAAM,IAAI,MAAM,2BAA2B;CAC9E,IAAI,OAAO,UAAU,gBAAgB,OAAO,MAAM,IAAI,MAAM,iDAAiD;CAE7G,OAAO,OAAO;AACf;;;;;;;;;;;;;;;AAgBA,SAAgB,wBAAwB,OAA6C;CACpF,MAAM,cAAc,IAAI,KAAK,MAAM,aAAa,GAAU;CAC1D,IAAI,OAAO,MAAM,YAAY,QAAQ,CAAC,GACrC,MAAM,IAAI,MAAM,sCAAsC,MAAM,YAAY;CAGzE,MAAM,OAAO,MAAM,OAAO;CAE1B,OAAO;EAAE,OADM,QAAQ,MAAM,UAAU,KAAK,SAAS,eAAe,SAAS,cAAc,SAAS,cAC3E,WAAW;EAAuB;CAAY;AACxE"}
@@ -520,6 +520,17 @@ export type GetBitbucketServerPullRequestsForCurrentUserFn = (input: {
520
520
  };
521
521
  data: GitPullRequest[];
522
522
  }>;
523
+ /**
524
+ * One page of a Jira issue search; `nextCursor` is the raw provider cursor, fed back verbatim. `metadata` is the
525
+ * SDK's: a page that dropped an issue it could not read reports it there as `partial` with a `provider` failure
526
+ * (GKDEV-3652).
527
+ */
528
+ export type JiraIssuePage = {
529
+ data: ProviderIssue[];
530
+ hasMore: boolean;
531
+ nextCursor: string | undefined;
532
+ metadata?: CollectionMetadata;
533
+ } | undefined;
523
534
  export type GetIssuesForProjectFn = Jira['getIssuesForProject'];
524
535
  export type GetJiraServerCurrentUserFn = JiraServer['getCurrentUser'];
525
536
  export type GetJiraServerProjectsFn = (options?: EnterpriseOptions) => Promise<{
@@ -710,7 +721,6 @@ export type ProvidersMetadata = Record<IntegrationIds, ProviderMetadata>;
710
721
  export declare const providersMetadata: ProvidersMetadata;
711
722
  export declare function getReasonsForUserIssue(issue: ProviderIssue, userLogin: string): string[];
712
723
  export declare function toIssueShape(issue: ProviderIssue, provider: ProviderReference, options?: {
713
- reliableStateCategory?: boolean;
714
724
  projection?: IssueProjection;
715
725
  }): IssueShape | undefined;
716
726
  /**
@@ -2,9 +2,11 @@ import { equalsIgnoreCase } from "../../../utils/string.js";
2
2
  import { gitSuffixRegex } from "../../../git/utils/remote.utils.js";
3
3
  import { Issue } from "../../../git/models/issue.js";
4
4
  import { PullRequest, PullRequestFilter } from "../../../git/models/pullRequest.js";
5
+ import { isTerminalIssueStateCategory } from "../../../git/utils/issue.utils.js";
5
6
  import "../constants.js";
6
7
  import { githubSearchResultLimit } from "../../git-github/api/config.js";
7
8
  import { IssueFilter } from "../providerFilters.js";
9
+ import { isJiraIntegrationId } from "../utils/integration.utils.js";
8
10
  import { fromProviderAccount, toProviderAccount } from "./accounts.js";
9
11
  import { azurePullRequestSearchSorts, azureWorkItemSearchRelationships, azureWorkItemSearchResultLimit, azureWorkItemSearchSorts } from "./azure/search.js";
10
12
  import { azureAccountWideIssueSorts, azureIssueSorts, githubAccountWideIssueSorts, githubIssueSorts, gitlabAccountWideIssueSorts, gitlabIssueSorts, jiraIssueSorts, linearIssueSorts, trelloIssueSorts } from "./issueSorts.js";
@@ -574,19 +576,17 @@ function getReasonsForUserIssue(issue, userLogin) {
574
576
  function toIssueIdentifier(value) {
575
577
  return String(value);
576
578
  }
577
- function toIssueProviderState(state, reliableStateCategory = true) {
579
+ function toIssueProviderState(state) {
578
580
  return state == null ? void 0 : {
579
581
  id: state.id,
580
582
  name: state.name,
581
583
  color: state.color ?? void 0,
582
- category: reliableStateCategory ? state.category : void 0
584
+ category: state.category
583
585
  };
584
586
  }
585
587
  function toIssueShape(issue, provider, options) {
586
588
  if (issue.updatedDate == null || issue.url == null) return void 0;
587
- const isJira = provider.id === "jira" || provider.id === "jira-server";
588
- const reliableStateCategory = !isJira || options?.reliableStateCategory === true;
589
- const closed = issue.closedDate != null || issue.state?.category === "DONE" && reliableStateCategory;
589
+ const closed = issue.closedDate != null || isTerminalIssueStateCategory(issue.state?.category);
590
590
  return {
591
591
  type: "issue",
592
592
  provider,
@@ -599,7 +599,7 @@ function toIssueShape(issue, provider, options) {
599
599
  closedDate: issue.closedDate ?? void 0,
600
600
  closed,
601
601
  state: closed ? "closed" : "opened",
602
- providerState: toIssueProviderState(issue.state, reliableStateCategory),
602
+ providerState: toIssueProviderState(issue.state),
603
603
  author: {
604
604
  id: issue.author?.id ?? "",
605
605
  name: issue.author?.name ?? void 0,
@@ -630,7 +630,7 @@ function toIssueShape(issue, provider, options) {
630
630
  commentsCount: issue.commentCount ?? void 0,
631
631
  thumbsUpCount: issue.upvoteCount ?? void 0,
632
632
  body: issue.description ?? void 0,
633
- bodyFormat: isJira ? "jira-wiki" : void 0,
633
+ bodyFormat: isJiraIntegrationId(provider.id) ? "jira-wiki" : void 0,
634
634
  issueType: issue.type ?? void 0,
635
635
  iterations: toIssueIterations(issue),
636
636
  ...options?.projection != null ? { projection: options.projection } : {}
@@ -956,7 +956,7 @@ function authoredByCurrentAccount(author, provider, currentAccount) {
956
956
  }
957
957
  function fromProviderIssue(issue, integration, options) {
958
958
  const identifier = toIssueIdentifier(issue.number);
959
- const closed = issue.closedDate != null || issue.state?.category === "DONE";
959
+ const closed = issue.closedDate != null || isTerminalIssueStateCategory(issue.state?.category);
960
960
  return new Issue(integration, identifier, issue.graphQLId ?? issue.id, issue.title, issue.url ?? "", issue.createdDate, issue.updatedDate ?? issue.closedDate ?? issue.createdDate, closed, closed ? "closed" : "opened", fromProviderAccount(issue.author), issue.assignees?.map(fromProviderAccount) ?? void 0, issue.repository != null ? {
961
961
  owner: issue.repository.owner.login ?? "",
962
962
  repo: issue.repository.name,
@@ -974,7 +974,7 @@ function fromProviderIssue(issue, integration, options) {
974
974
  name: issue.project.name,
975
975
  resourceId: issue.project.resourceId,
976
976
  resourceName: issue.project.namespace
977
- } : void 0, identifier, issue.type ?? void 0, toIssueProviderState(issue.state, integration.id !== "jira"), integration.id === "jira" ? "jira-wiki" : void 0, toIssueIterations(issue), options?.projection);
977
+ } : void 0, identifier, issue.type ?? void 0, toIssueProviderState(issue.state), integration.id === "jira" ? "jira-wiki" : void 0, toIssueIterations(issue), options?.projection);
978
978
  }
979
979
  function toProviderPullRequestWithUniqueId(pr) {
980
980
  const { reviews, mergeableState, ...providerPr } = toProviderPullRequest(pr);