settingBody.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import { TAPi18n } from '/imports/i18n';
  3. import { ALLOWED_WAIT_SPINNERS } from '/config/const';
  4. import LockoutSettings from '/models/lockoutSettings';
  5. // Template helpers for settingBody
  6. Template.setting.helpers({
  7. generalSetting() {
  8. const instance = Template.instance();
  9. if (instance && instance.generalSetting) {
  10. return instance.generalSetting.get();
  11. }
  12. return false;
  13. },
  14. emailSetting() {
  15. const instance = Template.instance();
  16. if (instance && instance.emailSetting) {
  17. return instance.emailSetting.get();
  18. }
  19. return false;
  20. },
  21. accountSetting() {
  22. const instance = Template.instance();
  23. if (instance && instance.accountSetting) {
  24. return instance.accountSetting.get();
  25. }
  26. return false;
  27. },
  28. tableVisibilityModeSetting() {
  29. const instance = Template.instance();
  30. if (instance && instance.tableVisibilityModeSetting) {
  31. return instance.tableVisibilityModeSetting.get();
  32. }
  33. return false;
  34. },
  35. announcementSetting() {
  36. const instance = Template.instance();
  37. if (instance && instance.announcementSetting) {
  38. return instance.announcementSetting.get();
  39. }
  40. return false;
  41. },
  42. accessibilitySetting() {
  43. const instance = Template.instance();
  44. if (instance && instance.accessibilitySetting) {
  45. return instance.accessibilitySetting.get();
  46. }
  47. return false;
  48. },
  49. layoutSetting() {
  50. const instance = Template.instance();
  51. if (instance && instance.layoutSetting) {
  52. return instance.layoutSetting.get();
  53. }
  54. return false;
  55. },
  56. webhookSetting() {
  57. const instance = Template.instance();
  58. if (instance && instance.webhookSetting) {
  59. return instance.webhookSetting.get();
  60. }
  61. return false;
  62. },
  63. attachmentSettings() {
  64. const instance = Template.instance();
  65. if (instance && instance.attachmentSettings) {
  66. return instance.attachmentSettings.get();
  67. }
  68. return false;
  69. },
  70. cronSettings() {
  71. const instance = Template.instance();
  72. if (instance && instance.cronSettings) {
  73. return instance.cronSettings.get();
  74. }
  75. return false;
  76. },
  77. loading() {
  78. const instance = Template.instance();
  79. if (instance && instance.loading) {
  80. return instance.loading.get();
  81. }
  82. return false;
  83. }
  84. });
  85. BlazeComponent.extendComponent({
  86. onCreated() {
  87. this.error = new ReactiveVar('');
  88. this.loading = new ReactiveVar(false);
  89. this.forgotPasswordSetting = new ReactiveVar(true);
  90. this.generalSetting = new ReactiveVar(true);
  91. this.emailSetting = new ReactiveVar(false);
  92. this.accountSetting = new ReactiveVar(false);
  93. this.tableVisibilityModeSetting = new ReactiveVar(false);
  94. this.announcementSetting = new ReactiveVar(false);
  95. this.accessibilitySetting = new ReactiveVar(false);
  96. this.layoutSetting = new ReactiveVar(false);
  97. this.webhookSetting = new ReactiveVar(false);
  98. this.attachmentSettings = new ReactiveVar(false);
  99. this.cronSettings = new ReactiveVar(false);
  100. Meteor.subscribe('setting');
  101. Meteor.subscribe('mailServer');
  102. Meteor.subscribe('accountSettings');
  103. Meteor.subscribe('tableVisibilityModeSettings');
  104. Meteor.subscribe('announcements');
  105. Meteor.subscribe('accessibilitySettings');
  106. Meteor.subscribe('globalwebhooks');
  107. Meteor.subscribe('lockoutSettings');
  108. },
  109. setError(error) {
  110. this.error.set(error);
  111. },
  112. setLoading(w) {
  113. this.loading.set(w);
  114. },
  115. checkField(selector) {
  116. const value = $(selector).val();
  117. if (!value || value.trim() === '') {
  118. $(selector)
  119. .parents('li.smtp-form')
  120. .addClass('has-error');
  121. throw Error('blank field');
  122. } else {
  123. return value;
  124. }
  125. },
  126. boards() {
  127. const ret = ReactiveCache.getBoards(
  128. {
  129. archived: false,
  130. 'members.userId': Meteor.userId(),
  131. 'members.isAdmin': true,
  132. },
  133. {
  134. sort: { sort: 1 /* boards default sorting */ },
  135. },
  136. );
  137. return ret;
  138. },
  139. toggleForgotPassword() {
  140. this.setLoading(true);
  141. const forgotPasswordClosed = ReactiveCache.getCurrentSetting().disableForgotPassword;
  142. Settings.update(ReactiveCache.getCurrentSetting()._id, {
  143. $set: { disableForgotPassword: !forgotPasswordClosed },
  144. });
  145. this.setLoading(false);
  146. },
  147. toggleRegistration() {
  148. this.setLoading(true);
  149. const registrationClosed = ReactiveCache.getCurrentSetting().disableRegistration;
  150. Settings.update(ReactiveCache.getCurrentSetting()._id, {
  151. $set: { disableRegistration: !registrationClosed },
  152. });
  153. this.setLoading(false);
  154. if (registrationClosed) {
  155. $('.invite-people').slideUp();
  156. } else {
  157. $('.invite-people').slideDown();
  158. }
  159. },
  160. toggleTLS() {
  161. $('#mail-server-tls').toggleClass('is-checked');
  162. },
  163. toggleHideLogo() {
  164. $('#hide-logo').toggleClass('is-checked');
  165. },
  166. toggleHideCardCounterList() {
  167. $('#hide-card-counter-list').toggleClass('is-checked');
  168. },
  169. toggleHideBoardMemberList() {
  170. $('#hide-board-member-list').toggleClass('is-checked');
  171. },
  172. toggleAccessibilityPageEnabled() {
  173. $('#accessibility-page-enabled').toggleClass('is-checked');
  174. },
  175. toggleDisplayAuthenticationMethod() {
  176. $('#display-authentication-method').toggleClass('is-checked');
  177. },
  178. switchAttachmentTab(event) {
  179. event.preventDefault();
  180. const target = $(event.target);
  181. const targetID = target.data('id');
  182. // Update active tab
  183. $('.tab-nav li.active').removeClass('active');
  184. target.parent().addClass('active');
  185. // Call the attachment settings component method if available
  186. if (window.attachmentSettings && window.attachmentSettings.switchMenu) {
  187. window.attachmentSettings.switchMenu(event, targetID);
  188. }
  189. },
  190. switchCronTab(event) {
  191. event.preventDefault();
  192. const target = $(event.target);
  193. const targetID = target.data('id');
  194. // Update active tab
  195. $('.tab-nav li.active').removeClass('active');
  196. target.parent().addClass('active');
  197. // Call the cron settings template method if available
  198. const cronTemplate = Template.instance();
  199. if (cronTemplate && cronTemplate.switchMenu) {
  200. cronTemplate.switchMenu(event, targetID);
  201. }
  202. },
  203. initializeAttachmentSubMenu() {
  204. // Set default sub-menu state for attachment settings
  205. // This will be handled by the attachment settings component
  206. console.log('Initializing attachment sub-menu');
  207. },
  208. initializeCronSubMenu() {
  209. // Set default sub-menu state for cron settings
  210. // This will be handled by the cron settings template
  211. console.log('Initializing cron sub-menu');
  212. },
  213. switchMenu(event) {
  214. const target = $(event.target);
  215. if (!target.hasClass('active')) {
  216. $('.side-menu li.active').removeClass('active');
  217. target.parent().addClass('active');
  218. const targetID = target.data('id');
  219. this.forgotPasswordSetting.set('forgot-password-setting' === targetID);
  220. this.generalSetting.set('registration-setting' === targetID);
  221. this.emailSetting.set('email-setting' === targetID);
  222. this.accountSetting.set('account-setting' === targetID);
  223. this.announcementSetting.set('announcement-setting' === targetID);
  224. this.accessibilitySetting.set('accessibility-setting' === targetID);
  225. this.layoutSetting.set('layout-setting' === targetID);
  226. this.webhookSetting.set('webhook-setting' === targetID);
  227. this.attachmentSettings.set('attachment-settings' === targetID);
  228. this.cronSettings.set('cron-settings' === targetID);
  229. // Initialize sub-menu states
  230. if ('attachment-settings' === targetID) {
  231. this.initializeAttachmentSubMenu();
  232. } else if ('cron-settings' === targetID) {
  233. this.initializeCronSubMenu();
  234. }
  235. this.tableVisibilityModeSetting.set('tableVisibilityMode-setting' === targetID);
  236. }
  237. },
  238. checkBoard(event) {
  239. let target = $(event.target);
  240. if (!target.hasClass('js-toggle-board-choose')) {
  241. target = target.parent();
  242. }
  243. const checkboxId = target.attr('id');
  244. $(`#${checkboxId} .materialCheckBox`).toggleClass('is-checked');
  245. $(`#${checkboxId}`).toggleClass('is-checked');
  246. },
  247. inviteThroughEmail() {
  248. const emails = $('#email-to-invite')
  249. .val()
  250. .toLowerCase()
  251. .trim()
  252. .split('\n')
  253. .join(',')
  254. .split(',');
  255. const boardsToInvite = [];
  256. $('.js-toggle-board-choose .materialCheckBox.is-checked').each(function() {
  257. boardsToInvite.push($(this).data('id'));
  258. });
  259. const validEmails = [];
  260. emails.forEach(email => {
  261. if (email && SimpleSchema.RegEx.Email.test(email.trim())) {
  262. validEmails.push(email.trim());
  263. }
  264. });
  265. if (validEmails.length) {
  266. this.setLoading(true);
  267. Meteor.call('sendInvitation', validEmails, boardsToInvite, () => {
  268. // if (!err) {
  269. // TODO - show more info to user
  270. // }
  271. this.setLoading(false);
  272. });
  273. }
  274. },
  275. saveMailServerInfo() {
  276. this.setLoading(true);
  277. $('li').removeClass('has-error');
  278. try {
  279. const host = this.checkField('#mail-server-host');
  280. const port = this.checkField('#mail-server-port');
  281. const username = $('#mail-server-username')
  282. .val()
  283. .trim();
  284. const password = $('#mail-server-password')
  285. .val()
  286. .trim();
  287. const from = this.checkField('#mail-server-from');
  288. const tls = $('#mail-server-tls.is-checked').length > 0;
  289. Settings.update(ReactiveCache.getCurrentSetting()._id, {
  290. $set: {
  291. 'mailServer.host': host,
  292. 'mailServer.port': port,
  293. 'mailServer.username': username,
  294. 'mailServer.password': password,
  295. 'mailServer.enableTLS': tls,
  296. 'mailServer.from': from,
  297. },
  298. });
  299. } catch (e) {
  300. return;
  301. } finally {
  302. this.setLoading(false);
  303. }
  304. },
  305. saveLayout() {
  306. this.setLoading(true);
  307. $('li').removeClass('has-error');
  308. const productName = ($('#product-name').val() || '').trim();
  309. const customLoginLogoImageUrl = ($('#custom-login-logo-image-url').val() || '').trim();
  310. const customLoginLogoLinkUrl = ($('#custom-login-logo-link-url').val() || '').trim();
  311. const customHelpLinkUrl = ($('#custom-help-link-url').val() || '').trim();
  312. const textBelowCustomLoginLogo = ($('#text-below-custom-login-logo').val() || '').trim();
  313. const automaticLinkedUrlSchemes = ($('#automatic-linked-url-schemes').val() || '').trim();
  314. const customTopLeftCornerLogoImageUrl = ($('#custom-top-left-corner-logo-image-url').val() || '').trim();
  315. const customTopLeftCornerLogoLinkUrl = ($('#custom-top-left-corner-logo-link-url').val() || '').trim();
  316. const customTopLeftCornerLogoHeight = ($('#custom-top-left-corner-logo-height').val() || '').trim();
  317. const oidcBtnText = ($('#oidcBtnTextvalue').val() || '').trim();
  318. const mailDomainName = ($('#mailDomainNamevalue').val() || '').trim();
  319. const legalNotice = ($('#legalNoticevalue').val() || '').trim();
  320. const hideLogoChange = $('input[name=hideLogo]:checked').val() === 'true';
  321. const hideCardCounterListChange = $('input[name=hideCardCounterList]:checked').val() === 'true';
  322. const hideBoardMemberListChange = $('input[name=hideBoardMemberList]:checked').val() === 'true';
  323. const displayAuthenticationMethod =
  324. $('input[name=displayAuthenticationMethod]:checked').val() === 'true';
  325. const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
  326. const accessibilityPageEnabled = $('input[name=accessibilityPageEnabled]:checked').val() === 'true';
  327. const accessibilityTitle = ($('#accessibility-title').val() || '').trim();
  328. const accessibilityContent = ($('#accessibility-content').val() || '').trim();
  329. const spinnerName = ($('#spinnerName').val() || '').trim();
  330. try {
  331. Settings.update(ReactiveCache.getCurrentSetting()._id, {
  332. $set: {
  333. productName,
  334. hideLogo: hideLogoChange,
  335. hideCardCounterList: hideCardCounterListChange,
  336. hideBoardMemberList: hideBoardMemberListChange,
  337. customLoginLogoImageUrl,
  338. customLoginLogoLinkUrl,
  339. customHelpLinkUrl,
  340. textBelowCustomLoginLogo,
  341. customTopLeftCornerLogoImageUrl,
  342. customTopLeftCornerLogoLinkUrl,
  343. customTopLeftCornerLogoHeight,
  344. displayAuthenticationMethod,
  345. defaultAuthenticationMethod,
  346. automaticLinkedUrlSchemes,
  347. spinnerName,
  348. oidcBtnText,
  349. mailDomainName,
  350. legalNotice,
  351. accessibilityPageEnabled,
  352. accessibilityTitle,
  353. accessibilityContent,
  354. },
  355. });
  356. } catch (e) {
  357. return;
  358. } finally {
  359. this.setLoading(false);
  360. }
  361. DocHead.setTitle(productName);
  362. },
  363. sendSMTPTestEmail() {
  364. Meteor.call('sendSMTPTestEmail', (err, ret) => {
  365. if (!err && ret) {
  366. const message = `${TAPi18n.__(ret.message)}: ${ret.email}`;
  367. alert(message);
  368. } else {
  369. const reason = err.reason || '';
  370. const message = `${TAPi18n.__(err.error)}\n${reason}`;
  371. alert(message);
  372. }
  373. });
  374. },
  375. events() {
  376. return [
  377. {
  378. 'click a.js-toggle-forgot-password': this.toggleForgotPassword,
  379. 'click a.js-toggle-registration': this.toggleRegistration,
  380. 'click a.js-toggle-tls': this.toggleTLS,
  381. 'click a.js-setting-menu': this.switchMenu,
  382. 'click a.js-toggle-board-choose': this.checkBoard,
  383. 'click button.js-email-invite': this.inviteThroughEmail,
  384. 'click button.js-save': this.saveMailServerInfo,
  385. 'click button.js-send-smtp-test-email': this.sendSMTPTestEmail,
  386. 'click a.js-toggle-hide-logo': this.toggleHideLogo,
  387. 'click a.js-toggle-hide-card-counter-list': this.toggleHideCardCounterList,
  388. 'click a.js-toggle-hide-board-member-list': this.toggleHideBoardMemberList,
  389. 'click button.js-save-layout': this.saveLayout,
  390. 'click a.js-toggle-display-authentication-method': this
  391. .toggleDisplayAuthenticationMethod,
  392. 'click a.js-attachment-storage-settings': this.switchAttachmentTab,
  393. 'click a.js-attachment-migration': this.switchAttachmentTab,
  394. 'click a.js-attachment-monitoring': this.switchAttachmentTab,
  395. 'click a.js-cron-migrations': this.switchCronTab,
  396. 'click a.js-cron-board-operations': this.switchCronTab,
  397. 'click a.js-cron-jobs': this.switchCronTab,
  398. 'click a.js-cron-add': this.switchCronTab,
  399. },
  400. ];
  401. },
  402. }).register('setting');
  403. BlazeComponent.extendComponent({
  404. saveAccountsChange() {
  405. const allowEmailChange =
  406. $('input[name=allowEmailChange]:checked').val() === 'true';
  407. const allowUserNameChange =
  408. $('input[name=allowUserNameChange]:checked').val() === 'true';
  409. const allowUserDelete =
  410. $('input[name=allowUserDelete]:checked').val() === 'true';
  411. AccountSettings.update('accounts-allowEmailChange', {
  412. $set: { booleanValue: allowEmailChange },
  413. });
  414. AccountSettings.update('accounts-allowUserNameChange', {
  415. $set: { booleanValue: allowUserNameChange },
  416. });
  417. AccountSettings.update('accounts-allowUserDelete', {
  418. $set: { booleanValue: allowUserDelete },
  419. });
  420. },
  421. // Brute force lockout settings method moved to lockedUsersBody.js
  422. allowEmailChange() {
  423. return AccountSettings.findOne('accounts-allowEmailChange')?.booleanValue || false;
  424. },
  425. allowUserNameChange() {
  426. return AccountSettings.findOne('accounts-allowUserNameChange')?.booleanValue || false;
  427. },
  428. allowUserDelete() {
  429. return AccountSettings.findOne('accounts-allowUserDelete')?.booleanValue || false;
  430. },
  431. // Lockout settings helper methods moved to lockedUsersBody.js
  432. allBoardsHideActivities() {
  433. Meteor.call('setAllBoardsHideActivities', (err, ret) => {
  434. if (!err && ret) {
  435. if (ret === true) {
  436. const message = `${TAPi18n.__(
  437. 'now-activities-of-all-boards-are-hidden',
  438. )}`;
  439. alert(message);
  440. }
  441. } else {
  442. const reason = err.reason || '';
  443. const message = `${TAPi18n.__(err.error)}\n${reason}`;
  444. alert(message);
  445. }
  446. });
  447. },
  448. events() {
  449. return [
  450. {
  451. 'click button.js-accounts-save': this.saveAccountsChange,
  452. },
  453. {
  454. 'click button.js-all-boards-hide-activities': this.allBoardsHideActivities,
  455. },
  456. ];
  457. },
  458. }).register('accountSettings');
  459. BlazeComponent.extendComponent({
  460. saveTableVisibilityChange() {
  461. const allowPrivateOnly =
  462. $('input[name=allowPrivateOnly]:checked').val() === 'true';
  463. TableVisibilityModeSettings.update('tableVisibilityMode-allowPrivateOnly', {
  464. $set: { booleanValue: allowPrivateOnly },
  465. });
  466. },
  467. allowPrivateOnly() {
  468. return TableVisibilityModeSettings.findOne('tableVisibilityMode-allowPrivateOnly').booleanValue;
  469. },
  470. allBoardsHideActivities() {
  471. Meteor.call('setAllBoardsHideActivities', (err, ret) => {
  472. if (!err && ret) {
  473. if (ret === true) {
  474. const message = `${TAPi18n.__(
  475. 'now-activities-of-all-boards-are-hidden',
  476. )}`;
  477. alert(message);
  478. }
  479. } else {
  480. const reason = err.reason || '';
  481. const message = `${TAPi18n.__(err.error)}\n${reason}`;
  482. alert(message);
  483. }
  484. });
  485. },
  486. events() {
  487. return [
  488. {
  489. 'click button.js-tableVisibilityMode-save': this.saveTableVisibilityChange,
  490. },
  491. {
  492. 'click button.js-all-boards-hide-activities': this.allBoardsHideActivities,
  493. },
  494. ];
  495. },
  496. }).register('tableVisibilityModeSettings');
  497. BlazeComponent.extendComponent({
  498. onCreated() {
  499. this.loading = new ReactiveVar(false);
  500. },
  501. setLoading(w) {
  502. this.loading.set(w);
  503. },
  504. currentAnnouncements() {
  505. return Announcements.findOne();
  506. },
  507. saveMessage() {
  508. const message = $('#admin-announcement')
  509. .val()
  510. .trim();
  511. Announcements.update(Announcements.findOne()._id, {
  512. $set: { body: message },
  513. });
  514. },
  515. toggleActive() {
  516. this.setLoading(true);
  517. const announcements = this.currentAnnouncements();
  518. const isActive = announcements.enabled;
  519. Announcements.update(announcements._id, {
  520. $set: { enabled: !isActive },
  521. });
  522. this.setLoading(false);
  523. if (isActive) {
  524. $('.admin-announcement').slideUp();
  525. } else {
  526. $('.admin-announcement').slideDown();
  527. }
  528. },
  529. events() {
  530. return [
  531. {
  532. 'click a.js-toggle-activemessage': this.toggleActive,
  533. 'click button.js-announcement-save': this.saveMessage,
  534. },
  535. ];
  536. },
  537. }).register('announcementSettings');
  538. BlazeComponent.extendComponent({
  539. onCreated() {
  540. this.loading = new ReactiveVar(false);
  541. },
  542. setLoading(w) {
  543. this.loading.set(w);
  544. },
  545. currentAccessibility() {
  546. return AccessibilitySettings.findOne();
  547. },
  548. saveAccessibility() {
  549. const title = $('#admin-accessibility-title')
  550. .val()
  551. .trim();
  552. const content = $('#admin-accessibility-content')
  553. .val()
  554. .trim();
  555. AccessibilitySettings.update(AccessibilitySettings.findOne()._id, {
  556. $set: {
  557. title: title,
  558. body: content
  559. },
  560. });
  561. },
  562. toggleAccessibility() {
  563. this.setLoading(true);
  564. const accessibilitySetting = this.currentAccessibility();
  565. const isActive = accessibilitySetting.enabled;
  566. AccessibilitySettings.update(accessibilitySetting._id, {
  567. $set: { enabled: !isActive },
  568. });
  569. this.setLoading(false);
  570. if (isActive) {
  571. $('.accessibility-content').slideUp();
  572. } else {
  573. $('.accessibility-content').slideDown();
  574. }
  575. },
  576. events() {
  577. return [
  578. {
  579. 'click a.js-toggle-accessibility': this.toggleAccessibility,
  580. 'click button.js-accessibility-save': this.saveAccessibility,
  581. },
  582. ];
  583. },
  584. }).register('accessibilitySettings');
  585. Template.selectAuthenticationMethod.onCreated(function() {
  586. this.authenticationMethods = new ReactiveVar([]);
  587. Meteor.call('getAuthenticationsEnabled', (_, result) => {
  588. if (result) {
  589. // TODO : add a management of different languages
  590. // (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
  591. this.authenticationMethods.set([
  592. { value: 'password' },
  593. // Gets only the authentication methods availables
  594. ...Object.entries(result)
  595. .filter(e => e[1])
  596. .map(e => ({ value: e[0] })),
  597. ]);
  598. }
  599. });
  600. });
  601. Template.selectAuthenticationMethod.helpers({
  602. authentications() {
  603. return Template.instance().authenticationMethods.get();
  604. },
  605. isSelected(match) {
  606. return Template.instance().data.authenticationMethod === match;
  607. },
  608. });
  609. Template.selectSpinnerName.helpers({
  610. spinners() {
  611. return ALLOWED_WAIT_SPINNERS;
  612. },
  613. isSelected(match) {
  614. return Template.instance().data.spinnerName === match;
  615. },
  616. });