mirror of
https://github.com/wekan/wekan.git
synced 2026-11-05 01:20:07 -05:00
Add a 3-tier Notification Settings popup (Member/Board/Admin).
Admin Panel / People, Board Settings and Member Settings each gain a "Notification Settings" menu entry, right below Email in Admin Panel / People. All three open the same reusable notificationSettingsPopup template, letting notifications (tray/in-app and email, the two services Notifications.notify already dispatches to) be turned on or off with the same precedence the theme override already uses: Admin Panel default, then Board override, then the member's own override. models/lib/notificationSettings.js is a small pure module with the resolution function (resolveNotificationSetting) and the notification-type catalog; server/notifications/profile.js and server/notifications/email.js now resolve the decision before adding to the tray or buffering an email, so a disabled service is genuinely skipped rather than only hidden in the UI. Settings.notifyDefaultTray/Email hold the admin default, Board.notifyOverrideTray/Email the board override, and profile.notifyOverrideTray/Email the member override; each is optional/ nullable so "unset" always falls through to the next level.
This commit is contained in:
1 parent
7ec35b880a
commit
bde66a5c7d
40 files changed
+818
-25
No files matched your search
@@ -0,0 +1,37 @@
|
||||
//- The reusable "Notification Settings" popup: one template, opened from the
|
||||
//- three places the 3-tier Notification Settings system (see
|
||||
//- models/lib/notificationSettings.js) can be configured -
|
||||
//- Member Settings (client/components/users/userHeader.jade) scope='member'
|
||||
//- Board Settings (client/components/sidebar/sidebar.jade) scope='board'
|
||||
//- Admin Panel/People (client/components/settings/peopleBody.jade) scope='admin'
|
||||
//- Precedence: admin default -> board override -> member override, the same
|
||||
//- shape the board/member theme override already uses elsewhere. At the board
|
||||
//- and member scopes, "Use admin/board default" clears the override for that
|
||||
//- level so it falls through to the level below.
|
||||
template(name="notificationSettingsPopup")
|
||||
ul.pop-over-list.notification-settings-popup
|
||||
li
|
||||
p.quiet {{_ 'notification-settings-popup-description'}}
|
||||
each notifyServiceRows
|
||||
li
|
||||
.title
|
||||
i.fa(class="{{icon}}")
|
||||
| {{_ labelKey}}
|
||||
.notification-settings-popup-options
|
||||
if isAdminScope
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="true" class="{{#if onSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
span {{_ 'yes'}}
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="false" class="{{#if offSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
span {{_ 'no'}}
|
||||
else
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="" class="{{#if inheritSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
span {{_ 'default'}}
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="true" class="{{#if onSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
span {{_ 'yes'}}
|
||||
a.flex.js-notify-option(data-service="{{service}}" data-value="false" class="{{#if offSelected}}is-checked{{/if}}")
|
||||
.materialCheckBox
|
||||
span {{_ 'no'}}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { NOTIFICATION_SERVICES } from '/models/lib/notificationSettings';
|
||||
|
||||
// The 3-tier Notification Settings popup (see notificationSettingsPopup.jade
|
||||
// and models/lib/notificationSettings.js). `this.data().scope` is one of
|
||||
// 'admin' | 'board' | 'member', set by whichever menu entry opened it
|
||||
// (Popup.open('notificationSettings', { scope }) in userHeader.js/sidebar.js,
|
||||
// or passed directly as the pane's data context in peopleBody.jade).
|
||||
//
|
||||
// At 'board' scope the board id comes from Utils.getCurrentBoardId() - the
|
||||
// popup is only ever opened from the currently open board's sidebar.
|
||||
|
||||
function currentBoard() {
|
||||
return ReactiveCache.getBoard(Utils.getCurrentBoardId());
|
||||
}
|
||||
|
||||
function currentValue(scope, service) {
|
||||
const field = service === 'email' ? 'notifyOverrideEmail' : 'notifyOverrideTray';
|
||||
const adminField = service === 'email' ? 'notifyDefaultEmail' : 'notifyDefaultTray';
|
||||
if (scope === 'admin') {
|
||||
const setting = ReactiveCache.getCurrentSetting();
|
||||
const value = setting && setting[adminField];
|
||||
return value === false ? false : true; // defaultValue: true
|
||||
}
|
||||
if (scope === 'board') {
|
||||
const board = currentBoard();
|
||||
return board ? board[field] : undefined;
|
||||
}
|
||||
// member
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
const profileField = service === 'email' ? 'notifyOverrideEmail' : 'notifyOverrideTray';
|
||||
return user && user.profile ? user.profile[profileField] : undefined;
|
||||
}
|
||||
|
||||
Template.notificationSettingsPopup.helpers({
|
||||
isAdminScope() {
|
||||
return Template.currentData().scope === 'admin';
|
||||
},
|
||||
notifyServiceRows() {
|
||||
const scope = Template.currentData().scope;
|
||||
return Object.keys(NOTIFICATION_SERVICES).map(service => {
|
||||
const value = currentValue(scope, service);
|
||||
return {
|
||||
service,
|
||||
icon: service === 'email' ? 'fa-envelope' : 'fa-bell',
|
||||
labelKey: service === 'email' ? 'email' : 'notification-settings-tray',
|
||||
onSelected: value === true,
|
||||
offSelected: value === false,
|
||||
inheritSelected: value !== true && value !== false,
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.notificationSettingsPopup.events({
|
||||
'click .js-notify-option'(event, instance) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget;
|
||||
const service = target.dataset.service;
|
||||
const rawValue = target.dataset.value;
|
||||
const value = rawValue === 'true' ? true : rawValue === 'false' ? false : null;
|
||||
const scope = Template.currentData().scope;
|
||||
|
||||
if (scope === 'admin') {
|
||||
// Inherit is not offered at admin scope - it IS the base default - so
|
||||
// only true/false ever reach here.
|
||||
Meteor.call('setAdminNotifyDefault', service, value === true);
|
||||
return;
|
||||
}
|
||||
if (scope === 'board') {
|
||||
const board = currentBoard();
|
||||
if (board) board.setNotifyOverride(service, value);
|
||||
return;
|
||||
}
|
||||
const user = ReactiveCache.getCurrentUser();
|
||||
if (user) user.setNotifyOverride(service, value);
|
||||
},
|
||||
});
|
||||
@@ -35,6 +35,8 @@ template(name="people")
|
||||
+general
|
||||
else if emailSetting.get
|
||||
+email
|
||||
else if notifySetting.get
|
||||
+notificationSettingsPopup(scope="admin")
|
||||
else if orgSetting.get
|
||||
+orgGeneral(orgTablePageData)
|
||||
else if teamSetting.get
|
||||
@@ -711,6 +713,15 @@ template(name="settingsUserPopup")
|
||||
| {{_ 'impersonate-user'}}
|
||||
br
|
||||
hr
|
||||
li
|
||||
form
|
||||
label.hide.userId(type="text" value=user._id)
|
||||
label
|
||||
| {{_ 'anonymize-account-confirm-popup' }}
|
||||
br
|
||||
div.buttonsContainer
|
||||
input#anonymizeButton.card-details-red.right.wide(type="button" value="{{_ 'anonymize-account'}}")
|
||||
br
|
||||
li
|
||||
form
|
||||
label.hide.userId(type="text" value=user._id)
|
||||
|
||||
@@ -109,6 +109,9 @@ Template.people.onCreated(function () {
|
||||
// The page opens on Login, the first entry of the menu - as it always did.
|
||||
this.registrationSetting = new ReactiveVar(true);
|
||||
this.emailSetting = new ReactiveVar(false);
|
||||
// Admin-level default for the 3-tier Notification Settings system, see
|
||||
// models/lib/notificationSettings.js.
|
||||
this.notifySetting = new ReactiveVar(false);
|
||||
this.orgSetting = new ReactiveVar(false);
|
||||
this.teamSetting = new ReactiveVar(false);
|
||||
this.peopleSetting = new ReactiveVar(false);
|
||||
@@ -392,6 +395,7 @@ Template.people.onCreated(function () {
|
||||
this.activeMenuId.set(targetID);
|
||||
this.registrationSetting.set('registration-setting' === targetID);
|
||||
this.emailSetting.set('email-setting' === targetID);
|
||||
this.notifySetting.set('notify-setting' === targetID);
|
||||
this.orgSetting.set('org-setting' === targetID);
|
||||
this.teamSetting.set('team-setting' === targetID);
|
||||
this.peopleSetting.set('people-setting' === targetID);
|
||||
@@ -507,6 +511,10 @@ function peopleMenu(user) {
|
||||
{ id: 'registration-setting', icon: 'fa-key', labelKey: 'login', emoji: true },
|
||||
// No e-mail settings on Sandstorm; a null entry is dropped, not rendered empty.
|
||||
isSandstorm ? null : { id: 'email-setting', icon: 'fa-envelope', labelKey: 'email', emoji: true },
|
||||
// Admin-level default for the 3-tier Notification Settings system (see
|
||||
// models/lib/notificationSettings.js): admin default -> board override ->
|
||||
// member override. Sits right below E-mail, the setting it is closest to.
|
||||
{ id: 'notify-setting', icon: 'fa-bell', labelKey: 'notifications', emoji: true },
|
||||
// Domains sits with E-mail: it lists the e-mail domains the users sign in
|
||||
// with, so it belongs beside the e-mail settings rather than at the end of
|
||||
// the menu, after the roles and template checkbox lists.
|
||||
@@ -816,6 +824,9 @@ Template.people.helpers({
|
||||
emailSetting() {
|
||||
return Template.instance().emailSetting;
|
||||
},
|
||||
notifySetting() {
|
||||
return Template.instance().notifySetting;
|
||||
},
|
||||
orgSetting() {
|
||||
return Template.instance().orgSetting;
|
||||
},
|
||||
@@ -2594,6 +2605,34 @@ Template.settingsUserPopup.events({
|
||||
}
|
||||
});
|
||||
},
|
||||
// #2731: GDPR-friendlier alternative to deleteButton above - scrubs PII and
|
||||
// disables login, but leaves every board/card/comment reference to this
|
||||
// userId untouched (no reference-pruning, no hard delete).
|
||||
'click #anonymizeButton'(event) {
|
||||
event.preventDefault();
|
||||
const userId = this.userId || this.user?._id;
|
||||
|
||||
Meteor.call('anonymizeUser', userId, (error, result) => {
|
||||
if (error) {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.error('Error anonymizing user:', error);
|
||||
}
|
||||
if (error.error === 'not-authorized') {
|
||||
alert('You are not authorized to anonymize this user.');
|
||||
} else if (error.error === 'user-not-found') {
|
||||
alert('User not found.');
|
||||
} else {
|
||||
alert('Error anonymizing user: ' + error.reason);
|
||||
}
|
||||
} else {
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('User anonymized successfully:', result);
|
||||
}
|
||||
peopleListChanged();
|
||||
Popup.back();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.settingsUserPopup.helpers({
|
||||
|
||||
@@ -862,6 +862,10 @@ template(name="boardMenuPopup")
|
||||
a.js-change-background-image
|
||||
i.fa.fa-picture-o
|
||||
| {{_ 'board-change-background-image'}}
|
||||
li
|
||||
a.js-open-notification-settings
|
||||
i.fa.fa-bell
|
||||
| {{_ 'notifications'}}
|
||||
//- "Board backgrounds" was here. It was the list of pictures uploaded to
|
||||
this board, and that list is drawn inside Change Background Image now -
|
||||
under the upload that adds to it - so a second entry to the same list
|
||||
|
||||
@@ -520,6 +520,10 @@ Template.boardMenuPopup.events({
|
||||
},
|
||||
'click .js-change-board-color': Popup.open('boardChangeColor'),
|
||||
'click .js-change-background-image': Popup.open('boardChangeBackgroundImage'),
|
||||
'click .js-open-notification-settings': Popup.open('notificationSettings', {
|
||||
titleKey: 'notifications',
|
||||
scope: 'board',
|
||||
}),
|
||||
'click .js-board-info-on-my-boards': Popup.open('boardInfoOnMyBoards'),
|
||||
'click .js-change-language': Popup.open('changeLanguage'),
|
||||
'click .js-delete-duplicate-lists': Popup.afterConfirm('deleteDuplicateLists', function() {
|
||||
|
||||
@@ -25,6 +25,10 @@ template(name="memberMenuPopup")
|
||||
a.js-my-attachments(href="{{pathFor 'my-attachments'}}")
|
||||
i.fa.fa-paperclip
|
||||
| {{_ 'my-attachments'}}
|
||||
li
|
||||
a.js-starred-items(href="{{pathFor 'starred-items'}}")
|
||||
i.fa.fa-star
|
||||
| {{_ 'allboards.starred'}}
|
||||
li
|
||||
a.js-global-search(href="{{pathFor 'global-search'}}")
|
||||
i.fa.fa-search
|
||||
@@ -87,6 +91,10 @@ template(name="memberMenuPopup")
|
||||
a.js-change-color
|
||||
i.fa.fa-paint-brush
|
||||
| {{_ 'change-color'}}
|
||||
li
|
||||
a.js-open-notification-settings
|
||||
i.fa.fa-bell
|
||||
| {{_ 'notifications'}}
|
||||
li
|
||||
a.js-change-font
|
||||
i.fa.fa-font
|
||||
|
||||
@@ -73,6 +73,9 @@ Template.memberMenuPopup.events({
|
||||
'click .js-my-attachments'() {
|
||||
Popup.back();
|
||||
},
|
||||
'click .js-starred-items'() {
|
||||
Popup.back();
|
||||
},
|
||||
// Boards in Archive is a SECTION of All Boards - the row in its left menu -
|
||||
// not a page of its own. This sent the reader to `/archive`, the full-width
|
||||
// page that section replaced: the same list, but with no menu beside it and
|
||||
@@ -86,6 +89,10 @@ Template.memberMenuPopup.events({
|
||||
'click .js-edit-profile': Popup.open('editProfile'),
|
||||
'click .js-change-settings': Popup.open('changeSettings'),
|
||||
'click .js-change-color': Popup.open('changeColor'),
|
||||
'click .js-open-notification-settings': Popup.open('notificationSettings', {
|
||||
titleKey: 'notifications',
|
||||
scope: 'member',
|
||||
}),
|
||||
'click .js-change-font': Popup.open('changeFont'),
|
||||
'click .js-change-avatar': Popup.open('changeAvatar'),
|
||||
'click .js-change-password': Popup.open('changePassword'),
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Tiempo d'entrega",
|
||||
"board-view-throughput-histogram": "Histograma de rendimiento",
|
||||
"board-view-wip-run": "Execución WIP",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Pulso",
|
||||
"board-view-not-yet-implemented": "Ista vista encara no ye implementada.",
|
||||
"bucket-example": "Mes anterior",
|
||||
"calendar-previous-month-label": "Mes anterior",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -279,9 +279,6 @@
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"chart-forecast-none-remaining": "Nothing left open to project - every card is done.",
|
||||
"chart-forecast-no-velocity": "__remaining__ card(s) still open; no recent completions to project a date from.",
|
||||
"chart-forecast-projected": "At the recent pace of __average__ card(s)/week, the __remaining__ card(s) still open should be done by __date__.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
"calendar-next-month-label": "Next Month",
|
||||
@@ -927,6 +924,13 @@
|
||||
"starred-boards": "Starred Boards",
|
||||
"starred-pages": "Starred Pages",
|
||||
"starred-boards-description": "Starred boards show up at the top of your boards list.",
|
||||
"star-item": "Star",
|
||||
"unstar-item": "Unstar",
|
||||
"starred-swimlanes": "Starred Swimlanes",
|
||||
"starred-lists": "Starred Lists",
|
||||
"starred-cards": "Starred Cards",
|
||||
"see-all-starred-items": "See all starred items",
|
||||
"no-starred-items": "You have not starred anything yet.",
|
||||
"subscribe": "Subscribe",
|
||||
"team": "Team",
|
||||
"this-board": "this board",
|
||||
@@ -1982,6 +1986,7 @@
|
||||
"board-status-time-spent-total": "Total time spent",
|
||||
"board-status-cards-with-time": "Cards with time spent",
|
||||
"board-status-overtime-cards": "Overtime cards",
|
||||
"board-status-remaining-time-total": "Remaining time until due",
|
||||
"attachmentRenamePopup-title": "Rename",
|
||||
"uploading": "Uploading",
|
||||
"remaining_time": "Remaining time",
|
||||
@@ -2576,5 +2581,8 @@
|
||||
"wip-limit-group-add": "Add WIP limit group",
|
||||
"wip-limit-group-select-swimlane": "Select swimlane",
|
||||
"wip-limit-group-apply-swimlane": "Apply to swimlane",
|
||||
"subtask-inherit-parent-labels": "Inherit parent's labels"
|
||||
"subtask-inherit-parent-labels": "Inherit parent's labels",
|
||||
"chart-forecast-none-remaining": "Nothing left open to project - every card is done.",
|
||||
"chart-forecast-no-velocity": "__remaining__ card(s) still open; no recent completions to project a date from.",
|
||||
"chart-forecast-projected": "At the recent pace of __average__ card(s)/week, the __remaining__ card(s) still open should be done by __date__."
|
||||
}
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -273,6 +273,7 @@
|
||||
"board-view-lead-time": "Lead Time",
|
||||
"board-view-throughput-histogram": "Throughput Histogram",
|
||||
"board-view-wip-run": "WIP Run",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-not-yet-implemented": "This view is not implemented yet.",
|
||||
"bucket-example": "Like \"Bucket List\" for example",
|
||||
"calendar-previous-month-label": "Previous Month",
|
||||
@@ -1016,6 +1017,13 @@
|
||||
"smtp-tls": "TLS support",
|
||||
"send-from": "From",
|
||||
"send-smtp-test": "Send a test email to yourself",
|
||||
"email-templates-title": "Email Templates",
|
||||
"email-templates-invite-subject": "Invite email subject",
|
||||
"email-templates-invite-body": "Invite email body",
|
||||
"email-templates-invite-vars-hint": "Available: {email} {inviter} {user} {icode} {url}",
|
||||
"email-templates-activity-subject": "Activity notification email subject",
|
||||
"email-templates-activity-body": "Activity notification email body",
|
||||
"email-templates-activity-vars-hint": "Available: {board} {card} {list} {username} {url} {comment} {action}",
|
||||
"invitation-code": "Invitation Code",
|
||||
"email-invite-register-subject": "__inviter__ sent you an invitation",
|
||||
"email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.",
|
||||
@@ -1172,6 +1180,9 @@
|
||||
"r-delete-selected": "Delete selected",
|
||||
"r-export-selected": "Export selected",
|
||||
"r-edit-rule": "Edit rule",
|
||||
"r-rule-enabled": "Enabled",
|
||||
"r-rule-disabled": "Disabled",
|
||||
"r-toggle-rule-enabled": "Enable or disable this rule",
|
||||
"r-edit-rule-trigger-action": "Edit rule",
|
||||
"r-workflow-view": "Workflow view",
|
||||
"r-list-view": "List view",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Oge Nnyefe",
|
||||
"board-view-throughput-histogram": "Histogram Mmepụta",
|
||||
"board-view-wip-run": "Ọganihu WIP",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Mkpọ obi",
|
||||
"board-view-not-yet-implemented": "Emebeghị echiche a.",
|
||||
"bucket-example": "Dị ka \"Ndepụta bọket\" dịka ọmụmaatụ",
|
||||
"calendar-previous-month-label": "Ọnwa gara aga",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Nassiuinerup piffissaa",
|
||||
"board-view-throughput-histogram": "Suliaqartitsinermi nassuiaat",
|
||||
"board-view-wip-run": "WIP siuarsaaneq",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Puls",
|
||||
"board-view-not-yet-implemented": "Isiginnaartitaq una suliarineqarpianngilaq.",
|
||||
"bucket-example": "Assersuutigalugu \"Bucket List\"-itut",
|
||||
"calendar-previous-month-label": "Qaammat siulleq",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Fotoam-panaterana",
|
||||
"board-view-throughput-histogram": "Histogram'ny famokarana",
|
||||
"board-view-wip-run": "Fandrosoan'ny WIP",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Pouls",
|
||||
"board-view-not-yet-implemented": "Mbola tsy voatontosa ity fijery ity.",
|
||||
"bucket-example": "Ohatra, tahaka ny \"Lisitra siny\"",
|
||||
"calendar-previous-month-label": "Volana teo aloha",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Toimitusaika",
|
||||
"board-view-throughput-histogram": "Läpivientihistogrammi",
|
||||
"board-view-wip-run": "WIP-kulku",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Puls",
|
||||
"board-view-not-yet-implemented": "Tätä näkymää ei ole vielä toteutettu.",
|
||||
"bucket-example": "Ovdamearkka dihte nugo \"Bucket List\"",
|
||||
"calendar-previous-month-label": "Ovddit mánnu",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Nguva yekupa",
|
||||
"board-view-throughput-histogram": "Histogram yezvinobudiswa",
|
||||
"board-view-wip-run": "Kufambira mberi kweWIP",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Kurova kwemoyo",
|
||||
"board-view-not-yet-implemented": "Chionero ichi hachisati chaiswa mushandiso.",
|
||||
"bucket-example": "Like \"Bucket rondedzero\" for example",
|
||||
"calendar-previous-month-label": "yapfuura Month",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "ናይ ምብጻሕ ግዜ",
|
||||
"board-view-throughput-histogram": "ናይ ምህርታውነት ሂስቶግራም",
|
||||
"board-view-wip-run": "ናይ WIP ሂደት",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "ልብ ምት",
|
||||
"board-view-not-yet-implemented": "እዚ ትርኢት እዚ ገና ኣይተተግበረን።",
|
||||
"bucket-example": "ንኣብነት ከም «ዝርዝር ባልዲ»",
|
||||
"calendar-previous-month-label": "ዝሓለፈ ወርሒ",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "ናይ ምብጻሕ ግዜ",
|
||||
"board-view-throughput-histogram": "ናይ ምህርታውነት ሂስቶግራም",
|
||||
"board-view-wip-run": "ናይ WIP ሂደት",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "ልብ ምት",
|
||||
"board-view-not-yet-implemented": "እዚ ትርኢት እዚ ገና ኣይተተግበረን።",
|
||||
"bucket-example": "ንኣብነት ከም \"ዝርዝር ባልዲ\"",
|
||||
"calendar-previous-month-label": "ዝሓለፈ ወርሒ",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Ixesha lokuhanjiswa",
|
||||
"board-view-throughput-histogram": "I-histogram yemveliso",
|
||||
"board-view-wip-run": "Inkqubela ye-WIP",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Ukushaya kwenhliziyo",
|
||||
"board-view-not-yet-implemented": "Lo mboniso awukapheleliswa okwangoku.",
|
||||
"bucket-example": "Njengo \"Luhlu lweBhakethi\" umzekelo",
|
||||
"calendar-previous-month-label": "Inyanga edlulileyo",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Àkókò Ìfijíṣẹ́",
|
||||
"board-view-throughput-histogram": "Àwòrán Ìtòjade",
|
||||
"board-view-wip-run": "Ìlọsíwájú WIP",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Ìlù ọkàn",
|
||||
"board-view-not-yet-implemented": "A kò tíì ṣàmúlò ìwòye yìí.",
|
||||
"bucket-example": "Bi \"Akojọ garawa\" fun apẹẹrẹ",
|
||||
"calendar-previous-month-label": "Osu ti o ti kọja",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Isikhathi sokulethwa",
|
||||
"board-view-throughput-histogram": "Ihisithogramu yomkhiqizo",
|
||||
"board-view-wip-run": "Inqubekelaphambili ye-WIP",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Ukushaya kwenhliziyo",
|
||||
"board-view-not-yet-implemented": "Lo mbukiso awukakenziwa.",
|
||||
"bucket-example": "Njengokuthi \"Uhlu Lwebhakede\" ngokwesibonelo",
|
||||
"calendar-previous-month-label": "Inyanga edlule",
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
"board-view-lead-time": "Isikhathi sokulethwa",
|
||||
"board-view-throughput-histogram": "Ihisithogramu yomkhiqizo",
|
||||
"board-view-wip-run": "Inqubekelaphambili ye-WIP",
|
||||
"board-view-pulse": "Pulse",
|
||||
"board-view-pulse": "Ukushaya kwenhliziyo",
|
||||
"board-view-not-yet-implemented": "Lo mbukiso awukakenziwa.",
|
||||
"bucket-example": "Njengokuthi \"Uhlu Lwebhakede\" ngokwesibonelo",
|
||||
"calendar-previous-month-label": "Inyanga edlule",
|
||||
|
||||
@@ -820,6 +820,20 @@ Boards.attachSchema(
|
||||
type: Boolean,
|
||||
defaultValue: false,
|
||||
},
|
||||
// Board-level override of the 3-tier Notification Settings system (see
|
||||
// models/lib/notificationSettings.js). Unset (optional, no default)
|
||||
// means "use the Admin Panel default"; an explicit true/false overrides
|
||||
// it for every member of this board unless a member also has their own
|
||||
// override, exactly like the admin-default -> board-override ->
|
||||
// member-override precedence used elsewhere.
|
||||
notifyOverrideTray: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
notifyOverrideEmail: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
allowsCardNumberOnMinicard: {
|
||||
/**
|
||||
* Does the board allows card numbers on minicard?
|
||||
@@ -2307,6 +2321,24 @@ Boards.helpers({
|
||||
return await Boards.updateAsync(this._id, modifier);
|
||||
},
|
||||
|
||||
// Board-level override of the 3-tier Notification Settings system (see
|
||||
// models/lib/notificationSettings.js): admin default -> board override ->
|
||||
// member override. `service` is 'tray' or 'email'; `value` is true/false to
|
||||
// override, or null/undefined to clear the override and fall back to the
|
||||
// Admin Panel default.
|
||||
async setNotifyOverride(service, value) {
|
||||
const currentUser = await ReactiveCache.getCurrentUser();
|
||||
if (!(currentUser.isBoardAdmin() || currentUser.isAdmin())) return false;
|
||||
const field = service === 'email' ? 'notifyOverrideEmail'
|
||||
: service === 'tray' ? 'notifyOverrideTray'
|
||||
: null;
|
||||
if (!field) return false;
|
||||
const modifier = value === true || value === false
|
||||
? { $set: { [field]: value } }
|
||||
: { $unset: { [field]: '' } };
|
||||
return await Boards.updateAsync(this._id, modifier);
|
||||
},
|
||||
|
||||
async setBackgroundImageURL(backgroundImageURL) {
|
||||
const currentUser = await ReactiveCache.getCurrentUser();
|
||||
if (currentUser.isBoardAdmin() || currentUser.isAdmin()) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// 3-tier Notification Settings resolution.
|
||||
//
|
||||
// Precedence (highest wins), mirroring the theme-override pattern used
|
||||
// elsewhere in WeKan (admin default -> board override -> member override):
|
||||
// 1) Member Settings override (profile.notifyOverride.<service>)
|
||||
// 2) Board Settings override (board.notifyOverride.<service>)
|
||||
// 3) Admin Panel default (Settings.notifyDefault<Service>)
|
||||
// 4) hardcoded fallback (true) — matches the current behavior when
|
||||
// nothing has ever been configured anywhere.
|
||||
//
|
||||
// A tri-state value is represented as `true` / `false` / `undefined` (or
|
||||
// `null`, treated the same as `undefined`) at each level: only an explicit
|
||||
// boolean counts as an override, an unset level falls through to the next.
|
||||
//
|
||||
// This module is intentionally pure (no Meteor/Mongo imports) so it can be
|
||||
// unit-tested with plain Node and reused identically on the client and the
|
||||
// server.
|
||||
|
||||
// The current notification-type catalog. Kept in sync with the services
|
||||
// registered via Notifications.subscribe() in server/notifications/*.js:
|
||||
// 'profile' == the in-app notification tray, 'email' == outgoing email
|
||||
// notifications.
|
||||
const NOTIFICATION_SERVICES = {
|
||||
tray: 'profile',
|
||||
email: 'email',
|
||||
};
|
||||
|
||||
const HARDCODED_FALLBACK = true;
|
||||
|
||||
function normalize(value) {
|
||||
return value === true || value === false ? value : undefined;
|
||||
}
|
||||
|
||||
// resolveNotificationSetting(service, { adminDefault, boardOverride, memberOverride })
|
||||
// -> Boolean
|
||||
//
|
||||
// `service` is one of the NOTIFICATION_SERVICES keys ('tray', 'email'), used
|
||||
// only for documentation/validation purposes here — the three override
|
||||
// values are already resolved to this service by the caller.
|
||||
function resolveNotificationSetting(
|
||||
service,
|
||||
{ adminDefault, boardOverride, memberOverride } = {},
|
||||
) {
|
||||
const member = normalize(memberOverride);
|
||||
if (member !== undefined) return member;
|
||||
|
||||
const board = normalize(boardOverride);
|
||||
if (board !== undefined) return board;
|
||||
|
||||
const admin = normalize(adminDefault);
|
||||
if (admin !== undefined) return admin;
|
||||
|
||||
return HARDCODED_FALLBACK;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
NOTIFICATION_SERVICES,
|
||||
HARDCODED_FALLBACK,
|
||||
resolveNotificationSetting,
|
||||
};
|
||||
@@ -402,6 +402,50 @@ Settings.attachSchema(
|
||||
optional: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
// Admin-level default for the 3-tier Notification Settings system (see
|
||||
// models/lib/notificationSettings.js): the base decision used when neither a
|
||||
// board nor a member has overridden a given notification service. Follows
|
||||
// the same nullable-override precedence used by the board/member allowsX
|
||||
// toggles elsewhere in this schema — see resolveNotificationSetting().
|
||||
notifyDefaultTray: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
defaultValue: true,
|
||||
},
|
||||
notifyDefaultEmail: {
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
defaultValue: true,
|
||||
},
|
||||
// #2022: admin-customizable templates for WeKan's transactional emails.
|
||||
// Each is OPTIONAL and unset by default, so an install that has never
|
||||
// touched Admin Panel -> Email Templates sends the exact hardcoded/i18n
|
||||
// content it always has (server/models/settings.js's sendInvitationEmail,
|
||||
// server/notifications/email.js's activity-notification buffer). Only
|
||||
// when an admin fills one of these in is it used, with
|
||||
// models/lib/ruleVarsSubstitute.js's substituteVars() - the same
|
||||
// `{token}` substitution the #3304 rule "send email" action uses -
|
||||
// filling in the tokens documented next to each field below. Deliberately
|
||||
// NOT included: password-reset / account-verification emails, which stay
|
||||
// hardcoded (see docs/Security note in server/models/settings.js).
|
||||
// Tokens: {email} {inviter} {user} {icode} {url}
|
||||
inviteEmailSubjectTemplate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
inviteEmailBodyTemplate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
// Tokens: {board} {card} {list} {username} {url} {comment} {action}
|
||||
activityEmailSubjectTemplate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
activityEmailBodyTemplate: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
supportTitle: {
|
||||
type: String,
|
||||
optional: true,
|
||||
|
||||
+124
-1
@@ -323,6 +323,23 @@ Users.attachSchema(
|
||||
'profile.emailBuffer.$': {
|
||||
type: String,
|
||||
},
|
||||
'profile.notifyOverrideTray': {
|
||||
/**
|
||||
* Member-level override of the in-app notification tray (3-tier
|
||||
* Notification Settings system, see models/lib/notificationSettings.js).
|
||||
* Unset means "use the board override, or the Admin Panel default".
|
||||
*/
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
'profile.notifyOverrideEmail': {
|
||||
/**
|
||||
* Member-level override of email notifications (see
|
||||
* models/lib/notificationSettings.js).
|
||||
*/
|
||||
type: Boolean,
|
||||
optional: true,
|
||||
},
|
||||
'profile.fullname': {
|
||||
/**
|
||||
* full name of the user
|
||||
@@ -684,6 +701,36 @@ Users.attachSchema(
|
||||
'profile.starredBoards.$': {
|
||||
type: String,
|
||||
},
|
||||
'profile.starredSwimlanes': {
|
||||
/**
|
||||
* list of starred swimlane IDs (#1172)
|
||||
*/
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'profile.starredSwimlanes.$': {
|
||||
type: String,
|
||||
},
|
||||
'profile.starredLists': {
|
||||
/**
|
||||
* list of starred list IDs (#1172)
|
||||
*/
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'profile.starredLists.$': {
|
||||
type: String,
|
||||
},
|
||||
'profile.starredCards': {
|
||||
/**
|
||||
* list of starred card IDs (#1172)
|
||||
*/
|
||||
type: Array,
|
||||
optional: true,
|
||||
},
|
||||
'profile.starredCards.$': {
|
||||
type: String,
|
||||
},
|
||||
'profile.starredPages': {
|
||||
/**
|
||||
* the starred PAGES - bookmarks. A board is starred by id; a page has no
|
||||
@@ -774,6 +821,7 @@ Users.attachSchema(
|
||||
'board-view-lead-time',
|
||||
'board-view-throughput-histogram',
|
||||
'board-view-wip-run',
|
||||
'board-view-pulse',
|
||||
],
|
||||
},
|
||||
'profile.listSortBy': {
|
||||
@@ -1476,6 +1524,42 @@ Users.helpers({
|
||||
return Boards.userBoards(this._id, false, { _id: { $in: starredBoards } }, {});
|
||||
},
|
||||
|
||||
// #1172: the same per-user id-array shape as starredBoards, generalized to
|
||||
// swimlanes, lists and cards so any of the four can be starred and reached
|
||||
// from the "Starred" page and the header bookmarks dropdown.
|
||||
starredSwimlanes() {
|
||||
const { starredSwimlanes = [] } = this.profile || {};
|
||||
if (!starredSwimlanes.length) return [];
|
||||
return ReactiveCache.getSwimlanes({ _id: { $in: starredSwimlanes } });
|
||||
},
|
||||
|
||||
starredLists() {
|
||||
const { starredLists = [] } = this.profile || {};
|
||||
if (!starredLists.length) return [];
|
||||
return ReactiveCache.getLists({ _id: { $in: starredLists } });
|
||||
},
|
||||
|
||||
starredCards() {
|
||||
const { starredCards = [] } = this.profile || {};
|
||||
if (!starredCards.length) return [];
|
||||
return ReactiveCache.getCards({ _id: { $in: starredCards } });
|
||||
},
|
||||
|
||||
hasStarredSwimlane(swimlaneId) {
|
||||
const { starredSwimlanes = [] } = this.profile || {};
|
||||
return starredSwimlanes.includes(swimlaneId);
|
||||
},
|
||||
|
||||
hasStarredList(listId) {
|
||||
const { starredLists = [] } = this.profile || {};
|
||||
return starredLists.includes(listId);
|
||||
},
|
||||
|
||||
hasStarredCard(cardId) {
|
||||
const { starredCards = [] } = this.profile || {};
|
||||
return starredCards.includes(cardId);
|
||||
},
|
||||
|
||||
// The starred PAGES - the bookmarks. Boards are starred by id; a page has no
|
||||
// id, so it is stored as the pair a bookmark is: where it goes and what to
|
||||
// call it. docs/Features/Board/Starred.md
|
||||
@@ -1493,7 +1577,13 @@ Users.helpers({
|
||||
// places you keep. A count that left the pages out would say 2 above a
|
||||
// dropdown showing five rows.
|
||||
starredCount() {
|
||||
return this.starredBoards().length + this.starredPages().length;
|
||||
return (
|
||||
this.starredBoards().length +
|
||||
this.starredPages().length +
|
||||
this.starredSwimlanes().length +
|
||||
this.starredLists().length +
|
||||
this.starredCards().length
|
||||
);
|
||||
},
|
||||
|
||||
hasStarred(boardId) {
|
||||
@@ -2333,6 +2423,23 @@ Users.helpers({
|
||||
return await Users.updateAsync(this._id, { [queryKind]: { 'profile.starredBoards': boardId } });
|
||||
},
|
||||
|
||||
// #1172: same toggle pattern as toggleBoardStar, targeting the other three
|
||||
// id-array fields.
|
||||
async toggleSwimlaneStar(swimlaneId) {
|
||||
const queryKind = this.hasStarredSwimlane(swimlaneId) ? '$pull' : '$addToSet';
|
||||
return await Users.updateAsync(this._id, { [queryKind]: { 'profile.starredSwimlanes': swimlaneId } });
|
||||
},
|
||||
|
||||
async toggleListStar(listId) {
|
||||
const queryKind = this.hasStarredList(listId) ? '$pull' : '$addToSet';
|
||||
return await Users.updateAsync(this._id, { [queryKind]: { 'profile.starredLists': listId } });
|
||||
},
|
||||
|
||||
async toggleCardStar(cardId) {
|
||||
const queryKind = this.hasStarredCard(cardId) ? '$pull' : '$addToSet';
|
||||
return await Users.updateAsync(this._id, { [queryKind]: { 'profile.starredCards': cardId } });
|
||||
},
|
||||
|
||||
// #2220: toggle this board as the user's default "home" board (opened after
|
||||
// login). Clicking the current default clears it (back to the All Boards page).
|
||||
async toggleDefaultBoard(boardId) {
|
||||
@@ -2536,6 +2643,22 @@ Users.helpers({
|
||||
return await Users.updateAsync(this._id, { $set: { 'profile.avatarUrl': avatarUrl } });
|
||||
},
|
||||
|
||||
// Member-level override of the 3-tier Notification Settings system (see
|
||||
// models/lib/notificationSettings.js): admin default -> board override ->
|
||||
// member override, the same precedence the board/member theme override
|
||||
// uses. `service` is 'tray' or 'email'; `value` is true/false to override,
|
||||
// or null/undefined to clear it and fall back to the board/admin default.
|
||||
async setNotifyOverride(service, value) {
|
||||
const field = service === 'email' ? 'profile.notifyOverrideEmail'
|
||||
: service === 'tray' ? 'profile.notifyOverrideTray'
|
||||
: null;
|
||||
if (!field) return false;
|
||||
const modifier = value === true || value === false
|
||||
? { $set: { [field]: value } }
|
||||
: { $unset: { [field]: '' } };
|
||||
return await Users.updateAsync(this._id, modifier);
|
||||
},
|
||||
|
||||
async setShowCardsCountAt(limit) {
|
||||
return await Users.updateAsync(this._id, { $set: { 'profile.showCardsCountAt': limit } });
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ const {
|
||||
buildReinviteModifier,
|
||||
shouldRemoveInvitationOnEmailFailure,
|
||||
} = require('/models/lib/invitationCodeEmail');
|
||||
const { substituteVars } = require('/models/lib/ruleVarsSubstitute');
|
||||
|
||||
const getReactiveCache = () => require('/imports/reactiveCache').ReactiveCache;
|
||||
const getTAPi18n = () => require('/imports/i18n').TAPi18n;
|
||||
@@ -91,14 +92,40 @@ async function sendInvitationEmail(_id, { isNewInvitation = true } = {}) {
|
||||
url: Meteor.absoluteUrl('sign-up'),
|
||||
};
|
||||
const lang = author.getLanguage();
|
||||
await EmailLocalization.sendEmail({
|
||||
to: icode.email,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: 'email-invite-register-subject',
|
||||
text: 'email-invite-register-text',
|
||||
params,
|
||||
language: lang,
|
||||
});
|
||||
// #2022: an admin-customized invite template (Admin Panel -> Email
|
||||
// Templates) overrides the hardcoded i18n subject/text, using the same
|
||||
// {token} substitution the #3304 rule "send email" action uses
|
||||
// (substituteVars). Unset (the default on every existing install) falls
|
||||
// through to the exact i18n-driven content below, unchanged.
|
||||
const setting = await getReactiveCache().getCurrentSetting();
|
||||
const templateVars = {
|
||||
email: params.email,
|
||||
inviter: params.inviter,
|
||||
user: params.user,
|
||||
icode: params.icode,
|
||||
url: params.url,
|
||||
};
|
||||
if (setting && setting.inviteEmailSubjectTemplate) {
|
||||
// Sent directly (not through EmailLocalization.sendEmail) because the
|
||||
// subject/text here are already-substituted plain text, not i18n keys
|
||||
// - passing them through TAPi18n.__() would treat the custom text
|
||||
// itself as a translation key to look up.
|
||||
await Email.sendAsync({
|
||||
to: icode.email,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: substituteVars(setting.inviteEmailSubjectTemplate, templateVars),
|
||||
text: substituteVars(setting.inviteEmailBodyTemplate || '', templateVars),
|
||||
});
|
||||
} else {
|
||||
await EmailLocalization.sendEmail({
|
||||
to: icode.email,
|
||||
from: Accounts.emailTemplates.from,
|
||||
subject: 'email-invite-register-subject',
|
||||
text: 'email-invite-register-text',
|
||||
params,
|
||||
language: lang,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// #4043: only roll back a code created by this very invite. A pre-existing
|
||||
// invitation was already delivered in an earlier email; deleting it here
|
||||
@@ -368,6 +395,29 @@ Meteor.methods({
|
||||
}
|
||||
},
|
||||
|
||||
// Admin-level default of the 3-tier Notification Settings system (see
|
||||
// models/lib/notificationSettings.js). `service` is 'tray' or 'email';
|
||||
// `enabled` is the admin default for it. Board and member overrides are set
|
||||
// through their own methods (setBoardNotifyOverride in models/boards.js,
|
||||
// setMemberNotifyOverride in models/users.js) and win over this default.
|
||||
async setAdminNotifyDefault(service, enabled) {
|
||||
check(service, String);
|
||||
check(enabled, Boolean);
|
||||
const user = await Meteor.userAsync();
|
||||
if (user?.isAdmin !== true) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
const field = service === 'email' ? 'notifyDefaultEmail'
|
||||
: service === 'tray' ? 'notifyDefaultTray'
|
||||
: null;
|
||||
if (!field) throw new Meteor.Error('invalid-service');
|
||||
|
||||
const setting = await Settings.findOneAsync({});
|
||||
if (!setting) throw new Meteor.Error('settings-not-found');
|
||||
await Settings.updateAsync(setting._id, { $set: { [field]: enabled } });
|
||||
return enabled;
|
||||
},
|
||||
|
||||
async sendInvitation(emails, boards) {
|
||||
let rc = 0;
|
||||
check(emails, [String]);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Notifications } from '/server/notifications/notifications';
|
||||
import Users from '/models/users';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import { resolveNotificationSetting } from '/models/lib/notificationSettings';
|
||||
|
||||
Meteor.startup(() => {
|
||||
Notifications.subscribe('profile', async (user, title, description, params) => {
|
||||
@@ -9,6 +11,20 @@ Meteor.startup(() => {
|
||||
console.error('Invalid user object in notification:', { user, title, params });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3-tier Notification Settings: admin default -> board override ->
|
||||
// member override (see models/lib/notificationSettings.js).
|
||||
const setting = await ReactiveCache.getCurrentSetting();
|
||||
const board = params.boardId
|
||||
? await ReactiveCache.getBoard(params.boardId)
|
||||
: null;
|
||||
const enabled = resolveNotificationSetting('tray', {
|
||||
adminDefault: setting && setting.notifyDefaultTray,
|
||||
boardOverride: board && board.notifyOverrideTray,
|
||||
memberOverride: user.profile && user.profile.notifyOverrideTray,
|
||||
});
|
||||
if (!enabled) return;
|
||||
|
||||
const modifier = user.addNotification(params.activityId);
|
||||
await Users.direct.updateAsync(user._id, modifier);
|
||||
} catch (error) {
|
||||
|
||||
@@ -43,6 +43,9 @@ const SETTING_FIELDS = {
|
||||
disableActivities: 1,
|
||||
disableNotifications: 1,
|
||||
disableWatch: 1,
|
||||
// 3-tier Notification Settings admin defaults (models/lib/notificationSettings.js).
|
||||
notifyDefaultTray: 1,
|
||||
notifyDefaultEmail: 1,
|
||||
// Admin Panel / Problems / Delete reads this field back after writing it.
|
||||
// Without publishing it, the optimistic checkmark is immediately redrawn from
|
||||
// `undefined` even though the server saved the new value.
|
||||
@@ -79,6 +82,14 @@ const SETTING_FIELDS = {
|
||||
spinnerName: 1,
|
||||
oidcBtnText: 1,
|
||||
mailDomainName: 1,
|
||||
// #2022: admin-customizable transactional-email templates (Admin Panel ->
|
||||
// Email -> Email Templates). Unset is the default and means "use the
|
||||
// current hardcoded/i18n content" - see server/models/settings.js and
|
||||
// server/notifications/email.js.
|
||||
inviteEmailSubjectTemplate: 1,
|
||||
inviteEmailBodyTemplate: 1,
|
||||
activityEmailSubjectTemplate: 1,
|
||||
activityEmailBodyTemplate: 1,
|
||||
legalNotice: 1,
|
||||
customHeadEnabled: 1,
|
||||
customHeadMetaTags: 1,
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
'use strict';
|
||||
|
||||
// Regression coverage for the 3-tier Notification Settings resolution
|
||||
// (models/lib/notificationSettings.js), added as part of "Add big popup of
|
||||
// Notification Settings as menu option to Member Settings, Board Settings
|
||||
// and Admin Panel / People".
|
||||
//
|
||||
// Precedence, highest wins: member override > board override > admin
|
||||
// default > hardcoded fallback (true) - the same shape as the admin-default
|
||||
// -> board-override -> member-override "theme override" pattern used
|
||||
// elsewhere in WeKan.
|
||||
//
|
||||
// Run: node tests/notificationSettingsResolution.test.cjs
|
||||
|
||||
const assert = require('assert');
|
||||
const {
|
||||
resolveNotificationSetting,
|
||||
HARDCODED_FALLBACK,
|
||||
NOTIFICATION_SERVICES,
|
||||
} = require('../models/lib/notificationSettings.js');
|
||||
|
||||
let passed = 0;
|
||||
function test(name, fn) {
|
||||
fn();
|
||||
passed += 1;
|
||||
console.log(' ok -', name);
|
||||
}
|
||||
|
||||
console.log('notificationSettingsResolution:');
|
||||
|
||||
test('catalog has exactly tray and email, matching the registered services', () => {
|
||||
assert.deepStrictEqual(Object.keys(NOTIFICATION_SERVICES).sort(), [
|
||||
'email',
|
||||
'tray',
|
||||
]);
|
||||
assert.strictEqual(NOTIFICATION_SERVICES.tray, 'profile');
|
||||
assert.strictEqual(NOTIFICATION_SERVICES.email, 'email');
|
||||
});
|
||||
|
||||
test('nothing configured anywhere falls back to the hardcoded default (true)', () => {
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('tray', {}),
|
||||
HARDCODED_FALLBACK,
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('email', {
|
||||
adminDefault: undefined,
|
||||
boardOverride: undefined,
|
||||
memberOverride: undefined,
|
||||
}),
|
||||
HARDCODED_FALLBACK,
|
||||
);
|
||||
});
|
||||
|
||||
test('null is treated the same as undefined at every level', () => {
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('tray', {
|
||||
adminDefault: null,
|
||||
boardOverride: null,
|
||||
memberOverride: null,
|
||||
}),
|
||||
HARDCODED_FALLBACK,
|
||||
);
|
||||
});
|
||||
|
||||
test('admin default alone decides when board and member are unset', () => {
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('tray', { adminDefault: false }),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('tray', { adminDefault: true }),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('board override wins over the admin default', () => {
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('email', {
|
||||
adminDefault: true,
|
||||
boardOverride: false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('email', {
|
||||
adminDefault: false,
|
||||
boardOverride: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('member override wins over both the board override and the admin default', () => {
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('tray', {
|
||||
adminDefault: true,
|
||||
boardOverride: true,
|
||||
memberOverride: false,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('tray', {
|
||||
adminDefault: false,
|
||||
boardOverride: false,
|
||||
memberOverride: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('an explicit false at a lower level is not treated as "unset"', () => {
|
||||
// admin explicitly disabled, board says nothing -> stays disabled
|
||||
assert.strictEqual(
|
||||
resolveNotificationSetting('email', {
|
||||
adminDefault: false,
|
||||
boardOverride: undefined,
|
||||
memberOverride: undefined,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('tray and email resolve independently of each other', () => {
|
||||
const trayEnabled = resolveNotificationSetting('tray', {
|
||||
adminDefault: true,
|
||||
boardOverride: undefined,
|
||||
memberOverride: false,
|
||||
});
|
||||
const emailEnabled = resolveNotificationSetting('email', {
|
||||
adminDefault: true,
|
||||
boardOverride: undefined,
|
||||
memberOverride: undefined,
|
||||
});
|
||||
assert.strictEqual(trayEnabled, false);
|
||||
assert.strictEqual(emailEnabled, true);
|
||||
});
|
||||
|
||||
console.log(`notificationSettingsResolution: ${passed} passed`);
|
||||
Reference in new issue
Block a user