Merge branch 'feature-custom-fields' of https://github.com/feuerball11/wekan into feature-custom-fields

This commit is contained in:
IgnatzHome 2018-06-14 19:42:09 +02:00
commit a433f7d9fe
76 changed files with 3087 additions and 450 deletions

View file

@ -6,9 +6,17 @@ template(name="archivedBoards")
ul.archived-lists
each archivedBoards
li.archived-lists-item
button.js-restore-board
i.fa.fa-undo
| {{_ 'restore-board'}}
= title
div.board-header-btns
button.board-header-btn.js-delete-board
i.fa.fa-trash-o
| {{_ 'delete-board'}}
button.board-header-btn.js-restore-board
i.fa.fa-undo
| {{_ 'restore-board'}}
= title
else
li.no-items-message {{_ 'no-archived-boards'}}
template(name="boardDeletePopup")
p {{_ 'delete-board-confirm-popup'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}

View file

@ -29,6 +29,18 @@ BlazeComponent.extendComponent({
board.restore();
Utils.goBoardId(board._id);
},
'click .js-delete-board': Popup.afterConfirm('boardDelete', function() {
Popup.close();
const isSandstorm = Meteor.settings && Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && Session.get('currentBoard')) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
Boards.remove(currentBoard._id);
}
const board = this.currentData();
Boards.remove(board._id);
FlowRouter.go('home');
}),
}];
},
}).register('archivedBoards');

View file

@ -88,11 +88,13 @@ BlazeComponent.extendComponent({
isViewSwimlanes() {
const currentUser = Meteor.user();
if (!currentUser) return false;
return (currentUser.profile.boardView === 'board-view-swimlanes');
},
isViewLists() {
const currentUser = Meteor.user();
if (!currentUser) return true;
return (currentUser.profile.boardView === 'board-view-lists');
},

View file

@ -17,6 +17,12 @@ Template.boardMenuPopup.events({
// confirm that the board was successfully archived.
FlowRouter.go('home');
}),
'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
Popup.close();
Boards.remove(currentBoard._id);
FlowRouter.go('home');
}),
'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
'click .js-import-board': Popup.open('chooseBoardSource'),
});

View file

@ -93,7 +93,7 @@ Template.dateBadge.helpers({
});
// editCardReceivedDatePopup
(class extends EditCardDate {
(class extends DatePicker {
onCreated() {
super.onCreated();
this.data().receivedAt && this.date.set(moment(this.data().receivedAt));
@ -156,7 +156,7 @@ Template.dateBadge.helpers({
}).register('editCardDueDatePopup');
// editCardEndDatePopup
(class extends EditCardDate {
(class extends DatePicker {
onCreated() {
super.onCreated();
this.data().endAt && this.date.set(moment(this.data().endAt));
@ -279,11 +279,14 @@ class CardDueDate extends CardDate {
classes() {
let classes = 'due-date' + ' ';
if (this.now.get().diff(this.date.get(), 'days') >= 2)
if ((this.now.get().diff(this.date.get(), 'days') >= 2) &&
(this.date.get().isBefore(this.data().endAt)))
classes += 'long-overdue';
else if (this.now.get().diff(this.date.get(), 'minute') >= 0)
else if ((this.now.get().diff(this.date.get(), 'minute') >= 0) &&
(this.date.get().isBefore(this.data().endAt)))
classes += 'due';
else if (this.now.get().diff(this.date.get(), 'days') >= -1)
else if ((this.now.get().diff(this.date.get(), 'days') >= -1) &&
(this.date.get().isBefore(this.data().endAt)))
classes += 'almost-due';
return classes;
}
@ -355,4 +358,3 @@ CardEndDate.register('cardEndDate');
return this.date.get().format('l');
}
}).register('minicardEndDate');

View file

@ -108,6 +108,39 @@ template(name="cardDetails")
+viewer
= description
.card-details-items
.card-details-item.card-details-item-name
h3.card-details-item-title {{_ 'requested-by'}}
if canModifyCard
+inlinedForm(classNames="js-card-details-requester")
+editCardRequesterForm
else
a.js-open-inlined-form
if requestedBy
+viewer
= requestedBy
else
| {{_ 'add'}}
else if requestedBy
+viewer
= requestedBy
.card-details-item.card-details-item-name
h3.card-details-item-title {{_ 'assigned-by'}}
if canModifyCard
+inlinedForm(classNames="js-card-details-assigner")
+editCardAssignerForm
else
a.js-open-inlined-form
if assignedBy
+viewer
= assignedBy
else
| {{_ 'add'}}
else if requestedBy
+viewer
= assignedBy
hr
+checklists(cardId = _id)
@ -141,6 +174,18 @@ template(name="editCardTitleForm")
button.primary.confirm.js-submit-edit-card-title-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
template(name="editCardRequesterForm")
input.js-edit-card-requester(type='text' autofocus value=requestedBy)
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-requester-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
template(name="editCardAssignerForm")
input.js-edit-card-assigner(type='text' autofocus value=assignedBy)
.edit-controls.clearfix
button.primary.confirm.js-submit-edit-card-assigner-form(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
template(name="cardDetailsActionsPopup")
ul.pop-over-list
li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
@ -150,8 +195,8 @@ template(name="cardDetailsActionsPopup")
li: a.js-members {{_ 'card-edit-members'}}
li: a.js-labels {{_ 'card-edit-labels'}}
li: a.js-attachments {{_ 'card-edit-attachments'}}
li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
li: a.js-custom-fields {{_ 'card-edit-custom-fields'}}
li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
@ -178,7 +223,6 @@ template(name="copyCardPopup")
= title
+boardsAndLists
template(name="copyChecklistToManyCardsPopup")
label(for='copy-checklist-cards-title') {{_ 'copyChecklistToManyCardsPopup-instructions'}}:
textarea#copy-card-title.minicard-composer-textarea.js-card-title(autofocus)

View file

@ -150,6 +150,20 @@ BlazeComponent.extendComponent({
this.data().setTitle(title);
}
},
'submit .js-card-details-assigner'(evt) {
evt.preventDefault();
const assigner = this.currentComponent().getValue().trim();
if (assigner) {
this.data().setAssignedBy(assigner);
}
},
'submit .js-card-details-requester'(evt) {
evt.preventDefault();
const requester = this.currentComponent().getValue().trim();
if (requester) {
this.data().setRequestedBy(requester);
}
},
'click .js-member': Popup.open('cardMember'),
'click .js-add-members': Popup.open('cardMembers'),
'click .js-add-labels': Popup.open('cardLabels'),
@ -221,8 +235,8 @@ Template.cardDetailsActionsPopup.events({
'click .js-members': Popup.open('cardMembers'),
'click .js-labels': Popup.open('cardLabels'),
'click .js-attachments': Popup.open('cardAttachments'),
'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-custom-fields': Popup.open('cardCustomFields'),
'click .js-received-date': Popup.open('editCardReceivedDate'),
'click .js-start-date': Popup.open('editCardStartDate'),
'click .js-due-date': Popup.open('editCardDueDate'),
'click .js-end-date': Popup.open('editCardEndDate'),
@ -269,6 +283,32 @@ Template.editCardTitleForm.events({
},
});
Template.editCardRequesterForm.onRendered(function() {
autosize(this.$('.js-edit-card-requester'));
});
Template.editCardRequesterForm.events({
'keydown .js-edit-card-requester'(evt) {
// If enter key was pressed, submit the data
if (evt.keyCode === 13) {
$('.js-submit-edit-card-requester-form').click();
}
},
});
Template.editCardAssignerForm.onRendered(function() {
autosize(this.$('.js-edit-card-assigner'));
});
Template.editCardAssignerForm.events({
'keydown .js-edit-card-assigner'(evt) {
// If enter key was pressed, submit the data
if (evt.keyCode === 13) {
$('.js-submit-edit-card-assigner-form').click();
}
},
});
Template.moveCardPopup.events({
'click .js-done' () {
// XXX We should *not* get the currentCard from the global state, but

View file

@ -82,7 +82,8 @@
&.card-details-item-start,
&.card-details-item-due,
&.card-details-item-end,
&.card-details-item-customfield
&.card-details-item-customfield,
&.card-details-item-name
max-width: 50%
flex-grow: 1

View file

@ -3,7 +3,7 @@
// XXX Use .board-widget-labels as a flexbox container
.card-label
border-radius: 4px
color: white
color: white //Default white text, in select cases, changed to black to improve contrast between label colour and text
display: inline-block
font-weight: 700
font-size: 13px
@ -48,9 +48,11 @@
.card-label-yellow
background-color: #fad900
color: #000000 //Black text for better visibility
.card-label-orange
background-color: #ff9f19
color: #000000 //Black text for better visibility
.card-label-red
background-color: #eb4646
@ -63,6 +65,7 @@
.card-label-pink
background-color: #ff78cb
color: #000000 //Black text for better visibility
.card-label-sky
background-color: #00c2e0
@ -72,6 +75,55 @@
.card-label-lime
background-color: #51e898
color: #000000 //Black text for better visibility
.card-label-silver
background-color: #c0c0c0
color: #000000 //Black text for better visibility
.card-label-peachpuff
background-color: #ffdab9
color: #000000 //Black text for better visibility
.card-label-crimson
background-color: #dc143c
.card-label-plum
background-color: #dda0dd
color: #000000 //Black text for better visibility
.card-label-darkgreen
background-color: #006400
.card-label-slateblue
background-color: #6a5acd
.card-label-magenta
background-color: #ff00ff
.card-label-gold
background-color: #ffd700
color: #000000 //Black text for better visibility
.card-label-navy
background-color: #000080
.card-label-gray
background-color: #808080
.card-label-saddlebrown
background-color: #8b4513
.card-label-paleturquoise
background-color: #afeeee
color: #000000 //Black text for better visibility
.card-label-mistyrose
background-color: #ffe4e1
color: #000000 //Black text for better visibility
.card-label-indigo
background-color: #4b0082
.edit-label,
.create-label

View file

@ -10,20 +10,41 @@ template(name="minicard")
+viewer
= title
.dates
if receivedAt
unless startAt
unless dueAt
unless endAt
.date
+miniCardReceivedDate
if startAt
.date
+minicardStartDate
if dueAt
unless endAt
.date
+minicardDueDate
if endAt
.date
+minicardDueDate
+minicardEndDate
if spentTime
.date
+cardSpentTime
.minicard-custom-fields
each customFieldsWD
if definition.showOnCard
.minicard-custom-field
.minicard-custom-field-item
= definition.name
.minicard-custom-field-item
+viewer
= trueValue
if members
.minicard-members.js-minicard-members
each members
+userAvatar(userId=this)
.badges
if comments.count
.badge(title="{{_ 'card-comments-title' comments.count }}")

View file

@ -77,6 +77,13 @@
height: @width
border-radius: 2px
margin-left: 3px
.minicard-custom-fields
display:block;
.minicard-custom-field
display:flex;
.minicard-custom-field-item
max-width:50%;
flex-grow:1;
.minicard-title
p:last-child
margin-bottom: 0

View file

@ -37,7 +37,7 @@ template(name="createCustomFieldPopup")
each dropdownItems.get
input.js-dropdown-item(type="text" value=name placeholder="")
input.js-dropdown-item.last(type="text" value="" placeholder="{{_ 'custom-field-dropdown-options-placeholder'}}")
a.flex.js-field-show-on-card
a.flex.js-field-show-on-card(class="{{#if showOnCard}}is-checked{{/if}}")
.materialCheckBox(class="{{#if showOnCard}}is-checked{{/if}}")
span {{_ 'show-field-on-card'}}
@ -49,4 +49,4 @@ template(name="createCustomFieldPopup")
template(name="deleteCustomFieldPopup")
p {{_ "custom-field-delete-pop"}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}

View file

@ -40,6 +40,25 @@ template(name="filterSidebar")
| (<span class="username">{{ username }}</span>)
if Filter.members.isSelected _id
i.fa.fa-check
hr
ul.sidebar-list
li(class="{{#if Filter.customFields.isSelected undefined}}active{{/if}}")
a.name.js-toggle-custom-fields-filter
span.sidebar-list-item-description
| {{_ 'filter-no-custom-fields'}}
if Filter.customFields.isSelected undefined
i.fa.fa-check
each currentBoard.customFields
li(class="{{#if Filter.customFields.isSelected _id}}active{{/if}}")
a.name.js-toggle-custom-fields-filter
span.sidebar-list-item-description
{{ name }}
if Filter.customFields.isSelected _id
i.fa.fa-check
hr
span {{_ 'advanced-filter-label'}}
input.js-field-advanced-filter(type="text")
span {{_ 'advanced-filter-description'}}
if Filter.isActive
hr
a.sidebar-btn.js-clear-all

View file

@ -11,6 +11,16 @@ BlazeComponent.extendComponent({
Filter.members.toggle(this.currentData()._id);
Filter.resetExceptions();
},
'click .js-toggle-custom-fields-filter'(evt) {
evt.preventDefault();
Filter.customFields.toggle(this.currentData()._id);
Filter.resetExceptions();
},
'change .js-field-advanced-filter'(evt) {
evt.preventDefault();
Filter.advanced.set(this.find('.js-field-advanced-filter').value.trim());
Filter.resetExceptions();
},
'click .js-clear-all'(evt) {
evt.preventDefault();
Filter.reset();

View file

@ -10,10 +10,13 @@ function showFilterSidebar() {
// Use a "set" filter for a field that is a set of documents uniquely
// identified. For instance `{ labels: ['labelA', 'labelC', 'labelD'] }`.
// use "subField" for searching inside object Fields.
// For instance '{ 'customFields._id': ['field1','field2']} (subField would be: _id)
class SetFilter {
constructor() {
constructor(subField = '') {
this._dep = new Tracker.Dependency();
this._selectedElements = [];
this.subField = subField;
}
isSelected(val) {
@ -76,6 +79,295 @@ class SetFilter {
}
}
// Advanced filter forms a MongoSelector from a users String.
// Build by: Ignatz 19.05.2018 (github feuerball11)
class AdvancedFilter {
constructor() {
this._dep = new Tracker.Dependency();
this._filter = '';
this._lastValide = {};
}
set(str) {
this._filter = str;
this._dep.changed();
}
reset() {
this._filter = '';
this._lastValide = {};
this._dep.changed();
}
_isActive() {
this._dep.depend();
return this._filter !== '';
}
_filterToCommands() {
const commands = [];
let current = '';
let string = false;
let wasString = false;
let ignore = false;
for (let i = 0; i < this._filter.length; i++) {
const char = this._filter.charAt(i);
if (ignore) {
ignore = false;
continue;
}
if (char === '\'') {
string = !string;
if (string) wasString = true;
continue;
}
if (char === '\\') {
ignore = true;
continue;
}
if (char === ' ' && !string) {
commands.push({ 'cmd': current, 'string': wasString });
wasString = false;
current = '';
continue;
}
current += char;
}
if (current !== '') {
commands.push({ 'cmd': current, 'string': wasString });
}
return commands;
}
_fieldNameToId(field) {
const found = CustomFields.findOne({ 'name': field });
return found._id;
}
_fieldValueToId(field, value)
{
const found = CustomFields.findOne({ 'name': field });
if (found.settings.dropdownItems && found.settings.dropdownItems.length > 0)
{
for (let i = 0; i < found.settings.dropdownItems.length; i++)
{
if (found.settings.dropdownItems[i].name === value)
{
return found.settings.dropdownItems[i]._id;
}
}
}
return value;
}
_arrayToSelector(commands) {
try {
//let changed = false;
this._processSubCommands(commands);
}
catch (e) { return this._lastValide; }
this._lastValide = { $or: commands };
return { $or: commands };
}
_processSubCommands(commands) {
const subcommands = [];
let level = 0;
let start = -1;
for (let i = 0; i < commands.length; i++) {
if (commands[i].cmd) {
switch (commands[i].cmd) {
case '(':
{
level++;
if (start === -1) start = i;
continue;
}
case ')':
{
level--;
commands.splice(i, 1);
i--;
continue;
}
default:
{
if (level > 0) {
subcommands.push(commands[i]);
commands.splice(i, 1);
i--;
continue;
}
}
}
}
}
if (start !== -1) {
this._processSubCommands(subcommands);
if (subcommands.length === 1)
commands.splice(start, 0, subcommands[0]);
else
commands.splice(start, 0, subcommands);
}
this._processConditions(commands);
this._processLogicalOperators(commands);
}
_processConditions(commands) {
for (let i = 0; i < commands.length; i++) {
if (!commands[i].string && commands[i].cmd) {
switch (commands[i].cmd) {
case '=':
case '==':
case '===':
{
const field = commands[i - 1].cmd;
const str = commands[i + 1].cmd;
commands[i] = { 'customFields._id': this._fieldNameToId(field), 'customFields.value': {$in: [this._fieldValueToId(field, str), parseInt(str, 10)]} };
commands.splice(i - 1, 1);
commands.splice(i, 1);
//changed = true;
i--;
break;
}
case '!=':
case '!==':
{
const field = commands[i - 1].cmd;
const str = commands[i + 1].cmd;
commands[i] = { 'customFields._id': this._fieldNameToId(field), 'customFields.value': { $not: {$in: [this._fieldValueToId(field, str), parseInt(str, 10)]} } };
commands.splice(i - 1, 1);
commands.splice(i, 1);
//changed = true;
i--;
break;
}
case '>':
case 'gt':
case 'Gt':
case 'GT':
{
const field = commands[i - 1].cmd;
const str = commands[i + 1].cmd;
commands[i] = { 'customFields._id': this._fieldNameToId(field), 'customFields.value': { $gt: parseInt(str, 10) } };
commands.splice(i - 1, 1);
commands.splice(i, 1);
//changed = true;
i--;
break;
}
case '>=':
case '>==':
case 'gte':
case 'Gte':
case 'GTE':
{
const field = commands[i - 1].cmd;
const str = commands[i + 1].cmd;
commands[i] = { 'customFields._id': this._fieldNameToId(field), 'customFields.value': { $gte: parseInt(str, 10) } };
commands.splice(i - 1, 1);
commands.splice(i, 1);
//changed = true;
i--;
break;
}
case '<':
case 'lt':
case 'Lt':
case 'LT':
{
const field = commands[i - 1].cmd;
const str = commands[i + 1].cmd;
commands[i] = { 'customFields._id': this._fieldNameToId(field), 'customFields.value': { $lt: parseInt(str, 10) } };
commands.splice(i - 1, 1);
commands.splice(i, 1);
//changed = true;
i--;
break;
}
case '<=':
case '<==':
case 'lte':
case 'Lte':
case 'LTE':
{
const field = commands[i - 1].cmd;
const str = commands[i + 1].cmd;
commands[i] = { 'customFields._id': this._fieldNameToId(field), 'customFields.value': { $lte: parseInt(str, 10) } };
commands.splice(i - 1, 1);
commands.splice(i, 1);
//changed = true;
i--;
break;
}
}
}
}
}
_processLogicalOperators(commands) {
for (let i = 0; i < commands.length; i++) {
if (!commands[i].string && commands[i].cmd) {
switch (commands[i].cmd) {
case 'or':
case 'Or':
case 'OR':
case '|':
case '||':
{
const op1 = commands[i - 1];
const op2 = commands[i + 1];
commands[i] = { $or: [op1, op2] };
commands.splice(i - 1, 1);
commands.splice(i, 1);
//changed = true;
i--;
break;
}
case 'and':
case 'And':
case 'AND':
case '&':
case '&&':
{
const op1 = commands[i - 1];
const op2 = commands[i + 1];
commands[i] = { $and: [op1, op2] };
commands.splice(i - 1, 1);
commands.splice(i, 1);
//changed = true;
i--;
break;
}
case 'not':
case 'Not':
case 'NOT':
case '!':
{
const op1 = commands[i + 1];
commands[i] = { $not: op1 };
commands.splice(i + 1, 1);
//changed = true;
i--;
break;
}
}
}
}
}
_getMongoSelector() {
this._dep.depend();
const commands = this._filterToCommands();
return this._arrayToSelector(commands);
}
}
// The global Filter object.
// XXX It would be possible to re-write this object more elegantly, and removing
// the need to provide a list of `_fields`. We also should move methods into the
@ -86,8 +378,10 @@ Filter = {
// before changing the schema.
labelIds: new SetFilter(),
members: new SetFilter(),
customFields: new SetFilter('_id'),
advanced: new AdvancedFilter(),
_fields: ['labelIds', 'members'],
_fields: ['labelIds', 'members', 'customFields'],
// We don't filter cards that have been added after the last filter change. To
// implement this we keep the id of these cards in this `_exceptions` fields
@ -98,7 +392,7 @@ Filter = {
isActive() {
return _.any(this._fields, (fieldName) => {
return this[fieldName]._isActive();
});
}) || this.advanced._isActive();
},
_getMongoSelector() {
@ -111,7 +405,12 @@ Filter = {
this._fields.forEach((fieldName) => {
const filter = this[fieldName];
if (filter._isActive()) {
filterSelector[fieldName] = filter._getMongoSelector();
if (filter.subField !== '') {
filterSelector[`${fieldName}.${filter.subField}`] = filter._getMongoSelector();
}
else {
filterSelector[fieldName] = filter._getMongoSelector();
}
emptySelector[fieldName] = filter._getEmptySelector();
if (emptySelector[fieldName] !== null) {
includeEmptySelectors = true;
@ -119,13 +418,18 @@ Filter = {
}
});
const exceptionsSelector = {_id: {$in: this._exceptions}};
const exceptionsSelector = { _id: { $in: this._exceptions } };
this._exceptionsDep.depend();
if (includeEmptySelectors)
return {$or: [filterSelector, exceptionsSelector, emptySelector]};
else
return {$or: [filterSelector, exceptionsSelector]};
const selectors = [exceptionsSelector];
if (_.any(this._fields, (fieldName) => {
return this[fieldName]._isActive();
})) selectors.push(filterSelector);
if (includeEmptySelectors) selectors.push(emptySelector);
if (this.advanced._isActive()) selectors.push(this.advanced._getMongoSelector());
return { $or: selectors };
},
mongoSelector(additionalSelector) {
@ -133,7 +437,7 @@ Filter = {
if (_.isUndefined(additionalSelector))
return filterSelector;
else
return {$and: [filterSelector, additionalSelector]};
return { $and: [filterSelector, additionalSelector] };
},
reset() {
@ -141,6 +445,7 @@ Filter = {
const filter = this[fieldName];
filter.reset();
});
this.advanced.reset();
this.resetExceptions();
},