notification-element.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. /*
  2. * decaffeinate suggestions:
  3. * DS101: Remove unnecessary use of Array.from
  4. * DS102: Remove unnecessary code created because of implicit returns
  5. * DS103: Rewrite code to no longer use __guard__, or convert again using --optional-chaining
  6. * DS206: Consider reworking classes to avoid initClass
  7. * DS207: Consider shorter variations of null checks
  8. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
  9. */
  10. let NotificationElement;
  11. const fs = require('fs-plus');
  12. const path = require('path');
  13. const {shell} = require('electron');
  14. const NotificationIssue = require('./notification-issue');
  15. const TemplateHelper = require('./template-helper');
  16. const UserUtilities = require('./user-utilities');
  17. const NotificationTemplate = `\
  18. <div class="content">
  19. <div class="message item"></div>
  20. <div class="detail item">
  21. <div class="detail-content"></div>
  22. <a href="#" class="stack-toggle"></a>
  23. <div class="stack-container"></div>
  24. </div>
  25. <div class="meta item"></div>
  26. </div>
  27. <div class="close icon icon-x"></div>
  28. <div class="close-all btn btn-error">Close All</div>\
  29. `;
  30. const FatalMetaNotificationTemplate = `\
  31. <div class="description fatal-notification"></div>
  32. <div class="btn-toolbar">
  33. <a href="#" class="btn-issue btn btn-error"></a>
  34. <a href="#" class="btn-copy-report icon icon-clippy" title="Copy error report to clipboard"></a>
  35. </div>\
  36. `;
  37. const MetaNotificationTemplate = `\
  38. <div class="description"></div>\
  39. `;
  40. const ButtonListTemplate = `\
  41. <div class="btn-toolbar"></div>\
  42. `;
  43. const ButtonTemplate = `\
  44. <a href="#" class="btn"></a>\
  45. `;
  46. module.exports =
  47. (NotificationElement = (function() {
  48. NotificationElement = class NotificationElement {
  49. static initClass() {
  50. this.prototype.animationDuration = 360;
  51. this.prototype.visibilityDuration = 5000;
  52. this.prototype.autohideTimeout = null;
  53. }
  54. constructor(model, visibilityDuration) {
  55. this.model = model;
  56. this.visibilityDuration = visibilityDuration;
  57. this.fatalTemplate = TemplateHelper.create(FatalMetaNotificationTemplate);
  58. this.metaTemplate = TemplateHelper.create(MetaNotificationTemplate);
  59. this.buttonListTemplate = TemplateHelper.create(ButtonListTemplate);
  60. this.buttonTemplate = TemplateHelper.create(ButtonTemplate);
  61. this.element = document.createElement('atom-notification');
  62. if (this.model.getType() === 'fatal') { this.issue = new NotificationIssue(this.model); }
  63. this.renderPromise = this.render().catch(function(e) {
  64. console.error(e.message);
  65. return console.error(e.stack);
  66. });
  67. this.model.onDidDismiss(() => this.removeNotification());
  68. if (!this.model.isDismissable()) {
  69. this.autohide();
  70. this.element.addEventListener('click', this.makeDismissable.bind(this), {once: true});
  71. }
  72. this.element.issue = this.issue;
  73. this.element.getRenderPromise = this.getRenderPromise.bind(this);
  74. }
  75. getModel() { return this.model; }
  76. getRenderPromise() { return this.renderPromise; }
  77. render() {
  78. let detail, metaContainer, metaContent;
  79. this.element.classList.add(`${this.model.getType()}`);
  80. this.element.classList.add("icon", `icon-${this.model.getIcon()}`, "native-key-bindings");
  81. if (detail = this.model.getDetail()) { this.element.classList.add('has-detail'); }
  82. if (this.model.isDismissable()) { this.element.classList.add('has-close'); }
  83. if (detail && (this.model.getOptions().stack != null)) { this.element.classList.add('has-stack'); }
  84. this.element.setAttribute('tabindex', '-1');
  85. this.element.innerHTML = NotificationTemplate;
  86. const options = this.model.getOptions();
  87. const notificationContainer = this.element.querySelector('.message');
  88. notificationContainer.innerHTML = atom.ui.markdown.render(this.model.getMessage());
  89. if (detail = this.model.getDetail()) {
  90. let stack;
  91. addSplitLinesToContainer(this.element.querySelector('.detail-content'), detail);
  92. if (stack = options.stack) {
  93. const stackToggle = this.element.querySelector('.stack-toggle');
  94. const stackContainer = this.element.querySelector('.stack-container');
  95. addSplitLinesToContainer(stackContainer, stack);
  96. stackToggle.addEventListener('click', e => this.handleStackTraceToggleClick(e, stackContainer));
  97. this.handleStackTraceToggleClick({currentTarget: stackToggle}, stackContainer);
  98. }
  99. }
  100. if (metaContent = options.description) {
  101. this.element.classList.add('has-description');
  102. metaContainer = this.element.querySelector('.meta');
  103. metaContainer.appendChild(TemplateHelper.render(this.metaTemplate));
  104. const description = this.element.querySelector('.description');
  105. description.innerHTML = atom.ui.markdown.render(metaContent);
  106. }
  107. if (options.buttons && (options.buttons.length > 0)) {
  108. this.element.classList.add('has-buttons');
  109. metaContainer = this.element.querySelector('.meta');
  110. metaContainer.appendChild(TemplateHelper.render(this.buttonListTemplate));
  111. const toolbar = this.element.querySelector('.btn-toolbar');
  112. let buttonClass = this.model.getType();
  113. if (buttonClass === 'fatal') { buttonClass = 'error'; }
  114. buttonClass = `btn-${buttonClass}`;
  115. options.buttons.forEach(button => {
  116. toolbar.appendChild(TemplateHelper.render(this.buttonTemplate));
  117. const buttonEl = toolbar.childNodes[toolbar.childNodes.length - 1];
  118. buttonEl.textContent = button.text;
  119. buttonEl.classList.add(buttonClass);
  120. if (button.className != null) {
  121. buttonEl.classList.add.apply(buttonEl.classList, button.className.split(' '));
  122. }
  123. if (button.onDidClick != null) {
  124. return buttonEl.addEventListener('click', e => {
  125. return button.onDidClick.call(this, e);
  126. });
  127. }
  128. });
  129. }
  130. const closeButton = this.element.querySelector('.close');
  131. closeButton.addEventListener('click', () => this.handleRemoveNotificationClick());
  132. const closeAllButton = this.element.querySelector('.close-all');
  133. closeAllButton.classList.add(this.getButtonClass());
  134. closeAllButton.addEventListener('click', () => this.handleRemoveAllNotificationsClick());
  135. if (this.model.getType() === 'fatal') {
  136. return this.renderFatalError();
  137. } else {
  138. return Promise.resolve();
  139. }
  140. }
  141. renderFatalError() {
  142. const repoUrl = this.issue.getRepoUrl();
  143. const packageName = this.issue.getPackageName();
  144. const fatalContainer = this.element.querySelector('.meta');
  145. fatalContainer.appendChild(TemplateHelper.render(this.fatalTemplate));
  146. const fatalNotification = this.element.querySelector('.fatal-notification');
  147. const issueButton = fatalContainer.querySelector('.btn-issue');
  148. const copyReportButton = fatalContainer.querySelector('.btn-copy-report');
  149. atom.tooltips.add(copyReportButton, {title: copyReportButton.getAttribute('title')});
  150. copyReportButton.addEventListener('click', e => {
  151. e.preventDefault();
  152. return this.issue.getIssueBody().then(issueBody => atom.clipboard.write(issueBody));
  153. });
  154. if ((packageName != null) && (repoUrl != null)) {
  155. fatalNotification.innerHTML = `The error was thrown from the <a href=\"${repoUrl}\">${packageName} package</a>. `;
  156. } else if (packageName != null) {
  157. issueButton.remove();
  158. fatalNotification.textContent = `The error was thrown from the ${packageName} package. `;
  159. } else {
  160. fatalNotification.textContent = "This is likely a bug in Pulsar. ";
  161. }
  162. // We only show the create issue button if it's clearly in atom core or in a package with a repo url
  163. if (issueButton.parentNode != null) {
  164. if ((packageName != null) && (repoUrl != null)) {
  165. issueButton.textContent = `Create issue on the ${packageName} package`;
  166. } else {
  167. issueButton.textContent = "Create issue on pulsar-edit/pulsar";
  168. }
  169. const promises = [];
  170. promises.push(this.issue.findSimilarIssues());
  171. promises.push(UserUtilities.checkPulsarUpToDate());
  172. if (packageName != null) {
  173. promises.push(UserUtilities.checkPackageUpToDate(packageName));
  174. }
  175. return Promise.all(promises).then(allData => {
  176. let issue;
  177. const [issues, atomCheck, packageCheck] = Array.from(allData);
  178. if ((issues != null ? issues.open : undefined) || (issues != null ? issues.closed : undefined)) {
  179. issue = issues.open || issues.closed;
  180. issueButton.setAttribute('href', issue.html_url);
  181. issueButton.textContent = "View Issue";
  182. fatalNotification.innerHTML += " This issue has already been reported.";
  183. } else if ((packageCheck != null) && !packageCheck.upToDate && !packageCheck.isCore) {
  184. issueButton.setAttribute('href', '#');
  185. issueButton.textContent = "Check for package updates";
  186. issueButton.addEventListener('click', function(e) {
  187. e.preventDefault();
  188. const command = 'settings-view:check-for-package-updates';
  189. return atom.commands.dispatch(atom.views.getView(atom.workspace), command);
  190. });
  191. fatalNotification.innerHTML += `\
  192. <code>${packageName}</code> is out of date: ${packageCheck.installedVersion} installed;
  193. ${packageCheck.latestVersion} latest.
  194. Upgrading to the latest version may fix this issue.\
  195. `;
  196. } else if ((packageCheck != null) && !packageCheck.upToDate && packageCheck.isCore) {
  197. issueButton.remove();
  198. fatalNotification.innerHTML += `\
  199. <br><br>
  200. Locally installed core Pulsar package <code>${packageName}</code> is out of date: ${packageCheck.installedVersion} installed locally;
  201. ${packageCheck.versionShippedWithPulsar} included with the version of Pulsar you're running.
  202. Removing the locally installed version may fix this issue.\
  203. `;
  204. const packagePath = __guard__(atom.packages.getLoadedPackage(packageName), x => x.path);
  205. if (fs.isSymbolicLinkSync(packagePath)) {
  206. fatalNotification.innerHTML += `\
  207. <br><br>
  208. Use: <code>apm unlink ${packagePath}</code>\
  209. `;
  210. }
  211. } else if ((atomCheck != null) && !atomCheck.upToDate) {
  212. issueButton.remove();
  213. fatalNotification.innerHTML += `\
  214. Pulsar is out of date: ${atomCheck.installedVersion} installed;
  215. ${atomCheck.latestVersion} latest.
  216. Upgrading to the <a href='https://github.com/pulsar-edit/pulsar/releases/tag/v${atomCheck.latestVersion}'>latest version</a> may fix this issue.\
  217. `;
  218. } else {
  219. fatalNotification.innerHTML += " You can help by creating an issue. Please explain what actions triggered this error.";
  220. issueButton.addEventListener('click', e => {
  221. e.preventDefault();
  222. issueButton.classList.add('opening');
  223. return this.issue.getIssueUrlForSystem().then(function(issueUrl) {
  224. shell.openExternal(issueUrl);
  225. return issueButton.classList.remove('opening');
  226. });
  227. });
  228. }
  229. });
  230. } else {
  231. return Promise.resolve();
  232. }
  233. }
  234. makeDismissable() {
  235. if (!this.model.isDismissable()) {
  236. clearTimeout(this.autohideTimeout);
  237. this.model.options.dismissable = true;
  238. this.model.dismissed = false;
  239. return this.element.classList.add('has-close');
  240. }
  241. }
  242. removeNotification() {
  243. if (!this.element.classList.contains('remove')) {
  244. this.element.classList.add('remove');
  245. return this.removeNotificationAfterTimeout();
  246. }
  247. }
  248. handleRemoveNotificationClick() {
  249. this.removeNotification();
  250. return this.model.dismiss();
  251. }
  252. handleRemoveAllNotificationsClick() {
  253. const notifications = atom.notifications.getNotifications();
  254. for (var notification of Array.from(notifications)) {
  255. atom.views.getView(notification).removeNotification();
  256. if (notification.isDismissable() && !notification.isDismissed()) {
  257. notification.dismiss();
  258. }
  259. }
  260. }
  261. handleStackTraceToggleClick(e, container) {
  262. if (typeof e.preventDefault === 'function') {
  263. e.preventDefault();
  264. }
  265. if (container.style.display === 'none') {
  266. e.currentTarget.innerHTML = '<span class="icon icon-dash"></span>Hide Stack Trace';
  267. return container.style.display = 'block';
  268. } else {
  269. e.currentTarget.innerHTML = '<span class="icon icon-plus"></span>Show Stack Trace';
  270. return container.style.display = 'none';
  271. }
  272. }
  273. autohide() {
  274. return this.autohideTimeout = setTimeout(() => {
  275. return this.removeNotification();
  276. }
  277. , this.visibilityDuration);
  278. }
  279. removeNotificationAfterTimeout() {
  280. if (this.element === document.activeElement) { atom.workspace.getActivePane().activate(); }
  281. return setTimeout(() => {
  282. return this.element.remove();
  283. }
  284. , this.animationDuration); // keep in sync with CSS animation
  285. }
  286. getButtonClass() {
  287. const type = `btn-${this.model.getType()}`;
  288. if (type === 'btn-fatal') { return 'btn-error'; } else { return type; }
  289. }
  290. };
  291. NotificationElement.initClass();
  292. return NotificationElement;
  293. })());
  294. var addSplitLinesToContainer = function(container, content) {
  295. if (typeof content !== 'string') { content = content.toString(); }
  296. for (var line of Array.from(content.split('\n'))) {
  297. var div = document.createElement('div');
  298. div.classList.add('line');
  299. div.textContent = line;
  300. container.appendChild(div);
  301. }
  302. };
  303. function __guard__(value, transform) {
  304. return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;
  305. }