PageRenderTime 54ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 1ms

/Atlassian.Jira/Issue.cs

https://bitbucket.org/farmas/atlassian.net-sdk
C# | 1280 lines | 772 code | 157 blank | 351 comment | 91 complexity | 50221f1e27da994eca54301654a284d7 MD5 | raw file
Possible License(s): BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. using Atlassian.Jira.Linq;
  2. using Atlassian.Jira.Remote;
  3. using Newtonsoft.Json.Linq;
  4. using System;
  5. using System.Collections.Generic;
  6. using System.Collections.ObjectModel;
  7. using System.IO;
  8. using System.Linq;
  9. using System.Reflection;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. namespace Atlassian.Jira
  13. {
  14. /// <summary>
  15. /// A JIRA issue
  16. /// </summary>
  17. public class Issue : IRemoteIssueFieldProvider
  18. {
  19. private readonly Jira _jira;
  20. private ComparableString _key;
  21. private string _project;
  22. private RemoteIssue _originalIssue;
  23. private DateTime? _createDate;
  24. private DateTime? _updateDate;
  25. private DateTime? _dueDate;
  26. private DateTime? _resolutionDate;
  27. private IssueSecurityLevel _securityLevel;
  28. private ProjectVersionCollection _affectsVersions = null;
  29. private ProjectVersionCollection _fixVersions = null;
  30. private ProjectComponentCollection _components = null;
  31. private CustomFieldValueCollection _customFields = null;
  32. private IssueLabelCollection _labels = null;
  33. private IssueStatus _status;
  34. private string _parentIssueKey;
  35. private IssueType _issueType;
  36. /// <summary>
  37. /// Create a new Issue.
  38. /// </summary>
  39. /// <param name="jira">Jira instance that owns this issue.</param>
  40. /// <param name="fields">Fields to be included in the payload when creating the issue.</param>
  41. public Issue(Jira jira, CreateIssueFields fields)
  42. : this(jira, new RemoteIssue() { project = fields.ProjectKey, timeTracking = fields.TimeTrackingData }, fields.ParentIssueKey)
  43. {
  44. }
  45. /// <summary>
  46. /// Creates a new Issue.
  47. /// </summary>
  48. /// <param name="jira">Jira instance that owns this issue.</param>
  49. /// <param name="projectKey">Project key that owns this issue.</param>
  50. /// <param name="parentIssueKey">If provided, marks this issue as a subtask of the given parent issue.</param>
  51. public Issue(Jira jira, string projectKey, string parentIssueKey = null)
  52. : this(jira, new RemoteIssue() { project = projectKey }, parentIssueKey)
  53. {
  54. }
  55. /// <summary>
  56. /// Creates a new Issue from a remote issue.
  57. /// </summary>
  58. /// <param name="jira">The Jira instance that owns this issue.</param>
  59. /// <param name="remoteIssue">The remote issue object.</param>
  60. /// <param name="parentIssueKey">If provided, marks this issue as a subtask of the given parent issue.</param>
  61. public Issue(Jira jira, RemoteIssue remoteIssue, string parentIssueKey = null)
  62. {
  63. _jira = jira;
  64. _parentIssueKey = parentIssueKey;
  65. Initialize(remoteIssue);
  66. }
  67. private void Initialize(RemoteIssue remoteIssue)
  68. {
  69. _originalIssue = remoteIssue;
  70. _project = remoteIssue.project;
  71. _key = remoteIssue.key;
  72. _createDate = remoteIssue.created;
  73. _dueDate = remoteIssue.duedate;
  74. _updateDate = remoteIssue.updated;
  75. _resolutionDate = remoteIssue.resolutionDateReadOnly;
  76. _securityLevel = remoteIssue.securityLevelReadOnly;
  77. TimeTrackingData = remoteIssue.timeTracking;
  78. Assignee = remoteIssue.assignee;
  79. AssigneeUser = remoteIssue.assigneeJiraUser;
  80. Description = remoteIssue.description;
  81. Environment = remoteIssue.environment;
  82. Reporter = remoteIssue.reporter;
  83. ReporterUser = remoteIssue.reporterJiraUser;
  84. Summary = remoteIssue.summary;
  85. Votes = remoteIssue.votesData?.votes;
  86. HasUserVoted = remoteIssue.votesData != null ? remoteIssue.votesData.hasVoted : false;
  87. if (!String.IsNullOrEmpty(remoteIssue.parentKey))
  88. {
  89. _parentIssueKey = remoteIssue.parentKey;
  90. }
  91. // named entities
  92. _status = remoteIssue.status == null ? null : new IssueStatus(remoteIssue.status);
  93. Priority = remoteIssue.priority == null ? null : new IssuePriority(remoteIssue.priority);
  94. Resolution = remoteIssue.resolution == null ? null : new IssueResolution(remoteIssue.resolution);
  95. Type = remoteIssue.type == null ? null : new IssueType(remoteIssue.type);
  96. // collections
  97. _customFields = _originalIssue.customFieldValues == null ? new CustomFieldValueCollection(this)
  98. : new CustomFieldValueCollection(this, _originalIssue.customFieldValues.Select(f => new CustomFieldValue(f.customfieldId, this) { Values = f.values, RawValue = f.rawValue }).ToList());
  99. var affectsVersions = _originalIssue.affectsVersions ?? Enumerable.Empty<RemoteVersion>();
  100. _affectsVersions = new ProjectVersionCollection("versions", _jira, Project, affectsVersions.Select(v =>
  101. {
  102. v.ProjectKey = _originalIssue.project;
  103. return new ProjectVersion(_jira, v);
  104. }).ToList());
  105. var fixVersions = _originalIssue.fixVersions ?? Enumerable.Empty<RemoteVersion>();
  106. _fixVersions = new ProjectVersionCollection("fixVersions", _jira, Project, fixVersions.Select(v =>
  107. {
  108. v.ProjectKey = _originalIssue.project;
  109. return new ProjectVersion(_jira, v);
  110. }).ToList());
  111. var labels = _originalIssue.labels ?? new string[0];
  112. _labels = new IssueLabelCollection(labels.ToList());
  113. var components = _originalIssue.components ?? Enumerable.Empty<RemoteComponent>();
  114. _components = new ProjectComponentCollection("components", _jira, Project, components.Select(c =>
  115. {
  116. c.ProjectKey = _originalIssue.project;
  117. return new ProjectComponent(c);
  118. }).ToList());
  119. // additional fields
  120. this.AdditionalFields = new IssueFields(_originalIssue, Jira);
  121. }
  122. internal RemoteIssue OriginalRemoteIssue
  123. {
  124. get
  125. {
  126. return this._originalIssue;
  127. }
  128. }
  129. /// <summary>
  130. /// Fields not represented as properties that were retrieved from the server.
  131. /// </summary>
  132. public IssueFields AdditionalFields { get; private set; }
  133. /// <summary>
  134. /// The parent key if this issue is a subtask.
  135. /// </summary>
  136. /// <remarks>
  137. /// Only available if issue was retrieved using REST API.
  138. /// </remarks>
  139. public string ParentIssueKey
  140. {
  141. get { return _parentIssueKey; }
  142. }
  143. /// <summary>
  144. /// The JIRA server that created this issue
  145. /// </summary>
  146. public Jira Jira
  147. {
  148. get
  149. {
  150. return _jira;
  151. }
  152. }
  153. /// <summary>
  154. /// Gets the security level set on the issue.
  155. /// </summary>
  156. public IssueSecurityLevel SecurityLevel
  157. {
  158. get
  159. {
  160. return _securityLevel;
  161. }
  162. }
  163. /// <summary>
  164. /// Brief one-line summary of the issue
  165. /// </summary>
  166. [JqlContainsEquality]
  167. public string Summary { get; set; }
  168. /// <summary>
  169. /// Detailed description of the issue
  170. /// </summary>
  171. [JqlContainsEquality]
  172. public string Description { get; set; }
  173. /// <summary>
  174. /// Hardware or software environment to which the issue relates.
  175. /// </summary>
  176. [JqlContainsEquality]
  177. public string Environment { get; set; }
  178. /// <summary>
  179. /// Username or account id of user to whom the issue is currently assigned.
  180. /// </summary>
  181. public string Assignee { get; set; }
  182. /// <summary>
  183. /// User object of user to whom the issue is currently assigned.
  184. /// </summary>
  185. public JiraUser AssigneeUser { get; private set; }
  186. /// <summary>
  187. /// Time tracking data for this issue.
  188. /// </summary>
  189. public IssueTimeTrackingData TimeTrackingData { get; private set; }
  190. /// <summary>
  191. /// Gets the internal identifier assigned by JIRA.
  192. /// </summary>
  193. public string JiraIdentifier
  194. {
  195. get
  196. {
  197. if (String.IsNullOrEmpty(this._originalIssue.key))
  198. {
  199. throw new InvalidOperationException("Unable to retrieve JIRA id, issue has not been created.");
  200. }
  201. return this._originalIssue.id;
  202. }
  203. }
  204. /// <summary>
  205. /// Unique identifier for this issue
  206. /// </summary>
  207. public ComparableString Key
  208. {
  209. get
  210. {
  211. return _key;
  212. }
  213. }
  214. /// <summary>
  215. /// Importance of the issue in relation to other issues
  216. /// </summary>
  217. public IssuePriority Priority { get; set; }
  218. /// <summary>
  219. /// Parent project to which the issue belongs
  220. /// </summary>
  221. public string Project
  222. {
  223. get
  224. {
  225. return _project;
  226. }
  227. }
  228. /// <summary>
  229. /// Username or account id of user who created the issue in Jira.
  230. /// </summary>
  231. public string Reporter { get; set; }
  232. /// <summary>
  233. /// User object of user who created the issue in Jira.
  234. /// </summary>
  235. public JiraUser ReporterUser { get; private set; }
  236. /// <summary>
  237. /// Record of the issue's resolution, if the issue has been resolved or closed
  238. /// </summary>
  239. public IssueResolution Resolution { get; set; }
  240. /// <summary>
  241. /// The stage the issue is currently at in its lifecycle.
  242. /// </summary>
  243. public IssueStatus Status
  244. {
  245. get
  246. {
  247. return _status;
  248. }
  249. }
  250. /// <summary>
  251. /// The type of the issue
  252. /// </summary>
  253. [RemoteFieldName("issuetype")]
  254. public IssueType Type
  255. {
  256. get
  257. {
  258. return _issueType;
  259. }
  260. set
  261. {
  262. _issueType = value;
  263. if (_issueType != null)
  264. {
  265. _issueType.ProjectKey = _project;
  266. }
  267. }
  268. }
  269. /// <summary>
  270. /// Number of votes the issue has
  271. /// </summary>
  272. public long? Votes { get; private set; }
  273. /// <summary>
  274. /// Whether the user that retrieved this issue has voted on it.
  275. /// </summary>
  276. public bool HasUserVoted { get; private set; }
  277. /// <summary>
  278. /// Time and date on which this issue was entered into JIRA
  279. /// </summary>
  280. public DateTime? Created
  281. {
  282. get
  283. {
  284. return _createDate;
  285. }
  286. }
  287. /// <summary>
  288. /// Date by which this issue is scheduled to be completed
  289. /// </summary>
  290. public DateTime? DueDate
  291. {
  292. get
  293. {
  294. return _dueDate;
  295. }
  296. set
  297. {
  298. _dueDate = value;
  299. }
  300. }
  301. /// <summary>
  302. /// Time and date on which this issue was last edited
  303. /// </summary>
  304. public DateTime? Updated
  305. {
  306. get
  307. {
  308. return _updateDate;
  309. }
  310. }
  311. /// <summary>
  312. /// Time and date on which this issue was resolved.
  313. /// </summary>
  314. /// <remarks>
  315. /// Only available if issue was retrieved using REST API, use GetResolutionDate
  316. /// method for SOAP clients.
  317. /// </remarks>
  318. public DateTime? ResolutionDate
  319. {
  320. get
  321. {
  322. return _resolutionDate;
  323. }
  324. }
  325. /// <summary>
  326. /// The components associated with this issue
  327. /// </summary>
  328. [JqlFieldName("component")]
  329. public ProjectComponentCollection Components
  330. {
  331. get
  332. {
  333. return _components;
  334. }
  335. }
  336. /// <summary>
  337. /// The versions that are affected by this issue
  338. /// </summary>
  339. [JqlFieldName("AffectedVersion")]
  340. public ProjectVersionCollection AffectsVersions
  341. {
  342. get
  343. {
  344. return _affectsVersions;
  345. }
  346. }
  347. /// <summary>
  348. /// The versions in which this issue is fixed
  349. /// </summary>
  350. [JqlFieldName("FixVersion")]
  351. public ProjectVersionCollection FixVersions
  352. {
  353. get
  354. {
  355. return _fixVersions;
  356. }
  357. }
  358. /// <summary>
  359. /// The labels assigned to this issue.
  360. /// </summary>
  361. public IssueLabelCollection Labels
  362. {
  363. get
  364. {
  365. return _labels;
  366. }
  367. }
  368. /// <summary>
  369. /// The custom fields associated with this issue
  370. /// </summary>
  371. public CustomFieldValueCollection CustomFields
  372. {
  373. get
  374. {
  375. return _customFields;
  376. }
  377. }
  378. /// <summary>
  379. /// Gets or sets the value of a custom field
  380. /// </summary>
  381. /// <param name="customFieldName">Custom field name</param>
  382. /// <returns>Value of the custom field</returns>
  383. public ComparableString this[string customFieldName]
  384. {
  385. get
  386. {
  387. var customField = _customFields[customFieldName];
  388. if (customField != null && customField.Values != null && customField.Values.Count() > 0)
  389. {
  390. return customField.Values[0];
  391. }
  392. return null;
  393. }
  394. set
  395. {
  396. var customField = _customFields[customFieldName];
  397. string[] customFieldValue = value == null ? null : new string[] { value.Value };
  398. if (customField != null)
  399. {
  400. customField.Values = customFieldValue;
  401. }
  402. else
  403. {
  404. _customFields.Add(customFieldName, customFieldValue);
  405. }
  406. }
  407. }
  408. /// <summary>
  409. /// Saves field changes to server.
  410. /// </summary>
  411. public void SaveChanges()
  412. {
  413. Issue serverIssue = null;
  414. if (String.IsNullOrEmpty(_originalIssue.key))
  415. {
  416. var newKey = _jira.Issues.CreateIssueAsync(this).Result;
  417. serverIssue = _jira.Issues.GetIssueAsync(newKey).Result;
  418. }
  419. else
  420. {
  421. _jira.Issues.UpdateIssueAsync(this).Wait();
  422. serverIssue = _jira.Issues.GetIssueAsync(_originalIssue.key).Result;
  423. }
  424. Initialize(serverIssue.OriginalRemoteIssue);
  425. }
  426. /// <summary>
  427. /// Saves field changes to server.
  428. /// </summary>
  429. /// <param name="token">Cancellation token for this operation.</param>
  430. public async Task<Issue> SaveChangesAsync(CancellationToken token = default(CancellationToken))
  431. {
  432. Issue serverIssue;
  433. if (String.IsNullOrEmpty(_originalIssue.key))
  434. {
  435. var newKey = await _jira.Issues.CreateIssueAsync(this, token).ConfigureAwait(false);
  436. serverIssue = await _jira.Issues.GetIssueAsync(newKey, token).ConfigureAwait(false);
  437. }
  438. else
  439. {
  440. await _jira.Issues.UpdateIssueAsync(this, token).ConfigureAwait(false);
  441. serverIssue = await _jira.Issues.GetIssueAsync(_originalIssue.key, token).ConfigureAwait(false);
  442. }
  443. Initialize(serverIssue.OriginalRemoteIssue);
  444. return serverIssue;
  445. }
  446. /// <summary>
  447. /// Creates a link between this issue and the issue specified.
  448. /// </summary>
  449. /// <param name="inwardIssueKey">Key of the issue to link.</param>
  450. /// <param name="linkName">Name of the issue link type.</param>
  451. /// <param name="comment">Comment to add to this issue.</param>
  452. /// <param name="token">Cancellation token for this operation.</param>
  453. public Task LinkToIssueAsync(string inwardIssueKey, string linkName, string comment = null, CancellationToken token = default(CancellationToken))
  454. {
  455. if (String.IsNullOrEmpty(_originalIssue.key))
  456. {
  457. throw new InvalidOperationException("Unable to link issue, issue has not been created.");
  458. }
  459. return this.Jira.Links.CreateLinkAsync(this.Key.Value, inwardIssueKey, linkName, comment, token);
  460. }
  461. /// <summary>
  462. /// Gets the issue links associated with this issue.
  463. /// </summary>
  464. /// <param name="token">Cancellation token for this operation.</param>
  465. public Task<IEnumerable<IssueLink>> GetIssueLinksAsync(CancellationToken token = default(CancellationToken))
  466. {
  467. if (String.IsNullOrEmpty(_originalIssue.key))
  468. {
  469. throw new InvalidOperationException("Unable to get issue links issues, issue has not been created.");
  470. }
  471. return this.Jira.Links.GetLinksForIssueAsync(this, null, token);
  472. }
  473. /// <summary>
  474. /// Gets the issue links associated with this issue.
  475. /// </summary>
  476. /// <param name="linkTypeNames">Optional subset of link types to retrieve.</param>
  477. /// <param name="token">Cancellation token for this operation.</param>
  478. public Task<IEnumerable<IssueLink>> GetIssueLinksAsync(IEnumerable<string> linkTypeNames, CancellationToken token = default(CancellationToken))
  479. {
  480. if (String.IsNullOrEmpty(_originalIssue.key))
  481. {
  482. throw new InvalidOperationException("Unable to get issue links issues, issue has not been created.");
  483. }
  484. return this.Jira.Links.GetLinksForIssueAsync(this, linkTypeNames, token);
  485. }
  486. /// <summary>
  487. /// Creates an remote link for an issue.
  488. /// </summary>
  489. /// <param name="remoteUrl">Remote url to link to.</param>
  490. /// <param name="title">Title of the remote link.</param>
  491. /// <param name="summary">Summary of the remote link.</param>
  492. public Task AddRemoteLinkAsync(string remoteUrl, string title, string summary = null)
  493. {
  494. if (String.IsNullOrEmpty(_originalIssue.key))
  495. {
  496. throw new InvalidOperationException("Unable to add remote link, issue has not been created.");
  497. }
  498. return this.Jira.RemoteLinks.CreateRemoteLinkAsync(this.Key.Value, remoteUrl, title, summary);
  499. }
  500. /// <summary>
  501. /// Gets the remote links associated with this issue.
  502. /// </summary>
  503. /// <param name="token">Cancellation token for this operation.</param>
  504. public Task<IEnumerable<IssueRemoteLink>> GetRemoteLinksAsync(CancellationToken token = default(CancellationToken))
  505. {
  506. if (String.IsNullOrEmpty(_originalIssue.key))
  507. {
  508. throw new InvalidOperationException("Unable to get remote links, issue has not been created.");
  509. }
  510. return this.Jira.RemoteLinks.GetRemoteLinksForIssueAsync(_originalIssue.key, token);
  511. }
  512. /// <summary>
  513. /// Transition an issue through a workflow action.
  514. /// </summary>
  515. /// <param name="actionNameOrId">The workflow action name or id to transition to.</param>
  516. /// <param name="additionalUpdates">Additional updates to perform when transitioning the issue.</param>
  517. /// <param name="token">Cancellation token for this operation.</param>
  518. public async Task WorkflowTransitionAsync(string actionNameOrId, WorkflowTransitionUpdates additionalUpdates = null, CancellationToken token = default(CancellationToken))
  519. {
  520. if (String.IsNullOrEmpty(_originalIssue.key))
  521. {
  522. throw new InvalidOperationException("Unable to execute workflow transition, issue has not been created.");
  523. }
  524. await _jira.Issues.ExecuteWorkflowActionAsync(this, actionNameOrId, additionalUpdates, token).ConfigureAwait(false);
  525. var issue = await _jira.Issues.GetIssueAsync(_originalIssue.key, token).ConfigureAwait(false);
  526. Initialize(issue.OriginalRemoteIssue);
  527. }
  528. /// <summary>
  529. /// Returns the issues that are marked as sub tasks of this issue.
  530. /// </summary>
  531. /// <param name="maxIssues">Maximum number of issues to retrieve.</param>
  532. /// <param name="startAt">Index of the first issue to return (0-based).</param>
  533. /// <param name="token">Cancellation token for this operation.</param>
  534. public Task<IPagedQueryResult<Issue>> GetSubTasksAsync(int? maxIssues = null, int startAt = 0, CancellationToken token = default(CancellationToken))
  535. {
  536. if (String.IsNullOrEmpty(_originalIssue.key))
  537. {
  538. throw new InvalidOperationException("Unable to retrieve subtasks from server, issue has not been created.");
  539. }
  540. return _jira.Issues.GetSubTasksAsync(_originalIssue.key, maxIssues, startAt, token);
  541. }
  542. /// <summary>
  543. /// Retrieve attachment metadata from server for this issue
  544. /// </summary>
  545. public Task<IEnumerable<Attachment>> GetAttachmentsAsync(CancellationToken token = default(CancellationToken))
  546. {
  547. if (String.IsNullOrEmpty(_originalIssue.key))
  548. {
  549. throw new InvalidOperationException("Unable to retrieve attachments from server, issue has not been created.");
  550. }
  551. return this.Jira.Issues.GetAttachmentsAsync(this.Key.Value, token);
  552. }
  553. /// <summary>
  554. /// Add one or more attachments to this issue
  555. /// </summary>
  556. /// <param name="filePaths">Full paths of files to upload</param>
  557. public void AddAttachment(params string[] filePaths)
  558. {
  559. var attachments = filePaths.Select(f => new UploadAttachmentInfo(Path.GetFileName(f), _jira.FileSystem.FileReadAllBytes(f))).ToArray();
  560. AddAttachment(attachments);
  561. }
  562. /// <summary>
  563. /// Add an attachment to this issue
  564. /// </summary>
  565. /// <param name="name">Attachment name with extension</param>
  566. /// <param name="data">Attachment data</param>
  567. public void AddAttachment(string name, byte[] data)
  568. {
  569. AddAttachment(new UploadAttachmentInfo(name, data));
  570. }
  571. /// <summary>
  572. /// Add one or more attachments to this issue.
  573. /// </summary>
  574. /// <param name="attachments">Attachment objects that describe the files to upload.</param>
  575. public void AddAttachment(params UploadAttachmentInfo[] attachments)
  576. {
  577. if (String.IsNullOrEmpty(_originalIssue.key))
  578. {
  579. throw new InvalidOperationException("Unable to upload attachments to server, issue has not been created.");
  580. }
  581. AddAttachmentAsync(attachments).Wait();
  582. }
  583. /// <summary>
  584. /// Add one or more attachments to this issue.
  585. /// </summary>
  586. /// <param name="attachments">Attachment objects that describe the files to upload.</param>
  587. /// <param name="token">Cancellation token for this operation.</param>
  588. public Task AddAttachmentAsync(UploadAttachmentInfo[] attachments, CancellationToken token = default(CancellationToken))
  589. {
  590. if (String.IsNullOrEmpty(_originalIssue.key))
  591. {
  592. throw new InvalidOperationException("Unable to upload attachments to server, issue has not been created.");
  593. }
  594. return _jira.Issues.AddAttachmentsAsync(_originalIssue.key, attachments, token);
  595. }
  596. /// <summary>
  597. /// Removes an attachment from this issue.
  598. /// </summary>
  599. /// <param name="attachment">Attachment to remove.</param>
  600. /// <param name="token">Cancellation token for this operation.</param>
  601. public Task DeleteAttachmentAsync(Attachment attachment, CancellationToken token = default(CancellationToken))
  602. {
  603. if (String.IsNullOrEmpty(_originalIssue.key))
  604. {
  605. throw new InvalidOperationException("Unable to delete attachment from server, issue has not been created.");
  606. }
  607. return _jira.Issues.DeleteAttachmentAsync(_originalIssue.key, attachment.Id, token);
  608. }
  609. /// <summary>
  610. /// Gets a dictionary with issue field names as keys and their metadata as values.
  611. /// </summary>
  612. public Task<IDictionary<String, IssueFieldEditMetadata>> GetIssueFieldsEditMetadataAsync(CancellationToken token = default(CancellationToken))
  613. {
  614. if (String.IsNullOrEmpty(_originalIssue.key))
  615. {
  616. throw new InvalidOperationException("Unable to retrieve issue fields from server, make sure the issue has been created.");
  617. }
  618. return _jira.Issues.GetFieldsEditMetadataAsync(_originalIssue.key, token);
  619. }
  620. /// <summary>
  621. /// Retrieve change logs from server for this issue.
  622. /// </summary>
  623. /// <param name="token">Cancellation token for this operation.</param>
  624. public Task<IEnumerable<IssueChangeLog>> GetChangeLogsAsync(CancellationToken token = default(CancellationToken))
  625. {
  626. if (String.IsNullOrEmpty(_originalIssue.key))
  627. {
  628. throw new InvalidOperationException("Unable to retrieve change logs from server, issue has not been created.");
  629. }
  630. return _jira.Issues.GetChangeLogsAsync(_originalIssue.key, token);
  631. }
  632. /// <summary>
  633. /// Get the comments for this issue.
  634. /// </summary>
  635. /// <param name="token">Cancellation token for this operation.</param>
  636. public Task<IEnumerable<Comment>> GetCommentsAsync(CancellationToken token = default(CancellationToken))
  637. {
  638. if (String.IsNullOrEmpty(_originalIssue.key))
  639. {
  640. throw new InvalidOperationException("Unable to retrieve comments from server, issue has not been created.");
  641. }
  642. return _jira.Issues.GetCommentsAsync(_originalIssue.key, token);
  643. }
  644. /// <summary>
  645. /// Get the comments for this issue.
  646. /// </summary>
  647. /// <param name="options">Options to use when querying the comments.</param>
  648. /// <param name="token">Cancellation token for this operation.</param>
  649. public Task<IEnumerable<Comment>> GetCommentsAsync(CommentQueryOptions options, CancellationToken token = default(CancellationToken))
  650. {
  651. if (String.IsNullOrEmpty(_originalIssue.key))
  652. {
  653. throw new InvalidOperationException("Unable to retrieve comments from server, issue has not been created.");
  654. }
  655. return _jira.Issues.GetCommentsAsync(_originalIssue.key, options, token);
  656. }
  657. /// <summary>
  658. /// Get the comments for this issue.
  659. /// </summary>
  660. /// <param name="maxComments">Maximum number of comments to retrieve.</param>
  661. /// <param name="startAt">Index of the first comment to return (0-based).</param>
  662. /// <param name="token">Cancellation token for this operation.</param>
  663. public Task<IPagedQueryResult<Comment>> GetPagedCommentsAsync(int? maxComments = null, int startAt = 0, CancellationToken token = default(CancellationToken))
  664. {
  665. if (String.IsNullOrEmpty(_originalIssue.key))
  666. {
  667. throw new InvalidOperationException("Unable to retrieve comments from server, issue has not been created.");
  668. }
  669. return this.Jira.Issues.GetPagedCommentsAsync(this.Key.Value, maxComments, startAt, token);
  670. }
  671. /// <summary>
  672. /// Add a comment to this issue.
  673. /// </summary>
  674. /// <param name="comment">Comment text to add.</param>
  675. /// <param name="token">Cancellation token for this operation.</param>
  676. public async Task<Comment> AddCommentAsync(string comment, CancellationToken token = default(CancellationToken))
  677. {
  678. var jiraUser = await this.Jira.Users.GetMyselfAsync(token);
  679. var author = this.Jira.RestClient.Settings.EnableUserPrivacyMode ? jiraUser.AccountId : jiraUser.Username;
  680. return await this.AddCommentAsync(new Comment() { Author = author, Body = comment }, token);
  681. }
  682. /// <summary>
  683. /// Removes a comment from this issue.
  684. /// </summary>
  685. /// <param name="comment">Comment to remove.</param>
  686. /// <param name="token">Cancellation token for this operation.</param>
  687. public Task DeleteCommentAsync(Comment comment, CancellationToken token = default(CancellationToken))
  688. {
  689. if (String.IsNullOrEmpty(_originalIssue.key))
  690. {
  691. throw new InvalidOperationException("Unable to delete comment from server, issue has not been created.");
  692. }
  693. return _jira.Issues.DeleteCommentAsync(_originalIssue.key, comment.Id, token);
  694. }
  695. /// <summary>
  696. /// Add a comment to this issue.
  697. /// </summary>
  698. /// <param name="comment">Comment object to add.</param>
  699. /// <param name="token">Cancellation token for this operation.</param>
  700. public Task<Comment> AddCommentAsync(Comment comment, CancellationToken token = default(CancellationToken))
  701. {
  702. if (String.IsNullOrEmpty(_originalIssue.key))
  703. {
  704. throw new InvalidOperationException("Unable to add comment to issue, issue has not been created.");
  705. }
  706. return this.Jira.Issues.AddCommentAsync(this.Key.Value, comment, token);
  707. }
  708. /// <summary>
  709. /// Update a comment in this issue.
  710. /// </summary>
  711. /// <param name="comment">Comment object to update.</param>
  712. /// <param name="token">Cancellation token for this operation.</param>
  713. public Task<Comment> UpdateCommentAsync(Comment comment, CancellationToken token = default(CancellationToken))
  714. {
  715. if (String.IsNullOrEmpty(_originalIssue.key))
  716. {
  717. throw new InvalidOperationException("Unable to update comment to issue, issue has not been created.");
  718. }
  719. return this.Jira.Issues.UpdateCommentAsync(this.Key.Value, comment, token);
  720. }
  721. /// <summary>
  722. /// Retrieve the labels from server for this issue.
  723. /// </summary>
  724. /// <param name="token">Cancellation token for this operation.</param>
  725. [Obsolete("Use Issue.Labels instead.")]
  726. public Task<string[]> GetLabelsAsync(CancellationToken token = default(CancellationToken))
  727. {
  728. if (String.IsNullOrEmpty(_originalIssue.key))
  729. {
  730. throw new InvalidOperationException("Unable to get labels from issue, issue has not been created.");
  731. }
  732. return Jira.Issues.GetLabelsAsync(_originalIssue.key, token);
  733. }
  734. /// <summary>
  735. /// Sets the labels of this issue.
  736. /// </summary>
  737. /// <param name="labels">The list of labels to set on the issue</param>
  738. [Obsolete("Modify the Issue.Labels collection and call Issue.SaveChanges to update the labels field.")]
  739. public Task SetLabelsAsync(params string[] labels)
  740. {
  741. return SetLabelsAsync(labels, CancellationToken.None);
  742. }
  743. /// <summary>
  744. /// Sets the labels of this issue.
  745. /// </summary>
  746. /// <param name="labels">The list of labels to set on the issue</param>
  747. /// <param name="token">Cancellation token for this operation.</param>
  748. [Obsolete("Modify the Issue.Labels collection and call Issue.SaveChanges to update the labels field.")]
  749. public Task SetLabelsAsync(string[] labels, CancellationToken token = default(CancellationToken))
  750. {
  751. if (String.IsNullOrEmpty(_originalIssue.key))
  752. {
  753. throw new InvalidOperationException("Unable to add label to issue, issue has not been created.");
  754. }
  755. return _jira.Issues.SetLabelsAsync(_originalIssue.key, labels, token);
  756. }
  757. /// <summary>
  758. /// Adds a worklog to this issue.
  759. /// </summary>
  760. /// <param name="timespent">Specifies a time duration in JIRA duration format, representing the time spent working on the worklog</param>
  761. /// <param name="worklogStrategy">How to handle the remaining estimate, defaults to AutoAdjustRemainingEstimate</param>
  762. /// <param name="newEstimate">New estimate (only used if worklogStrategy set to NewRemainingEstimate)</param>
  763. /// <returns>Worklog as constructed by server</returns>
  764. public Task<Worklog> AddWorklogAsync(string timespent,
  765. WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate,
  766. string newEstimate = null,
  767. CancellationToken token = default(CancellationToken))
  768. {
  769. // todo: Use the CancellationToken Parameter
  770. return AddWorklogAsync(new Worklog(timespent, DateTime.Now), worklogStrategy, newEstimate);
  771. }
  772. /// <summary>
  773. /// Adds a worklog to this issue.
  774. /// </summary>
  775. /// <param name="worklog">The worklog instance to add</param>
  776. /// <param name="worklogStrategy">How to handle the remaining estimate, defaults to AutoAdjustRemainingEstimate</param>
  777. /// <param name="newEstimate">New estimate (only used if worklogStrategy set to NewRemainingEstimate)</param>
  778. /// <param name="token">Cancellation token for this operation.</param>
  779. /// <returns>Worklog as constructed by server</returns>
  780. public Task<Worklog> AddWorklogAsync(Worklog worklog,
  781. WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate,
  782. string newEstimate = null,
  783. CancellationToken token = default(CancellationToken))
  784. {
  785. if (String.IsNullOrEmpty(_originalIssue.key))
  786. {
  787. throw new InvalidOperationException("Unable to add worklog to issue, issue has not been saved to server.");
  788. }
  789. return _jira.Issues.AddWorklogAsync(_originalIssue.key, worklog, worklogStrategy, newEstimate, token);
  790. }
  791. /// <summary>
  792. /// Deletes the given worklog from the issue and updates the remaining estimate field.
  793. /// </summary>
  794. /// <param name="worklog">The worklog to remove.</param>
  795. /// <param name="worklogStrategy">How to handle the remaining estimate, defaults to AutoAdjustRemainingEstimate.</param>
  796. /// <param name="newEstimate">New estimate (only used if worklogStrategy set to NewRemainingEstimate)</param>
  797. /// <param name="token">Cancellation token for this operation.</param>
  798. public Task DeleteWorklogAsync(Worklog worklog, WorklogStrategy worklogStrategy = WorklogStrategy.AutoAdjustRemainingEstimate, string newEstimate = null, CancellationToken token = default(CancellationToken))
  799. {
  800. if (String.IsNullOrEmpty(_originalIssue.key))
  801. {
  802. throw new InvalidOperationException("Unable to delete worklog from issue, issue has not been saved to server.");
  803. }
  804. return _jira.Issues.DeleteWorklogAsync(_originalIssue.key, worklog.Id, worklogStrategy, newEstimate, token);
  805. }
  806. /// <summary>
  807. /// Retrieve worklogs for this issue.
  808. /// </summary>
  809. /// <param name="token">Cancellation token for this operation.</param>
  810. public Task<IEnumerable<Worklog>> GetWorklogsAsync(CancellationToken token = default(CancellationToken))
  811. {
  812. if (String.IsNullOrEmpty(_originalIssue.key))
  813. {
  814. throw new InvalidOperationException("Unable to retrieve worklogs, issue has not been saved to server.");
  815. }
  816. return _jira.Issues.GetWorklogsAsync(_originalIssue.key, token);
  817. }
  818. /// <summary>
  819. /// Updates all fields from server.
  820. /// </summary>
  821. public void Refresh()
  822. {
  823. this.RefreshAsync().Wait();
  824. }
  825. /// <summary>
  826. /// Updates all fields from server.
  827. /// </summary>
  828. /// <param name="token">Cancellation token for this operation.</param>
  829. public async Task RefreshAsync(CancellationToken token = default(CancellationToken))
  830. {
  831. if (String.IsNullOrEmpty(_originalIssue.key))
  832. {
  833. throw new InvalidOperationException("Unable to refresh, issue has not been saved to server.");
  834. }
  835. // todo: Use the CancellationToken Parameter
  836. var serverIssue = await _jira.Issues.GetIssueAsync(_originalIssue.key).ConfigureAwait(false);
  837. Initialize(serverIssue.OriginalRemoteIssue);
  838. }
  839. /// <summary>
  840. /// Gets the workflow actions that the issue can be transitioned to.
  841. /// </summary>
  842. /// <param name="token">Cancellation token for this operation.</param>
  843. public Task<IEnumerable<IssueTransition>> GetAvailableActionsAsync(CancellationToken token = default(CancellationToken))
  844. {
  845. if (String.IsNullOrEmpty(_originalIssue.key))
  846. {
  847. throw new InvalidOperationException("Unable to retrieve actions, issue has not been saved to server.");
  848. }
  849. return this._jira.Issues.GetActionsAsync(_originalIssue.key, token);
  850. }
  851. /// <summary>
  852. /// Gets the workflow actions that the issue can be transitioned to including the fields that are required per action.
  853. /// </summary>
  854. /// <param name="token">Cancellation token for this operation.</param>
  855. public Task<IEnumerable<IssueTransition>> GetAvailableActionsAsync(bool expandTransitionFields, CancellationToken token = default(CancellationToken))
  856. {
  857. if (String.IsNullOrEmpty(_originalIssue.key))
  858. {
  859. throw new InvalidOperationException("Unable to retrieve actions, issue has not been saved to server.");
  860. }
  861. return this._jira.Issues.GetActionsAsync(_originalIssue.key, expandTransitionFields, token);
  862. }
  863. /// <summary>
  864. /// Gets time tracking information for this issue.
  865. /// </summary>
  866. /// <remarks>
  867. /// - Returns information as it was at the moment the issue was read from server, to get latest data use the GetTimeTrackingData method.
  868. /// - Use the AddWorklog methods to edit the time tracking information.
  869. /// </remarks>
  870. /// <param name="token">Cancellation token for this operation.</param>
  871. public Task<IssueTimeTrackingData> GetTimeTrackingDataAsync(CancellationToken token = default(CancellationToken))
  872. {
  873. if (String.IsNullOrEmpty(_originalIssue.key))
  874. {
  875. throw new InvalidOperationException("Unable to retrieve time tracking data, issue has not been saved to server.");
  876. }
  877. return _jira.Issues.GetTimeTrackingDataAsync(_originalIssue.key, token);
  878. }
  879. /// <summary>
  880. /// Adds a user to the watchers of the issue.
  881. /// </summary>
  882. /// <param name="usernameOrAccountId">Username or account id of the user to add as a watcher.</param>
  883. /// <param name="token">Cancellation token for this operation.</param>
  884. public Task AddWatcherAsync(string usernameOrAccountId, CancellationToken token = default(CancellationToken))
  885. {
  886. if (String.IsNullOrEmpty(_originalIssue.key))
  887. {
  888. throw new InvalidOperationException("Unable to add watcher, issue has not been saved to server.");
  889. }
  890. return _jira.Issues.AddWatcherAsync(_originalIssue.key, usernameOrAccountId, token);
  891. }
  892. /// <summary>
  893. /// Gets the users that are watching the issue.
  894. /// </summary>
  895. /// <param name="token">Cancellation token for this operation.</param>
  896. public Task<IEnumerable<JiraUser>> GetWatchersAsync(CancellationToken token = default(CancellationToken))
  897. {
  898. if (String.IsNullOrEmpty(_originalIssue.key))
  899. {
  900. throw new InvalidOperationException("Unable to get watchers, issue has not been saved to server.");
  901. }
  902. return _jira.Issues.GetWatchersAsync(_originalIssue.key, token);
  903. }
  904. /// <summary>
  905. /// Removes a user from the watchers of the issue.
  906. /// </summary>
  907. /// <param name="usernameOrAccountId">Username or account id of the user to remove as a watcher.</param>
  908. /// <param name="token">Cancellation token for this operation.</param>
  909. public Task DeleteWatcherAsync(string usernameOrAccountId, CancellationToken token = default(CancellationToken))
  910. {
  911. if (String.IsNullOrEmpty(_originalIssue.key))
  912. {
  913. throw new InvalidOperationException("Unable to remove watcher, issue has not been saved to server.");
  914. }
  915. return _jira.Issues.DeleteWatcherAsync(_originalIssue.key, usernameOrAccountId, token);
  916. }
  917. /// <summary>
  918. /// Assigns this issue to the specified user.
  919. /// </summary>
  920. /// <param name="assignee">The username or account id of the user to assign this issue to.</param>
  921. /// <param name="token">Cancellation token for this operation.</param>
  922. public async Task AssignAsync(string assignee, CancellationToken token = default(CancellationToken))
  923. {
  924. if (String.IsNullOrEmpty(_originalIssue.key))
  925. {
  926. throw new InvalidOperationException("Unable to assign issue, issue has not been saved to server.");
  927. }
  928. await _jira.Issues.AssignIssueAsync(Key.Value, assignee, token);
  929. var issue = await _jira.Issues.GetIssueAsync(_originalIssue.key, token).ConfigureAwait(false);
  930. Initialize(issue.OriginalRemoteIssue);
  931. }
  932. /// <summary>
  933. /// Fetches the requested properties and their mapping.
  934. /// </summary>
  935. /// <remarks>
  936. /// Property keys yielded by <paramref name="propertyKeys"/> must have a length between 0 and 256 (both exclusive).
  937. /// </remarks>
  938. /// <param name="propertyKeys">Enumeration of requested property keys.</param>
  939. /// <param name="token">Asynchronous operation control token.</param>
  940. /// <returns>A dictionary of property values mapped to their keys.</returns>
  941. public Task<ReadOnlyDictionary<string, JToken>> GetPropertiesAsync(IEnumerable<string> propertyKeys, CancellationToken token = default)
  942. {
  943. if (String.IsNullOrEmpty(_originalIssue.key))
  944. {
  945. throw new InvalidOperationException("Unable to fetch issue properties, issue has not been saved to server.");
  946. }
  947. return _jira.Issues.GetPropertiesAsync(_originalIssue.key, propertyKeys, token);
  948. }
  949. /// <summary>
  950. /// Sets the value of the specified property key. The key is created if it didn't already exist.
  951. /// </summary>
  952. /// <remarks>
  953. /// The property key (<paramref name="propertyKey"/>) must have a length between 0 and 256 (both exclusive).
  954. /// </remarks>
  955. /// <param name="propertyKey">The property key.</param>
  956. /// <param name="obj">The value to store.</param>
  957. /// <param name="token">Asynchronous operation control token.</param>
  958. public Task SetPropertyAsync(string propertyKey, JToken obj, CancellationToken token = default)
  959. {
  960. if (String.IsNullOrEmpty(_originalIssue.key))
  961. {
  962. throw new InvalidOperationException("Unable to add issue properties, issue has not been saved to server.");
  963. }
  964. return _jira.Issues.SetPropertyAsync(_originalIssue.key, propertyKey, obj, token);
  965. }
  966. /// <summary>
  967. /// Remove the specified property key and its stored value.
  968. /// </summary>
  969. /// <remarks>
  970. /// The property key (<paramref name="propertyKey"/>) must have a length between 0 and 256 (both exclusive).
  971. /// </remarks>
  972. /// <param name="propertyKey">The property key.</param>
  973. /// <param name="token">Asynchronous operation control token.</param>
  974. public Task DeletePropertyAsync(string propertyKey, CancellationToken token = default)
  975. {
  976. if (String.IsNullOrEmpty(_originalIssue.key))
  977. {
  978. throw new InvalidOperationException("Unable to remove issue properties, issue has not been saved to server.");
  979. }
  980. return _jira.Issues.DeletePropertyAsync(_originalIssue.key, propertyKey, token);
  981. }
  982. /// <summary>
  983. /// Gets the RemoteFields representing the fields that were updated
  984. /// </summary>
  985. /// <param name="token">Cancellation token for this operation.</param>
  986. async Task<RemoteFieldValue[]> IRemoteIssueFieldProvider.GetRemoteFieldValuesAsync(CancellationToken token)
  987. {
  988. var fields = new List<RemoteFieldValue>();
  989. var remoteFields = typeof(RemoteIssue).GetProperties();
  990. foreach (var localProperty in typeof(Issue).GetProperties())
  991. {
  992. if (typeof(IRemoteIssueFieldProvider).IsAssignableFrom(localProperty.PropertyType))
  993. {
  994. var fieldsProvider = localProperty.GetValue(this, null) as IRemoteIssueFieldProvider;
  995. if (fieldsProvider != null)
  996. {
  997. var remoteFieldValues = await fieldsProvider.GetRemoteFieldValuesAsync(token).ConfigureAwait(false);
  998. fields.AddRange(remoteFieldValues);
  999. }
  1000. }
  1001. else
  1002. {
  1003. var remoteProperty = remoteFields.FirstOrDefault(i => i.Name.Equals(localProperty.Name, StringComparison.OrdinalIgnoreCase));
  1004. if (remoteProperty == null)
  1005. {
  1006. continue;
  1007. }
  1008. var localStringValue = await GetStringValueForPropertyAsync(this, localProperty, token).ConfigureAwait(false);
  1009. var remoteStringValue = await GetStringValueForPropertyAsync(_originalIssue, remoteProperty, token).ConfigureAwait(false);
  1010. if (remoteStringValue != localStringValue)
  1011. {
  1012. var remoteFieldName = remoteProperty.Name;
  1013. var remoteFieldNameAttr = localProperty.GetCustomAttributes(typeof(RemoteFieldNameAttribute), true).OfType<RemoteFieldNameAttribute>().FirstOrDefault();
  1014. if (remoteFieldNameAttr != null)
  1015. {
  1016. remoteFieldName = remoteFieldNameAttr.Name;
  1017. }
  1018. fields.Add(new RemoteFieldValue()
  1019. {
  1020. id = remoteFieldName,
  1021. values = new string[1] { localStringValue }
  1022. });
  1023. }
  1024. }
  1025. }
  1026. return fields.ToArray();
  1027. }
  1028. internal async Task<RemoteIssue> ToRemoteAsync(CancellationToken token)
  1029. {
  1030. var remote = new RemoteIssue()
  1031. {
  1032. assignee = this.Assignee,
  1033. description = this.Description,
  1034. environment = this.Environment,
  1035. project = this.Project,
  1036. reporter = this.Reporter,
  1037. summary = this.Summary,
  1038. votesData = this.Votes != null ? new RemoteVotes() { hasVoted = this.HasUserVoted == true, votes = this.Votes.Value } : null,
  1039. duedate

Large files files are truncated, but you can click here to view the full file