mirror of
https://github.com/wekan/wekan.git
synced 2025-12-25 11:48:48 +01:00
Merge branch 'search' of github.com:jrsupplee/wekan into search
This commit is contained in:
commit
be970e4cea
41 changed files with 633 additions and 8860 deletions
3
.github/ISSUE_TEMPLATE.md
vendored
3
.github/ISSUE_TEMPLATE.md
vendored
|
|
@ -1,5 +1,8 @@
|
|||
## Issue
|
||||
|
||||
Email settings:
|
||||
- https://github.com/wekan/wekan/wiki/Troubleshooting-Mail
|
||||
|
||||
Add these issues to elsewhere:
|
||||
- SECURITY ISSUES: https://github.com/wekan/wekan/blob/master/SECURITY.md
|
||||
- UCS: https://github.com/wekan/univention/issues
|
||||
|
|
|
|||
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -1,11 +1,40 @@
|
|||
# Upcoming Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Added autolinking settings in Admin Panel](https://github.com/wekan/wekan/pull/3633).
|
||||
Thanks to chrisi51.
|
||||
- [Add custom field editing to the REST API](https://github.com/wekan/wekan/pull/3593).
|
||||
Thanks to dudeofawesome.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Try to fix Snap: Removed fibers multi arch from Snap, because Snap build servers do not build correctly with
|
||||
it](https://github.com/wekan/wekan/commit/a44ca39eb84508441f0f8bdac852745f417f12e7).
|
||||
Thanks to xet7.
|
||||
- [Fix search on labels server error](https://github.com/wekan/wekan/pull/3634).
|
||||
Thanks to jrsupplee.
|
||||
- [Fixed Bug: inconsistent use of relative/absolute URLs](https://github.com/wekan/wekan/pull/3635).
|
||||
Thanks to Majed6.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v5.03 2021-03-03 Wekan release
|
||||
|
||||
This release adds the following changes:
|
||||
|
||||
- [Hide email settings from Sandstorm Wekan Admin Panel](https://github.com/wekan/wekan/commit/626f435edf75fac68448ba2e14c62acb749f9c9b).
|
||||
Thanks to ocdtrekkie and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Revert Removed extra imports of Meteor. Hopefully fixes email notifications and rules
|
||||
on old cars not working](https://github.com/wekan/wekan/commit/e4a9dc25ecc230829afea07dbb3915b96115f7f7).
|
||||
Thanks to xet7.
|
||||
- [Fixed Bug: Link at board title can not be edited](https://github.com/wekan/wekan/commit/7d3917adb79be09356d32612585029392bac1e49).
|
||||
Thanks to jonesrussell42, aiac, bbyszio and xet7.
|
||||
|
||||
Thanks to above GitHub and Wekan vanila.io community users for their contributions and translators for their translations.
|
||||
|
||||
# v5.02 2021-03-02 Wekan release
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v5.02.0"
|
||||
appVersion: "v5.03.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ template(name="activity")
|
|||
+viewer
|
||||
= activity.checklist.title
|
||||
else
|
||||
a.activity-checklist(href="{{ activity.card.absoluteUrl }}")
|
||||
a.activity-checklist(href="{{ activity.card.originRelativeUrl }}")
|
||||
+viewer
|
||||
= activity.checklist.title
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ template(name="activity")
|
|||
|
||||
if($eq activity.activityType 'addChecklistItem')
|
||||
| {{{_ 'activity-checklist-item-added' (sanitize activity.checklist.title) cardLink}}}.
|
||||
.activity-checklist(href="{{ activity.card.absoluteUrl }}")
|
||||
.activity-checklist(href="{{ activity.card.originRelativeUrl }}")
|
||||
+viewer
|
||||
= activity.checklistItem.title
|
||||
|
||||
|
|
@ -139,7 +139,7 @@ template(name="activity")
|
|||
//- if we are not in card mode we only display a summary of the comment
|
||||
if($eq activity.activityType 'addComment')
|
||||
| {{{_ 'activity-on' cardLink}}}
|
||||
a.activity-comment(href="{{ activity.card.absoluteUrl }}")
|
||||
a.activity-comment(href="{{ activity.card.originRelativeUrl }}")
|
||||
+viewer
|
||||
= activity.comment.text
|
||||
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ function createCardLink(card) {
|
|||
Blaze.toHTML(
|
||||
HTML.A(
|
||||
{
|
||||
href: card.absoluteUrl(),
|
||||
href: card.originRelativeUrl(),
|
||||
class: 'action-card',
|
||||
},
|
||||
sanitizeXss(card.title),
|
||||
|
|
@ -260,7 +260,7 @@ function createBoardLink(board, list) {
|
|||
Blaze.toHTML(
|
||||
HTML.A(
|
||||
{
|
||||
href: board.absoluteUrl(),
|
||||
href: board.originRelativeUrl(),
|
||||
class: 'action-board',
|
||||
},
|
||||
sanitizeXss(text),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
template(name="boardHeaderBar")
|
||||
h1.header-board-menu
|
||||
with currentBoard
|
||||
a(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}")
|
||||
+viewer
|
||||
= title
|
||||
+viewer
|
||||
= title
|
||||
|
||||
.board-header-btns.left
|
||||
unless isMiniScreen
|
||||
if currentBoard
|
||||
if currentUser
|
||||
with currentBoard
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
|
|
@ -45,6 +48,10 @@ template(name="boardHeaderBar")
|
|||
if currentBoard
|
||||
if isMiniScreen
|
||||
if currentUser
|
||||
with currentBoard
|
||||
a.board-header-btn(class="{{#if currentUser.isBoardAdmin}}js-edit-board-title{{else}}is-disabled{{/if}}" title="{{_ 'edit'}}" value=title)
|
||||
i.fa.fa-pencil-square-o
|
||||
|
||||
a.board-header-btn.js-star-board(class="{{#if isStarred}}is-active{{/if}}"
|
||||
title="{{#if isStarred}}{{_ 'click-to-unstar'}}{{else}}{{_ 'click-to-star'}}{{/if}} {{_ 'starred-boards-description'}}")
|
||||
i.fa(class="fa-star{{#unless isStarred}}-o{{/unless}}")
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ template(name="cardDetails")
|
|||
a.fa.fa-times-thin.close-card-details.js-close-card-details
|
||||
if currentUser.isBoardMember
|
||||
a.fa.fa-navicon.card-details-menu.js-open-card-details-menu
|
||||
input.inline-input(type="text" id="cardURL_copy" value="{{ absoluteUrl }}")
|
||||
input.inline-input(type="text" id="cardURL_copy" value="{{ originRelativeUrl }}")
|
||||
a.fa.fa-link.card-copy-button.js-copy-link(
|
||||
class="fa-link"
|
||||
title="{{_ 'copy-card-link-to-clipboard'}}"
|
||||
value="{{ absoluteUrl }}"
|
||||
value="{{ originRelativeUrl }}"
|
||||
)
|
||||
if isMiniScreen
|
||||
a.fa.fa-times-thin.close-card-details-mobile-web.js-close-card-details
|
||||
|
|
@ -533,7 +533,7 @@ template(name="cardMorePopup")
|
|||
span {{_ 'link-card'}}
|
||||
= ' '
|
||||
i.fa.colorful(class="{{#if board.isPublic}}fa-globe{{else}}fa-lock{{/if}}")
|
||||
input.inline-input(type="text" id="cardURL" readonly value="{{ absoluteUrl }}" autofocus="autofocus")
|
||||
input.inline-input(type="text" id="cardURL" readonly value="{{ originRelativeUrl }}" autofocus="autofocus")
|
||||
button.js-copy-card-link-to-clipboard(class="btn" id="clipboard") {{_ 'copy-card-link-to-clipboard'}}
|
||||
span.clearfix
|
||||
br
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
template(name="resultCard")
|
||||
.result-card-wrapper
|
||||
a.minicard-wrapper.card-title(href=absoluteUrl)
|
||||
a.minicard-wrapper.card-title(href=originRelativeUrl)
|
||||
+minicard(this)
|
||||
//= card.title
|
||||
ul.result-card-context-list
|
||||
li.result-card-context(title="{{_ 'board'}}")
|
||||
.result-card-block-wrapper
|
||||
+viewer
|
||||
= getBoard.title
|
||||
if boardId
|
||||
+viewer
|
||||
= getBoard.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
if getBoard.archived
|
||||
i.fa.fa-archive
|
||||
li.result-card-context.result-card-context-separator
|
||||
|
|
@ -16,8 +20,12 @@ template(name="resultCard")
|
|||
= ' '
|
||||
li.result-card-context(title="{{_ 'swimlane'}}")
|
||||
.result-card-block-wrapper
|
||||
+viewer
|
||||
= getSwimlane.title
|
||||
if swimlaneId
|
||||
+viewer
|
||||
= getSwimlane.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
if getSwimlane.archived
|
||||
i.fa.fa-archive
|
||||
li.result-card-context.result-card-context-separator
|
||||
|
|
@ -26,7 +34,11 @@ template(name="resultCard")
|
|||
= ' '
|
||||
li.result-card-context(title="{{_ 'list'}}")
|
||||
.result-card-block-wrapper
|
||||
+viewer
|
||||
= getList.title
|
||||
if listId
|
||||
+viewer
|
||||
= getList.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
if getList.archived
|
||||
i.fa.fa-archive
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ template(name="listBody")
|
|||
+inlinedForm(autoclose=false position="top")
|
||||
+addCardForm(listId=_id position="top")
|
||||
each (cardsWithLimit (idOrNull ../../_id))
|
||||
a.minicard-wrapper.js-minicard(href=absoluteUrl
|
||||
a.minicard-wrapper.js-minicard(href=originRelativeUrl
|
||||
class="{{#if cardIsSelected}}is-selected{{/if}}"
|
||||
class="{{#if MultiSelection.isSelected _id}}is-checked{{/if}}")
|
||||
if MultiSelection.isActive
|
||||
|
|
|
|||
|
|
@ -3,39 +3,15 @@ template(name="brokenCardsHeaderBar")
|
|||
| {{_ 'broken-cards'}}
|
||||
|
||||
template(name="brokenCards")
|
||||
.wrapper
|
||||
.broken-cards-wrapper
|
||||
each card in brokenCardsList
|
||||
.broken-cards-card-wrapper
|
||||
.broken-cards-card-title
|
||||
= card.title
|
||||
ul.broken-cards-context-list
|
||||
li.broken-cards-context(title="{{_ 'board'}}")
|
||||
if card.boardId
|
||||
+viewer
|
||||
= card.getBoard.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
li.broken-cards-context.broken-cards-context-separator
|
||||
= ' '
|
||||
| {{_ 'context-separator'}}
|
||||
= ' '
|
||||
li.broken-cards-context(title="{{_ 'swimlane'}}")
|
||||
if card.swimlaneId
|
||||
+viewer
|
||||
= card.getSwimlane.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
li.broken-cards-context
|
||||
= ' '
|
||||
| {{_ 'context-separator'}}
|
||||
= ' '
|
||||
li.broken-cards-context(title="{{_ 'list'}}")
|
||||
if card.listId
|
||||
+viewer
|
||||
= card.getList.title
|
||||
else
|
||||
.broken-cards-null
|
||||
| NULL
|
||||
if currentUser
|
||||
if searching.get
|
||||
+spinner
|
||||
else if hasResults.get
|
||||
.global-search-results-list-wrapper
|
||||
if hasQueryErrors.get
|
||||
div
|
||||
each msg in errorMessages
|
||||
span.global-search-error-messages
|
||||
= msg
|
||||
else
|
||||
+resultsPaged(this)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { CardSearchPagedComponent } from "../../lib/cardSearch";
|
||||
|
||||
BlazeComponent.extendComponent({}).register('brokenCardsHeaderBar');
|
||||
|
||||
Template.brokenCards.helpers({
|
||||
|
|
@ -6,23 +8,11 @@ Template.brokenCards.helpers({
|
|||
},
|
||||
});
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
class BrokenCardsComponent extends CardSearchPagedComponent {
|
||||
onCreated() {
|
||||
Meteor.subscribe('setting');
|
||||
Meteor.subscribe('brokenCards');
|
||||
},
|
||||
super.onCreated();
|
||||
|
||||
brokenCardsList() {
|
||||
const selector = {
|
||||
$or: [
|
||||
{ boardId: { $in: [null, ''] } },
|
||||
{ swimlaneId: { $in: [null, ''] } },
|
||||
{ listId: { $in: [null, ''] } },
|
||||
{ permission: 'public' },
|
||||
{ members: { $elemMatch: { userId: user._id, isActive: true } } },
|
||||
],
|
||||
};
|
||||
|
||||
return Cards.find(selector);
|
||||
},
|
||||
}).register('brokenCards');
|
||||
Meteor.subscribe('brokenCards', this.sessionId);
|
||||
}
|
||||
}
|
||||
BrokenCardsComponent.register('brokenCards');
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class DueCardsComponent extends CardSearchPagedComponent {
|
|||
queryParams.users = [Meteor.user().username];
|
||||
}
|
||||
|
||||
this.autorunGlobalSearch(queryParams);
|
||||
this.runGlobalSearch(queryParams);
|
||||
}
|
||||
|
||||
dueCardsView() {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ template(name="globalSearch")
|
|||
= msg
|
||||
else
|
||||
+resultsPaged(this)
|
||||
else if serverError.get
|
||||
.global-search-page
|
||||
.global-search-help
|
||||
h1 {{_ 'server-error' }}
|
||||
+viewer
|
||||
| {{_ 'server-error-troubleshooting' }}
|
||||
else
|
||||
.global-search-page
|
||||
.global-search-help
|
||||
|
|
|
|||
|
|
@ -431,7 +431,7 @@ class GlobalSearchComponent extends CardSearchPagedComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
this.autorunGlobalSearch(params);
|
||||
this.runGlobalSearch(params);
|
||||
}
|
||||
|
||||
searchInstructions() {
|
||||
|
|
@ -477,57 +477,42 @@ class GlobalSearchComponent extends CardSearchPagedComponent {
|
|||
predicate_member: TAPi18n.__('predicate-member'),
|
||||
};
|
||||
|
||||
let text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`;
|
||||
text += `\n${TAPi18n.__('globalSearch-instructions-description', tags)}`;
|
||||
text += `\n\n${TAPi18n.__('globalSearch-instructions-operators', tags)}`;
|
||||
|
||||
let text = '';
|
||||
[
|
||||
'globalSearch-instructions-operator-board',
|
||||
'globalSearch-instructions-operator-list',
|
||||
'globalSearch-instructions-operator-swimlane',
|
||||
'globalSearch-instructions-operator-comment',
|
||||
'globalSearch-instructions-operator-label',
|
||||
'globalSearch-instructions-operator-hash',
|
||||
'globalSearch-instructions-operator-user',
|
||||
'globalSearch-instructions-operator-at',
|
||||
'globalSearch-instructions-operator-member',
|
||||
'globalSearch-instructions-operator-assignee',
|
||||
'globalSearch-instructions-operator-due',
|
||||
'globalSearch-instructions-operator-created',
|
||||
'globalSearch-instructions-operator-modified',
|
||||
'globalSearch-instructions-operator-status',
|
||||
].forEach(instruction => {
|
||||
text += `\n* ${TAPi18n.__(instruction, tags)}`;
|
||||
});
|
||||
|
||||
[
|
||||
'globalSearch-instructions-status-archived',
|
||||
'globalSearch-instructions-status-public',
|
||||
'globalSearch-instructions-status-private',
|
||||
'globalSearch-instructions-status-all',
|
||||
'globalSearch-instructions-status-ended',
|
||||
].forEach(instruction => {
|
||||
text += `\n * ${TAPi18n.__(instruction, tags)}`;
|
||||
});
|
||||
|
||||
[
|
||||
'globalSearch-instructions-operator-has',
|
||||
'globalSearch-instructions-operator-sort',
|
||||
'globalSearch-instructions-operator-limit',
|
||||
].forEach(instruction => {
|
||||
text += `\n* ${TAPi18n.__(instruction, tags)}`;
|
||||
});
|
||||
|
||||
text += `\n## ${TAPi18n.__('heading-notes')}`;
|
||||
[
|
||||
'globalSearch-instructions-notes-1',
|
||||
'globalSearch-instructions-notes-2',
|
||||
'globalSearch-instructions-notes-3',
|
||||
'globalSearch-instructions-notes-3-2',
|
||||
'globalSearch-instructions-notes-4',
|
||||
'globalSearch-instructions-notes-5',
|
||||
].forEach(instruction => {
|
||||
text += `\n* ${TAPi18n.__(instruction, tags)}`;
|
||||
['# ', 'globalSearch-instructions-heading'],
|
||||
['\n', 'globalSearch-instructions-description'],
|
||||
['\n\n', 'globalSearch-instructions-operators'],
|
||||
['\n* ', 'globalSearch-instructions-operator-board'],
|
||||
['\n* ', 'globalSearch-instructions-operator-list'],
|
||||
['\n* ', 'globalSearch-instructions-operator-swimlane'],
|
||||
['\n* ', 'globalSearch-instructions-operator-comment'],
|
||||
['\n* ', 'globalSearch-instructions-operator-label'],
|
||||
['\n* ', 'globalSearch-instructions-operator-hash'],
|
||||
['\n* ', 'globalSearch-instructions-operator-user'],
|
||||
['\n* ', 'globalSearch-instructions-operator-at'],
|
||||
['\n* ', 'globalSearch-instructions-operator-member'],
|
||||
['\n* ', 'globalSearch-instructions-operator-assignee'],
|
||||
['\n* ', 'globalSearch-instructions-operator-due'],
|
||||
['\n* ', 'globalSearch-instructions-operator-created'],
|
||||
['\n* ', 'globalSearch-instructions-operator-modified'],
|
||||
['\n* ', 'globalSearch-instructions-operator-status'],
|
||||
['\n * ', 'globalSearch-instructions-status-archived'],
|
||||
['\n * ', 'globalSearch-instructions-status-public'],
|
||||
['\n * ', 'globalSearch-instructions-status-private'],
|
||||
['\n * ', 'globalSearch-instructions-status-all'],
|
||||
['\n * ', 'globalSearch-instructions-status-ended'],
|
||||
['\n* ', 'globalSearch-instructions-operator-has'],
|
||||
['\n* ', 'globalSearch-instructions-operator-sort'],
|
||||
['\n* ', 'globalSearch-instructions-operator-limit'],
|
||||
['\n## ', 'heading-notes'],
|
||||
['\n* ', 'globalSearch-instructions-notes-1'],
|
||||
['\n* ', 'globalSearch-instructions-notes-2'],
|
||||
['\n* ', 'globalSearch-instructions-notes-3'],
|
||||
['\n* ', 'globalSearch-instructions-notes-3-2'],
|
||||
['\n* ', 'globalSearch-instructions-notes-4'],
|
||||
['\n* ', 'globalSearch-instructions-notes-5'],
|
||||
].forEach(([prefix, instruction]) => {
|
||||
text += `${prefix}${TAPi18n.__(instruction, tags)}`;
|
||||
});
|
||||
|
||||
return text;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ template(name="myCards")
|
|||
each board in myCardsList
|
||||
.my-cards-board-wrapper
|
||||
.my-cards-board-title(class=board.colorClass, id="header")
|
||||
a(href=board.absoluteUrl)
|
||||
a(href=board.originRelativeUrl)
|
||||
+viewer
|
||||
= board.title
|
||||
each swimlane in board.mySwimlanes
|
||||
|
|
@ -46,7 +46,7 @@ template(name="myCards")
|
|||
= list.title
|
||||
each card in list.myCards
|
||||
.my-cards-card-wrapper
|
||||
a.minicard-wrapper(href=card.absoluteUrl)
|
||||
a.minicard-wrapper(href=card.originRelativeUrl)
|
||||
+minicard(card)
|
||||
else
|
||||
.my-cards-dueat-list-wrapper
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class MyCardsComponent extends CardSearchPagedComponent {
|
|||
sort: { name: 'dueAt', order: 'des' },
|
||||
};
|
||||
|
||||
this.autorunGlobalSearch(queryParams);
|
||||
this.runGlobalSearch(queryParams);
|
||||
Meteor.subscribe('setting');
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
client/components/rules/.DS_Store
vendored
BIN
client/components/rules/.DS_Store
vendored
Binary file not shown.
|
|
@ -211,6 +211,10 @@ template(name='layoutSettings')
|
|||
.title {{_ 'custom-top-left-corner-logo-height'}}
|
||||
.form-group
|
||||
input.wekan-form-control#custom-top-left-corner-logo-height(type="text", placeholder="" value="{{currentSetting.customTopLeftCornerLogoHeight}}")
|
||||
li.layout-form
|
||||
.title {{_ 'automatic-linked-url-schemes'}}
|
||||
.form-group
|
||||
textarea#automatic-linked-url-schemes.wekan-form-control= currentSetting.automaticLinkedUrlSchemes
|
||||
li
|
||||
button.js-save-layout.primary {{_ 'save'}}
|
||||
|
||||
|
|
|
|||
|
|
@ -176,6 +176,9 @@ BlazeComponent.extendComponent({
|
|||
const textBelowCustomLoginLogo = $('#text-below-custom-login-logo')
|
||||
.val()
|
||||
.trim();
|
||||
const automaticLinkedUrlSchemes = $('#automatic-linked-url-schemes')
|
||||
.val()
|
||||
.trim();
|
||||
const customTopLeftCornerLogoImageUrl = $(
|
||||
'#custom-top-left-corner-logo-image-url',
|
||||
)
|
||||
|
|
@ -209,6 +212,7 @@ BlazeComponent.extendComponent({
|
|||
customTopLeftCornerLogoHeight,
|
||||
displayAuthenticationMethod,
|
||||
defaultAuthenticationMethod,
|
||||
automaticLinkedUrlSchemes,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ template(name="searchSidebar")
|
|||
.list-body
|
||||
.minilists.clearfix.js-minilists
|
||||
each (lists)
|
||||
a.minilist-wrapper.js-minilist(href=absoluteUrl)
|
||||
a.minilist-wrapper.js-minilist(href=originRelativeUrl)
|
||||
+minilist(this)
|
||||
.minicards.clearfix.js-minicards
|
||||
each (results)
|
||||
a.minicard-wrapper.js-minicard(href=absoluteUrl)
|
||||
a.minicard-wrapper.js-minicard(href=originRelativeUrl)
|
||||
+minicard(this)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,28 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
|||
this.totalHits = 0;
|
||||
this.queryErrors = null;
|
||||
this.resultsPerPage = 25;
|
||||
this.sessionId = SessionData.getSessionId();
|
||||
this.subscriptionHandle = null;
|
||||
this.serverError = new ReactiveVar(false);
|
||||
|
||||
const that = this;
|
||||
this.subscriptionCallbacks = {
|
||||
onReady() {
|
||||
that.getResults();
|
||||
that.searching.set(false);
|
||||
that.hasResults.set(true);
|
||||
that.serverError.set(false);
|
||||
},
|
||||
onError(error) {
|
||||
that.searching.set(false);
|
||||
that.hasResults.set(false);
|
||||
that.serverError.set(true);
|
||||
console.log('Error.reason:', error.reason);
|
||||
console.log('Error.message:', error.message);
|
||||
console.log('Error.stack:', error.stack);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
resetSearch() {
|
||||
|
|
@ -21,15 +43,15 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
|||
this.hasResults.set(false);
|
||||
this.hasQueryErrors.set(false);
|
||||
this.resultsHeading.set('');
|
||||
this.serverError.set(false);
|
||||
this.resultsCount = 0;
|
||||
this.totalHits = 0;
|
||||
this.queryErrors = null;
|
||||
}
|
||||
|
||||
getSessionData() {
|
||||
getSessionData(sessionId) {
|
||||
return SessionData.findOne({
|
||||
userId: Meteor.userId(),
|
||||
sessionId: SessionData.getSessionId(),
|
||||
sessionId: sessionId ? sessionId : SessionData.getSessionId(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +67,7 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
|||
const cards = Cards.find({ _id: { $in: sessionData.cards } }, projection);
|
||||
this.queryErrors = sessionData.errors;
|
||||
if (this.queryErrors.length) {
|
||||
// console.log('queryErrors:', this.queryErrorMessages());
|
||||
this.hasQueryErrors.set(true);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -67,25 +90,21 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
autorunGlobalSearch(params) {
|
||||
this.searching.set(true);
|
||||
stopSubscription() {
|
||||
if (this.subscriptionHandle) {
|
||||
this.subscriptionHandle.stop();
|
||||
}
|
||||
}
|
||||
|
||||
this.autorun(() => {
|
||||
const handle = Meteor.subscribe(
|
||||
'globalSearch',
|
||||
SessionData.getSessionId(),
|
||||
params,
|
||||
);
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (handle.ready()) {
|
||||
this.getResults();
|
||||
this.searching.set(false);
|
||||
this.hasResults.set(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
runGlobalSearch(params) {
|
||||
this.searching.set(true);
|
||||
this.stopSubscription();
|
||||
this.subscriptionHandle = Meteor.subscribe(
|
||||
'globalSearch',
|
||||
this.sessionId,
|
||||
params,
|
||||
this.subscriptionCallbacks,
|
||||
);
|
||||
}
|
||||
|
||||
queryErrorMessages() {
|
||||
|
|
@ -103,37 +122,23 @@ export class CardSearchPagedComponent extends BlazeComponent {
|
|||
}
|
||||
|
||||
nextPage() {
|
||||
const sessionData = this.getSessionData();
|
||||
|
||||
this.autorun(() => {
|
||||
const handle = Meteor.subscribe('nextPage', sessionData.sessionId);
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (handle.ready()) {
|
||||
this.getResults();
|
||||
this.searching.set(false);
|
||||
this.hasResults.set(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
this.searching.set(true);
|
||||
this.stopSubscription();
|
||||
this.subscriptionHandle = Meteor.subscribe(
|
||||
'nextPage',
|
||||
this.sessionId,
|
||||
this.subscriptionCallbacks,
|
||||
);
|
||||
}
|
||||
|
||||
previousPage() {
|
||||
const sessionData = this.getSessionData();
|
||||
|
||||
this.autorun(() => {
|
||||
const handle = Meteor.subscribe('previousPage', sessionData.sessionId);
|
||||
Tracker.nonreactive(() => {
|
||||
Tracker.autorun(() => {
|
||||
if (handle.ready()) {
|
||||
this.getResults();
|
||||
this.searching.set(false);
|
||||
this.hasResults.set(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
this.searching.set(true);
|
||||
this.stopSubscription();
|
||||
this.subscriptionHandle = Meteor.subscribe(
|
||||
'previousPage',
|
||||
this.sessionId,
|
||||
this.subscriptionCallbacks,
|
||||
);
|
||||
}
|
||||
|
||||
getResultsHeading() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"accept": "Přijmout",
|
||||
"act-activity-notify": "Notifikace aktivit",
|
||||
"act-activity-notify": "Oznámení",
|
||||
"act-addAttachment": "přidal(a) přílohu __attachment__ na kartu __card__ ve sloupci __list__ ve swimlane __swimlane__ na tablu __board__",
|
||||
"act-deleteAttachment": "smazal(a) přílohu __attachment__ na kartě __card__ ve sloupci __list__ ve swimlane __swimlane__ na tablu __board__",
|
||||
"act-addSubtask": "přidal(a) podúkol __subtask__ na kartu __card__ ve sloupci __list__ ve swimlane __swimlane__ na tablu __board__",
|
||||
|
|
@ -322,8 +322,8 @@
|
|||
"error-user-notAllowSelf": "Nemůžeš pozvat sám sebe",
|
||||
"error-user-notCreated": "Tento uživatel není vytvořen",
|
||||
"error-username-taken": "Toto uživatelské jméno již existuje",
|
||||
"error-orgname-taken": "This organization name is already taken",
|
||||
"error-teamname-taken": "This team name is already taken",
|
||||
"error-orgname-taken": "Jméno organizace již existuje",
|
||||
"error-teamname-taken": "Jméno týmu již existuje",
|
||||
"error-email-taken": "Tento email byl již použit",
|
||||
"export-board": "Exportovat tablo",
|
||||
"export-board-json": "Exportovat tablo do JSON",
|
||||
|
|
@ -908,7 +908,7 @@
|
|||
"operator-has": "has",
|
||||
"operator-limit": "limit",
|
||||
"predicate-archived": "archivováno",
|
||||
"predicate-open": "open",
|
||||
"predicate-open": "otevřít",
|
||||
"predicate-ended": "ukončeno",
|
||||
"predicate-all": "vše",
|
||||
"predicate-overdue": "po termínu",
|
||||
|
|
@ -919,21 +919,21 @@
|
|||
"predicate-due": "do",
|
||||
"predicate-modified": "modifikováno",
|
||||
"predicate-created": "vytvořeno",
|
||||
"predicate-attachment": "attachment",
|
||||
"predicate-description": "description",
|
||||
"predicate-attachment": "příloha",
|
||||
"predicate-description": "popis",
|
||||
"predicate-checklist": "zaškrtávací seznam",
|
||||
"predicate-start": "začátek",
|
||||
"predicate-end": "konec",
|
||||
"predicate-assignee": "řešitel",
|
||||
"predicate-member": "člen",
|
||||
"predicate-public": "public",
|
||||
"predicate-private": "private",
|
||||
"predicate-public": "veřejný",
|
||||
"predicate-private": "soukromý",
|
||||
"operator-unknown-error": "%s není operátor",
|
||||
"operator-number-expected": "operátor __operator__ očekával číslo, ale dostal '__value__'",
|
||||
"operator-sort-invalid": "pořadí '%s' není platné",
|
||||
"operator-status-invalid": "'%s' není platný stav",
|
||||
"operator-has-invalid": "%s is not a valid existence check",
|
||||
"operator-limit-invalid": "%s is not a valid limit. Limit should be a positive integer.",
|
||||
"operator-limit-invalid": "není platný limit. Limit musí být pozitivní číslo.",
|
||||
"next-page": "Následující stránka",
|
||||
"previous-page": "Předchozí stránka",
|
||||
"heading-notes": "Poznámky",
|
||||
|
|
@ -965,19 +965,19 @@
|
|||
"globalSearch-instructions-notes-1": "Lze zadat více operátorů současně.",
|
||||
"globalSearch-instructions-notes-2": "Podobné operátory jsou spojeny pomocí *OR*. Jsou zobrazeny karty odpovídající kterékoli v podmínek.\n`__operator_list__:Available __operator_list__:Blocked` zobrazí karty obsažené v jakémkoli sloupci s názvem *Blocked* nebo *Available*.",
|
||||
"globalSearch-instructions-notes-3": "Odlišné operátory jsou spojeny pomocí *AND*. Jsou zobrazeny pouze karty, které odpovídají všem definovaným operátorům. `__operator_list__:Available __operator_label__:red` zobrazí pouze karty ve sloupci *Available* with a štítkem *red*.",
|
||||
"globalSearch-instructions-notes-3-2": "Days can be specified as a positive or negative integer or using `__predicate_week__`, `__predicate_month__`, `__predicate_quarter__` or `__predicate_year__` for the current period.",
|
||||
"globalSearch-instructions-notes-3-2": "Dny mohou být specifikovány jako pozitivní nebo negativní číslic, nebo použitím `__predicate_week__`, `__predicate_month__`, `__predicate_quarter__` or `__predicate_year__` for the current period.",
|
||||
"globalSearch-instructions-notes-4": "Vyhledávání rozlišuje velikost písmen.",
|
||||
"globalSearch-instructions-notes-5": "By default archived cards are not searched.",
|
||||
"globalSearch-instructions-notes-5": "Ve výchozím zobrazení nejsou vyhledávány archivované položky.",
|
||||
"link-to-search": "Odkaz na toto vyhledávání",
|
||||
"excel-font": "Arial",
|
||||
"number": "Číslo",
|
||||
"label-colors": "Štítek barvy",
|
||||
"label-names": "Štítek jména",
|
||||
"archived-at": "archivováno",
|
||||
"sort-cards": "Sort Cards",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
"created-at-newest-first": "Created At (Newest First)",
|
||||
"created-at-oldest-first": "Created At (Oldest First)"
|
||||
"sort-cards": "Třídit",
|
||||
"cardsSortPopup-title": "Třídit",
|
||||
"due-date": "Požadovaný termín",
|
||||
"title-alphabetically": "Nadpis (Abecedně)",
|
||||
"created-at-newest-first": "Vyvtořeno (Od nejnovějších)",
|
||||
"created-at-oldest-first": "Vytvořeno (Od nejstarších)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -532,6 +532,7 @@
|
|||
"custom-login-logo-image-url": "Custom Login Logo Image URL",
|
||||
"custom-login-logo-link-url": "Custom Login Logo Link URL",
|
||||
"text-below-custom-login-logo": "Text below Custom Login Logo",
|
||||
"automatic-linked-url-schemes": "Custom URL Schemes which should automatically be clickable. One URL Scheme per line",
|
||||
"username": "Username",
|
||||
"import-usernames": "Import Usernames",
|
||||
"view-it": "View it",
|
||||
|
|
@ -946,7 +947,7 @@
|
|||
"globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:<title>` - cards in swimlanes matching the specified *<title>*",
|
||||
"globalSearch-instructions-operator-comment": "`__operator_comment__:<text>` - cards with a comment containing *<text>*.",
|
||||
"globalSearch-instructions-operator-label": "`__operator_label__:<color>` `__operator_label__:<name>` - cards that have a label matching *<color>* or *<name>",
|
||||
"globalSearch-instructions-operator-hash": "`__operator_label_abbrev__<name | color>` - shorthand for `__operator_label__:<color>` or `__operator_label__:<name>`",
|
||||
"globalSearch-instructions-operator-hash": "`__operator_label_abbrev__<name|color>` - shorthand for `__operator_label__:<color>` or `__operator_label__:<name>`",
|
||||
"globalSearch-instructions-operator-user": "`__operator_user__:<username>` - cards where *<username>* is a *member* or *assignee*",
|
||||
"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*",
|
||||
|
|
@ -978,6 +979,8 @@
|
|||
"sort-cards": "Sort Cards",
|
||||
"cardsSortPopup-title": "Sort Cards",
|
||||
"due-date": "Due Date",
|
||||
"server-error": "Server Error",
|
||||
"server-error-troubleshooting": "Please submit the error generated by the server.\nFor a snap installation on Linux, run: `sudo journalctl -u 'snap.wekan.*'`",
|
||||
"title-alphabetically": "Title (Alphabetically)",
|
||||
"created-at-newest-first": "Created At (Newest First)",
|
||||
"created-at-oldest-first": "Created At (Oldest First)"
|
||||
|
|
|
|||
|
|
@ -944,8 +944,8 @@
|
|||
"globalSearch-instructions-operator-list": "`__operator_list__:<titre>` - cartes dont les listes correspondent à *<titre>*",
|
||||
"globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:<titre>` - cartes dans les couloirs correspondant au *<titre>* spécifié",
|
||||
"globalSearch-instructions-operator-comment": "`__operator_comment__:<texte>` - cartes dont le commentaire contient *<texte>*.",
|
||||
"globalSearch-instructions-operator-label": "`__operator_label__:<color>` `__operator_label__:<name>` - cards that have a label matching *<color>* or *<name>",
|
||||
"globalSearch-instructions-operator-hash": "`__operator_label_abbrev__<name | color>` - shorthand for `__operator_label__:<color>` or `__operator_label__:<name>`",
|
||||
"globalSearch-instructions-operator-label": "`__operator_label__:<couleur>` `__operator_label__:<nom>` - cartes qui ont une étiquette correspondant à *<couleur>* ou à *<nom>*.",
|
||||
"globalSearch-instructions-operator-hash": "`__operator_label_abbrev__<nom|couleur>` - raccourci pour `__operator_label__:<couleur>` ou `__operator_label__:<nom>`",
|
||||
"globalSearch-instructions-operator-user": "`__operator_user__:<username>` - cards where *<username>* is a *member* or *assignee*",
|
||||
"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*",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Actions = new Mongo.Collection('actions');
|
||||
|
||||
Actions.allow({
|
||||
|
|
|
|||
|
|
@ -777,6 +777,9 @@ Boards.helpers({
|
|||
absoluteUrl() {
|
||||
return FlowRouter.url('board', { id: this._id, slug: this.slug });
|
||||
},
|
||||
originRelativeUrl() {
|
||||
return FlowRouter.path('board', { id: this._id, slug: this.slug });
|
||||
},
|
||||
|
||||
colorClass() {
|
||||
return `board-color-${this.color}`;
|
||||
|
|
|
|||
|
|
@ -758,6 +758,14 @@ Cards.helpers({
|
|||
cardId: this._id,
|
||||
});
|
||||
},
|
||||
originRelativeUrl() {
|
||||
const board = this.board();
|
||||
return FlowRouter.path('card', {
|
||||
boardId: board._id,
|
||||
slug: board.slug,
|
||||
cardId: this._id,
|
||||
});
|
||||
},
|
||||
|
||||
canBeRestored() {
|
||||
const list = Lists.findOne({
|
||||
|
|
|
|||
|
|
@ -370,6 +370,180 @@ if (Meteor.isServer) {
|
|||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @operation edit_custom_field
|
||||
* @summary Update a Custom Field
|
||||
*
|
||||
* @param {string} name the name of the custom field
|
||||
* @param {string} type the type of the custom field
|
||||
* @param {string} settings the settings object of the custom field
|
||||
* @param {boolean} showOnCard should we show the custom field on cards?
|
||||
* @param {boolean} automaticallyOnCard should the custom fields automatically be added on cards?
|
||||
* @param {boolean} showLabelOnMiniCard should the label of the custom field be shown on minicards?
|
||||
* @return_type {_id: string}
|
||||
*/
|
||||
JsonRoutes.add(
|
||||
'PUT',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId',
|
||||
(req, res) => {
|
||||
Authentication.checkUserId(req.userId);
|
||||
|
||||
const paramFieldId = req.params.customFieldId;
|
||||
|
||||
if (req.body.hasOwnProperty('name')) {
|
||||
CustomFields.direct.update(
|
||||
{ _id: paramFieldId },
|
||||
{ $set: { name: req.body.name } },
|
||||
);
|
||||
}
|
||||
if (req.body.hasOwnProperty('type')) {
|
||||
CustomFields.direct.update(
|
||||
{ _id: paramFieldId },
|
||||
{ $set: { type: req.body.type } },
|
||||
);
|
||||
}
|
||||
if (req.body.hasOwnProperty('settings')) {
|
||||
CustomFields.direct.update(
|
||||
{ _id: paramFieldId },
|
||||
{ $set: { settings: req.body.settings } },
|
||||
);
|
||||
}
|
||||
if (req.body.hasOwnProperty('showOnCard')) {
|
||||
CustomFields.direct.update(
|
||||
{ _id: paramFieldId },
|
||||
{ $set: { showOnCard: req.body.showOnCard } },
|
||||
);
|
||||
}
|
||||
if (req.body.hasOwnProperty('automaticallyOnCard')) {
|
||||
CustomFields.direct.update(
|
||||
{ _id: paramFieldId },
|
||||
{ $set: { automaticallyOnCard: req.body.automaticallyOnCard } },
|
||||
);
|
||||
}
|
||||
if (req.body.hasOwnProperty('alwaysOnCard')) {
|
||||
CustomFields.direct.update(
|
||||
{ _id: paramFieldId },
|
||||
{ $set: { alwaysOnCard: req.body.alwaysOnCard } },
|
||||
);
|
||||
}
|
||||
if (req.body.hasOwnProperty('showLabelOnMiniCard')) {
|
||||
CustomFields.direct.update(
|
||||
{ _id: paramFieldId },
|
||||
{ $set: { showLabelOnMiniCard: req.body.showLabelOnMiniCard } },
|
||||
);
|
||||
}
|
||||
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: { _id: paramFieldId },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @operation add_custom_field_dropdown_items
|
||||
* @summary Update a Custom Field's dropdown items
|
||||
*
|
||||
* @param {string[]} items names of the custom field
|
||||
* @return_type {_id: string}
|
||||
*/
|
||||
JsonRoutes.add(
|
||||
'POST',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items',
|
||||
(req, res) => {
|
||||
Authentication.checkUserId(req.userId);
|
||||
|
||||
if (req.body.hasOwnProperty('items') && Array.isArray(req.body.items)) {
|
||||
CustomFields.direct.update(
|
||||
{ _id: req.params.customFieldId },
|
||||
{
|
||||
$push: {
|
||||
'settings.dropdownItems': {
|
||||
$each: req.body.items
|
||||
.filter(name => typeof name === 'string')
|
||||
.map(name => ({
|
||||
_id: Random.id(6),
|
||||
name,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: { _id: req.params.customFieldId },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @operation edit_custom_field_dropdown_item
|
||||
* @summary Update a Custom Field's dropdown item
|
||||
*
|
||||
* @param {string} name names of the custom field
|
||||
* @return_type {_id: string}
|
||||
*/
|
||||
JsonRoutes.add(
|
||||
'PUT',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items/:dropdownItemId',
|
||||
(req, res) => {
|
||||
Authentication.checkUserId(req.userId);
|
||||
|
||||
if (req.body.hasOwnProperty('name')) {
|
||||
CustomFields.direct.update(
|
||||
{
|
||||
_id: req.params.customFieldId,
|
||||
'settings.dropdownItems._id': req.params.dropdownItemId,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
'settings.dropdownItems.$': {
|
||||
_id: req.params.dropdownItemId,
|
||||
name: req.body.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: { _id: req.params.customFieldId },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @operation delete_custom_field_dropdown_item
|
||||
* @summary Update a Custom Field's dropdown items
|
||||
*
|
||||
* @param {string} itemId ID of the dropdown item
|
||||
* @return_type {_id: string}
|
||||
*/
|
||||
JsonRoutes.add(
|
||||
'DELETE',
|
||||
'/api/boards/:boardId/custom-fields/:customFieldId/dropdown-items/:dropdownItemId',
|
||||
(req, res) => {
|
||||
Authentication.checkUserId(req.userId);
|
||||
|
||||
CustomFields.direct.update(
|
||||
{ _id: req.params.customFieldId },
|
||||
{
|
||||
$pull: {
|
||||
'settings.dropdownItems': { _id: req.params.dropdownItemId },
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
JsonRoutes.sendResult(res, {
|
||||
code: 200,
|
||||
data: { _id: req.params.customFieldId },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* @operation delete_custom_field
|
||||
* @summary Delete a Custom Fields attached to a board
|
||||
|
|
|
|||
|
|
@ -280,6 +280,10 @@ Lists.helpers({
|
|||
const card = Cards.findOne({ listId: this._id });
|
||||
return card && card.absoluteUrl();
|
||||
},
|
||||
originRelativeUrl() {
|
||||
const card = Cards.findOne({ listId: this._id });
|
||||
return card && card.originRelativeUrl();
|
||||
},
|
||||
remove() {
|
||||
Lists.remove({ _id: this._id });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Rules = new Mongo.Collection('rules');
|
||||
|
||||
Rules.attachSchema(
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ Settings.attachSchema(
|
|||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
automaticLinkedUrlSchemes: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
customTopLeftCornerLogoImageUrl: {
|
||||
type: String,
|
||||
optional: true,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
Triggers = new Mongo.Collection('triggers');
|
||||
|
||||
Triggers.mutations({
|
||||
|
|
|
|||
8522
package-lock.json
generated
8522
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "wekan",
|
||||
"version": "v5.02.0",
|
||||
"version": "v5.03.0",
|
||||
"description": "Open-Source kanban",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -6,23 +6,40 @@ var Markdown = require('markdown-it')({
|
|||
breaks: true,
|
||||
});
|
||||
|
||||
|
||||
// Static URL Scheme Listing
|
||||
var urlschemes = [
|
||||
"aodroplink",
|
||||
"thunderlink",
|
||||
"cbthunderlink",
|
||||
"onenote",
|
||||
"file",
|
||||
"abasurl",
|
||||
"conisio",
|
||||
"mailspring"
|
||||
];
|
||||
|
||||
// Better would be a field in the admin backend to set this dynamically
|
||||
// instead of putting all known or wanted url schemes here hard into code
|
||||
// but i was not able to access those settings
|
||||
// var urlschemes = currentSetting.automaticLinkedUrlSchemes.split('\n');
|
||||
|
||||
// put all url schemes into the linkify configuration to automatically make it clickable
|
||||
for(var i=0; i<urlschemes.length;i++){
|
||||
//console.log("adding autolink for "+urlschemes[i]);
|
||||
Markdown.linkify.add(urlschemes[i]+":",'http:');
|
||||
}
|
||||
|
||||
// Additional safeAttrValue function to allow for other specific protocols
|
||||
// See https://github.com/leizongmin/js-xss/issues/52#issuecomment-241354114
|
||||
function mySafeAttrValue(tag, name, value, cssFilter) {
|
||||
// only when the tag is 'a' and attribute is 'href'
|
||||
// then use your custom function
|
||||
if (tag === 'a' && name === 'href') {
|
||||
// only filter the value if starts with 'cbthunderlink:' or 'aodroplink'
|
||||
if (/^thunderlink:/ig.test(value) ||
|
||||
/^cbthunderlink:/ig.test(value) ||
|
||||
/^aodroplink:/ig.test(value) ||
|
||||
/^onenote:/ig.test(value) ||
|
||||
/^file:/ig.test(value) ||
|
||||
/^abasurl:/ig.test(value) ||
|
||||
/^conisio:/ig.test(value) ||
|
||||
/^mailspring:/ig.test(value)) {
|
||||
return value;
|
||||
}
|
||||
// only filter the value if starts with an registered url scheme
|
||||
urlscheme = value.split(/:\/\//);
|
||||
//console.log("validating "+urlscheme[0]);
|
||||
if(urlschemes.includes(urlscheme[0])) return value;
|
||||
else {
|
||||
// use the default safeAttrValue function to process all non cbthunderlinks
|
||||
return sanitizeXss.safeAttrValue(tag, name, value, cssFilter);
|
||||
|
|
|
|||
|
|
@ -1524,7 +1524,7 @@ var n=this.pipeline.run(e.tokenizer(t)),r=new e.Vector,i=[],o=this._fields.reduc
|
|||
<ul class="toc-list-h1">
|
||||
|
||||
<li>
|
||||
<a href="#wekan-rest-api" class="toc-h1 toc-link" data-title="Wekan REST API v5.02">Wekan REST API v5.02</a>
|
||||
<a href="#wekan-rest-api" class="toc-h1 toc-link" data-title="Wekan REST API v5.03">Wekan REST API v5.03</a>
|
||||
|
||||
</li>
|
||||
|
||||
|
|
@ -2047,7 +2047,7 @@ var n=this.pipeline.run(e.tokenizer(t)),r=new e.Vector,i=[],o=this._fields.reduc
|
|||
<div class="page-wrapper">
|
||||
<div class="dark-box"></div>
|
||||
<div class="content">
|
||||
<h1 id="wekan-rest-api">Wekan REST API v5.01</h1>
|
||||
<h1 id="wekan-rest-api">Wekan REST API v5.03</h1>
|
||||
<blockquote>
|
||||
<p>Scroll down for code samples, example requests and responses. Select a language for code samples from the tabs above or the mobile navigation menu.</p>
|
||||
</blockquote>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
swagger: '2.0'
|
||||
info:
|
||||
title: Wekan REST API
|
||||
version: v5.01
|
||||
version: v5.03
|
||||
description: |
|
||||
The REST API allows you to control and extend Wekan with ease.
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ const pkgdef :Spk.PackageDefinition = (
|
|||
appTitle = (defaultText = "Wekan"),
|
||||
# The name of the app as it is displayed to the user.
|
||||
|
||||
appVersion = 502,
|
||||
appVersion = 503,
|
||||
# Increment this for every release.
|
||||
|
||||
appMarketingVersion = (defaultText = "5.02.0~2021-03-02"),
|
||||
appMarketingVersion = (defaultText = "5.03.0~2021-03-03"),
|
||||
# Human-readable presentation of the app version.
|
||||
|
||||
minUpgradableAppVersion = 0,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ Meteor.publish('myCards', function(sessionId) {
|
|||
// sort: { name: 'dueAt', order: 'des' },
|
||||
};
|
||||
|
||||
return buildQuery(sessionId, queryParams);
|
||||
return findCards(sessionId, buildQuery(queryParams));
|
||||
});
|
||||
|
||||
// Meteor.publish('dueCards', function(sessionId, allUsers = false) {
|
||||
|
|
@ -44,85 +44,104 @@ Meteor.publish('globalSearch', function(sessionId, queryParams) {
|
|||
// eslint-disable-next-line no-console
|
||||
// console.log('queryParams:', queryParams);
|
||||
|
||||
return buildQuery(sessionId, queryParams);
|
||||
return findCards(sessionId, buildQuery(queryParams));
|
||||
});
|
||||
|
||||
function buildQuery(sessionId, queryParams) {
|
||||
const userId = Meteor.userId();
|
||||
class QueryErrors {
|
||||
constructor() {
|
||||
this.notFound = {
|
||||
boards: [],
|
||||
swimlanes: [],
|
||||
lists: [],
|
||||
labels: [],
|
||||
users: [],
|
||||
members: [],
|
||||
assignees: [],
|
||||
status: [],
|
||||
comments: [],
|
||||
};
|
||||
|
||||
const errors = new (class {
|
||||
constructor() {
|
||||
this.notFound = {
|
||||
boards: [],
|
||||
swimlanes: [],
|
||||
lists: [],
|
||||
labels: [],
|
||||
users: [],
|
||||
members: [],
|
||||
assignees: [],
|
||||
status: [],
|
||||
comments: [],
|
||||
};
|
||||
this.colorMap = Boards.colorMap();
|
||||
}
|
||||
|
||||
this.colorMap = Boards.colorMap();
|
||||
}
|
||||
|
||||
hasErrors() {
|
||||
for (const value of Object.values(this.notFound)) {
|
||||
if (value.length) {
|
||||
return true;
|
||||
}
|
||||
hasErrors() {
|
||||
for (const value of Object.values(this.notFound)) {
|
||||
if (value.length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessages() {
|
||||
const messages = [];
|
||||
errorMessages() {
|
||||
const messages = [];
|
||||
|
||||
this.notFound.boards.forEach(board => {
|
||||
messages.push({ tag: 'board-title-not-found', value: board });
|
||||
this.notFound.boards.forEach(board => {
|
||||
messages.push({ tag: 'board-title-not-found', value: board });
|
||||
});
|
||||
this.notFound.swimlanes.forEach(swim => {
|
||||
messages.push({ tag: 'swimlane-title-not-found', value: swim });
|
||||
});
|
||||
this.notFound.lists.forEach(list => {
|
||||
messages.push({ tag: 'list-title-not-found', value: list });
|
||||
});
|
||||
this.notFound.comments.forEach(comments => {
|
||||
comments.forEach(text => {
|
||||
messages.push({ tag: 'comment-not-found', value: text });
|
||||
});
|
||||
this.notFound.swimlanes.forEach(swim => {
|
||||
messages.push({ tag: 'swimlane-title-not-found', value: swim });
|
||||
});
|
||||
this.notFound.lists.forEach(list => {
|
||||
messages.push({ tag: 'list-title-not-found', value: list });
|
||||
});
|
||||
this.notFound.comments.forEach(comments => {
|
||||
comments.forEach(text => {
|
||||
messages.push({ tag: 'comment-not-found', value: text });
|
||||
});
|
||||
this.notFound.labels.forEach(label => {
|
||||
if (Boards.labelColors().includes(label)) {
|
||||
messages.push({
|
||||
tag: 'label-color-not-found',
|
||||
value: label,
|
||||
color: true,
|
||||
});
|
||||
});
|
||||
this.notFound.labels.forEach(label => {
|
||||
} else {
|
||||
messages.push({
|
||||
tag: 'label-not-found',
|
||||
value: label,
|
||||
color: Boards.labelColors().includes(label),
|
||||
color: false,
|
||||
});
|
||||
});
|
||||
this.notFound.users.forEach(user => {
|
||||
messages.push({ tag: 'user-username-not-found', value: user });
|
||||
});
|
||||
this.notFound.members.forEach(user => {
|
||||
messages.push({ tag: 'user-username-not-found', value: user });
|
||||
});
|
||||
this.notFound.assignees.forEach(user => {
|
||||
messages.push({ tag: 'user-username-not-found', value: user });
|
||||
});
|
||||
}
|
||||
});
|
||||
this.notFound.users.forEach(user => {
|
||||
messages.push({ tag: 'user-username-not-found', value: user });
|
||||
});
|
||||
this.notFound.members.forEach(user => {
|
||||
messages.push({ tag: 'user-username-not-found', value: user });
|
||||
});
|
||||
this.notFound.assignees.forEach(user => {
|
||||
messages.push({ tag: 'user-username-not-found', value: user });
|
||||
});
|
||||
|
||||
return messages;
|
||||
return messages;
|
||||
}
|
||||
};
|
||||
|
||||
class Query {
|
||||
params = {};
|
||||
selector = {};
|
||||
projection = {};
|
||||
errors = new QueryErrors();
|
||||
|
||||
constructor(selector, projection) {
|
||||
if (selector) {
|
||||
this.selector = selector;
|
||||
}
|
||||
})();
|
||||
|
||||
if (projection) {
|
||||
this.projection = projection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildSelector(queryParams) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
errors = new QueryErrors();
|
||||
|
||||
let selector = {};
|
||||
let skip = 0;
|
||||
if (queryParams.skip) {
|
||||
skip = queryParams.skip;
|
||||
}
|
||||
let limit = 25;
|
||||
if (queryParams.limit) {
|
||||
limit = queryParams.limit;
|
||||
}
|
||||
|
||||
if (queryParams.selector) {
|
||||
selector = queryParams.selector;
|
||||
|
|
@ -365,6 +384,9 @@ function buildQuery(sessionId, queryParams) {
|
|||
boards.forEach(board => {
|
||||
board.labels
|
||||
.filter(boardLabel => {
|
||||
if (!boardLabel.name) {
|
||||
return false;
|
||||
}
|
||||
return boardLabel.name.match(reLabel);
|
||||
})
|
||||
.forEach(boardLabel => {
|
||||
|
|
@ -476,6 +498,25 @@ function buildQuery(sessionId, queryParams) {
|
|||
// eslint-disable-next-line no-console
|
||||
// console.log('selector.$and:', selector.$and);
|
||||
|
||||
const query = new Query();
|
||||
query.selector = selector;
|
||||
query.params = queryParams;
|
||||
query.errors = errors;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
function buildProjection(query) {
|
||||
|
||||
let skip = 0;
|
||||
if (query.params.skip) {
|
||||
skip = query.params.skip;
|
||||
}
|
||||
let limit = 25;
|
||||
if (query.params.limit) {
|
||||
limit = query.params.limit;
|
||||
}
|
||||
|
||||
const projection = {
|
||||
fields: {
|
||||
_id: 1,
|
||||
|
|
@ -505,9 +546,9 @@ function buildQuery(sessionId, queryParams) {
|
|||
limit,
|
||||
};
|
||||
|
||||
if (queryParams.sort) {
|
||||
const order = queryParams.sort.order === 'asc' ? 1 : -1;
|
||||
switch (queryParams.sort.name) {
|
||||
if (query.params.sort) {
|
||||
const order = query.params.sort.order === 'asc' ? 1 : -1;
|
||||
switch (query.params.sort.name) {
|
||||
case 'dueAt':
|
||||
projection.sort = {
|
||||
dueAt: order,
|
||||
|
|
@ -550,77 +591,33 @@ function buildQuery(sessionId, queryParams) {
|
|||
// eslint-disable-next-line no-console
|
||||
// console.log('projection:', projection);
|
||||
|
||||
return findCards(sessionId, selector, projection, errors);
|
||||
query.projection = projection;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Meteor.publish('brokenCards', function() {
|
||||
const user = Users.findOne({ _id: this.userId });
|
||||
function buildQuery(queryParams) {
|
||||
const query = buildSelector(queryParams);
|
||||
|
||||
const permiitedBoards = [null];
|
||||
let selector = {};
|
||||
selector.$or = [
|
||||
{ permission: 'public' },
|
||||
{ members: { $elemMatch: { userId: user._id, isActive: true } } },
|
||||
];
|
||||
return buildProjection(query);
|
||||
}
|
||||
|
||||
Boards.find(selector).forEach(board => {
|
||||
permiitedBoards.push(board._id);
|
||||
});
|
||||
Meteor.publish('brokenCards', function(sessionId) {
|
||||
|
||||
selector = {
|
||||
boardId: { $in: permiitedBoards },
|
||||
$or: [
|
||||
{ boardId: { $in: [null, ''] } },
|
||||
{ swimlaneId: { $in: [null, ''] } },
|
||||
{ listId: { $in: [null, ''] } },
|
||||
],
|
||||
const queryParams = {
|
||||
users: [Meteor.user().username],
|
||||
// limit: 25,
|
||||
skip: 0,
|
||||
// sort: { name: 'dueAt', order: 'des' },
|
||||
};
|
||||
|
||||
const cards = Cards.find(selector, {
|
||||
fields: {
|
||||
_id: 1,
|
||||
archived: 1,
|
||||
boardId: 1,
|
||||
swimlaneId: 1,
|
||||
listId: 1,
|
||||
title: 1,
|
||||
type: 1,
|
||||
sort: 1,
|
||||
members: 1,
|
||||
assignees: 1,
|
||||
colors: 1,
|
||||
dueAt: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const boards = [];
|
||||
const swimlanes = [];
|
||||
const lists = [];
|
||||
const users = [];
|
||||
|
||||
cards.forEach(card => {
|
||||
if (card.boardId) boards.push(card.boardId);
|
||||
if (card.swimlaneId) swimlanes.push(card.swimlaneId);
|
||||
if (card.listId) lists.push(card.listId);
|
||||
if (card.members) {
|
||||
card.members.forEach(userId => {
|
||||
users.push(userId);
|
||||
});
|
||||
}
|
||||
if (card.assignees) {
|
||||
card.assignees.forEach(userId => {
|
||||
users.push(userId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return [
|
||||
cards,
|
||||
Boards.find({ _id: { $in: boards } }),
|
||||
Swimlanes.find({ _id: { $in: swimlanes } }),
|
||||
Lists.find({ _id: { $in: lists } }),
|
||||
Users.find({ _id: { $in: users } }, { fields: Users.safeFields }),
|
||||
const query = buildQuery(queryParams);
|
||||
query.selector.$or = [
|
||||
{ boardId: { $in: [null, ''] } },
|
||||
{ swimlaneId: { $in: [null, ''] } },
|
||||
{ listId: { $in: [null, ''] } },
|
||||
];
|
||||
|
||||
return findCards(sessionId, query);
|
||||
});
|
||||
|
||||
Meteor.publish('nextPage', function(sessionId) {
|
||||
|
|
@ -630,7 +627,7 @@ Meteor.publish('nextPage', function(sessionId) {
|
|||
const projection = session.getProjection();
|
||||
projection.skip = session.lastHit;
|
||||
|
||||
return findCards(sessionId, session.getSelector(), projection);
|
||||
return findCards(sessionId, new Query(session.getSelector(), projection));
|
||||
});
|
||||
|
||||
Meteor.publish('previousPage', function(sessionId) {
|
||||
|
|
@ -640,20 +637,20 @@ Meteor.publish('previousPage', function(sessionId) {
|
|||
const projection = session.getProjection();
|
||||
projection.skip = session.lastHit - session.resultsCount - projection.limit;
|
||||
|
||||
return findCards(sessionId, session.getSelector(), projection);
|
||||
return findCards(sessionId, new Query(session.getSelector(), projection));
|
||||
});
|
||||
|
||||
function findCards(sessionId, selector, projection, errors = null) {
|
||||
function findCards(sessionId, query) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('selector:', selector);
|
||||
console.log('selector.$and:', selector.$and);
|
||||
console.log('selector:', query.selector);
|
||||
console.log('selector.$and:', query.selector.$and);
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('projection:', projection);
|
||||
let cards;
|
||||
if (!errors || !errors.hasErrors()) {
|
||||
cards = Cards.find(selector, projection);
|
||||
cards = Cards.find(query.selector, query.projection);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
// console.log('count:', cards.count());
|
||||
|
|
@ -664,19 +661,20 @@ function findCards(sessionId, selector, projection, errors = null) {
|
|||
lastHit: 0,
|
||||
resultsCount: 0,
|
||||
cards: [],
|
||||
selector: SessionData.pickle(selector),
|
||||
projection: SessionData.pickle(projection),
|
||||
selector: SessionData.pickle(query.selector),
|
||||
projection: SessionData.pickle(query.projection),
|
||||
errors: query.errors.errorMessages(),
|
||||
},
|
||||
};
|
||||
if (errors) {
|
||||
update.$set.errors = errors.errorMessages();
|
||||
}
|
||||
// if (errors) {
|
||||
// update.$set.errors = errors.errorMessages();
|
||||
// }
|
||||
|
||||
if (cards) {
|
||||
update.$set.totalHits = cards.count();
|
||||
update.$set.lastHit =
|
||||
projection.skip + projection.limit < cards.count()
|
||||
? projection.skip + projection.limit
|
||||
query.projection.skip + query.projection.limit < cards.count()
|
||||
? query.projection.skip + query.projection.limit
|
||||
: cards.count();
|
||||
update.$set.cards = cards.map(card => {
|
||||
return card._id;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ Meteor.publish('setting', () => {
|
|||
customLoginLogoImageUrl: 1,
|
||||
customLoginLogoLinkUrl: 1,
|
||||
textBelowCustomLoginLogo: 1,
|
||||
automaticLinkedUrlSchemes: 1,
|
||||
customTopLeftCornerLogoImageUrl: 1,
|
||||
customTopLeftCornerLogoLinkUrl: 1,
|
||||
customTopLeftCornerLogoHeight: 1,
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ parts:
|
|||
- execstack
|
||||
- nodejs
|
||||
- npm
|
||||
- p7zip-full
|
||||
# - p7zip-full
|
||||
stage-packages:
|
||||
- libfontconfig1
|
||||
override-build: |
|
||||
|
|
@ -128,11 +128,11 @@ parts:
|
|||
find . -name '*.swp' | xargs rm -f
|
||||
cd ../..
|
||||
# Add fibers multi arch
|
||||
cd .build/bundle/programs/server/node_modules/fibers/bin
|
||||
curl https://releases.wekan.team/fibers-multi.7z -o fibers-multi.7z
|
||||
7z x fibers-multi.7z
|
||||
rm fibers-multi.7z
|
||||
cd ../../../../../../..
|
||||
#cd .build/bundle/programs/server/node_modules/fibers/bin
|
||||
#curl https://releases.wekan.team/fibers-multi.7z -o fibers-multi.7z
|
||||
#7z x fibers-multi.7z
|
||||
#rm fibers-multi.7z
|
||||
#cd ../../../../../../..
|
||||
# Copy to Snap
|
||||
cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
|
||||
cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue