Merge branch 'admin-reports' of https://github.com/jrsupplee/wekan into jrsupplee-admin-reports

This commit is contained in:
Lauri Ojansivu 2021-04-13 23:50:37 +03:00
commit decab9256b
35 changed files with 695 additions and 509 deletions

View file

@ -1,7 +1,7 @@
template(name="commentForm") template(name="commentForm")
.new-comment.js-new-comment( .new-comment.js-new-comment(
class="{{#if commentFormIsOpen}}is-open{{/if}}") class="{{#if commentFormIsOpen}}is-open{{/if}}")
+userAvatar(userId=currentUser._id) +userAvatar(userId=currentUser._id noRemove=true)
form.js-new-comment-form form.js-new-comment-form
+editor(class="js-new-comment-input") +editor(class="js-new-comment-input")
| {{getUnsavedValue 'cardComment' currentCard._id}} | {{getUnsavedValue 'cardComment' currentCard._id}}

View file

@ -1,3 +1,7 @@
import { DatePicker } from '/client/lib/datepicker';
import moment from 'moment';
import Cards from '/models/cards';
Template.cardCustomFieldsPopup.helpers({ Template.cardCustomFieldsPopup.helpers({
hasCustomField() { hasCustomField() {
const card = Cards.findOne(Session.get('currentCard')); const card = Cards.findOne(Session.get('currentCard'));
@ -286,8 +290,9 @@ CardCustomField.register('cardCustomField');
} else { } else {
event.target.blur(); event.target.blur();
const idx = Array.from(this.findAll('input')) const idx = Array.from(this.findAll('input')).indexOf(
.indexOf(event.target); event.target,
);
items.splice(idx + 1, 0, ''); items.splice(idx + 1, 0, '');
Tracker.afterFlush(() => { Tracker.afterFlush(() => {
@ -303,7 +308,10 @@ CardCustomField.register('cardCustomField');
}, },
'blur .js-card-customfield-stringtemplate-item'(event) { 'blur .js-card-customfield-stringtemplate-item'(event) {
if (!event.target.value.trim() || event.target === this.find('input.last')) { if (
!event.target.value.trim() ||
event.target === this.find('input.last')
) {
const items = this.getItems(); const items = this.getItems();
this.stringtemplateItems.set(items); this.stringtemplateItems.set(items);
this.find('input.last').value = ''; this.find('input.last').value = '';

View file

@ -1,122 +1,5 @@
// Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours import moment from 'moment';
function adjustedTimeFormat() { import { DatePicker } from '/client/lib/datepicker';
return moment
.localeData()
.longDateFormat('LT')
.replace(/HH/i, 'H');
}
// Edit received, start, due & end dates
BlazeComponent.extendComponent({
template() {
return 'editCardDate';
},
onCreated() {
this.error = new ReactiveVar('');
this.card = this.data();
this.date = new ReactiveVar(moment.invalid());
},
onRendered() {
const $picker = this.$('.js-datepicker')
.datepicker({
todayHighlight: true,
todayBtn: 'linked',
language: TAPi18n.getLanguage(),
})
.on(
'changeDate',
function(evt) {
this.find('#date').value = moment(evt.date).format('L');
this.error.set('');
this.find('#time').focus();
}.bind(this),
);
if (this.date.get().isValid()) {
$picker.datepicker('update', this.date.get().toDate());
}
},
showDate() {
if (this.date.get().isValid()) return this.date.get().format('L');
return '';
},
showTime() {
if (this.date.get().isValid()) return this.date.get().format('LT');
return '';
},
dateFormat() {
return moment.localeData().longDateFormat('L');
},
timeFormat() {
return moment.localeData().longDateFormat('LT');
},
events() {
return [
{
'keyup .js-date-field'() {
// parse for localized date format in strict mode
const dateMoment = moment(this.find('#date').value, 'L', true);
if (dateMoment.isValid()) {
this.error.set('');
this.$('.js-datepicker').datepicker('update', dateMoment.toDate());
}
},
'keyup .js-time-field'() {
// parse for localized time format in strict mode
const dateMoment = moment(
this.find('#time').value,
adjustedTimeFormat(),
true,
);
if (dateMoment.isValid()) {
this.error.set('');
}
},
'submit .edit-date'(evt) {
evt.preventDefault();
// if no time was given, init with 12:00
const time =
evt.target.time.value ||
moment(new Date().setHours(12, 0, 0)).format('LT');
const newTime = moment(time, adjustedTimeFormat(), true);
const newDate = moment(evt.target.date.value, 'L', true);
const dateString = `${evt.target.date.value} ${time}`;
const newCompleteDate = moment(
dateString,
'L ' + adjustedTimeFormat(),
true,
);
if (!newTime.isValid()) {
this.error.set('invalid-time');
evt.target.time.focus();
}
if (!newDate.isValid()) {
this.error.set('invalid-date');
evt.target.date.focus();
}
if (newCompleteDate.isValid()) {
this._storeDate(newCompleteDate.toDate());
Popup.close();
} else {
if (!this.error) {
this.error.set('invalid');
}
}
},
'click .js-delete-date'(evt) {
evt.preventDefault();
this._deleteDate();
Popup.close();
},
},
];
},
});
Template.dateBadge.helpers({ Template.dateBadge.helpers({
canModifyCard() { canModifyCard() {

View file

@ -110,15 +110,24 @@ template(name="cardDetails")
a.card-label.add-label.js-end-date a.card-label.add-label.js-end-date
i.fa.fa-plus i.fa.fa-plus
hr
if currentBoard.allowsCreator
.card-details-item.card-details-item-creator
h3.card-details-item-title
i.fa.fa-user
| {{_ 'creator'}}
+userAvatar(userId=userId noRemove=true)
| {{! XXX Hack to hide syntaxic coloration /// }}
//.card-details-items //.card-details-items
if currentBoard.allowsMembers if currentBoard.allowsMembers
hr
.card-details-item.card-details-item-members .card-details-item.card-details-item-members
h3.card-details-item-title h3.card-details-item-title
i.fa.fa-users i.fa.fa-users
| {{_ 'members'}} | {{_ 'members'}}
each getMembers each userId in getMembers
+userAvatar(userId=this cardId=../_id) +userAvatar(userId=userId cardId=_id)
| {{! XXX Hack to hide syntaxic coloration /// }} | {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard if canModifyCard
unless currentUser.isWorker unless currentUser.isWorker
@ -131,8 +140,8 @@ template(name="cardDetails")
h3.card-details-item-title h3.card-details-item-title
i.fa.fa-user i.fa.fa-user
| {{_ 'assignee'}} | {{_ 'assignee'}}
each getAssignees each userId in getAssignees
+userAvatarAssignee(userId=this cardId=../_id) +userAvatar(userId=userId cardId=_id assignee=true)
| {{! XXX Hack to hide syntaxic coloration /// }} | {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard if canModifyCard
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}") a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
@ -488,23 +497,6 @@ template(name="cardAssigneesPopup")
if currentUser.isCardAssignee if currentUser.isCardAssignee
i.fa.fa-check i.fa.fa-check
template(name="userAvatarAssignee")
a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
if userData.profile.avatarUrl
img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}")
else
+userAvatarAssigneeInitials(userId=userData._id)
if showStatus
span.assignee-presence-status(class=presenceStatusClassName)
span.member-type(class=memberType)
unless isSandstorm
if showEdit
if $eq currentUser._id userData._id
a.edit-avatar.js-change-avatar
i.fa.fa-pencil
template(name="cardAssigneePopup") template(name="cardAssigneePopup")
.board-assignee-menu .board-assignee-menu
.mini-profile-info .mini-profile-info
@ -522,10 +514,6 @@ template(name="cardAssigneePopup")
with currentUser with currentUser
li: a.js-edit-profile {{_ 'edit-profile'}} li: a.js-edit-profile {{_ 'edit-profile'}}
template(name="userAvatarAssigneeInitials")
svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15")
text(x="50%" y="13" text-anchor="middle")= initials
template(name="cardMorePopup") template(name="cardMorePopup")
p.quiet p.quiet
span.clearfix span.clearfix

View file

@ -1,11 +1,18 @@
import { DatePicker } from '/client/lib/datepicker';
import Cards from '/models/cards';
import Boards from '/models/boards';
import Checklists from '/models/checklists';
import Integrations from '/models/integrations';
import Users from '/models/users';
import Lists from '/models/lists';
import CardComments from '/models/cardComments';
import { ALLOWED_COLORS } from '/config/const';
import moment from 'moment';
import { UserAvatar } from '../users/userAvatar';
const subManager = new SubsManager(); const subManager = new SubsManager();
const { calculateIndexData } = Utils; const { calculateIndexData } = Utils;
let cardColors;
Meteor.startup(() => {
cardColors = Cards.simpleSchema()._schema.color.allowedValues;
});
BlazeComponent.extendComponent({ BlazeComponent.extendComponent({
mixins() { mixins() {
return [Mixins.InfiniteScrolling]; return [Mixins.InfiniteScrolling];
@ -160,9 +167,7 @@ BlazeComponent.extendComponent({
integration, integration,
'CardSelected', 'CardSelected',
params, params,
() => { () => {},
return;
},
); );
}); });
} }
@ -290,7 +295,7 @@ BlazeComponent.extendComponent({
Utils.goBoardId(this.data().boardId); Utils.goBoardId(this.data().boardId);
}, },
'click .js-copy-link'() { 'click .js-copy-link'() {
StringToCopyElement = document.getElementById('cardURL_copy'); const StringToCopyElement = document.getElementById('cardURL_copy');
StringToCopyElement.value = StringToCopyElement.value =
window.location.origin + window.location.pathname; window.location.origin + window.location.pathname;
StringToCopyElement.select(); StringToCopyElement.select();
@ -407,122 +412,6 @@ BlazeComponent.extendComponent({
}, },
}).register('cardDetails'); }).register('cardDetails');
Template.cardDetails.helpers({
userData() {
// We need to handle a special case for the search results provided by the
// `matteodem:easy-search` package. Since these results gets published in a
// separate collection, and not in the standard Meteor.Users collection as
// expected, we use a component parameter ("property") to distinguish the
// two cases.
const userCollection = this.esSearch ? ESSearchResults : Users;
return userCollection.findOne(this.userId, {
fields: {
profile: 1,
username: 1,
},
});
},
receivedSelected() {
if (this.getReceived().length === 0) {
return false;
} else {
return true;
}
},
startSelected() {
if (this.getStart().length === 0) {
return false;
} else {
return true;
}
},
endSelected() {
if (this.getEnd().length === 0) {
return false;
} else {
return true;
}
},
dueSelected() {
if (this.getDue().length === 0) {
return false;
} else {
return true;
}
},
memberSelected() {
if (this.getMembers().length === 0) {
return false;
} else {
return true;
}
},
labelSelected() {
if (this.getLabels().length === 0) {
return false;
} else {
return true;
}
},
assigneeSelected() {
if (this.getAssignees().length === 0) {
return false;
} else {
return true;
}
},
requestBySelected() {
if (this.getRequestBy().length === 0) {
return false;
} else {
return true;
}
},
assigneeBySelected() {
if (this.getAssigneeBy().length === 0) {
return false;
} else {
return true;
}
},
memberType() {
const user = Users.findOne(this.userId);
return user && user.isBoardAdmin() ? 'admin' : 'normal';
},
presenceStatusClassName() {
const user = Users.findOne(this.userId);
const userPresence = presences.findOne({ userId: this.userId });
if (user && user.isInvitedTo(Session.get('currentBoard'))) return 'pending';
else if (!userPresence) return 'disconnected';
else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
return 'active';
else return 'idle';
},
});
Template.userAvatarAssigneeInitials.helpers({
initials() {
const user = Users.findOne(this.userId);
return user && user.getInitials();
},
viewPortWidth() {
const user = Users.findOne(this.userId);
return ((user && user.getInitials().length) || 1) * 12;
},
});
// We extends the normal InlinedForm component to support UnsavedEdits draft // We extends the normal InlinedForm component to support UnsavedEdits draft
// feature. // feature.
(class extends InlinedForm { (class extends InlinedForm {
@ -697,7 +586,7 @@ BlazeComponent.extendComponent({
}, },
boards() { boards() {
const boards = Boards.find( return Boards.find(
{ {
archived: false, archived: false,
'members.userId': Meteor.userId(), 'members.userId': Meteor.userId(),
@ -707,7 +596,6 @@ BlazeComponent.extendComponent({
sort: { sort: 1 /* boards default sorting */ }, sort: { sort: 1 /* boards default sorting */ },
}, },
); );
return boards;
}, },
swimlanes() { swimlanes() {
@ -736,7 +624,7 @@ Template.copyCardPopup.events({
'click .js-done'() { 'click .js-done'() {
const card = Cards.findOne(Session.get('currentCard')); const card = Cards.findOne(Session.get('currentCard'));
const lSelect = $('.js-select-lists')[0]; const lSelect = $('.js-select-lists')[0];
listId = lSelect.options[lSelect.selectedIndex].value; const listId = lSelect.options[lSelect.selectedIndex].value;
const slSelect = $('.js-select-swimlanes')[0]; const slSelect = $('.js-select-swimlanes')[0];
const swimlaneId = slSelect.options[slSelect.selectedIndex].value; const swimlaneId = slSelect.options[slSelect.selectedIndex].value;
const bSelect = $('.js-select-boards')[0]; const bSelect = $('.js-select-boards')[0];
@ -801,7 +689,7 @@ Template.copyChecklistToManyCardsPopup.events({
}); });
// copy subtasks // copy subtasks
cursor = Cards.find({ parentId: oldId }); const cursor = Cards.find({ parentId: oldId });
cursor.forEach(function() { cursor.forEach(function() {
'use strict'; 'use strict';
const subtask = arguments[0]; const subtask = arguments[0];
@ -827,7 +715,7 @@ BlazeComponent.extendComponent({
}, },
colors() { colors() {
return cardColors.map(color => ({ color, name: '' })); return ALLOWED_COLORS.map(color => ({ color, name: '' }));
}, },
isSelected(color) { isSelected(color) {
@ -871,7 +759,7 @@ BlazeComponent.extendComponent({
}, },
boards() { boards() {
const boards = Boards.find( return Boards.find(
{ {
archived: false, archived: false,
'members.userId': Meteor.userId(), 'members.userId': Meteor.userId(),
@ -883,7 +771,6 @@ BlazeComponent.extendComponent({
sort: { sort: 1 /* boards default sorting */ }, sort: { sort: 1 /* boards default sorting */ },
}, },
); );
return boards;
}, },
cards() { cards() {

View file

@ -211,6 +211,7 @@ avatar-radius = 50%
word-wrap: break-word word-wrap: break-word
max-width: 36% max-width: 36%
flex-grow: 1 flex-grow: 1
&.card-details-item-creator,
&.card-details-item-received, &.card-details-item-received,
&.card-details-item-start, &.card-details-item-start,
&.card-details-item-due, &.card-details-item-due,

View file

@ -99,6 +99,10 @@ template(name="minicard")
each getMembers each getMembers
+userAvatar(userId=this) +userAvatar(userId=this)
if showCreator
.minicard-creator
+userAvatar(userId=this.userId noRemove=true)
.badges .badges
unless currentUser.isNoComments unless currentUser.isNoComments
if comments.count if comments.count

View file

@ -31,10 +31,24 @@ BlazeComponent.extendComponent({
return customFieldTrueValue return customFieldTrueValue
.filter(value => !!value.trim()) .filter(value => !!value.trim())
.map(value => definition.settings.stringtemplateFormat.replace(/%\{value\}/gi, value)) .map(value =>
definition.settings.stringtemplateFormat.replace(/%\{value\}/gi, value),
)
.join(definition.settings.stringtemplateSeparator ?? ''); .join(definition.settings.stringtemplateSeparator ?? '');
}, },
showCreator() {
if (this.data().board()) {
return (
this.data().board.allowsCreator === null ||
this.data().board().allowsCreator === undefined ||
this.data().board().allowsCreator
);
// return this.data().board().allowsCreator;
}
return false;
},
events() { events() {
return [ return [
{ {

View file

@ -163,7 +163,8 @@
line-height: 12px line-height: 12px
.minicard-members, .minicard-members,
.minicard-assignees .minicard-assignees,
.minicard-creator
float: right float: right
margin-left: 5px margin-left: 5px
margin-bottom: 4px margin-bottom: 4px
@ -187,6 +188,9 @@
.minicard-assignees .minicard-assignees
border-bottom: 1px solid red border-bottom: 1px solid red
.minicard-creator
border-bottom: 1px solid green
.minicard-members:empty, .minicard-members:empty,
.minicard-assignees:empty .minicard-assignees:empty
display: none display: none

View file

@ -11,6 +11,7 @@ template(name="globalSearchModalTitle")
| {{_ 'globalSearch-title'}} | {{_ 'globalSearch-title'}}
template(name="resultsPaged") template(name="resultsPaged")
if resultsHeading.get
h1 h1
= resultsHeading.get = resultsHeading.get
a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}")
@ -50,7 +51,6 @@ template(name="globalSearch")
each msg in errorMessages each msg in errorMessages
li.global-search-error-messages li.global-search-error-messages
= msg = msg
else
+resultsPaged(this) +resultsPaged(this)
else if serverError.get else if serverError.get
.global-search-page .global-search-page

View file

@ -123,6 +123,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent {
operator_member_abbrev: TAPi18n.__('operator-member-abbrev'), operator_member_abbrev: TAPi18n.__('operator-member-abbrev'),
operator_assignee: TAPi18n.__('operator-assignee'), operator_assignee: TAPi18n.__('operator-assignee'),
operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'), operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'),
operator_creator: TAPi18n.__('operator-creator'),
operator_due: TAPi18n.__('operator-due'), operator_due: TAPi18n.__('operator-due'),
operator_created: TAPi18n.__('operator-created'), operator_created: TAPi18n.__('operator-created'),
operator_modified: TAPi18n.__('operator-modified'), operator_modified: TAPi18n.__('operator-modified'),
@ -167,6 +168,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent {
['\n* ', 'globalSearch-instructions-operator-at'], ['\n* ', 'globalSearch-instructions-operator-at'],
['\n* ', 'globalSearch-instructions-operator-member'], ['\n* ', 'globalSearch-instructions-operator-member'],
['\n* ', 'globalSearch-instructions-operator-assignee'], ['\n* ', 'globalSearch-instructions-operator-assignee'],
['\n* ', 'globalSearch-instructions-operator-creator'],
['\n* ', 'globalSearch-instructions-operator-due'], ['\n* ', 'globalSearch-instructions-operator-due'],
['\n* ', 'globalSearch-instructions-operator-created'], ['\n* ', 'globalSearch-instructions-operator-created'],
['\n* ', 'globalSearch-instructions-operator-modified'], ['\n* ', 'globalSearch-instructions-operator-modified'],
@ -202,9 +204,8 @@ class GlobalSearchComponent extends CardSearchPagedComponent {
} }
events() { events() {
return [ return super.events().concat([
{ {
...super.events()[0],
'submit .js-search-query-form'(evt) { 'submit .js-search-query-form'(evt) {
evt.preventDefault(); evt.preventDefault();
this.searchAllBoards(evt.target.searchQuery.value); this.searchAllBoards(evt.target.searchQuery.value);
@ -257,7 +258,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent {
this.hasResults.set(false); this.hasResults.set(false);
}, },
}, },
]; ]);
} }
} }

View file

@ -0,0 +1,111 @@
template(name="adminReports")
.setting-content
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
.content-body
.side-menu
ul
li
a.js-report-broken(data-id="report-broken")
i.fa.fa-chain-broken
| {{_ 'broken-cards'}}
li
a.js-report-files(data-id="report-orphaned-files")
i.fa.fa-paperclip
| {{_ 'orphanedFilesReportTitle'}}
li
a.js-report-files(data-id="report-files")
i.fa.fa-paperclip
| {{_ 'filesReportTitle'}}
li
a.js-report-rules(data-id="report-rules")
i.fa.fa-paperclip
| {{_ 'rulesReportTitle'}}
.main-body
if loading.get
+spinner
else if showBrokenCardsReport.get
+brokenCardsReport
else if showFilesReport.get
+filesReport
else if showOrphanedFilesReport.get
+orphanedFilesReport
else if showRulesReport.get
+rulesReport
template(name="brokenCardsReport")
.global-search-results-list-wrapper
h1 {{_ 'broken-cards'}}
if resultsCount
+resultsPaged(this)
else
div {{_ 'no-results' }}
template(name="rulesReport")
h1 {{_ 'rulesReportTitle'}}
if resultsCount
table.table
tr
th Rule Title
th Board Title
th actionType
th activityType
each rule in rows
tr
td {{ rule.title }}
td {{ rule.boardTitle }}
td {{ rule.action.actionType }}
td {{ rule.trigger.activityType }}
else
div {{_ 'no-results' }}
template(name="filesReport")
h1 {{_ 'filesReportTitle'}}
if resultsCount
table.table
tr
th Filename
th.right Size (kB)
th MIME Type
th.center Usage
th MD5 Sum
th ID
each att in attachmentFiles
tr
td {{ att.filename }}
td.right {{fileSize att.length }}
td {{ att.contentType }}
td.center {{usageCount att._id.toHexString }}
td {{ att.md5 }}
td {{ att._id.toHexString }}
else
div {{_ 'no-results' }}
template(name="orphanedFilesReport")
h1 {{_ 'orphanedFilesReportTitle'}}
if resultsCount
table.table
tr
th Filename
th.right Size (kB)
th MIME Type
th MD5 Sum
th ID
each att in attachmentFiles
tr
td {{ att.filename }}
td.right {{fileSize att.length }}
td {{ att.contentType }}
td {{ att.md5 }}
td {{ att._id.toHexString }}
else
div {{_ 'no-results' }}

View file

@ -0,0 +1,156 @@
import { AttachmentStorage } from '/models/attachments';
import { CardSearchPagedComponent } from '/client/lib/cardSearch';
import SessionData from '/models/usersessiondata';
BlazeComponent.extendComponent({
subscription: null,
showFilesReport: new ReactiveVar(false),
showBrokenCardsReport: new ReactiveVar(false),
showOrphanedFilesReport: new ReactiveVar(false),
showRulesReport: new ReactiveVar(false),
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
},
events() {
return [
{
'click a.js-report-broken': this.switchMenu,
'click a.js-report-files': this.switchMenu,
'click a.js-report-orphaned-files': this.switchMenu,
'click a.js-report-rules': this.switchMenu,
},
];
},
switchMenu(event) {
const target = $(event.target);
if (!target.hasClass('active')) {
this.loading.set(true);
this.showFilesReport.set(false);
this.showBrokenCardsReport.set(false);
this.showOrphanedFilesReport.set(false);
if (this.subscription) {
this.subscription.stop();
}
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
const targetID = target.data('id');
if ('report-broken' === targetID) {
this.showBrokenCardsReport.set(true);
this.subscription = Meteor.subscribe(
'brokenCards',
SessionData.getSessionId(),
() => {
this.loading.set(false);
},
);
} else if ('report-files' === targetID) {
this.showFilesReport.set(true);
this.subscription = Meteor.subscribe('attachmentsList', () => {
this.loading.set(false);
});
} else if ('report-orphaned-files' === targetID) {
this.showOrphanedFilesReport.set(true);
this.subscription = Meteor.subscribe('orphanedAttachments', () => {
this.loading.set(false);
});
} else if ('report-rules' === targetID) {
this.subscription = Meteor.subscribe('rulesReport', () => {
this.showRulesReport.set(true);
this.loading.set(false);
});
}
}
},
}).register('adminReports');
Template.filesReport.helpers({
attachmentFiles() {
// eslint-disable-next-line no-console
// console.log('attachments:', AttachmentStorage.find());
// console.log('attachments.count:', AttachmentStorage.find().count());
return AttachmentStorage.find();
},
rulesReport() {
const rules = [];
Rules.find().forEach(rule => {
rules.push({
_id: rule._id,
title: rule.title,
boardId: rule.boardId,
boardTitle: rule.board().title,
action: rule.action().fetch(),
trigger: rule.trigger().fetch(),
});
});
return rules;
},
resultsCount() {
return AttachmentStorage.find().count();
},
fileSize(size) {
return Math.round(size / 1024);
},
usageCount(key) {
return Attachments.find({ 'copies.attachments.key': key }).count();
},
});
Template.orphanedFilesReport.helpers({
attachmentFiles() {
// eslint-disable-next-line no-console
// console.log('attachments:', AttachmentStorage.find());
// console.log('attachments.count:', AttachmentStorage.find().count());
return AttachmentStorage.find();
},
resultsCount() {
return AttachmentStorage.find().count();
},
fileSize(size) {
return Math.round(size / 1024);
},
});
Template.rulesReport.helpers({
rows() {
const rules = [];
Rules.find().forEach(rule => {
rules.push({
_id: rule._id,
title: rule.title,
boardId: rule.boardId,
boardTitle: rule.board().title,
action: rule.action(),
trigger: rule.trigger(),
});
});
console.log('rows:', rules);
return rules;
},
resultsCount() {
return Rules.find().count();
},
});
class BrokenCardsComponent extends CardSearchPagedComponent {
onCreated() {
super.onCreated();
}
}
BrokenCardsComponent.register('brokenCardsReport');

View file

@ -12,6 +12,10 @@ template(name="settingHeaderBar")
i.fa(class="fa-users") i.fa(class="fa-users")
span {{_ 'people'}} span {{_ 'people'}}
a.setting-header-btn.informations(href="{{pathFor 'admin-reports'}}")
i.fa(class="fa-list")
span {{_ 'reports'}}
a.setting-header-btn.informations(href="{{pathFor 'information'}}") a.setting-header-btn.informations(href="{{pathFor 'information'}}")
i.fa(class="fa-info-circle") i.fa(class="fa-info-circle")
span {{_ 'info'}} span {{_ 'info'}}

View file

@ -105,6 +105,14 @@ template(name="boardCardSettingsPopup")
span span
i.fa.fa-users i.fa.fa-users
| {{_ 'members'}} | {{_ 'members'}}
div.check-div
a.flex.js-field-has-creator(class="{{#if allowsCreator}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsCreator}}is-checked{{/if}}")
span
i.fa.fa-user
| {{_ 'creator'}}
div.check-div div.check-div
a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}") a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}") .materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}")

View file

@ -730,6 +730,14 @@ BlazeComponent.extendComponent({
return this.currentBoard.allowsSubtasks; return this.currentBoard.allowsSubtasks;
}, },
allowsCreator() {
return (
this.currentBoard.allowsCreator === null ||
this.currentBoard.allowsCreator === undefined ||
this.currentBoard.allowsCreator
);
},
allowsMembers() { allowsMembers() {
return this.currentBoard.allowsMembers; return this.currentBoard.allowsMembers;
}, },
@ -889,6 +897,19 @@ BlazeComponent.extendComponent({
this.currentBoard.allowsSubtasks, this.currentBoard.allowsSubtasks,
); );
}, },
'click .js-field-has-creator'(evt) {
evt.preventDefault();
this.currentBoard.allowsCreator = !this.currentBoard.allowsCreator;
this.currentBoard.setAllowsCreator(this.currentBoard.allowsCreator);
$(`.js-field-has-creator ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsCreator,
);
$('.js-field-has-creator').toggleClass(
CKCLS,
this.currentBoard.allowsCreator,
);
},
'click .js-field-has-members'(evt) { 'click .js-field-has-members'(evt) {
evt.preventDefault(); evt.preventDefault();
this.currentBoard.allowsMembers = !this.currentBoard.allowsMembers; this.currentBoard.allowsMembers = !this.currentBoard.allowsMembers;

View file

@ -1,5 +1,5 @@
template(name="userAvatar") template(name="userAvatar")
a.member.js-member(title="{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}") a.member(class="js-{{#if assignee}}assignee{{else}}member{{/if}}" title="{{userData.profile.fullname}} ({{userData.username}}) {{_ memberType}}")
if userData.profile.avatarUrl if userData.profile.avatarUrl
img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}") img.avatar.avatar-image(src="{{userData.profile.avatarUrl}}")
else else
@ -72,6 +72,7 @@ template(name="cardMemberPopup")
h3= user.profile.fullname h3= user.profile.fullname
p.quiet @{{ user.username }} p.quiet @{{ user.username }}
ul.pop-over-list ul.pop-over-list
unless noRemove
if currentUser.isNotCommentOnly if currentUser.isNotCommentOnly
if currentUser.isNotWorker if currentUser.isNotWorker
li: a.js-remove-member {{_ 'remove-member-from-card'}} li: a.js-remove-member {{_ 'remove-member-from-card'}}

View file

@ -1,3 +1,7 @@
import Cards from '/models/cards';
import Avatars from '/models/avatars';
import Users from '/models/users';
Template.userAvatar.helpers({ Template.userAvatar.helpers({
userData() { userData() {
// We need to handle a special case for the search results provided by the // We need to handle a special case for the search results provided by the
@ -30,10 +34,6 @@ Template.userAvatar.helpers({
}, },
}); });
Template.userAvatar.events({
'click .js-change-avatar': Popup.open('changeAvatar'),
});
Template.userAvatarInitials.helpers({ Template.userAvatarInitials.helpers({
initials() { initials() {
const user = Users.findOne(this.userId); const user = Users.findOne(this.userId);

View file

@ -25,10 +25,6 @@ template(name="memberMenuPopup")
a.js-global-search(href="{{pathFor 'global-search'}}") a.js-global-search(href="{{pathFor 'global-search'}}")
i.fa.fa-search i.fa.fa-search
| {{_ 'globalSearch-title'}} | {{_ 'globalSearch-title'}}
li
a.js-broken-cards(href="{{pathFor 'broken-cards'}}")
i.fa.fa-chain-broken
| {{_ 'broken-cards'}}
li li
a(href="{{pathFor 'home'}}") a(href="{{pathFor 'home'}}")
span.fa.fa-home span.fa.fa-home

View file

@ -32,8 +32,11 @@ export class CardSearchPagedComponent extends BlazeComponent {
that.searching.set(false); that.searching.set(false);
that.hasResults.set(false); that.hasResults.set(false);
that.serverError.set(true); that.serverError.set(true);
// eslint-disable-next-line no-console
console.log('Error.reason:', error.reason); console.log('Error.reason:', error.reason);
// eslint-disable-next-line no-console
console.log('Error.message:', error.message); console.log('Error.message:', error.message);
// eslint-disable-next-line no-console
console.log('Error.stack:', error.stack); console.log('Error.stack:', error.stack);
}, },
}; };
@ -72,7 +75,7 @@ export class CardSearchPagedComponent extends BlazeComponent {
if (this.queryErrors.length) { if (this.queryErrors.length) {
// console.log('queryErrors:', this.queryErrorMessages()); // console.log('queryErrors:', this.queryErrorMessages());
this.hasQueryErrors.set(true); this.hasQueryErrors.set(true);
return null; // return null;
} }
if (cards) { if (cards) {

View file

@ -1,3 +1,5 @@
import moment from 'moment';
// Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours // Helper function to replace HH with H for 24 hours format, because H allows also single-digit hours
function adjustedTimeFormat() { function adjustedTimeFormat() {
return moment return moment
@ -6,17 +8,17 @@ function adjustedTimeFormat() {
.replace(/HH/i, 'H'); .replace(/HH/i, 'H');
} }
DatePicker = BlazeComponent.extendComponent({ export class DatePicker extends BlazeComponent {
template() { template() {
return 'datepicker'; return 'datepicker';
}, }
onCreated(defaultTime = '1970-01-01 08:00:00') { onCreated(defaultTime = '1970-01-01 08:00:00') {
this.error = new ReactiveVar(''); this.error = new ReactiveVar('');
this.card = this.data(); this.card = this.data();
this.date = new ReactiveVar(moment.invalid()); this.date = new ReactiveVar(moment.invalid());
this.defaultTime = defaultTime; this.defaultTime = defaultTime;
}, }
startDayOfWeek() { startDayOfWeek() {
const currentUser = Meteor.user(); const currentUser = Meteor.user();
@ -25,7 +27,7 @@ DatePicker = BlazeComponent.extendComponent({
} else { } else {
return 1; return 1;
} }
}, }
onRendered() { onRendered() {
const $picker = this.$('.js-datepicker') const $picker = this.$('.js-datepicker')
@ -42,7 +44,7 @@ DatePicker = BlazeComponent.extendComponent({
this.error.set(''); this.error.set('');
const timeInput = this.find('#time'); const timeInput = this.find('#time');
timeInput.focus(); timeInput.focus();
if (!timeInput.value) { if (!timeInput.value && this.defaultTime) {
const currentHour = evt.date.getHours(); const currentHour = evt.date.getHours();
const defaultMoment = moment( const defaultMoment = moment(
currentHour > 0 ? evt.date : this.defaultTime, currentHour > 0 ? evt.date : this.defaultTime,
@ -55,22 +57,22 @@ DatePicker = BlazeComponent.extendComponent({
if (this.date.get().isValid()) { if (this.date.get().isValid()) {
$picker.datepicker('update', this.date.get().toDate()); $picker.datepicker('update', this.date.get().toDate());
} }
}, }
showDate() { showDate() {
if (this.date.get().isValid()) return this.date.get().format('L'); if (this.date.get().isValid()) return this.date.get().format('L');
return ''; return '';
}, }
showTime() { showTime() {
if (this.date.get().isValid()) return this.date.get().format('LT'); if (this.date.get().isValid()) return this.date.get().format('LT');
return ''; return '';
}, }
dateFormat() { dateFormat() {
return moment.localeData().longDateFormat('L'); return moment.localeData().longDateFormat('L');
}, }
timeFormat() { timeFormat() {
return moment.localeData().longDateFormat('LT'); return moment.localeData().longDateFormat('LT');
}, }
events() { events() {
return [ return [
@ -106,7 +108,7 @@ DatePicker = BlazeComponent.extendComponent({
const dateString = `${evt.target.date.value} ${time}`; const dateString = `${evt.target.date.value} ${time}`;
const newCompleteDate = moment( const newCompleteDate = moment(
dateString, dateString,
'L ' + adjustedTimeFormat(), `L ${adjustedTimeFormat()}`,
true, true,
); );
if (!newTime.isValid()) { if (!newTime.isValid()) {
@ -120,11 +122,9 @@ DatePicker = BlazeComponent.extendComponent({
if (newCompleteDate.isValid()) { if (newCompleteDate.isValid()) {
this._storeDate(newCompleteDate.toDate()); this._storeDate(newCompleteDate.toDate());
Popup.close(); Popup.close();
} else { } else if (!this.error) {
if (!this.error) {
this.error.set('invalid'); this.error.set('invalid');
} }
}
}, },
'click .js-delete-date'(evt) { 'click .js-delete-date'(evt) {
evt.preventDefault(); evt.preventDefault();
@ -133,5 +133,5 @@ DatePicker = BlazeComponent.extendComponent({
}, },
}, },
]; ];
}, }
}); }

51
config/const.js Normal file
View file

@ -0,0 +1,51 @@
export const ALLOWED_BOARD_COLORS = [
'belize',
'nephritis',
'pomegranate',
'pumpkin',
'wisteria',
'moderatepink',
'strongcyan',
'limegreen',
'midnight',
'dark',
'relax',
'corteza',
'clearblue',
'natural',
'modern',
'moderndark',
];
export const ALLOWED_COLORS = [
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'silver',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
];
export const TYPE_BOARD = 'board';
export const TYPE_CARD = 'cardType-card';
export const TYPE_LINKED_BOARD = 'cardType-linkedBoard';
export const TYPE_LINKED_CARD = 'cardType-linkedCard';
export const TYPE_TEMPLATE_BOARD = 'template-board';
export const TYPE_TEMPLATE_CONTAINER = 'template-container';

View file

@ -3,6 +3,7 @@ import {
OPERATOR_BOARD, OPERATOR_BOARD,
OPERATOR_COMMENT, OPERATOR_COMMENT,
OPERATOR_CREATED_AT, OPERATOR_CREATED_AT,
OPERATOR_CREATOR,
OPERATOR_DUE, OPERATOR_DUE,
OPERATOR_HAS, OPERATOR_HAS,
OPERATOR_LABEL, OPERATOR_LABEL,
@ -107,6 +108,7 @@ export class QueryErrors {
[OPERATOR_USER, 'user-username-not-found'], [OPERATOR_USER, 'user-username-not-found'],
[OPERATOR_ASSIGNEE, 'user-username-not-found'], [OPERATOR_ASSIGNEE, 'user-username-not-found'],
[OPERATOR_MEMBER, 'user-username-not-found'], [OPERATOR_MEMBER, 'user-username-not-found'],
[OPERATOR_CREATOR, 'user-username-not-found'],
]; ];
constructor() { constructor() {
@ -238,6 +240,7 @@ export class Query {
'operator-member': OPERATOR_MEMBER, 'operator-member': OPERATOR_MEMBER,
'operator-member-abbrev': OPERATOR_MEMBER, 'operator-member-abbrev': OPERATOR_MEMBER,
'operator-assignee': OPERATOR_ASSIGNEE, 'operator-assignee': OPERATOR_ASSIGNEE,
'operator-creator': OPERATOR_CREATOR,
'operator-assignee-abbrev': OPERATOR_ASSIGNEE, 'operator-assignee-abbrev': OPERATOR_ASSIGNEE,
'operator-status': OPERATOR_STATUS, 'operator-status': OPERATOR_STATUS,
'operator-due': OPERATOR_DUE, 'operator-due': OPERATOR_DUE,

View file

@ -291,6 +291,28 @@ FlowRouter.route('/people', {
}, },
}); });
FlowRouter.route('/admin-reports', {
name: 'admin-reports',
triggersEnter: [
AccountsTemplates.ensureSignedIn,
() => {
Session.set('currentBoard', null);
Session.set('currentList', null);
Session.set('currentCard', null);
Filter.reset();
Session.set('sortBy', '');
EscapeActions.executeAll();
},
],
action() {
BlazeLayout.render('defaultLayout', {
headerBar: 'settingHeaderBar',
content: 'adminReports',
});
},
});
FlowRouter.notFound = { FlowRouter.notFound = {
action() { action() {
BlazeLayout.render('defaultLayout', { content: 'notFound' }); BlazeLayout.render('defaultLayout', { content: 'notFound' });

View file

@ -1,14 +1,15 @@
export const DEFAULT_LIMIT = 25; export const DEFAULT_LIMIT = 25;
export const OPERATOR_ASSIGNEE = 'assignee'; export const OPERATOR_ASSIGNEE = 'assignees';
export const OPERATOR_COMMENT = 'comment'; export const OPERATOR_COMMENT = 'comment';
export const OPERATOR_CREATED_AT = 'createdAt'; export const OPERATOR_CREATED_AT = 'createdAt';
export const OPERATOR_CREATOR = 'userId';
export const OPERATOR_DUE = 'dueAt'; export const OPERATOR_DUE = 'dueAt';
export const OPERATOR_BOARD = 'board'; export const OPERATOR_BOARD = 'board';
export const OPERATOR_HAS = 'has'; export const OPERATOR_HAS = 'has';
export const OPERATOR_LABEL = 'label'; export const OPERATOR_LABEL = 'label';
export const OPERATOR_LIMIT = 'limit'; export const OPERATOR_LIMIT = 'limit';
export const OPERATOR_LIST = 'list'; export const OPERATOR_LIST = 'list';
export const OPERATOR_MEMBER = 'member'; export const OPERATOR_MEMBER = 'members';
export const OPERATOR_MODIFIED_AT = 'modifiedAt'; export const OPERATOR_MODIFIED_AT = 'modifiedAt';
export const OPERATOR_SORT = 'sort'; export const OPERATOR_SORT = 'sort';
export const OPERATOR_STATUS = 'status'; export const OPERATOR_STATUS = 'status';

View file

@ -901,6 +901,7 @@
"operator-member-abbrev": "m", "operator-member-abbrev": "m",
"operator-assignee": "assignee", "operator-assignee": "assignee",
"operator-assignee-abbrev": "a", "operator-assignee-abbrev": "a",
"operator-creator": "creator",
"operator-status": "status", "operator-status": "status",
"operator-due": "due", "operator-due": "due",
"operator-created": "created", "operator-created": "created",
@ -952,6 +953,7 @@
"globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:<username>`", "globalSearch-instructions-operator-at": "`__operator_user_abbrev__username` - shorthand for `user:<username>`",
"globalSearch-instructions-operator-member": "`__operator_member__:<username>` - cards where *<username>* is a *member*", "globalSearch-instructions-operator-member": "`__operator_member__:<username>` - cards where *<username>* is a *member*",
"globalSearch-instructions-operator-assignee": "`__operator_assignee__:<username>` - cards where *<username>* is an *assignee*", "globalSearch-instructions-operator-assignee": "`__operator_assignee__:<username>` - cards where *<username>* is an *assignee*",
"globalSearch-instructions-operator-creator": "`__operator_creator__:<username>` - cards where *<username>* is the card's creator",
"globalSearch-instructions-operator-due": "`__operator_due__:<n>` - cards which are due up to *<n>* days from now. `__operator_due__:__predicate_overdue__ lists all cards past their due date.", "globalSearch-instructions-operator-due": "`__operator_due__:<n>` - cards which are due up to *<n>* days from now. `__operator_due__:__predicate_overdue__ lists all cards past their due date.",
"globalSearch-instructions-operator-created": "`__operator_created__:<n>` - cards which were created *<n>* days ago or less", "globalSearch-instructions-operator-created": "`__operator_created__:<n>` - cards which were created *<n>* days ago or less",
"globalSearch-instructions-operator-modified": "`__operator_modified__:<n>` - cards which were modified *<n>* days ago or less", "globalSearch-instructions-operator-modified": "`__operator_modified__:<n>` - cards which were modified *<n>* days ago or less",
@ -992,5 +994,9 @@
"custom-field-stringtemplate": "String Template", "custom-field-stringtemplate": "String Template",
"custom-field-stringtemplate-format": "Format (use %{value} as placeholder)", "custom-field-stringtemplate-format": "Format (use %{value} as placeholder)",
"custom-field-stringtemplate-separator": "Separator (use &#32; or &nbsp; for a space)", "custom-field-stringtemplate-separator": "Separator (use &#32; or &nbsp; for a space)",
"custom-field-stringtemplate-item-placeholder": "Press enter to add more items" "custom-field-stringtemplate-item-placeholder": "Press enter to add more items",
"creator": "Creator",
"filesReportTitle": "Files Report",
"orphanedFilesReportTitle": "Orphaned Files Report",
"reports": "Reports"
} }

View file

@ -1,3 +1,8 @@
export const AttachmentStorage = new Mongo.Collection(
'cfs_gridfs.attachments.files',
);
export const AvatarStorage = new Mongo.Collection('cfs_gridfs.avatars.files');
const localFSStore = process.env.ATTACHMENTS_STORE_PATH; const localFSStore = process.env.ATTACHMENTS_STORE_PATH;
const storeName = 'attachments'; const storeName = 'attachments';
const defaultStoreOptions = { const defaultStoreOptions = {

View file

@ -1,3 +1,11 @@
import {
ALLOWED_BOARD_COLORS,
ALLOWED_COLORS,
TYPE_BOARD,
TYPE_TEMPLATE_BOARD,
TYPE_TEMPLATE_CONTAINER,
} from '/config/const';
const escapeForRegex = require('escape-string-regexp'); const escapeForRegex = require('escape-string-regexp');
Boards = new Mongo.Collection('boards'); Boards = new Mongo.Collection('boards');
@ -144,32 +152,7 @@ Boards.attachSchema(
* `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo` * `saddlebrown`, `paleturquoise`, `mistyrose`, `indigo`
*/ */
type: String, type: String,
allowedValues: [ allowedValues: ALLOWED_COLORS,
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'silver',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
}, },
// XXX We might want to maintain more informations under the member sub- // XXX We might want to maintain more informations under the member sub-
// documents like de-normalized meta-data (the date the member joined the // documents like de-normalized meta-data (the date the member joined the
@ -246,28 +229,11 @@ Boards.attachSchema(
* The color of the board. * The color of the board.
*/ */
type: String, type: String,
allowedValues: [ allowedValues: ALLOWED_BOARD_COLORS,
'belize',
'nephritis',
'pomegranate',
'pumpkin',
'wisteria',
'moderatepink',
'strongcyan',
'limegreen',
'midnight',
'dark',
'relax',
'corteza',
'clearblue',
'natural',
'modern',
'moderndark',
],
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
autoValue() { autoValue() {
if (this.isInsert && !this.isSet) { if (this.isInsert && !this.isSet) {
return Boards.simpleSchema()._schema.color.allowedValues[0]; return ALLOWED_BOARD_COLORS[0];
} }
}, },
}, },
@ -372,6 +338,14 @@ Boards.attachSchema(
defaultValue: true, defaultValue: true,
}, },
allowsCreator: {
/**
* Does the board allow creator?
*/
type: Boolean,
defaultValue: true,
},
allowsAssignee: { allowsAssignee: {
/** /**
* Does the board allows assignee? * Does the board allows assignee?
@ -497,9 +471,11 @@ Boards.attachSchema(
type: { type: {
/** /**
* The type of board * The type of board
* possible values: board, template-board, template-container
*/ */
type: String, type: String,
defaultValue: 'board', defaultValue: TYPE_BOARD,
allowedValues: [TYPE_BOARD, TYPE_TEMPLATE_BOARD, TYPE_TEMPLATE_CONTAINER],
}, },
sort: { sort: {
/** /**
@ -1187,6 +1163,10 @@ Boards.mutations({
return { $set: { allowsSubtasks } }; return { $set: { allowsSubtasks } };
}, },
setAllowsCreator(allowsCreator) {
return { $set: { allowsCreator } };
},
setAllowsMembers(allowsMembers) { setAllowsMembers(allowsMembers) {
return { $set: { allowsMembers } }; return { $set: { allowsMembers } };
}, },
@ -1318,8 +1298,11 @@ Boards.userBoards = (userId, archived = false, selector = {}) => {
if (typeof archived === 'boolean') { if (typeof archived === 'boolean') {
selector.archived = archived; selector.archived = archived;
} }
selector.$or = [{ permission: 'public' }]; if (!selector.type) {
selector.type = 'board';
}
selector.$or = [{ permission: 'public' }];
if (userId) { if (userId) {
selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } }); selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } });
} }
@ -1341,7 +1324,7 @@ Boards.colorMap = () => {
}; };
Boards.labelColors = () => { Boards.labelColors = () => {
return _.clone(Boards.simpleSchema()._schema['labels.$.color'].allowedValues); return ALLOWED_COLORS;
}; };
if (Meteor.isServer) { if (Meteor.isServer) {
@ -1423,8 +1406,7 @@ if (Meteor.isServer) {
}, },
myLabelNames() { myLabelNames() {
let names = []; let names = [];
Boards.userBoards(Meteor.userId(), false, { type: 'board' }).forEach( Boards.userBoards(Meteor.userId()).forEach(board => {
board => {
names = names.concat( names = names.concat(
board.labels board.labels
.filter(label => !!label.name) .filter(label => !!label.name)
@ -1432,8 +1414,7 @@ if (Meteor.isServer) {
return label.name; return label.name;
}), }),
); );
}, });
);
return _.uniq(names).sort(); return _.uniq(names).sort();
}, },
myBoardNames() { myBoardNames() {

View file

@ -1,3 +1,10 @@
import {
ALLOWED_COLORS,
TYPE_CARD,
TYPE_LINKED_BOARD,
TYPE_LINKED_CARD,
} from '../config/const';
Cards = new Mongo.Collection('cards'); Cards = new Mongo.Collection('cards');
// XXX To improve pub/sub performances a card document should include a // XXX To improve pub/sub performances a card document should include a
@ -77,33 +84,7 @@ Cards.attachSchema(
color: { color: {
type: String, type: String,
optional: true, optional: true,
allowedValues: [ allowedValues: ALLOWED_COLORS,
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'silver',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
}, },
createdAt: { createdAt: {
/** /**
@ -305,7 +286,8 @@ Cards.attachSchema(
* type of the card * type of the card
*/ */
type: String, type: String,
defaultValue: 'cardType-card', defaultValue: TYPE_CARD,
allowedValues: [TYPE_CARD, TYPE_LINKED_CARD, TYPE_LINKED_BOARD],
}, },
linkedId: { linkedId: {
/** /**

View file

@ -1,3 +1,5 @@
import { ALLOWED_COLORS } from '/config/const';
Lists = new Mongo.Collection('lists'); Lists = new Mongo.Collection('lists');
/** /**
@ -144,32 +146,7 @@ Lists.attachSchema(
type: String, type: String,
optional: true, optional: true,
// silver is the default, so it is left out // silver is the default, so it is left out
allowedValues: [ allowedValues: ALLOWED_COLORS,
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
}, },
type: { type: {
/** /**

View file

@ -62,6 +62,15 @@ Rules.helpers({
getTrigger() { getTrigger() {
return Triggers.findOne({ _id: this.triggerId }); return Triggers.findOne({ _id: this.triggerId });
}, },
board() {
return Boards.findOne({ _id: this.boardId });
},
trigger() {
return Triggers.findOne({ _id: this.triggerId });
},
action() {
return Actions.findOne({ _id: this.actionId });
},
}); });
Rules.allow({ Rules.allow({

View file

@ -1,3 +1,5 @@
import { ALLOWED_COLORS } from '/config/const';
Swimlanes = new Mongo.Collection('swimlanes'); Swimlanes = new Mongo.Collection('swimlanes');
/** /**
@ -68,32 +70,7 @@ Swimlanes.attachSchema(
type: String, type: String,
optional: true, optional: true,
// silver is the default, so it is left out // silver is the default, so it is left out
allowedValues: [ allowedValues: ALLOWED_COLORS,
'white',
'green',
'yellow',
'orange',
'red',
'purple',
'blue',
'sky',
'lime',
'pink',
'black',
'peachpuff',
'crimson',
'plum',
'darkgreen',
'slateblue',
'magenta',
'gold',
'navy',
'gray',
'saddlebrown',
'paleturquoise',
'mistyrose',
'indigo',
],
}, },
updatedAt: { updatedAt: {
/** /**

View file

@ -0,0 +1,60 @@
import Attachments, { AttachmentStorage } from '/models/attachments';
import { ObjectID } from 'bson';
Meteor.publish('attachmentsList', function() {
// eslint-disable-next-line no-console
// console.log('attachments:', AttachmentStorage.find());
const files = AttachmentStorage.find(
{},
{
fields: {
_id: 1,
filename: 1,
md5: 1,
length: 1,
contentType: 1,
metadata: 1,
},
sort: {
filename: 1,
},
limit: 250,
},
);
const attIds = [];
files.forEach(file => {
attIds.push(file._id._str);
});
return [
files,
Attachments.find({ 'copies.attachments.key': { $in: attIds } }),
];
});
Meteor.publish('orphanedAttachments', function() {
let keys = [];
Attachments.find({}, { fields: { copies: 1 } }).forEach(att => {
keys.push(new ObjectID(att.copies.attachments.key));
});
keys.sort();
keys = _.uniq(keys, true);
return AttachmentStorage.find(
{ _id: { $nin: keys } },
{
fields: {
_id: 1,
filename: 1,
md5: 1,
length: 1,
contentType: 1,
metadata: 1,
},
sort: {
filename: 1,
},
limit: 250,
},
);
});

View file

@ -15,12 +15,15 @@ import {
OPERATOR_ASSIGNEE, OPERATOR_ASSIGNEE,
OPERATOR_BOARD, OPERATOR_BOARD,
OPERATOR_COMMENT, OPERATOR_COMMENT,
OPERATOR_CREATED_AT,
OPERATOR_CREATOR,
OPERATOR_DUE, OPERATOR_DUE,
OPERATOR_HAS, OPERATOR_HAS,
OPERATOR_LABEL, OPERATOR_LABEL,
OPERATOR_LIMIT, OPERATOR_LIMIT,
OPERATOR_LIST, OPERATOR_LIST,
OPERATOR_MEMBER, OPERATOR_MEMBER,
OPERATOR_MODIFIED_AT,
OPERATOR_SORT, OPERATOR_SORT,
OPERATOR_STATUS, OPERATOR_STATUS,
OPERATOR_SWIMLANE, OPERATOR_SWIMLANE,
@ -42,8 +45,8 @@ import {
PREDICATE_PUBLIC, PREDICATE_PUBLIC,
PREDICATE_START_AT, PREDICATE_START_AT,
PREDICATE_SYSTEM, PREDICATE_SYSTEM,
} from '../../config/search-const'; } from '/config/search-const';
import { QueryErrors, QueryParams, Query } from '../../config/query-classes'; import { QueryErrors, QueryParams, Query } from '/config/query-classes';
const escapeForRegex = require('escape-string-regexp'); const escapeForRegex = require('escape-string-regexp');
@ -99,7 +102,7 @@ function buildSelector(queryParams) {
let selector = {}; let selector = {};
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('queryParams:', queryParams); console.log('queryParams:', queryParams);
if (queryParams.selector) { if (queryParams.selector) {
selector = queryParams.selector; selector = queryParams.selector;
@ -163,7 +166,7 @@ function buildSelector(queryParams) {
if (queryParams.hasOperator(OPERATOR_BOARD)) { if (queryParams.hasOperator(OPERATOR_BOARD)) {
const queryBoards = []; const queryBoards = [];
queryParams.hasOperator(OPERATOR_BOARD).forEach(query => { queryParams.getPredicates(OPERATOR_BOARD).forEach(query => {
const boards = Boards.userSearch(userId, { const boards = Boards.userSearch(userId, {
title: new RegExp(escapeForRegex(query), 'i'), title: new RegExp(escapeForRegex(query), 'i'),
}); });
@ -240,7 +243,7 @@ function buildSelector(queryParams) {
} }
} }
[OPERATOR_DUE, 'createdAt', 'modifiedAt'].forEach(field => { [OPERATOR_DUE, OPERATOR_CREATED_AT, OPERATOR_MODIFIED_AT].forEach(field => {
if (queryParams.hasOperator(field)) { if (queryParams.hasOperator(field)) {
selector[field] = {}; selector[field] = {};
const predicate = queryParams.getPredicate(field); const predicate = queryParams.getPredicate(field);
@ -251,57 +254,46 @@ function buildSelector(queryParams) {
const queryUsers = {}; const queryUsers = {};
queryUsers[OPERATOR_ASSIGNEE] = []; queryUsers[OPERATOR_ASSIGNEE] = [];
queryUsers[OPERATOR_MEMBER] = []; queryUsers[OPERATOR_MEMBER] = [];
queryUsers[OPERATOR_CREATOR] = [];
if (queryParams.hasOperator(OPERATOR_USER)) { if (queryParams.hasOperator(OPERATOR_USER)) {
const users = [];
queryParams.getPredicates(OPERATOR_USER).forEach(username => { queryParams.getPredicates(OPERATOR_USER).forEach(username => {
const user = Users.findOne({ username }); const user = Users.findOne({ username });
if (user) { if (user) {
queryUsers[OPERATOR_MEMBER].push(user._id); users.push(user._id);
queryUsers[OPERATOR_ASSIGNEE].push(user._id);
} else { } else {
errors.addNotFound(OPERATOR_USER, username); errors.addNotFound(OPERATOR_USER, username);
} }
}); });
} if (users.length) {
[OPERATOR_MEMBER, OPERATOR_ASSIGNEE].forEach(key => {
if (queryParams.hasOperator(key)) {
queryParams.getPredicates(key).forEach(query => {
const users = Users.find({
username: query,
});
if (users.count()) {
users.forEach(user => {
queryUsers[key].push(user._id);
});
} else {
errors.addNotFound(key, query);
}
});
}
});
if (
queryUsers[OPERATOR_MEMBER].length &&
queryUsers[OPERATOR_ASSIGNEE].length
) {
selector.$and.push({ selector.$and.push({
$or: [ $or: [{ members: { $in: users } }, { assignees: { $in: users } }],
{ members: { $in: queryUsers[OPERATOR_MEMBER] } },
{ assignees: { $in: queryUsers[OPERATOR_ASSIGNEE] } },
],
}); });
} else if (queryUsers[OPERATOR_MEMBER].length) {
selector.members = { $in: queryUsers[OPERATOR_MEMBER] };
} else if (queryUsers[OPERATOR_ASSIGNEE].length) {
selector.assignees = { $in: queryUsers[OPERATOR_ASSIGNEE] };
} }
}
[OPERATOR_MEMBER, OPERATOR_ASSIGNEE, OPERATOR_CREATOR].forEach(key => {
if (queryParams.hasOperator(key)) {
const users = [];
queryParams.getPredicates(key).forEach(username => {
const user = Users.findOne({ username });
if (user) {
users.push(user._id);
} else {
errors.addNotFound(key, username);
}
});
if (users.length) {
selector[key] = { $in: users };
}
}
});
if (queryParams.hasOperator(OPERATOR_LABEL)) { if (queryParams.hasOperator(OPERATOR_LABEL)) {
queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
const queryLabels = []; const queryLabels = [];
queryParams.getPredicates(OPERATOR_LABEL).forEach(label => {
let boards = Boards.userSearch(userId, { let boards = Boards.userBoards(userId, null, {
labels: { $elemMatch: { color: label.toLowerCase() } }, labels: { $elemMatch: { color: label.toLowerCase() } },
}); });
@ -325,7 +317,7 @@ function buildSelector(queryParams) {
const reLabel = new RegExp(escapeForRegex(label), 'i'); const reLabel = new RegExp(escapeForRegex(label), 'i');
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('reLabel:', reLabel); // console.log('reLabel:', reLabel);
boards = Boards.userSearch(userId, { boards = Boards.userBoards(userId, null, {
labels: { $elemMatch: { name: reLabel } }, labels: { $elemMatch: { name: reLabel } },
}); });
@ -346,9 +338,12 @@ function buildSelector(queryParams) {
errors.addNotFound(OPERATOR_LABEL, label); errors.addNotFound(OPERATOR_LABEL, label);
} }
} }
selector.labelIds = { $in: _.uniq(queryLabels) };
}); });
if (queryLabels.length) {
// eslint-disable-next-line no-console
// console.log('queryLabels:', queryLabels);
selector.labelIds = { $in: _.uniq(queryLabels) };
}
} }
if (queryParams.hasOperator(OPERATOR_HAS)) { if (queryParams.hasOperator(OPERATOR_HAS)) {
@ -443,9 +438,9 @@ function buildSelector(queryParams) {
} }
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('selector:', selector); console.log('selector:', selector);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('selector.$and:', selector.$and); console.log('selector.$and:', selector.$and);
const query = new Query(); const query = new Query();
query.selector = selector; query.selector = selector;
@ -488,6 +483,7 @@ function buildProjection(query) {
modifiedAt: 1, modifiedAt: 1,
labelIds: 1, labelIds: 1,
customFields: 1, customFields: 1,
userId: 1,
}, },
sort: { sort: {
boardId: 1, boardId: 1,
@ -602,10 +598,8 @@ function findCards(sessionId, query) {
// console.log('selector.$and:', query.selector.$and); // console.log('selector.$and:', query.selector.$and);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('projection:', projection); // console.log('projection:', projection);
let cards;
if (!query.hasErrors()) { const cards = Cards.find(query.selector, query.projection);
cards = Cards.find(query.selector, query.projection);
}
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
// console.log('count:', cards.count()); // console.log('count:', cards.count());
@ -658,6 +652,9 @@ function findCards(sessionId, query) {
if (card.boardId) boards.push(card.boardId); if (card.boardId) boards.push(card.boardId);
if (card.swimlaneId) swimlanes.push(card.swimlaneId); if (card.swimlaneId) swimlanes.push(card.swimlaneId);
if (card.listId) lists.push(card.listId); if (card.listId) lists.push(card.listId);
if (card.userId) {
users.push(card.userId);
}
if (card.members) { if (card.members) {
card.members.forEach(userId => { card.members.forEach(userId => {
users.push(userId); users.push(userId);

View file

@ -1,3 +1,8 @@
import Boards from '/models/boards';
import Actions from '/models/actions';
import Triggers from '/models/triggers';
import Rules from '/models/rules';
Meteor.publish('rules', ruleId => { Meteor.publish('rules', ruleId => {
check(ruleId, String); check(ruleId, String);
return Rules.find({ return Rules.find({
@ -16,3 +21,23 @@ Meteor.publish('allTriggers', () => {
Meteor.publish('allActions', () => { Meteor.publish('allActions', () => {
return Actions.find({}); return Actions.find({});
}); });
Meteor.publish('rulesReport', () => {
const rules = Rules.find();
const actionIds = [];
const triggerIds = [];
const boardIds = [];
rules.forEach(rule => {
actionIds.push(rule.actionId);
triggerIds.push(rule.triggerId);
boardIds.push(rule.boardId);
});
return [
rules,
Actions.find({ _id: { $in: actionIds } }),
Triggers.find({ _id: { $in: triggerIds } }),
Boards.find({ _id: { $in: boardIds } }, { fields: { title: 1 } }),
];
});