summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.editorconfig10
-rw-r--r--.eslintrc187
-rw-r--r--.gitignore1
-rw-r--r--.meteor/.finished-upgraders4
-rw-r--r--.meteor/cordova-plugins1
-rw-r--r--.meteor/packages9
-rw-r--r--.meteor/release2
-rw-r--r--.meteor/versions209
-rw-r--r--.travis.yml8
-rw-r--r--Changelog.md (renamed from History.md)27
-rw-r--r--client/components/activities/activities.jade59
-rw-r--r--client/components/activities/activities.js31
-rw-r--r--client/components/activities/comments.js10
-rw-r--r--client/components/boards/boardArchive.js10
-rw-r--r--client/components/boards/boardBody.js23
-rw-r--r--client/components/boards/boardHeader.jade10
-rw-r--r--client/components/boards/boardHeader.js36
-rw-r--r--client/components/boards/boardHeader.styl2
-rw-r--r--client/components/boards/boardsList.jade26
-rw-r--r--client/components/boards/boardsList.js18
-rw-r--r--client/components/boards/boardsList.styl9
-rw-r--r--client/components/cards/attachments.jade12
-rw-r--r--client/components/cards/attachments.js85
-rw-r--r--client/components/cards/attachments.styl11
-rw-r--r--client/components/cards/cardDetails.js54
-rw-r--r--client/components/cards/labels.jade2
-rw-r--r--client/components/cards/labels.js55
-rw-r--r--client/components/cards/minicard.jade2
-rw-r--r--client/components/forms/forms.styl9
-rw-r--r--client/components/import/import.jade54
-rw-r--r--client/components/import/import.js271
-rw-r--r--client/components/import/import.styl17
-rw-r--r--client/components/lists/list.js29
-rw-r--r--client/components/lists/listBody.jade15
-rw-r--r--client/components/lists/listBody.js109
-rw-r--r--client/components/lists/listHeader.jade1
-rw-r--r--client/components/lists/listHeader.js45
-rw-r--r--client/components/main/editor.js36
-rw-r--r--client/components/main/header.jade4
-rw-r--r--client/components/main/keyboardShortcuts.styl5
-rw-r--r--client/components/main/layouts.jade13
-rw-r--r--client/components/main/layouts.js33
-rw-r--r--client/components/main/layouts.styl9
-rw-r--r--client/components/main/popup.styl6
-rw-r--r--client/components/sidebar/sidebar.jade78
-rw-r--r--client/components/sidebar/sidebar.js167
-rw-r--r--client/components/sidebar/sidebarArchives.js18
-rw-r--r--client/components/sidebar/sidebarFilters.jade6
-rw-r--r--client/components/sidebar/sidebarFilters.js49
-rw-r--r--client/components/users/userAvatar.jade4
-rw-r--r--client/components/users/userAvatar.js27
-rw-r--r--client/components/users/userAvatar.styl4
-rw-r--r--client/components/users/userForm.styl10
-rw-r--r--client/components/users/userHeader.js6
-rw-r--r--client/config/blazeHelpers.js4
-rw-r--r--client/config/router.js23
-rw-r--r--client/lib/accessibility.js41
-rw-r--r--client/lib/dropImage.js62
-rw-r--r--client/lib/filter.js4
-rw-r--r--client/lib/keyboard.js34
-rw-r--r--client/lib/modal.js4
-rw-r--r--client/lib/multiSelection.js9
-rw-r--r--client/lib/pasteImage.js57
-rw-r--r--client/lib/popup.js4
-rw-r--r--client/lib/textComplete.js54
-rw-r--r--client/lib/unsavedEdits.js2
-rw-r--r--client/lib/utils.js14
-rw-r--r--collections/users.js151
-rw-r--r--config/accounts.js (renamed from client/config/accounts.js)15
-rw-r--r--i18n/ar.i18n.json229
-rw-r--r--i18n/ca.i18n.json229
-rw-r--r--i18n/de.i18n.json75
-rw-r--r--i18n/en.i18n.json64
-rw-r--r--i18n/es.i18n.json84
-rw-r--r--i18n/fi.i18n.json70
-rw-r--r--i18n/fr.i18n.json33
-rw-r--r--i18n/it.i18n.json229
-rw-r--r--i18n/ja.i18n.json64
-rw-r--r--i18n/ko.i18n.json64
-rw-r--r--i18n/pt-BR.i18n.json142
-rw-r--r--i18n/ru.i18n.json229
-rw-r--r--i18n/tr.i18n.json72
-rw-r--r--i18n/zh-CN.i18n.json181
-rw-r--r--models/activities.js (renamed from collections/activities.js)0
-rw-r--r--models/attachments.js (renamed from collections/attachments.js)2
-rw-r--r--models/avatars.js (renamed from collections/avatars.js)0
-rw-r--r--models/boards.js (renamed from collections/boards.js)232
-rw-r--r--models/cardComments.js69
-rw-r--r--models/cards.js (renamed from collections/cards.js)173
-rw-r--r--models/import.js511
-rw-r--r--models/lists.js (renamed from collections/lists.js)46
-rw-r--r--models/unsavedEdits.js (renamed from collections/unsavedEdits.js)0
-rw-r--r--models/users.js291
-rw-r--r--package.json24
-rw-r--r--sandstorm-pkgdef.capnp2
-rw-r--r--sandstorm.js84
-rw-r--r--server/migrations.js12
-rw-r--r--server/publications/boards.js3
-rw-r--r--server/publications/fast-render.js7
99 files changed, 4498 insertions, 1139 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..4ba559c2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,10 @@
+# EditorConfig is awesome: http://EditorConfig.org
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
diff --git a/.eslintrc b/.eslintrc
index 7d596fa3..f9321bfb 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,138 +1,113 @@
ecmaFeatures:
experimentalObjectRestSpread: true
+
+plugins:
+ - meteor
+
+parser: babel-eslint
+
rules:
- indent:
- - 2
- - 2
- semi:
- - 2
- - always
- comma-dangle:
- - 2
- - always-multiline
- no-inner-declarations:
- - 0
- dot-notation:
- - 2
- eqeqeq:
- - 2
- no-eval:
- - 2
- radix:
- - 2
+ strict: 0
+ no-undef: 2
+ accessor-pairs: 2
+ comma-dangle: [2, 'always-multiline']
+ consistent-return: 2
+ dot-notation: 2
+ eqeqeq: 2
+ indent: [2, 2]
+ no-cond-assign: 2
+ no-constant-condition: 2
+ no-eval: 2
+ no-inner-declarations: [0]
+ no-unneeded-ternary: 2
+ radix: 2
+ semi: [2, always]
# Stylistic Issues
- camelcase:
- - 2
- comma-spacing:
- - 2
- comma-style:
- - 2
- new-parens:
- - 2
- no-lonely-if:
- - 2
- no-multiple-empty-lines:
- - 2
- no-nested-ternary:
- - 2
- linebreak-style:
- - 2
- - unix
- quotes:
- - 2
- - single
- semi-spacing:
- - 2
- spaced-comment:
- - 2
- - always
- - markers:
- - '/'
- space-unary-ops:
- - 2
+ camelcase: 2
+ comma-spacing: 2
+ comma-style: 2
+ linebreak-style: [2, unix]
+ new-parens: 2
+ no-lonely-if: 2
+ no-multiple-empty-lines: 2
+ no-nested-ternary: 2
+ no-spaced-func: 2
+ operator-linebreak: 2
+ quotes: [2, single]
+ semi-spacing: 2
+ space-unary-ops: 2
+ spaced-comment: [2, always, markers: ['/']]
# ECMAScript 6
- arrow-parens:
- - 2
- arrow-spacing:
- - 2
- no-class-assign:
- - 2
- no-dupe-class-members:
- - 2
- no-var:
- - 2
- object-shorthand:
- - 2
- prefer-const:
- - 2
- prefer-template:
- - 2
- prefer-spread:
- - 2
-globals:
- # Meteor globals
- Meteor: false
- DDP: false
- Mongo: false
- Session: false
- Accounts: false
- Template: false
- Blaze: false
- UI: false
- Match: false
- check: false
- Tracker: false
- Deps: false
- ReactiveVar: false
- EJSON: false
- HTTP: false
- Email: false
- Assets: false
- Handlebars: false
- Package: false
- App: false
- Npm: false
- Tinytest: false
- Random: false
- HTML: false
+ arrow-parens: 2
+ arrow-spacing: 2
+ no-class-assign: 2
+ no-dupe-class-members: 2
+ no-var: 2
+ object-shorthand: 2
+ prefer-const: 2
+ prefer-spread: 2
+ prefer-template: 2
+
+ # eslint-plugin-meteor
+ ## Meteor API
+ meteor/globals: 2
+ meteor/core: 2
+ meteor/pubsub: 2
+ meteor/methods: 2
+ meteor/check: 2
+ meteor/connections: 2
+ meteor/collections: 2
+ meteor/session: [2, 'no-equal']
+
+ ## Best practices
+ meteor/no-session: 0
+ meteor/no-zero-timeout: 2
+ meteor/no-blaze-lifecycle-assignment: 2
+settings:
+ meteor:
+
+ # Our collections
+ collections:
+ - AccountsTemplates
+ - Activities
+ - Attachments
+ - Boards
+ - CardComments
+ - Cards
+ - Lists
+ - UnsavedEditCollection
+ - Users
+
+globals:
# Exported by packages we use
- '$': false
- _: false
autosize: false
Avatar: true
Avatars: true
BlazeComponent: false
BlazeLayout: false
+ DocHead: false
ESSearchResults: false
+ FastRender: false
FlowRouter: false
FS: false
getSlug: false
Migrations: false
+ moment: false
Mousetrap: false
Picker: false
Presence: true
presences: true
Ps: true
ReactiveTabs: false
+ Restivus: false
SimpleSchema: false
SubsManager: false
T9n: false
TAPi18n: false
- # Our collections
- AccountsTemplates: true
- Activities: true
- Attachments: true
- Boards: true
- CardComments: true
- Cards: true
- Lists: true
- UnsavedEditCollection: true
- Users: true
-
# Our objects
CSSEvents: true
EscapeActions: true
@@ -151,8 +126,10 @@ globals:
allowIsBoardAdmin: true
allowIsBoardMember: true
Emoji: true
+
env:
es6: true
node: true
browser: true
+
extends: 'eslint:recommended'
diff --git a/.gitignore b/.gitignore
index 986a8b46..7c40fcd4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@
.tx/
*.sublime-workspace
tmp/
+node_modules/
diff --git a/.meteor/.finished-upgraders b/.meteor/.finished-upgraders
index 8a761038..61ee3132 100644
--- a/.meteor/.finished-upgraders
+++ b/.meteor/.finished-upgraders
@@ -6,3 +6,7 @@ notices-for-0.9.0
notices-for-0.9.1
0.9.4-platform-file
notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
diff --git a/.meteor/cordova-plugins b/.meteor/cordova-plugins
index 8b137891..e69de29b 100644
--- a/.meteor/cordova-plugins
+++ b/.meteor/cordova-plugins
@@ -1 +0,0 @@
-
diff --git a/.meteor/packages b/.meteor/packages
index 0aa0fa68..98c06cc9 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -2,9 +2,6 @@
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
-#
-# XXX Should we replace tmeasday:presence by 3stack:presence? Or maybe the
-# packages will merge in the future?
meteor-base
@@ -18,7 +15,6 @@ mquandalle:stylus
es5-shim
# Collections
-mongo
aldeed:collection2
cfs:gridfs
cfs:standard-packages
@@ -26,6 +22,8 @@ dburles:collection-helpers
idmontie:migrations
matb33:collection-hooks
matteodem:easy-search
+mongo
+mquandalle:collection-mutations
reywood:publish-composite
# Account system
@@ -35,6 +33,7 @@ service-configuration
useraccounts:core
useraccounts:unstyled
useraccounts:flow-routing
+email
# Utilities
check
@@ -49,7 +48,9 @@ alethes:pages
arillo:flow-router-helpers
audit-argument-checks
kadira:blaze-layout
+kadira:dochead
kadira:flow-router
+meteorhacks:fast-render
meteorhacks:picker
meteorhacks:subs-manager
mquandalle:autofocus
diff --git a/.meteor/release b/.meteor/release
index e1990ae6..3a05e0a2 100644
--- a/.meteor/release
+++ b/.meteor/release
@@ -1 +1 @@
-METEOR@1.2-rc.12
+METEOR@1.2.1
diff --git a/.meteor/versions b/.meteor/versions
index 6c410d0b..9d7fe1b3 100644
--- a/.meteor/versions
+++ b/.meteor/versions
@@ -1,22 +1,23 @@
-3stack:presence@1.0.3
-accounts-base@1.2.1-rc.2
-accounts-password@1.1.2-rc.1
+3stack:presence@1.0.5
+accounts-base@1.2.2
+accounts-password@1.1.4
aldeed:collection2@2.5.0
aldeed:simple-schema@1.3.3
alethes:pages@1.8.4
-arillo:flow-router-helpers@0.4.5
-audit-argument-checks@1.0.4-rc.0
-autoupdate@1.2.3-rc.1
-babel-compiler@5.8.22-rc.1
-babel-runtime@0.1.4-rc.0
-base64@1.0.4-rc.0
-binary-heap@1.0.4-rc.0
-blaze@2.1.3-rc.0
-blaze-tools@1.0.4-rc.0
-boilerplate-generator@1.0.4-rc.1
-caching-compiler@1.0.0-rc.0
-caching-html-compiler@1.0.1-rc.0
-callback-hook@1.0.4-rc.0
+arillo:flow-router-helpers@0.4.7
+audit-argument-checks@1.0.4
+autoupdate@1.2.4
+babel-compiler@5.8.24_1
+babel-runtime@0.1.4
+base64@1.0.4
+binary-heap@1.0.4
+blaze@2.1.3
+blaze-html-templates@1.0.1
+blaze-tools@1.0.4
+boilerplate-generator@1.0.4
+caching-compiler@1.0.0
+caching-html-compiler@1.0.2
+callback-hook@1.0.4
cfs:access-point@0.1.49
cfs:base-package@0.0.30
cfs:collection@0.5.5
@@ -24,117 +25,123 @@ cfs:collection-filters@0.2.4
cfs:data-man@0.0.6
cfs:file@0.1.17
cfs:gridfs@0.0.33
-cfs:http-methods@0.0.29
+cfs:http-methods@0.0.30
cfs:http-publish@0.0.13
cfs:power-queue@0.9.11
cfs:reactive-list@0.0.9
cfs:reactive-property@0.0.4
cfs:standard-packages@0.5.9
-cfs:storage-adapter@0.2.2
+cfs:storage-adapter@0.2.3
cfs:tempstore@0.1.5
cfs:upload-http@0.0.20
cfs:worker@0.1.4
-check@1.0.6-rc.0
-coffeescript@1.0.8-rc.3
-cosmos:browserify@0.5.0
-dburles:collection-helpers@1.0.3
-ddp@1.2.1-rc.0
-ddp-client@1.2.1-rc.1
-ddp-common@1.2.1-rc.0
-ddp-rate-limiter@1.0.0-rc.0
-ddp-server@1.2.1-rc.1
-deps@1.0.8-rc.0
-diff-sequence@1.0.1-rc.0
-ecmascript@0.1.3-rc.2
-ecmascript-collections@0.1.5-rc.1
-ejson@1.0.7-rc.0
-email@1.0.7-rc.0
-es5-shim@0.1.0-rc.0
-fastclick@1.0.7-rc.0
-fortawesome:fontawesome@4.4.0
-geojson-utils@1.0.4-rc.0
-hot-code-push@1.0.0-rc.0
-html-tools@1.0.5-rc.0
-htmljs@1.0.5-rc.1
-http@1.1.1-rc.1
-id-map@1.0.4-rc.0
-idmontie:migrations@1.0.0
-jquery@1.11.4-rc.0
-kadira:blaze-layout@2.1.0
-kadira:flow-router@2.5.0
-kenton:accounts-sandstorm@0.1.4
-launch-screen@1.0.3-rc.1
-less@2.5.0-rc.3_1
-livedata@1.0.14-rc.0
-localstorage@1.0.4-rc.0
-logging@1.0.8-rc.1
-matb33:collection-hooks@0.8.0
-matteodem:easy-search@1.6.3
-meteor@1.1.7-rc.1
-meteor-base@1.0.1-rc.0
-meteor-platform@1.2.3-rc.0
+check@1.1.0
+chuangbo:cookie@1.1.0
+coffeescript@1.0.11
+cosmos:browserify@0.9.2
+dburles:collection-helpers@1.0.4
+ddp@1.2.2
+ddp-client@1.2.1
+ddp-common@1.2.2
+ddp-rate-limiter@1.0.0
+ddp-server@1.2.2
+deps@1.0.9
+diff-sequence@1.0.1
+ecmascript@0.1.6
+ecmascript-runtime@0.2.6
+ejson@1.0.7
+email@1.0.8
+es5-shim@4.1.14
+fastclick@1.0.7
+fortawesome:fontawesome@4.5.0
+geojson-utils@1.0.4
+hot-code-push@1.0.0
+html-tools@1.0.5
+htmljs@1.0.5
+http@1.1.1
+id-map@1.0.4
+idmontie:migrations@1.0.1
+jquery@1.11.4
+kadira:blaze-layout@2.3.0
+kadira:dochead@1.4.0
+kadira:flow-router@2.10.0
+kenton:accounts-sandstorm@0.1.8
+launch-screen@1.0.4
+livedata@1.0.15
+localstorage@1.0.5
+logging@1.0.8
+matb33:collection-hooks@0.8.1
+matteodem:easy-search@1.6.4
+meteor@1.1.10
+meteor-base@1.0.1
+meteor-platform@1.2.3
meteorhacks:aggregate@1.3.0
meteorhacks:collection-utils@1.2.0
+meteorhacks:fast-render@2.11.0
+meteorhacks:inject-data@1.4.1
meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.2
meteorspark:util@0.2.0
-minifiers@1.1.6-rc.1
-minimongo@1.0.9-rc.0
-mobile-status-bar@1.0.5-rc.1
-mongo@1.1.1-rc.3
-mongo-id@1.0.1-rc.0
-mongo-livedata@1.0.9-rc.0
+minifiers@1.1.7
+minimongo@1.0.10
+mobile-status-bar@1.0.6
+mongo@1.1.3
+mongo-id@1.0.1
+mongo-livedata@1.0.9
mousetrap:mousetrap@1.4.6_1
mquandalle:autofocus@1.0.0
-mquandalle:jade@0.4.3_1
-mquandalle:jade-compiler@0.4.3
-mquandalle:jquery-textcomplete@0.3.9_1
+mquandalle:collection-mutations@0.1.0
+mquandalle:jade@0.4.5
+mquandalle:jade-compiler@0.4.5
+mquandalle:jquery-textcomplete@0.8.0_1
mquandalle:jquery-ui-drag-drop-sort@0.1.0
-mquandalle:moment@1.0.0
+mquandalle:moment@1.0.1
mquandalle:mousetrap-bindglobal@0.0.1
mquandalle:perfect-scrollbar@0.6.5_2
mquandalle:stylus@1.1.1
npm-bcrypt@0.7.8_2
-npm-mongo@1.4.39-rc.0_1
-observe-sequence@1.0.7-rc.0
+npm-mongo@1.4.39_1
+observe-sequence@1.0.7
ongoworks:speakingurl@1.1.0
-ordered-dict@1.0.4-rc.0
+ordered-dict@1.0.4
peerlibrary:assert@0.2.5
-peerlibrary:base-component@0.10.0
-peerlibrary:blaze-components@0.13.0
+peerlibrary:base-component@0.14.0
+peerlibrary:blaze-components@0.15.1
+peerlibrary:computed-field@0.3.1
+peerlibrary:reactive-field@0.1.0
perak:markdown@1.0.5
-promise@0.4.8-rc.0
+promise@0.5.1
raix:eventemitter@0.1.3
-raix:handlebar-helpers@0.2.4
-random@1.0.4-rc.0
-rate-limit@1.0.0-rc.0
-reactive-dict@1.1.1-rc.0
-reactive-var@1.0.6-rc.0
-reload@1.1.4-rc.0
-retry@1.0.4-rc.0
+raix:handlebar-helpers@0.2.5
+random@1.0.5
+rate-limit@1.0.0
+reactive-dict@1.1.3
+reactive-var@1.0.6
+reload@1.1.4
+retry@1.0.4
reywood:publish-composite@1.4.2
-routepolicy@1.0.6-rc.0
+routepolicy@1.0.6
seriousm:emoji-continued@1.4.0
-service-configuration@1.0.5-rc.0
-session@1.1.1-rc.0
-sha@1.0.4-rc.0
-softwarerero:accounts-t9n@1.1.4
-spacebars@1.0.7-rc.0
-spacebars-compiler@1.0.7-rc.0
-srp@1.0.4-rc.0
-standard-minifiers@1.0.0-rc.1
-tap:i18n@1.5.1
+service-configuration@1.0.5
+session@1.1.1
+sha@1.0.4
+softwarerero:accounts-t9n@1.1.7
+spacebars@1.0.7
+spacebars-compiler@1.0.7
+srp@1.0.4
+standard-minifiers@1.0.2
+tap:i18n@1.7.0
templates:tabs@2.2.0
-templating@1.1.2-rc.4
-templating-tools@1.0.0-rc.0
-tracker@1.0.8-rc.0
-ui@1.0.7-rc.0
-underscore@1.0.4-rc.0
-url@1.0.5-rc.0
-useraccounts:core@1.12.3
-useraccounts:flow-routing@1.12.3
-useraccounts:unstyled@1.12.3
+templating@1.1.5
+templating-tools@1.0.0
+tracker@1.0.9
+ui@1.0.8
+underscore@1.0.4
+url@1.0.5
+useraccounts:core@1.13.0
+useraccounts:flow-routing@1.13.0
+useraccounts:unstyled@1.13.0
verron:autosize@3.0.8
-webapp@1.2.2-rc.2
-webapp-hashing@1.0.4-rc.0
+webapp@1.2.3
+webapp-hashing@1.0.5
zimme:active-route@2.3.2
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..a8724631
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,8 @@
+sudo: false
+language: node_js
+node_js:
+ - "0.10.40"
+install:
+ - "npm install"
+script:
+ - "npm test"
diff --git a/History.md b/Changelog.md
index ddf7032f..9f7aca32 100644
--- a/History.md
+++ b/Changelog.md
@@ -1,9 +1,26 @@
-# NEXT — v0.9
+# v0.10
-This release is a large re-write of the previous code base. Despite being
-relatively similar to v0.8 feature-wise, this release marks the beginning of our
-new user interface and continues to improve the overall performance and
-security. It also features the following improvements:
+This release features:
+
+* Trello boards and cards importation, including card history, assigned members,
+ labels, comments, and attachments;
+* Invite new users to a board using a email address;
+* Autocompletion in the minicard editor. Start with <kbd>@</kbd> to start a
+ board member autocompletion, or <kbd>#</kbd> for a label;
+* Accelerate the initial page rendering by sending the data on the intial HTTP
+ response instead of waiting for the DDP connection to open;
+* Support images attachments copy pasting.
+
+New languages supported: Arabic, Catalan, Italian, and Russian.
+
+Thanks to GitHub users AlexanderS, fisle, floatinghotpot, FuzzyWuzzie, mnutt,
+ndarilek, SirCmpwn, and xavierpriour for their contributions.
+
+# v0.9
+
+This release is a large re-write of the previous code base. This release marks
+the beginning of our new user interface and continues to improve the overall
+performance and security. It also features the following improvements:
* A new user account system, including the possibility to reset a forgotten
password, to change the password, or to enable email confirmation (all of
diff --git a/client/components/activities/activities.jade b/client/components/activities/activities.jade
index 85b1276e..28a9f9c9 100644
--- a/client/components/activities/activities.jade
+++ b/client/components/activities/activities.jade
@@ -14,32 +14,41 @@ template(name="boardActivities")
p.activity-desc
+memberName(user=user)
- if($eq activityType 'createBoard')
- | {{_ 'activity-created' boardLabel}}.
+ if($eq activityType 'addAttachment')
+ | {{{_ 'activity-attached' attachmentLink cardLink}}}.
- if($eq activityType 'createList')
- | {{_ 'activity-added' list.title boardLabel}}.
+ if($eq activityType 'addBoardMember')
+ | {{{_ 'activity-added' memberLink boardLabel}}}.
+
+ if($eq activityType 'addComment')
+ | {{{_ 'activity-on' cardLink}}}
+ a.activity-comment(href="{{ card.absoluteUrl }}")
+ +viewer
+ = comment.text
+
+ if($eq activityType 'archivedCard')
+ | {{{_ 'activity-archived' cardLink}}}.
if($eq activityType 'archivedList')
| {{_ 'activity-archived' list.title}}.
+ if($eq activityType 'createBoard')
+ | {{_ 'activity-created' boardLabel}}.
+
if($eq activityType 'createCard')
| {{{_ 'activity-added' cardLink boardLabel}}}.
- if($eq activityType 'archivedCard')
- | {{{_ 'activity-archived' cardLink}}}.
-
- if($eq activityType 'restoredCard')
- | {{{_ 'activity-sent' cardLink boardLabel}}}.
+ if($eq activityType 'createList')
+ | {{_ 'activity-added' list.title boardLabel}}.
- if($eq activityType 'moveCard')
- | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
+ if($eq activityType 'importBoard')
+ | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
- if($eq activityType 'addBoardMember')
- | {{{_ 'activity-added' memberLink boardLabel}}}.
+ if($eq activityType 'importCard')
+ | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
- if($eq activityType 'removeBoardMember')
- | {{{_ 'activity-excluded' memberLink boardLabel}}}.
+ if($eq activityType 'importList')
+ | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
if($eq activityType 'joinMember')
if($eq currentUser._id member._id)
@@ -47,21 +56,21 @@ template(name="boardActivities")
else
| {{{_ 'activity-added' memberLink cardLink}}}.
+ if($eq activityType 'moveCard')
+ | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
+
+ if($eq activityType 'removeBoardMember')
+ | {{{_ 'activity-excluded' memberLink boardLabel}}}.
+
+ if($eq activityType 'restoredCard')
+ | {{{_ 'activity-sent' cardLink boardLabel}}}.
+
if($eq activityType 'unjoinMember')
if($eq currentUser._id member._id)
| {{{_ 'activity-unjoined' cardLink}}}.
else
| {{{_ 'activity-removed' memberLink cardLink}}}.
- if($eq activityType 'addComment')
- | {{{_ 'activity-on' cardLink}}}
- a.activity-comment(href="{{ card.absoluteUrl }}")
- +viewer
- = comment.text
-
- if($eq activityType 'addAttachment')
- | {{{_ 'activity-attached' attachmentLink cardLink}}}.
-
span.activity-meta {{ moment createdAt }}
template(name="cardActivities")
@@ -72,6 +81,8 @@ template(name="cardActivities")
+memberName(user=user)
if($eq activityType 'createCard')
| {{_ 'activity-added' cardLabel list.title}}.
+ if($eq activityType 'importCard')
+ | {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
if($eq activityType 'joinMember')
if($eq currentUser._id member._id)
| {{_ 'activity-joined' cardLabel}}.
diff --git a/client/components/activities/activities.js b/client/components/activities/activities.js
index 5c5d8370..c1465b04 100644
--- a/client/components/activities/activities.js
+++ b/client/components/activities/activities.js
@@ -9,7 +9,7 @@ BlazeComponent.extendComponent({
// XXX Should we use ReactiveNumber?
this.page = new ReactiveVar(1);
this.loadNextPageLocked = false;
- const sidebar = this.componentParent(); // XXX for some reason not working
+ const sidebar = this.parentComponent(); // XXX for some reason not working
sidebar.callFirstWith(null, 'resetNextPeak');
this.autorun(() => {
const mode = this.data().mode;
@@ -55,11 +55,29 @@ BlazeComponent.extendComponent({
cardLink() {
const card = this.currentData().card();
return card && Blaze.toHTML(HTML.A({
- href: card.absoluteUrl(),
+ href: FlowRouter.path(card.absoluteUrl()),
'class': 'action-card',
}, card.title));
},
+ listLabel() {
+ return this.currentData().list().title;
+ },
+
+ sourceLink() {
+ const source = this.currentData().source;
+ if(source) {
+ if(source.url) {
+ return Blaze.toHTML(HTML.A({
+ href: source.url,
+ }, source.system));
+ } else {
+ return source.system;
+ }
+ }
+ return null;
+ },
+
memberLink() {
return Blaze.toHTMLWithData(Template.memberName, {
user: this.currentData().member(),
@@ -68,8 +86,9 @@ BlazeComponent.extendComponent({
attachmentLink() {
const attachment = this.currentData().attachment();
- return attachment && Blaze.toHTML(HTML.A({
- href: attachment.url({ download: true }),
+ // trying to display url before file is stored generates js errors
+ return attachment && attachment.url({ download: true }) && Blaze.toHTML(HTML.A({
+ href: FlowRouter.path(attachment.url({ download: true })),
target: '_blank',
}, attachment.name()));
},
@@ -83,9 +102,9 @@ BlazeComponent.extendComponent({
},
'submit .js-edit-comment'(evt) {
evt.preventDefault();
- const commentText = this.currentComponent().getValue();
+ const commentText = this.currentComponent().getValue().trim();
const commentId = Template.parentData().commentId;
- if ($.trim(commentText)) {
+ if (commentText) {
CardComments.update(commentId, {
$set: {
text: commentText,
diff --git a/client/components/activities/comments.js b/client/components/activities/comments.js
index 08401caa..18bf9ef0 100644
--- a/client/components/activities/comments.js
+++ b/client/components/activities/comments.js
@@ -24,11 +24,12 @@ BlazeComponent.extendComponent({
},
'submit .js-new-comment-form'(evt) {
const input = this.getInput();
- if ($.trim(input.val())) {
+ const text = input.val().trim();
+ if (text) {
CardComments.insert({
+ text,
boardId: this.currentData().boardId,
cardId: this.currentData()._id,
- text: input.val(),
});
resetCommentInput(input);
Tracker.flush();
@@ -72,8 +73,9 @@ EscapeActions.register('inlinedForm',
docId: Session.get('currentCard'),
};
const commentInput = $('.js-new-comment-input');
- if ($.trim(commentInput.val())) {
- UnsavedEdits.set(draftKey, commentInput.val());
+ const draft = commentInput.val().trim();
+ if (draft) {
+ UnsavedEdits.set(draftKey, draft);
} else {
UnsavedEdits.reset(draftKey);
}
diff --git a/client/components/boards/boardArchive.js b/client/components/boards/boardArchive.js
index 9d7ca7f2..35f795f3 100644
--- a/client/components/boards/boardArchive.js
+++ b/client/components/boards/boardArchive.js
@@ -22,13 +22,9 @@ BlazeComponent.extendComponent({
events() {
return [{
'click .js-restore-board'() {
- const boardId = this.currentData()._id;
- Boards.update(boardId, {
- $set: {
- archived: false,
- },
- });
- Utils.goBoardId(boardId);
+ const board = this.currentData();
+ board.restore();
+ Utils.goBoardId(board._id);
},
}];
},
diff --git a/client/components/boards/boardBody.js b/client/components/boards/boardBody.js
index 95590beb..a601bc2e 100644
--- a/client/components/boards/boardBody.js
+++ b/client/components/boards/boardBody.js
@@ -34,7 +34,7 @@ BlazeComponent.extendComponent({
},
openNewListForm() {
- this.componentChildren('addListForm')[0].open();
+ this.childComponents('addListForm')[0].open();
},
// XXX Flow components allow us to avoid creating these two setter methods by
@@ -45,7 +45,8 @@ BlazeComponent.extendComponent({
},
scrollLeft(position = 0) {
- this.$('.js-lists').animate({
+ const lists = this.$('.js-lists');
+ lists && lists.animate({
scrollLeft: position,
});
},
@@ -133,7 +134,7 @@ Template.boardBody.onRendered(function() {
if (!Meteor.user() || !Meteor.user().isBoardMember())
return;
- self.$(self.listsDom).sortable({
+ $(self.listsDom).sortable({
tolerance: 'pointer',
helper: 'clone',
handle: '.js-list-header',
@@ -145,7 +146,7 @@ Template.boardBody.onRendered(function() {
Popup.close();
},
stop() {
- self.$('.js-lists').find('.js-list:not(.js-list-composer)').each(
+ $(self.listsDom).find('.js-list:not(.js-list-composer)').each(
(i, list) => {
const data = Blaze.getData(list);
Lists.update(data._id, {
@@ -160,7 +161,7 @@ Template.boardBody.onRendered(function() {
// Disable drag-dropping while in multi-selection mode
self.autorun(() => {
- self.$(self.listsDom).sortable('option', 'disabled',
+ $(self.listsDom).sortable('option', 'disabled',
MultiSelection.isActive());
});
@@ -179,22 +180,24 @@ BlazeComponent.extendComponent({
// Proxy
open() {
- this.componentChildren('inlinedForm')[0].open();
+ this.childComponents('inlinedForm')[0].open();
},
events() {
return [{
submit(evt) {
evt.preventDefault();
- const title = this.find('.list-name-input');
- if ($.trim(title.value)) {
+ const titleInput = this.find('.list-name-input');
+ const title = titleInput.value.trim();
+ if (title) {
Lists.insert({
- title: title.value,
+ title,
boardId: Session.get('currentBoard'),
sort: $('.list').length,
});
- title.value = '';
+ titleInput.value = '';
+ titleInput.focus();
}
},
}];
diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade
index 94225730..a0160382 100644
--- a/client/components/boards/boardHeader.jade
+++ b/client/components/boards/boardHeader.jade
@@ -32,7 +32,7 @@ template(name="headerBoard")
title="{{#if MultiSelection.isActive}}{{_ 'filter-on-desc'}}{{/if}}"
class="{{#if MultiSelection.isActive}}emphasis{{/if}}")
i.fa.fa-check-square-o
- span Multi-Selection {{#if MultiSelection.isActive}}is on{{/if}}
+ span {{#if MultiSelection.isActive}}{{_ 'multi-selection-on'}}{{else}}{{_ 'multi-selection'}}{{/if}}
if MultiSelection.isActive
a.board-header-btn-close.js-multiselection-reset(title="{{_ 'filter-clear'}}")
i.fa.fa-times-thin
@@ -105,8 +105,11 @@ template(name="createBoardPopup")
span.fa.fa-lock.colorful
= " "
| {{{_ 'board-private-info'}}}
- a.js-change-visibility Change.
+ a.js-change-visibility {{_ 'change'}}.
input.primary.wide(type="submit" value="{{_ 'create'}}")
+ span.quiet
+ | {{_ 'or'}}
+ a.js-import {{_ 'import-board'}}
template(name="boardChangeTitlePopup")
@@ -114,6 +117,9 @@ template(name="boardChangeTitlePopup")
label
| {{_ 'title'}}
input.js-board-name(type="text" value=title autofocus)
+ label
+ | {{_ 'description'}}
+ textarea.js-board-desc= description
input.primary.wide(type="submit" value="{{_ 'rename'}}")
template(name="archiveBoardPopup")
diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js
index f259b2a6..3dc6d754 100644
--- a/client/components/boards/boardHeader.js
+++ b/client/components/boards/boardHeader.js
@@ -6,9 +6,9 @@ Template.boardMenuPopup.events({
},
'click .js-change-board-color': Popup.open('boardChangeColor'),
'click .js-change-language': Popup.open('changeLanguage'),
- 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', () => {
- const boardId = Session.get('currentBoard');
- Boards.update(boardId, { $set: { archived: true }});
+ 'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ currentBoard.archive();
// XXX We should have some kind of notification on top of the page to
// confirm that the board was successfully archived.
FlowRouter.go('home');
@@ -17,13 +17,11 @@ Template.boardMenuPopup.events({
Template.boardChangeTitlePopup.events({
submit(evt, tpl) {
- const title = tpl.$('.js-board-name').val().trim();
- if (title) {
- Boards.update(this._id, {
- $set: {
- title,
- },
- });
+ const newTitle = tpl.$('.js-board-name').val().trim();
+ const newDesc = tpl.$('.js-board-desc').val().trim();
+ if (newTitle) {
+ this.rename(newTitle);
+ this.setDesciption(newDesc);
Popup.close();
}
evt.preventDefault();
@@ -95,12 +93,9 @@ BlazeComponent.extendComponent({
events() {
return [{
'click .js-select-background'(evt) {
- const currentBoardId = Session.get('currentBoard');
- Boards.update(currentBoardId, {
- $set: {
- color: this.currentData().toString(),
- },
- });
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const newColor = this.currentData().toString();
+ currentBoard.setColor(newColor);
evt.preventDefault();
},
}];
@@ -152,6 +147,7 @@ BlazeComponent.extendComponent({
this.setVisibility(this.currentData());
},
'click .js-change-visibility': this.toggleVisibilityMenu,
+ 'click .js-import': Popup.open('boardImportBoard'),
submit: this.onSubmit,
}];
},
@@ -168,11 +164,9 @@ BlazeComponent.extendComponent({
},
selectBoardVisibility() {
- Boards.update(Session.get('currentBoard'), {
- $set: {
- permission: this.currentData(),
- },
- });
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const visibility = this.currentData();
+ currentBoard.setVisibility(visibility);
Popup.close();
},
diff --git a/client/components/boards/boardHeader.styl b/client/components/boards/boardHeader.styl
new file mode 100644
index 00000000..adfe4b19
--- /dev/null
+++ b/client/components/boards/boardHeader.styl
@@ -0,0 +1,2 @@
+a.js-import
+ text-decoration underline
diff --git a/client/components/boards/boardsList.jade b/client/components/boards/boardsList.jade
index 11333eee..7099cdc9 100644
--- a/client/components/boards/boardsList.jade
+++ b/client/components/boards/boardsList.jade
@@ -3,11 +3,23 @@ template(name="boardList")
ul.board-list.clearfix
each boards
li(class="{{#if isStarred}}starred{{/if}}" class=colorClass)
- a.js-open-board(href="{{pathFor 'board' id=_id slug=slug}}")
- span.details
- span.board-list-item-name= title
- i.fa.js-star-board(
- class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
- title="{{_ 'star-board-title'}}")
+ if isInvited
+ .board-list-item
+ span.details
+ span.board-list-item-name= title
+ i.fa.js-star-board(
+ class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
+ title="{{_ 'star-board-title'}}")
+ p.board-list-item-desc {{_ 'just-invited'}}
+ button.js-accept-invite.primary {{_ 'accept'}}
+ button.js-decline-invite {{_ 'decline'}}
+ else
+ a.js-open-board.board-list-item(href="{{pathFor 'board' id=_id slug=slug}}")
+ span.details
+ span.board-list-item-name= title
+ i.fa.js-star-board(
+ class="fa-star{{#if isStarred}} is-star-active{{else}}-o{{/if}}"
+ title="{{_ 'star-board-title'}}")
+ p.board-list-item-desc= description
li.js-add-board
- a.label {{_ 'add-board'}}
+ a.board-list-item.label {{_ 'add-board'}}
diff --git a/client/components/boards/boardsList.js b/client/components/boards/boardsList.js
index 1a2d3c9a..131adf9d 100644
--- a/client/components/boards/boardsList.js
+++ b/client/components/boards/boardsList.js
@@ -17,6 +17,11 @@ BlazeComponent.extendComponent({
return user && user.hasStarred(this.currentData()._id);
},
+ isInvited() {
+ const user = Meteor.user();
+ return user && user.isInvitedTo(this.currentData()._id);
+ },
+
events() {
return [{
'click .js-add-board': Popup.open('createBoard'),
@@ -25,6 +30,19 @@ BlazeComponent.extendComponent({
Meteor.user().toggleBoardStar(boardId);
evt.preventDefault();
},
+ 'click .js-accept-invite'() {
+ const boardId = this.currentData()._id;
+ Meteor.user().removeInvite(boardId);
+ },
+ 'click .js-decline-invite'() {
+ const boardId = this.currentData()._id;
+ Meteor.call('quitBoard', boardId, (err, ret) => {
+ if (!err && ret) {
+ Meteor.user().removeInvite(boardId);
+ FlowRouter.go('home');
+ }
+ });
+ },
}];
},
}).register('boardList');
diff --git a/client/components/boards/boardsList.styl b/client/components/boards/boardsList.styl
index 9978fab8..e24940a0 100644
--- a/client/components/boards/boardsList.styl
+++ b/client/components/boards/boardsList.styl
@@ -14,7 +14,7 @@ $spaceBetweenTiles = 16px
.fa-star-o
opacity: 1
- a
+ .board-list-item
background-color: #999
color: #f6f6f6
height: 90px
@@ -40,6 +40,13 @@ $spaceBetweenTiles = 16px
font-weight: 400
line-height: 22px
+ .board-list-item-desc
+ color: rgba(255, 255, 255, .5)
+ display: block
+ font-size: 10px
+ font-weight: 400
+ line-height: 18px
+
.js-add-board
text-align:center
diff --git a/client/components/cards/attachments.jade b/client/components/cards/attachments.jade
index 59eaf077..2cb3bb85 100644
--- a/client/components/cards/attachments.jade
+++ b/client/components/cards/attachments.jade
@@ -3,6 +3,16 @@ template(name="cardAttachmentsPopup")
li
input.js-attach-file.hide(type="file" name="file" multiple)
a.js-computer-upload {{_ 'computer'}}
+ li
+ a.js-upload-clipboard-image {{_ 'clipboard'}}
+
+template(name="previewClipboardImagePopup")
+ p <kbd>Ctrl</kbd>+<kbd>V</kbd> {{_ "paste-or-dragdrop"}}
+ img.preview-clipboard-image()
+ button.primary.js-upload-pasted-image {{_ 'upload'}}
+
+template(name="previewAttachedImagePopup")
+ img.preview-large-image.js-large-image-clicked(src="{{pathFor url}}")
template(name="attachmentDeletePopup")
p {{_ "attachment-delete-pop"}}
@@ -15,7 +25,7 @@ template(name="attachmentsGalery")
.attachment-thumbnail
if isUploaded
if isImage
- img.attachment-thumbnail-img(src=url)
+ img.attachment-thumbnail-img.js-preview-image(src="{{pathFor url}}")
else
span.attachment-thumbnail-ext= extension
else
diff --git a/client/components/cards/attachments.js b/client/components/cards/attachments.js
index ba56aa1a..1e5aa03b 100644
--- a/client/components/cards/attachments.js
+++ b/client/components/cards/attachments.js
@@ -1,7 +1,7 @@
Template.attachmentsGalery.events({
'click .js-add-attachment': Popup.open('cardAttachments'),
'click .js-confirm-delete': Popup.afterConfirm('attachmentDelete',
- () => {
+ function() {
Attachments.remove(this._id);
Popup.close();
}
@@ -15,10 +15,43 @@ Template.attachmentsGalery.events({
// XXX Not implemented!
},
'click .js-add-cover'() {
- Cards.update(this.cardId, { $set: { coverId: this._id } });
+ Cards.findOne(this.cardId).setCover(this._id);
},
'click .js-remove-cover'() {
- Cards.update(this.cardId, { $unset: { coverId: '' } });
+ Cards.findOne(this.cardId).unsetCover();
+ },
+ 'click .js-preview-image'(evt) {
+ Popup.open('previewAttachedImage').call(this, evt);
+ // when multiple thumbnails, if click one then another very fast,
+ // we might get a wrong width from previous img.
+ // when popup reused, onRendered() won't be called, so we cannot get there.
+ // here make sure to get correct size when this img fully loaded.
+ const img = $('img.preview-large-image')[0];
+ if (!img) return;
+ const rePosPopup = () => {
+ const w = img.width;
+ const h = img.height;
+ // if the image is too large, we resize & center the popup.
+ if (w > 300) {
+ $('div.pop-over').css({
+ width: (w + 20),
+ position: 'absolute',
+ left: (window.innerWidth - w)/2,
+ top: (window.innerHeight - h)/2,
+ });
+ }
+ };
+ const url = $(evt.currentTarget).attr('src');
+ if (img.src === url && img.complete)
+ rePosPopup();
+ else
+ img.onload = rePosPopup;
+ },
+});
+
+Template.previewAttachedImagePopup.events({
+ 'click .js-large-image-clicked'(){
+ Popup.close();
},
});
@@ -28,7 +61,7 @@ Template.cardAttachmentsPopup.events({
FS.Utility.eachFile(evt, (f) => {
const file = new FS.File(f);
file.boardId = card.boardId;
- file.cardId = card._id;
+ file.cardId = card._id;
Attachments.insert(file);
Popup.close();
@@ -38,4 +71,48 @@ Template.cardAttachmentsPopup.events({
tpl.find('.js-attach-file').click();
evt.preventDefault();
},
+ 'click .js-upload-clipboard-image': Popup.open('previewClipboardImage'),
+});
+
+let pastedResults = null;
+
+Template.previewClipboardImagePopup.onRendered(() => {
+ // we can paste image from clipboard
+ $(document.body).pasteImageReader((results) => {
+ if (results.dataURL.startsWith('data:image/')) {
+ $('img.preview-clipboard-image').attr('src', results.dataURL);
+ pastedResults = results;
+ }
+ });
+
+ // we can also drag & drop image file to it
+ $(document.body).dropImageReader((results) => {
+ if (results.dataURL.startsWith('data:image/')) {
+ $('img.preview-clipboard-image').attr('src', results.dataURL);
+ pastedResults = results;
+ }
+ });
+});
+
+Template.previewClipboardImagePopup.events({
+ 'click .js-upload-pasted-image'() {
+ const results = pastedResults;
+ if (results && results.file) {
+ const card = this;
+ const file = new FS.File(results.file);
+ if (!results.name) {
+ // if no filename, it's from clipboard. then we give it a name, with ext name from MIME type
+ if (typeof results.file.type === 'string') {
+ file.name(results.file.type.replace('image/', 'clipboard.'));
+ }
+ }
+ file.updatedAt(new Date());
+ file.boardId = card.boardId;
+ file.cardId = card._id;
+ Attachments.insert(file);
+ pastedResults = null;
+ $(document.body).pasteImageReader(() => {});
+ Popup.close();
+ }
+ },
});
diff --git a/client/components/cards/attachments.styl b/client/components/cards/attachments.styl
index 5cdf7386..a582f3af 100644
--- a/client/components/cards/attachments.styl
+++ b/client/components/cards/attachments.styl
@@ -45,3 +45,14 @@
display: block
box-shadow: 0 1px 2px rgba(0,0,0,.2)
+.preview-large-image
+ max-width: 1000px
+ display: block
+ box-shadow: 0 1px 2px rgba(0,0,0,.2)
+
+.preview-clipboard-image
+ width: 280px
+ height: 200px
+ display: block
+ border: 1px solid black
+ box-shadow: 0 1px 2px rgba(0,0,0,.2)
diff --git a/client/components/cards/cardDetails.js b/client/components/cards/cardDetails.js
index 09c99f4e..b4fdca52 100644
--- a/client/components/cards/cardDetails.js
+++ b/client/components/cards/cardDetails.js
@@ -13,19 +13,19 @@ BlazeComponent.extendComponent({
},
reachNextPeak() {
- const activitiesComponent = this.componentChildren('activities')[0];
+ const activitiesComponent = this.childComponents('activities')[0];
activitiesComponent.loadNextPage();
},
onCreated() {
this.isLoaded = new ReactiveVar(false);
- this.componentParent().showOverlay.set(true);
- this.componentParent().mouseHasEnterCardDetails = false;
+ this.parentComponent().showOverlay.set(true);
+ this.parentComponent().mouseHasEnterCardDetails = false;
},
scrollParentContainer() {
const cardPanelWidth = 510;
- const bodyBoardComponent = this.componentParent();
+ const bodyBoardComponent = this.parentComponent();
const $cardContainer = bodyBoardComponent.$('.js-lists');
const $cardView = this.$(this.firstNode());
@@ -52,13 +52,7 @@ BlazeComponent.extendComponent({
},
onDestroyed() {
- this.componentParent().showOverlay.set(false);
- },
-
- updateCard(modifier) {
- Cards.update(this.data()._id, {
- $set: modifier,
- });
+ this.parentComponent().showOverlay.set(false);
},
events() {
@@ -68,7 +62,8 @@ BlazeComponent.extendComponent({
},
};
- return [_.extend(events, {
+ return [{
+ ...events,
'click .js-close-card-details'() {
Utils.goBoardId(this.data().boardId);
},
@@ -76,23 +71,23 @@ BlazeComponent.extendComponent({
'submit .js-card-description'(evt) {
evt.preventDefault();
const description = this.currentComponent().getValue();
- this.updateCard({ description });
+ this.data().setDescription(description);
},
'submit .js-card-details-title'(evt) {
evt.preventDefault();
- const title = this.currentComponent().getValue();
- if ($.trim(title)) {
- this.updateCard({ title });
+ const title = this.currentComponent().getValue().trim();
+ if (title) {
+ this.data().setTitle(title);
}
},
'click .js-member': Popup.open('cardMember'),
'click .js-add-members': Popup.open('cardMembers'),
'click .js-add-labels': Popup.open('cardLabels'),
'mouseenter .js-card-details'() {
- this.componentParent().showOverlay.set(true);
- this.componentParent().mouseHasEnterCardDetails = true;
+ this.parentComponent().showOverlay.set(true);
+ this.parentComponent().mouseHasEnterCardDetails = true;
},
- })];
+ }];
},
}).register('cardDetails');
@@ -111,7 +106,7 @@ BlazeComponent.extendComponent({
close(isReset = false) {
if (this.isOpen.get() && !isReset) {
- const draft = $.trim(this.getValue());
+ const draft = this.getValue().trim();
if (draft !== Cards.findOne(Session.get('currentCard')).description) {
UnsavedEdits.set(this._getUnsavedEditKey(), this.getValue());
}
@@ -138,14 +133,9 @@ Template.cardDetailsActionsPopup.events({
'click .js-labels': Popup.open('cardLabels'),
'click .js-attachments': Popup.open('cardAttachments'),
'click .js-move-card': Popup.open('moveCard'),
- // 'click .js-copy': Popup.open(),
'click .js-archive'(evt) {
evt.preventDefault();
- Cards.update(this._id, {
- $set: {
- archived: true,
- },
- });
+ this.archive();
Popup.close();
},
'click .js-more': Popup.open('cardMore'),
@@ -155,22 +145,18 @@ Template.moveCardPopup.events({
'click .js-select-list'() {
// XXX We should *not* get the currentCard from the global state, but
// instead from a “component” state.
- const cardId = Session.get('currentCard');
+ const card = Cards.findOne(Session.get('currentCard'));
const newListId = this._id;
- Cards.update(cardId, {
- $set: {
- listId: newListId,
- },
- });
+ card.move(newListId);
Popup.close();
},
});
Template.cardMorePopup.events({
- 'click .js-delete': Popup.afterConfirm('cardDelete', () => {
+ 'click .js-delete': Popup.afterConfirm('cardDelete', function() {
Popup.close();
Cards.remove(this._id);
- Utils.goBoardId(this.board()._id);
+ Utils.goBoardId(this.boardId);
}),
});
diff --git a/client/components/cards/labels.jade b/client/components/cards/labels.jade
index a868627c..31bd4d06 100644
--- a/client/components/cards/labels.jade
+++ b/client/components/cards/labels.jade
@@ -18,7 +18,7 @@ template(name="editLabelPopup")
form.edit-label
+formLabel
button.primary.wide.left(type="submit") {{_ 'save'}}
- span.right
+ button.js-delete-label.negate.wide.right {{_ 'delete'}}
template(name="deleteLabelPopup")
p {{_ "label-delete-pop"}}
diff --git a/client/components/cards/labels.js b/client/components/cards/labels.js
index 2da3b80b..4e61a0c6 100644
--- a/client/components/cards/labels.js
+++ b/client/components/cards/labels.js
@@ -13,7 +13,7 @@ BlazeComponent.extendComponent({
},
labels() {
- return _.map(labelColors, (color) => {
+ return labelColors.map((color) => {
return { color, name: '' };
});
},
@@ -45,19 +45,9 @@ Template.createLabelPopup.helpers({
Template.cardLabelsPopup.events({
'click .js-select-label'(evt) {
- const cardId = Template.parentData(2).data._id;
+ const card = Cards.findOne(Session.get('currentCard'));
const labelId = this._id;
- let operation;
- if (Cards.find({ _id: cardId, labelIds: labelId}).count() === 0)
- operation = '$addToSet';
- else
- operation = '$pull';
-
- Cards.update(cardId, {
- [operation]: {
- labelIds: labelId,
- },
- });
+ card.toggleLabel(labelId);
evt.preventDefault();
},
'click .js-edit-label': Popup.open('editLabel'),
@@ -79,52 +69,27 @@ Template.formLabel.events({
Template.createLabelPopup.events({
// Create the new label
'submit .create-label'(evt, tpl) {
+ evt.preventDefault();
+ const board = Boards.findOne(Session.get('currentBoard'));
const name = tpl.$('#labelName').val().trim();
- const boardId = Session.get('currentBoard');
const color = Blaze.getData(tpl.find('.fa-check')).color;
-
- Boards.update(boardId, {
- $push: {
- labels: {
- name,
- color,
- _id: Random.id(6),
- },
- },
- });
-
+ board.addLabel(name, color);
Popup.back();
- evt.preventDefault();
},
});
Template.editLabelPopup.events({
'click .js-delete-label': Popup.afterConfirm('deleteLabel', function() {
- const boardId = Session.get('currentBoard');
- Boards.update(boardId, {
- $pull: {
- labels: {
- _id: this._id,
- },
- },
- });
-
+ const board = Boards.findOne(Session.get('currentBoard'));
+ board.removeLabel(this._id);
Popup.back(2);
}),
'submit .edit-label'(evt, tpl) {
evt.preventDefault();
+ const board = Boards.findOne(Session.get('currentBoard'));
const name = tpl.$('#labelName').val().trim();
- const boardId = Session.get('currentBoard');
- const getLabel = Utils.getLabelIndex(boardId, this._id);
const color = Blaze.getData(tpl.find('.fa-check')).color;
-
- Boards.update(boardId, {
- $set: {
- [getLabel.key('name')]: name,
- [getLabel.key('color')]: color,
- },
- });
-
+ board.editLabel(this._id, name, color);
Popup.back();
},
});
diff --git a/client/components/cards/minicard.jade b/client/components/cards/minicard.jade
index 660b0fa5..573b3da1 100644
--- a/client/components/cards/minicard.jade
+++ b/client/components/cards/minicard.jade
@@ -2,7 +2,7 @@ template(name="minicard")
.minicard
if cover
.minicard-cover
- img(src=cover.url)
+ img(src="{{pathFor cover.url}}")
if labels
.minicard-labels
each labels
diff --git a/client/components/forms/forms.styl b/client/components/forms/forms.styl
index 83d25370..9ae95140 100644
--- a/client/components/forms/forms.styl
+++ b/client/components/forms/forms.styl
@@ -617,8 +617,15 @@ button
margin-right: 5px
vertical-align: middle
+ .minicard-label
+ width: 11px
+ height: @width
+ border-radius: 2px
+ margin: 2px 7px -2px -2px
+ display: inline-block
+
&.active
background: #005377
- a
+ a, .quiet
color: white
diff --git a/client/components/import/import.jade b/client/components/import/import.jade
new file mode 100644
index 00000000..74b6ca13
--- /dev/null
+++ b/client/components/import/import.jade
@@ -0,0 +1,54 @@
+template(name="importPopup")
+ if error.get
+ .warning {{_ error.get}}
+ form
+ p: label(for='import-textarea') {{_ getLabel}}
+ textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
+ | {{jsonText}}
+ if membersMapping
+ div
+ a.show-mapping
+ | {{_ 'import-show-user-mapping'}}
+ input.primary.wide(type="submit" value="{{_ 'import'}}")
+
+template(name="mapMembersPopup")
+ .map-members
+ p {{_ 'import-members-map'}}
+ .mapping-list
+ each members
+ .mapping
+ a.source
+ div.full-name
+ = fullName
+ div.username
+ | ({{username}})
+ .wekan
+ if wekan
+ +userAvatar(userId=wekan._id)
+ else
+ a.member.add-member.js-add-members
+ i.fa.fa-plus
+ form
+ input.primary.wide(type="submit" value="{{_ 'done'}}")
+
+ template(name="addMemberPopup")
+
+template(name="mapMembersAddPopup")
+ .select-member
+ p
+ | {{_ 'import-user-select'}}
+ .js-map-member
+ +esInput(index="users")
+ ul.pop-over-list
+ +esEach(index="users")
+ li.item.js-member-item
+ a.name.js-select-import(title="{{profile.name}} ({{username}})" data-id="{{_id}}")
+ +userAvatar(userId=_id esSearch=true)
+ span.full-name
+ = profile.name
+ | (<span class="username">{{username}}</span>)
+ +ifEsIsSearching(index='users')
+ +spinner
+ +ifEsHasNoResults(index="users")
+ .manage-member-section
+ p.quiet {{_ 'no-results'}}
diff --git a/client/components/import/import.js b/client/components/import/import.js
new file mode 100644
index 00000000..63285e57
--- /dev/null
+++ b/client/components/import/import.js
@@ -0,0 +1,271 @@
+/// Abstract root for all import popup screens.
+/// Descendants must define:
+/// - getMethodName(): return the Meteor method to call for import, passing json
+/// data decoded as object and additional data (see below);
+/// - getAdditionalData(): return object containing additional data passed to
+/// Meteor method (like list ID and position for a card import);
+/// - getLabel(): i18n key for the text displayed in the popup, usually to
+/// explain how to get the data out of the source system.
+const ImportPopup = BlazeComponent.extendComponent({
+ template() {
+ return 'importPopup';
+ },
+
+ jsonText() {
+ return Session.get('import.text');
+ },
+
+ membersMapping() {
+ return Session.get('import.membersToMap');
+ },
+
+ onCreated() {
+ this.error = new ReactiveVar('');
+ this.dataToImport = '';
+ },
+
+ onFinish() {
+ Popup.close();
+ },
+
+ onShowMapping(evt) {
+ this._storeText(evt);
+ Popup.open('mapMembers')(evt);
+ },
+
+ onSubmit(evt){
+ evt.preventDefault();
+ const dataJson = this._storeText(evt);
+ let dataObject;
+ try {
+ dataObject = JSON.parse(dataJson);
+ this.setError('');
+ } catch (e) {
+ this.setError('error-json-malformed');
+ return;
+ }
+ if(this._hasAllNeededData(dataObject)) {
+ this._import(dataObject);
+ } else {
+ this._prepareAdditionalData(dataObject);
+ Popup.open(this._screenAdditionalData())(evt);
+
+ }
+ },
+
+ events() {
+ return [{
+ submit: this.onSubmit,
+ 'click .show-mapping': this.onShowMapping,
+ }];
+ },
+
+ setError(error) {
+ this.error.set(error);
+ },
+
+ _import(dataObject) {
+ const additionalData = this.getAdditionalData();
+ const membersMapping = this.membersMapping();
+ if (membersMapping) {
+ const mappingById = {};
+ membersMapping.forEach((member) => {
+ if (member.wekan) {
+ mappingById[member.id] = member.wekan._id;
+ }
+ });
+ additionalData.membersMapping = mappingById;
+ }
+ Session.set('import.membersToMap', null);
+ Session.set('import.text', null);
+ Meteor.call(this.getMethodName(), dataObject, additionalData,
+ (error, response) => {
+ if (error) {
+ this.setError(error.error);
+ } else {
+ // ensure will display what we just imported
+ Filter.addException(response);
+ this.onFinish(response);
+ }
+ }
+ );
+ },
+
+ _hasAllNeededData(dataObject) {
+ // import has no members or they are already mapped
+ return dataObject.members.length === 0 || this.membersMapping();
+ },
+
+ _prepareAdditionalData(dataObject) {
+ // we will work on the list itself (an ordered array of objects)
+ // when a mapping is done, we add a 'wekan' field to the object representing the imported member
+ const membersToMap = dataObject.members;
+ // auto-map based on username
+ membersToMap.forEach((importedMember) => {
+ const wekanUser = Users.findOne({username: importedMember.username});
+ if(wekanUser) {
+ importedMember.wekan = wekanUser;
+ }
+ });
+ // store members data and mapping in Session
+ // (we go deep and 2-way, so storing in data context is not a viable option)
+ Session.set('import.membersToMap', membersToMap);
+ return membersToMap;
+ },
+
+ _screenAdditionalData() {
+ return 'mapMembers';
+ },
+
+ _storeText() {
+ const dataJson = this.$('.js-import-json').val();
+ Session.set('import.text', dataJson);
+ return dataJson;
+ },
+});
+
+ImportPopup.extendComponent({
+ getAdditionalData() {
+ const listId = this.currentData()._id;
+ const selector = `#js-list-${this.currentData()._id} .js-minicard:first`;
+ const firstCardDom = $(selector).get(0);
+ const sortIndex = Utils.calculateIndex(null, firstCardDom).base;
+ const result = {listId, sortIndex};
+ return result;
+ },
+
+ getMethodName() {
+ return 'importTrelloCard';
+ },
+
+ getLabel() {
+ return 'import-card-trello-instruction';
+ },
+}).register('listImportCardPopup');
+
+ImportPopup.extendComponent({
+ getAdditionalData() {
+ const result = {};
+ return result;
+ },
+
+ getMethodName() {
+ return 'importTrelloBoard';
+ },
+
+ getLabel() {
+ return 'import-board-trello-instruction';
+ },
+
+ onFinish(response) {
+ Utils.goBoardId(response);
+ },
+}).register('boardImportBoardPopup');
+
+const ImportMapMembers = BlazeComponent.extendComponent({
+ members() {
+ return Session.get('import.membersToMap');
+ },
+ _refreshMembers(listOfMembers) {
+ Session.set('import.membersToMap', listOfMembers);
+ },
+ /**
+ * Will look into the list of members to import for the specified memberId,
+ * then set its property to the supplied value.
+ * If unset is true, it will remove the property from the rest of the list as well.
+ *
+ * use:
+ * - memberId = null to use selected member
+ * - value = null to unset a property
+ * - unset = true to ensure property is only set on 1 member at a time
+ */
+ _setPropertyForMember(property, value, memberId, unset = false) {
+ const listOfMembers = this.members();
+ let finder = null;
+ if(memberId) {
+ finder = (member) => member.id === memberId;
+ } else {
+ finder = (member) => member.selected;
+ }
+ listOfMembers.forEach((member) => {
+ if(finder(member)) {
+ if(value !== null) {
+ member[property] = value;
+ } else {
+ delete member[property];
+ }
+ if(!unset) {
+ // we shortcut if we don't care about unsetting the others
+ return false;
+ }
+ } else if(unset) {
+ delete member[property];
+ }
+ return true;
+ });
+ // Session.get gives us a copy, we have to set it back so it sticks
+ this._refreshMembers(listOfMembers);
+ },
+ setSelectedMember(memberId) {
+ return this._setPropertyForMember('selected', true, memberId, true);
+ },
+ /**
+ * returns the member with specified id,
+ * or the selected member if memberId is not specified
+ */
+ getMember(memberId = null) {
+ const allMembers = Session.get('import.membersToMap');
+ let finder = null;
+ if(memberId) {
+ finder = (user) => user.id === memberId;
+ } else {
+ finder = (user) => user.selected;
+ }
+ return allMembers.find(finder);
+ },
+ mapSelectedMember(wekan) {
+ return this._setPropertyForMember('wekan', wekan, null);
+ },
+ unmapMember(memberId){
+ return this._setPropertyForMember('wekan', null, memberId);
+ },
+});
+
+ImportMapMembers.extendComponent({
+ onMapMember(evt) {
+ const memberToMap = this.currentData();
+ if(memberToMap.wekan) {
+ // todo xxx ask for confirmation?
+ this.unmapMember(memberToMap.id);
+ } else {
+ this.setSelectedMember(memberToMap.id);
+ Popup.open('mapMembersAdd')(evt);
+ }
+ },
+ onSubmit(evt) {
+ evt.preventDefault();
+ Popup.back();
+ },
+ events() {
+ return [{
+ 'submit': this.onSubmit,
+ 'click .mapping': this.onMapMember,
+ }];
+ },
+}).register('mapMembersPopup');
+
+ImportMapMembers.extendComponent({
+ onSelectUser(){
+ this.mapSelectedMember(this.currentData());
+ Popup.back();
+ },
+ events() {
+ return [{
+ 'click .js-select-import': this.onSelectUser,
+ }];
+ },
+ onRendered() {
+ // todo XXX why do I not get the focus??
+ this.find('.js-map-member input').focus();
+ },
+}).register('mapMembersAddPopup');
diff --git a/client/components/import/import.styl b/client/components/import/import.styl
new file mode 100644
index 00000000..3c6cfdf3
--- /dev/null
+++ b/client/components/import/import.styl
@@ -0,0 +1,17 @@
+.map-members
+ .mapping:first-of-type
+ border-top: solid 1px #999
+ .mapping
+ padding: 10px 0
+ border-bottom: solid 1px #999
+ .source
+ display: inline-block
+ width: 80%
+ .wekan
+ display: inline-block
+ width: 35px
+ .member
+ float: none
+
+a.show-mapping
+ text-decoration underline
diff --git a/client/components/lists/list.js b/client/components/lists/list.js
index cdf30fc2..f5410ed0 100644
--- a/client/components/lists/list.js
+++ b/client/components/lists/list.js
@@ -7,7 +7,7 @@ BlazeComponent.extendComponent({
// Proxy
openForm(options) {
- this.componentChildren('listBody')[0].openForm(options);
+ this.childComponents('listBody')[0].openForm(options);
},
onCreated() {
@@ -25,7 +25,7 @@ BlazeComponent.extendComponent({
if (!Meteor.user() || !Meteor.user().isBoardMember())
return;
- const boardComponent = this.componentParent();
+ const boardComponent = this.parentComponent();
const itemsSelector = '.js-minicard:not(.placeholder, .js-card-composer)';
const $cards = this.$('.js-minicards');
$cards.sortable({
@@ -73,23 +73,13 @@ BlazeComponent.extendComponent({
$cards.sortable('cancel');
if (MultiSelection.isActive()) {
- Cards.find(MultiSelection.getMongoSelector()).forEach((c, i) => {
- Cards.update(c._id, {
- $set: {
- listId,
- sort: sortIndex.base + i * sortIndex.increment,
- },
- });
+ Cards.find(MultiSelection.getMongoSelector()).forEach((card, i) => {
+ card.move(listId, sortIndex.base + i * sortIndex.increment);
});
} else {
const cardDomElement = ui.item.get(0);
- const cardId = Blaze.getData(cardDomElement)._id;
- Cards.update(cardId, {
- $set: {
- listId,
- sort: sortIndex.base,
- },
- });
+ const card = Blaze.getData(cardDomElement);
+ card.move(listId, sortIndex.base);
}
boardComponent.setIsDragging(false);
},
@@ -107,16 +97,15 @@ BlazeComponent.extendComponent({
accept: '.js-member,.js-label',
drop(event, ui) {
const cardId = Blaze.getData(this)._id;
- let addToSet;
+ const card = Cards.findOne(cardId);
if (ui.draggable.hasClass('js-member')) {
const memberId = Blaze.getData(ui.draggable.get(0)).userId;
- addToSet = { members: memberId };
+ card.assignMember(memberId);
} else {
const labelId = Blaze.getData(ui.draggable.get(0))._id;
- addToSet = { labelIds: labelId };
+ card.addLabel(labelId);
}
- Cards.update(cardId, { $addToSet: addToSet });
},
});
});
diff --git a/client/components/lists/listBody.jade b/client/components/lists/listBody.jade
index b0a374ea..e659b179 100644
--- a/client/components/lists/listBody.jade
+++ b/client/components/lists/listBody.jade
@@ -22,9 +22,20 @@ template(name="listBody")
template(name="addCardForm")
.minicard.minicard-composer.js-composer
- .minicard-detailss.clearfix
- textarea.minicard-composer-textarea.js-card-title(autofocus)
+ if getLabels
+ .minicard-labels
+ each getLabels
+ .minicard-label(class="card-label-{{color}}" title="{{name}}")
+ textarea.minicard-composer-textarea.js-card-title(autofocus)
+ if members.get
.minicard-members.js-minicard-composer-members
+ each members.get
+ +userAvatar(userId=this)
+
.add-controls.clearfix
button.primary.confirm(type="submit") {{_ 'add'}}
a.fa.fa-times-thin.js-close-inlined-form
+
+template(name="autocompleteLabelLine")
+ .minicard-label(class="card-label-{{colorName}}" title=labelName)
+ span(class="{{#if hasNoName}}quiet{{/if}}")= labelName
diff --git a/client/components/lists/listBody.js b/client/components/lists/listBody.js
index 2e00cb4f..36b60d06 100644
--- a/client/components/lists/listBody.js
+++ b/client/components/lists/listBody.js
@@ -11,8 +11,8 @@ BlazeComponent.extendComponent({
options = options || {};
options.position = options.position || 'top';
- const forms = this.componentChildren('inlinedForm');
- let form = _.find(forms, (component) => {
+ const forms = this.childComponents('inlinedForm');
+ let form = forms.find((component) => {
return component.data().position === options.position;
});
if (!form && forms.length > 0) {
@@ -26,8 +26,10 @@ BlazeComponent.extendComponent({
const firstCardDom = this.find('.js-minicard:first');
const lastCardDom = this.find('.js-minicard:last');
const textarea = $(evt.currentTarget).find('textarea');
- const title = textarea.val();
- const position = Blaze.getData(evt.currentTarget).position;
+ const position = this.currentData().position;
+ const title = textarea.val().trim();
+
+ const formComponent = this.childComponents('addCardForm')[0];
let sortIndex;
if (position === 'top') {
sortIndex = Utils.calculateIndex(null, firstCardDom).base;
@@ -35,9 +37,14 @@ BlazeComponent.extendComponent({
sortIndex = Utils.calculateIndex(lastCardDom, null).base;
}
- if ($.trim(title)) {
+ const members = formComponent.members.get();
+ const labelIds = formComponent.labels.get();
+
+ if (title) {
const _id = Cards.insert({
title,
+ members,
+ labelIds,
listId: this.data()._id,
boardId: this.data().board()._id,
sort: sortIndex,
@@ -53,6 +60,8 @@ BlazeComponent.extendComponent({
if (position === 'bottom') {
this.scrollToBottom();
}
+
+ formComponent.reset();
}
},
@@ -100,11 +109,39 @@ BlazeComponent.extendComponent({
},
}).register('listBody');
+function toggleValueInReactiveArray(reactiveValue, value) {
+ const array = reactiveValue.get();
+ const valueIndex = array.indexOf(value);
+ if (valueIndex === -1) {
+ array.push(value);
+ } else {
+ array.splice(valueIndex, 1);
+ }
+ reactiveValue.set(array);
+}
+
BlazeComponent.extendComponent({
template() {
return 'addCardForm';
},
+ onCreated() {
+ this.labels = new ReactiveVar([]);
+ this.members = new ReactiveVar([]);
+ },
+
+ reset() {
+ this.labels.set([]);
+ this.members.set([]);
+ },
+
+ getLabels() {
+ const currentBoardId = Session.get('currentBoard');
+ return Boards.findOne(currentBoardId).labels.filter((label) => {
+ return this.labels.get().indexOf(label._id) > -1;
+ });
+ },
+
pressKey(evt) {
// Pressing Enter should submit the card
if (evt.keyCode === 13) {
@@ -140,4 +177,66 @@ BlazeComponent.extendComponent({
keydown: this.pressKey,
}];
},
+
+ onRendered() {
+ const editor = this;
+ this.$('textarea').escapeableTextComplete([
+ // User mentions
+ {
+ match: /\B@(\w*)$/,
+ search(term, callback) {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ callback($.map(currentBoard.members, (member) => {
+ const user = Users.findOne(member.userId);
+ return user.username.indexOf(term) === 0 ? user : null;
+ }));
+ },
+ template(user) {
+ return user.username;
+ },
+ replace(user) {
+ toggleValueInReactiveArray(editor.members, user._id);
+ return '';
+ },
+ index: 1,
+ },
+
+ // Labels
+ {
+ match: /\B#(\w*)$/,
+ search(term, callback) {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ callback($.map(currentBoard.labels, (label) => {
+ if (label.name.indexOf(term) > -1 ||
+ label.color.indexOf(term) > -1) {
+ return label;
+ }
+ }));
+ },
+ template(label) {
+ return Blaze.toHTMLWithData(Template.autocompleteLabelLine, {
+ hasNoName: !Boolean(label.name),
+ colorName: label.color,
+ labelName: label.name || label.color,
+ });
+ },
+ replace(label) {
+ toggleValueInReactiveArray(editor.labels, label._id);
+ return '';
+ },
+ index: 1,
+ },
+ ], {
+ // When the autocomplete menu is shown we want both a press of both `Tab`
+ // or `Enter` to validation the auto-completion. We also need to stop the
+ // event propagation to prevent the card from submitting (on `Enter`) or
+ // going on the next column (on `Tab`).
+ onKeydown(evt, commands) {
+ if (evt.keyCode === 9 || evt.keyCode === 13) {
+ evt.stopPropagation();
+ return commands.KEY_ENTER;
+ }
+ },
+ });
+ },
}).register('addCardForm');
diff --git a/client/components/lists/listHeader.jade b/client/components/lists/listHeader.jade
index 7d01f1ba..72cd0fe9 100644
--- a/client/components/lists/listHeader.jade
+++ b/client/components/lists/listHeader.jade
@@ -25,6 +25,7 @@ template(name="listActionPopup")
li: a.js-archive-cards {{_ 'list-archive-cards'}}
hr
ul.pop-over-list
+ li: a.js-import-card {{_ 'import-card'}}
li: a.js-close-list {{_ 'archive-list'}}
template(name="listMoveCardsPopup")
diff --git a/client/components/lists/listHeader.js b/client/components/lists/listHeader.js
index 9431b461..d660508a 100644
--- a/client/components/lists/listHeader.js
+++ b/client/components/lists/listHeader.js
@@ -5,14 +5,10 @@ BlazeComponent.extendComponent({
editTitle(evt) {
evt.preventDefault();
- const form = this.componentChildren('inlinedForm')[0];
- const newTitle = form.getValue();
- if ($.trim(newTitle)) {
- Lists.update(this.currentData()._id, {
- $set: {
- title: newTitle,
- },
- });
+ const newTitle = this.childComponents('inlinedForm')[0].getValue().trim();
+ const list = this.currentData();
+ if (newTitle) {
+ list.rename(newTitle.trim());
}
},
@@ -33,45 +29,32 @@ Template.listActionPopup.events({
},
'click .js-list-subscribe'() {},
'click .js-select-cards'() {
- const cardIds = Cards.find(
- {listId: this._id},
- {fields: { _id: 1 }}
- ).map((card) => card._id);
+ const cardIds = this.allCards().map((card) => card._id);
MultiSelection.add(cardIds);
Popup.close();
},
+ 'click .js-import-card': Popup.open('listImportCard'),
'click .js-move-cards': Popup.open('listMoveCards'),
- 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', () => {
- Cards.find({listId: this._id}).forEach((card) => {
- Cards.update(card._id, {
- $set: {
- archived: true,
- },
- });
+ 'click .js-archive-cards': Popup.afterConfirm('listArchiveCards', function() {
+ this.allCards().forEach((card) => {
+ card.archive();
});
Popup.close();
}),
+
'click .js-close-list'(evt) {
evt.preventDefault();
- Lists.update(this._id, {
- $set: {
- archived: true,
- },
- });
+ this.archive();
Popup.close();
},
});
Template.listMoveCardsPopup.events({
'click .js-select-list'() {
- const fromList = Template.parentData(2).data._id;
+ const fromList = Template.parentData(2).data;
const toList = this._id;
- Cards.find({ listId: fromList }).forEach((card) => {
- Cards.update(card._id, {
- $set: {
- listId: toList,
- },
- });
+ fromList.allCards().forEach((card) => {
+ card.move(toList);
});
Popup.close();
},
diff --git a/client/components/main/editor.js b/client/components/main/editor.js
index 1d88fe74..82fce641 100644
--- a/client/components/main/editor.js
+++ b/client/components/main/editor.js
@@ -1,17 +1,15 @@
-let dropdownMenuIsOpened = false;
-
Template.editor.onRendered(() => {
const $textarea = this.$('textarea');
autosize($textarea);
- $textarea.textcomplete([
+ $textarea.escapeableTextComplete([
// Emojies
{
match: /\B:([\-+\w]*)$/,
search(term, callback) {
- callback($.map(Emoji.values, (emoji) => {
- return emoji.indexOf(term) === 0 ? emoji : null;
+ callback(Emoji.values.map((emoji) => {
+ return emoji.includes(term) ? emoji : null;
}));
},
template(value) {
@@ -30,9 +28,9 @@ Template.editor.onRendered(() => {
match: /\B@(\w*)$/,
search(term, callback) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- callback($.map(currentBoard.members, (member) => {
+ callback(currentBoard.members.map((member) => {
const username = Users.findOne(member.userId).username;
- return username.indexOf(term) === 0 ? username : null;
+ return username.includes(term) ? username : null;
}));
},
template(value) {
@@ -44,30 +42,8 @@ Template.editor.onRendered(() => {
index: 1,
},
]);
-
- // Since commit d474017 jquery-textComplete automatically closes a potential
- // opened dropdown menu when the user press Escape. This behavior conflicts
- // with our EscapeActions system, but it's too complicated and hacky to
- // monkey-pach textComplete to disable it -- I tried. Instead we listen to
- // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown
- // is opened (and rely on textComplete to execute the actual action).
- $textarea.on({
- 'textComplete:show'() {
- dropdownMenuIsOpened = true;
- },
- 'textComplete:hide'() {
- Tracker.afterFlush(() => {
- dropdownMenuIsOpened = false;
- });
- },
- });
});
-EscapeActions.register('textcomplete',
- () => {},
- () => dropdownMenuIsOpened
-);
-
// XXX I believe we should compute a HTML rendered field on the server that
// would handle markdown, emojies and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
@@ -78,7 +54,7 @@ const at = HTML.CharRef({html: '&commat;', str: '@'});
Blaze.Template.registerHelper('mentions', new Template('mentions', function() {
const view = this;
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const knowedUsers = _.map(currentBoard.members, (member) => {
+ const knowedUsers = currentBoard.members.map((member) => {
member.username = Users.findOne(member.userId).username;
return member;
});
diff --git a/client/components/main/header.jade b/client/components/main/header.jade
index 4715bfc8..86dfd6a7 100644
--- a/client/components/main/header.jade
+++ b/client/components/main/header.jade
@@ -43,10 +43,10 @@ template(name="header")
the list of all boards.
if isSandstorm
.wekan-logo
- img(src="/wekan-logo-header.png" alt="Wekan")
+ img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
else
a.wekan-logo(href="{{pathFor 'home'}}" title="{{_ 'header-logo-title'}}")
- img(src="/wekan-logo-header.png" alt="Wekan")
+ img(src="{{pathFor '/wekan-logo-header.png'}}" alt="Wekan")
template(name="headerTitle")
h1 {{_ 'my-boards'}}
diff --git a/client/components/main/keyboardShortcuts.styl b/client/components/main/keyboardShortcuts.styl
index 42e0637b..f77d387f 100644
--- a/client/components/main/keyboardShortcuts.styl
+++ b/client/components/main/keyboardShortcuts.styl
@@ -14,11 +14,6 @@
padding: 5px 8px
margin: 5px
font-size: 18px
- font-weight: bold
- background: white
- border-radius: 3px
- border: 1px solid darken(white, 10%)
- box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15)
.shortcuts-list-item-action
font-size: 1.4em
diff --git a/client/components/main/layouts.jade b/client/components/main/layouts.jade
index f5a8db59..65b53f04 100644
--- a/client/components/main/layouts.jade
+++ b/client/components/main/layouts.jade
@@ -2,13 +2,24 @@ head
title Wekan
meta(name="viewport"
content="maximum-scale=1.0,width=device-width,initial-scale=1.0,user-scalable=0")
+ //- XXX We should use pathFor in the following `href` to support the case
+ where the application is deployed with a path prefix, but it seems to be
+ difficult to do that cleanly with Blaze -- at least without adding extra
+ packages.
link(rel="shortcut icon" href="/wekan-favicon.png")
template(name="userFormsLayout")
section.auth-layout
h1.at-form-landing-logo
- img(src="/wekan-logo.png" alt="Wekan")
+ img(src="{{pathFor '/wekan-logo.png'}}" alt="Wekan")
+Template.dynamic(template=content)
+ div.at-form-lang
+ select.select-lang.js-userform-set-language
+ each languages
+ if isCurrentLanguage
+ option(value="{{tag}}" selected="selected") {{name}}
+ else
+ option(value="{{tag}}") {{name}}
template(name="defaultLayout")
+header
diff --git a/client/components/main/layouts.js b/client/components/main/layouts.js
index ab62e76a..3df17f41 100644
--- a/client/components/main/layouts.js
+++ b/client/components/main/layouts.js
@@ -2,10 +2,43 @@ Meteor.subscribe('boards');
BlazeLayout.setRoot('body');
+const i18nTagToT9n = (i18nTag) => {
+ // t9n/i18n tags are same now, see: https://github.com/softwarerero/meteor-accounts-t9n/pull/129
+ // but we keep this conversion function here, to be aware that that they are different system.
+ return i18nTag;
+};
+
Template.userFormsLayout.onRendered(() => {
+ const i18nTag = navigator.language;
+ if (i18nTag) {
+ T9n.setLanguage(i18nTagToT9n(i18nTag));
+ }
EscapeActions.executeAll();
});
+Template.userFormsLayout.helpers({
+ languages() {
+ return _.map(TAPi18n.getLanguages(), (lang, tag) => {
+ const name = lang.name;
+ return { tag, name };
+ });
+ },
+
+ isCurrentLanguage() {
+ const t9nTag = i18nTagToT9n(this.tag);
+ const curLang = T9n.getLanguage() || 'en';
+ return t9nTag === curLang;
+ },
+});
+
+Template.userFormsLayout.events({
+ 'change .js-userform-set-language'(evt) {
+ const i18nTag = $(evt.currentTarget).val();
+ T9n.setLanguage(i18nTagToT9n(i18nTag));
+ evt.preventDefault();
+ },
+});
+
Template.defaultLayout.events({
'click .js-close-modal': () => {
Modal.close();
diff --git a/client/components/main/layouts.styl b/client/components/main/layouts.styl
index 1dbefc20..fcc94251 100644
--- a/client/components/main/layouts.styl
+++ b/client/components/main/layouts.styl
@@ -172,6 +172,15 @@ dl, dt
dd
margin: 0 0 16px 24px
+kbd
+ padding: 1px 3px
+ margin: 3px
+ font-weight: bold
+ background: darken(white, 2%)
+ border-radius: 3px
+ border: 1px solid darken(white, 10%)
+ box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.15)
+
.clear
clear: both
diff --git a/client/components/main/popup.styl b/client/components/main/popup.styl
index 3bef4f7d..8a685069 100644
--- a/client/components/main/popup.styl
+++ b/client/components/main/popup.styl
@@ -17,9 +17,11 @@ $popupWidth = 300px
margin: 4px -10px
width: $popupWidth
+ p,
+ textarea,
input[type="text"],
input[type="email"],
- input[type="password"]
+ input[type="password"],
input[type="file"]
margin: 4px 0 12px
width: 100%
@@ -30,8 +32,6 @@ $popupWidth = 300px
textarea
height: 72px
- margin: 4px 0 12px
- width: 100%
.header
height: 36px
diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade
index 7f7519c6..3a5c7fdb 100644
--- a/client/components/sidebar/sidebar.jade
+++ b/client/components/sidebar/sidebar.jade
@@ -33,6 +33,13 @@ template(name="membersWidget")
a.member.add-member.js-manage-board-members
i.fa.fa-plus
.clearfix
+ if isInvited
+ hr
+ p
+ i.fa.fa-exclamation-circle
+ | {{_ 'just-invited'}}
+ button.js-member-invite-accept.primary {{_ 'accept'}}
+ button.js-member-invite-decline {{_ 'decline'}}
template(name="labelsWidget")
.board-widget.board-widget-labels
@@ -56,51 +63,58 @@ template(name="memberPopup")
h3
.js-profile= user.profile.fullname
p.quiet @#{user.username}
+ if isInvited
+ p
+ i.fa.fa-exclamation-circle
+ | {{_ 'not-accepted-yet'}}
- if currentUser.isBoardMember
- ul.pop-over-list
- li
- a.js-filter-member Filter cards
+ ul.pop-over-list
+ li
+ a.js-filter-member {{_ 'filter-cards'}}
+ if currentUser.isBoardAdmin
unless isSandstorm
- if currentUser.isBoardAdmin
- li
- a.js-change-role
- | {{_ 'change-permissions'}}
- span.quiet (#{memberType})
- li
- if $eq currentUser._id userId
- //-
- XXX Not implemented!
- // a.js-leave-member {{_ 'leave-board'}}
- else
- a.js-remove-member {{_ 'remove-from-board'}}
+ li
+ a.js-change-role
+ | {{_ 'change-permissions'}}
+ span.quiet (#{memberType})
+ li
+ if $eq currentUser._id userId
+ a.js-leave-member {{_ 'leave-board'}}
+ else
+ a.js-remove-member {{_ 'remove-from-board'}}
template(name="removeMemberPopup")
- p {{_ 'remove-member-pop' name=user.profile.name username=user.username boardTitle=board.title}}
+ p {{_ 'remove-member-pop' name=user.profile.fullname username=user.username boardTitle=board.title}}
button.js-confirm.negate.full(type="submit") {{_ 'remove-member'}}
template(name="addMemberPopup")
.js-search-member
+esInput(index="users")
- ul.pop-over-list
- +esEach(index="users")
- li.item.js-member-item(class="{{#if isBoardMember}}disabled{{/if}}")
- a.name.js-select-member(title="{{profile.name}} ({{username}})")
- +userAvatar(userId=_id esSearch=true)
- span.full-name
- = profile.name
- | (<span class="username">{{username}}</span>)
- if isBoardMember
- .quiet ({{_ 'joined'}})
+ if loading.get
+ +spinner
+ else if error.get
+ .warning {{_ error.get}}
+ else
+ ul.pop-over-list
+ +esEach(index="users")
+ li.item.js-member-item(class="{{#if isBoardMember}}disabled{{/if}}")
+ a.name.js-select-member(title="{{profile.name}} ({{username}})")
+ +userAvatar(userId=_id esSearch=true)
+ span.full-name
+ = profile.fullname
+ | (<span class="username">{{username}}</span>)
+ if isBoardMember
+ .quiet ({{_ 'joined'}})
- +ifEsIsSearching(index='users')
- +spinner
+ +ifEsIsSearching(index='users')
+ +spinner
- +ifEsHasNoResults(index="users")
- .manage-member-section
- p.quiet {{_ 'no-results'}}
+ +ifEsHasNoResults(index="users")
+ .manage-member-section
+ p.quiet {{_ 'no-results'}}
+ button.js-email-invite.primary.full {{_ 'email-invite'}}
template(name="changePermissionsPopup")
ul.pop-over-list
diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js
index eff0ef52..5b58dbd9 100644
--- a/client/components/sidebar/sidebar.js
+++ b/client/components/sidebar/sidebar.js
@@ -54,7 +54,7 @@ BlazeComponent.extendComponent({
},
reachNextPeak() {
- const activitiesComponent = this.componentChildren('activities')[0];
+ const activitiesComponent = this.childComponents('activities')[0];
activitiesComponent.loadNextPage();
},
@@ -95,10 +95,10 @@ BlazeComponent.extendComponent({
events() {
// XXX Hacky, we need some kind of `super`
const mixinEvents = this.getMixin(Mixins.InfiniteScrolling).events();
- return mixinEvents.concat([{
+ return [...mixinEvents, {
'click .js-toggle-sidebar': this.toggle,
'click .js-back-home': this.setView,
- }]);
+ }];
},
}).register('sidebar');
@@ -109,14 +109,6 @@ EscapeActions.register('sidebarView',
() => { return Sidebar && Sidebar.getView() !== defaultView; }
);
-function getMemberIndex(board, searchId) {
- for (let i = 0; i < board.members.length; i++) {
- if (board.members[i].userId === searchId)
- return i;
- }
- throw new Meteor.Error('Member not found');
-}
-
Template.memberPopup.helpers({
user() {
return Users.findOne(this.userId);
@@ -125,6 +117,9 @@ Template.memberPopup.helpers({
const type = Users.findOne(this.userId).isBoardAdmin() ? 'admin' : 'normal';
return TAPi18n.__(type).toLowerCase();
},
+ isInvited() {
+ return Users.findOne(this.userId).isInvitedTo(Session.get('currentBoard'));
+ },
});
Template.memberPopup.events({
@@ -135,24 +130,53 @@ Template.memberPopup.events({
'click .js-change-role': Popup.open('changePermissions'),
'click .js-remove-member': Popup.afterConfirm('removeMember', function() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const memberIndex = getMemberIndex(currentBoard, this.userId);
-
- Boards.update(currentBoard._id, {
- $set: {
- [`members.${memberIndex}.isActive`]: false,
- },
- });
+ const memberId = this.userId;
+ currentBoard.removeMember(memberId);
Popup.close();
}),
'click .js-leave-member'() {
- // XXX Not implemented
- Popup.close();
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ Meteor.call('quitBoard', currentBoard, (err, ret) => {
+ if (!ret && ret) {
+ Popup.close();
+ FlowRouter.go('home');
+ }
+ });
+ },
+});
+
+Template.removeMemberPopup.helpers({
+ user() {
+ return Users.findOne(this.userId);
+ },
+ board() {
+ return Boards.findOne(Session.get('currentBoard'));
+ },
+});
+
+Template.membersWidget.helpers({
+ isInvited() {
+ const user = Meteor.user();
+ return user && user.isInvitedTo(Session.get('currentBoard'));
},
});
Template.membersWidget.events({
'click .js-member': Popup.open('member'),
'click .js-manage-board-members': Popup.open('addMember'),
+ 'click .js-member-invite-accept'() {
+ const boardId = Session.get('currentBoard');
+ Meteor.user().removeInvite(boardId);
+ },
+ 'click .js-member-invite-decline'() {
+ const boardId = Session.get('currentBoard');
+ Meteor.call('quitBoard', boardId, (err, ret) => {
+ if (!err && ret) {
+ Meteor.user().removeInvite(boardId);
+ FlowRouter.go('home');
+ }
+ });
+ },
});
Template.labelsWidget.events({
@@ -198,56 +222,83 @@ function draggableMembersLabelsWidgets() {
Template.membersWidget.onRendered(draggableMembersLabelsWidgets);
Template.labelsWidget.onRendered(draggableMembersLabelsWidgets);
-Template.addMemberPopup.helpers({
+BlazeComponent.extendComponent({
+ template() {
+ return 'addMemberPopup';
+ },
+
+ onCreated() {
+ this.error = new ReactiveVar('');
+ this.loading = new ReactiveVar(false);
+ },
+
+ onRendered() {
+ this.find('.js-search-member input').focus();
+ this.setLoading(false);
+ },
+
isBoardMember() {
- const user = Users.findOne(this._id);
+ const userId = this.currentData()._id;
+ const user = Users.findOne(userId);
return user && user.isBoardMember();
},
-});
-Template.addMemberPopup.events({
- 'click .js-select-member'() {
- const userId = this._id;
- const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const currentMembersIds = _.pluck(currentBoard.members, 'userId');
- if (currentMembersIds.indexOf(userId) === -1) {
- Boards.update(currentBoard._id, {
- $push: {
- members: {
- userId,
- isAdmin: false,
- isActive: true,
- },
- },
- });
- } else {
- const memberIndex = getMemberIndex(currentBoard, userId);
+ isValidEmail(email) {
+ return SimpleSchema.RegEx.Email.test(email);
+ },
- Boards.update(currentBoard._id, {
- $set: {
- [`members.${memberIndex}.isActive`]: true,
- },
- });
- }
- Popup.close();
+ setError(error) {
+ this.error.set(error);
},
-});
-Template.addMemberPopup.onRendered(function() {
- this.find('.js-search-member input').focus();
-});
+ setLoading(w) {
+ this.loading.set(w);
+ },
+
+ isLoading() {
+ return this.loading.get();
+ },
+
+ inviteUser(idNameEmail) {
+ const boardId = Session.get('currentBoard');
+ this.setLoading(true);
+ const self = this;
+ Meteor.call('inviteUserToBoard', idNameEmail, boardId, (err, ret) => {
+ self.setLoading(false);
+ if (err) self.setError(err.error);
+ else if (ret.email) self.setError('email-sent');
+ else Popup.close();
+ });
+ },
+
+ events() {
+ return [{
+ 'keyup input'() {
+ this.setError('');
+ },
+ 'click .js-select-member'() {
+ const userId = this.currentData()._id;
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ if (currentBoard.memberIndex(userId)<0) {
+ this.inviteUser(userId);
+ }
+ },
+ 'click .js-email-invite'() {
+ const idNameEmail = $('.js-search-member input').val();
+ if (idNameEmail.indexOf('@')<0 || this.isValidEmail(idNameEmail)) {
+ this.inviteUser(idNameEmail);
+ } else this.setError('email-invalid');
+ },
+ }];
+ },
+}).register('addMemberPopup');
Template.changePermissionsPopup.events({
'click .js-set-admin, click .js-set-normal'(event) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
- const memberIndex = getMemberIndex(currentBoard, this.userId);
+ const memberId = this.userId;
const isAdmin = $(event.currentTarget).hasClass('js-set-admin');
-
- Boards.update(currentBoard._id, {
- $set: {
- [`members.${memberIndex}.isAdmin`]: isAdmin,
- },
- });
+ currentBoard.setMemberPermission(memberId, isAdmin);
Popup.back(1);
},
});
diff --git a/client/components/sidebar/sidebarArchives.js b/client/components/sidebar/sidebarArchives.js
index f2597c3c..18970267 100644
--- a/client/components/sidebar/sidebarArchives.js
+++ b/client/components/sidebar/sidebarArchives.js
@@ -11,11 +11,17 @@ BlazeComponent.extendComponent({
},
archivedCards() {
- return Cards.find({ archived: true });
+ return Cards.find({
+ archived: true,
+ boardId: Session.get('currentBoard'),
+ });
},
archivedLists() {
- return Lists.find({ archived: true });
+ return Lists.find({
+ archived: true,
+ boardId: Session.get('currentBoard'),
+ });
},
cardIsInArchivedList() {
@@ -29,8 +35,8 @@ BlazeComponent.extendComponent({
events() {
return [{
'click .js-restore-card'() {
- const cardId = this.currentData()._id;
- Cards.update(cardId, {$set: {archived: false}});
+ const card = this.currentData();
+ card.restore();
},
'click .js-delete-card': Popup.afterConfirm('cardDelete', function() {
const cardId = this._id;
@@ -38,8 +44,8 @@ BlazeComponent.extendComponent({
Popup.close();
}),
'click .js-restore-list'() {
- const listId = this.currentData()._id;
- Lists.update(listId, {$set: {archived: false}});
+ const list = this.currentData();
+ list.restore();
},
}];
},
diff --git a/client/components/sidebar/sidebarFilters.jade b/client/components/sidebar/sidebarFilters.jade
index c894bc8b..ef26ef76 100644
--- a/client/components/sidebar/sidebarFilters.jade
+++ b/client/components/sidebar/sidebarFilters.jade
@@ -13,7 +13,7 @@ template(name="filterSidebar")
if name
= name
else
- span.quiet {{_ "label-default" color}}
+ span.quiet {{_ "label-default" (_ (concat "color-" color))}}
if Filter.labelIds.isSelected _id
i.fa.fa-check
hr
@@ -75,8 +75,8 @@ template(name="multiselectionSidebar")
template(name="disambiguateMultiLabelPopup")
p {{_ 'what-to-do'}}
- button.wide.js-remove-label Remove {{_ 'remove-label'}}
- button.wide.js-add-label Add {{_ 'add-label'}}
+ button.wide.js-remove-label {{_ 'remove-label'}}
+ button.wide.js-add-label {{_ 'add-label'}}
template(name="disambiguateMultiMemberPopup")
p {{_ 'what-to-do'}}
diff --git a/client/components/sidebar/sidebarFilters.js b/client/components/sidebar/sidebarFilters.js
index 335cc7d6..bdecd63e 100644
--- a/client/components/sidebar/sidebarFilters.js
+++ b/client/components/sidebar/sidebarFilters.js
@@ -30,9 +30,9 @@ BlazeComponent.extendComponent({
},
}).register('filterSidebar');
-function updateSelectedCards(query) {
+function mutateSelectedCards(mutationName, ...args) {
Cards.find(MultiSelection.getMongoSelector()).forEach((card) => {
- Cards.update(card._id, query);
+ card[mutationName](...args);
});
}
@@ -67,47 +67,34 @@ BlazeComponent.extendComponent({
'click .js-toggle-label-multiselection'(evt) {
const labelId = this.currentData()._id;
const mappedSelection = this.mapSelection('label', labelId);
- let operation;
- if (_.every(mappedSelection))
- operation = '$pull';
- else if (_.every(mappedSelection, (bool) => !bool))
- operation = '$addToSet';
- else {
+
+ if (_.every(mappedSelection)) {
+ mutateSelectedCards('removeLabel', labelId);
+ } else if (_.every(mappedSelection, (bool) => !bool)) {
+ mutateSelectedCards('addLabel', labelId);
+ } else {
const popup = Popup.open('disambiguateMultiLabel');
// XXX We need to have a better integration between the popup and the
// UI components systems.
return popup.call(this.currentData(), evt);
}
-
- updateSelectedCards({
- [operation]: {
- labelIds: labelId,
- },
- });
},
'click .js-toggle-member-multiselection'(evt) {
const memberId = this.currentData()._id;
const mappedSelection = this.mapSelection('member', memberId);
- let operation;
- if (_.every(mappedSelection))
- operation = '$pull';
- else if (_.every(mappedSelection, (bool) => !bool))
- operation = '$addToSet';
- else {
+ if (_.every(mappedSelection)) {
+ mutateSelectedCards('unassignMember', memberId);
+ } else if (_.every(mappedSelection, (bool) => !bool)) {
+ mutateSelectedCards('assignMember', memberId);
+ } else {
const popup = Popup.open('disambiguateMultiMember');
// XXX We need to have a better integration between the popup and the
// UI components systems.
return popup.call(this.currentData(), evt);
}
-
- updateSelectedCards({
- [operation]: {
- members: memberId,
- },
- });
},
'click .js-archive-selection'() {
- updateSelectedCards({$set: {archived: true}});
+ mutateSelectedCards('archive');
},
}];
},
@@ -115,22 +102,22 @@ BlazeComponent.extendComponent({
Template.disambiguateMultiLabelPopup.events({
'click .js-remove-label'() {
- updateSelectedCards({$pull: {labelIds: this._id}});
+ mutateSelectedCards('removeLabel', this._id);
Popup.close();
},
'click .js-add-label'() {
- updateSelectedCards({$addToSet: {labelIds: this._id}});
+ mutateSelectedCards('addLabel', this._id);
Popup.close();
},
});
Template.disambiguateMultiMemberPopup.events({
'click .js-unassign-member'() {
- updateSelectedCards({$pull: {members: this._id}});
+ mutateSelectedCards('assignMember', this._id);
Popup.close();
},
'click .js-assign-member'() {
- updateSelectedCards({$addToSet: {members: this._id}});
+ mutateSelectedCards('unassignMember', this._id);
Popup.close();
},
});
diff --git a/client/components/users/userAvatar.jade b/client/components/users/userAvatar.jade
index e08666e5..44e899a7 100644
--- a/client/components/users/userAvatar.jade
+++ b/client/components/users/userAvatar.jade
@@ -1,7 +1,7 @@
template(name="userAvatar")
a.member.js-member(title="{{userData.profile.fullname}} ({{userData.username}})")
- if userData.profile.avatarUrl
- img.avatar.avatar-image(src=userData.profile.avatarUrl)
+ if userData.getAvatarUrl
+ img.avatar.avatar-image(src=userData.getAvatarUrl)
else
+userAvatarInitials(userId=userData._id)
diff --git a/client/components/users/userAvatar.js b/client/components/users/userAvatar.js
index 04add0a6..1e531882 100644
--- a/client/components/users/userAvatar.js
+++ b/client/components/users/userAvatar.js
@@ -22,8 +22,11 @@ Template.userAvatar.helpers({
},
presenceStatusClassName() {
+ const user = Users.findOne(this.userId);
const userPresence = presences.findOne({ userId: this.userId });
- if (!userPresence)
+ if (user && user.isInvitedTo(Session.get('currentBoard')))
+ return 'pending';
+ else if (!userPresence)
return 'disconnected';
else if (Session.equals('currentBoard', userPresence.state.currentBoardId))
return 'active';
@@ -82,11 +85,7 @@ BlazeComponent.extendComponent({
},
setAvatar(avatarUrl) {
- Meteor.users.update(Meteor.userId(), {
- $set: {
- 'profile.avatarUrl': avatarUrl,
- },
- });
+ Meteor.user().setAvatarUrl(avatarUrl);
},
setError(error) {
@@ -151,19 +150,9 @@ Template.cardMembersPopup.helpers({
Template.cardMembersPopup.events({
'click .js-select-member'(evt) {
- const cardId = Template.parentData(2).data._id;
+ const card = Cards.findOne(Session.get('currentCard'));
const memberId = this.userId;
- let operation;
- if (Cards.find({ _id: cardId, members: memberId}).count() === 0)
- operation = '$addToSet';
- else
- operation = '$pull';
-
- Cards.update(cardId, {
- [operation]: {
- members: memberId,
- },
- });
+ card.toggleMember(memberId);
evt.preventDefault();
},
});
@@ -176,7 +165,7 @@ Template.cardMemberPopup.helpers({
Template.cardMemberPopup.events({
'click .js-remove-member'() {
- Cards.update(this.cardId, {$pull: {members: this.userId}});
+ Cards.findOne(this.cardId).unassignMember(this.userId);
Popup.close();
},
'click .js-edit-profile': Popup.open('editProfile'),
diff --git a/client/components/users/userAvatar.styl b/client/components/users/userAvatar.styl
index 83257792..b962b01c 100644
--- a/client/components/users/userAvatar.styl
+++ b/client/components/users/userAvatar.styl
@@ -56,6 +56,10 @@ avatar-radius = 50%
background: #bdbdbd
border-color: #ededed
+ &.pending
+ background: #e44242
+ border-color: #f1dada
+
.edit-avatar
position: absolute
top: 0
diff --git a/client/components/users/userForm.styl b/client/components/users/userForm.styl
index 9b6e86ce..dbe62b4e 100644
--- a/client/components/users/userForm.styl
+++ b/client/components/users/userForm.styl
@@ -45,3 +45,13 @@
.at-signUp,
.at-signIn
font-weight: bold
+
+ .at-form-lang
+ margin: auto
+ width: 275px
+ padding: 25px
+ padding-bottom: 10px
+
+ .select-lang
+ width: 275px
+ font-size: 1.0em
diff --git a/client/components/users/userHeader.js b/client/components/users/userHeader.js
index 0f91fd15..a478da0c 100644
--- a/client/components/users/userHeader.js
+++ b/client/components/users/userHeader.js
@@ -18,9 +18,9 @@ Template.memberMenuPopup.events({
Template.editProfilePopup.events({
submit(evt, tpl) {
evt.preventDefault();
- const fullname = $.trim(tpl.find('.js-profile-fullname').value);
- const username = $.trim(tpl.find('.js-profile-username').value);
- const initials = $.trim(tpl.find('.js-profile-initials').value);
+ const fullname = tpl.find('.js-profile-fullname').value.trim();
+ const username = tpl.find('.js-profile-username').value.trim();
+ const initials = tpl.find('.js-profile-initials').value.trim();
Users.update(Meteor.userId(), {$set: {
'profile.fullname': fullname,
'profile.initials': initials,
diff --git a/client/config/blazeHelpers.js b/client/config/blazeHelpers.js
index 12990ed7..adf5ef6a 100644
--- a/client/config/blazeHelpers.js
+++ b/client/config/blazeHelpers.js
@@ -13,3 +13,7 @@ Blaze.registerHelper('currentCard', () => {
});
Blaze.registerHelper('getUser', (userId) => Users.findOne(userId));
+
+UI.registerHelper('concat', function (...args) {
+ return Array.prototype.slice.call(args, 0, -1).join('');
+});
diff --git a/client/config/router.js b/client/config/router.js
index 1cac43a0..0a6958d0 100644
--- a/client/config/router.js
+++ b/client/config/router.js
@@ -88,3 +88,26 @@ _.each(redirections, (newPath, oldPath) => {
}],
});
});
+
+// As it is not possible to use template helpers in the page <head> we create a
+// reactive function whose role is to set any page-specific tag in the <head>
+// using the `kadira:dochead` package. Currently we only use it to display the
+// board title if we are in a board page (see #364) but we may want to support
+// some <meta> tags in the future.
+const appTitle = 'Wekan';
+
+// XXX The `Meteor.startup` should not be necessary -- we don't need to wait for
+// the complete DOM to be ready to call `DocHead.setTitle`. But the problem is
+// that the global variable `Boards` is undefined when this file loads so we
+// wait a bit until hopefully all files are loaded. This will be fixed in a
+// clean way once Meteor will support ES6 modules -- hopefully in Meteor 1.3.
+Meteor.startup(() => {
+ Tracker.autorun(() => {
+ const currentBoard = Boards.findOne(Session.get('currentBoard'));
+ const titleStack = [appTitle];
+ if (currentBoard) {
+ titleStack.push(currentBoard.title);
+ }
+ DocHead.setTitle(titleStack.reverse().join(' - '));
+ });
+});
diff --git a/client/lib/accessibility.js b/client/lib/accessibility.js
new file mode 100644
index 00000000..52b771d4
--- /dev/null
+++ b/client/lib/accessibility.js
@@ -0,0 +1,41 @@
+// In this file we define a set of DOM transformations that are specifically
+// intended for blind screen readers.
+//
+// See https://github.com/wekan/wekan/issues/337 for the general accessibility
+// considerations.
+
+// Without an href, links are non-keyboard-focusable and are not presented on
+// blind screen readers. We default to the empty anchor `#` href.
+function enforceHref(attributes) {
+ if (!_.has(attributes, 'href')) {
+ attributes.href = '#';
+ }
+ return attributes;
+}
+
+// `title` is inconsistently used on the web, and is thus inconsistently
+// presented by screen readers. `aria-label`, on the other hand, is specific to
+// accessibility and is presented in ways that title shouldn't be.
+function copyTitleInAriaLabel(attributes) {
+ if (!_.has(attributes, 'aria-label') && _.has(attributes, 'title')) {
+ attributes['aria-label'] = attributes.title;
+ }
+ return attributes;
+}
+
+// XXX Our implementation relies on overwriting Blaze virtual DOM functions,
+// which is a little bit hacky -- but still reasonable with our ES6 usage. If we
+// end up switching to React we will probably create lower level small
+// components to handle that without overwriting any build-in function.
+const {
+ A: superA,
+ I: superI,
+} = HTML;
+
+HTML.A = (attributes, ...others) => {
+ return superA(copyTitleInAriaLabel(enforceHref(attributes)), ...others);
+};
+
+HTML.I = (attributes, ...others) => {
+ return superI(copyTitleInAriaLabel(attributes), ...others);
+};
diff --git a/client/lib/dropImage.js b/client/lib/dropImage.js
new file mode 100644
index 00000000..592d5c8f
--- /dev/null
+++ b/client/lib/dropImage.js
@@ -0,0 +1,62 @@
+/* eslint-disable */
+
+// ------------------------------------------------------------------------
+// Created by STRd6
+// MIT License
+// https://github.com/distri/jquery-image_reader/blob/master/drop.coffee.md
+//
+// Raymond re-write it to javascript
+
+(function($) {
+ $.event.fix = (function(originalFix) {
+ return function(event) {
+ event = originalFix.apply(this, arguments);
+ if (event.type.indexOf('drag') === 0 || event.type.indexOf('drop') === 0) {
+ event.dataTransfer = event.originalEvent.dataTransfer;
+ }
+ return event;
+ };
+ })($.event.fix);
+
+ const defaults = {
+ callback: $.noop,
+ matchType: /image.*/,
+ };
+
+ return $.fn.dropImageReader = function(options) {
+ if (typeof options === 'function') {
+ options = {
+ callback: options,
+ };
+ }
+ options = $.extend({}, defaults, options);
+ const stopFn = function(event) {
+ event.stopPropagation();
+ return event.preventDefault();
+ };
+ return this.each(function() {
+ const element = this;
+ $(element).bind('dragenter dragover dragleave', stopFn);
+ return $(element).bind('drop', function(event) {
+ stopFn(event);
+ const files = event.dataTransfer.files;
+ for(let i=0; i<files.length; i++) {
+ const f = files[i];
+ if(f.type.match(options.matchType)) {
+ const reader = new FileReader();
+ reader.onload = function(evt) {
+ return options.callback.call(element, {
+ dataURL: evt.target.result,
+ event: evt,
+ file: f,
+ name: f.name,
+ });
+ };
+ reader.readAsDataURL(f);
+ return;
+ }
+ }
+ });
+ });
+ };
+})(jQuery);
diff --git a/client/lib/filter.js b/client/lib/filter.js
index f7baf480..74305284 100644
--- a/client/lib/filter.js
+++ b/client/lib/filter.js
@@ -95,7 +95,7 @@ Filter = {
return {};
const filterSelector = {};
- _.forEach(this._fields, (fieldName) => {
+ this._fields.forEach((fieldName) => {
const filter = this[fieldName];
if (filter._isActive())
filterSelector[fieldName] = filter._getMongoSelector();
@@ -116,7 +116,7 @@ Filter = {
},
reset() {
- _.forEach(this._fields, (fieldName) => {
+ this._fields.forEach((fieldName) => {
const filter = this[fieldName];
filter.reset();
});
diff --git a/client/lib/keyboard.js b/client/lib/keyboard.js
index af5fb7a2..f8212c9b 100644
--- a/client/lib/keyboard.js
+++ b/client/lib/keyboard.js
@@ -23,6 +23,14 @@ Mousetrap.bind('x', () => {
}
});
+Mousetrap.bind('f', () => {
+ if (Sidebar.isOpen() && Sidebar.getView() === 'filter') {
+ Sidebar.toggle();
+ } else {
+ Sidebar.setView('filter');
+ }
+});
+
Mousetrap.bind(['down', 'up'], (evt, key) => {
if (!Session.get('currentCard')) {
return;
@@ -36,6 +44,26 @@ Mousetrap.bind(['down', 'up'], (evt, key) => {
}
});
+// XXX This shortcut should also work when hovering over a card in board view
+Mousetrap.bind('space', (evt) => {
+ if (!Session.get('currentCard')) {
+ return;
+ }
+
+ const currentUserId = Meteor.userId();
+ if (currentUserId === null) {
+ return;
+ }
+
+ if (Meteor.user().isBoardMember()) {
+ const card = Cards.findOne(Session.get('currentCard'));
+ card.toggleMember(currentUserId);
+ // We should prevent scrolling in card when spacebar is clicked
+ // This should do it according to Mousetrap docs, but it doesn't
+ evt.preventDefault();
+ }
+});
+
Template.keyboardShortcuts.helpers({
mapping: [{
keys: ['W'],
@@ -44,6 +72,9 @@ Template.keyboardShortcuts.helpers({
keys: ['Q'],
action: 'shortcut-filter-my-cards',
}, {
+ keys: ['F'],
+ action: 'shortcut-toggle-filterbar',
+ }, {
keys: ['X'],
action: 'shortcut-clear-filters',
}, {
@@ -58,5 +89,8 @@ Template.keyboardShortcuts.helpers({
}, {
keys: [':'],
action: 'shortcut-autocomplete-emojies',
+ }, {
+ keys: ['SPACE'],
+ action: 'shortcut-assign-self',
}],
});
diff --git a/client/lib/modal.js b/client/lib/modal.js
index 5b3392b2..e6301cb5 100644
--- a/client/lib/modal.js
+++ b/client/lib/modal.js
@@ -21,9 +21,9 @@ window.Modal = new class {
}
}
- open(modalName, options) {
+ open(modalName, { onCloseGoTo = ''} = {}) {
this._currentModal.set(modalName);
- this._onCloseGoTo = options && options.onCloseGoTo || '';
+ this._onCloseGoTo = onCloseGoTo;
}
};
diff --git a/client/lib/multiSelection.js b/client/lib/multiSelection.js
index c2bb2bbc..eeb2015d 100644
--- a/client/lib/multiSelection.js
+++ b/client/lib/multiSelection.js
@@ -119,12 +119,13 @@ MultiSelection = {
}
},
- toggle(cardIds, options) {
+ toggle(cardIds, options = {}) {
cardIds = _.isString(cardIds) ? [cardIds] : cardIds;
- options = _.extend({
+ options = {
add: true,
remove: true,
- }, options || {});
+ ...options,
+ };
if (!this.isActive()) {
this.reset();
@@ -133,7 +134,7 @@ MultiSelection = {
const selectedCards = this._selectedCards.get();
- _.each(cardIds, (cardId) => {
+ cardIds.forEach((cardId) => {
const indexOfCard = selectedCards.indexOf(cardId);
if (options.remove && indexOfCard > -1)
diff --git a/client/lib/pasteImage.js b/client/lib/pasteImage.js
new file mode 100644
index 00000000..264d77ac
--- /dev/null
+++ b/client/lib/pasteImage.js
@@ -0,0 +1,57 @@
+/* eslint-disable */
+
+// ------------------------------------------------------------------------
+// Created by STRd6
+// MIT License
+// https://github.com/distri/jquery-image_reader/blob/master/paste.coffee.md
+//
+// Raymond re-write it to javascript
+
+(function($) {
+ $.event.fix = (function(originalFix) {
+ return function(event) {
+ event = originalFix.apply(this, arguments);
+ if (event.type.indexOf('copy') === 0 || event.type.indexOf('paste') === 0) {
+ event.clipboardData = event.originalEvent.clipboardData;
+ }
+ return event;
+ };
+ })($.event.fix);
+
+ const defaults = {
+ callback: $.noop,
+ matchType: /image.*/,
+ };
+
+ return $.fn.pasteImageReader = function(options) {
+ if (typeof options === 'function') {
+ options = {
+ callback: options,
+ };
+ }
+ options = $.extend({}, defaults, options);
+ return this.each(function() {
+ const element = this;
+ return $(element).bind('paste', function(event) {
+ const types = event.clipboardData.types;
+ const items = event.clipboardData.items;
+ for(let i=0; i<types.length; i++) {
+ if(types[i].match(options.matchType) || items[i].type.match(options.matchType)) {
+ const f = items[i].getAsFile();
+ const reader = new FileReader();
+ reader.onload = function(evt) {
+ return options.callback.call(element, {
+ dataURL: evt.target.result,
+ event: evt,
+ file: f,
+ name: f.name,
+ });
+ };
+ reader.readAsDataURL(f);
+ return;
+ }
+ }
+ });
+ });
+ };
+})(jQuery);
diff --git a/client/lib/popup.js b/client/lib/popup.js
index 3c39af29..7418d938 100644
--- a/client/lib/popup.js
+++ b/client/lib/popup.js
@@ -91,7 +91,7 @@ window.Popup = new class {
if (!self.isOpen()) {
self.current = Blaze.renderWithData(self.template, () => {
self._dep.depend();
- return _.extend(self._getTopStack(), { stack: self._stack });
+ return { ...self._getTopStack(), stack: self._stack };
}, document.body);
} else {
@@ -191,7 +191,7 @@ window.Popup = new class {
// We close a potential opened popup on any left click on the document, or go
// one step back by pressing escape.
const escapeActions = ['back', 'close'];
-_.each(escapeActions, (actionName) => {
+escapeActions.forEach((actionName) => {
EscapeActions.register(`popup-${actionName}`,
() => Popup[actionName](),
() => Popup.isOpen(),
diff --git a/client/lib/textComplete.js b/client/lib/textComplete.js
new file mode 100644
index 00000000..3e69d07f
--- /dev/null
+++ b/client/lib/textComplete.js
@@ -0,0 +1,54 @@
+// We “inherit” the jquery-textcomplete plugin to integrate with our
+// EscapeActions system. You should always use `escapeableTextComplete` instead
+// of the vanilla `textcomplete`.
+let dropdownMenuIsOpened = false;
+
+$.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) {
+ // When the autocomplete menu is shown we want both a press of both `Tab`
+ // or `Enter` to validation the auto-completion. We also need to stop the
+ // event propagation to prevent EscapeActions side effect, for instance the
+ // minicard submission (on `Enter`) or going on the next column (on `Tab`).
+ options = {
+ onKeydown(evt, commands) {
+ if (evt.keyCode === 9 || evt.keyCode === 13) {
+ evt.stopPropagation();
+ return commands.KEY_ENTER;
+ }
+ },
+ ...options,
+ };
+
+ // Proxy to the vanilla jQuery component
+ this.textcomplete(strategies, options, ...otherArgs);
+
+ // Since commit d474017 jquery-textComplete automatically closes a potential
+ // opened dropdown menu when the user press Escape. This behavior conflicts
+ // with our EscapeActions system, but it's too complicated and hacky to
+ // monkey-pach textComplete to disable it -- I tried. Instead we listen to
+ // 'open' and 'hide' events, and create a ghost escapeAction when the dropdown
+ // is opened (and rely on textComplete to execute the actual action).
+ this.on({
+ 'textComplete:show'() {
+ dropdownMenuIsOpened = true;
+ },
+ 'textComplete:hide'() {
+ Tracker.afterFlush(() => {
+ // XXX Hack. We unfortunately need to set a setTimeout here to make the
+ // `noClickEscapeOn` work bellow, otherwise clicking on a autocomplete
+ // item will close both the autocomplete menu (as expected) but also the
+ // next item in the stack (for example the minicard editor) which we
+ // don't want.
+ setTimeout(() => {
+ dropdownMenuIsOpened = false;
+ }, 100);
+ });
+ },
+ });
+};
+
+EscapeActions.register('textcomplete',
+ () => {},
+ () => dropdownMenuIsOpened, {
+ noClickEscapeOn: '.textcomplete-dropdown',
+ }
+);
diff --git a/client/lib/unsavedEdits.js b/client/lib/unsavedEdits.js
index dc267bfb..17bb29b5 100644
--- a/client/lib/unsavedEdits.js
+++ b/client/lib/unsavedEdits.js
@@ -65,7 +65,7 @@ UnsavedEdits = {
};
Blaze.registerHelper('getUnsavedValue', (fieldName, docId, defaultTo) => {
- // Workaround some blaze feature that ass a list of keywords arguments as the
+ // Workaround some blaze feature that pass a list of keywords arguments as the
// last parameter (even if the caller didn't specify any).
if (!_.isString(defaultTo)) {
defaultTo = '';
diff --git a/client/lib/utils.js b/client/lib/utils.js
index 0cd93419..6bdd5822 100644
--- a/client/lib/utils.js
+++ b/client/lib/utils.js
@@ -22,20 +22,6 @@ Utils = {
return string.charAt(0).toUpperCase() + string.slice(1);
},
- getLabelIndex(boardId, labelId) {
- const board = Boards.findOne(boardId);
- const labels = {};
- _.each(board.labels, (a, b) => {
- labels[a._id] = b;
- });
- return {
- index: labels[labelId],
- key(key) {
- return `labels.${labels[labelId]}.${key}`;
- },
- };
- },
-
// Determine the new sort index
calculateIndex(prevCardDomElement, nextCardDomElement, nCards = 1) {
let base, increment;
diff --git a/collections/users.js b/collections/users.js
deleted file mode 100644
index fa910c4a..00000000
--- a/collections/users.js
+++ /dev/null
@@ -1,151 +0,0 @@
-Users = Meteor.users;
-
-// Search a user in the complete server database by its name or username. This
-// is used for instance to add a new user to a board.
-const searchInFields = ['username', 'profile.name'];
-Users.initEasySearch(searchInFields, {
- use: 'mongo-db',
- returnFields: [...searchInFields, 'profile.avatarUrl'],
-});
-
-Users.helpers({
- boards() {
- return Boards.find({ userId: this._id });
- },
-
- starredBoards() {
- const starredBoardIds = this.profile.starredBoards || [];
- return Boards.find({archived: false, _id: {$in: starredBoardIds}});
- },
-
- hasStarred(boardId) {
- const starredBoardIds = this.profile.starredBoards || [];
- return _.contains(starredBoardIds, boardId);
- },
-
- isBoardMember() {
- const board = Boards.findOne(Session.get('currentBoard'));
- return board && _.contains(_.pluck(board.members, 'userId'), this._id) &&
- _.where(board.members, {userId: this._id})[0].isActive;
- },
-
- isBoardAdmin() {
- const board = Boards.findOne(Session.get('currentBoard'));
- return board && this.isBoardMember(board) &&
- _.where(board.members, {userId: this._id})[0].isAdmin;
- },
-
- getInitials() {
- const profile = this.profile || {};
- if (profile.initials)
- return profile.initials;
-
- else if (profile.fullname) {
- return _.reduce(profile.fullname.split(/\s+/), (memo, word) => {
- return memo + word[0];
- }, '').toUpperCase();
-
- } else {
- return this.username[0].toUpperCase();
- }
- },
-
- toggleBoardStar(boardId) {
- const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet';
- Meteor.users.update(this._id, {
- [queryKind]: {
- 'profile.starredBoards': boardId,
- },
- });
- },
-});
-
-Meteor.methods({
- setUsername(username) {
- check(username, String);
- const nUsersWithUsername = Users.find({ username }).count();
- if (nUsersWithUsername > 0) {
- throw new Meteor.Error('username-already-taken');
- } else {
- Users.update(this.userId, {$set: { username }});
- }
- },
-});
-
-Users.before.insert((userId, doc) => {
- doc.profile = doc.profile || {};
-
- if (!doc.username && doc.profile.name) {
- doc.username = doc.profile.name.toLowerCase().replace(/\s/g, '');
- }
-});
-
-if (Meteor.isServer) {
- // Let mongoDB ensure username unicity
- Meteor.startup(() => {
- Users._collection._ensureIndex({
- username: 1,
- }, { unique: true });
- });
-
- // Each board document contains the de-normalized number of users that have
- // starred it. If the user star or unstar a board, we need to update this
- // counter.
- // We need to run this code on the server only, otherwise the incrementation
- // will be done twice.
- Users.after.update(function(userId, user, fieldNames) {
- // The `starredBoards` list is hosted on the `profile` field. If this
- // field hasn't been modificated we don't need to run this hook.
- if (!_.contains(fieldNames, 'profile'))
- return;
-
- // To calculate a diff of board starred ids, we get both the previous
- // and the newly board ids list
- function getStarredBoardsIds(doc) {
- return doc.profile && doc.profile.starredBoards;
- }
- const oldIds = getStarredBoardsIds(this.previous);
- const newIds = getStarredBoardsIds(user);
-
- // The _.difference(a, b) method returns the values from a that are not in
- // b. We use it to find deleted and newly inserted ids by using it in one
- // direction and then in the other.
- function incrementBoards(boardsIds, inc) {
- _.forEach(boardsIds, (boardId) => {
- Boards.update(boardId, {$inc: {stars: inc}});
- });
- }
- incrementBoards(_.difference(oldIds, newIds), -1);
- incrementBoards(_.difference(newIds, oldIds), +1);
- });
-
- // XXX i18n
- Users.after.insert((userId, doc) => {
- const ExampleBoard = {
- title: 'Welcome Board',
- userId: doc._id,
- permission: 'private',
- };
-
- // Insert the Welcome Board
- Boards.insert(ExampleBoard, (err, boardId) => {
-
- _.forEach(['Basics', 'Advanced'], (title) => {
- const list = {
- title,
- boardId,
- userId: ExampleBoard.userId,
-
- // XXX Not certain this is a bug, but we except these fields get
- // inserted by the Lists.before.insert collection-hook. Since this
- // hook is not called in this case, we have to dublicate the logic and
- // set them here.
- archived: false,
- createdAt: new Date(),
- };
-
- Lists.insert(list);
- });
- });
- });
-}
diff --git a/client/config/accounts.js b/config/accounts.js
index df0935f7..3a6a116e 100644
--- a/client/config/accounts.js
+++ b/config/accounts.js
@@ -25,7 +25,7 @@ AccountsTemplates.configure({
},
});
-_.each(['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'],
+['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'].forEach(
(routeName) => AccountsTemplates.configureRoute(routeName));
// We display the form to change the password in a popup window that already
@@ -46,3 +46,16 @@ AccountsTemplates.configureRoute('changePwd', {
Popup.back();
},
});
+
+if (Meteor.isServer) {
+ if (process.env.MAIL_FROM) {
+ Accounts.emailTemplates.from = process.env.MAIL_FROM;
+ }
+
+ ['resetPassword-subject', 'resetPassword-text', 'verifyEmail-subject', 'verifyEmail-text', 'enrollAccount-subject', 'enrollAccount-text'].forEach((str) => {
+ const words = str.split('-');
+ Accounts.emailTemplates[words[0]][words[1]] = (user, url) => {
+ return TAPi18n.__(`email-${str}`, { user: user.getName(), url }, user.getLanguage());
+ };
+ });
+}
diff --git a/i18n/ar.i18n.json b/i18n/ar.i18n.json
new file mode 100644
index 00000000..0cbbf195
--- /dev/null
+++ b/i18n/ar.i18n.json
@@ -0,0 +1,229 @@
+{
+ "actions": "الإجراءات",
+ "activities": "الأنشطة",
+ "activity": "النشاط",
+ "activity-added": "تمت إضافة %s ل %s",
+ "activity-archived": "إلى الأرشيف %s",
+ "activity-attached": "إرفاق %s ل %s",
+ "activity-created": "أنشأ %s",
+ "activity-excluded": "استبعاد %s عن %s",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
+ "activity-joined": "انضم %s",
+ "activity-moved": "تم نقل %s من %s إلى %s",
+ "activity-on": "على %s",
+ "activity-removed": "حذف %s إلى %s",
+ "activity-sent": "إرسال %s إلى %s",
+ "activity-unjoined": "غادر %s",
+ "add": "أضف",
+ "add-attachment": "إرفاق ملف",
+ "add-board": "إضافة لوحة",
+ "add-card": "إضافة بطاقة",
+ "add-cover": "إضافة غلاف",
+ "add-label": "إضافة علامة",
+ "add-list": "إضافة قائمة",
+ "add-members": "تعيين أعضاء",
+ "added": "أُضيف",
+ "addMemberPopup-title": "الأعضاء",
+ "admin": "المدير",
+ "admin-desc": "إمكانية مشاهدة و تعديل و حذف أعضاء ، و تعديل إعدادات اللوحة أيضا.",
+ "all-boards": "كل اللوحات",
+ "and-n-other-card": "And __count__ other بطاقة",
+ "and-n-other-card_plural": "And __count__ other بطاقات",
+ "archive": "أرشف",
+ "archive-all": "أرشف الكل",
+ "archive-board": "أرشف اللوحة",
+ "archive-card": "أرشف البطاقة",
+ "archive-list": "أرشف هذه القائمة",
+ "archive-selection": "أرشف المُحدّد",
+ "archiveBoardPopup-title": "غلق اللوحة ?",
+ "archived-items": "عناصر في الأرشيف",
+ "archives": "أرشيفات",
+ "assign-member": "تعيين عضو",
+ "attached": "أُرفق)",
+ "attachment": "مرفق",
+ "attachment-delete-pop": "حذف المرق هو حذف نهائي . لا يمكن التراجع إذا حذف.",
+ "attachmentDeletePopup-title": "تريد حذف المرفق ?",
+ "attachments": "المرفقات",
+ "avatar-too-big": "حجم ملف الصورة الخاصة بك كبير . لا يمكن أن تتجاوز 70 كيلو أكتي",
+ "back": "رجوع",
+ "board-change-color": "تغيير اللومr",
+ "board-nb-stars": "%s نجوم",
+ "board-not-found": "لوحة مفقودة",
+ "board-private-info": "سوف تصبح هذه اللوحة <strong>خاصة</strong>",
+ "board-public-info": "سوف تصبح هذه اللوحة <strong>عامّة</strong>.",
+ "boardChangeColorPopup-title": "تعديل خلفية الشاشة",
+ "boardChangeTitlePopup-title": "إعادة تسمية اللوحة",
+ "boardChangeVisibilityPopup-title": "تعديل وضوح الرؤية",
+ "boardImportBoardPopup-title": "Import board from Trello",
+ "boardMenuPopup-title": "قائمة اللوحة",
+ "boards": "لوحات",
+ "bucket-example": "مثل « todo list » على سبيل المثال",
+ "cancel": "إلغاء",
+ "card-archived": "هذه البطاقة أُرشفت.",
+ "card-comments-title": "%s تعليقات لهذه البطاقة",
+ "card-delete-notice": "هذا حذف أبديّ . سوف تفقد كل الإجراءات المنوطة بهذه البطاقة",
+ "card-delete-pop": "سيتم إزالة جميع الإجراءات من تبعات النشاط، وأنك لن تكون قادرا على إعادة فتح البطاقة. لا يوجد التراجع.",
+ "card-delete-suggest-archive": "يمكنك أرشفة بطاقة لحذفها من اللوحة والمحافظة على النشاط.",
+ "card-edit-attachments": "تعديل المرفقات",
+ "card-edit-labels": "تعديل العلامات",
+ "card-edit-members": "تعديل الأعضاء",
+ "card-labels-title": "تعديل علامات البطاقة.",
+ "card-members-title": "إضافة او حذف أعضاء للبطاقة.",
+ "cardAttachmentsPopup-title": "إرفاق من",
+ "cardDeletePopup-title": "حذف البطاقة ?",
+ "cardDetailsActionsPopup-title": "إجراءات على البطاقة",
+ "cardLabelsPopup-title": "علامات",
+ "cardMembersPopup-title": "أعضاء",
+ "cardMorePopup-title": "المزيد",
+ "cards": "بطاقات",
+ "change": "Change",
+ "change-avatar": "تعديل الصورة الشخصية",
+ "change-password": "تغيير كلمة المرور",
+ "change-permissions": "تعديل الصلاحيات",
+ "changeAvatarPopup-title": "تعديل الصورة الشخصية",
+ "changeLanguagePopup-title": "تغيير اللغة",
+ "changePasswordPopup-title": "تغيير كلمة المرور",
+ "changePermissionsPopup-title": "تعديل الصلاحيات",
+ "click-to-star": "اضغط لإضافة اللوحة للمفضلة.",
+ "click-to-unstar": "اضغط لحذف اللوحة من المفضلة.",
+ "close": "غلق",
+ "close-board": "غلق اللوحة",
+ "close-board-pop": "يمكنك إعادة فتح اللوحة بالنقر على عنصر اللوحات من القائمة الفوقية، ثم اختيار -مشاهدة اللوحات المغلقة- ثم ستجد اللوحة و يمكنك إعادة فتحها",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
+ "comment": "تعليق",
+ "comment-placeholder": "صياغة تعليق",
+ "computer": "حاسوب",
+ "create": "إنشاء",
+ "createBoardPopup-title": "إنشاء لوحة",
+ "createLabelPopup-title": "إنشاء علامة",
+ "current": "الحالي",
+ "default-avatar": "صورة شخصية افتراضية",
+ "delete": "حذف",
+ "deleteLabelPopup-title": "حذف العلامة ?",
+ "description": "وصف",
+ "disambiguateMultiLabelPopup-title": "تحديد الإجراء على العلامة",
+ "disambiguateMultiMemberPopup-title": "تحديد الإجراء على العضو",
+ "discard": "التخلص منها",
+ "download": "تنزيل",
+ "edit": "تعديل",
+ "edit-avatar": "تعديل الصورة الشخصية",
+ "edit-profile": "تعديل الملف الشخصي",
+ "editLabelPopup-title": "تعديل العلامة",
+ "editProfilePopup-title": "تعديل الملف الشخصي",
+ "email": "البريد الإلكتروني",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data does not include the proper information in the correct format",
+ "error-list-doesNotExist": "This list does not exist",
+ "filter": "تصفية",
+ "filter-cards": "تصفية البطاقات",
+ "filter-clear": "مسح التصفية",
+ "filter-on": "التصفية تشتغل",
+ "filter-on-desc": "أنت بصدد تصفية بطاقات هذه اللوحة. اضغط هنا لتعديل التصفية.",
+ "filter-to-selection": "تصفية بالتحديد",
+ "fullname": "الإسم الكامل",
+ "header-logo-title": "الرجوع إلى صفحة اللوحات",
+ "home": "الرئيسية",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "info": "معلومات",
+ "initials": "أولية",
+ "joined": "انضمّ",
+ "keyboard-shortcuts": "اختصار لوحة المفاتيح",
+ "label-create": "إنشاء علامة جديدة",
+ "label-default": "%s علامة (افتراضية)",
+ "label-delete-pop": "لا يوجد تراجع. سيؤدي هذا إلى إزالة هذه العلامة من جميع بطاقات والقضاء على تأريخها",
+ "labels": "علامات",
+ "language": "لغة",
+ "last-admin-desc": "لا يمكن تعديل الأدوار لأن ذلك يتطلب صلاحيات المدير.",
+ "leave-board": "مغادرة اللوحة",
+ "link-card": "ربط هذه البطاقة",
+ "list-archive-cards": "أرضفت بطاقات هذه القائمة",
+ "list-archive-cards-pop": "سيقتضي هذا أرشفة جميع بطاقات هذه القائمة. لمشاهدة أرشيف البطاقات أو إعادتها إلى اللوحة، اضغط على -القائمة- ثم - أرشيف العناصر-",
+ "list-move-cards": "نقل بطاقات هذه القائمة",
+ "list-select-cards": "تحديد بطاقات هذه القائمة",
+ "listActionPopup-title": "قائمة الإجراءات",
+ "listArchiveCardsPopup-title": "أرشفة بطاقات القائمة ?",
+ "listImportCardPopup-title": "Import a Trello card",
+ "listMoveCardsPopup-title": "نقل بطاقات القائمة",
+ "lists": "القائمات",
+ "log-out": "تسجيل الخروج",
+ "loginPopup-title": "تسجيل الدخول",
+ "memberMenuPopup-title": "أفضليات الأعضاء",
+ "members": "أعضاء",
+ "menu": "القائمة",
+ "moveCardPopup-title": "نقل البطاقة",
+ "multi-selection": "تحديد أكثر من واحدة",
+ "multi-selection-on": "Multi-Selection is on",
+ "my-boards": "لوحاتي",
+ "name": "اسم",
+ "no-archived-cards": "لا يوجد بطاقة في الأرشيف.",
+ "no-archived-lists": "لا يوجد قائمة في الأرشيف.",
+ "no-results": "لا توجد نتائج",
+ "normal": "عادي",
+ "normal-desc": "يمكن مشاهدة و تعديل البطاقات. لا يمكن تغيير إعدادات الضبط.",
+ "optional": "اختياري",
+ "or": "or",
+ "page-maybe-private": "قدتكون هذه الصفحة خاصة . قد تستطيع مشاهدتها ب <a href='%s'>تسجيل الدخول</a>.",
+ "page-not-found": "صفحة غير موجودة",
+ "password": "كلمة المرور",
+ "private": "خاص",
+ "private-desc": "هذه اللوحة خاصة . لا يسمح إلا للأعضاء .",
+ "profile": "ملف شخصي",
+ "public": "عامّ",
+ "public-desc": "هذه اللوحة عامة: مرئية لكلّ من يحصل على الرابط ، و هي مرئية أيضا في محركات البحث مثل جوجل. التعديل مسموح به للأعضاء فقط.",
+ "quick-access-description": "أضف لوحة إلى المفضلة لإنشاء اختصار في هذا الشريط.",
+ "remove-cover": "حذف الغلاف",
+ "remove-from-board": "حذف من اللوحة",
+ "remove-label": "حذف هذه العلامة",
+ "remove-member": "حذف العضو",
+ "remove-member-from-card": "حذف من البطاقة",
+ "remove-member-pop": "حذف __name__ (__username__) من __boardTitle__ ? سيتم حذف هذا العضو من جميع بطاقة اللوحة مع إرسال إشعار له بذاك.",
+ "removeMemberPopup-title": "حذف العضو ?",
+ "rename": "إعادة التسمية",
+ "rename-board": "إعادة تسمية اللوحة",
+ "restore": "استعادة",
+ "save": "حفظ",
+ "search": "بحث",
+ "select-color": "اختيار لون",
+ "shortcut-assign-self": "Assign yourself to current card",
+ "shortcut-autocomplete-emojies": "الإكمال التلقائي للرموز التعبيرية",
+ "shortcut-autocomplete-members": "الإكمال التلقائي لأسماء الأعضاء",
+ "shortcut-clear-filters": "مسح التصفيات",
+ "shortcut-close-dialog": "غلق النافذة",
+ "shortcut-filter-my-cards": "تصفية بطاقاتي",
+ "shortcut-show-shortcuts": "عرض قائمة الإختصارات ،تلك",
+ "shortcut-toggle-filterbar": "Toggle Filter Sidebar",
+ "shortcut-toggle-sidebar": "إظهار-إخفاء الشريط الجانبي للوحة",
+ "signupPopup-title": "إنشاء حساب",
+ "star-board-title": "اضغط لإضافة هذه اللوحة إلى المفضلة . سوف يتم إظهارها على رأس بقية اللوحات.",
+ "starred-boards": "اللوحات المفضلة",
+ "starred-boards-description": "تعرض اللوحات المفضلة على رأس بقية اللوحات.",
+ "subscribe": "اشتراك و متابعة",
+ "team": "فريق",
+ "this-board": "هذه اللوحة",
+ "this-card": "هذه البطاقة",
+ "title": "عنوان",
+ "unassign-member": "إلغاء تعيين العضو",
+ "unsaved-description": "لديك وصف غير محفوظ",
+ "upload-avatar": "رفع صورة شخصية",
+ "uploaded-avatar": "تم رفع الصورة الشخصية",
+ "username": "اسم المستخدم",
+ "view-it": "شاهدها",
+ "warn-list-archived": "انتبه : هذه البطاقة في أرشيف القائمات",
+ "what-to-do": "ماذا تريد أن تنجز?"
+} \ No newline at end of file
diff --git a/i18n/ca.i18n.json b/i18n/ca.i18n.json
new file mode 100644
index 00000000..fae76861
--- /dev/null
+++ b/i18n/ca.i18n.json
@@ -0,0 +1,229 @@
+{
+ "actions": "Accions",
+ "activities": "Activitats",
+ "activity": "Activitat",
+ "activity-added": "ha afegit %s a %s",
+ "activity-archived": "ha arxivat %s",
+ "activity-attached": "ha adjuntat %s a %s",
+ "activity-created": "ha creat %s",
+ "activity-excluded": "ha exclòs %s de %s",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
+ "activity-joined": "s'ha unit a %s",
+ "activity-moved": "ha mogut %s de %s a %s",
+ "activity-on": "en %s",
+ "activity-removed": "ha eliminat %s de %s",
+ "activity-sent": "ha enviat %s %s",
+ "activity-unjoined": "unjoined %s",
+ "add": "Afegeix",
+ "add-attachment": "Afegeix arxiu adjunt",
+ "add-board": "Afegeix un nou tauler",
+ "add-card": "Afegeix fitxa",
+ "add-cover": "Afegeix coberta",
+ "add-label": "Afegeix etiqueta",
+ "add-list": "Afegeix llista",
+ "add-members": "Afegeix membres",
+ "added": "Afegit",
+ "addMemberPopup-title": "Membres",
+ "admin": "Administrador",
+ "admin-desc": "Pots veure i editar fitxes, eliminar membres, i canviar la configuració del tauler",
+ "all-boards": "Tots els taulers",
+ "and-n-other-card": "And __count__ other card",
+ "and-n-other-card_plural": "And __count__ other cards",
+ "archive": "Desa",
+ "archive-all": "Desa Tot",
+ "archive-board": "Arxiva tauler",
+ "archive-card": "Arxiva fitxa",
+ "archive-list": "Arxiva aquesta llista",
+ "archive-selection": "Arxiva selecció",
+ "archiveBoardPopup-title": "Tanca el tauler",
+ "archived-items": "Elements arxivats",
+ "archives": "Arxivats",
+ "assign-member": "Assignar membre",
+ "attached": "adjuntat",
+ "attachment": "Adjunt",
+ "attachment-delete-pop": "L'esborrat d'un arxiu adjunt és permanent. No es pot desfer.",
+ "attachmentDeletePopup-title": "Esborrar adjunt?",
+ "attachments": "Adjunts",
+ "avatar-too-big": "L'avatar és massa gran (70Kb max)",
+ "back": "Enrere",
+ "board-change-color": "Canvia el color",
+ "board-nb-stars": "%s estrelles",
+ "board-not-found": "No s'ha trobat el tauler",
+ "board-private-info": "Aquest tauler serà <strong> privat </ strong>.",
+ "board-public-info": "Aquest tauler serà <strong> públic </ strong>.",
+ "boardChangeColorPopup-title": "Canvia fons",
+ "boardChangeTitlePopup-title": "Canvia el nom tauler",
+ "boardChangeVisibilityPopup-title": "Canvia visibilitat",
+ "boardImportBoardPopup-title": "Import board from Trello",
+ "boardMenuPopup-title": "Menú del tauler",
+ "boards": "Taulers",
+ "bucket-example": "Igual que “Bucket List”, per exemple",
+ "cancel": "Cancel·la",
+ "card-archived": "Aquesta fitxa està arxivada.",
+ "card-comments-title": "Aquesta fitxa té %s comentaris.",
+ "card-delete-notice": "L'esborrat és permanent. Perdreu totes les accions associades a aquesta fitxa.",
+ "card-delete-pop": "Totes les accions s'eliminaran de l'activitat i no podreu tornar a obrir la fitxa. No es pot desfer.",
+ "card-delete-suggest-archive": "Podeu arxivar una fitxa per extreure-la del tauler i preservar l'activitat.",
+ "card-edit-attachments": "Edita arxius adjunts",
+ "card-edit-labels": "Edita etiquetes",
+ "card-edit-members": "Edita membres",
+ "card-labels-title": "Canvia les etiquetes de la fitxa",
+ "card-members-title": "Afegeix o eliminar membres del tauler des de la fitxa.",
+ "cardAttachmentsPopup-title": "Adjunta des de",
+ "cardDeletePopup-title": "Esborrar fitxa?",
+ "cardDetailsActionsPopup-title": "Accions de fitxes",
+ "cardLabelsPopup-title": "Etiquetes",
+ "cardMembersPopup-title": "Membres",
+ "cardMorePopup-title": "Més",
+ "cards": "Fitxes",
+ "change": "Change",
+ "change-avatar": "Canvia Avatar",
+ "change-password": "Canvia la clau",
+ "change-permissions": "Canvia permisos",
+ "changeAvatarPopup-title": "Canvia Avatar",
+ "changeLanguagePopup-title": "Canvia idioma",
+ "changePasswordPopup-title": "Canvia la contrasenya",
+ "changePermissionsPopup-title": "Canvia permisos",
+ "click-to-star": "Fes clic per destacar aquest tauler.",
+ "click-to-unstar": "Fes clic per deixar de destacar aquest tauler.",
+ "close": "Tanca",
+ "close-board": "Tanca tauler",
+ "close-board-pop": "Podeu tornar a obrir el tauler fent clic al menú \"Taulers\" de la capçalera, seleccionar \"Veure Taulers Tancats \", cercar el tauler i fer clic a \"Tornar a obrir \".",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
+ "comment": "Comentari",
+ "comment-placeholder": "Escriu un comentari",
+ "computer": "Ordinador",
+ "create": "Crea",
+ "createBoardPopup-title": "Crea tauler",
+ "createLabelPopup-title": "Crea etiqueta",
+ "current": "Actual",
+ "default-avatar": "Avatar per defecte",
+ "delete": "Esborra",
+ "deleteLabelPopup-title": "Esborra etiqueta",
+ "description": "Descripció",
+ "disambiguateMultiLabelPopup-title": "Desfe l'ambigüitat en les etiquetes",
+ "disambiguateMultiMemberPopup-title": "Desfe l'ambigüitat en els membres",
+ "discard": "Descarta",
+ "download": "Descarrega",
+ "edit": "Edita",
+ "edit-avatar": "Canvia Avatar",
+ "edit-profile": "Edita el teu Perfil",
+ "editLabelPopup-title": "Canvia etiqueta",
+ "editProfilePopup-title": "Edita teu Perfil",
+ "email": "Correu electrònic",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data does not include the proper information in the correct format",
+ "error-list-doesNotExist": "This list does not exist",
+ "filter": "Filtre",
+ "filter-cards": "Fitxes de filtre",
+ "filter-clear": "Elimina filtre",
+ "filter-on": "Filtra per",
+ "filter-on-desc": "Estau filtrant fitxes en aquest tauler. Feu clic aquí per editar el filtre.",
+ "filter-to-selection": "Filtra selecció",
+ "fullname": "Nom complet",
+ "header-logo-title": "Torna a la teva pàgina de taulers",
+ "home": "Inici",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "info": "Informacions",
+ "initials": "Inicials",
+ "joined": "s'ha unit",
+ "keyboard-shortcuts": "Dreceres de teclat",
+ "label-create": "Crea una etiqueta nova",
+ "label-default": "%s etiqueta (per defecte)",
+ "label-delete-pop": "No es pot desfer. Això eliminarà aquesta etiqueta de totes les fitxes i destruirà la seva història.",
+ "labels": "Etiquetes",
+ "language": "Idioma",
+ "last-admin-desc": "No podeu canviar rols perquè ha d'haver-hi almenys un administrador.",
+ "leave-board": "Abandona tauler",
+ "link-card": "Enllaç a aquesta fitxa",
+ "list-archive-cards": "Arxiva totes les fitxes d'aquesta llista",
+ "list-archive-cards-pop": "Això eliminarà totes les fitxes d'aquesta llista del tauler. Per veure les fitxes arxivades i recuperar-les en el tauler, feu clic a \" Menú \"/ \" Articles Arxivat \".",
+ "list-move-cards": "Mou totes les fitxes d'aquesta llista",
+ "list-select-cards": "Selecciona totes les fitxes d'aquesta llista",
+ "listActionPopup-title": "Accions de la llista",
+ "listArchiveCardsPopup-title": "Arxivar totes les fitxes d'aquesta llista?",
+ "listImportCardPopup-title": "Import a Trello card",
+ "listMoveCardsPopup-title": "Moure totes les fitxes de la llista",
+ "lists": "Llistes",
+ "log-out": "Finalitza la sessió",
+ "loginPopup-title": "Inicia sessió",
+ "memberMenuPopup-title": "Configura membres",
+ "members": "Membres",
+ "menu": "Menú",
+ "moveCardPopup-title": "Moure fitxa",
+ "multi-selection": "Multi-Selecció",
+ "multi-selection-on": "Multi-Selection is on",
+ "my-boards": "Els meus taulers",
+ "name": "Nom",
+ "no-archived-cards": "No hi ha fitxes arxivades.",
+ "no-archived-lists": "No hi ha llistes arxivades.",
+ "no-results": "Sense resultats",
+ "normal": "Normal",
+ "normal-desc": "Podeu veure i editar fitxes. No podeu canviar la configuració.",
+ "optional": "opcional",
+ "or": "or",
+ "page-maybe-private": "Aquesta pàgina és privada. Per veure-la <a href='%s'> entra </a>.",
+ "page-not-found": "Pàgina no trobada.",
+ "password": "Contrasenya",
+ "private": "Privat",
+ "private-desc": "Aquest tauler és privat. Només les persones afegides al tauler poden veure´l i editar-lo.",
+ "profile": "Perfil",
+ "public": "Públic",
+ "public-desc": "Aquest tauler és públic. És visible per a qualsevol persona amb l'enllaç i es mostrarà en els motors de cerca com Google. Només persones afegides al tauler poden editar-lo.",
+ "quick-access-description": "Inicia un tauler per afegir un accés directe en aquest barra",
+ "remove-cover": "Elimina coberta",
+ "remove-from-board": "Elimina del tauler",
+ "remove-label": "Eliminia etiqueta",
+ "remove-member": "Elimina membre",
+ "remove-member-from-card": "Elimina de la fitxa",
+ "remove-member-pop": "Eliminar __name__ (__username__) de __boardTitle__ ? El membre serà eliminat de totes les fitxes d'aquest tauler. Ells rebran una notificació.",
+ "removeMemberPopup-title": "Vols suprimir el membre?",
+ "rename": "Canvia el nom",
+ "rename-board": "Canvia el nom del tauler",
+ "restore": "Restaura",
+ "save": "Desa",
+ "search": "Cerca",
+ "select-color": "Selecciona un color",
+ "shortcut-assign-self": "Assign yourself to current card",
+ "shortcut-autocomplete-emojies": "Autocompleta emojies",
+ "shortcut-autocomplete-members": "Autocompleta membres",
+ "shortcut-clear-filters": "Elimina tots els filters",
+ "shortcut-close-dialog": "Tanca el diàleg",
+ "shortcut-filter-my-cards": "Filtra les meves fitxes",
+ "shortcut-show-shortcuts": "Mostra aquesta lista d'accessos directes",
+ "shortcut-toggle-filterbar": "Toggle Filter Sidebar",
+ "shortcut-toggle-sidebar": "Canvia Sidebar del Tauler",
+ "signupPopup-title": "Crea un compte",
+ "star-board-title": "Fes clic per destacar aquest tauler. Es mostrarà a la part superior de la llista de taulers.",
+ "starred-boards": "Taulers destacats",
+ "starred-boards-description": "Els taulers destacats es mostraran a la part superior de la llista de taulers.",
+ "subscribe": "Subscriure",
+ "team": "Equip",
+ "this-board": "aquest tauler",
+ "this-card": "aquesta fitxa",
+ "title": "Títol",
+ "unassign-member": "Desassignar membre",
+ "unsaved-description": "Tens una descripció sense desar.",
+ "upload-avatar": "Actualitza avatar",
+ "uploaded-avatar": "Avatar actualitzat",
+ "username": "Nom d'Usuari",
+ "view-it": "Vist",
+ "warn-list-archived": "Avís: aquesta fitxa està en una llista arxivada",
+ "what-to-do": "Què vols fer?"
+} \ No newline at end of file
diff --git a/i18n/de.i18n.json b/i18n/de.i18n.json
index 0f017ad0..bb18a335 100644
--- a/i18n/de.i18n.json
+++ b/i18n/de.i18n.json
@@ -2,17 +2,19 @@
"actions": "Aktionen",
"activities": "Aktivitäten",
"activity": "Aktivität",
- "activity-added": "%s zu %s hinzugefügt",
- "activity-archived": "%s archiviert",
- "activity-attached": "%s an %s angehängt",
- "activity-created": "%s erstellt",
- "activity-excluded": "%s von %s ausgeschlossen",
- "activity-joined": "%s beigetreten",
- "activity-moved": "%s von %s nach %s verschoben",
+ "activity-added": "hat %s zu %s hinzugefügt",
+ "activity-archived": "hat %s archiviert",
+ "activity-attached": "hat %s an %s angehängt",
+ "activity-created": "hat %s erstellt",
+ "activity-excluded": "hat %s von %s ausgeschlossen",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
+ "activity-joined": "hat %s beigetreten",
+ "activity-moved": "hat %s von %s nach %s verschoben",
"activity-on": "on %s",
- "activity-removed": "%s von %s entfernt",
- "activity-sent": "%s an %s gesendet",
- "activity-unjoined": "%s verlassen",
+ "activity-removed": "hat %s von %s entfernt",
+ "activity-sent": "hat %s an %s gesendet",
+ "activity-unjoined": "unjoined %s",
"add": "Hinzufügen",
"add-attachment": "Anhang hinzufügen",
"add-board": "Neues Board erstellen",
@@ -26,8 +28,8 @@
"admin": "Admin",
"admin-desc": "Kann Karten anschauen und bearbeiten, Mitglieder entfernen und Boardeinstellungen ändern.",
"all-boards": "Alle Boards",
- "and-n-other-card": "And __count__ other card",
- "and-n-other-card_plural": "And __count__ other cards",
+ "and-n-other-card": "und eine andere Karte",
+ "and-n-other-card_plural": "und __count__ andere Karten",
"archive": "Archiv",
"archive-all": "Alles archivieren",
"archive-board": "Board archivieren",
@@ -53,27 +55,29 @@
"boardChangeColorPopup-title": "Boardfarbe ändern",
"boardChangeTitlePopup-title": "Board umbenennen",
"boardChangeVisibilityPopup-title": "Sichtbarkeit ändern",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Boardmenü",
"boards": "Boards",
- "bucket-example": "Like “Bucket List” for example",
+ "bucket-example": "Zum Beispiel \"Bucket List\"",
"cancel": "Abbrechen",
"card-archived": "Diese Karte wurde archiviert.",
"card-comments-title": "Diese Karte hat %s Kommentare.",
"card-delete-notice": "Löschen ist irreversiebel. Alle Aktionen, die mit dieser Karte zu tun haben, werden ebenfalls gelöscht.",
- "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 archive a card to remove it from the board and preserve the activity.",
+ "card-delete-pop": "Alle Aktionen werden vom Aktivitätsfeed entfernt und du kannst die Karte nicht mehr öffnen. Es gibt keine Möglichkeit diese Aktion rückgängig zu machen.",
+ "card-delete-suggest-archive": "Du kannst die Karte statdessen archivieren, um sie vom Bord zu entfernen und die Aktivitäten zu erhalten.",
"card-edit-attachments": "Anhang ändern",
"card-edit-labels": "Labels ändern",
"card-edit-members": "Nutzer ändern",
"card-labels-title": "Label für diese Karte ändern.",
"card-members-title": "Füge dem Board Nutzer hinzu oder entferne sie von der Karte.",
- "cardAttachmentsPopup-title": "Attach From",
+ "cardAttachmentsPopup-title": "Anhängen von",
"cardDeletePopup-title": "Karte löschen?",
"cardDetailsActionsPopup-title": "Kartenaktionen",
"cardLabelsPopup-title": "Labels",
"cardMembersPopup-title": "Mitglieder",
"cardMorePopup-title": "Mehr",
"cards": "Karten",
+ "change": "Change",
"change-avatar": "Profilbild ändern",
"change-password": "Passwort ändern",
"change-permissions": "Ändere Berechtigungen",
@@ -86,19 +90,29 @@
"close": "Schließen",
"close-board": "Board schließen",
"close-board-pop": "Du kannst das Board wiederherstellen, indem du auf den \"Boards\" Menüeintrag im der Kopfleiste klickst, \"Zeige geschlossene Boards an\" auswählst, dein Board suchst und auf \"Wiederherstellen\" klickst.",
+ "color-green": "grün",
+ "color-yellow": "gelb",
+ "color-orange": "orange",
+ "color-red": "rot",
+ "color-purple": "lila",
+ "color-blue": "blau",
+ "color-sky": "himmelblau",
+ "color-lime": "hellgrün",
+ "color-pink": "pink",
+ "color-black": "schwarz",
"comment": "Kommentar",
"comment-placeholder": "Kommentar schreiben",
"computer": "Computer",
"create": "Erstellen",
"createBoardPopup-title": "Erstelle ein Board",
"createLabelPopup-title": "Label erstellen",
- "current": "current",
+ "current": "aktuell",
"default-avatar": "Standard Profilbild",
"delete": "Löschen",
"deleteLabelPopup-title": "Label löschen?",
"description": "Beschreibung",
- "disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
- "disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
+ "disambiguateMultiLabelPopup-title": "Labels vereinheitlichen",
+ "disambiguateMultiMemberPopup-title": "Mitglieder vereinheitlichen",
"discard": "Verwerfen",
"download": "Download",
"edit": "Bearbeiten",
@@ -107,15 +121,25 @@
"editLabelPopup-title": "Ändere Label",
"editProfilePopup-title": "Profil ändern",
"email": "Email",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data does not include the proper information in the correct format",
+ "error-list-doesNotExist": "This list does not exist",
"filter": "Filter",
"filter-cards": "Karten filtern",
"filter-clear": "Filter entfernen",
"filter-on": "Filter ist aktiv",
"filter-on-desc": "Du filterst die Karten auf diesem Board. Klicke hier, um die Filter zu bearbeiten.",
- "filter-to-selection": "Filter to selection",
+ "filter-to-selection": "Ergebnisse auswählen",
"fullname": "Voller Name",
"header-logo-title": "Zurück zur Board Seite.",
"home": "Home",
+ "import": "Importieren",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
"info": "Informationen",
"initials": "Initialien",
"joined": "beigetreten",
@@ -134,6 +158,7 @@
"list-select-cards": "Alle Karten in dieser Liste auswählen",
"listActionPopup-title": "Listenaktionen",
"listArchiveCardsPopup-title": "Alle Karten in der Liste archivieren?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "Verschiebe alle Karten in dieser Liste",
"lists": "Listen",
"log-out": "Ausloggen",
@@ -143,6 +168,7 @@
"menu": "Menü",
"moveCardPopup-title": "Karte verschieben",
"multi-selection": "Mehrfachauswahl",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "Meine Boards",
"name": "Name",
"no-archived-cards": "Keine archivierten Karten.",
@@ -151,6 +177,7 @@
"normal": "Normal",
"normal-desc": "Kann Karten anschauen und bearbeiten, aber keine Einstellungen ändern.",
"optional": "optional",
+ "or": "oder",
"page-maybe-private": "Diese Seite könnte privat sein. Vielleicht kannst du sie sehen, wenn du dich <a href='%s'>einloggst</a>.",
"page-not-found": "Seite nicht gefunden.",
"password": "Passwort",
@@ -159,7 +186,7 @@
"profile": "Profil",
"public": "Öffentlich",
"public-desc": "Dieses Board ist öffentlich. Es ist für jeden, der den Link kennt, sichtbar und taucht in Suchmaschinen wie Google auf. Nur Nutzer, die zum Board hinzugefügt wurden, können es bearbeiten.",
- "quick-access-description": "Star a board to add a shortcut in this bar.",
+ "quick-access-description": "Markiere ein Board mit einem Stern um eine Verknüpfung in diese Leise hinzuzufügen.",
"remove-cover": "Cover entfernen",
"remove-from-board": "Von Board entfernen",
"remove-label": "Label entfernen",
@@ -173,13 +200,15 @@
"save": "Speichern",
"search": "Suchen",
"select-color": "Wähle eine Farbe aus",
+ "shortcut-assign-self": "Assign yourself to current card",
"shortcut-autocomplete-emojies": "Autovervollständige Emojis",
"shortcut-autocomplete-members": "Autovervollständige Nutzer",
"shortcut-clear-filters": "Alle Filter entfernen",
"shortcut-close-dialog": "Dialog schließen",
"shortcut-filter-my-cards": "Meine Karten filtern",
- "shortcut-show-shortcuts": "Bring up this shortcuts list",
- "shortcut-toggle-sidebar": "Toggle Board Sidebar",
+ "shortcut-show-shortcuts": "Liste der Tastaturkürzel anzeigen",
+ "shortcut-toggle-filterbar": "Toggle Filter Sidebar",
+ "shortcut-toggle-sidebar": "Seitenleiste ein-/ausblenden",
"signupPopup-title": "Account erstellen",
"star-board-title": "Klicke, um das Board mit einem Stern zu kennzeichnen. Es erscheint dann oben in deiner Boardliste.",
"starred-boards": "Gekennzeichnete Boards",
diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json
index 3486aa93..04c0959f 100644
--- a/i18n/en.i18n.json
+++ b/i18n/en.i18n.json
@@ -1,4 +1,5 @@
{
+ "accept": "Accept",
"actions": "Actions",
"activities": "Activities",
"activity": "Activity",
@@ -7,12 +8,14 @@
"activity-attached": "attached %s to %s",
"activity-created": "created %s",
"activity-excluded": "excluded %s from %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": "unjoinded %s",
+ "activity-unjoined": "unjoined %s",
"add": "Add",
"add-attachment": "Add an attachment",
"add-board": "Add a new board",
@@ -53,6 +56,7 @@
"boardChangeColorPopup-title": "Change Board Background",
"boardChangeTitlePopup-title": "Rename Board",
"boardChangeVisibilityPopup-title": "Change Visibility",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Board Menu",
"boards": "Boards",
"bucket-example": "Like “Bucket List” for example",
@@ -74,6 +78,7 @@
"cardMembersPopup-title": "Members",
"cardMorePopup-title": "More",
"cards": "Cards",
+ "change": "Change",
"change-avatar": "Change Avatar",
"change-password": "Change Password",
"change-permissions": "Change permissions",
@@ -83,9 +88,20 @@
"changePermissionsPopup-title": "Change Permissions",
"click-to-star": "Click to star this board.",
"click-to-unstar": "Click to unstar this board.",
+ "clipboard" : "Clipboard or drag & drop",
"close": "Close",
"close-board": "Close Board",
"close-board-pop": "You can re-open the board by clicking the “Boards” menu from the header, selecting “View Closed Boards”, finding the board and clicking “Re-open”.",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
"comment": "Comment",
"comment-placeholder": "Write a comment",
"computer": "Computer",
@@ -93,6 +109,7 @@
"createBoardPopup-title": "Create Board",
"createLabelPopup-title": "Create Label",
"current": "current",
+ "decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Delete",
"deleteLabelPopup-title": "Delete Label?",
@@ -100,6 +117,7 @@
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
"discard": "Discard",
+ "done": "Done",
"download": "Download",
"edit": "Edit",
"edit-avatar": "Change Avatar",
@@ -108,6 +126,27 @@
"editLabelPopup-title": "Change Label",
"editProfilePopup-title": "Edit Profile",
"email": "Email",
+ "email-enrollAccount-subject": "An account created for you on __url__",
+ "email-enrollAccount-text": "Hello __user__,\n\nTo start using the service, simply click the link below.\n\n__url__\n\nThanks.\n",
+ "email-fail": "Sending email failed",
+ "email-invalid": "Invalid email",
+ "email-invite": "Invite via Email",
+ "email-invite-subject": "__inviter__ sent you an invitation",
+ "email-invite-text": "Dear __user__,\n\n__inviter__ invites you to join board \"__board__\" for collaborations.\n\nPlease follow the link below:\n\n__url__\n\nThanks.\n",
+ "email-resetPassword-subject": "Reset your password on __url__",
+ "email-resetPassword-text": "Hello __user__,\n\nTo reset your password, simply click the link below.\n\n__url__\n\nThanks.\n",
+ "email-verifyEmail-subject": "Verify your email address on __url__",
+ "email-verifyEmail-text": "Hello __user__,\n\nTo verify your account email, simply click the link below.\n\n__url__\n\nThanks.\n",
+ "email-sent": "Email sent",
+ "error-board-doesNotExist": "This board does not exist",
+ "error-board-notAdmin": "You need to be admin of this board to do that",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data 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": "This action on self is not allowed",
+ "error-user-notCreated": "This user is not created",
"filter": "Filter",
"filter-cards": "Filter Cards",
"filter-clear": "Clear filter",
@@ -117,9 +156,19 @@
"fullname": "Full Name",
"header-logo-title": "Go back to your boards page.",
"home": "Home",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
+ "import-show-user-mapping": "Review members mapping",
+ "import-user-select": "Pick the Wekan user you want to use as this member",
"info": "Infos",
"initials": "Initials",
"joined": "joined",
+ "just-invited": "You are just invited to this board",
"keyboard-shortcuts": "Keyboard shortcuts",
"label-create": "Create a new label",
"label-default": "%s label (default)",
@@ -135,15 +184,19 @@
"list-select-cards": "Select all cards in this list",
"listActionPopup-title": "List Actions",
"listArchiveCardsPopup-title": "Archive All Cards in this List?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "Move All Cards in List",
"lists": "Lists",
"log-out": "Log Out",
"loginPopup-title": "Log In",
+ "mapMembersPopup-title": "Map members",
+ "mapMembersAddPopup-title": "Select Wekan member",
"memberMenuPopup-title": "Member Settings",
"members": "Members",
"menu": "Menu",
"moveCardPopup-title": "Move Card",
"multi-selection": "Multi-Selection",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "My Boards",
"name": "Name",
"name": "Name",
@@ -152,10 +205,16 @@
"no-results": "No results",
"normal": "Normal",
"normal-desc": "Can view and edit cards. Can't change settings.",
+ "not-accepted-yet": "Invitation not accepted yet",
"optional": "optional",
+ "or": "or",
"page-maybe-private": "This page may be private. You may be able to view it by <a href='%s'>logging in</a>.",
"page-not-found": "Page not found.",
"password": "Password",
+ "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
+ "preview": "Preview",
+ "previewClipboardImagePopup-title": "Preview",
+ "previewAttachedImagePopup-title": "Preview",
"private": "Private",
"private-desc": "This board is private. Only people added to the board can view and edit it.",
"profile": "Profile",
@@ -175,12 +234,14 @@
"save": "Save",
"search": "Search",
"select-color": "Select a color",
+ "shortcut-assign-self": "Assign yourself to current card",
"shortcut-autocomplete-emojies": "Autocomplete emojies",
"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-toggle-filterbar": "Toggle Filter Sidebar",
"shortcut-toggle-sidebar": "Toggle Board Sidebar",
"signupPopup-title": "Create an Account",
"star-board-title": "Click to star this board. It will show up at top of your boards list.",
@@ -193,6 +254,7 @@
"title": "Title",
"unassign-member": "Unassign member",
"unsaved-description": "You have an unsaved description.",
+ "upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"username": "Username",
diff --git a/i18n/es.i18n.json b/i18n/es.i18n.json
index cd5bef66..466e52cd 100644
--- a/i18n/es.i18n.json
+++ b/i18n/es.i18n.json
@@ -1,4 +1,5 @@
{
+ "accept": "Accept",
"actions": "Acciones",
"activities": "Activities",
"activity": "Actividad",
@@ -7,25 +8,27 @@
"activity-attached": "adjuntado %s a %s",
"activity-created": "creado %s",
"activity-excluded": "excluido %s de %s",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
"activity-joined": "se ha unido %s",
"activity-moved": "movido %s de %s a %s",
"activity-on": "en %s",
"activity-removed": "eliminado %s de %s",
"activity-sent": "enviado %s a %s",
- "activity-unjoined": "ha dejado %s",
+ "activity-unjoined": "unjoined %s",
"add": "Añadir",
- "add-attachment": "Add an attachment",
+ "add-attachment": "Añadir un adjunto",
"add-board": "Añadir un nuevo tablero",
"add-card": "Add a card",
"add-cover": "Añadir cubierta",
"add-label": "Add the label",
"add-list": "Add a list",
- "add-members": "Add Members",
+ "add-members": "Añadir Miembros",
"added": "Añadido",
"addMemberPopup-title": "Miembros",
"admin": "Administrador",
"admin-desc": "Puedes ver y editar fichas, eliminar miembros, y cambiar los ajustes del tablero",
- "all-boards": "All boards",
+ "all-boards": "Tableros",
"and-n-other-card": "And __count__ other card",
"and-n-other-card_plural": "And __count__ other cards",
"archive": "Guardar",
@@ -34,7 +37,7 @@
"archive-card": "Archive Card",
"archive-list": "Archivar esta lista",
"archive-selection": "Archive selection",
- "archiveBoardPopup-title": "Cerrar el tablero",
+ "archiveBoardPopup-title": "¿Cerrar el tablero?",
"archived-items": "Items archivados",
"archives": "Archives",
"assign-member": "Assign member",
@@ -45,7 +48,7 @@
"attachments": "Adjuntos",
"avatar-too-big": "The avatar is too large (70Kb max)",
"back": "Atrás",
- "board-change-color": "Change color",
+ "board-change-color": "Cambiar color",
"board-nb-stars": "%s stars",
"board-not-found": "Tablero no encontrado",
"board-private-info": "This board will be <strong>private</strong>.",
@@ -53,6 +56,7 @@
"boardChangeColorPopup-title": "Change Board Background",
"boardChangeTitlePopup-title": "Renombrar tablero",
"boardChangeVisibilityPopup-title": "Cambiar visibilidad",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Board Menu",
"boards": "Tableros",
"bucket-example": "Like “Bucket List” for example",
@@ -74,32 +78,46 @@
"cardMembersPopup-title": "Miembros",
"cardMorePopup-title": "Más",
"cards": "Cards",
+ "change": "Change",
"change-avatar": "Cambiar Avatar",
"change-password": "Cambiar la clave",
- "change-permissions": "Change permissions",
+ "change-permissions": "Cambiar permisos",
"changeAvatarPopup-title": "Cambiar Avatar",
"changeLanguagePopup-title": "Cambiar idioma",
"changePasswordPopup-title": "Cambiar la clave",
"changePermissionsPopup-title": "Cambiar permisos",
"click-to-star": "Haz clic para destacar este tablero. ",
"click-to-unstar": "Haz clic para dejar de destacar este tablero. ",
+ "clipboard": "Clipboard or drag & drop",
"close": "Cerrar",
- "close-board": "Close Board",
+ "close-board": "Cerrar el tablero",
"close-board-pop": "Para reabrir el tablero haz clic en el menú \"Tableros\" de la cabecera, selecciona \"Ver Tableros Cerrados\", busca el tablero y haz clic en \"Reabrir\".",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
"comment": "Comentario",
- "comment-placeholder": "Write a comment",
+ "comment-placeholder": "Escribe un comentario",
"computer": "Ordenador",
"create": "Crear",
"createBoardPopup-title": "Crear tablero",
"createLabelPopup-title": "Crear etiqueta",
- "current": "current",
- "default-avatar": "Default avatar",
+ "current": "actual",
+ "decline": "Decline",
+ "default-avatar": "Avatar por defecto",
"delete": "Borrar",
"deleteLabelPopup-title": "Borrar etiqueta",
"description": "Descripcion",
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
"discard": "Discard",
+ "done": "Done",
"download": "Descargar",
"edit": "Editar",
"edit-avatar": "Cambiar Avatar",
@@ -107,6 +125,27 @@
"editLabelPopup-title": "Cambiar etiqueta",
"editProfilePopup-title": "Edit Profile",
"email": "Correo electrónico",
+ "email-enrollAccount-subject": "An account created for you on __url__",
+ "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",
+ "email-invalid": "Invalid email",
+ "email-invite": "Invite via Email",
+ "email-invite-subject": "__inviter__ sent you an invitation",
+ "email-invite-text": "Dear __user__,\n\n__inviter__ invites you to join board \"__board__\" for collaborations.\n\nPlease follow the link below:\n\n__url__\n\nThanks.",
+ "email-resetPassword-subject": "Reset your password on __url__",
+ "email-resetPassword-text": "Hello __user__,\n\nTo reset your password, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-verifyEmail-subject": "Verify your email address on __url__",
+ "email-verifyEmail-text": "Hello __user__,\n\nTo verify your account email, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-sent": "Email sent",
+ "error-board-doesNotExist": "This board does not exist",
+ "error-board-notAdmin": "You need to be admin of this board to do that",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data 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": "This action on self is not allowed",
+ "error-user-notCreated": "This user is not created",
"filter": "Filter",
"filter-cards": "Fichas de filtro",
"filter-clear": "Clear filter",
@@ -116,9 +155,19 @@
"fullname": "Nombre Completo",
"header-logo-title": "Volver a tu página de tableros",
"home": "Inicio",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
+ "import-show-user-mapping": "Review members mapping",
+ "import-user-select": "Pick the Wekan user you want to use as this member",
"info": "Informaciones",
"initials": "Initials",
"joined": "se ha unido",
+ "just-invited": "You are just invited to this board",
"keyboard-shortcuts": "Keyboard shortcuts",
"label-create": "Crear una etiqueta nueva ",
"label-default": "%s etiqueta (por Defecto)",
@@ -134,15 +183,19 @@
"list-select-cards": "Select all cards in this list",
"listActionPopup-title": "Acciones de la lista",
"listArchiveCardsPopup-title": "¿Archivar todas las fichas de esta lista?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "Trasladar todas las fichas de la lista",
"lists": "Lists",
"log-out": "Finalizar la sesion",
"loginPopup-title": "Iniciar sesion",
+ "mapMembersPopup-title": "Map members",
+ "mapMembersAddPopup-title": "Select Wekan member",
"memberMenuPopup-title": "Member Settings",
"members": "Miembros",
"menu": "Menu",
"moveCardPopup-title": "Move Card",
"multi-selection": "Multi-Selection",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "Mis tableros",
"name": "Nombre",
"no-archived-cards": "No archived cards.",
@@ -150,10 +203,16 @@
"no-results": "Sin resultados",
"normal": "Normal",
"normal-desc": "Puedes ver y editar fichas. No puedes cambiar la configuración.",
+ "not-accepted-yet": "Invitation not accepted yet",
"optional": "opcional",
+ "or": "or",
"page-maybe-private": "Esta página puede ser privada. Puedes verla por <a href='%s'>logging in</a>.",
"page-not-found": "Página no encontrada.",
"password": "Clave",
+ "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
+ "preview": "Preview",
+ "previewClipboardImagePopup-title": "Preview",
+ "previewAttachedImagePopup-title": "Preview",
"private": "Privado",
"private-desc": "Este tablero es privado. Sólo las personas añadidas al tablero pueden verlo y editarlo.",
"profile": "Perfil",
@@ -173,12 +232,14 @@
"save": "Guardar",
"search": "Buscar",
"select-color": "Selecciona un color",
+ "shortcut-assign-self": "Assign yourself to current card",
"shortcut-autocomplete-emojies": "Autocomplete emojies",
"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-toggle-filterbar": "Toggle Filter Sidebar",
"shortcut-toggle-sidebar": "Toggle Board Sidebar",
"signupPopup-title": "Crear una Cuenta",
"star-board-title": "Haz clic para destacar este tablero. Se mostrará en la parte superior de tu lista de tableros.",
@@ -191,6 +252,7 @@
"title": "Título",
"unassign-member": "Unassign member",
"unsaved-description": "You have an unsaved description.",
+ "upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"username": "Nombre de Usuario",
diff --git a/i18n/fi.i18n.json b/i18n/fi.i18n.json
index 2088e98a..1a0cdad5 100644
--- a/i18n/fi.i18n.json
+++ b/i18n/fi.i18n.json
@@ -1,4 +1,5 @@
{
+ "accept": "Accept",
"actions": "Toimet",
"activities": "Toimet",
"activity": "Toiminta",
@@ -7,12 +8,14 @@
"activity-attached": "liitetty %s kohteeseen %s",
"activity-created": "luotu %s",
"activity-excluded": "poistettu %s kohteesta %s",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
"activity-joined": "liitytty kohteeseen %s",
"activity-moved": "siirretty %s kohteesta %s kohteeseen %s",
"activity-on": "kohteessa %s",
"activity-removed": "poistettu %s kohteesta %s",
"activity-sent": "lähetetty %s kohteeseen %s",
- "activity-unjoined": "peruutettu liittyminen kohteeseen %s",
+ "activity-unjoined": "unjoined %s",
"add": "Lisää",
"add-attachment": "Lisää liitetiedosto",
"add-board": "Lisää uusi taulu",
@@ -26,8 +29,8 @@
"admin": "Ylläpitäjä",
"admin-desc": "Voi nähfä ja muokata kortteja, poistaa jäseniä, ja muuttaa taulun asetuksia.",
"all-boards": "Kaikki taulut",
- "and-n-other-card": "And __count__ other card",
- "and-n-other-card_plural": "And __count__ other cards",
+ "and-n-other-card": "Ja __count__ muu kortti",
+ "and-n-other-card_plural": "Ja __count__ muuta korttia",
"archive": "Arkistoi",
"archive-all": "Arkistoi kaikki",
"archive-board": "Arkistoi taulu",
@@ -53,6 +56,7 @@
"boardChangeColorPopup-title": "Vaihda taulun tausta",
"boardChangeTitlePopup-title": "Nimeä taulu uudelleen",
"boardChangeVisibilityPopup-title": "Vaihda näkyvyyttä",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Taulu valikko",
"boards": "Taulut",
"bucket-example": "Kuten “Laatikko lista” esimerkiksi",
@@ -74,6 +78,7 @@
"cardMembersPopup-title": "Jäsenet",
"cardMorePopup-title": "Lisää",
"cards": "Kortit",
+ "change": "Change",
"change-avatar": "Vaihda profiilikuva",
"change-password": "Vaihda salasana",
"change-permissions": "Muuta oikeuksia",
@@ -83,9 +88,20 @@
"changePermissionsPopup-title": "Vaihda oikeuksia",
"click-to-star": "Klikkaa merkataksesi tämä taulu tähdellä.",
"click-to-unstar": "Klikkaa poistaaksesi tähtimerkintä taululta.",
+ "clipboard": "Clipboard or drag & drop",
"close": "Sulje",
"close-board": "Sulje taulu",
"close-board-pop": "Voit uudelleenavata taulun klikkaamalla “Taulut” valikkoa ylätunnisteesta, valitsemalla “Näytä suljetut taulut”, löytämällä taulu ja klikkaamalla “Uudelleenavaa”.",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
"comment": "Kommentti",
"comment-placeholder": "Kirjoita kommentti",
"computer": "Tietokone",
@@ -93,6 +109,7 @@
"createBoardPopup-title": "Luo taulu",
"createLabelPopup-title": "Luo tunniste",
"current": "nykyinen",
+ "decline": "Decline",
"default-avatar": "Oletus profiilikuva",
"delete": "Poista",
"deleteLabelPopup-title": "Poista tunniste?",
@@ -100,6 +117,7 @@
"disambiguateMultiLabelPopup-title": "Yksikäsitteistä tunniste toiminta",
"disambiguateMultiMemberPopup-title": "Yksikäsitteistä jäsen toiminta",
"discard": "Hylkää",
+ "done": "Done",
"download": "Lataa",
"edit": "Muokkaa",
"edit-avatar": "Vaihda profiilikuva",
@@ -107,6 +125,27 @@
"editLabelPopup-title": "Vaihda tunniste",
"editProfilePopup-title": "Muokkaa profiilia",
"email": "Sähköposti",
+ "email-enrollAccount-subject": "An account created for you on __url__",
+ "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",
+ "email-invalid": "Invalid email",
+ "email-invite": "Invite via Email",
+ "email-invite-subject": "__inviter__ sent you an invitation",
+ "email-invite-text": "Dear __user__,\n\n__inviter__ invites you to join board \"__board__\" for collaborations.\n\nPlease follow the link below:\n\n__url__\n\nThanks.",
+ "email-resetPassword-subject": "Reset your password on __url__",
+ "email-resetPassword-text": "Hello __user__,\n\nTo reset your password, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-verifyEmail-subject": "Verify your email address on __url__",
+ "email-verifyEmail-text": "Hello __user__,\n\nTo verify your account email, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-sent": "Email sent",
+ "error-board-doesNotExist": "This board does not exist",
+ "error-board-notAdmin": "You need to be admin of this board to do that",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data 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": "This action on self is not allowed",
+ "error-user-notCreated": "This user is not created",
"filter": "Suodata",
"filter-cards": "Suodata kortit",
"filter-clear": "Poista suodatin",
@@ -116,9 +155,19 @@
"fullname": "Koko nimi",
"header-logo-title": "Palaa taulut sivullesi.",
"home": "Koti",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
+ "import-show-user-mapping": "Review members mapping",
+ "import-user-select": "Pick the Wekan user you want to use as this member",
"info": "Tietoja",
"initials": "Nimikirjaimet",
"joined": "liittyi",
+ "just-invited": "You are just invited to this board",
"keyboard-shortcuts": "Pikanäppäimet",
"label-create": "Luo uusi tunniste",
"label-default": "%s tunniste (oletus)",
@@ -134,15 +183,19 @@
"list-select-cards": "Valitse kaikki kortit tässä listassa",
"listActionPopup-title": "Listaa toimet",
"listArchiveCardsPopup-title": "Arkistoi kaikki kortit tässä listassa?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "Siirrä kaikki listan kortit",
"lists": "Listat",
"log-out": "Kirjaudu ulos",
"loginPopup-title": "Kirjaudu sisään",
+ "mapMembersPopup-title": "Map members",
+ "mapMembersAddPopup-title": "Select Wekan member",
"memberMenuPopup-title": "Jäsen asetukset",
"members": "Jäsenet",
"menu": "Valikko",
"moveCardPopup-title": "Siirrä kortti",
"multi-selection": "Monivalinta",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "Tauluni",
"name": "Nimi",
"no-archived-cards": "Ei arkistoituja kortteja.",
@@ -150,10 +203,16 @@
"no-results": "Ei tuloksia",
"normal": "Normaali",
"normal-desc": "Voi nähdä ja muokata kortteja. Ei voi muokata asetuksia.",
+ "not-accepted-yet": "Invitation not accepted yet",
"optional": "valinnainen",
+ "or": "or",
"page-maybe-private": "Tämä sivu voi olla yksityinen. Voit ehkä pystyä näkemään sen <a href='%s'>kirjautumalla sisään</a>.",
"page-not-found": "Sivua ei löytynyt.",
"password": "Salasana",
+ "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
+ "preview": "Preview",
+ "previewClipboardImagePopup-title": "Preview",
+ "previewAttachedImagePopup-title": "Preview",
"private": "Yksityinen",
"private-desc": "Tämä taulu on yksityinen. Vain taululle lisätyt henkilöt voivat nähdä ja muokata sitä.",
"profile": "Profiili",
@@ -173,13 +232,15 @@
"save": "Tallenna",
"search": "Etsi",
"select-color": "Valitse väri",
+ "shortcut-assign-self": "Assign yourself to current card",
"shortcut-autocomplete-emojies": "Automaattinen täydennys emojille",
"shortcut-autocomplete-members": "Automaattinen täydennys jäsenille",
"shortcut-clear-filters": "Poista kaikki suodattimet",
"shortcut-close-dialog": "Sulje valintaikkuna",
"shortcut-filter-my-cards": "Suodata korttini",
"shortcut-show-shortcuts": "Tuo esiin tämä pikavalinta lista",
- "shortcut-toggle-sidebar": "Toggle Board Sidebar",
+ "shortcut-toggle-filterbar": "Toggle Filter Sidebar",
+ "shortcut-toggle-sidebar": "Vaihda taulu sivupalkin näkyvyys",
"signupPopup-title": "Luo tili",
"star-board-title": "Klikkaa merkataksesi taulu tähdellä. Se tulee näkymään ylimpänä taululistallasi.",
"starred-boards": "Tähdellä merkatut taulut",
@@ -191,6 +252,7 @@
"title": "Otsikko",
"unassign-member": "Peru jäsenvalinta",
"unsaved-description": "Sinulla on tallentamaton kuvaus.",
+ "upload": "Upload",
"upload-avatar": "Lähetä profiilikuva",
"uploaded-avatar": "Profiilikuva lähetetty",
"username": "Käyttäjänimi",
diff --git a/i18n/fr.i18n.json b/i18n/fr.i18n.json
index a63fb0ce..eabd58d2 100644
--- a/i18n/fr.i18n.json
+++ b/i18n/fr.i18n.json
@@ -7,6 +7,8 @@
"activity-attached": "a attaché %s à %s",
"activity-created": "a créé %s",
"activity-excluded": "a exclu %s de %s",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
"activity-joined": "a rejoint %s",
"activity-moved": "a déplacé %s depuis %s vers %s",
"activity-on": "sur %s",
@@ -33,7 +35,7 @@
"archive-board": "Archiver le tableau",
"archive-card": "Archiver la carte",
"archive-list": "Archiver cette liste",
- "archive-selection": "Archiver la selection ",
+ "archive-selection": "Archiver la selection",
"archiveBoardPopup-title": "Fermer le tableau ?",
"archived-items": "Éléments archivés",
"archives": "Archives",
@@ -53,6 +55,7 @@
"boardChangeColorPopup-title": "Change la fond du tableau",
"boardChangeTitlePopup-title": "Renommer le tableau",
"boardChangeVisibilityPopup-title": "Changer la visibilité",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Menu du tableau",
"boards": "Tableaux",
"bucket-example": "Comme « todo list » par exemple",
@@ -74,6 +77,7 @@
"cardMembersPopup-title": "Membres",
"cardMorePopup-title": "Plus",
"cards": "Cartes",
+ "change": "Change",
"change-avatar": "Changer l'avatar",
"change-password": "Changer le mot de passe",
"change-permissions": "Changer les permissions",
@@ -86,6 +90,16 @@
"close": "Fermer",
"close-board": "Fermer le tableau",
"close-board-pop": "Vous pouvez ré-ouvrir le tableau en cliquant sur le menu « Tableau » dans la barre d'en-tête, puis en sélection « Voir les tableaux fermés », en trouvant le tableau désiré puis en cliquant sur « Ré-ouvrir ».",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
"comment": "Commentaire",
"comment-placeholder": "Rédiger un commentaire",
"computer": "Ordinateur",
@@ -107,6 +121,10 @@
"editLabelPopup-title": "Changer l'étiquette",
"editProfilePopup-title": "Éditer le profil",
"email": "Email",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data does not include the proper information in the correct format",
+ "error-list-doesNotExist": "This list does not exist",
"filter": "Filtrer",
"filter-cards": "Filtrer les cartes",
"filter-clear": "Retirer les filtres",
@@ -116,6 +134,12 @@
"fullname": "Nom complet",
"header-logo-title": "Retourner à la page des tableaux",
"home": "Accueil",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
"info": "Infos",
"initials": "Initiales",
"joined": "a joint",
@@ -124,7 +148,7 @@
"label-default": "%s label (default)",
"label-delete-pop": "Cette action est irréversible. Elle supprimera cette étiquette de toutes les cartes ainsi que l'historique associé.",
"labels": "Étiquettes",
- "language": "Langage ",
+ "language": "Langage",
"last-admin-desc": "Vous ne pouvez pas changer les rôles car il doit y avoir au moins un admin.",
"leave-board": "Quitter le tableau",
"link-card": "Lier cette carte",
@@ -134,6 +158,7 @@
"list-select-cards": "Sélectionner les cartes de cette liste",
"listActionPopup-title": "Liste des actions",
"listArchiveCardsPopup-title": "Archiver les cartes de la liste ?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "Déplacer les cartes de la liste",
"lists": "Listes",
"log-out": "Déconnexion",
@@ -143,6 +168,7 @@
"menu": "Menu",
"moveCardPopup-title": "Déplacer la carte",
"multi-selection": "Sélection multiple",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "Mes tableaux",
"name": "Nom",
"no-archived-cards": "Pas de carte archivée.",
@@ -151,6 +177,7 @@
"normal": "Normal",
"normal-desc": "Peut voir et éditer les cartes. Ne peut pas changer les paramètres.",
"optional": "optionnel",
+ "or": "or",
"page-maybe-private": "Cette page est peut-être privée. Vous pourrez peut-être la voir en vous <a href='%s'>connectant</a>.",
"page-not-found": "Page non trouvée",
"password": "Mot de passe",
@@ -173,12 +200,14 @@
"save": "Sauvegarder",
"search": "Chercher",
"select-color": "Choisissez une couleur",
+ "shortcut-assign-self": "Assign yourself to current card",
"shortcut-autocomplete-emojies": "Auto-complétion des emojies",
"shortcut-autocomplete-members": "Auto-complétion des membres",
"shortcut-clear-filters": "Retirer tous les filtres",
"shortcut-close-dialog": "Fermer le dialogue",
"shortcut-filter-my-cards": "Filtrer mes cartes",
"shortcut-show-shortcuts": "Afficher cette liste de raccourcis",
+ "shortcut-toggle-filterbar": "Toggle Filter Sidebar",
"shortcut-toggle-sidebar": "Afficher/Cacher la barre latérale du tableau",
"signupPopup-title": "Créer un compe",
"star-board-title": "Cliquer pour ajouter ce tableau aux favoris. Il sera affiché en haut de votre liste de tableaux.",
diff --git a/i18n/it.i18n.json b/i18n/it.i18n.json
new file mode 100644
index 00000000..1291e0df
--- /dev/null
+++ b/i18n/it.i18n.json
@@ -0,0 +1,229 @@
+{
+ "actions": "Azioni",
+ "activities": "Attività",
+ "activity": "Attività",
+ "activity-added": "aggiunto %s a %s",
+ "activity-archived": "archiviato %s",
+ "activity-attached": "allegato %s a %s",
+ "activity-created": "creato %s",
+ "activity-excluded": "escluso %s da %s",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
+ "activity-joined": "è stato unito %s",
+ "activity-moved": "spostato %s da %s a %s",
+ "activity-on": "su %s",
+ "activity-removed": "rimosso %s da %s",
+ "activity-sent": "inviato %s a %s",
+ "activity-unjoined": "unjoined %s",
+ "add": "Aggiungere",
+ "add-attachment": "Aggiungi allegato",
+ "add-board": "Aggiungi una nuova bachecha",
+ "add-card": "Aggiungi una scheda",
+ "add-cover": "Aggiungi copertina",
+ "add-label": "Aggiungi l'etichetta",
+ "add-list": "Aggiungi una lista",
+ "add-members": "Aggiungi membri",
+ "added": "Aggiunto",
+ "addMemberPopup-title": "Membri",
+ "admin": "Amministratore",
+ "admin-desc": "Può vedere e modificare schede, rimuovere membri e modificare le impostazioni della bacheca.",
+ "all-boards": "Tutte le bacheche",
+ "and-n-other-card": "E __count__ altra scheda",
+ "and-n-other-card_plural": "E __count__ altre schede",
+ "archive": "Archivia",
+ "archive-all": "Archivia tutto",
+ "archive-board": "Archivia bacheca",
+ "archive-card": "Archivia scheda",
+ "archive-list": "Archivia questa lista",
+ "archive-selection": "Archivia selezione",
+ "archiveBoardPopup-title": "Chiudere la bacheca?",
+ "archived-items": "Elementi archiviati",
+ "archives": "Archives",
+ "assign-member": "Assegna membri",
+ "attached": "allegato",
+ "attachment": "Allegato",
+ "attachment-delete-pop": "L'eliminazione di un allegato è permanente. Non è possibile annullare.",
+ "attachmentDeletePopup-title": "Eliminare l'allegato?",
+ "attachments": "Allegati",
+ "avatar-too-big": "L'avatar è troppo grande (max 70Kb)",
+ "back": "Indietro",
+ "board-change-color": "Cambia colore",
+ "board-nb-stars": "%s stelle",
+ "board-not-found": "Bacheca non trovata",
+ "board-private-info": "Questa bacheca sarà <strong>privata</strong>.",
+ "board-public-info": "Questa bacheca sarà <strong>pubblica</strong>.",
+ "boardChangeColorPopup-title": "Cambia sfondo della bacheca",
+ "boardChangeTitlePopup-title": "Rinomina bacheca",
+ "boardChangeVisibilityPopup-title": "Cambia visibilità",
+ "boardImportBoardPopup-title": "Import board from Trello",
+ "boardMenuPopup-title": "Menu bacheca",
+ "boards": "Bacheche",
+ "bucket-example": "Like “Bucket List” for example",
+ "cancel": "Cancella",
+ "card-archived": "Questa scheda è archiviata.",
+ "card-comments-title": "Questa scheda ha %s commenti.",
+ "card-delete-notice": "L'eliminazione è permanente. Tutte le azioni associate a questa scheda andranno perse.",
+ "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 archive a card to remove it from the board and preserve the activity.",
+ "card-edit-attachments": "Edit attachments",
+ "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.",
+ "cardAttachmentsPopup-title": "Attach From",
+ "cardDeletePopup-title": "Delete Card?",
+ "cardDetailsActionsPopup-title": "Card Actions",
+ "cardLabelsPopup-title": "Labels",
+ "cardMembersPopup-title": "Membr",
+ "cardMorePopup-title": "More",
+ "cards": "Cards",
+ "change": "Change",
+ "change-avatar": "Change Avatar",
+ "change-password": "Change Password",
+ "change-permissions": "Change permissions",
+ "changeAvatarPopup-title": "Change Avatar",
+ "changeLanguagePopup-title": "Change Language",
+ "changePasswordPopup-title": "Change Password",
+ "changePermissionsPopup-title": "Change Permissions",
+ "click-to-star": "Click to star this board.",
+ "click-to-unstar": "Click to unstar this board.",
+ "close": "Close",
+ "close-board": "Close Board",
+ "close-board-pop": "You can re-open the board by clicking the “Boards” menu from the header, selecting “View Closed Boards”, finding the board and clicking “Re-open”.",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
+ "comment": "Comment",
+ "comment-placeholder": "Write a comment",
+ "computer": "Computer",
+ "create": "Create",
+ "createBoardPopup-title": "Create Board",
+ "createLabelPopup-title": "Create Label",
+ "current": "current",
+ "default-avatar": "Default avatar",
+ "delete": "Delete",
+ "deleteLabelPopup-title": "Delete Label?",
+ "description": "Description",
+ "disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
+ "disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
+ "discard": "Discard",
+ "download": "Download",
+ "edit": "Edit",
+ "edit-avatar": "Change Avatar",
+ "edit-profile": "Edit Profile",
+ "editLabelPopup-title": "Change Label",
+ "editProfilePopup-title": "Edit Profile",
+ "email": "Email",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data does not include the proper information in the correct format",
+ "error-list-doesNotExist": "This list does not exist",
+ "filter": "Filter",
+ "filter-cards": "Filter Cards",
+ "filter-clear": "Clear filter",
+ "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",
+ "fullname": "Full Name",
+ "header-logo-title": "Go back to your boards page.",
+ "home": "Home",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "info": "Infos",
+ "initials": "Initials",
+ "joined": "joined",
+ "keyboard-shortcuts": "Keyboard shortcuts",
+ "label-create": "Create a new 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",
+ "link-card": "Link to this card",
+ "list-archive-cards": "Archive all cards in this list",
+ "list-archive-cards-pop": "This will remove all the cards in this list from the board. To view archived cards and bring them back to the board, click “Menu” > “Archived Items”.",
+ "list-move-cards": "Move all cards in this list",
+ "list-select-cards": "Select all cards in this list",
+ "listActionPopup-title": "List Actions",
+ "listArchiveCardsPopup-title": "Archive All Cards in this List?",
+ "listImportCardPopup-title": "Import a Trello card",
+ "listMoveCardsPopup-title": "Move All Cards in List",
+ "lists": "Lists",
+ "log-out": "Log Out",
+ "loginPopup-title": "Log In",
+ "memberMenuPopup-title": "Member Settings",
+ "members": "Membr",
+ "menu": "Menu",
+ "moveCardPopup-title": "Move Card",
+ "multi-selection": "Multi-Selection",
+ "multi-selection-on": "Multi-Selection is on",
+ "my-boards": "My Boards",
+ "name": "Name",
+ "no-archived-cards": "No archived cards.",
+ "no-archived-lists": "No archived lists.",
+ "no-results": "No results",
+ "normal": "Normal",
+ "normal-desc": "Can view and edit cards. Can't change settings.",
+ "optional": "optional",
+ "or": "or",
+ "page-maybe-private": "This page may be private. You may be able to view it by <a href='%s'>logging in</a>.",
+ "page-not-found": "Page not found.",
+ "password": "Password",
+ "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 the label",
+ "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": "Rinomina bacheca",
+ "restore": "Restore",
+ "save": "Save",
+ "search": "Search",
+ "select-color": "Select a color",
+ "shortcut-assign-self": "Assign yourself to current card",
+ "shortcut-autocomplete-emojies": "Autocomplete emojies",
+ "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-toggle-filterbar": "Toggle Filter Sidebar",
+ "shortcut-toggle-sidebar": "Toggle Board 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",
+ "title": "Title",
+ "unassign-member": "Unassign member",
+ "unsaved-description": "You have an unsaved description.",
+ "upload-avatar": "Upload an avatar",
+ "uploaded-avatar": "Uploaded an avatar",
+ "username": "Username",
+ "view-it": "View it",
+ "warn-list-archived": "warning: this card is in an archived list",
+ "what-to-do": "What do you want to do?"
+} \ No newline at end of file
diff --git a/i18n/ja.i18n.json b/i18n/ja.i18n.json
index a6594f41..c346adfa 100644
--- a/i18n/ja.i18n.json
+++ b/i18n/ja.i18n.json
@@ -1,4 +1,5 @@
{
+ "accept": "Accept",
"actions": "操作",
"activities": "Activities",
"activity": "アクティビティ",
@@ -7,12 +8,14 @@
"activity-attached": "%s を %s に添付しました",
"activity-created": "%s を作成しました",
"activity-excluded": "%s を %s から除外しました",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s 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-unjoined": "unjoined %s",
"add": "追加",
"add-attachment": "Add an attachment",
"add-board": "ボード追加",
@@ -53,6 +56,7 @@
"boardChangeColorPopup-title": "Change Board Background",
"boardChangeTitlePopup-title": "ボード名の変更",
"boardChangeVisibilityPopup-title": "公開範囲の変更",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Board Menu",
"boards": "ボード",
"bucket-example": "Like “Bucket List” for example",
@@ -74,6 +78,7 @@
"cardMembersPopup-title": "メンバー",
"cardMorePopup-title": "さらに見る",
"cards": "Cards",
+ "change": "Change",
"change-avatar": "アバターの変更",
"change-password": "パスワードの変更",
"change-permissions": "Change permissions",
@@ -83,9 +88,20 @@
"changePermissionsPopup-title": "パーミッションの変更",
"click-to-star": "ボードにスターをつける",
"click-to-unstar": "ボードからスターを外す",
+ "clipboard": "Clipboard or drag & drop",
"close": "閉じる",
"close-board": "Close Board",
"close-board-pop": "ヘッダーの\"ボード\"メニューから\"閉じたボードを見る\"を選択し、そこでボードを選択して、\"ボードの再開\"をクリックすると、ボードを再度利用できるようになります。",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
"comment": "コメント",
"comment-placeholder": "Write a comment",
"computer": "コンピューター",
@@ -93,6 +109,7 @@
"createBoardPopup-title": "ボードの作成",
"createLabelPopup-title": "ラベルの作成",
"current": "current",
+ "decline": "Decline",
"default-avatar": "Default avatar",
"delete": "削除",
"deleteLabelPopup-title": "ラベルを削除しますか?",
@@ -100,6 +117,7 @@
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
"discard": "Discard",
+ "done": "Done",
"download": "ダウンロード",
"edit": "編集",
"edit-avatar": "アバターの変更",
@@ -107,6 +125,27 @@
"editLabelPopup-title": "ラベルの変更",
"editProfilePopup-title": "Edit Profile",
"email": "メールアドレス",
+ "email-enrollAccount-subject": "An account created for you on __url__",
+ "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",
+ "email-invalid": "Invalid email",
+ "email-invite": "Invite via Email",
+ "email-invite-subject": "__inviter__ sent you an invitation",
+ "email-invite-text": "Dear __user__,\n\n__inviter__ invites you to join board \"__board__\" for collaborations.\n\nPlease follow the link below:\n\n__url__\n\nThanks.",
+ "email-resetPassword-subject": "Reset your password on __url__",
+ "email-resetPassword-text": "Hello __user__,\n\nTo reset your password, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-verifyEmail-subject": "Verify your email address on __url__",
+ "email-verifyEmail-text": "Hello __user__,\n\nTo verify your account email, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-sent": "Email sent",
+ "error-board-doesNotExist": "This board does not exist",
+ "error-board-notAdmin": "You need to be admin of this board to do that",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data 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": "This action on self is not allowed",
+ "error-user-notCreated": "This user is not created",
"filter": "Filter",
"filter-cards": "カードをフィルターする",
"filter-clear": "Clear filter",
@@ -116,9 +155,19 @@
"fullname": "フルネーム",
"header-logo-title": "自分のボードページに戻る。",
"home": "ホーム",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
+ "import-show-user-mapping": "Review members mapping",
+ "import-user-select": "Pick the Wekan user you want to use as this member",
"info": "情報",
"initials": "Initials",
"joined": "参加しました",
+ "just-invited": "You are just invited to this board",
"keyboard-shortcuts": "Keyboard shortcuts",
"label-create": "ラベル作成",
"label-default": "%s ラベル(デフォルト)",
@@ -134,15 +183,19 @@
"list-select-cards": "Select all cards in this list",
"listActionPopup-title": "操作一覧",
"listArchiveCardsPopup-title": "このリスト内の善カードをアーカイブしますか?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "リスト内のすべてのカードを移動する",
"lists": "Lists",
"log-out": "ログアウト",
"loginPopup-title": "ログイン",
+ "mapMembersPopup-title": "Map members",
+ "mapMembersAddPopup-title": "Select Wekan member",
"memberMenuPopup-title": "Member Settings",
"members": "メンバー",
"menu": "メニュー",
"moveCardPopup-title": "Move Card",
"multi-selection": "Multi-Selection",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "自分のボード",
"name": "名前",
"no-archived-cards": "No archived cards.",
@@ -150,10 +203,16 @@
"no-results": "該当するものはありません",
"normal": "通常",
"normal-desc": "カードの閲覧と編集が可能。設定変更不可。",
+ "not-accepted-yet": "Invitation not accepted yet",
"optional": "任意",
+ "or": "or",
"page-maybe-private": "このページはプライベートです。<a href='%s'>ログイン</a>して見てください。",
"page-not-found": "ページが見つかりません。",
"password": "パスワード",
+ "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
+ "preview": "Preview",
+ "previewClipboardImagePopup-title": "Preview",
+ "previewAttachedImagePopup-title": "Preview",
"private": "プライベート",
"private-desc": "このボードはプライベートです。ボードメンバーのみが閲覧・編集可能です。",
"profile": "プロフィール",
@@ -173,12 +232,14 @@
"save": "保存",
"search": "検索",
"select-color": "色を選択",
+ "shortcut-assign-self": "Assign yourself to current card",
"shortcut-autocomplete-emojies": "Autocomplete emojies",
"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-toggle-filterbar": "Toggle Filter Sidebar",
"shortcut-toggle-sidebar": "Toggle Board Sidebar",
"signupPopup-title": "アカウント作成",
"star-board-title": "ボードにスターをつけると自分のボード一覧のトップに表示されます。",
@@ -191,6 +252,7 @@
"title": "タイトル",
"unassign-member": "Unassign member",
"unsaved-description": "You have an unsaved description.",
+ "upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"username": "ユーザー名",
diff --git a/i18n/ko.i18n.json b/i18n/ko.i18n.json
index c43dd0e6..3baf3096 100644
--- a/i18n/ko.i18n.json
+++ b/i18n/ko.i18n.json
@@ -1,4 +1,5 @@
{
+ "accept": "Accept",
"actions": "동작",
"activities": "Activities",
"activity": "활동 상태",
@@ -7,12 +8,14 @@
"activity-attached": "%s를 %s에 첨부함",
"activity-created": "%s 생성됨",
"activity-excluded": "%s를 %s에서 제외함",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s 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-unjoined": "unjoined %s",
"add": "추가",
"add-attachment": "Add an attachment",
"add-board": "새로운 보드를 추가",
@@ -53,6 +56,7 @@
"boardChangeColorPopup-title": "Change Board Background",
"boardChangeTitlePopup-title": "보드 이름 바꾸기",
"boardChangeVisibilityPopup-title": "표시 여부 변경",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Board Menu",
"boards": "보드",
"bucket-example": "Like “Bucket List” for example",
@@ -74,6 +78,7 @@
"cardMembersPopup-title": "멤버",
"cardMorePopup-title": "더보기",
"cards": "Cards",
+ "change": "Change",
"change-avatar": "아바타 변경",
"change-password": "암호 변경",
"change-permissions": "Change permissions",
@@ -83,9 +88,20 @@
"changePermissionsPopup-title": "권한 변경",
"click-to-star": "보드 별 추가.",
"click-to-unstar": "보드 별 삭제.",
+ "clipboard": "Clipboard or drag & drop",
"close": "닫기",
"close-board": "Close Board",
"close-board-pop": "보드를 다시 열 수 있습니다. 상단 \"보드\" 메뉴를 클릭해 \"닫힌 보드 보기\"를 선택하여, 보드를 찾아 \"다시 열기\" 버튼을 클릭합니다.",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
"comment": "코멘트",
"comment-placeholder": "Write a comment",
"computer": "내 컴퓨터",
@@ -93,6 +109,7 @@
"createBoardPopup-title": "보드 생성",
"createLabelPopup-title": "라벨 생성",
"current": "current",
+ "decline": "Decline",
"default-avatar": "Default avatar",
"delete": "삭제",
"deleteLabelPopup-title": "라벨을 삭제합니까?",
@@ -100,6 +117,7 @@
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
"discard": "Discard",
+ "done": "Done",
"download": "다운로드",
"edit": "수정",
"edit-avatar": "아바타 변경",
@@ -107,6 +125,27 @@
"editLabelPopup-title": "라벨 변경",
"editProfilePopup-title": "Edit Profile",
"email": "이메일",
+ "email-enrollAccount-subject": "An account created for you on __url__",
+ "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",
+ "email-invalid": "Invalid email",
+ "email-invite": "Invite via Email",
+ "email-invite-subject": "__inviter__ sent you an invitation",
+ "email-invite-text": "Dear __user__,\n\n__inviter__ invites you to join board \"__board__\" for collaborations.\n\nPlease follow the link below:\n\n__url__\n\nThanks.",
+ "email-resetPassword-subject": "Reset your password on __url__",
+ "email-resetPassword-text": "Hello __user__,\n\nTo reset your password, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-verifyEmail-subject": "Verify your email address on __url__",
+ "email-verifyEmail-text": "Hello __user__,\n\nTo verify your account email, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-sent": "Email sent",
+ "error-board-doesNotExist": "This board does not exist",
+ "error-board-notAdmin": "You need to be admin of this board to do that",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data 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": "This action on self is not allowed",
+ "error-user-notCreated": "This user is not created",
"filter": "Filter",
"filter-cards": "카드 필터",
"filter-clear": "Clear filter",
@@ -116,9 +155,19 @@
"fullname": "전체 이름",
"header-logo-title": "보드 페이지로 돌아가기.",
"home": "홈",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
+ "import-show-user-mapping": "Review members mapping",
+ "import-user-select": "Pick the Wekan user you want to use as this member",
"info": "정보",
"initials": "Initials",
"joined": "참가함",
+ "just-invited": "You are just invited to this board",
"keyboard-shortcuts": "Keyboard shortcuts",
"label-create": "새로운 라벨 생성",
"label-default": "%s 라벨 (기본)",
@@ -134,15 +183,19 @@
"list-select-cards": "Select all cards in this list",
"listActionPopup-title": "동작 목록",
"listArchiveCardsPopup-title": "목록에서 모든 카드를 보관합니까?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "목록에서 모든 카드 이동",
"lists": "Lists",
"log-out": "로그아웃",
"loginPopup-title": "로그인",
+ "mapMembersPopup-title": "Map members",
+ "mapMembersAddPopup-title": "Select Wekan member",
"memberMenuPopup-title": "Member Settings",
"members": "멤버",
"menu": "메뉴",
"moveCardPopup-title": "Move Card",
"multi-selection": "Multi-Selection",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "내 보드",
"name": "이름",
"no-archived-cards": "No archived cards.",
@@ -150,10 +203,16 @@
"no-results": "결과 값 없음",
"normal": "표준",
"normal-desc": "카드를 보거나 수정할 수 있습니다. 설정값은 변경할 수 없습니다.",
+ "not-accepted-yet": "Invitation not accepted yet",
"optional": "옴션",
+ "or": "or",
"page-maybe-private": "이 페이지를 비공개일 수 있습니다. 이것을 보고 싶으면 <a href='%s'>로그인</a>을 하십시오.",
"page-not-found": "페이지를 찾지 못 했습니다",
"password": "암호",
+ "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
+ "preview": "Preview",
+ "previewClipboardImagePopup-title": "Preview",
+ "previewAttachedImagePopup-title": "Preview",
"private": "비공개",
"private-desc": "비공개된 보드입니다. 오직 보드에 추가된 사람들만 보고 수정할 수 있습니다",
"profile": "프로파일",
@@ -173,12 +232,14 @@
"save": "저장",
"search": "검색",
"select-color": "색 선택",
+ "shortcut-assign-self": "Assign yourself to current card",
"shortcut-autocomplete-emojies": "Autocomplete emojies",
"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-toggle-filterbar": "Toggle Filter Sidebar",
"shortcut-toggle-sidebar": "Toggle Board Sidebar",
"signupPopup-title": "계정 생성",
"star-board-title": "보드에 별을 클릭합니다. 보드 목록에서 최상위로 둘 수 있습니다.",
@@ -191,6 +252,7 @@
"title": "제목",
"unassign-member": "Unassign member",
"unsaved-description": "You have an unsaved description.",
+ "upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"username": "사용자 이름",
diff --git a/i18n/pt-BR.i18n.json b/i18n/pt-BR.i18n.json
index 381dc9b5..0a9e46e1 100644
--- a/i18n/pt-BR.i18n.json
+++ b/i18n/pt-BR.i18n.json
@@ -1,4 +1,5 @@
{
+ "accept": "Accept",
"actions": "Ações",
"activities": "Atividades",
"activity": "Atividade",
@@ -7,12 +8,14 @@
"activity-attached": "anexou %s a %s",
"activity-created": "criou %s",
"activity-excluded": "excluiu %s de %s",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
"activity-joined": "juntou-se a %s",
"activity-moved": "moveu %s de %s para %s",
"activity-on": "em %s",
"activity-removed": "removeu %s de %s",
"activity-sent": "enviou %s de %s",
- "activity-unjoined": "deixou %s",
+ "activity-unjoined": "unjoined %s",
"add": "Novo",
"add-attachment": "Adicionar um anexo",
"add-board": "Criar um quadro novo",
@@ -26,8 +29,8 @@
"admin": "Administrador",
"admin-desc": "Pode ver e editar cartões, remover membros e alterar configurações do quadro.",
"all-boards": "Todos os quadros",
- "and-n-other-card": "And __count__ other card",
- "and-n-other-card_plural": "And __count__ other cards",
+ "and-n-other-card": "E __count__ outro cartão",
+ "and-n-other-card_plural": "E __count__ outros cartões",
"archive": "Arquivar",
"archive-all": "Arquivar Tudo",
"archive-board": "Arquivar Quadro",
@@ -43,25 +46,26 @@
"attachment-delete-pop": "Excluir um anexo é permanente. Não será possível recuperá-lo.",
"attachmentDeletePopup-title": "Excluir Anexo?",
"attachments": "Anexos",
- "avatar-too-big": "The avatar is too large (70Kb max)",
+ "avatar-too-big": "Imagem de avatar muito grande (máx 70KB)",
"back": "Voltar",
"board-change-color": "Alterar cor",
- "board-nb-stars": "%s stars",
+ "board-nb-stars": "%s estrelas",
"board-not-found": "Quadro não encontrado",
"board-private-info": "Este quadro será <strong>privado</strong>.",
"board-public-info": "Este quadro será <strong>público</strong>.",
"boardChangeColorPopup-title": "Alterar Tela de Fundo",
"boardChangeTitlePopup-title": "Renomear Quadro",
"boardChangeVisibilityPopup-title": "Alterar Visibilidade",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Menu do Quadro",
"boards": "Quadros",
- "bucket-example": "Like “Bucket List” for example",
+ "bucket-example": "\"Bucket List\", por exemplo",
"cancel": "Cancelar",
"card-archived": "Este cartão está arquivado.",
"card-comments-title": "Este cartão possui %s comentários.",
"card-delete-notice": "A exclusão será permanente. Você perderá todas as ações associadas a este cartão.",
- "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 archive a card to remove it from the board and preserve the activity.",
+ "card-delete-pop": "Todas as ações serão removidas da lista de Atividades e vocês não poderá re-abrir o cartão. Não há como desfazer.",
+ "card-delete-suggest-archive": "Você pode arquivar um cartão para removê-lo do quadro e preservar suas atividades.",
"card-edit-attachments": "Editar anexos",
"card-edit-labels": "Editar etiquetas",
"card-edit-members": "Editar membros",
@@ -69,11 +73,12 @@
"card-members-title": "Acrescentar ou remover membros do quadro deste cartão.",
"cardAttachmentsPopup-title": "Anexar a partir de",
"cardDeletePopup-title": "Excluir Cartão?",
- "cardDetailsActionsPopup-title": "Card Actions",
+ "cardDetailsActionsPopup-title": "Ações do cartão",
"cardLabelsPopup-title": "Etiquetas",
"cardMembersPopup-title": "Membros",
"cardMorePopup-title": "Mais",
"cards": "Cartões",
+ "change": "Change",
"change-avatar": "Alterar Avatar",
"change-password": "Alterar Senha",
"change-permissions": "Alterar permissões",
@@ -83,23 +88,36 @@
"changePermissionsPopup-title": "Alterar Permissões",
"click-to-star": "Marcar quadro como favorito.",
"click-to-unstar": "Remover quadro dos favoritos.",
+ "clipboard": "Clipboard or drag & drop",
"close": "Fechar",
"close-board": "Fechar Quadro",
"close-board-pop": "Você pode reabrir um quadro clicando em “Quadros” no menu no cabeçalho, selecionando “Exibir Quadros Fechados”, encontrando-o e clicando em “Reabrir”.",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
"comment": "Comentário",
"comment-placeholder": "Escrever um comentário",
"computer": "Computador",
"create": "Criar",
"createBoardPopup-title": "Criar Quadro",
"createLabelPopup-title": "Criar Etiqueta",
- "current": "current",
- "default-avatar": "Default avatar",
+ "current": "atual",
+ "decline": "Decline",
+ "default-avatar": "Avatar padrão",
"delete": "Excluir",
"deleteLabelPopup-title": "Excluir Etiqueta?",
"description": "Descrição",
- "disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
- "disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
- "discard": "Discard",
+ "disambiguateMultiLabelPopup-title": "Desambiguar ações de etiquetas",
+ "disambiguateMultiMemberPopup-title": "Desambiguar ações de membros",
+ "discard": "Descartar",
+ "done": "Done",
"download": "Baixar",
"edit": "Editar",
"edit-avatar": "Alterar Avatar",
@@ -107,19 +125,50 @@
"editLabelPopup-title": "Alterar Etiqueta",
"editProfilePopup-title": "Editar Perfil",
"email": "E-mail",
+ "email-enrollAccount-subject": "An account created for you on __url__",
+ "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",
+ "email-invalid": "Invalid email",
+ "email-invite": "Invite via Email",
+ "email-invite-subject": "__inviter__ sent you an invitation",
+ "email-invite-text": "Dear __user__,\n\n__inviter__ invites you to join board \"__board__\" for collaborations.\n\nPlease follow the link below:\n\n__url__\n\nThanks.",
+ "email-resetPassword-subject": "Reset your password on __url__",
+ "email-resetPassword-text": "Hello __user__,\n\nTo reset your password, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-verifyEmail-subject": "Verify your email address on __url__",
+ "email-verifyEmail-text": "Hello __user__,\n\nTo verify your account email, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-sent": "Email sent",
+ "error-board-doesNotExist": "This board does not exist",
+ "error-board-notAdmin": "You need to be admin of this board to do that",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data 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": "This action on self is not allowed",
+ "error-user-notCreated": "This user is not created",
"filter": "Filtrar",
"filter-cards": "Filtrar Cartões",
"filter-clear": "Limpar filtro",
"filter-on": "Filtro está ativo",
"filter-on-desc": "Você está filtrando cartões neste quadro. Clique aqui para editar o filtro.",
- "filter-to-selection": "Filter to selection",
+ "filter-to-selection": "Filtrar esta seleção",
"fullname": "Nome Completo",
"header-logo-title": "Voltar para a lista de quadros.",
"home": "Início",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
+ "import-show-user-mapping": "Review members mapping",
+ "import-user-select": "Pick the Wekan user you want to use as this member",
"info": "Informações",
"initials": "Iniciais",
"joined": "juntou-se",
- "keyboard-shortcuts": "Keyboard shortcuts",
+ "just-invited": "You are just invited to this board",
+ "keyboard-shortcuts": "Atalhos do teclado",
"label-create": "Criar uma nova etiqueta",
"label-default": "%s etiqueta (padrão)",
"label-delete-pop": "Não será possível recuperá-la. A etiqueta será removida de todos os cartões e seu histórico será destruído.",
@@ -128,41 +177,51 @@
"last-admin-desc": "Você não pode alterar funções porque deve existir pelo menos um administrador.",
"leave-board": "Sair do Quadro",
"link-card": "Vincular a este cartão",
- "list-archive-cards": "Archive all cards in this list",
+ "list-archive-cards": "Arquivar todos os cartões nesta lista",
"list-archive-cards-pop": "Isto removerá todos os cartões desta lista do quadro. Para visualizar os cartões arquivados e trazê-los de volta para o quadro, clique em “Menu” > “Itens Arquivados”.",
- "list-move-cards": "Move all cards in this list",
- "list-select-cards": "Select all cards in this list",
+ "list-move-cards": "Mover todos os cartões desta lista",
+ "list-select-cards": "Selecionar todos os cartões nesta lista",
"listActionPopup-title": "Listar Ações",
"listArchiveCardsPopup-title": "Arquivar Todos Os Cartões Nesta Lista?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "Mover Todos Os Cartões Nesta Lista",
"lists": "Listas",
"log-out": "Sair",
"loginPopup-title": "Entrar",
- "memberMenuPopup-title": "Member Settings",
+ "mapMembersPopup-title": "Map members",
+ "mapMembersAddPopup-title": "Select Wekan member",
+ "memberMenuPopup-title": "Configuração de Membros",
"members": "Membros",
"menu": "Menu",
"moveCardPopup-title": "Mover Cartão",
- "multi-selection": "Multi-Selection",
+ "multi-selection": "Multi-Seleção",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "Meus Quadros",
"name": "Nome",
- "no-archived-cards": "No archived cards.",
- "no-archived-lists": "No archived lists.",
+ "no-archived-cards": "Nenhum cartão arquivado",
+ "no-archived-lists": "Sem listas arquivadas",
"no-results": "Nenhum resultado.",
"normal": "Normal",
"normal-desc": "Pode ver e editar cartões. Não pode alterar configurações.",
+ "not-accepted-yet": "Invitation not accepted yet",
"optional": "opcional",
+ "or": "or",
"page-maybe-private": "Esta página pode ser privada. Você poderá vê-la se estiver <a href='%s'>logado</a>.",
"page-not-found": "Página não encontrada.",
"password": "Senha",
+ "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
+ "preview": "Preview",
+ "previewClipboardImagePopup-title": "Preview",
+ "previewAttachedImagePopup-title": "Preview",
"private": "Privado",
"private-desc": "Este quadro é privado. Apenas seus membros podem acessar e editá-lo.",
"profile": "Perfil",
"public": "Público",
"public-desc": "Este quadro é público. Ele é visível a qualquer pessoa com o link e será exibido em mecanismos de busca como o Google. Apenas seus membros podem editá-lo.",
- "quick-access-description": "Star a board to add a shortcut in this bar.",
+ "quick-access-description": "Clique na estrela para adicionar um atalho nesta barra.",
"remove-cover": "Remover Capa",
- "remove-from-board": "Remove from Board",
- "remove-label": "Remove the label",
+ "remove-from-board": "Remover do Quadro",
+ "remove-label": "Remover Etiqueta",
"remove-member": "Remover Membro",
"remove-member-from-card": "Remover do Cartão",
"remove-member-pop": "Remover __name__ (__username__) de __boardTitle__? O membro será removido de todos os cartões neste quadro e será notificado.",
@@ -173,13 +232,15 @@
"save": "Salvar",
"search": "Buscar",
"select-color": "Selecione uma cor",
- "shortcut-autocomplete-emojies": "Autocomplete emojies",
- "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-toggle-sidebar": "Toggle Board Sidebar",
+ "shortcut-assign-self": "Assign yourself to current card",
+ "shortcut-autocomplete-emojies": "Preenchimento automático de emojies",
+ "shortcut-autocomplete-members": "Preenchimento automático de membros",
+ "shortcut-clear-filters": "Limpar todos filtros",
+ "shortcut-close-dialog": "Fechar dialogo",
+ "shortcut-filter-my-cards": "Filtrar meus cartões",
+ "shortcut-show-shortcuts": "Mostrar lista de atalhos",
+ "shortcut-toggle-filterbar": "Toggle Filter Sidebar",
+ "shortcut-toggle-sidebar": "Fechar barra lateral.",
"signupPopup-title": "Criar uma Conta",
"star-board-title": "Clique para marcar este quadro como favorito. Ele aparecerá no topo na lista dos seus quadros.",
"starred-boards": "Quadros Favoritos",
@@ -189,12 +250,13 @@
"this-board": "este quadro",
"this-card": "este cartão",
"title": "Título",
- "unassign-member": "Unassign member",
- "unsaved-description": "You have an unsaved description.",
- "upload-avatar": "Upload an avatar",
- "uploaded-avatar": "Uploaded an avatar",
+ "unassign-member": "Membro não associado",
+ "unsaved-description": "Você possui uma descrição não salva",
+ "upload": "Upload",
+ "upload-avatar": "Carregar um avatar",
+ "uploaded-avatar": "Avatar carregado",
"username": "Nome de usuário",
- "view-it": "View it",
- "warn-list-archived": "warning: this card is in an archived list",
- "what-to-do": "What do you want to do?"
+ "view-it": "Visualizar",
+ "warn-list-archived": "aviso: este cartão está em uma lista arquivada",
+ "what-to-do": "O que você gostaria de fazer?"
} \ No newline at end of file
diff --git a/i18n/ru.i18n.json b/i18n/ru.i18n.json
new file mode 100644
index 00000000..9528a3c9
--- /dev/null
+++ b/i18n/ru.i18n.json
@@ -0,0 +1,229 @@
+{
+ "actions": "Действия",
+ "activities": "Activities",
+ "activity": "Активность",
+ "activity-added": "добавил %s на %s",
+ "activity-archived": "отправил в архив %s",
+ "activity-attached": "прикрепил %s к %s",
+ "activity-created": "создал %s",
+ "activity-excluded": "исключено %s из %s",
+ "activity-imported": "imported %s into %s from %s",
+ "activity-imported-board": "imported %s from %s",
+ "activity-joined": "присоединились %s",
+ "activity-moved": "переместил %s из %s на %s",
+ "activity-on": "%s",
+ "activity-removed": "удалено %s из %s",
+ "activity-sent": "отправлено %s в %s",
+ "activity-unjoined": "unjoined %s",
+ "add": "Создать",
+ "add-attachment": "Add an attachment",
+ "add-board": "Создать новую доску",
+ "add-card": "Add a card",
+ "add-cover": "Прикрепить",
+ "add-label": "Add the label",
+ "add-list": "Add a list",
+ "add-members": "Add Members",
+ "added": "Добавлено",
+ "addMemberPopup-title": "Участники",
+ "admin": "Администратор",
+ "admin-desc": "Может просматривать и редактировать карточки, удалять участников и управлять настройками доски.",
+ "all-boards": "All boards",
+ "and-n-other-card": "And __count__ other card",
+ "and-n-other-card_plural": "And __count__ other cards",
+ "archive": "Архивировать",
+ "archive-all": "Архивировать все",
+ "archive-board": "Archive Board",
+ "archive-card": "Archive Card",
+ "archive-list": "Архивировать список",
+ "archive-selection": "Archive selection",
+ "archiveBoardPopup-title": "Закрыть доску?",
+ "archived-items": "Объекты в архиве",
+ "archives": "Archives",
+ "assign-member": "Assign member",
+ "attached": "прикреплено",
+ "attachment": "Вложение",
+ "attachment-delete-pop": "Если удалить вложение, его нельзя будет восстановить.",
+ "attachmentDeletePopup-title": "Удалить вложение?",
+ "attachments": "Вложения",
+ "avatar-too-big": "The avatar is too large (70Kb max)",
+ "back": "Назад",
+ "board-change-color": "Change color",
+ "board-nb-stars": "%s stars",
+ "board-not-found": "Доска не найдена",
+ "board-private-info": "This board will be <strong>private</strong>.",
+ "board-public-info": "Эта доска будет <strong>доступной всем</strong>.",
+ "boardChangeColorPopup-title": "Change Board Background",
+ "boardChangeTitlePopup-title": "Переименовать доску",
+ "boardChangeVisibilityPopup-title": "Изменить настройки видимости",
+ "boardImportBoardPopup-title": "Import board from Trello",
+ "boardMenuPopup-title": "Board Menu",
+ "boards": "Доски",
+ "bucket-example": "Like “Bucket List” for example",
+ "cancel": "Отмена",
+ "card-archived": "Эта карточка помещена в архив.",
+ "card-comments-title": "Комментарии (%s)",
+ "card-delete-notice": "Это действие невозможно будет отменить. Все изменения, которые вы вносили в карточку будут потеряны.",
+ "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 archive a card to remove it from the board and preserve the activity.",
+ "card-edit-attachments": "Edit attachments",
+ "card-edit-labels": "Edit labels",
+ "card-edit-members": "Edit members",
+ "card-labels-title": "Редактировать метки.",
+ "card-members-title": "Добавить или удалить участника.",
+ "cardAttachmentsPopup-title": "Attach From",
+ "cardDeletePopup-title": "Удалить карточку?",
+ "cardDetailsActionsPopup-title": "Card Actions",
+ "cardLabelsPopup-title": "Метки",
+ "cardMembersPopup-title": "Участники",
+ "cardMorePopup-title": "Поделиться",
+ "cards": "Cards",
+ "change": "Change",
+ "change-avatar": "Изменить аватар",
+ "change-password": "Изменить пароль",
+ "change-permissions": "Change permissions",
+ "changeAvatarPopup-title": "Изменить аватар",
+ "changeLanguagePopup-title": "Сменить язык",
+ "changePasswordPopup-title": "Изменить пароль",
+ "changePermissionsPopup-title": "Изменить настройки доступа",
+ "click-to-star": "Отметить как «Избранное»",
+ "click-to-unstar": "Снять отметку",
+ "close": "Закрыть",
+ "close-board": "Close Board",
+ "close-board-pop": "Вы сможете снова открыть доску нажав кнопку \"Доски\" в верхнем меню и выбрав \"Показать скрытые доски\".",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
+ "comment": "Отправить",
+ "comment-placeholder": "Write a comment",
+ "computer": "Загрузить с компьютера",
+ "create": "Создать",
+ "createBoardPopup-title": "Создать доску",
+ "createLabelPopup-title": "Создать метку",
+ "current": "current",
+ "default-avatar": "Default avatar",
+ "delete": "Удалить",
+ "deleteLabelPopup-title": "Удалить метку?",
+ "description": "Описание",
+ "disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
+ "disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
+ "discard": "Discard",
+ "download": "Скачать",
+ "edit": "Редактировать",
+ "edit-avatar": "Изменить аватар",
+ "edit-profile": "Edit Profile",
+ "editLabelPopup-title": "Редактирование метки",
+ "editProfilePopup-title": "Edit Profile",
+ "email": "Эл.почта",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data does not include the proper information in the correct format",
+ "error-list-doesNotExist": "This list does not exist",
+ "filter": "Filter",
+ "filter-cards": "Фильтр",
+ "filter-clear": "Clear filter",
+ "filter-on": "Filter is on",
+ "filter-on-desc": "Показываются карточки, соответствующие настройкам фильтра. Нажмите для редактирования.",
+ "filter-to-selection": "Filter to selection",
+ "fullname": "Полное имя",
+ "header-logo-title": "Вернуться к доскам.",
+ "home": "Главная",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "info": "Информация",
+ "initials": "Initials",
+ "joined": "вступил",
+ "keyboard-shortcuts": "Keyboard shortcuts",
+ "label-create": "Создать метку",
+ "label-default": "%s",
+ "label-delete-pop": "Это действие невозможно будет отменить. Метка будет удалена во всех карточках.",
+ "labels": "Метки",
+ "language": "Язык",
+ "last-admin-desc": "Вы не можете изменять роли, для этого требуются права администратора.",
+ "leave-board": "Leave Board",
+ "link-card": "Доступна по ссылке",
+ "list-archive-cards": "Archive all cards in this list",
+ "list-archive-cards-pop": "Это действие переместит все карточки в архив и они перестанут быть видимым на доске. Для просмотра карточек в архиве нажмите “Меню” > “Объекты в архиве”.",
+ "list-move-cards": "Move all cards in this list",
+ "list-select-cards": "Select all cards in this list",
+ "listActionPopup-title": "Список действий",
+ "listArchiveCardsPopup-title": "Архивировать все карточки в списке?",
+ "listImportCardPopup-title": "Import a Trello card",
+ "listMoveCardsPopup-title": "Перенос карточек",
+ "lists": "Lists",
+ "log-out": "Выйти",
+ "loginPopup-title": "Войти",
+ "memberMenuPopup-title": "Member Settings",
+ "members": "Участники",
+ "menu": "Меню",
+ "moveCardPopup-title": "Move Card",
+ "multi-selection": "Multi-Selection",
+ "multi-selection-on": "Multi-Selection is on",
+ "my-boards": "Мои доски",
+ "name": "Имя",
+ "no-archived-cards": "No archived cards.",
+ "no-archived-lists": "No archived lists.",
+ "no-results": "Ничего не найдено",
+ "normal": "Обычный",
+ "normal-desc": "Может редактировать карточки. Не может управлять настройками.",
+ "optional": "не обязательно",
+ "or": "or",
+ "page-maybe-private": "Возможно, эта страница скрыта от незарегистрированных пользователей. Попробуйте <a href='%s'>войти на сайт</a>.",
+ "page-not-found": "Страница не найдена.",
+ "password": "Пароль",
+ "private": "Закрытая",
+ "private-desc": "Эта доска с ограниченным доступом. Только участники могут работать с ней.",
+ "profile": "Профиль",
+ "public": "Открытая",
+ "public-desc": "Эта доска может быть видна всем у кого есть ссылка. Также может быть проиндексирована поисковыми системами. Вносить изменения могут только участники.",
+ "quick-access-description": "Star a board to add a shortcut in this bar.",
+ "remove-cover": "Открепить",
+ "remove-from-board": "Remove from Board",
+ "remove-label": "Remove the label",
+ "remove-member": "Удалить участника",
+ "remove-member-from-card": "Удалить из карточки",
+ "remove-member-pop": "Удалить участника __name__ (__username__) из доски __boardTitle__? Участник будет удален из всех карточек. Также он получит уведомление о совершаемом действии.",
+ "removeMemberPopup-title": "Удалить участника?",
+ "rename": "Переименовать",
+ "rename-board": "Переименовать доску",
+ "restore": "Restore",
+ "save": "Сохранить",
+ "search": "Поиск",
+ "select-color": "Выбрать цвет",
+ "shortcut-assign-self": "Assign yourself to current card",
+ "shortcut-autocomplete-emojies": "Autocomplete emojies",
+ "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-toggle-filterbar": "Toggle Filter Sidebar",
+ "shortcut-toggle-sidebar": "Toggle Board Sidebar",
+ "signupPopup-title": "Создать учетную запись",
+ "star-board-title": "Отметьте как «Избранное». Эта доска будет всегда на виду.",
+ "starred-boards": "Помеченные как «Избранное»",
+ "starred-boards-description": "Доска будет всегда на видном месте.",
+ "subscribe": "Подписаться",
+ "team": "Участники",
+ "this-board": "эту доску",
+ "this-card": "активная карточка",
+ "title": "Название",
+ "unassign-member": "Unassign member",
+ "unsaved-description": "You have an unsaved description.",
+ "upload-avatar": "Upload an avatar",
+ "uploaded-avatar": "Uploaded an avatar",
+ "username": "Имя пользователя",
+ "view-it": "View it",
+ "warn-list-archived": "warning: this card is in an archived list",
+ "what-to-do": "What do you want to do?"
+} \ No newline at end of file
diff --git a/i18n/tr.i18n.json b/i18n/tr.i18n.json
index 812efcbf..d4158947 100644
--- a/i18n/tr.i18n.json
+++ b/i18n/tr.i18n.json
@@ -1,18 +1,21 @@
{
+ "accept": "Accept",
"actions": "İşlemler",
- "activities": "Activities",
+ "activities": "Aktiviteler",
"activity": "Etkinlik",
"activity-added": "added %s to %s",
"activity-archived": "%s arşivledi",
"activity-attached": "attached %s to %s",
"activity-created": "%s oluşturdu",
"activity-excluded": "excluded %s from %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": "unjoinded %s",
+ "activity-unjoined": "unjoined %s",
"add": "Ekle",
"add-attachment": "Add an attachment",
"add-board": "Yeni bir pano ekle",
@@ -36,7 +39,7 @@
"archive-selection": "Archive selection",
"archiveBoardPopup-title": "Pano Kapatılsın mı?",
"archived-items": "Arşivlenmiş Öğeler",
- "archives": "Archives",
+ "archives": "Arşiv",
"assign-member": "Assign member",
"attached": "dosya eklendi",
"attachment": "Ek Dosya",
@@ -53,6 +56,7 @@
"boardChangeColorPopup-title": "Change Board Background",
"boardChangeTitlePopup-title": "Pano Adı Değiştirme",
"boardChangeVisibilityPopup-title": "Görünebilirliği Değiştir",
+ "boardImportBoardPopup-title": "Import board from Trello",
"boardMenuPopup-title": "Board Menu",
"boards": "Panolar",
"bucket-example": "Like “Bucket List” for example",
@@ -74,6 +78,7 @@
"cardMembersPopup-title": "Üyeler",
"cardMorePopup-title": "More",
"cards": "Cards",
+ "change": "Change",
"change-avatar": "Avatar Değiştir",
"change-password": "Parola Değiştir",
"change-permissions": "Change permissions",
@@ -83,9 +88,20 @@
"changePermissionsPopup-title": "Yetkileri Değiştirme",
"click-to-star": "Bu panoyu yıldızlamak için tıkla.",
"click-to-unstar": "Bu panunun yıldızını kaldırmak için tıkla.",
+ "clipboard": "Clipboard or drag & drop",
"close": "Kapat",
"close-board": "Close Board",
"close-board-pop": "You can re-open the board by clicking the “Boards” menu from the header, selecting “View Closed Boards”, finding the board and clicking “Re-open”.",
+ "color-green": "green",
+ "color-yellow": "yellow",
+ "color-orange": "orange",
+ "color-red": "red",
+ "color-purple": "purple",
+ "color-blue": "blue",
+ "color-sky": "sky",
+ "color-lime": "lime",
+ "color-pink": "pink",
+ "color-black": "black",
"comment": "Yorum Gönder",
"comment-placeholder": "Write a comment",
"computer": "Bilgisayar",
@@ -93,6 +109,7 @@
"createBoardPopup-title": "Pano Oluşturma",
"createLabelPopup-title": "Etiket Oluşturma",
"current": "current",
+ "decline": "Decline",
"default-avatar": "Default avatar",
"delete": "Sil",
"deleteLabelPopup-title": "Etiket Silinsin mi?",
@@ -100,13 +117,35 @@
"disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
"disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
"discard": "Discard",
+ "done": "Done",
"download": "İndir",
"edit": "Düzenle",
"edit-avatar": "Avatar Değiştir",
- "edit-profile": "Edit Profile",
+ "edit-profile": "Profili Düzenle",
"editLabelPopup-title": "Etiket Değiştirme",
- "editProfilePopup-title": "Edit Profile",
+ "editProfilePopup-title": "Profili Düzenle",
"email": "E-posta",
+ "email-enrollAccount-subject": "An account created for you on __url__",
+ "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",
+ "email-invalid": "Invalid email",
+ "email-invite": "Invite via Email",
+ "email-invite-subject": "__inviter__ sent you an invitation",
+ "email-invite-text": "Dear __user__,\n\n__inviter__ invites you to join board \"__board__\" for collaborations.\n\nPlease follow the link below:\n\n__url__\n\nThanks.",
+ "email-resetPassword-subject": "Reset your password on __url__",
+ "email-resetPassword-text": "Hello __user__,\n\nTo reset your password, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-verifyEmail-subject": "Verify your email address on __url__",
+ "email-verifyEmail-text": "Hello __user__,\n\nTo verify your account email, simply click the link below.\n\n__url__\n\nThanks.",
+ "email-sent": "Email sent",
+ "error-board-doesNotExist": "This board does not exist",
+ "error-board-notAdmin": "You need to be admin of this board to do that",
+ "error-board-notAMember": "You need to be a member of this board to do that",
+ "error-json-malformed": "Your text is not valid JSON",
+ "error-json-schema": "Your JSON data 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": "This action on self is not allowed",
+ "error-user-notCreated": "This user is not created",
"filter": "Filter",
"filter-cards": "Kartları Süz",
"filter-clear": "Clear filter",
@@ -116,9 +155,19 @@
"fullname": "Ad Soyad",
"header-logo-title": "Panolar sayfanıza geri dön.",
"home": "Home",
+ "import": "Import",
+ "import-board": "import from Trello",
+ "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
+ "import-card": "Import a Trello card",
+ "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+ "import-json-placeholder": "Paste your valid JSON data here",
+ "import-members-map": "Your imported board has some members. Please map the members you want to import to Wekan users",
+ "import-show-user-mapping": "Review members mapping",
+ "import-user-select": "Pick the Wekan user you want to use as this member",
"info": "Infos",
"initials": "Initials",
"joined": "joined",
+ "just-invited": "You are just invited to this board",
"keyboard-shortcuts": "Keyboard shortcuts",
"label-create": "Yeni bir etiket oluştur",
"label-default": "%s etiket (varsayılan)",
@@ -134,15 +183,19 @@
"list-select-cards": "Select all cards in this list",
"listActionPopup-title": "Liste İşlemleri",
"listArchiveCardsPopup-title": "Bu Listedeki Tüm Kartlar Taşınsın mı?",
+ "listImportCardPopup-title": "Import a Trello card",
"listMoveCardsPopup-title": "Listedeki Tüm Kartları Taşıma",
"lists": "Lists",
"log-out": "Oturum Kapat",
"loginPopup-title": "Oturum Aç",
+ "mapMembersPopup-title": "Map members",
+ "mapMembersAddPopup-title": "Select Wekan member",
"memberMenuPopup-title": "Member Settings",
"members": "Üyeler",
"menu": "Menü",
"moveCardPopup-title": "Move Card",
"multi-selection": "Multi-Selection",
+ "multi-selection-on": "Multi-Selection is on",
"my-boards": "Panolarım",
"name": "Adı",
"no-archived-cards": "No archived cards.",
@@ -150,10 +203,16 @@
"no-results": "Sonuç yok",
"normal": "Normal",
"normal-desc": "Kartları görüntüler ve düzenler. Ayarları değiştiremez.",
+ "not-accepted-yet": "Invitation not accepted yet",
"optional": "isteğe bağlı",
+ "or": "or",
"page-maybe-private": "Bu sayfa özel olabilir. <a href='%s'>Oturum açarak</a> görülebilir.",
"page-not-found": "Sayda bulunamadı.",
"password": "Parola",
+ "paste-or-dragdrop": "to paste, or drag & drop image file to it (image only)",
+ "preview": "Preview",
+ "previewClipboardImagePopup-title": "Preview",
+ "previewAttachedImagePopup-title": "Preview",
"private": "Özel",
"private-desc": "Bu pano özel. Sadece panoya ekli kişiler görüntüleyebilir ve düzenleyebilir.",
"profile": "Kullanıcı Sayfası",
@@ -173,12 +232,14 @@
"save": "Kaydet",
"search": "Search",
"select-color": "Bir renk seç",
+ "shortcut-assign-self": "Assign yourself to current card",
"shortcut-autocomplete-emojies": "Autocomplete emojies",
"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-toggle-filterbar": "Toggle Filter Sidebar",
"shortcut-toggle-sidebar": "Toggle Board Sidebar",
"signupPopup-title": "Bir Hesap Oluştur",
"star-board-title": "Bu panoyu yıldızlamak için tıkla. Pano listesinin en üstünde gösterilir.",
@@ -191,6 +252,7 @@
"title": "Başlık",
"unassign-member": "Unassign member",
"unsaved-description": "You have an unsaved description.",
+ "upload": "Upload",
"upload-avatar": "Upload an avatar",
"uploaded-avatar": "Uploaded an avatar",
"username": "Kullanıcı adı",
diff --git a/i18n/zh-CN.i18n.json b/i18n/zh-CN.i18n.json
index 49b6bba5..d26cd6b1 100644
--- a/i18n/zh-CN.i18n.json
+++ b/i18n/zh-CN.i18n.json
@@ -1,12 +1,14 @@
{
"actions": "动作",
- "activities": "Activities",
+ "activities": "活动",
"activity": "活动",
"activity-added": "添加 %s 至 %s",
"activity-archived": "归档 %s",
"activity-attached": "附加 %s 至 %s",
"activity-created": "创建 %s",
"activity-excluded": "排除 %s 从 %s",
+ "activity-imported": "导入 %s 至 %s 从 %s 中",
+ "activity-imported-board": "已导入 %s 从 %s 中",
"activity-joined": "关联 %s",
"activity-moved": "将 %s 从 %s 移动到 %s",
"activity-on": "在 %s",
@@ -14,69 +16,71 @@
"activity-sent": "发送 %s 至 %s",
"activity-unjoined": "解除关联 %s",
"add": "添加",
- "add-attachment": "Add an attachment",
- "add-board": "添加一个新的看板",
- "add-card": "Add a card",
+ "add-attachment": "添加附件",
+ "add-board": "添加新看板",
+ "add-card": "添加卡片",
"add-cover": "添加封面",
- "add-label": "Add the label",
- "add-list": "Add a list",
- "add-members": "Add Members",
+ "add-label": "添加标签",
+ "add-list": "添加列表",
+ "add-members": "添加成员",
"added": "添加",
"addMemberPopup-title": "成员",
"admin": "管理员",
"admin-desc": "可以浏览并编辑卡片,移除成员,并且更改该看板的设置",
- "all-boards": "All boards",
- "and-n-other-card": "And __count__ other card",
- "and-n-other-card_plural": "And __count__ other cards",
+ "all-boards": "全部看板",
+ "and-n-other-card": "和另外 __count__ 个卡片",
+ "and-n-other-card_plural": "和另外 __count__ 个卡片",
"archive": "归档",
- "archive-all": "归档所有",
- "archive-board": "Archive Board",
- "archive-card": "Archive Card",
+ "archive-all": "全部归档",
+ "archive-board": "归档看板",
+ "archive-card": "归档卡片",
"archive-list": "归档该列表",
- "archive-selection": "Archive selection",
+ "archive-selection": "归档选中内容",
"archiveBoardPopup-title": "关闭看板?",
- "archived-items": "归档项",
- "archives": "Archives",
- "assign-member": "Assign member",
+ "archived-items": "归档条目",
+ "archives": "归档",
+ "assign-member": "分配人员",
"attached": "附加",
"attachment": "附件",
"attachment-delete-pop": "删除附件操作不可逆。",
"attachmentDeletePopup-title": "删除附件?",
"attachments": "附件",
- "avatar-too-big": "The avatar is too large (70Kb max)",
+ "avatar-too-big": "头像太大 (最大 70 Kb)",
"back": "返回",
- "board-change-color": "Change color",
- "board-nb-stars": "%s stars",
+ "board-change-color": "更改颜色",
+ "board-nb-stars": "%s 星标",
"board-not-found": "看板不存在",
- "board-private-info": "This board will be <strong>private</strong>.",
+ "board-private-info": "该看板将被 <strong>私有化</strong>.",
"board-public-info": "该看板将 <strong>公开</strong>.",
- "boardChangeColorPopup-title": "Change Board Background",
+ "boardChangeColorPopup-title": "修改看板背景",
"boardChangeTitlePopup-title": "重命名看板",
"boardChangeVisibilityPopup-title": "更改可视级别",
- "boardMenuPopup-title": "Board Menu",
+ "boardImportBoardPopup-title": "从 Trello 导入看板",
+ "boardMenuPopup-title": "看板菜单",
"boards": "看板",
- "bucket-example": "Like “Bucket List” for example",
+ "bucket-example": "例如 “目标清单”",
"cancel": "取消",
"card-archived": "该卡片已被归档",
"card-comments-title": "该卡片拥有 %s 条评论",
"card-delete-notice": "删除操作不可恢复,你将会丢失该卡片的所有相关动作。",
- "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 archive a card to remove it from the board and preserve the activity.",
- "card-edit-attachments": "Edit attachments",
- "card-edit-labels": "Edit labels",
- "card-edit-members": "Edit members",
+ "card-delete-pop": "所有的动作将从活动动态中被移除且您将无法重新打开该卡片。此操作无法撤销。",
+ "card-delete-suggest-archive": "你可以通过归档一个卡片来将它从看板中移除且保留活动。",
+ "card-edit-attachments": "编辑附件",
+ "card-edit-labels": "编辑标签",
+ "card-edit-members": "编辑成员",
"card-labels-title": "更改该卡片上的标签",
"card-members-title": "在该卡片中添加或移除看板成员",
- "cardAttachmentsPopup-title": "Attach From",
+ "cardAttachmentsPopup-title": "附件位置",
"cardDeletePopup-title": "删除卡片?",
- "cardDetailsActionsPopup-title": "Card Actions",
+ "cardDetailsActionsPopup-title": "卡片动作",
"cardLabelsPopup-title": "标签",
"cardMembersPopup-title": "成员",
"cardMorePopup-title": "更多",
- "cards": "Cards",
+ "cards": "卡片",
+ "change": "变更",
"change-avatar": "更改头像",
"change-password": "更改密码",
- "change-permissions": "Change permissions",
+ "change-permissions": "更改权限",
"changeAvatarPopup-title": "更改头像",
"changeLanguagePopup-title": "更改语言",
"changePasswordPopup-title": "更改密码",
@@ -84,73 +88,96 @@
"click-to-star": "点此来标记该看板",
"click-to-unstar": "点此来去除该看板的标记",
"close": "关闭",
- "close-board": "Close Board",
+ "close-board": "关闭看板",
"close-board-pop": "你可以通过点击头部的\"看板\"菜单,选择\"浏览已关闭看板\",查找看板并且点击\"重开\"来重开看板。",
+ "color-green": "绿色",
+ "color-yellow": "黄色",
+ "color-orange": "橙色",
+ "color-red": "红色",
+ "color-purple": "紫色",
+ "color-blue": "蓝色",
+ "color-sky": "天蓝",
+ "color-lime": "绿黄",
+ "color-pink": "粉红",
+ "color-black": "黑色",
"comment": "评论",
- "comment-placeholder": "Write a comment",
+ "comment-placeholder": "添加评论",
"computer": "从本机上传",
"create": "创建",
"createBoardPopup-title": "创建看板",
"createLabelPopup-title": "创建标签",
- "current": "current",
- "default-avatar": "Default avatar",
+ "current": "当前",
+ "default-avatar": "默认头像",
"delete": "删除",
"deleteLabelPopup-title": "删除标签?",
"description": "描述",
- "disambiguateMultiLabelPopup-title": "Disambiguate Label Action",
- "disambiguateMultiMemberPopup-title": "Disambiguate Member Action",
- "discard": "Discard",
+ "disambiguateMultiLabelPopup-title": "消除标签动作歧义",
+ "disambiguateMultiMemberPopup-title": "消除会员动作歧义",
+ "discard": "放弃",
"download": "下载",
"edit": "编辑",
"edit-avatar": "更改头像",
- "edit-profile": "Edit Profile",
+ "edit-profile": "编辑资料",
"editLabelPopup-title": "更改标签",
- "editProfilePopup-title": "Edit Profile",
+ "editProfilePopup-title": "编辑资料",
"email": "邮箱",
- "filter": "Filter",
+ "error-board-notAMember": "需要成为看板成员才能执行此动作",
+ "error-json-malformed": "文本不是合法的 JSON",
+ "error-json-schema": "JSON 数据没有用正确的格式包含合适的信息",
+ "error-list-doesNotExist": "不存在此列表",
+ "filter": "过滤",
"filter-cards": "过滤卡片",
- "filter-clear": "Clear filter",
- "filter-on": "Filter is on",
+ "filter-clear": "清空过滤器",
+ "filter-on": "过滤器启用",
"filter-on-desc": "你正在过滤该看板上的卡片,点此编辑过滤。",
- "filter-to-selection": "Filter to selection",
+ "filter-to-selection": "要选择的过滤器",
"fullname": "全称",
"header-logo-title": "返回您的看板页",
"home": "首页",
+ "import": "导入",
+ "import-board": "从 Trello 导入",
+ "import-board-trello-instruction": "在你的Trello看板中,点击“菜单”,然后选择“更多”,“打印与导出”,“导出为 JSON” 并拷贝结果文本",
+ "import-card": "导入 Trello 卡片",
+ "import-card-trello-instruction": "进入一个 Trello 卡片,选择“分享与更多”,然后选择 “导出为 JSON” 并且拷贝结果文本",
+ "import-json-placeholder": "粘贴您有效的 JSON 数据至此",
"info": "信息",
- "initials": "Initials",
+ "initials": "首字母",
"joined": "关联",
- "keyboard-shortcuts": "Keyboard shortcuts",
+ "keyboard-shortcuts": "键盘快捷键",
"label-create": "创建新标签",
"label-default": "%s 标签 (默认)",
"label-delete-pop": "此操作不可逆,这将会删除该标签并清除它的历史记录。",
"labels": "标签",
"language": "语言",
"last-admin-desc": "你不能更改角色,因为至少需要一名管理员。",
- "leave-board": "Leave Board",
+ "leave-board": "离开面板",
"link-card": "关联至该卡片",
- "list-archive-cards": "Archive all cards in this list",
+ "list-archive-cards": "归档列表中的所有卡片",
"list-archive-cards-pop": "这将会从本看板中移除该列表中的所有卡片。如果需要浏览已归档的卡片并且将其恢复至看板,请点击\"菜单\">\"归档项\"",
- "list-move-cards": "Move all cards in this list",
- "list-select-cards": "Select all cards in this list",
+ "list-move-cards": "移动列表中的所有卡片",
+ "list-select-cards": "选择列表中的所有卡片",
"listActionPopup-title": "列出动作",
"listArchiveCardsPopup-title": "归档该列表中的所有卡片?",
+ "listImportCardPopup-title": "导入 Trello 卡片",
"listMoveCardsPopup-title": "移动该列表的所有卡片",
- "lists": "Lists",
+ "lists": "列表",
"log-out": "登出",
"loginPopup-title": "登录",
- "memberMenuPopup-title": "Member Settings",
+ "memberMenuPopup-title": "成员设置",
"members": "成员",
"menu": "菜单",
- "moveCardPopup-title": "Move Card",
- "multi-selection": "Multi-Selection",
+ "moveCardPopup-title": "移动卡片",
+ "multi-selection": "多选",
+ "multi-selection-on": "多选启用",
"my-boards": "我的看板",
"name": "名称",
- "no-archived-cards": "No archived cards.",
- "no-archived-lists": "No archived lists.",
+ "no-archived-cards": "无归档的卡片",
+ "no-archived-lists": "无归档的列表",
"no-results": "无结果",
"normal": "普通",
"normal-desc": "可以创建以及编辑卡片,无法更改设置。",
"optional": "可选",
+ "or": "或",
"page-maybe-private": "本页面被设为私有. 您必须 <a href='%s'>登录</a>以浏览其中内容。",
"page-not-found": "页面不存在。",
"password": "密码",
@@ -159,27 +186,29 @@
"profile": "资料",
"public": "公共",
"public-desc": "该看板将被公共。任何人均可通过链接查看,并且将对Google和其他搜索引擎开放,只有添加至该看板的成员才可进行编辑。",
- "quick-access-description": "Star a board to add a shortcut in this bar.",
+ "quick-access-description": "星标一个看板来在该导航条中添加一个快捷方式",
"remove-cover": "移除封面",
- "remove-from-board": "Remove from Board",
- "remove-label": "Remove the label",
+ "remove-from-board": "从看板中删除",
+ "remove-label": "移除标签",
"remove-member": "移除成员",
"remove-member-from-card": "从该卡片中移除",
"remove-member-pop": "欲从 __boardTitle__ 中移除 __name__ (__username__) ? 该成员将会从该看板的所有卡片中被移除,他将会收到一条提醒。",
"removeMemberPopup-title": "删除成员?",
"rename": "重命名",
"rename-board": "重命名看板",
- "restore": "Restore",
+ "restore": "还原",
"save": "保存",
"search": "搜索",
"select-color": "选择颜色",
- "shortcut-autocomplete-emojies": "Autocomplete emojies",
- "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-toggle-sidebar": "Toggle Board Sidebar",
+ "shortcut-assign-self": "分配当前卡片给自己",
+ "shortcut-autocomplete-emojies": "自动补全表情",
+ "shortcut-autocomplete-members": "自动补全成员",
+ "shortcut-clear-filters": "清空全部过滤器",
+ "shortcut-close-dialog": "关闭对话框",
+ "shortcut-filter-my-cards": "过滤我的卡片",
+ "shortcut-show-shortcuts": "显示此快捷键列表",
+ "shortcut-toggle-filterbar": "切换过滤器边栏",
+ "shortcut-toggle-sidebar": "切换面板边栏",
"signupPopup-title": " 创建账户",
"star-board-title": "点此来标记该看板,它将会出现在您的看板列表顶部。",
"starred-boards": "已标记看板",
@@ -189,12 +218,12 @@
"this-board": "该看板",
"this-card": "该卡片",
"title": "标题",
- "unassign-member": "Unassign member",
- "unsaved-description": "You have an unsaved description.",
- "upload-avatar": "Upload an avatar",
- "uploaded-avatar": "Uploaded an avatar",
+ "unassign-member": "取消分配成员",
+ "unsaved-description": "存在未保存的描述",
+ "upload-avatar": "上传头像",
+ "uploaded-avatar": "头像已经上传",
"username": "用户名",
- "view-it": "View it",
- "warn-list-archived": "warning: this card is in an archived list",
- "what-to-do": "What do you want to do?"
+ "view-it": "查看",
+ "warn-list-archived": "警告: 卡片位于一个归档列表",
+ "what-to-do": "要做什么?"
} \ No newline at end of file
diff --git a/collections/activities.js b/models/activities.js
index 5de07ee5..5de07ee5 100644
--- a/collections/activities.js
+++ b/models/activities.js
diff --git a/collections/attachments.js b/models/attachments.js
index 8ef0fef0..01e467ff 100644
--- a/collections/attachments.js
+++ b/models/attachments.js
@@ -1,4 +1,4 @@
-Attachments = new FS.Collection('attachments', {
+Attachments = new FS.Collection('attachments', { // eslint-disable-line meteor/collections
stores: [
// XXX Add a new store for cover thumbnails so we don't load big images in
diff --git a/collections/avatars.js b/models/avatars.js
index 53924ffb..53924ffb 100644
--- a/collections/avatars.js
+++ b/models/avatars.js
diff --git a/collections/boards.js b/models/boards.js
index fcd04153..6aba0b1e 100644
--- a/collections/boards.js
+++ b/models/boards.js
@@ -71,8 +71,190 @@ Boards.attachSchema(new SimpleSchema({
'midnight',
],
},
+ description: {
+ type: String,
+ optional: true,
+ },
}));
+
+Boards.helpers({
+ isPublic() {
+ return this.permission === 'public';
+ },
+
+ lists() {
+ return Lists.find({ boardId: this._id, archived: false }, { sort: { sort: 1 }});
+ },
+
+ activities() {
+ return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }});
+ },
+
+ activeMembers() {
+ return _.where(this.members, {isActive: true});
+ },
+
+ activeAdmins() {
+ return _.where(this.members, {isActive: true, isAdmin: true});
+ },
+
+ memberUsers() {
+ return Users.find({ _id: {$in: _.pluck(this.members, 'userId')} });
+ },
+
+ getLabel(name, color) {
+ return _.findWhere(this.labels, { name, color });
+ },
+
+ labelIndex(labelId) {
+ return _.pluck(this.labels, '_id').indexOf(labelId);
+ },
+
+ memberIndex(memberId) {
+ return _.pluck(this.members, 'userId').indexOf(memberId);
+ },
+
+ absoluteUrl() {
+ return FlowRouter.path('board', { id: this._id, slug: this.slug });
+ },
+
+ colorClass() {
+ return `board-color-${this.color}`;
+ },
+
+ // XXX currently mutations return no value so we have an issue when using addLabel in import
+ // XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove...
+ pushLabel(name, color) {
+ const _id = Random.id(6);
+ Boards.direct.update(this._id, { $push: {labels: { _id, name, color }}});
+ return _id;
+ },
+});
+
+Boards.mutations({
+ archive() {
+ return { $set: { archived: true }};
+ },
+
+ restore() {
+ return { $set: { archived: false }};
+ },
+
+ rename(title) {
+ return { $set: { title }};
+ },
+
+ setDesciption(description) {
+ return { $set: {description} };
+ },
+
+ setColor(color) {
+ return { $set: { color }};
+ },
+
+ setVisibility(visibility) {
+ return { $set: { permission: visibility }};
+ },
+
+ addLabel(name, color) {
+ // If label with the same name and color already exists we don't want to
+ // create another one because they would be indistinguishable in the UI
+ // (they would still have different `_id` but that is not exposed to the
+ // user).
+ if (!this.getLabel(name, color)) {
+ const _id = Random.id(6);
+ return { $push: {labels: { _id, name, color }}};
+ }
+ },
+
+ editLabel(labelId, name, color) {
+ if (!this.getLabel(name, color)) {
+ const labelIndex = this.labelIndex(labelId);
+ return {
+ $set: {
+ [`labels.${labelIndex}.name`]: name,
+ [`labels.${labelIndex}.color`]: color,
+ },
+ };
+ }
+ },
+
+ removeLabel(labelId) {
+ return { $pull: { labels: { _id: labelId }}};
+ },
+
+ addMember(memberId) {
+ const memberIndex = this.memberIndex(memberId);
+ if (memberIndex === -1) {
+ const xIndex = this.memberIndex('x');
+ if (xIndex === -1) {
+ return {
+ $push: {
+ members: {
+ userId: memberId,
+ isAdmin: false,
+ isActive: true,
+ },
+ },
+ };
+ } else {
+ return {
+ $set: {
+ [`members.${xIndex}.userId`]: memberId,
+ [`members.${xIndex}.isActive`]: true,
+ [`members.${xIndex}.isAdmin`]: false,
+ },
+ };
+ }
+ } else {
+ return {
+ $set: {
+ [`members.${memberIndex}.isActive`]: true,
+ },
+ };
+ }
+ },
+
+ removeMember(memberId) {
+ const memberIndex = this.memberIndex(memberId);
+
+ // we do not allow the only one admin to be removed
+ const allowRemove = (!this.members[memberIndex].isAdmin) || (this.activeAdmins().length > 1);
+
+ if (allowRemove) {
+ return {
+ $set: {
+ [`members.${memberIndex}.userId`]: 'x',
+ [`members.${memberIndex}.isActive`]: false,
+ [`members.${memberIndex}.isAdmin`]: false,
+ },
+ };
+ } else {
+ return {
+ $set: {
+ [`members.${memberIndex}.isActive`]: true,
+ },
+ };
+ }
+ },
+
+ setMemberPermission(memberId, isAdmin) {
+ const memberIndex = this.memberIndex(memberId);
+
+ // do not allow change permission of self
+ if (memberId === Meteor.userId()) {
+ isAdmin = this.members[memberIndex].isAdmin;
+ }
+
+ return {
+ $set: {
+ [`members.${memberIndex}.isAdmin`]: isAdmin,
+ },
+ };
+ },
+});
+
if (Meteor.isServer) {
Boards.allow({
insert: Meteor.userId,
@@ -101,9 +283,7 @@ if (Meteor.isServer) {
return false;
// If there is more than one admin, it's ok to remove anyone
- const nbAdmins = _.filter(doc.members, (member) => {
- return member.isAdmin;
- }).length;
+ const nbAdmins = _.where(doc.members, {isActive: true, isAdmin: true}).length;
if (nbAdmins > 1)
return false;
@@ -117,34 +297,22 @@ if (Meteor.isServer) {
},
fetch: ['members'],
});
-}
-Boards.helpers({
- isPublic() {
- return this.permission === 'public';
- },
-
- lists() {
- return Lists.find({ boardId: this._id, archived: false },
- { sort: { sort: 1 }});
- },
-
- activities() {
- return Activities.find({ boardId: this._id }, { sort: { createdAt: -1 }});
- },
-
- activeMembers() {
- return _.where(this.members, {isActive: true});
- },
-
- absoluteUrl() {
- return FlowRouter.path('board', { id: this._id, slug: this.slug });
- },
-
- colorClass() {
- return `board-color-${this.color}`;
- },
-});
+ Meteor.methods({
+ quitBoard(boardId) {
+ check(boardId, String);
+ const board = Boards.findOne(boardId);
+ if (board) {
+ const userId = Meteor.userId();
+ const index = board.memberIndex(userId);
+ if (index>=0) {
+ board.removeMember(userId);
+ return true;
+ } else throw new Meteor.Error('error-board-notAMember');
+ } else throw new Meteor.Error('error-board-doesNotExist');
+ },
+ });
+}
Boards.before.insert((userId, doc) => {
// XXX We need to improve slug management. Only the id should be necessary
@@ -167,7 +335,7 @@ Boards.before.insert((userId, doc) => {
// Handle labels
const colors = Boards.simpleSchema()._schema['labels.$.color'].allowedValues;
const defaultLabelsColors = _.clone(colors).splice(0, 6);
- doc.labels = _.map(defaultLabelsColors, (color) => {
+ doc.labels = defaultLabelsColors.map((color) => {
return {
color,
_id: Random.id(6),
@@ -215,7 +383,7 @@ if (Meteor.isServer) {
{ boardId: doc._id },
{
$pull: {
- labels: removedLabelId,
+ labelIds: removedLabelId,
},
},
{ multi: true }
diff --git a/models/cardComments.js b/models/cardComments.js
new file mode 100644
index 00000000..224deb03
--- /dev/null
+++ b/models/cardComments.js
@@ -0,0 +1,69 @@
+CardComments = new Mongo.Collection('card_comments');
+
+CardComments.attachSchema(new SimpleSchema({
+ boardId: {
+ type: String,
+ },
+ cardId: {
+ type: String,
+ },
+ // XXX Rename in `content`? `text` is a bit vague...
+ text: {
+ type: String,
+ },
+ // XXX We probably don't need this information here, since we already have it
+ // in the associated comment creation activity
+ createdAt: {
+ type: Date,
+ denyUpdate: false,
+ },
+ // XXX Should probably be called `authorId`
+ userId: {
+ type: String,
+ },
+}));
+
+CardComments.allow({
+ insert(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ update(userId, doc) {
+ return userId === doc.userId;
+ },
+ remove(userId, doc) {
+ return userId === doc.userId;
+ },
+ fetch: ['userId', 'boardId'],
+});
+
+CardComments.helpers({
+ user() {
+ return Users.findOne(this.userId);
+ },
+});
+
+CardComments.hookOptions.after.update = { fetchPrevious: false };
+
+CardComments.before.insert((userId, doc) => {
+ doc.createdAt = new Date();
+ doc.userId = userId;
+});
+
+if (Meteor.isServer) {
+ CardComments.after.insert((userId, doc) => {
+ Activities.insert({
+ userId,
+ activityType: 'addComment',
+ boardId: doc.boardId,
+ cardId: doc.cardId,
+ commentId: doc._id,
+ });
+ });
+
+ CardComments.after.remove((userId, doc) => {
+ const activity = Activities.findOne({ commentId: doc._id });
+ if (activity) {
+ Activities.remove(activity._id);
+ }
+ });
+}
diff --git a/collections/cards.js b/models/cards.js
index 97ba4e3c..1895fc69 100644
--- a/collections/cards.js
+++ b/models/cards.js
@@ -1,5 +1,4 @@
Cards = new Mongo.Collection('cards');
-CardComments = new Mongo.Collection('card_comments');
// XXX To improve pub/sub performances a card document should include a
// de-normalized number of comments so we don't have to publish the whole list
@@ -54,64 +53,28 @@ Cards.attachSchema(new SimpleSchema({
},
}));
-CardComments.attachSchema(new SimpleSchema({
- boardId: {
- type: String,
+Cards.allow({
+ insert(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
},
- cardId: {
- type: String,
- },
- // XXX Rename in `content`? `text` is a bit vague...
- text: {
- type: String,
+ update(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
},
- // XXX We probably don't need this information here, since we already have it
- // in the associated comment creation activity
- createdAt: {
- type: Date,
- denyUpdate: false,
- },
- // XXX Should probably be called `authorId`
- userId: {
- type: String,
+ remove(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
},
-}));
-
-if (Meteor.isServer) {
- Cards.allow({
- insert(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- update(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- remove(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- fetch: ['boardId'],
- });
-
- CardComments.allow({
- insert(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- update(userId, doc) {
- return userId === doc.userId;
- },
- remove(userId, doc) {
- return userId === doc.userId;
- },
- fetch: ['userId', 'boardId'],
- });
-}
+ fetch: ['boardId'],
+});
Cards.helpers({
list() {
return Lists.findOne(this.listId);
},
+
board() {
return Boards.findOne(this.boardId);
},
+
labels() {
const boardLabels = this.board().labels;
const cardLabels = _.filter(boardLabels, (label) => {
@@ -119,27 +82,38 @@ Cards.helpers({
});
return cardLabels;
},
+
hasLabel(labelId) {
return _.contains(this.labelIds, labelId);
},
+
user() {
return Users.findOne(this.userId);
},
+
isAssigned(memberId) {
return _.contains(this.members, memberId);
},
+
activities() {
return Activities.find({ cardId: this._id }, { sort: { createdAt: -1 }});
},
+
comments() {
return CardComments.find({ cardId: this._id }, { sort: { createdAt: -1 }});
},
+
attachments() {
return Attachments.find({ cardId: this._id }, { sort: { uploadedAt: -1 }});
},
+
cover() {
- return Attachments.findOne(this.coverId);
+ const cover = Attachments.findOne(this.coverId);
+ // if we return a cover before it is fully stored, we will get errors when we try to display it
+ // todo XXX we could return a default "upload pending" image in the meantime?
+ return cover && cover.url() && cover;
},
+
absoluteUrl() {
const board = this.board();
return FlowRouter.path('card', {
@@ -148,33 +122,87 @@ Cards.helpers({
cardId: this._id,
});
},
+
rootUrl() {
return Meteor.absoluteUrl(this.absoluteUrl().replace('/', ''));
},
});
-CardComments.helpers({
- user() {
- return Users.findOne(this.userId);
+Cards.mutations({
+ archive() {
+ return { $set: { archived: true }};
},
-});
-CardComments.hookOptions.after.update = { fetchPrevious: false };
-Cards.before.insert((userId, doc) => {
- doc.createdAt = new Date();
- doc.dateLastActivity = new Date();
+ restore() {
+ return { $set: { archived: false }};
+ },
- // defaults
- doc.archived = false;
+ setTitle(title) {
+ return { $set: { title }};
+ },
- // userId native set.
- if (!doc.userId)
- doc.userId = userId;
+ setDescription(description) {
+ return { $set: { description }};
+ },
+
+ move(listId, sortIndex) {
+ const mutatedFields = { listId };
+ if (sortIndex) {
+ mutatedFields.sort = sortIndex;
+ }
+ return { $set: mutatedFields };
+ },
+
+ addLabel(labelId) {
+ return { $addToSet: { labelIds: labelId }};
+ },
+
+ removeLabel(labelId) {
+ return { $pull: { labelIds: labelId }};
+ },
+
+ toggleLabel(labelId) {
+ if (this.labelIds && this.labelIds.indexOf(labelId) > -1) {
+ return this.removeLabel(labelId);
+ } else {
+ return this.addLabel(labelId);
+ }
+ },
+
+ assignMember(memberId) {
+ return { $addToSet: { members: memberId }};
+ },
+
+ unassignMember(memberId) {
+ return { $pull: { members: memberId }};
+ },
+
+ toggleMember(memberId) {
+ if (this.members && this.members.indexOf(memberId) > -1) {
+ return this.unassignMember(memberId);
+ } else {
+ return this.assignMember(memberId);
+ }
+ },
+
+ setCover(coverId) {
+ return { $set: { coverId }};
+ },
+
+ unsetCover() {
+ return { $unset: { coverId: '' }};
+ },
});
-CardComments.before.insert((userId, doc) => {
+Cards.before.insert((userId, doc) => {
doc.createdAt = new Date();
- doc.userId = userId;
+ doc.dateLastActivity = new Date();
+ if(!doc.hasOwnProperty('archived')){
+ doc.archived = false;
+ }
+ if (!doc.userId) {
+ doc.userId = userId;
+ }
});
if (Meteor.isServer) {
@@ -264,21 +292,4 @@ if (Meteor.isServer) {
cardId: doc._id,
});
});
-
- CardComments.after.insert((userId, doc) => {
- Activities.insert({
- userId,
- activityType: 'addComment',
- boardId: doc.boardId,
- cardId: doc.cardId,
- commentId: doc._id,
- });
- });
-
- CardComments.after.remove((userId, doc) => {
- const activity = Activities.findOne({ commentId: doc._id });
- if (activity) {
- Activities.remove(activity._id);
- }
- });
}
diff --git a/models/import.js b/models/import.js
new file mode 100644
index 00000000..4be1273c
--- /dev/null
+++ b/models/import.js
@@ -0,0 +1,511 @@
+const DateString = Match.Where(function (dateAsString) {
+ check(dateAsString, String);
+ return moment(dateAsString, moment.ISO_8601).isValid();
+});
+
+class TrelloCreator {
+ constructor(data) {
+ // we log current date, to use the same timestamp for all our actions.
+ // this helps to retrieve all elements performed by the same import.
+ this._nowDate = new Date();
+ // The object creation dates, indexed by Trello id
+ // (so we only parse actions once!)
+ this.createdAt = {
+ board: null,
+ cards: {},
+ lists: {},
+ };
+ // The object creator Trello Id, indexed by the object Trello id
+ // (so we only parse actions once!)
+ this.createdBy = {
+ cards: {}, // only cards have a field for that
+ };
+
+ // Map of labels Trello ID => Wekan ID
+ this.labels = {};
+ // Map of lists Trello ID => Wekan ID
+ this.lists = {};
+ // The comments, indexed by Trello card id (to map when importing cards)
+ this.comments = {};
+ // the members, indexed by Trello member id => Wekan user ID
+ this.members = data.membersMapping ? data.membersMapping : {};
+
+ // maps a trelloCardId to an array of trelloAttachments
+ this.attachments = {};
+ }
+
+ /**
+ * If dateString is provided,
+ * return the Date it represents.
+ * If not, will return the date when it was first called.
+ * This is useful for us, as we want all import operations to
+ * have the exact same date for easier later retrieval.
+ *
+ * @param {String} dateString a properly formatted Date
+ */
+ _now(dateString) {
+ if(dateString) {
+ return new Date(dateString);
+ }
+ if(!this._nowDate) {
+ this._nowDate = new Date();
+ }
+ return this._nowDate;
+ }
+
+ /**
+ * if trelloUserId is provided and we have a mapping,
+ * return it.
+ * Otherwise return current logged user.
+ * @param trelloUserId
+ * @private
+ */
+ _user(trelloUserId) {
+ if(trelloUserId && this.members[trelloUserId]) {
+ return this.members[trelloUserId];
+ }
+ return Meteor.userId();
+ }
+
+ checkActions(trelloActions) {
+ check(trelloActions, [Match.ObjectIncluding({
+ data: Object,
+ date: DateString,
+ type: String,
+ })]);
+ // XXX we could perform more thorough checks based on action type
+ }
+
+ checkBoard(trelloBoard) {
+ check(trelloBoard, Match.ObjectIncluding({
+ closed: Boolean,
+ name: String,
+ prefs: Match.ObjectIncluding({
+ // XXX refine control by validating 'background' against a list of
+ // allowed values (is it worth the maintenance?)
+ background: String,
+ permissionLevel: Match.Where((value) => {
+ return ['org', 'private', 'public'].indexOf(value)>= 0;
+ }),
+ }),
+ }));
+ }
+
+ checkCards(trelloCards) {
+ check(trelloCards, [Match.ObjectIncluding({
+ closed: Boolean,
+ dateLastActivity: DateString,
+ desc: String,
+ idLabels: [String],
+ idMembers: [String],
+ name: String,
+ pos: Number,
+ })]);
+ }
+
+ checkLabels(trelloLabels) {
+ check(trelloLabels, [Match.ObjectIncluding({
+ // XXX refine control by validating 'color' against a list of allowed
+ // values (is it worth the maintenance?)
+ color: String,
+ name: String,
+ })]);
+ }
+
+ checkLists(trelloLists) {
+ check(trelloLists, [Match.ObjectIncluding({
+ closed: Boolean,
+ name: String,
+ })]);
+ }
+
+ // You must call parseActions before calling this one.
+ createBoardAndLabels(trelloBoard) {
+ const boardToCreate = {
+ archived: trelloBoard.closed,
+ color: this.getColor(trelloBoard.prefs.background),
+ // very old boards won't have a creation activity so no creation date
+ createdAt: this._now(this.createdAt.board),
+ labels: [],
+ members: [{
+ userId: Meteor.userId(),
+ isAdmin: true,
+ isActive: true,
+ }],
+ permission: this.getPermission(trelloBoard.prefs.permissionLevel),
+ slug: getSlug(trelloBoard.name) || 'board',
+ stars: 0,
+ title: trelloBoard.name,
+ };
+ // now add other members
+ if(trelloBoard.memberships) {
+ trelloBoard.memberships.forEach((trelloMembership) => {
+ const trelloId = trelloMembership.idMember;
+ // do we have a mapping?
+ if(this.members[trelloId]) {
+ const wekanId = this.members[trelloId];
+ // do we already have it in our list?
+ const wekanMember = boardToCreate.members.find((wekanMember) => wekanMember.userId === wekanId);
+ if(wekanMember) {
+ // we're already mapped, but maybe with lower rights
+ if(!wekanMember.isAdmin) {
+ wekanMember.isAdmin = this.getAdmin(trelloMembership.memberType);
+ }
+ } else {
+ boardToCreate.members.push({
+ userId: wekanId,
+ isAdmin: this.getAdmin(trelloMembership.memberType),
+ isActive: true,
+ });
+ }
+ }
+ });
+ }
+ trelloBoard.labels.forEach((label) => {
+ const labelToCreate = {
+ _id: Random.id(6),
+ color: label.color,
+ name: label.name,
+ };
+ // We need to remember them by Trello ID, as this is the only ref we have
+ // when importing cards.
+ this.labels[label.id] = labelToCreate._id;
+ boardToCreate.labels.push(labelToCreate);
+ });
+ const boardId = Boards.direct.insert(boardToCreate);
+ Boards.direct.update(boardId, {$set: {modifiedAt: this._now()}});
+ // log activity
+ Activities.direct.insert({
+ activityType: 'importBoard',
+ boardId,
+ createdAt: this._now(),
+ source: {
+ id: trelloBoard.id,
+ system: 'Trello',
+ url: trelloBoard.url,
+ },
+ // We attribute the import to current user,
+ // not the author from the original object.
+ userId: this._user(),
+ });
+ return boardId;
+ }
+
+ /**
+ * Create the Wekan cards corresponding to the supplied Trello cards,
+ * as well as all linked data: activities, comments, and attachments
+ * @param trelloCards
+ * @param boardId
+ * @returns {Array}
+ */
+ createCards(trelloCards, boardId) {
+ const result = [];
+ trelloCards.forEach((card) => {
+ const cardToCreate = {
+ archived: card.closed,
+ boardId,
+ // very old boards won't have a creation activity so no creation date
+ createdAt: this._now(this.createdAt.cards[card.id]),
+ dateLastActivity: this._now(),
+ description: card.desc,
+ listId: this.lists[card.idList],
+ sort: card.pos,
+ title: card.name,
+ // we attribute the card to its creator if available
+ userId: this._user(this.createdBy.cards[card.id]),
+ };
+ // add labels
+ if (card.idLabels) {
+ cardToCreate.labelIds = card.idLabels.map((trelloId) => {
+ return this.labels[trelloId];
+ });
+ }
+ // add members {
+ if(card.idMembers) {
+ const wekanMembers = [];
+ // we can't just map, as some members may not have been mapped
+ card.idMembers.forEach((trelloId) => {
+ if(this.members[trelloId]) {
+ const wekanId = this.members[trelloId];
+ // we may map multiple Trello members to the same wekan user
+ // in which case we risk adding the same user multiple times
+ if(!wekanMembers.find((wId) => wId === wekanId)){
+ wekanMembers.push(wekanId);
+ }
+ }
+ return true;
+ });
+ if(wekanMembers.length>0) {
+ cardToCreate.members = wekanMembers;
+ }
+ }
+ // insert card
+ const cardId = Cards.direct.insert(cardToCreate);
+ // log activity
+ Activities.direct.insert({
+ activityType: 'importCard',
+ boardId,
+ cardId,
+ createdAt: this._now(),
+ listId: cardToCreate.listId,
+ source: {
+ id: card.id,
+ system: 'Trello',
+ url: card.url,
+ },
+ // we attribute the import to current user,
+ // not the author of the original card
+ userId: this._user(),
+ });
+ // add comments
+ const comments = this.comments[card.id];
+ if (comments) {
+ comments.forEach((comment) => {
+ const commentToCreate = {
+ boardId,
+ cardId,
+ createdAt: this._now(comment.date),
+ text: comment.data.text,
+ // we attribute the comment to the original author, default to current user
+ userId: this._user(comment.memberCreator.id),
+ };
+ // dateLastActivity will be set from activity insert, no need to
+ // update it ourselves
+ const commentId = CardComments.direct.insert(commentToCreate);
+ Activities.direct.insert({
+ activityType: 'addComment',
+ boardId: commentToCreate.boardId,
+ cardId: commentToCreate.cardId,
+ commentId,
+ createdAt: this._now(commentToCreate.createdAt),
+ // we attribute the addComment (not the import)
+ // to the original author - it is needed by some UI elements.
+ userId: commentToCreate.userId,
+ });
+ });
+ }
+ const attachments = this.attachments[card.id];
+ const trelloCoverId = card.idAttachmentCover;
+ if (attachments) {
+ attachments.forEach((att) => {
+ const file = new FS.File();
+ // Simulating file.attachData on the client generates multiple errors
+ // - HEAD returns null, which causes exception down the line
+ // - the template then tries to display the url to the attachment which causes other errors
+ // so we make it server only, and let UI catch up once it is done, forget about latency comp.
+ if(Meteor.isServer) {
+ file.attachData(att.url, function (error) {
+ file.boardId = boardId;
+ file.cardId = cardId;
+ if (error) {
+ throw(error);
+ } else {
+ const wekanAtt = Attachments.insert(file, () => {
+ // we do nothing
+ });
+ //
+ if(trelloCoverId === att.id) {
+ Cards.direct.update(cardId, { $set: {coverId: wekanAtt._id}});
+ }
+ }
+ });
+ }
+ // todo XXX set cover - if need be
+ });
+ }
+ result.push(cardId);
+ });
+ return result;
+ }
+
+ // Create labels if they do not exist and load this.labels.
+ createLabels(trelloLabels, board) {
+ trelloLabels.forEach((label) => {
+ const color = label.color;
+ const name = label.name;
+ const existingLabel = board.getLabel(name, color);
+ if (existingLabel) {
+ this.labels[label.id] = existingLabel._id;
+ } else {
+ const idLabelCreated = board.pushLabel(name, color);
+ this.labels[label.id] = idLabelCreated;
+ }
+ });
+ }
+
+ createLists(trelloLists, boardId) {
+ trelloLists.forEach((list) => {
+ const listToCreate = {
+ archived: list.closed,
+ boardId,
+ // We are being defensing here by providing a default date (now) if the
+ // creation date wasn't found on the action log. This happen on old
+ // Trello boards (eg from 2013) that didn't log the 'createList' action
+ // we require.
+ createdAt: this._now(this.createdAt.lists[list.id]),
+ title: list.name,
+ };
+ const listId = Lists.direct.insert(listToCreate);
+ Lists.direct.update(listId, {$set: {'updatedAt': this._now()}});
+ this.lists[list.id] = listId;
+ // log activity
+ Activities.direct.insert({
+ activityType: 'importList',
+ boardId,
+ createdAt: this._now(),
+ listId,
+ source: {
+ id: list.id,
+ system: 'Trello',
+ },
+ // We attribute the import to current user,
+ // not the creator of the original object
+ userId: this._user(),
+ });
+ });
+ }
+
+ getAdmin(trelloMemberType) {
+ return trelloMemberType === 'admin';
+ }
+
+ getColor(trelloColorCode) {
+ // trello color name => wekan color
+ const mapColors = {
+ 'blue': 'belize',
+ 'orange': 'pumpkin',
+ 'green': 'nephritis',
+ 'red': 'pomegranate',
+ 'purple': 'wisteria',
+ 'pink': 'pomegranate',
+ 'lime': 'nephritis',
+ 'sky': 'belize',
+ 'grey': 'midnight',
+ };
+ const wekanColor = mapColors[trelloColorCode];
+ return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0];
+ }
+
+ getPermission(trelloPermissionCode) {
+ if (trelloPermissionCode === 'public') {
+ return 'public';
+ }
+ // Wekan does NOT have organization level, so we default both 'private' and
+ // 'org' to private.
+ return 'private';
+ }
+
+ parseActions(trelloActions) {
+ trelloActions.forEach((action) => {
+ switch (action.type) {
+ case 'addAttachmentToCard':
+ // We have to be cautious, because the attachment could have been removed later.
+ // In that case Trello still reports its addition, but removes its 'url' field.
+ // So we test for that
+ const trelloAttachment = action.data.attachment;
+ if(trelloAttachment.url) {
+ // we cannot actually create the Wekan attachment, because we don't yet
+ // have the cards to attach it to, so we store it in the instance variable.
+ const trelloCardId = action.data.card.id;
+ if(!this.attachments[trelloCardId]) {
+ this.attachments[trelloCardId] = [];
+ }
+ this.attachments[trelloCardId].push(trelloAttachment);
+ }
+ break;
+ case 'commentCard':
+ const id = action.data.card.id;
+ if (this.comments[id]) {
+ this.comments[id].push(action);
+ } else {
+ this.comments[id] = [action];
+ }
+ break;
+ case 'createBoard':
+ this.createdAt.board = action.date;
+ break;
+ case 'createCard':
+ const cardId = action.data.card.id;
+ this.createdAt.cards[cardId] = action.date;
+ this.createdBy.cards[cardId] = action.idMemberCreator;
+ break;
+ case 'createList':
+ const listId = action.data.list.id;
+ this.createdAt.lists[listId] = action.date;
+ break;
+ default:
+ // do nothing
+ break;
+ }
+ });
+ }
+}
+
+Meteor.methods({
+ importTrelloBoard(trelloBoard, data) {
+ const trelloCreator = new TrelloCreator(data);
+
+ // 1. check all parameters are ok from a syntax point of view
+ try {
+ check(data, {
+ membersMapping: Match.Optional(Object),
+ });
+ trelloCreator.checkActions(trelloBoard.actions);
+ trelloCreator.checkBoard(trelloBoard);
+ trelloCreator.checkLabels(trelloBoard.labels);
+ trelloCreator.checkLists(trelloBoard.lists);
+ trelloCreator.checkCards(trelloBoard.cards);
+ } catch (e) {
+ throw new Meteor.Error('error-json-schema');
+ }
+
+ // 2. check parameters are ok from a business point of view (exist &
+ // authorized) nothing to check, everyone can import boards in their account
+
+ // 3. create all elements
+ trelloCreator.parseActions(trelloBoard.actions);
+ const boardId = trelloCreator.createBoardAndLabels(trelloBoard);
+ trelloCreator.createLists(trelloBoard.lists, boardId);
+ trelloCreator.createCards(trelloBoard.cards, boardId);
+ // XXX add members
+ return boardId;
+ },
+
+ importTrelloCard(trelloCard, data) {
+ const trelloCreator = new TrelloCreator(data);
+
+ // 1. check parameters are ok from a syntax point of view
+ try {
+ check(data, {
+ listId: String,
+ sortIndex: Number,
+ membersMapping: Match.Optional(Object),
+ });
+ trelloCreator.checkCards([trelloCard]);
+ trelloCreator.checkLabels(trelloCard.labels);
+ trelloCreator.checkActions(trelloCard.actions);
+ } catch(e) {
+ throw new Meteor.Error('error-json-schema');
+ }
+
+ // 2. check parameters are ok from a business point of view (exist &
+ // authorized)
+ const list = Lists.findOne(data.listId);
+ if (!list) {
+ throw new Meteor.Error('error-list-doesNotExist');
+ }
+ if (Meteor.isServer) {
+ if (!allowIsBoardMember(Meteor.userId(), Boards.findOne(list.boardId))) {
+ throw new Meteor.Error('error-board-notAMember');
+ }
+ }
+
+ // 3. create all elements
+ trelloCreator.lists[trelloCard.idList] = data.listId;
+ trelloCreator.parseActions(trelloCard.actions);
+ const board = list.board();
+ trelloCreator.createLabels(trelloCard.labels, board);
+ const cardIds = trelloCreator.createCards([trelloCard], board._id);
+ return cardIds[0];
+ },
+});
diff --git a/collections/lists.js b/models/lists.js
index 0c6ba407..4e4a1134 100644
--- a/collections/lists.js
+++ b/models/lists.js
@@ -27,20 +27,18 @@ Lists.attachSchema(new SimpleSchema({
},
}));
-if (Meteor.isServer) {
- Lists.allow({
- insert(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- update(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- remove(userId, doc) {
- return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
- },
- fetch: ['boardId'],
- });
-}
+Lists.allow({
+ insert(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ update(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ remove(userId, doc) {
+ return allowIsBoardMember(userId, Boards.findOne(doc.boardId));
+ },
+ fetch: ['boardId'],
+});
Lists.helpers({
cards() {
@@ -49,12 +47,30 @@ Lists.helpers({
archived: false,
}), { sort: ['sort'] });
},
+
+ allCards() {
+ return Cards.find({ listId: this._id });
+ },
+
board() {
return Boards.findOne(this.boardId);
},
});
-// HOOKS
+Lists.mutations({
+ rename(title) {
+ return { $set: { title }};
+ },
+
+ archive() {
+ return { $set: { archived: true }};
+ },
+
+ restore() {
+ return { $set: { archived: false }};
+ },
+});
+
Lists.hookOptions.after.update = { fetchPrevious: false };
Lists.before.insert((userId, doc) => {
diff --git a/collections/unsavedEdits.js b/models/unsavedEdits.js
index 87a70e22..87a70e22 100644
--- a/collections/unsavedEdits.js
+++ b/models/unsavedEdits.js
diff --git a/models/users.js b/models/users.js
new file mode 100644
index 00000000..2c9ae380
--- /dev/null
+++ b/models/users.js
@@ -0,0 +1,291 @@
+Users = Meteor.users; // eslint-disable-line meteor/collections
+
+// Search a user in the complete server database by its name or username. This
+// is used for instance to add a new user to a board.
+const searchInFields = ['username', 'profile.fullname'];
+Users.initEasySearch(searchInFields, {
+ use: 'mongo-db',
+ returnFields: [...searchInFields, 'profile.avatarUrl'],
+});
+
+if (Meteor.isClient) {
+ Users.helpers({
+ isBoardMember() {
+ const board = Boards.findOne(Session.get('currentBoard'));
+ return board &&
+ _.contains(_.pluck(board.members, 'userId'), this._id) &&
+ _.where(board.members, {userId: this._id})[0].isActive;
+ },
+
+ isBoardAdmin() {
+ const board = Boards.findOne(Session.get('currentBoard'));
+ return board &&
+ this.isBoardMember(board) &&
+ _.where(board.members, {userId: this._id})[0].isAdmin;
+ },
+ });
+}
+
+Users.helpers({
+ boards() {
+ return Boards.find({ userId: this._id });
+ },
+
+ starredBoards() {
+ const {starredBoards = []} = this.profile;
+ return Boards.find({archived: false, _id: {$in: starredBoards}});
+ },
+
+ hasStarred(boardId) {
+ const {starredBoards = []} = this.profile;
+ return _.contains(starredBoards, boardId);
+ },
+
+ invitedBoards() {
+ const {invitedBoards = []} = this.profile;
+ return Boards.find({archived: false, _id: {$in: invitedBoards}});
+ },
+
+ isInvitedTo(boardId) {
+ const {invitedBoards = []} = this.profile;
+ return _.contains(invitedBoards, boardId);
+ },
+
+ getAvatarUrl() {
+ // Although we put the avatar picture URL in the `profile` object, we need
+ // to support Sandstorm which put in the `picture` attribute by default.
+ // XXX Should we move both cases to `picture`?
+ if (this.picture) {
+ return this.picture;
+ } else if (this.profile && this.profile.avatarUrl) {
+ return this.profile.avatarUrl;
+ } else {
+ return null;
+ }
+ },
+
+ getInitials() {
+ const profile = this.profile || {};
+ if (profile.initials)
+ return profile.initials;
+
+ else if (profile.fullname) {
+ return profile.fullname.split(/\s+/).reduce((memo = '', word) => {
+ return memo + word[0];
+ }).toUpperCase();
+
+ } else {
+ return this.username[0].toUpperCase();
+ }
+ },
+
+ getName() {
+ const profile = this.profile || {};
+ return profile.fullname || this.username;
+ },
+
+ getLanguage() {
+ const profile = this.profile || {};
+ return profile.language || 'en';
+ },
+});
+
+Users.mutations({
+ toggleBoardStar(boardId) {
+ const queryKind = this.hasStarred(boardId) ? '$pull' : '$addToSet';
+ return {
+ [queryKind]: {
+ 'profile.starredBoards': boardId,
+ },
+ };
+ },
+
+ addInvite(boardId) {
+ return {
+ $addToSet: {
+ 'profile.invitedBoards': boardId,
+ },
+ };
+ },
+
+ removeInvite(boardId) {
+ return {
+ $pull: {
+ 'profile.invitedBoards': boardId,
+ },
+ };
+ },
+
+ setAvatarUrl(avatarUrl) {
+ return { $set: { 'profile.avatarUrl': avatarUrl }};
+ },
+});
+
+Meteor.methods({
+ setUsername(username) {
+ check(username, String);
+ const nUsersWithUsername = Users.find({ username }).count();
+ if (nUsersWithUsername > 0) {
+ throw new Meteor.Error('username-already-taken');
+ } else {
+ Users.update(this.userId, {$set: { username }});
+ }
+ },
+});
+
+if (Meteor.isServer) {
+ Meteor.methods({
+ // we accept userId, username, email
+ inviteUserToBoard(username, boardId) {
+ check(username, String);
+ check(boardId, String);
+
+ const inviter = Meteor.user();
+ const board = Boards.findOne(boardId);
+ const allowInvite = inviter &&
+ board &&
+ board.members &&
+ _.contains(_.pluck(board.members, 'userId'), inviter._id) &&
+ _.where(board.members, {userId: inviter._id})[0].isActive &&
+ _.where(board.members, {userId: inviter._id})[0].isAdmin;
+ if (!allowInvite) throw new Meteor.Error('error-board-notAMember');
+
+ this.unblock();
+
+ const posAt = username.indexOf('@');
+ let user = null;
+ if (posAt>=0) {
+ user = Users.findOne({emails: {$elemMatch: {address: username}}});
+ } else {
+ user = Users.findOne(username) || Users.findOne({ username });
+ }
+ if (user) {
+ if (user._id === inviter._id) throw new Meteor.Error('error-user-notAllowSelf');
+ } else {
+ if (posAt <= 0) throw new Meteor.Error('error-user-doesNotExist');
+
+ const email = username;
+ username = email.substring(0, posAt);
+ const newUserId = Accounts.createUser({ username, email });
+ if (!newUserId) throw new Meteor.Error('error-user-notCreated');
+ // assume new user speak same language with inviter
+ if (inviter.profile && inviter.profile.language) {
+ Users.update(newUserId, {
+ $set: {
+ 'profile.language': inviter.profile.language,
+ },
+ });
+ }
+ Accounts.sendEnrollmentEmail(newUserId);
+ user = Users.findOne(newUserId);
+ }
+
+ board.addMember(user._id);
+ user.addInvite(boardId);
+
+ if (!process.env.MAIL_URL || (!Email)) return { username: user.username };
+
+ try {
+ let rootUrl = Meteor.absoluteUrl.defaultOptions.rootUrl || '';
+ if (!rootUrl.endsWith('/')) rootUrl = `${rootUrl}/`;
+ const boardUrl = `${rootUrl}b/${board._id}/${board.slug}`;
+
+ const vars = {
+ user: user.username,
+ inviter: inviter.username,
+ board: board.title,
+ url: boardUrl,
+ };
+ const lang = user.getLanguage();
+ Email.send({
+ to: user.emails[0].address,
+ from: Accounts.emailTemplates.from,
+ subject: TAPi18n.__('email-invite-subject', vars, lang),
+ text: TAPi18n.__('email-invite-text', vars, lang),
+ });
+ } catch (e) {
+ throw new Meteor.Error('email-fail', e.message);
+ }
+
+ return { username: user.username, email: user.emails[0].address };
+ },
+ });
+}
+
+Users.before.insert((userId, doc) => {
+ doc.profile = doc.profile || {};
+
+ if (!doc.username && doc.profile.name) {
+ doc.username = doc.profile.name.toLowerCase().replace(/\s/g, '');
+ }
+});
+
+if (Meteor.isServer) {
+ // Let mongoDB ensure username unicity
+ Meteor.startup(() => {
+ Users._collection._ensureIndex({
+ username: 1,
+ }, { unique: true });
+ });
+
+ // Each board document contains the de-normalized number of users that have
+ // starred it. If the user star or unstar a board, we need to update this
+ // counter.
+ // We need to run this code on the server only, otherwise the incrementation
+ // will be done twice.
+ Users.after.update(function(userId, user, fieldNames) {
+ // The `starredBoards` list is hosted on the `profile` field. If this
+ // field hasn't been modificated we don't need to run this hook.
+ if (!_.contains(fieldNames, 'profile'))
+ return;
+
+ // To calculate a diff of board starred ids, we get both the previous
+ // and the newly board ids list
+ function getStarredBoardsIds(doc) {
+ return doc.profile && doc.profile.starredBoards;
+ }
+ const oldIds = getStarredBoardsIds(this.previous);
+ const newIds = getStarredBoardsIds(user);
+
+ // The _.difference(a, b) method returns the values from a that are not in
+ // b. We use it to find deleted and newly inserted ids by using it in one
+ // direction and then in the other.
+ function incrementBoards(boardsIds, inc) {
+ boardsIds.forEach((boardId) => {
+ Boards.update(boardId, {$inc: {stars: inc}});
+ });
+ }
+ incrementBoards(_.difference(oldIds, newIds), -1);
+ incrementBoards(_.difference(newIds, oldIds), +1);
+ });
+
+ // XXX i18n
+ Users.after.insert((userId, doc) => {
+ const ExampleBoard = {
+ title: 'Welcome Board',
+ userId: doc._id,
+ permission: 'private',
+ };
+
+ // Insert the Welcome Board
+ Boards.insert(ExampleBoard, (err, boardId) => {
+
+ ['Basics', 'Advanced'].forEach((title) => {
+ const list = {
+ title,
+ boardId,
+ userId: ExampleBoard.userId,
+
+ // XXX Not certain this is a bug, but we except these fields get
+ // inserted by the Lists.before.insert collection-hook. Since this
+ // hook is not called in this case, we have to dublicate the logic and
+ // set them here.
+ archived: false,
+ createdAt: new Date(),
+ };
+
+ Lists.insert(list);
+ });
+ });
+ });
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..77a9fb74
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "wekan",
+ "version": "1.0.0",
+ "description": "The open-source Trello-like kanban",
+ "private": true,
+ "scripts": {
+ "lint": "eslint .",
+ "test": "npm run --silent lint"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/wekan/wekan.git"
+ },
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/wekan/wekan/issues"
+ },
+ "homepage": "http://wekan.io",
+ "devDependencies": {
+ "babel-eslint": "4.1.3",
+ "eslint": "1.7.3",
+ "eslint-plugin-meteor": "1.7.0"
+ }
+}
diff --git a/sandstorm-pkgdef.capnp b/sandstorm-pkgdef.capnp
index 0e41b5a1..586d2d71 100644
--- a/sandstorm-pkgdef.capnp
+++ b/sandstorm-pkgdef.capnp
@@ -22,7 +22,7 @@ const pkgdef :Spk.PackageDefinition = (
appTitle = (defaultText = "Wekan"),
# The name of the app as it is displayed to the user.
- appVersion = 5,
+ appVersion = 6,
# Increment this for every release.
appMarketingVersion = (defaultText = "0.9.0"),
diff --git a/sandstorm.js b/sandstorm.js
index c430c3a8..a711a960 100644
--- a/sandstorm.js
+++ b/sandstorm.js
@@ -21,24 +21,13 @@ if (isSandstorm && Meteor.isServer) {
permission: 'public',
};
- // This function should probably be handled by `accounts-sandstorm` but
- // apparently meteor-core misses an API to handle that cleanly, cf.
- // https://github.com/meteor/meteor/blob/ff783e9a12ffa04af6fd163843a563c9f4bbe8c1/packages/accounts-base/accounts_server.js#L1143
- function updateUserAvatar(userId, avatarUrl) {
- Users.update(userId, {
- $set: {
- 'profile.avatarUrl': avatarUrl,
- },
- });
- }
-
function updateUserPermissions(userId, permissions) {
const isActive = permissions.indexOf('participate') > -1;
const isAdmin = permissions.indexOf('configure') > -1;
const permissionDoc = { userId, isActive, isAdmin };
const boardMembers = Boards.findOne(sandstormBoard._id).members;
- const memberIndex = _.indexOf(_.pluck(boardMembers, 'userId'), userId);
+ const memberIndex = _.pluck(boardMembers, 'userId').indexOf(userId);
let modifier;
if (memberIndex > -1)
@@ -59,33 +48,28 @@ if (isSandstorm && Meteor.isServer) {
// and the home page was accessible by pressing the back button of the
// browser, a server-side redirection solves both of these issues.
//
- // XXX Maybe sandstorm manifest could provide some kind of "home URL"?
+ // XXX Maybe the sandstorm http-bridge could provide some kind of "home URL"
+ // in the manifest?
const base = req.headers['x-sandstorm-base-path'];
// XXX If this routing scheme changes, this will break. We should generate
// the location URL using the router, but at the time of writing, the
// it is only accessible on the client.
- const path = `/boards/${sandstormBoard._id}/${sandstormBoard.slug}`;
+ const boardPath = `/b/${sandstormBoard._id}/${sandstormBoard.slug}`;
res.writeHead(301, {
- Location: base + path,
+ Location: base + boardPath,
});
res.end();
// `accounts-sandstorm` populate the Users collection when new users
- // accesses the document, but in case a already known user come back, we
+ // accesses the document, but in case a already known user comes back, we
// need to update his associated document to match the request HTTP headers
// informations.
const user = Users.findOne({
'services.sandstorm.id': req.headers['x-sandstorm-user-id'],
});
if (user) {
- const userId = user._id;
- const avatarUrl = req.headers['x-sandstorm-user-picture'];
- const permissions = req.headers['x-sandstorm-permissions'].split(',') || [];
-
- // XXX The user may also change his name, we should handle it.
- updateUserAvatar(userId, avatarUrl);
- updateUserPermissions(userId, permissions);
+ updateUserPermissions(user._id, user.permissions);
}
});
@@ -96,25 +80,75 @@ if (isSandstorm && Meteor.isServer) {
// despite the appearances `userId` is null in this block.
Users.after.insert((userId, doc) => {
if (!Boards.findOne(sandstormBoard._id)) {
- Boards.insert(sandstormBoard, {validate: false});
+ Boards.insert(sandstormBoard, { validate: false });
Activities.update(
{ activityTypeId: sandstormBoard._id },
{ $set: { userId: doc._id }}
);
}
+ // We rely on username uniqueness for the user mention feature, but
+ // Sandstorm doesn't enforce this property -- see #352. Our strategy to
+ // generate unique usernames from the Sandstorm `preferredHandle` is to
+ // append a number that we increment until we generate a username that no
+ // one already uses (eg, 'max', 'max1', 'max2').
+ function generateUniqueUsername(username, appendNumber) {
+ return username + String(appendNumber === 0 ? '' : appendNumber);
+ }
+
+ const username = doc.services.sandstorm.preferredHandle;
+ let appendNumber = 0;
+ while (Users.findOne({
+ _id: { $ne: doc._id },
+ username: generateUniqueUsername(username, appendNumber),
+ })) {
+ appendNumber += 1;
+ }
+
+ Users.update(doc._id, {
+ $set: {
+ username: generateUniqueUsername(username, appendNumber),
+ 'profile.fullname': doc.services.sandstorm.name,
+ },
+ });
+
updateUserPermissions(doc._id, doc.services.sandstorm.permissions);
});
+
+ // LibreBoard v0.8 didn’t implement the Sandstorm sharing model and instead
+ // kept the visibility setting (“public” or “private”) in the UI as does the
+ // main Meteor application. We need to enforce “public” visibility as the
+ // sharing is now handled by Sandstorm.
+ // See https://github.com/wekan/wekan/issues/346
+ Migrations.add('enforce-public-visibility-for-sandstorm', () => {
+ Boards.update('sandstorm', { $set: { permission: 'public' }});
+ });
}
if (isSandstorm && Meteor.isClient) {
+ // Since the Sandstorm grain is displayed in an iframe of the Sandstorm shell,
+ // we need to explicitly expose meta data like the page title or the URL path
+ // so that they could appear in the browser window.
+ // See https://docs.sandstorm.io/en/latest/developing/path/
+ function updateSandstormMetaData(msg) {
+ return window.parent.postMessage(msg, '*');
+ }
+
+ FlowRouter.triggers.enter([({ path }) => {
+ updateSandstormMetaData({ setPath: path });
+ }]);
+
+ Tracker.autorun(() => {
+ updateSandstormMetaData({ setTitle: DocHead.getTitle() });
+ });
+
// XXX Hack. `Meteor.absoluteUrl` doesn't work in Sandstorm, since every
// session has a different URL whereas Meteor computes absoluteUrl based on
// the ROOT_URL environment variable. So we overwrite this function on a
// sandstorm client to return relative paths instead of absolutes.
const _absoluteUrl = Meteor.absoluteUrl;
const _defaultOptions = Meteor.absoluteUrl.defaultOptions;
- Meteor.absoluteUrl = (path, options) => {
+ Meteor.absoluteUrl = (path, options) => { // eslint-disable-line meteor/core
const url = _absoluteUrl(path, options);
return url.replace(/^https?:\/\/127\.0\.0\.1:[0-9]{2,5}/, '');
};
diff --git a/server/migrations.js b/server/migrations.js
index 05f5ff7d..99125976 100644
--- a/server/migrations.js
+++ b/server/migrations.js
@@ -4,6 +4,12 @@
//
// Migrations.add(name, migrationCallback, optionalOrder);
+// Note that we have extra migrations defined in `sandstorm.js` that are
+// exclusive to Sandstorm and shouldn’t be executed in the general case.
+// XXX I guess if we had ES6 modules we could
+// `import { isSandstorm } from sandstorm.js` and define the migration here as
+// well, but for now I want to avoid definied too many globals.
+
// In the context of migration functions we don't want to validate database
// mutation queries against the current (ie, latest) collection schema. Doing
// that would work at the time we write the migration but would break in the
@@ -37,7 +43,7 @@ Migrations.add('board-background-color', () => {
});
Migrations.add('lowercase-board-permission', () => {
- _.forEach(['Public', 'Private'], (permission) => {
+ ['Public', 'Private'].forEach((permission) => {
Boards.update(
{ permission },
{ $set: { permission: permission.toLowerCase() } },
@@ -110,11 +116,11 @@ Migrations.add('add-member-isactive-field', () => {
const formerUsers = _.difference(allUsersWithSomeActivity, currentUsers);
const newMemberSet = [];
- _.forEach(board.members, (member) => {
+ board.members.forEach((member) => {
member.isActive = true;
newMemberSet.push(member);
});
- _.forEach(formerUsers, (userId) => {
+ formerUsers.forEach((userId) => {
newMemberSet.push({
userId,
isAdmin: false,
diff --git a/server/publications/boards.js b/server/publications/boards.js
index 403d0084..814d1df8 100644
--- a/server/publications/boards.js
+++ b/server/publications/boards.js
@@ -10,7 +10,7 @@ Meteor.publish('boards', function() {
// Defensive programming to verify that starredBoards has the expected
// format -- since the field is in the `profile` a user can modify it.
- const starredBoards = Users.findOne(this.userId).profile.starredBoards || [];
+ const {starredBoards = []} = Users.findOne(this.userId).profile;
check(starredBoards, [String]);
return Boards.find({
@@ -25,6 +25,7 @@ Meteor.publish('boards', function() {
archived: 1,
slug: 1,
title: 1,
+ description: 1,
color: 1,
members: 1,
permission: 1,
diff --git a/server/publications/fast-render.js b/server/publications/fast-render.js
new file mode 100644
index 00000000..e28b6f2e
--- /dev/null
+++ b/server/publications/fast-render.js
@@ -0,0 +1,7 @@
+FastRender.onAllRoutes(function() {
+ this.subscribe('boards');
+});
+
+FastRender.route('/b/:id/:slug', function({ id }) {
+ this.subscribe('board', id);
+});