diff --git a/client/components/cards/resultCard.jade b/client/components/cards/resultCard.jade index 77f0473af..4a873b8fd 100644 --- a/client/components/cards/resultCard.jade +++ b/client/components/cards/resultCard.jade @@ -5,19 +5,28 @@ template(name="resultCard") //= card.title ul.result-card-context-list li.result-card-context(title="{{_ 'board'}}") - +viewer - = getBoard.title + .result-card-block-wrapper + +viewer + = getBoard.title + if getBoard.archived + i.fa.fa-archive li.result-card-context.result-card-context-separator = ' ' | {{_ 'context-separator'}} = ' ' li.result-card-context(title="{{_ 'swimlane'}}") - +viewer - = getSwimlane.title + .result-card-block-wrapper + +viewer + = getSwimlane.title + if getSwimlane.archived + i.fa.fa-archive li.result-card-context.result-card-context-separator = ' ' | {{_ 'context-separator'}} = ' ' li.result-card-context(title="{{_ 'list'}}") - +viewer - = getList.title + .result-card-block-wrapper + +viewer + = getList.title + if getList.archived + i.fa.fa-archive diff --git a/client/components/cards/resultCard.styl b/client/components/cards/resultCard.styl index def39a4d3..7aa94e90f 100644 --- a/client/components/cards/resultCard.styl +++ b/client/components/cards/resultCard.styl @@ -19,3 +19,6 @@ .result-card-context-list margin-bottom: 0.7rem + +.result-card-block-wrapper + display: inline-block diff --git a/client/components/main/globalSearch.jade b/client/components/main/globalSearch.jade index 3dac5c9ef..61ef2f2c4 100644 --- a/client/components/main/globalSearch.jade +++ b/client/components/main/globalSearch.jade @@ -30,13 +30,23 @@ template(name="globalSearch") div each msg in errorMessages span.global-search-error-messages - | {{_ msg.tag msg.value }} + = msg else h1 = resultsHeading.get a.fa.fa-link(title="{{_ 'link-to-search' }}" href="{{ getSearchHref }}") - each card in results + each card in results.get +resultCard(card) + table.global-search-footer + tr + td.global-search-previous-page + if hasPreviousPage.get + button.js-previous-page + | {{_ 'previous-page' }} + td.global-search-next-page(align="right") + if hasNextPage.get + button.js-next-page + | {{_ 'next-page' }} else .global-search-instructions h2 {{_ 'boards' }} diff --git a/client/components/main/globalSearch.js b/client/components/main/globalSearch.js index 37a4dfc04..e17e33505 100644 --- a/client/components/main/globalSearch.js +++ b/client/components/main/globalSearch.js @@ -45,19 +45,16 @@ BlazeComponent.extendComponent({ this.myLists = new ReactiveVar([]); this.myLabelNames = new ReactiveVar([]); this.myBoardNames = new ReactiveVar([]); + this.results = new ReactiveVar([]); + this.hasNextPage = new ReactiveVar(false); + this.hasPreviousPage = new ReactiveVar(false); this.queryParams = null; this.parsingErrors = []; this.resultsCount = 0; this.totalHits = 0; this.queryErrors = null; this.colorMap = null; - // this.colorMap = {}; - // for (const color of Boards.simpleSchema()._schema['labels.$.color'] - // .allowedValues) { - // this.colorMap[TAPi18n.__(`color-${color}`)] = color; - // } - // // eslint-disable-next-line no-console - // console.log('colorMap:', this.colorMap); + this.resultsPerPage = 25; Meteor.call('myLists', (err, data) => { if (!err) { @@ -80,6 +77,15 @@ BlazeComponent.extendComponent({ onRendered() { Meteor.subscribe('setting'); + + this.colorMap = {}; + for (const color of Boards.simpleSchema()._schema['labels.$.color'] + .allowedValues) { + this.colorMap[TAPi18n.__(`color-${color}`)] = color; + } + // // eslint-disable-next-line no-console + // console.log('colorMap:', this.colorMap); + if (Session.get('globalQuery')) { this.searchAllBoards(Session.get('globalQuery')); } @@ -87,6 +93,7 @@ BlazeComponent.extendComponent({ resetSearch() { this.searching.set(false); + this.results.set([]); this.hasResults.set(false); this.hasQueryErrors.set(false); this.resultsHeading.set(''); @@ -96,79 +103,83 @@ BlazeComponent.extendComponent({ this.queryErrors = null; }, - results() { + getSessionData() { + return SessionData.findOne({ + userId: Meteor.userId(), + sessionId: SessionData.getSessionId(), + }); + }, + + getResults() { // eslint-disable-next-line no-console // console.log('getting results'); if (this.queryParams) { - const results = Cards.globalSearch(this.queryParams); - this.queryErrors = results.errors; + const sessionData = this.getSessionData(); // eslint-disable-next-line no-console - // console.log('errors:', this.queryErrors); - if (this.errorMessages().length) { + console.log('selector:', sessionData.getSelector()); + // console.log('session data:', sessionData); + const cards = Cards.find({ _id: { $in: sessionData.cards } }); + this.queryErrors = sessionData.errors; + if (this.queryErrors.length) { this.hasQueryErrors.set(true); return null; } - if (results.cards) { - const sessionData = SessionData.findOne({ userId: Meteor.userId() }); + if (cards) { this.totalHits = sessionData.totalHits; - this.resultsCount = results.cards.count(); + this.resultsCount = cards.count(); + this.resultsStart = sessionData.lastHit - this.resultsCount + 1; + this.resultsEnd = sessionData.lastHit; this.resultsHeading.set(this.getResultsHeading()); - return results.cards; + this.results.set(cards); + this.hasNextPage.set(sessionData.lastHit < sessionData.totalHits); + this.hasPreviousPage.set( + sessionData.lastHit - sessionData.resultsCount > 0, + ); } } this.resultsCount = 0; - return []; + return null; }, errorMessages() { - const messages = []; - - if (this.queryErrors) { - this.queryErrors.notFound.boards.forEach(board => { - messages.push({ tag: 'board-title-not-found', value: board }); - }); - this.queryErrors.notFound.swimlanes.forEach(swim => { - messages.push({ tag: 'swimlane-title-not-found', value: swim }); - }); - this.queryErrors.notFound.lists.forEach(list => { - messages.push({ tag: 'list-title-not-found', value: list }); - }); - this.queryErrors.notFound.labels.forEach(label => { - const color = Object.entries(this.colorMap) - .filter(value => value[1] === label) - .map(value => value[0]); - if (color.length) { - messages.push({ - tag: 'label-color-not-found', - value: color[0], - }); - } else { - messages.push({ tag: 'label-not-found', value: label }); - } - }); - this.queryErrors.notFound.users.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - this.queryErrors.notFound.members.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); - this.queryErrors.notFound.assignees.forEach(user => { - messages.push({ tag: 'user-username-not-found', value: user }); - }); + if (this.parsingErrors.length) { + return this.parsingErrorMessages(); } + return this.queryErrorMessages(); + }, + + parsingErrorMessages() { + const messages = []; if (this.parsingErrors.length) { this.parsingErrors.forEach(err => { - messages.push(err); + messages.push(TAPi18n.__(err.tag, err.value)); }); } return messages; }, + queryErrorMessages() { + messages = []; + + this.queryErrors.forEach(err => { + let value = err.color ? TAPi18n.__(`color-${err.value}`) : err.value; + if (!value) { + value = err.value; + } + messages.push(TAPi18n.__(err.tag, value)); + }); + + return messages; + }, + searchAllBoards(query) { query = query.trim(); + // eslint-disable-next-line no-console + console.log('query:', query); + this.query.set(query); this.resetSearch(); @@ -177,23 +188,12 @@ BlazeComponent.extendComponent({ return; } - // eslint-disable-next-line no-console - // console.log('query:', query); - this.searching.set(true); - if (!this.colorMap) { - this.colorMap = {}; - for (const color of Boards.simpleSchema()._schema['labels.$.color'] - .allowedValues) { - this.colorMap[TAPi18n.__(`color-${color}`)] = color; - } - } - - const reOperator1 = /^((?\w+):|(?[#@]))(?\w+)(\s+|$)/; - const reOperator2 = /^((?\w+):|(?[#@]))(?["']*)(?.*?)\k(\s+|$)/; - const reText = /^(?\S+)(\s+|$)/; - const reQuotedText = /^(?["'])(?\w+)\k(\s+|$)/; + const reOperator1 = /^((?[\p{Letter}\p{Mark}]+):|(?[#@]))(?[\p{Letter}\p{Mark}]+)(\s+|$)/iu; + const reOperator2 = /^((?[\p{Letter}\p{Mark}]+):|(?[#@]))(?["']*)(?.*?)\k(\s+|$)/iu; + const reText = /^(?\S+)(\s+|$)/u; + const reQuotedText = /^(?["'])(?[\w\p{L}]+)\k(\s+|$)/u; const operators = { 'operator-board': 'boards', @@ -210,20 +210,53 @@ BlazeComponent.extendComponent({ 'operator-member-abbrev': 'members', 'operator-assignee': 'assignees', 'operator-assignee-abbrev': 'assignees', - 'operator-is': 'is', + 'operator-status': 'status', 'operator-due': 'dueAt', 'operator-created': 'createdAt', 'operator-modified': 'modifiedAt', + 'operator-comment': 'comments', }; - const operatorMap = {}; - for (const op in operators) { - operatorMap[TAPi18n.__(op).toLowerCase()] = operators[op]; - } - + const predicates = { + due: { + 'predicate-overdue': 'overdue', + }, + durations: { + 'predicate-week': 'week', + 'predicate-month': 'month', + 'predicate-quarter': 'quarter', + 'predicate-year': 'year', + }, + status: { + 'predicate-archived': 'archived', + 'predicate-all': 'all', + 'predicate-ended': 'ended', + }, + sorts: { + 'predicate-due': 'dueAt', + 'predicate-created': 'createdAt', + 'predicate-modified': 'modifiedAt', + }, + }; + const predicateTranslations = {}; + Object.entries(predicates).forEach(([category, catPreds]) => { + predicateTranslations[category] = {}; + Object.entries(catPreds).forEach(([tag, value]) => { + predicateTranslations[category][TAPi18n.__(tag)] = value; + }); + }); // eslint-disable-next-line no-console - console.log('operatorMap:', operatorMap); + // console.log('predicateTranslations:', predicateTranslations); + + const operatorMap = {}; + Object.entries(operators).forEach(([key, value]) => { + operatorMap[TAPi18n.__(key).toLowerCase()] = value; + }); + // eslint-disable-next-line no-console + // console.log('operatorMap:', operatorMap); + const params = { + limit: this.resultsPerPage, boards: [], swimlanes: [], lists: [], @@ -231,10 +264,11 @@ BlazeComponent.extendComponent({ members: [], assignees: [], labels: [], - is: [], + status: [], dueAt: null, createdAt: null, modifiedAt: null, + comments: [], }; let text = ''; @@ -255,48 +289,73 @@ BlazeComponent.extendComponent({ } else { op = m.groups.abbrev; } - if (op !== '__proto__') { - if (op in operatorMap) { - let value = m.groups.value; - if (operatorMap[op] === 'labels') { - if (value in this.colorMap) { - value = this.colorMap[value]; - } - } else if ( - ['dueAt', 'createdAt', 'modifiedAt'].includes(operatorMap[op]) - ) { - const days = parseInt(value, 10); - if (isNaN(days)) { - if ( - ['day', 'week', 'month', 'quarter', 'year'].includes(value) - ) { - value = moment() - .subtract(1, value) - .format(); - } else { - this.parsingErrors.push({ - tag: 'operator-number-expected', - value: { operator: op, value }, - }); - value = null; - } + if (operatorMap.hasOwnProperty(op)) { + let value = m.groups.value; + if (operatorMap[op] === 'labels') { + if (value in this.colorMap) { + value = this.colorMap[value]; + } + } else if ( + ['dueAt', 'createdAt', 'modifiedAt'].includes(operatorMap[op]) + ) { + let days = parseInt(value, 10); + let duration = null; + if (isNaN(days)) { + if (predicateTranslations.durations[value]) { + duration = predicateTranslations.durations[value]; + value = moment(); + } else if (predicateTranslations.due[value] === 'overdue') { + value = moment(); + duration = 'days'; + days = 0; } else { - value = moment() - .subtract(days, 'days') + this.parsingErrors.push({ + tag: 'operator-number-expected', + value: { operator: op, value }, + }); + value = null; + } + } else { + value = moment(); + } + if (value) { + if (operatorMap[op] === 'dueAt') { + value = value.add(days, duration ? duration : 'days').format(); + } else { + value = value + .subtract(days, duration ? duration : 'days') .format(); } } - if (Array.isArray(params[operatorMap[op]])) { - params[operatorMap[op]].push(value); + } else if (operatorMap[op] === 'sort') { + if (!predicateTranslations.sorts[value]) { + this.parsingErrors.push({ + tag: 'operator-sort-invalid', + value, + }); } else { - params[operatorMap[op]] = value; + value = predicateTranslations.sorts[value]; + } + } else if (operatorMap[op] === 'status') { + if (!predicateTranslations.status[value]) { + this.parsingErrors.push({ + tag: 'operator-status-invalid', + value, + }); + } else { + value = predicateTranslations.status[value]; } - } else { - this.parsingErrors.push({ - tag: 'operator-unknown-error', - value: op, - }); } + if (Array.isArray(params[operatorMap[op]])) { + params[operatorMap[op]].push(value); + } else { + params[operatorMap[op]] = value; + } + } else { + this.parsingErrors.push({ + tag: 'operator-unknown-error', + value: op, + }); } continue; } @@ -324,11 +383,79 @@ BlazeComponent.extendComponent({ this.queryParams = params; + if (this.parsingErrors.length) { + this.searching.set(false); + this.queryErrors = this.parsingErrorMessages(); + this.hasResults.set(true); + this.hasQueryErrors.set(true); + return; + } + this.autorun(() => { - const handle = subManager.subscribe('globalSearch', params); + 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); + } + }); + }); + }); + }, + + nextPage() { + sessionData = this.getSessionData(); + + const params = { + limit: this.resultsPerPage, + selector: sessionData.getSelector(), + skip: sessionData.lastHit, + }; + + 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); + } + }); + }); + }); + }, + + previousPage() { + sessionData = this.getSessionData(); + + const params = { + limit: this.resultsPerPage, + selector: sessionData.getSelector(), + skip: + sessionData.lastHit - sessionData.resultsCount - this.resultsPerPage, + }; + + 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); } @@ -347,8 +474,8 @@ BlazeComponent.extendComponent({ } return TAPi18n.__('n-n-of-n-cards-found', { - start: 1, - end: this.resultsCount, + start: this.resultsStart, + end: this.resultsEnd, total: this.totalHits, }); }, @@ -363,6 +490,7 @@ BlazeComponent.extendComponent({ operator_board: TAPi18n.__('operator-board'), operator_list: TAPi18n.__('operator-list'), operator_swimlane: TAPi18n.__('operator-swimlane'), + operator_comment: TAPi18n.__('operator-comment'), operator_label: TAPi18n.__('operator-label'), operator_label_abbrev: TAPi18n.__('operator-label-abbrev'), operator_user: TAPi18n.__('operator-user'), @@ -371,6 +499,18 @@ BlazeComponent.extendComponent({ operator_member_abbrev: TAPi18n.__('operator-member-abbrev'), operator_assignee: TAPi18n.__('operator-assignee'), operator_assignee_abbrev: TAPi18n.__('operator-assignee-abbrev'), + operator_due: TAPi18n.__('operator-due'), + operator_created: TAPi18n.__('operator-created'), + operator_modified: TAPi18n.__('operator-modified'), + operator_status: TAPi18n.__('operator-status'), + predicate_overdue: TAPi18n.__('predicate-overdue'), + predicate_archived: TAPi18n.__('predicate-archived'), + predicate_all: TAPi18n.__('predicate-all'), + predicate_ended: TAPi18n.__('predicate-ended'), + predicate_week: TAPi18n.__('predicate-week'), + predicate_month: TAPi18n.__('predicate-month'), + predicate_quarter: TAPi18n.__('predicate-quarter'), + predicate_year: TAPi18n.__('predicate-year'), }; text = `# ${TAPi18n.__('globalSearch-instructions-heading')}`; @@ -388,6 +528,10 @@ BlazeComponent.extendComponent({ 'globalSearch-instructions-operator-swimlane', tags, )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-comment', + tags, + )}`; text += `\n* ${TAPi18n.__( 'globalSearch-instructions-operator-label', tags, @@ -409,11 +553,27 @@ BlazeComponent.extendComponent({ 'globalSearch-instructions-operator-assignee', tags, )}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-operator-due', tags)}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-created', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-operator-modified', + tags, + )}`; + text += `\n* ${TAPi18n.__( + 'globalSearch-instructions-status-archived', + tags, + )}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-status-all', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-status-ended', tags)}`; text += `\n## ${TAPi18n.__('heading-notes')}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-1', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-2', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3', tags)}`; + text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-3-2', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-4', tags)}`; text += `\n* ${TAPi18n.__('globalSearch-instructions-notes-5', tags)}`; @@ -435,10 +595,19 @@ BlazeComponent.extendComponent({ evt.preventDefault(); this.searchAllBoards(evt.target.searchQuery.value); }, + 'click .js-next-page'(evt) { + evt.preventDefault(); + this.nextPage(); + }, + 'click .js-previous-page'(evt) { + evt.preventDefault(); + this.previousPage(); + }, 'click .js-label-color'(evt) { evt.preventDefault(); + const input = document.getElementById('global-search-input'); this.query.set( - `${this.query.get()} ${TAPi18n.__('operator-label')}:"${ + `${input.value} ${TAPi18n.__('operator-label')}:"${ evt.currentTarget.textContent }"`, ); @@ -446,8 +615,9 @@ BlazeComponent.extendComponent({ }, 'click .js-board-title'(evt) { evt.preventDefault(); + const input = document.getElementById('global-search-input'); this.query.set( - `${this.query.get()} ${TAPi18n.__('operator-board')}:"${ + `${input.value} ${TAPi18n.__('operator-board')}:"${ evt.currentTarget.textContent }"`, ); @@ -455,8 +625,9 @@ BlazeComponent.extendComponent({ }, 'click .js-list-title'(evt) { evt.preventDefault(); + const input = document.getElementById('global-search-input'); this.query.set( - `${this.query.get()} ${TAPi18n.__('operator-list')}:"${ + `${input.value} ${TAPi18n.__('operator-list')}:"${ evt.currentTarget.textContent }"`, ); @@ -464,8 +635,9 @@ BlazeComponent.extendComponent({ }, 'click .js-label-name'(evt) { evt.preventDefault(); + const input = document.getElementById('global-search-input'); this.query.set( - `${this.query.get()} ${TAPi18n.__('operator-label')}:"${ + `${input.value} ${TAPi18n.__('operator-label')}:"${ evt.currentTarget.textContent }"`, ); diff --git a/client/components/main/globalSearch.styl b/client/components/main/globalSearch.styl index b982f4eed..e460f506e 100644 --- a/client/components/main/globalSearch.styl +++ b/client/components/main/globalSearch.styl @@ -104,3 +104,15 @@ code .list-title background-color: darkgray + +.global-search-footer + border: none + width: 100% + +.global-search-next-page + border: none + text-align: right; + +.global-search-previous-page + border: none + text-align: left; diff --git a/i18n/ar-EG.i18n.json b/i18n/ar-EG.i18n.json index da6d8e09d..436d1871e 100644 --- a/i18n/ar-EG.i18n.json +++ b/i18n/ar-EG.i18n.json @@ -1,6 +1,6 @@ { - "accept": "Accept", - "act-activity-notify": "Activity Notification", + "accept": "قبول", + "act-activity-notify": "اشعار النشاط", "act-addAttachment": "added attachment __attachment__ to card __card__ at list __list__ at swimlane __swimlane__ at board __board__", "act-deleteAttachment": "deleted attachment __attachment__ at card __card__ at list __list__ at swimlane __swimlane__ at board __board__", "act-addSubtask": "added subtask __subtask__ to card __card__ at list __list__ at swimlane __swimlane__ at board __board__", @@ -40,133 +40,133 @@ "act-removeBoardMember": "removed member __member__ from board __board__", "act-restoredCard": "restored card __card__ to list __list__ at swimlane __swimlane__ at board __board__", "act-unjoinMember": "removed member __member__ from card __card__ at list __list__ at swimlane __swimlane__ at board __board__", - "act-withBoardTitle": "__board__", + "act-withBoardTitle": "__لوح__", "act-withCardTitle": "[__board__] __card__", - "actions": "Actions", - "activities": "Activities", - "activity": "Activity", - "activity-added": "added %s to %s", - "activity-archived": "%s moved to Archive", - "activity-attached": "attached %s to %s", - "activity-created": "created %s", - "activity-customfield-created": "created custom field %s", - "activity-excluded": "excluded %s from %s", + "actions": "الإجراءات", + "activities": "الأنشطة", + "activity": "النشاط", + "activity-added": "تمت إضافة %s ل %s", + "activity-archived": "%s انتقل الى الارشيف", + "activity-attached": "إرفاق %s ل %s", + "activity-created": "أنشأ %s", + "activity-customfield-created": "%s احدت حقل مخصص", + "activity-excluded": "استبعاد %s عن %s", "activity-imported": "imported %s into %s from %s", "activity-imported-board": "imported %s from %s", - "activity-joined": "joined %s", - "activity-moved": "moved %s from %s to %s", - "activity-on": "on %s", - "activity-removed": "removed %s from %s", - "activity-sent": "sent %s to %s", - "activity-unjoined": "unjoined %s", - "activity-subtask-added": "added subtask to %s", - "activity-checked-item": "checked %s in checklist %s of %s", - "activity-unchecked-item": "unchecked %s in checklist %s of %s", - "activity-checklist-added": "added checklist to %s", - "activity-checklist-removed": "removed a checklist from %s", + "activity-joined": "انضم %s", + "activity-moved": "تم نقل %s من %s إلى %s", + "activity-on": "على %s", + "activity-removed": "حذف %s إلى %s", + "activity-sent": "إرسال %s إلى %s", + "activity-unjoined": "غادر %s", + "activity-subtask-added": "تم اضافة مهمة فرعية الى %s", + "activity-checked-item": "تحقق %s في قائمة التحقق %s من %s", + "activity-unchecked-item": "ازالة تحقق %s من قائمة التحقق %s من %s", + "activity-checklist-added": "أضاف قائمة تحقق إلى %s", + "activity-checklist-removed": "ازالة قائمة التحقق من %s", "activity-checklist-completed": "completed checklist %s of %s", - "activity-checklist-uncompleted": "uncompleted the checklist %s of %s", - "activity-checklist-item-added": "added checklist item to '%s' in %s", - "activity-checklist-item-removed": "removed a checklist item from '%s' in %s", - "add": "Add", + "activity-checklist-uncompleted": "لم يتم انجاز قائمة التحقق %s من %s", + "activity-checklist-item-added": "تم اضافة عنصر قائمة التحقق الى '%s' في %s", + "activity-checklist-item-removed": "تم ازالة عنصر قائمة التحقق الى '%s' في %s", + "add": "أضف", "activity-checked-item-card": "checked %s in checklist %s", "activity-unchecked-item-card": "unchecked %s in checklist %s", "activity-checklist-completed-card": "completed checklist __checklist__ at card __card__ at list __list__ at swimlane __swimlane__ at board __board__", "activity-checklist-uncompleted-card": "uncompleted the checklist %s", "activity-editComment": "edited comment %s", - "activity-deleteComment": "deleted comment %s", + "activity-deleteComment": "تعليق محذوف %s", "activity-receivedDate": "edited received date to %s of %s", "activity-startDate": "edited start date to %s of %s", "activity-dueDate": "edited due date to %s of %s", "activity-endDate": "edited end date to %s of %s", - "add-attachment": "Add Attachment", - "add-board": "Add Board", - "add-card": "Add Card", + "add-attachment": "إضافة مرفق", + "add-board": "إضافة لوحة", + "add-card": "إضافة بطاقة", "add-swimlane": "Add Swimlane", - "add-subtask": "Add Subtask", - "add-checklist": "Add Checklist", - "add-checklist-item": "Add an item to checklist", - "add-cover": "Add Cover", - "add-label": "Add Label", - "add-list": "Add List", - "add-members": "Add Members", - "added": "Added", - "addMemberPopup-title": "Members", - "admin": "Admin", - "admin-desc": "Can view and edit cards, remove members, and change settings for the board.", - "admin-announcement": "Announcement", + "add-subtask": "إضافة مهمة فرعية", + "add-checklist": "إضافة قائمة تدقيق", + "add-checklist-item": "إضافة عنصر إلى قائمة التحقق", + "add-cover": "إضافة غلاف", + "add-label": "إضافة ملصق", + "add-list": "إضافة قائمة", + "add-members": "إضافة أعضاء", + "added": "أُضيف", + "addMemberPopup-title": "الأعضاء", + "admin": "المدير", + "admin-desc": "إمكانية مشاهدة و تعديل و حذف أعضاء ، و تعديل إعدادات اللوحة أيضا.", + "admin-announcement": "إعلان", "admin-announcement-active": "Active System-Wide Announcement", "admin-announcement-title": "Announcement from Administrator", - "all-boards": "All boards", - "and-n-other-card": "And __count__ other card", - "and-n-other-card_plural": "And __count__ other cards", - "apply": "Apply", + "all-boards": "كل اللوحات", + "and-n-other-card": "And __count__ other بطاقة", + "and-n-other-card_plural": "And __count__ other بطاقات", + "apply": "طبق", "app-is-offline": "Loading, please wait. Refreshing the page will cause data loss. If loading does not work, please check that server has not stopped.", - "archive": "Move to Archive", - "archive-all": "Move All to Archive", - "archive-board": "Move Board to Archive", - "archive-card": "Move Card to Archive", - "archive-list": "Move List to Archive", - "archive-swimlane": "Move Swimlane to Archive", - "archive-selection": "Move selection to Archive", - "archiveBoardPopup-title": "Move Board to Archive?", - "archived-items": "Archive", - "archived-boards": "Boards in Archive", - "restore-board": "Restore Board", - "no-archived-boards": "No Boards in Archive.", - "archives": "Archive", - "template": "Template", - "templates": "Templates", - "assign-member": "Assign member", - "attached": "attached", - "attachment": "Attachment", - "attachment-delete-pop": "Deleting an attachment is permanent. There is no undo.", - "attachmentDeletePopup-title": "Delete Attachment?", - "attachments": "Attachments", - "auto-watch": "Automatically watch boards when they are created", + "archive": "نقل الى الارشيف", + "archive-all": "نقل الكل الى الارشيف", + "archive-board": "نقل اللوح الى الارشيف", + "archive-card": "نقل البطاقة الى الارشيف", + "archive-list": "نقل القائمة الى الارشيف", + "archive-swimlane": "نقل خط السباحة الى الارشيف", + "archive-selection": "نقل التحديد إلى الأرشيف", + "archiveBoardPopup-title": "نقل الوح إلى الأرشيف", + "archived-items": "أرشيف", + "archived-boards": "الالواح في الأرشيف", + "restore-board": "استعادة اللوحة", + "no-archived-boards": "لا توجد لوحات في الأرشيف.", + "archives": "أرشيف", + "template": "نموذج", + "templates": "نماذج", + "assign-member": "تعيين عضو", + "attached": "أُرفق)", + "attachment": "مرفق", + "attachment-delete-pop": "حذف المرق هو حذف نهائي . لا يمكن التراجع إذا حذف.", + "attachmentDeletePopup-title": "تريد حذف المرفق ?", + "attachments": "المرفقات", + "auto-watch": "مراقبة لوحات تلقائيا عندما يتم إنشاؤها", "avatar-too-big": "The avatar is too large (520KB max)", - "back": "Back", - "board-change-color": "Change color", - "board-nb-stars": "%s stars", - "board-not-found": "Board not found", - "board-private-info": "This board will be private.", - "board-public-info": "This board will be public.", - "boardChangeColorPopup-title": "Change Board Background", - "boardChangeTitlePopup-title": "Rename Board", - "boardChangeVisibilityPopup-title": "Change Visibility", - "boardChangeWatchPopup-title": "Change Watch", + "back": "رجوع", + "board-change-color": "تغيير اللومr", + "board-nb-stars": "%s نجوم", + "board-not-found": "لوحة مفقودة", + "board-private-info": "سوف تصبح هذه اللوحة خاصة", + "board-public-info": "سوف تصبح هذه اللوحة عامّة.", + "boardChangeColorPopup-title": "تعديل خلفية الشاشة", + "boardChangeTitlePopup-title": "إعادة تسمية اللوحة", + "boardChangeVisibilityPopup-title": "تعديل وضوح الرؤية", + "boardChangeWatchPopup-title": "تغيير المتابعة", "boardMenuPopup-title": "Board Settings", - "boardChangeViewPopup-title": "Board View", - "boards": "Boards", - "board-view": "Board View", - "board-view-cal": "Calendar", - "board-view-swimlanes": "Swimlanes", + "boardChangeViewPopup-title": "عرض اللوحات", + "boards": "لوحات", + "board-view": "عرض اللوحات", + "board-view-cal": "التقويم", + "board-view-swimlanes": "خطوط السباحة", "board-view-collapse": "Collapse", "board-view-gantt": "Gantt", - "board-view-lists": "Lists", - "bucket-example": "Like “Bucket List” for example", - "cancel": "Cancel", - "card-archived": "This card is moved to Archive.", - "board-archived": "This board is moved to Archive.", - "card-comments-title": "This card has %s comment.", - "card-delete-notice": "Deleting is permanent. You will lose all actions associated with this card.", - "card-delete-pop": "All actions will be removed from the activity feed and you won't be able to re-open the card. There is no undo.", - "card-delete-suggest-archive": "You can move a card to Archive to remove it from the board and preserve the activity.", - "card-due": "Due", - "card-due-on": "Due on", - "card-spent": "Spent Time", - "card-edit-attachments": "Edit attachments", - "card-edit-custom-fields": "Edit custom fields", - "card-edit-labels": "Edit labels", - "card-edit-members": "Edit members", - "card-labels-title": "Change the labels for the card.", - "card-members-title": "Add or remove members of the board from the card.", - "card-start": "Start", - "card-start-on": "Starts on", - "cardAttachmentsPopup-title": "Attach From", - "cardCustomField-datePopup-title": "Change date", - "cardCustomFieldsPopup-title": "Edit custom fields", - "cardStartVotingPopup-title": "Start a vote", + "board-view-lists": "اللستات", + "bucket-example": "مثل « todo list » على سبيل المثال", + "cancel": "إلغاء", + "card-archived": "البطاقة منقولة الى الارشيف", + "board-archived": "اللوحات منقولة الى الارشيف", + "card-comments-title": "%s تعليقات لهذه البطاقة", + "card-delete-notice": "هذا حذف أبديّ . سوف تفقد كل الإجراءات المنوطة بهذه البطاقة", + "card-delete-pop": "سيتم إزالة جميع الإجراءات من تبعات النشاط، وأنك لن تكون قادرا على إعادة فتح البطاقة. لا يوجد التراجع.", + "card-delete-suggest-archive": "يمكنك نقل بطاقة إلى الأرشيف لإزالتها من اللوحة والمحافظة على النشاط.", + "card-due": "مستحق", + "card-due-on": "مستحق في", + "card-spent": "امضى وقتا", + "card-edit-attachments": "تعديل المرفقات", + "card-edit-custom-fields": "تعديل الحقل المعدل", + "card-edit-labels": "تعديل العلامات", + "card-edit-members": "تعديل الأعضاء", + "card-labels-title": "تعديل علامات البطاقة.", + "card-members-title": "إضافة او حذف أعضاء للبطاقة.", + "card-start": "بداية", + "card-start-on": "يبدأ في", + "cardAttachmentsPopup-title": "إرفاق من", + "cardCustomField-datePopup-title": "تغير التاريخ", + "cardCustomFieldsPopup-title": "تعديل الحقل المعدل", + "cardStartVotingPopup-title": "ابدأ تصويت", "positiveVoteMembersPopup-title": "Proponents", "negativeVoteMembersPopup-title": "Opponents", "card-edit-voting": "Edit voting", @@ -174,46 +174,46 @@ "allowNonBoardMembers": "Allow all logged in users", "vote-question": "Voting question", "vote-public": "Show who voted what", - "vote-for-it": "for it", - "vote-against": "against", + "vote-for-it": "مع", + "vote-against": "ضد", "deleteVotePopup-title": "Delete vote?", "vote-delete-pop": "Deleting is permanent. You will lose all actions associated with this vote.", - "cardDeletePopup-title": "Delete Card?", - "cardDetailsActionsPopup-title": "Card Actions", - "cardLabelsPopup-title": "Labels", - "cardMembersPopup-title": "Members", - "cardMorePopup-title": "More", + "cardDeletePopup-title": "حذف البطاقة ?", + "cardDetailsActionsPopup-title": "إجراءات على البطاقة", + "cardLabelsPopup-title": "علامات", + "cardMembersPopup-title": "أعضاء", + "cardMorePopup-title": "المزيد", "cardTemplatePopup-title": "Create template", - "cards": "Cards", - "cards-count": "Cards", - "casSignIn": "Sign In with CAS", - "cardType-card": "Card", - "cardType-linkedCard": "Linked Card", + "cards": "بطاقات", + "cards-count": "بطاقات", + "casSignIn": "تسجيل الدخول مع CAS", + "cardType-card": "بطاقة", + "cardType-linkedCard": "البطاقة المرتبطة", "cardType-linkedBoard": "Linked Board", "change": "Change", - "change-avatar": "Change Avatar", - "change-password": "Change Password", - "change-permissions": "Change permissions", - "change-settings": "Change Settings", - "changeAvatarPopup-title": "Change Avatar", - "changeLanguagePopup-title": "Change Language", - "changePasswordPopup-title": "Change Password", - "changePermissionsPopup-title": "Change Permissions", - "changeSettingsPopup-title": "Change Settings", - "subtasks": "Subtasks", - "checklists": "Checklists", - "click-to-star": "Click to star this board.", - "click-to-unstar": "Click to unstar this board.", + "change-avatar": "تعديل الصورة الشخصية", + "change-password": "تغيير كلمة المرور", + "change-permissions": "تعديل الصلاحيات", + "change-settings": "تغيير الاعدادات", + "changeAvatarPopup-title": "تعديل الصورة الشخصية", + "changeLanguagePopup-title": "تغيير اللغة", + "changePasswordPopup-title": "تغيير كلمة المرور", + "changePermissionsPopup-title": "تعديل الصلاحيات", + "changeSettingsPopup-title": "تغيير الاعدادات", + "subtasks": "المهمات الفرعية", + "checklists": "قوائم التّدقيق", + "click-to-star": "اضغط لإضافة اللوحة للمفضلة.", + "click-to-unstar": "اضغط لحذف اللوحة من المفضلة.", "clipboard": "Clipboard or drag & drop", - "close": "Close", - "close-board": "Close Board", + "close": "غلق", + "close-board": "غلق اللوحة", "close-board-pop": "You will be able to restore the board by clicking the “Archive” button from the home header.", "color-black": "black", "color-blue": "blue", "color-crimson": "crimson", - "color-darkgreen": "darkgreen", - "color-gold": "gold", - "color-gray": "gray", + "color-darkgreen": "اخضر غامق", + "color-gold": "ذهبي", + "color-gray": "رمادي", "color-green": "green", "color-indigo": "indigo", "color-lime": "lime", @@ -228,75 +228,75 @@ "color-purple": "purple", "color-red": "red", "color-saddlebrown": "saddlebrown", - "color-silver": "silver", + "color-silver": "فضي", "color-sky": "sky", "color-slateblue": "slateblue", - "color-white": "white", + "color-white": "أبيض", "color-yellow": "yellow", "unset-color": "Unset", - "comment": "Comment", - "comment-placeholder": "Write Comment", - "comment-only": "Comment only", - "comment-only-desc": "Can comment on cards only.", - "no-comments": "No comments", + "comment": "تعليق", + "comment-placeholder": "أكتب تعليق", + "comment-only": "التعليق فقط", + "comment-only-desc": "يمكن التعليق على بطاقات فقط.", + "no-comments": "لا يوجد تعليقات", "no-comments-desc": "Can not see comments and activities.", "worker": "Worker", "worker-desc": "Can only move cards, assign itself to card and comment.", - "computer": "Computer", + "computer": "حاسوب", "confirm-subtask-delete-dialog": "Are you sure you want to delete subtask?", "confirm-checklist-delete-dialog": "Are you sure you want to delete checklist?", - "copy-card-link-to-clipboard": "Copy card link to clipboard", - "linkCardPopup-title": "Link Card", - "searchElementPopup-title": "Search", - "copyCardPopup-title": "Copy Card", + "copy-card-link-to-clipboard": "نسخ رابط البطاقة إلى الحافظة", + "linkCardPopup-title": "ربط البطاقة", + "searchElementPopup-title": "بحث", + "copyCardPopup-title": "نسخ البطاقة", "copyChecklistToManyCardsPopup-title": "Copy Checklist Template to Many Cards", "copyChecklistToManyCardsPopup-instructions": "Destination Card Titles and Descriptions in this JSON format", "copyChecklistToManyCardsPopup-format": "[ {\"title\": \"First card title\", \"description\":\"First card description\"}, {\"title\":\"Second card title\",\"description\":\"Second card description\"},{\"title\":\"Last card title\",\"description\":\"Last card description\"} ]", - "create": "Create", - "createBoardPopup-title": "Create Board", - "chooseBoardSourcePopup-title": "Import board", - "createLabelPopup-title": "Create Label", - "createCustomField": "Create Field", - "createCustomFieldPopup-title": "Create Field", - "current": "current", + "create": "إنشاء", + "createBoardPopup-title": "إنشاء لوحة", + "chooseBoardSourcePopup-title": "استيراد لوحة", + "createLabelPopup-title": "إنشاء علامة", + "createCustomField": "انشاء حقل", + "createCustomFieldPopup-title": "انشاء حقل", + "current": "الحالي", "custom-field-delete-pop": "There is no undo. This will remove this custom field from all cards and destroy its history.", "custom-field-checkbox": "Checkbox", "custom-field-currency": "Currency", "custom-field-currency-option": "Currency Code", - "custom-field-date": "Date", + "custom-field-date": "تاريخ", "custom-field-dropdown": "Dropdown List", "custom-field-dropdown-none": "(none)", "custom-field-dropdown-options": "List Options", "custom-field-dropdown-options-placeholder": "Press enter to add more options", "custom-field-dropdown-unknown": "(unknown)", - "custom-field-number": "Number", - "custom-field-text": "Text", + "custom-field-number": "رقم", + "custom-field-text": "نص", "custom-fields": "Custom Fields", - "date": "Date", + "date": "تاريخ", "decline": "Decline", - "default-avatar": "Default avatar", - "delete": "Delete", + "default-avatar": "صورة شخصية افتراضية", + "delete": "حذف", "deleteCustomFieldPopup-title": "Delete Custom Field?", - "deleteLabelPopup-title": "Delete Label?", - "description": "Description", - "disambiguateMultiLabelPopup-title": "Disambiguate Label Action", - "disambiguateMultiMemberPopup-title": "Disambiguate Member Action", - "discard": "Discard", + "deleteLabelPopup-title": "حذف العلامة ?", + "description": "وصف", + "disambiguateMultiLabelPopup-title": "تحديد الإجراء على العلامة", + "disambiguateMultiMemberPopup-title": "تحديد الإجراء على العضو", + "discard": "التخلص منها", "done": "Done", - "download": "Download", - "edit": "Edit", - "edit-avatar": "Change Avatar", - "edit-profile": "Edit Profile", + "download": "تنزيل", + "edit": "تعديل", + "edit-avatar": "تعديل الصورة الشخصية", + "edit-profile": "تعديل الملف الشخصي", "edit-wip-limit": "Edit WIP Limit", "soft-wip-limit": "Soft WIP Limit", - "editCardStartDatePopup-title": "Change start date", - "editCardDueDatePopup-title": "Change due date", + "editCardStartDatePopup-title": "تغيير تاريخ البدء", + "editCardDueDatePopup-title": "تغيير تاريخ الاستحقاق", "editCustomFieldPopup-title": "Edit Field", "editCardSpentTimePopup-title": "Change spent time", - "editLabelPopup-title": "Change Label", - "editNotificationPopup-title": "Edit Notification", - "editProfilePopup-title": "Edit Profile", - "email": "Email", + "editLabelPopup-title": "تعديل العلامة", + "editNotificationPopup-title": "تصحيح الإشعار", + "editProfilePopup-title": "تعديل الملف الشخصي", + "email": "البريد الإلكتروني", "email-enrollAccount-subject": "An account created for you on __siteName__", "email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.", "email-fail": "Sending email failed", @@ -319,10 +319,10 @@ "error-csv-schema": "Your CSV(Comma Separated Values)/TSV (Tab Separated Values) does not include the proper information in the correct format", "error-list-doesNotExist": "This list does not exist", "error-user-doesNotExist": "This user does not exist", - "error-user-notAllowSelf": "You can not invite yourself", + "error-user-notAllowSelf": "لا يمكنك دعوة نفسك", "error-user-notCreated": "This user is not created", - "error-username-taken": "This username is already taken", - "error-email-taken": "Email has already been taken", + "error-username-taken": "إسم المستخدم مأخوذ مسبقا", + "error-email-taken": "البريد الإلكتروني مأخوذ بالفعل", "export-board": "Export board", "export-board-json": "Export board to JSON", "export-board-csv": "Export board to CSV", @@ -340,289 +340,289 @@ "list-label-short-modifiedAt": "(L)", "list-label-short-title": "(N)", "list-label-short-sort": "(M)", - "filter": "Filter", + "filter": "تصفية", "filter-cards": "Filter Cards or Lists", "list-filter-label": "Filter List by Title", - "filter-clear": "Clear filter", + "filter-clear": "مسح التصفية", "filter-labels-label": "Filter by label", - "filter-no-label": "No label", + "filter-no-label": "لا يوجد ملصق", "filter-member-label": "Filter by member", - "filter-no-member": "No member", + "filter-no-member": "ليس هناك أي عضو", "filter-assignee-label": "Filter by assignee", "filter-no-assignee": "No assignee", "filter-custom-fields-label": "Filter by Custom Fields", "filter-no-custom-fields": "No Custom Fields", "filter-show-archive": "Show archived lists", "filter-hide-empty": "Hide empty lists", - "filter-on": "Filter is on", - "filter-on-desc": "You are filtering cards on this board. Click here to edit filter.", - "filter-to-selection": "Filter to selection", + "filter-on": "التصفية تشتغل", + "filter-on-desc": "أنت بصدد تصفية بطاقات هذه اللوحة. اضغط هنا لتعديل التصفية.", + "filter-to-selection": "تصفية بالتحديد", "other-filters-label": "Other Filters", "advanced-filter-label": "Advanced Filter", "advanced-filter-description": "Advanced Filter allows to write a string containing following operators: == != <= >= && || ( ) A space is used as a separator between the Operators. You can filter for all Custom Fields by typing their names and values. For Example: Field1 == Value1. Note: If fields or values contains spaces, you need to encapsulate them into single quotes. For Example: 'Field 1' == 'Value 1'. For single control characters (' \\/) to be skipped, you can use \\. For example: Field1 == I\\'m. Also you can combine multiple conditions. For Example: F1 == V1 || F1 == V2. Normally all operators are interpreted from left to right. You can change the order by placing brackets. For Example: F1 == V1 && ( F2 == V2 || F2 == V3 ). Also you can search text fields using regex: F1 == /Tes.*/i", - "fullname": "Full Name", - "header-logo-title": "Go back to your boards page.", - "hide-system-messages": "Hide system messages", - "headerBarCreateBoardPopup-title": "Create Board", - "home": "Home", + "fullname": "الإسم الكامل", + "header-logo-title": "الرجوع إلى صفحة اللوحات", + "hide-system-messages": "إخفاء رسائل النظام", + "headerBarCreateBoardPopup-title": "إنشاء لوحة", + "home": "الرئيسية", "import": "Import", "impersonate-user": "Impersonate user", - "link": "Link", - "import-board": "import board", - "import-board-c": "Import board", + "link": "رابط", + "import-board": "استيراد لوحة", + "import-board-c": "استيراد لوحة", "import-board-title-trello": "Import board from Trello", "import-board-title-wekan": "Import board from previous export", "import-board-title-csv": "Import board from CSV/TSV", - "from-trello": "From Trello", + "from-trello": "من تريلو", "from-wekan": "From previous export", "from-csv": "From CSV/TSV", - "import-board-instruction-trello": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text.", + "import-board-instruction-trello": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text", "import-board-instruction-csv": "Paste in your Comma Separated Values(CSV)/ Tab Separated Values (TSV) .", "import-board-instruction-wekan": "In your board, go to 'Menu', then 'Export board', and copy the text in the downloaded file.", "import-board-instruction-about-errors": "If you get errors when importing board, sometimes importing still works, and board is at All Boards page.", "import-json-placeholder": "Paste your valid JSON data here", "import-csv-placeholder": "Paste your valid CSV/TSV data here", - "import-map-members": "Map members", + "import-map-members": "رسم خريطة الأعضاء", "import-members-map": "Your imported board has some members. Please map the members you want to import to your users", "import-show-user-mapping": "Review members mapping", "import-user-select": "Pick your existing user you want to use as this member", "importMapMembersAddPopup-title": "Select member", - "info": "Version", - "initials": "Initials", - "invalid-date": "Invalid date", + "info": "الإصدار", + "initials": "أولية", + "invalid-date": "تاريخ غير صالح", "invalid-time": "Invalid time", "invalid-user": "Invalid user", - "joined": "joined", + "joined": "انضمّ", "just-invited": "You are just invited to this board", - "keyboard-shortcuts": "Keyboard shortcuts", - "label-create": "Create Label", - "label-default": "%s label (default)", - "label-delete-pop": "There is no undo. This will remove this label from all cards and destroy its history.", - "labels": "Labels", - "language": "Language", - "last-admin-desc": "You can’t change roles because there must be at least one admin.", - "leave-board": "Leave Board", + "keyboard-shortcuts": "اختصار لوحة المفاتيح", + "label-create": "إنشاء علامة", + "label-default": "%s علامة (افتراضية)", + "label-delete-pop": "لا يوجد تراجع. سيؤدي هذا إلى إزالة هذه العلامة من جميع بطاقات والقضاء على تأريخها", + "labels": "علامات", + "language": "لغة", + "last-admin-desc": "لا يمكن تعديل الأدوار لأن ذلك يتطلب صلاحيات المدير.", + "leave-board": "مغادرة اللوحة", "leave-board-pop": "Are you sure you want to leave __boardTitle__? You will be removed from all cards on this board.", - "leaveBoardPopup-title": "Leave Board ?", - "link-card": "Link to this card", + "leaveBoardPopup-title": "مغادرة اللوحة ؟", + "link-card": "ربط هذه البطاقة", "list-archive-cards": "Move all cards in this list to Archive", "list-archive-cards-pop": "This will remove all the cards in this list from the board. To view cards in Archive and bring them back to the board, click “Menu” > “Archive”.", - "list-move-cards": "Move all cards in this list", - "list-select-cards": "Select all cards in this list", + "list-move-cards": "نقل بطاقات هذه القائمة", + "list-select-cards": "تحديد بطاقات هذه القائمة", "set-color-list": "Set Color", - "listActionPopup-title": "List Actions", + "listActionPopup-title": "قائمة الإجراءات", "settingsUserPopup-title": "User Settings", "swimlaneActionPopup-title": "Swimlane Actions", "swimlaneAddPopup-title": "Add a Swimlane below", "listImportCardPopup-title": "Import a Trello card", "listImportCardsTsvPopup-title": "Import Excel CSV/TSV", - "listMorePopup-title": "More", - "link-list": "Link to this list", + "listMorePopup-title": "المزيد", + "link-list": "رابط إلى هذه القائمة", "list-delete-pop": "All actions will be removed from the activity feed and you won't be able to recover the list. There is no undo.", "list-delete-suggest-archive": "You can move a list to Archive to remove it from the board and preserve the activity.", - "lists": "Lists", - "swimlanes": "Swimlanes", - "log-out": "Log Out", - "log-in": "Log In", - "loginPopup-title": "Log In", - "memberMenuPopup-title": "Member Settings", - "members": "Members", - "menu": "Menu", + "lists": "استات", + "swimlanes": "خطوط السباحة", + "log-out": "تسجيل الخروج", + "log-in": "تسجيل الدخول", + "loginPopup-title": "تسجيل الدخول", + "memberMenuPopup-title": "أفضليات الأعضاء", + "members": "أعضاء", + "menu": "القائمة", "move-selection": "Move selection", - "moveCardPopup-title": "Move Card", - "moveCardToBottom-title": "Move to Bottom", - "moveCardToTop-title": "Move to Top", + "moveCardPopup-title": "نقل البطاقة", + "moveCardToBottom-title": "التحرك إلى القاع", + "moveCardToTop-title": "التحرك إلى الأعلى", "moveSelectionPopup-title": "Move selection", - "multi-selection": "Multi-Selection", + "multi-selection": "تحديد أكثر من واحدة", "multi-selection-label": "Set label for selection", "multi-selection-member": "Set member for selection", "multi-selection-on": "Multi-Selection is on", - "muted": "Muted", + "muted": "مكتوم", "muted-info": "You will never be notified of any changes in this board", - "my-boards": "My Boards", - "name": "Name", + "my-boards": "لوحاتي", + "name": "اسم", "no-archived-cards": "No cards in Archive.", "no-archived-lists": "No lists in Archive.", "no-archived-swimlanes": "No swimlanes in Archive.", - "no-results": "No results", - "normal": "Normal", - "normal-desc": "Can view and edit cards. Can't change settings.", + "no-results": "لا توجد نتائج", + "normal": "عادي", + "normal-desc": "يمكن مشاهدة و تعديل البطاقات. لا يمكن تغيير إعدادات الضبط.", "not-accepted-yet": "Invitation not accepted yet", "notify-participate": "Receive updates to any cards you participate as creater or member", "notify-watch": "Receive updates to any boards, lists, or cards you’re watching", - "optional": "optional", + "optional": "اختياري", "or": "or", - "page-maybe-private": "This page may be private. You may be able to view it by logging in.", - "page-not-found": "Page not found.", - "password": "Password", + "page-maybe-private": "قدتكون هذه الصفحة خاصة . قد تستطيع مشاهدتها ب تسجيل الدخول.", + "page-not-found": "صفحة غير موجودة", + "password": "كلمة المرور", "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)", - "participating": "Participating", + "participating": "المشاركة", "preview": "Preview", "previewAttachedImagePopup-title": "Preview", "previewClipboardImagePopup-title": "Preview", - "private": "Private", - "private-desc": "This board is private. Only people added to the board can view and edit it.", - "profile": "Profile", - "public": "Public", - "public-desc": "This board is public. It's visible to anyone with the link and will show up in search engines like Google. Only people added to the board can edit.", - "quick-access-description": "Star a board to add a shortcut in this bar.", - "remove-cover": "Remove Cover", - "remove-from-board": "Remove from Board", - "remove-label": "Remove Label", - "listDeletePopup-title": "Delete List ?", - "remove-member": "Remove Member", - "remove-member-from-card": "Remove from Card", - "remove-member-pop": "Remove __name__ (__username__) from __boardTitle__? The member will be removed from all cards on this board. They will receive a notification.", - "removeMemberPopup-title": "Remove Member?", - "rename": "Rename", - "rename-board": "Rename Board", - "restore": "Restore", - "save": "Save", - "search": "Search", + "private": "خاص", + "private-desc": "هذه اللوحة خاصة . لا يسمح إلا للأعضاء .", + "profile": "ملف شخصي", + "public": "عامّ", + "public-desc": "هذه اللوحة عامة: مرئية لكلّ من يحصل على الرابط ، و هي مرئية أيضا في محركات البحث مثل جوجل. التعديل مسموح به للأعضاء فقط.", + "quick-access-description": "أضف لوحة إلى المفضلة لإنشاء اختصار في هذا الشريط.", + "remove-cover": "حذف الغلاف", + "remove-from-board": "حذف من اللوحة", + "remove-label": "إزالة التصنيف", + "listDeletePopup-title": "حذف القائمة ؟", + "remove-member": "حذف العضو", + "remove-member-from-card": "حذف من البطاقة", + "remove-member-pop": "حذف __name__ (__username__) من __boardTitle__ ? سيتم حذف هذا العضو من جميع بطاقة اللوحة مع إرسال إشعار له بذاك.", + "removeMemberPopup-title": "حذف العضو ?", + "rename": "إعادة التسمية", + "rename-board": "إعادة تسمية اللوحة", + "restore": "استعادة", + "save": "حفظ", + "search": "بحث", "rules": "Rules", "search-cards": "Search from card/list titles, descriptions and custom fields on this board", "search-example": "Write text you search and press Enter", - "select-color": "Select Color", + "select-color": "اختيار اللون", "select-board": "Select Board", "set-wip-limit-value": "Set a limit for the maximum number of tasks in this list", "setWipLimitPopup-title": "Set WIP Limit", "shortcut-assign-self": "Assign yourself to current card", - "shortcut-autocomplete-emoji": "Autocomplete emoji", - "shortcut-autocomplete-members": "Autocomplete members", - "shortcut-clear-filters": "Clear all filters", - "shortcut-close-dialog": "Close Dialog", - "shortcut-filter-my-cards": "Filter my cards", - "shortcut-show-shortcuts": "Bring up this shortcuts list", + "shortcut-autocomplete-emoji": "الإكمال التلقائي للرموز التعبيرية", + "shortcut-autocomplete-members": "الإكمال التلقائي لأسماء الأعضاء", + "shortcut-clear-filters": "مسح التصفيات", + "shortcut-close-dialog": "غلق النافذة", + "shortcut-filter-my-cards": "تصفية بطاقاتي", + "shortcut-show-shortcuts": "عرض قائمة الإختصارات ،تلك", "shortcut-toggle-filterbar": "Toggle Filter Sidebar", "shortcut-toggle-searchbar": "Toggle Search Sidebar", - "shortcut-toggle-sidebar": "Toggle Board Sidebar", - "show-cards-minimum-count": "Show cards count if list contains more than", - "sidebar-open": "Open Sidebar", - "sidebar-close": "Close Sidebar", - "signupPopup-title": "Create an Account", - "star-board-title": "Click to star this board. It will show up at top of your boards list.", - "starred-boards": "Starred Boards", - "starred-boards-description": "Starred boards show up at the top of your boards list.", - "subscribe": "Subscribe", - "team": "Team", - "this-board": "this board", - "this-card": "this card", + "shortcut-toggle-sidebar": "إظهار-إخفاء الشريط الجانبي للوحة", + "show-cards-minimum-count": "إظهار عدد البطاقات إذا كانت القائمة تتضمن أكثر من", + "sidebar-open": "فتح الشريط الجانبي", + "sidebar-close": "إغلاق الشريط الجانبي", + "signupPopup-title": "إنشاء حساب", + "star-board-title": "اضغط لإضافة هذه اللوحة إلى المفضلة . سوف يتم إظهارها على رأس بقية اللوحات.", + "starred-boards": "اللوحات المفضلة", + "starred-boards-description": "تعرض اللوحات المفضلة على رأس بقية اللوحات.", + "subscribe": "اشتراك و متابعة", + "team": "فريق", + "this-board": "هذه اللوحة", + "this-card": "هذه البطاقة", "spent-time-hours": "Spent time (hours)", - "overtime-hours": "Overtime (hours)", - "overtime": "Overtime", + "overtime-hours": "وقت اضافي (ساعات)", + "overtime": "وقت اضافي", "has-overtime-cards": "Has overtime cards", "has-spenttime-cards": "Has spent time cards", - "time": "Time", - "title": "Title", - "tracking": "Tracking", + "time": "الوقت", + "title": "عنوان", + "tracking": "تتبع", "tracking-info": "You will be notified of any changes to those cards you are involved as creator or member.", - "type": "Type", - "unassign-member": "Unassign member", - "unsaved-description": "You have an unsaved description.", - "unwatch": "Unwatch", + "type": "النوع", + "unassign-member": "إلغاء تعيين العضو", + "unsaved-description": "لديك وصف غير محفوظ", + "unwatch": "غير مُشاهد", "upload": "Upload", - "upload-avatar": "Upload an avatar", - "uploaded-avatar": "Uploaded an avatar", + "upload-avatar": "رفع صورة شخصية", + "uploaded-avatar": "تم رفع الصورة الشخصية", "custom-top-left-corner-logo-image-url": "Custom Top Left Corner Logo Image URL", "custom-top-left-corner-logo-link-url": "Custom Top Left Corner Logo Link URL", "custom-top-left-corner-logo-height": "Custom Top Left Corner Logo Height. Default: 27", "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", - "username": "Username", - "view-it": "View it", + "username": "اسم المستخدم", + "view-it": "شاهدها", "warn-list-archived": "warning: this card is in an list at Archive", - "watch": "Watch", - "watching": "Watching", + "watch": "مُشاهد", + "watching": "مشاهدة", "watching-info": "You will be notified of any change in this board", - "welcome-board": "Welcome Board", + "welcome-board": "لوحة التّرحيب", "welcome-swimlane": "Milestone 1", - "welcome-list1": "Basics", - "welcome-list2": "Advanced", + "welcome-list1": "المبادئ", + "welcome-list2": "متقدم", "card-templates-swimlane": "Card Templates", "list-templates-swimlane": "List Templates", "board-templates-swimlane": "Board Templates", - "what-to-do": "What do you want to do?", + "what-to-do": "ماذا تريد أن تنجز?", "wipLimitErrorPopup-title": "Invalid WIP Limit", "wipLimitErrorPopup-dialog-pt1": "The number of tasks in this list is higher than the WIP limit you've defined.", "wipLimitErrorPopup-dialog-pt2": "Please move some tasks out of this list, or set a higher WIP limit.", - "admin-panel": "Admin Panel", - "settings": "Settings", - "people": "People", - "registration": "Registration", + "admin-panel": "لوحة التحكم", + "settings": "الإعدادات", + "people": "الناس", + "registration": "تسجيل", "disable-self-registration": "Disable Self-Registration", - "invite": "Invite", - "invite-people": "Invite People", - "to-boards": "To board(s)", - "email-addresses": "Email Addresses", + "invite": "دعوة", + "invite-people": "الناس المدعوين", + "to-boards": "إلى اللوحات", + "email-addresses": "عناوين البريد الإلكتروني", "smtp-host-description": "The address of the SMTP server that handles your emails.", "smtp-port-description": "The port your SMTP server uses for outgoing emails.", - "smtp-tls-description": "Enable TLS support for SMTP server", - "smtp-host": "SMTP Host", - "smtp-port": "SMTP Port", - "smtp-username": "Username", - "smtp-password": "Password", - "smtp-tls": "TLS support", - "send-from": "From", + "smtp-tls-description": "تفعيل دعم TLS من اجل خادم SMTP", + "smtp-host": "مضيف SMTP", + "smtp-port": "منفذ SMTP", + "smtp-username": "اسم المستخدم", + "smtp-password": "كلمة المرور", + "smtp-tls": "دعم التي ال سي", + "send-from": "من", "send-smtp-test": "Send a test email to yourself", - "invitation-code": "Invitation Code", - "email-invite-register-subject": "__inviter__ sent you an invitation", + "invitation-code": "رمز الدعوة", + "email-invite-register-subject": "__inviter__ أرسل دعوة لك", "email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to kanban board for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.", "email-smtp-test-subject": "SMTP Test Email", "email-smtp-test-text": "You have successfully sent an email", - "error-invitation-code-not-exist": "Invitation code doesn't exist", - "error-notAuthorized": "You are not authorized to view this page.", + "error-invitation-code-not-exist": "رمز الدعوة غير موجود", + "error-notAuthorized": "أنتَ لا تملك الصلاحيات لرؤية هذه الصفحة.", "webhook-title": "Webhook Name", "webhook-token": "Token (Optional for Authentication)", - "outgoing-webhooks": "Outgoing Webhooks", + "outgoing-webhooks": "الويبهوك الصادرة", "bidirectional-webhooks": "Two-Way Webhooks", - "outgoingWebhooksPopup-title": "Outgoing Webhooks", + "outgoingWebhooksPopup-title": "الويبهوك الصادرة", "boardCardTitlePopup-title": "Card Title Filter", "disable-webhook": "Disable This Webhook", "global-webhook": "Global Webhooks", - "new-outgoing-webhook": "New Outgoing Webhook", - "no-name": "(Unknown)", - "Node_version": "Node version", + "new-outgoing-webhook": "ويبهوك جديدة ", + "no-name": "(غير معروف)", + "Node_version": "إصدار النود", "Meteor_version": "Meteor version", "MongoDB_version": "MongoDB version", "MongoDB_storage_engine": "MongoDB storage engine", "MongoDB_Oplog_enabled": "MongoDB Oplog enabled", - "OS_Arch": "OS Arch", - "OS_Cpus": "OS CPU Count", - "OS_Freemem": "OS Free Memory", - "OS_Loadavg": "OS Load Average", - "OS_Platform": "OS Platform", - "OS_Release": "OS Release", - "OS_Totalmem": "OS Total Memory", - "OS_Type": "OS Type", - "OS_Uptime": "OS Uptime", - "days": "days", - "hours": "hours", - "minutes": "minutes", - "seconds": "seconds", + "OS_Arch": "معمارية نظام التشغيل", + "OS_Cpus": "استهلاك وحدة المعالجة المركزية لنظام التشغيل", + "OS_Freemem": "الذاكرة الحرة لنظام التشغيل", + "OS_Loadavg": "متوسط حمل نظام التشغيل", + "OS_Platform": "منصة نظام التشغيل", + "OS_Release": "إصدار نظام التشغيل", + "OS_Totalmem": "الذاكرة الكلية لنظام التشغيل", + "OS_Type": "نوع نظام التشغيل", + "OS_Uptime": "مدة تشغيل نظام التشغيل", + "days": "أيام", + "hours": "الساعات", + "minutes": "الدقائق", + "seconds": "الثواني", "show-field-on-card": "Show this field on card", "automatically-field-on-card": "Add field to new cards", "always-field-on-card": "Add field to all cards", "showLabel-field-on-card": "Show field label on minicard", - "yes": "Yes", - "no": "No", - "accounts": "Accounts", - "accounts-allowEmailChange": "Allow Email Change", + "yes": "نعم", + "no": "لا", + "accounts": "الحسابات", + "accounts-allowEmailChange": "السماح بتغيير البريد الإلكتروني", "accounts-allowUserNameChange": "Allow Username Change", - "createdAt": "Created at", - "modifiedAt": "Modified at", + "createdAt": "تاريخ الإنشاء", + "modifiedAt": "تاريخ التعديل", "verified": "Verified", - "active": "Active", + "active": "نشط", "card-received": "Received", "card-received-on": "Received on", "card-end": "End", "card-end-on": "Ends on", "editCardReceivedDatePopup-title": "Change received date", "editCardEndDatePopup-title": "Change end date", - "setCardColorPopup-title": "Set color", - "setCardActionsColorPopup-title": "Choose a color", - "setSwimlaneColorPopup-title": "Choose a color", - "setListColorPopup-title": "Choose a color", + "setCardColorPopup-title": "حدد اللون", + "setCardActionsColorPopup-title": "اختر لوناً", + "setSwimlaneColorPopup-title": "اختر لوناً", + "setListColorPopup-title": "اختر لوناً", "assigned-by": "Assigned By", "requested-by": "Requested By", "board-delete-notice": "Deleting is permanent. You will lose all lists, cards and actions associated with this board.", @@ -699,10 +699,10 @@ "r-top-of": "Top of", "r-bottom-of": "Bottom of", "r-its-list": "its list", - "r-archive": "Move to Archive", + "r-archive": "نقل الى الارشيف", "r-unarchive": "Restore from Archive", "r-card": "card", - "r-add": "Add", + "r-add": "أضف", "r-remove": "Remove", "r-label": "label", "r-member": "member", @@ -855,7 +855,7 @@ "website": "Website", "person": "Person", "my-cards": "My Cards", - "card": "Card", + "card": "بطاقة", "board": "Board", "context-separator": "/", "myCardsSortChange-title": "My Cards Sort", @@ -869,30 +869,30 @@ "dueCardsViewChange-choice-all": "All Users", "dueCardsViewChange-choice-all-description": "Shows all incomplete cards with a *Due* date from boards for which the user has permission.", "broken-cards": "Broken Cards", - "board-title-not-found": "Board '%s' not found.", - "swimlane-title-not-found": "Swimlane '%s' not found.", - "list-title-not-found": "List '%s' not found.", - "label-not-found": "Label '%s' not found.", + "board-title-not-found": "لوحة '%s' غير موجود.", + "swimlane-title-not-found": "صف '%s' غير موجود.", + "list-title-not-found": "لستة '%s' غير موجود.", + "label-not-found": "ختم '%s' غير موجود.", "label-color-not-found": "Label color %s not found.", "user-username-not-found": "Username '%s' not found.", - "globalSearch-title": "Search All Boards", + "globalSearch-title": "بحث في كل لوحة", "no-cards-found": "No Cards Found", "one-card-found": "One Card Found", - "n-cards-found": "%s Cards Found", - "n-n-of-n-cards-found": "__start__-__end__ of __total__ Cards Found", - "operator-board": "board", + "n-cards-found": "%s بطاقة", + "n-n-of-n-cards-found": "__start__-__end__ من __total__", + "operator-board": "لوحة", "operator-board-abbrev": "b", - "operator-swimlane": "swimlane", + "operator-swimlane": "صف", "operator-swimlane-abbrev": "s", - "operator-list": "list", + "operator-list": "لستة", "operator-list-abbrev": "l", - "operator-label": "label", + "operator-label": "ختم", "operator-label-abbrev": "#", - "operator-user": "user", + "operator-user": "مستخدم", "operator-user-abbrev": "@", - "operator-member": "member", + "operator-member": "مشارك", "operator-member-abbrev": "m", - "operator-assignee": "assignee", + "operator-assignee": "مسؤول", "operator-assignee-abbrev": "a", "operator-is": "is", "operator-due": "due", @@ -900,8 +900,19 @@ "operator-modified": "modified", "operator-unknown-error": "%s is not an operator", "operator-number-expected": "operator __operator__ expected a number, got '__value__'", - "heading-notes": "Notes", - "globalSearch-instructions-heading": "Search Instructions", + "predicate-archived": "مؤرشف", + "predicate-ended": "ended", + "predicate-all": "كله", + "predicate-overdue": "متاخر", + "predicate-week": "اسبوع", + "predicate-month": "شهر", + "predicate-quarter": "ربع", + "predicate-year": "سنة", + "predicate-due": "due", + "predicate-modified": "متعديل", + "predicate-created": "created", + "heading-notes": "ملاحظات", + "globalSearch-instructions-heading": "تعليمات البحث", "globalSearch-instructions-description": "Searches can include operators to refine the search. Operators are specified by writing the operator name and value separated by a colon. For example, an operator specification of `list:Blocked` would limit the search to cards that are contained in a list named *Blocked*. If the value contains spaces or special characters it must be enclosed in quotation marks (e.g. `__operator_list__:\"To Review\"`).", "globalSearch-instructions-operators": "Available operators:", "globalSearch-instructions-operator-board": "`__operator_board__:title` - cards in boards matching the specified title", @@ -920,7 +931,7 @@ "globalSearch-instructions-notes-5": "Currently archived cards are not searched.", "link-to-search": "Link to this search", "excel-font": "Arial", - "number": "Number", - "label-colors": "Label Colors", - "label-names": "Label Names" + "number": "رقم", + "label-colors": "الوان الختم", + "label-names": "أسماء الختم" } diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index 60f011be0..accf914f4 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -876,6 +876,7 @@ "label-not-found": "Label '%s' not found.", "label-color-not-found": "Label color %s not found.", "user-username-not-found": "Username '%s' not found.", + "comment-not-found": "Card with comment containing text '%s' not found.", "globalSearch-title": "Search All Boards", "no-cards-found": "No Cards Found", "one-card-found": "One Card Found", @@ -895,12 +896,29 @@ "operator-member-abbrev": "m", "operator-assignee": "assignee", "operator-assignee-abbrev": "a", - "operator-is": "is", + "operator-status": "status", "operator-due": "due", "operator-created": "created", "operator-modified": "modified", + "operator-sort": "sort", + "operator-comment": "comment", + "predicate-archived": "archived", + "predicate-ended": "ended", + "predicate-all": "all", + "predicate-overdue": "overdue", + "predicate-week": "week", + "predicate-month": "month", + "predicate-quarter": "quarter", + "predicate-year": "year", + "predicate-due": "due", + "predicate-modified": "modified", + "predicate-created": "created", "operator-unknown-error": "%s is not an operator", "operator-number-expected": "operator __operator__ expected a number, got '__value__'", + "operator-sort-invalid": "sort of '%s' is invalid", + "operator-status-invalid": "'%s' is not a valid status", + "next-page": "Next Page", + "previous-page": "Previous Page", "heading-notes": "Notes", "globalSearch-instructions-heading": "Search Instructions", "globalSearch-instructions-description": "Searches can include operators to refine the search. Operators are specified by writing the operator name and value separated by a colon. For example, an operator specification of `list:Blocked` would limit the search to cards that are contained in a list named *Blocked*. If the value contains spaces or special characters it must be enclosed in quotation marks (e.g. `__operator_list__:\"To Review\"`).", @@ -908,15 +926,23 @@ "globalSearch-instructions-operator-board": "`__operator_board__:title` - cards in boards matching the specified title", "globalSearch-instructions-operator-list": "`__operator_list__:title` - cards in lists matching the specified title", "globalSearch-instructions-operator-swimlane": "`__operator_swimlane__:title` - cards in swimlanes matching the specified title", + "globalSearch-instructions-operator-comment": "`__operator_comment__:text` - cards with with a comment containing *text*.", "globalSearch-instructions-operator-label": "`__operator_label__:color` `__operator_label__:name` - cards that have a label matching the given color or name", "globalSearch-instructions-operator-hash": "`__operator_label_abbrev__label` - shorthand for `__operator_label__:label`", "globalSearch-instructions-operator-user": "`__operator_user__:username` - cards where the specified user 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 the specified user is a *member*", "globalSearch-instructions-operator-assignee": "`__operator_assignee__:username` - cards where the specified user is an *assignee*", + "globalSearch-instructions-operator-due": "`__operator_due__:n` - cards which are due *n* days from now. `__operator_due__:__predicate_overdue__ lists all cards past their due date.", + "globalSearch-instructions-operator-created": "`__operator_created__:n` - cards which which were created *n* days ago", + "globalSearch-instructions-operator-modified": "`__operator_modified__:n` - cards which which were modified *n* days ago", + "globalSearch-instructions-status-archived": "`__operator_status__:__predicate_archived__` - cards that are archived.", + "globalSearch-instructions-status-all": "`__operator_status__:__predicate_all__` - all archived and unarchived cards.", + "globalSearch-instructions-status-ended": "`__operator_status__:__predicate_ended__` - cards with an end date.", "globalSearch-instructions-notes-1": "Multiple operators may be specified.", "globalSearch-instructions-notes-2": "Similar operators are *OR*ed together. Cards that match any of the conditions will be returned.\n`__operator_list__:Available __operator_list__:Blocked` would return cards contained in any list named *Blocked* or *Available*.", - "globalSearch-instructions-notes-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned.\n`__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.", + "globalSearch-instructions-notes-3": "Differing operators are *AND*ed together. Only cards that match all of the differing operators are returned. `__operator_list__:Available __operator_label__:red` returns only cards in the list *Available* with a *red* label.", + "globalSearch-instructions-notes-3-2": "Days can be specified as an integer or using `__predicate_week__`, `__predicate_month__`, `__predicate_quarter__` or `__predicate_year__`", "globalSearch-instructions-notes-4": "Text searches are case insensitive.", "globalSearch-instructions-notes-5": "Currently archived cards are not searched.", "link-to-search": "Link to this search", diff --git a/models/boards.js b/models/boards.js index f4f0d8042..d1dabbc19 100644 --- a/models/boards.js +++ b/models/boards.js @@ -1278,37 +1278,33 @@ Boards.userSearch = ( userId, selector = {}, projection = {}, - includeArchived = false, + // includeArchived = false, ) => { - if (!includeArchived) { - selector.archived = false; - } - selector.$or = [ - { permission: 'public' }, - { members: { $elemMatch: { userId, isActive: true } } }, - ]; + // if (!includeArchived) { + // selector.archived = false; + // } + selector.$or = [{ permission: 'public' }]; + if (userId) { + selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } }); + } return Boards.find(selector, projection); }; -Boards.userBoards = (userId, includeArchived = false, selector = {}) => { - check(userId, String); - - if (!includeArchived) { - selector = { - archived: false, - }; +Boards.userBoards = (userId, archived = false, selector = {}) => { + if (typeof archived === 'boolean') { + selector.archived = archived; } - selector.$or = [ - { permission: 'public' }, - { members: { $elemMatch: { userId, isActive: true } } }, - ]; + selector.$or = [{ permission: 'public' }]; + if (userId) { + selector.$or.push({ members: { $elemMatch: { userId, isActive: true } } }); + } return Boards.find(selector); }; -Boards.userBoardIds = (userId, includeArchived = false, selector = {}) => { - return Boards.userBoards(userId, includeArchived, selector).map(board => { +Boards.userBoardIds = (userId, archived = false, selector = {}) => { + return Boards.userBoards(userId, archived, selector).map(board => { return board._id; }); }; diff --git a/models/cardComments.js b/models/cardComments.js index 39477e14a..88f384163 100644 --- a/models/cardComments.js +++ b/models/cardComments.js @@ -1,3 +1,4 @@ +const escapeForRegex = require('escape-string-regexp'); CardComments = new Mongo.Collection('card_comments'); /** @@ -109,6 +110,28 @@ function commentCreation(userId, doc) { }); } +CardComments.textSearch = (userId, textArray) => { + const selector = { + boardId: { $in: Boards.userBoardIds(userId) }, + $and: [], + }; + + for (const text of textArray) { + selector.$and.push({ text: new RegExp(escapeForRegex(text)) }); + } + + // eslint-disable-next-line no-console + // console.log('cardComments selector:', selector); + + const comments = CardComments.find(selector); + // eslint-disable-next-line no-console + // console.log('count:', comments.count()); + // eslint-disable-next-line no-console + // console.log('cards with comments:', comments.map(com => { return com.cardId })); + + return comments; +}; + if (Meteor.isServer) { // Comments are often fetched within a card, so we create an index to make these // queries more efficient. diff --git a/models/cards.js b/models/cards.js index a195f5b55..74b7b8bfb 100644 --- a/models/cards.js +++ b/models/cards.js @@ -1863,262 +1863,6 @@ Cards.mutations({ }, }); -Cards.globalSearch = queryParams => { - const userId = Meteor.userId(); - // eslint-disable-next-line no-console - // console.log('userId:', userId); - - const errors = new (class { - constructor() { - this.notFound = { - boards: [], - swimlanes: [], - lists: [], - labels: [], - users: [], - members: [], - assignees: [], - is: [], - }; - } - - hasErrors() { - for (const prop in this.notFound) { - if (this.notFound[prop].length) { - return true; - } - } - return false; - } - })(); - - const selector = { - archived: false, - type: 'cardType-card', - boardId: { $in: Boards.userBoardIds(userId) }, - swimlaneId: { $nin: Swimlanes.archivedSwimlaneIds() }, - listId: { $nin: Lists.archivedListIds() }, - }; - - if (queryParams.boards.length) { - const queryBoards = []; - queryParams.boards.forEach(query => { - const boards = Boards.userSearch(userId, { - title: new RegExp(query, 'i'), - }); - if (boards.count()) { - boards.forEach(board => { - queryBoards.push(board._id); - }); - } else { - errors.notFound.boards.push(query); - } - }); - - selector.boardId.$in = queryBoards; - } - - if (queryParams.swimlanes.length) { - const querySwimlanes = []; - queryParams.swimlanes.forEach(query => { - const swimlanes = Swimlanes.find({ - title: new RegExp(query, 'i'), - }); - if (swimlanes.count()) { - swimlanes.forEach(swim => { - querySwimlanes.push(swim._id); - }); - } else { - errors.notFound.swimlanes.push(query); - } - }); - - selector.swimlaneId.$in = querySwimlanes; - } - - if (queryParams.lists.length) { - const queryLists = []; - queryParams.lists.forEach(query => { - const lists = Lists.find({ - title: new RegExp(query, 'i'), - }); - if (lists.count()) { - lists.forEach(list => { - queryLists.push(list._id); - }); - } else { - errors.notFound.lists.push(query); - } - }); - - selector.listId.$in = queryLists; - } - - if (queryParams.dueAt !== null) { - selector.dueAt = { $gte: new Date(queryParams.dueAt) }; - } - - if (queryParams.createdAt !== null) { - selector.createdAt = { $gte: new Date(queryParams.createdAt) }; - } - - if (queryParams.modifiedAt !== null) { - selector.modifiedAt = { $gte: new Date(queryParams.modifiedAt) }; - } - - const queryMembers = []; - const queryAssignees = []; - if (queryParams.users.length) { - queryParams.users.forEach(query => { - const users = Users.find({ - username: query, - }); - if (users.count()) { - users.forEach(user => { - queryMembers.push(user._id); - queryAssignees.push(user._id); - }); - } else { - errors.notFound.users.push(query); - } - }); - } - - if (queryParams.members.length) { - queryParams.members.forEach(query => { - const users = Users.find({ - username: query, - }); - if (users.count()) { - users.forEach(user => { - queryMembers.push(user._id); - }); - } else { - errors.notFound.members.push(query); - } - }); - } - - if (queryParams.assignees.length) { - queryParams.assignees.forEach(query => { - const users = Users.find({ - username: query, - }); - if (users.count()) { - users.forEach(user => { - queryAssignees.push(user._id); - }); - } else { - errors.notFound.assignees.push(query); - } - }); - } - - if (queryMembers.length && queryAssignees.length) { - selector.$or = [ - { members: { $in: queryMembers } }, - { assignees: { $in: queryAssignees } }, - ]; - } else if (queryMembers.length) { - selector.members = { $in: queryMembers }; - } else if (queryAssignees.length) { - selector.assignees = { $in: queryAssignees }; - } - - if (queryParams.labels.length) { - queryParams.labels.forEach(label => { - const queryLabels = []; - - let boards = Boards.userSearch(userId, { - labels: { $elemMatch: { color: label.toLowerCase() } }, - }); - - if (boards.count()) { - boards.forEach(board => { - // eslint-disable-next-line no-console - // console.log('board:', board); - // eslint-disable-next-line no-console - // console.log('board.labels:', board.labels); - board.labels - .filter(boardLabel => { - return boardLabel.color === label.toLowerCase(); - }) - .forEach(boardLabel => { - queryLabels.push(boardLabel._id); - }); - }); - } else { - // eslint-disable-next-line no-console - // console.log('label:', label); - const reLabel = new RegExp(label, 'i'); - // eslint-disable-next-line no-console - // console.log('reLabel:', reLabel); - boards = Boards.userSearch(userId, { - labels: { $elemMatch: { name: reLabel } }, - }); - - if (boards.count()) { - boards.forEach(board => { - board.labels - .filter(boardLabel => { - return boardLabel.name.match(reLabel); - }) - .forEach(boardLabel => { - queryLabels.push(boardLabel._id); - }); - }); - } else { - errors.notFound.labels.push(label); - } - } - - selector.labelIds = { $in: queryLabels }; - }); - } - - if (errors.hasErrors()) { - return { cards: null, errors }; - } - - if (queryParams.text) { - const regex = new RegExp(queryParams.text, 'i'); - - selector.$or = [ - { title: regex }, - { description: regex }, - { customFields: { $elemMatch: { value: regex } } }, - ]; - } - - // eslint-disable-next-line no-console - // console.log('selector:', selector); - 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, - createdAt: 1, - modifiedAt: 1, - labelIds: 1, - }, - limit: 50, - }); - - // eslint-disable-next-line no-console - //console.log('count:', cards.count()); - - return { cards, errors }; -}; - //FUNCTIONS FOR creation of Activities function updateActivities(doc, fieldNames, modifier) { diff --git a/models/usersessiondata.js b/models/usersessiondata.js index e93cde2bd..129fe56ba 100644 --- a/models/usersessiondata.js +++ b/models/usersessiondata.js @@ -25,6 +25,13 @@ SessionData.attachSchema( type: String, optional: false, }, + sessionId: { + /** + * unique session ID + */ + type: String, + optional: false, + }, totalHits: { /** * total number of hits in the last report query @@ -32,6 +39,13 @@ SessionData.attachSchema( type: Number, optional: true, }, + resultsCount: { + /** + * number of results returned + */ + type: Number, + optional: true, + }, lastHit: { /** * the last hit returned from a report query @@ -39,6 +53,48 @@ SessionData.attachSchema( type: Number, optional: true, }, + cards: { + type: [String], + optional: true, + }, + selector: { + type: String, + optional: true, + blackbox: true, + }, + errorMessages: { + type: [String], + optional: true, + }, + errors: { + type: [Object], + optional: true, + defaultValue: [], + }, + 'errors.$': { + type: new SimpleSchema({ + tag: { + /** + * i18n tag + */ + type: String, + optional: false, + }, + value: { + /** + * value for the tag + */ + type: String, + optional: true, + defaultValue: null, + }, + color: { + type: Boolean, + optional: true, + defaultValue: false, + }, + }), + }, createdAt: { /** * creation date of the team @@ -70,4 +126,50 @@ SessionData.attachSchema( }), ); +SessionData.helpers({ + getSelector() { + return SessionData.unpickle(this.selector); + }, +}); + +SessionData.unpickle = pickle => { + return JSON.parse(pickle, (key, value) => { + if (typeof value === 'object') { + if (value.hasOwnProperty('$$class')) { + if (value.$$class === 'RegExp') { + return new RegExp(value.source, value.flags); + } + } + } + return value; + }); +}; + +SessionData.pickle = value => { + return JSON.stringify(value, (key, value) => { + if (typeof value === 'object') { + if (value.constructor.name === 'RegExp') { + return { + $$class: 'RegExp', + source: value.source, + flags: value.flags, + }; + } + } + return value; + }); +}; + +if (!Meteor.isServer) { + SessionData.getSessionId = () => { + let sessionId = Session.get('sessionId'); + if (!sessionId) { + sessionId = `${String(Meteor.userId())}-${String(Math.random())}`; + Session.set('sessionId', sessionId); + } + + return sessionId; + }; +} + export default SessionData; diff --git a/server/publications/cards.js b/server/publications/cards.js index 6d5223c50..5b93d9c7c 100644 --- a/server/publications/cards.js +++ b/server/publications/cards.js @@ -1,3 +1,5 @@ +const escapeForRegex = require('escape-string-regexp'); + Meteor.publish('card', cardId => { check(cardId, String); return Cards.find({ _id: cardId }); @@ -173,59 +175,522 @@ Meteor.publish('dueCards', function(allUsers = false) { ]; }); -Meteor.publish('globalSearch', function(queryParams) { +Meteor.publish('globalSearch', function(sessionId, queryParams) { + check(sessionId, String); check(queryParams, Object); // eslint-disable-next-line no-console // console.log('queryParams:', queryParams); - const cards = Cards.globalSearch(queryParams).cards; + const userId = Meteor.userId(); + // eslint-disable-next-line no-console + // console.log('userId:', userId); - if (!cards) { - return []; + const errors = new (class { + constructor() { + this.notFound = { + boards: [], + swimlanes: [], + lists: [], + labels: [], + users: [], + members: [], + assignees: [], + status: [], + comments: [], + }; + + this.colorMap = {}; + for (const color of Boards.simpleSchema()._schema['labels.$.color'] + .allowedValues) { + this.colorMap[TAPi18n.__(`color-${color}`)] = color; + } + } + + hasErrors() { + for (const value of Object.values(this.notFound)) { + if (value.length) { + return true; + } + } + return false; + } + + errorMessages() { + const messages = []; + + 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.labels.forEach(label => { + messages.push({ tag: 'label-not-found', value: label, color: true }); + }); + 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; + } + })(); + + let selector = {}; + let skip = 0; + if (queryParams.skip) { + skip = queryParams.skip; + } + let limit = 25; + if (queryParams.limit) { + limit = queryParams.limit; } - SessionData.upsert( - { userId: this.userId }, - { - $set: { - totalHits: cards.count(), - lastHit: cards.count() > 50 ? 50 : cards.count(), - }, - }, - ); - - const boards = []; - const swimlanes = []; - const lists = []; - const users = [this.userId]; - - 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 (queryParams.selector) { + selector = queryParams.selector; + } else { + let archived = false; + let endAt = null; + if (queryParams.status.length) { + queryParams.status.forEach(status => { + if (status === 'archived') { + archived = true; + } else if (status === 'all') { + archived = null; + } else if (status === 'ended') { + endAt = { $nin: [null, ''] }; + } }); } - if (card.assignees) { - card.assignees.forEach(userId => { - users.push(userId); + selector = { + type: 'cardType-card', + // boardId: { $in: Boards.userBoardIds(userId) }, + $and: [], + }; + + const boardsSelector = {}; + if (archived !== null) { + boardsSelector.archived = archived; + if (archived) { + selector.boardId = { $in: Boards.userBoardIds(userId, null) }; + selector.$and.push({ + $or: [ + { boardId: { $in: Boards.userBoardIds(userId, archived) } }, + { swimlaneId: { $in: Swimlanes.archivedSwimlaneIds() } }, + { listId: { $in: Lists.archivedListIds() } }, + { archived: true }, + ], + }); + } else { + selector.boardId = { $in: Boards.userBoardIds(userId, false) }; + selector.swimlaneId = { $nin: Swimlanes.archivedSwimlaneIds() }; + selector.listId = { $nin: Lists.archivedListIds() }; + selector.archived = false; + } + } else { + selector.boardId = { $in: Boards.userBoardIds(userId, null) }; + } + if (endAt !== null) { + selector.endAt = endAt; + } + + if (queryParams.boards.length) { + const queryBoards = []; + queryParams.boards.forEach(query => { + const boards = Boards.userSearch(userId, { + title: new RegExp(escapeForRegex(query), 'i'), + }); + if (boards.count()) { + boards.forEach(board => { + queryBoards.push(board._id); + }); + } else { + errors.notFound.boards.push(query); + } + }); + + selector.boardId.$in = queryBoards; + } + + if (queryParams.swimlanes.length) { + const querySwimlanes = []; + queryParams.swimlanes.forEach(query => { + const swimlanes = Swimlanes.find({ + title: new RegExp(escapeForRegex(query), 'i'), + }); + if (swimlanes.count()) { + swimlanes.forEach(swim => { + querySwimlanes.push(swim._id); + }); + } else { + errors.notFound.swimlanes.push(query); + } + }); + + if (!selector.swimlaneId.hasOwnProperty('swimlaneId')) { + selector.swimlaneId = { $in: [] }; + } + selector.swimlaneId.$in = querySwimlanes; + } + + if (queryParams.lists.length) { + const queryLists = []; + queryParams.lists.forEach(query => { + const lists = Lists.find({ + title: new RegExp(escapeForRegex(query), 'i'), + }); + if (lists.count()) { + lists.forEach(list => { + queryLists.push(list._id); + }); + } else { + errors.notFound.lists.push(query); + } + }); + + if (!selector.hasOwnProperty('listId')) { + selector.listId = { $in: [] }; + } + selector.listId.$in = queryLists; + } + + if (queryParams.comments.length) { + const cardIds = CardComments.textSearch(userId, queryParams.comments).map( + com => { + return com.cardId; + }, + ); + if (cardIds.length) { + selector._id = { $in: cardIds }; + } else { + errors.notFound.comments.push(queryParams.comments); + } + } + + if (queryParams.dueAt !== null) { + selector.dueAt = { $lte: new Date(queryParams.dueAt) }; + } + + if (queryParams.createdAt !== null) { + selector.createdAt = { $gte: new Date(queryParams.createdAt) }; + } + + if (queryParams.modifiedAt !== null) { + selector.modifiedAt = { $gte: new Date(queryParams.modifiedAt) }; + } + + const queryMembers = []; + const queryAssignees = []; + if (queryParams.users.length) { + queryParams.users.forEach(query => { + const users = Users.find({ + username: query, + }); + if (users.count()) { + users.forEach(user => { + queryMembers.push(user._id); + queryAssignees.push(user._id); + }); + } else { + errors.notFound.users.push(query); + } }); } - }); + + if (queryParams.members.length) { + queryParams.members.forEach(query => { + const users = Users.find({ + username: query, + }); + if (users.count()) { + users.forEach(user => { + queryMembers.push(user._id); + }); + } else { + errors.notFound.members.push(query); + } + }); + } + + if (queryParams.assignees.length) { + queryParams.assignees.forEach(query => { + const users = Users.find({ + username: query, + }); + if (users.count()) { + users.forEach(user => { + queryAssignees.push(user._id); + }); + } else { + errors.notFound.assignees.push(query); + } + }); + } + + if (queryMembers.length && queryAssignees.length) { + selector.$and.push({ + $or: [ + { members: { $in: queryMembers } }, + { assignees: { $in: queryAssignees } }, + ], + }); + } else if (queryMembers.length) { + selector.members = { $in: queryMembers }; + } else if (queryAssignees.length) { + selector.assignees = { $in: queryAssignees }; + } + + if (queryParams.labels.length) { + queryParams.labels.forEach(label => { + const queryLabels = []; + + let boards = Boards.userSearch(userId, { + labels: { $elemMatch: { color: label.toLowerCase() } }, + }); + + if (boards.count()) { + boards.forEach(board => { + // eslint-disable-next-line no-console + // console.log('board:', board); + // eslint-disable-next-line no-console + // console.log('board.labels:', board.labels); + board.labels + .filter(boardLabel => { + return boardLabel.color === label.toLowerCase(); + }) + .forEach(boardLabel => { + queryLabels.push(boardLabel._id); + }); + }); + } else { + // eslint-disable-next-line no-console + // console.log('label:', label); + const reLabel = new RegExp(escapeForRegex(label), 'i'); + // eslint-disable-next-line no-console + // console.log('reLabel:', reLabel); + boards = Boards.userSearch(userId, { + labels: { $elemMatch: { name: reLabel } }, + }); + + if (boards.count()) { + boards.forEach(board => { + board.labels + .filter(boardLabel => { + return boardLabel.name.match(reLabel); + }) + .forEach(boardLabel => { + queryLabels.push(boardLabel._id); + }); + }); + } else { + errors.notFound.labels.push(label); + } + } + + selector.labelIds = { $in: queryLabels }; + }); + } + + if (queryParams.text) { + const regex = new RegExp(escapeForRegex(queryParams.text), 'i'); + + selector.$and.push({ + $or: [ + { title: regex }, + { description: regex }, + { customFields: { $elemMatch: { value: regex } } }, + { + _id: { + $in: CardComments.textSearch(userId, [queryParams.text]).map( + com => com.cardId, + ), + }, + }, + ], + }); + } + + if (selector.$and.length === 0) { + delete selector.$and; + } + } // eslint-disable-next-line no-console - // console.log('users:', users); - 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 }), - SessionData.find({ userId: this.userId }), - ]; + // console.log('selector:', selector); + // eslint-disable-next-line no-console + // console.log('selector.$and:', selector.$and); + + let cards = null; + + if (!errors.hasErrors()) { + const projection = { + 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, + createdAt: 1, + modifiedAt: 1, + labelIds: 1, + customFields: 1, + }, + skip, + limit, + }; + + if (queryParams.sort === 'due') { + projection.sort = { + dueAt: 1, + boardId: 1, + swimlaneId: 1, + listId: 1, + sort: 1, + }; + } else if (queryParams.sort === 'modified') { + projection.sort = { + modifiedAt: -1, + boardId: 1, + swimlaneId: 1, + listId: 1, + sort: 1, + }; + } else if (queryParams.sort === 'created') { + projection.sort = { + createdAt: -1, + boardId: 1, + swimlaneId: 1, + listId: 1, + sort: 1, + }; + } else if (queryParams.sort === 'system') { + projection.sort = { + boardId: 1, + swimlaneId: 1, + listId: 1, + modifiedAt: 1, + sort: 1, + }; + } + + // eslint-disable-next-line no-console + // console.log('projection:', projection); + cards = Cards.find(selector, projection); + + // eslint-disable-next-line no-console + // console.log('count:', cards.count()); + } + + const update = { + $set: { + totalHits: 0, + lastHit: 0, + resultsCount: 0, + cards: [], + errors: errors.errorMessages(), + selector: SessionData.pickle(selector), + }, + }; + + if (cards) { + update.$set.totalHits = cards.count(); + update.$set.lastHit = + skip + limit < cards.count() ? skip + limit : cards.count(); + update.$set.cards = cards.map(card => { + return card._id; + }); + update.$set.resultsCount = update.$set.cards.length; + } + + SessionData.upsert({ userId, sessionId }, update); + + // remove old session data + SessionData.remove({ + userId, + modifiedAt: { + $lt: new Date( + moment() + .subtract(1, 'day') + .format(), + ), + }, + }); + + if (cards) { + const boards = []; + const swimlanes = []; + const lists = []; + const customFieldIds = []; + const users = [this.userId]; + + 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); + }); + } + if (card.customFields) { + card.customFields.forEach(field => { + customFieldIds.push(field._id); + }); + } + }); + + const fields = { + _id: 1, + title: 1, + archived: 1, + sort: 1, + type: 1, + }; + + return [ + cards, + Boards.find( + { _id: { $in: boards } }, + { fields: { ...fields, labels: 1, color: 1 } }, + ), + Swimlanes.find( + { _id: { $in: swimlanes } }, + { fields: { ...fields, color: 1 } }, + ), + Lists.find({ _id: { $in: lists } }, { fields }), + CustomFields.find({ _id: { $in: customFieldIds } }), + Users.find({ _id: { $in: users } }, { fields: Users.safeFields }), + SessionData.find({ userId: this.userId, sessionId }), + ]; + } + + return [SessionData.find({ userId: this.userId, sessionId })]; }); Meteor.publish('brokenCards', function() {