Compare commits

...
232 Commits
Author SHA1 Message Date
Lauri Ojansivu 8b118c1939 v3.86 2020-03-24 21:01:14 +02:00
Lauri Ojansivu d7a0ab3c15 Update translations. 2020-03-24 20:48:58 +02:00
Lauri Ojansivu 8609e0e82d Update ChangeLog. 2020-03-24 20:45:25 +02:00
Lauri Ojansivu b9099a8b7e 1) Fix Pasting text into a card is adding a line before and after
(and multiplies by pasting more) by changing paste "p" to "br".
2) Fixes to summernote and markdown comment editors, related
   to keeping them open when adding comments, having
   @member mention not close card, and disabling clicking of
   @member mention.

Thanks to xet7 !

Closes #2890
2020-03-24 20:39:49 +02:00
Lauri Ojansivu 87a81789d1 Update ChangeLog. 2020-03-24 11:10:15 +02:00
Lauri Ojansivu 12ab8fac5d Fix Rich editor can not be disabled, regression from changes yesterday at Wekan v3.85.
Thanks to uusijani, vjrj and xet7 !

Closes #2967,
closes #104
2020-03-24 11:04:04 +02:00
Lauri Ojansivu 10b09b2da1 Fix typo. 2020-03-24 02:01:08 +02:00
Lauri Ojansivu 7c5f4a3849 v3.85 2020-03-23 23:01:58 +02:00
Lauri Ojansivu ec71849d84 Update ChangeLog. 2020-03-23 22:49:28 +02:00
Lauri Ojansivu 482682e500 SECURITY VULNERABILITY FIX: Fix XSS bug reported today 4 hours ago by Cyb3rjunky.
Logged in users could run javascript in input fields.
This affects Wekan versions v3.12-v3.84.
In [Wekan v3.12](https://github.com/wekan/wekan/blob/master/CHANGELOG.md#v312-2019-08-09-wekan-release)
there was [changes for XSS filter to allow inserting images, videos etc
on comment WYSIWYG editor](https://github.com/wekan/wekan/pull/2593)
so features related to that are now removed.
After this fix, Javascript in input fields is not executed.

Thanks to Cyb3rjunky and xet7 !
2020-03-23 22:29:20 +02:00
Lauri Ojansivu 3a6303e5c2 Update translations. 2020-03-23 22:25:08 +02:00
Lauri Ojansivu 81635d9217 Revert: Fix Pasting text into a card is adding a line before and after
(and multiplies by pasting more) by disabling WYSIWYG comment editor
for everyone.

Thanks to xet7.

TODO later: Fix wysiwyg editor paste bug.

Opens #2890
2020-03-23 22:23:31 +02:00
Lauri Ojansivu e8f89a8c4c Update translations. 2020-03-20 14:03:11 +02:00
Lauri Ojansivu 393482315f Fix typo. 2020-03-20 14:02:29 +02:00
Lauri Ojansivu cb67af02d7 Update ChangeLog. 2020-03-20 13:57:44 +02:00
Lauri Ojansivu a064b8d643 Fix Pasting text into a card is adding a line before and after
(and multiplies by pasting more) by disabling WYSIWYG comment editor
for everyone.

Closes #2890
2020-03-20 13:52:27 +02:00
Lauri Ojansivu 556621e60e Meteor 1.8 only in use at Sandstorm. 2020-03-17 14:04:50 +02:00
Lauri Ojansivu 0f95a513bf Meteor 1.8 only in use at Sandstorm. 2020-03-17 14:02:43 +02:00
Lauri Ojansivu dce624486f Merge branch 'master' of github.com:wekan/wekan 2020-03-17 13:35:00 +02:00
Lauri Ojansivu 64e76af8de Fix snap syntax. 2020-03-17 13:34:34 +02:00
Lauri Ojansivu acf2ede94e Fix typo. 2020-03-16 10:11:20 -04:00
Lauri Ojansivu b3a7c4dc88 For Sandstorm version of Wekan, leep stylus in 1.1.0, because building v2 takes extra 52 minutes.
Thanks to zodern !
2020-03-16 10:01:28 -04:00
Lauri Ojansivu 8d83c641e3 v3.84 2020-03-16 14:04:18 +02:00
Lauri Ojansivu c1aa0047c6 Update ChangeLog. 2020-03-16 13:55:39 +02:00
Lauri Ojansivu 7af4fe75ba Update ChangeLog. 2020-03-16 13:53:12 +02:00
Lauri Ojansivu ea6baa5c2b Update minimist. 2020-03-16 13:42:29 +02:00
Lauri Ojansivu 8183b7bdaa Update prettier-eslint 2020-03-16 13:33:08 +02:00
Lauri Ojansivu 51514ae22c Update prettier-eslint 2020-03-16 13:29:44 +02:00
Lauri Ojansivu 14b8610837 Update ostrio:cookies 2020-03-16 13:24:39 +02:00
Lauri Ojansivu 369a29707b Update acorn. 2020-03-16 13:22:16 +02:00
Lauri Ojansivu ae240af323 Update translations. 2020-03-16 13:16:47 +02:00
Lauri Ojansivu 2a715ec6c6 Update ChangeLog. 2020-03-15 03:02:08 +02:00
Lauri Ojansivu fca4cdcebf Downgrade stylus to v1.1.0 to speed up building Wekan.
https://github.com/meteor/meteor/issues/10881#issuecomment-599152333

Thanks to zodern !
2020-03-15 02:49:53 +02:00
Lauri Ojansivu 49bbfef3ec Update dependencies. 2020-03-15 02:00:11 +02:00
Lauri Ojansivu f968109e73 Add profiling to build script. 2020-03-15 00:44:03 +02:00
Lauri Ojansivu c0349c1ff9 Update dependencies. 2020-03-15 00:06:09 +02:00
Lauri Ojansivu e009bcf06e Update dependencies. 2020-03-14 17:00:21 +02:00
Lauri Ojansivu 8aae812aa4 Merge pull request #2959 from daMihe/add-current-description-ldap-groups
Added some descriptions for ldap-group authentication
2020-03-14 16:59:37 +02:00
Michael Wodniok 0b8f1cabef Added some descriptions for ldap-group authentication 2020-03-14 15:38:19 +01:00
Lauri Ojansivu e80d3ac79a Update dependencies. 2020-03-14 14:30:59 +02:00
Lauri Ojansivu ed9ba169fe Update dependencies. 2020-03-14 14:08:48 +02:00
Lauri Ojansivu 4720a6fbc3 Fix to be valid JSON. 2020-03-14 14:07:15 +02:00
Lauri Ojansivu ad45f82e27 Update ChangeLog. 2020-03-14 13:21:44 +02:00
Lauri Ojansivu 2174936613 Update acorn. 2020-03-14 13:19:54 +02:00
Lauri Ojansivu a6d7388919 Update ChangeLog. 2020-03-14 13:09:29 +02:00
Lauri Ojansivu f4e2bc0d32 Update dependencies. 2020-03-14 13:06:17 +02:00
Lauri Ojansivu 2f13141907 Update dependencies. 2020-03-14 12:36:12 +02:00
Lauri Ojansivu 365eded5c4 Update ChangeLog. 2020-03-14 12:22:28 +02:00
Lauri Ojansivu ed365c3d41 Update translations. 2020-03-14 12:10:33 +02:00
Lauri Ojansivu e16c65babc Upgrade to Meteor 1.10.1 2020-03-14 12:05:48 +02:00
Lauri Ojansivu 6e0fa78022 Update markdown. 2020-03-14 12:03:34 +02:00
Lauri Ojansivu 06fb201d80 Update translations. 2020-03-11 08:48:11 +02:00
Lauri Ojansivu 62e5622523 Update ChangeLog. 2020-03-06 03:59:09 +02:00
Lauri Ojansivu 2b26bbe78a Fix: img tag did not allow width and height.
Removed swipebox from markdown editor img tag and
updated marked markdown to newest version.

Thanks to hradec and xet7 !

Closes #2956
2020-03-06 03:52:12 +02:00
Lauri Ojansivu a6d702d1ab Update ChangeLog. 2020-03-05 22:04:22 +02:00
Lauri Ojansivu aaecac0912 Mouse scroll part 2. 2020-03-05 22:02:52 +02:00
Lauri Ojansivu 08b35e1fe7 Fix typo. 2020-03-05 21:57:48 +02:00
Lauri Ojansivu 78cf4618d5 Update ChangeLog. 2020-03-05 21:56:44 +02:00
Lauri Ojansivu 9d13001b90 Add settings for mouse wheen scroll inertia and scroll amount.
For example:
  sudo snap set scrollinertia='200'
  sudo snap set scrollamount='200'

Thanks to danger89 and xet7 !

Related #2949
2020-03-05 21:51:03 +02:00
Lauri Ojansivu 965c3fb07f Update translations. 2020-03-05 21:09:33 +02:00
Lauri Ojansivu c43366b7ac Update ChangeLog. 2020-03-05 20:51:27 +02:00
Lauri Ojansivu 3a50556656 Merge pull request #2955 from benoitm76/master
Fix: Error when retrieve token from some OIDC due to not necessary sc…
2020-03-05 20:29:37 +02:00
Benoit MOUQUET b26e65c70c Fix: Error when retrieve token from some OIDC due to not necessary scope parameter 2020-03-05 12:52:46 +01:00
Lauri Ojansivu 75073bbdb2 v3.83 2020-03-01 22:12:11 +02:00
Lauri Ojansivu 88573ad2cd Try to revert remaining of the following changes:
- Revert change made at Wekan v3.81, because building did not work: [Try to make Meteor build time shorter
  by excluding legacy and cordova. This was made possible by
  Meteor 1.10-rc.2](https://github.com/wekan/wekan/commit/0d3002f69d97e646fa7368bfdade4f78c51e9884).

Thanks to xet7.
2020-03-01 22:07:55 +02:00
Lauri Ojansivu 058180687a v3.82 2020-03-01 21:44:38 +02:00
Lauri Ojansivu 897506eeac v3.81 2020-03-01 21:30:43 +02:00
Lauri Ojansivu aac7c380c8 - Fix critical and moderate security vulnerabilities reported at 2020-02-26 with
responsible disclosure by [Dejan Zelic](https://twitter.com/dejandayoff),
  Justin Benjamin and others at [Offensive Security](https://twitter.com/offsectraining),
  that follow standard 90 days before public disclosure.
  Thanks to xet7.
- Fix webhook error that prevented some card etc deleting from web UI of board.
  Thanks to xet7.
- Add some more Font Awesome icons.
  Thanks to xet7.
- Remove autofocus from many form input boxes so that they would not cause warnings.
  Thanks to xet7.
2020-03-01 20:59:53 +02:00
Lauri Ojansivu fc35c234a7 Update translations. 2020-03-01 20:38:05 +02:00
Lauri Ojansivu 0d3002f69d Try to make Meteor build time shorter by excluding legacy and cordova.
Thanks to xet7 !
2020-03-01 20:33:23 +02:00
Lauri Ojansivu 7d920e938b Merge branch 'master' of github.com:wekan/wekan 2020-03-01 20:27:22 +02:00
Lauri Ojansivu 26b521e86e Upgrade Meteor to 1.10-rc.2 2020-03-01 20:26:29 +02:00
Lauri Ojansivu 83b042701f Merge pull request #2948 from NicoP-S/master
Make profile.initials available in publications
2020-02-28 23:11:03 +02:00
Nico 21b75edcdd Make profile.initials available in publications 2020-02-28 22:05:33 +01:00
Lauri Ojansivu 493d71a198 Update translations. 2020-02-23 22:45:43 +02:00
Lauri Ojansivu 27f9c6b925 Update translations. 2020-02-22 20:07:16 +02:00
Lauri Ojansivu fa07dcf422 Update ChangeLog. 2020-02-22 16:39:28 +02:00
Lauri Ojansivu 2b9540ce02 Try to fix afterwards loading of cards by adding
fallback when requestIdleCallback is not available.

Thanks to xet7 !

Closes #2878,
closes #2250
2020-02-22 16:32:54 +02:00
Lauri Ojansivu 906c25d5e8 Update translations. 2020-02-22 10:32:48 +02:00
Lauri Ojansivu e9924a2f03 v3.80 2020-02-22 03:10:08 +02:00
Lauri Ojansivu 16e7fc6edc Update translations. 2020-02-22 02:55:26 +02:00
Lauri Ojansivu e0ca960a35 Create New User in Admin Panel. Works, but does not save fullname yet,
so currently it's needed to edit add fullname later.

Thanks to xet7 !

Related #802
2020-02-22 02:49:14 +02:00
Lauri Ojansivu 0e755e021b Update ChangeLog. 2020-02-21 00:48:02 +02:00
Lauri Ojansivu 9be3f3714a Update to Meteor 1.9.2 2020-02-21 00:46:05 +02:00
Lauri Ojansivu 02e425b39d Update ChangeLog. 2020-02-20 22:57:04 +02:00
Lauri Ojansivu ec01206030 Remove alethes:pages package, that had some indentation error.
Package is about pagination, but I did not find any pagination
related code in Wekan yet.

Thanks to xet7 !
2020-02-20 22:00:28 +02:00
Lauri Ojansivu cbbb5deff7 Update to Meteor 1.9.1, Node 12.16.1 etc newest dependencies. 2020-02-20 20:46:25 +02:00
Lauri Ojansivu 4377be8aa4 Merge branch 'master' of github.com:wekan/wekan 2020-02-20 20:20:11 +02:00
Lauri Ojansivu 26010476b8 Update translations. 2020-02-20 20:19:42 +02:00
Lauri Ojansivu aac27d4db3 Merge pull request #2937 from pbek/patch-1
Fix docker-compose link
2020-02-20 17:13:55 +02:00
Patrizio Bekerle 9762030920 Fix docker-compose link 2020-02-20 15:25:05 +01:00
Lauri Ojansivu cb1ffea5d6 Update translations. 2020-02-19 10:36:12 +02:00
Lauri Ojansivu a4ff6cc0af Update Sandstorm release build script. 2020-02-13 20:04:18 -05:00
Lauri Ojansivu 84acc65d04 v3.79 2020-02-13 15:16:07 +02:00
Lauri Ojansivu 2b1727c6e1 Update translations. 2020-02-13 15:12:46 +02:00
Lauri Ojansivu 178f376e21 Fix Card Opened Webhook can not be disabled.
Thanks to mvanvoorden and xet7 !

Closes #2932,
closes #2889,
closes #2666
2020-02-13 14:58:19 +02:00
Lauri Ojansivu 92538a6ac3 Update translations. 2020-02-13 14:34:53 +02:00
Lauri Ojansivu 3fcde252f7 v3.78 2020-02-12 02:23:26 +02:00
Lauri Ojansivu e89965f642 Remove card element grouping to create compact card layout.
Card Settings / Show on Card: Description Title and Description Text.

Thanks to e-stoniauk, 2020product and xet7 !

Fixes https://github.com/wekan/wekan/pull/2922
2020-02-12 02:08:29 +02:00
Lauri Ojansivu b4e5583065 Update translations. 2020-02-12 02:06:26 +02:00
Lauri Ojansivu 65b11ce3bd v3.77 2020-02-10 20:45:02 +02:00
Lauri Ojansivu ea74e6ad23 Update ChangeLog. 2020-02-10 19:39:30 +02:00
Lauri Ojansivu 4467a68b97 Part 2: Fix Copy Card Link to Clipboard button at card title did not work.
Thanks to 2020product and xet7 !

Related https://github.com/wekan/wekan/pull/2922
2020-02-10 19:37:25 +02:00
Lauri Ojansivu 799bf5735b Update translations. 2020-02-10 19:14:25 +02:00
Lauri Ojansivu 92d0385911 Update ChangeLog. 2020-02-10 19:04:03 +02:00
Lauri Ojansivu 9a21b0a1c9 Fix Copy Card Link to Clipboard button at card title did not work.
Thanks to 2020product and xet7 !

Related https://github.com/wekan/wekan/pull/2922
2020-02-10 18:47:03 +02:00
Lauri Ojansivu 6eb21faca2 Update ChangeLog. 2020-02-10 17:27:18 +02:00
Lauri Ojansivu 2a54218f3f Remove hiding comments and activities. 2020-02-10 17:25:27 +02:00
Lauri Ojansivu 6eac38bde5 Update translations. 2020-02-07 14:05:39 +02:00
Lauri Ojansivu 947c1f81ee Update ChangeLog. 2020-02-07 14:03:57 +02:00
Lauri Ojansivu 2fce02afbc Add Feature: Card Settings/Show on card/Activities.
Fix: When in Card Settings hiding Comments, only adding new comment is hidden, not old comments.

Thanks to xet7 !

Closes #2925
2020-02-07 13:58:43 +02:00
Lauri Ojansivu a58e6be78d v3.76 2020-02-07 03:29:21 +02:00
Lauri Ojansivu 77b43e75a2 Update translations. 2020-02-07 03:28:33 +02:00
Lauri Ojansivu 228e5389cf Update ChangeLog. 2020-02-07 03:20:46 +02:00
Lauri Ojansivu bf78b093ba Try to disable dragging Swimlanes/Lists/Cards/Checklists/Subtasks on small mobile smartphones webbrowsers, and hide drag handles on mobile web.
Thanks to xet7 !
2020-02-07 03:16:16 +02:00
Lauri Ojansivu 8384d68a06 Use Meteor 1.9 and Node.js 12.15.0 on Snap and Docker.
Thanks to xet7 !
2020-02-07 03:12:27 +02:00
Lauri Ojansivu 120a430deb Fix Bug enable/disable Comments in Card Settings.
Thanks to warnt, mdurokov and xet7 !

Closes #2923
2020-02-07 02:34:57 +02:00
Lauri Ojansivu 8e7b9b73b5 Update translations. 2020-02-07 00:08:36 +02:00
Lauri Ojansivu 47afee63d1 v3.75 2020-02-05 20:12:22 +02:00
Lauri Ojansivu 243e4c856e Update translations. 2020-02-05 19:57:19 +02:00
Lauri Ojansivu f22785dbcd - Fix adding comments.
- Added some working layout changes from https://github.com/wekan/wekan/pull/2920.
- Fixed Card Settings not working at Sandstorm.

Thanks to 2020product xet7 !

Closes #2918,
closes https://github.com/wekan/wekan/pull/2920
2020-02-05 19:47:08 +02:00
Lauri Ojansivu 5848e7fe29 Update translations. 2020-02-05 17:26:05 +02:00
Lauri Ojansivu 4c5398f643 Merge branch 'master' of github.com:wekan/wekan 2020-02-05 02:48:48 +02:00
Lauri Ojansivu 45ecd2d04f v3.74
And update translations.
2020-02-05 02:48:13 +02:00
Lauri Ojansivu 132ddfa931 Update translations. 2020-02-05 02:46:27 +02:00
Lauri Ojansivu b7f9dc77d1 Update translations. 2020-02-05 01:45:16 +02:00
Lauri Ojansivu b68557a293 Remove duplicate function. 2020-02-05 01:44:29 +02:00
Lauri Ojansivu bc6e78608b Merge branch '2020product-dollybean' 2020-02-05 01:34:28 +02:00
Lauri Ojansivu 1350694538 Add Board Card Settings to Show on Card only some of it's fields.
Default WYSIWYG comment editor not enabled, use markdown instead.
2020-02-05 01:18:01 +02:00
Lauri Ojansivu 65fa2f626f Merge branch 'dollybean' of https://github.com/2020product/wekan into 2020product-dollybean 2020-02-05 01:17:15 +02:00
dollybean 0b00a8095c Customize of some card's functions 2020-02-04 02:28:45 -08:00
Lauri Ojansivu 58df99d5be Update translations. 2020-01-31 19:50:09 +02:00
Lauri Ojansivu e37bbb9e44 Update package versions. 2020-01-31 19:48:21 +02:00
Lauri Ojansivu 85d47c4ff2 v3.73 2020-01-29 14:57:41 +02:00
Lauri Ojansivu 928973e310 Update translations. 2020-01-29 14:46:01 +02:00
Lauri Ojansivu 9e93540970 Update ChangeLog. 2020-01-29 14:41:38 +02:00
Lauri Ojansivu ecf76f2c61 Merge branch 'master' of github.com:wekan/wekan 2020-01-29 14:39:52 +02:00
Lauri Ojansivu 79223b0b8c Merge branch 'kuleuven-feature-action-move-other-board' 2020-01-29 14:39:29 +02:00
Lauri Ojansivu 5e6469bddc Add info how to install Meteor on Windows. 2020-01-29 01:40:16 +02:00
Lauri Ojansivu 2216cc1b07 Merge branch 'feature-action-move-other-board' of https://github.com/kuleuven/wekan into kuleuven-feature-action-move-other-board 2020-01-23 17:24:27 +02:00
Lauri Ojansivu f275eb5e47 Update ChangeLog. 2020-01-23 16:20:50 +02:00
Peter Verraedt 4b56bbfe6d Add rule action to move cards to other boards
Fixes #1996
2020-01-23 15:12:18 +01:00
Lauri Ojansivu d8c09369eb Merge branch 'izadpoor-hotfix-export-board' 2020-01-23 16:10:27 +02:00
Lauri Ojansivu 6ef532d187 Merge branch 'hotfix-export-board' of https://github.com/izadpoor/wekan into izadpoor-hotfix-export-board 2020-01-23 15:11:21 +02:00
Lauri Ojansivu c610ba0976 Update ChangeLog. 2020-01-23 15:07:33 +02:00
Lauri Ojansivu 362b6432a9 Merge pull request #2897 from bogie/oauth2-nc
Implemented Nextcloud OAuth2 Hack References Issue #1874
2020-01-23 15:00:17 +02:00
izadpoor f868b3d1f9 fixed board export with attchment 2020-01-23 01:16:56 -05:00
Bojan Hartmann 7bba07ccbf Implemented Nextcloud OAuth2 Hack
Fixed: OAuth2 authentication via Nextcloud(tested Nextcloud 17.0.2-18.0.0)
Todo: actually use the profile data: Fullname and Email in Profile
2020-01-22 22:28:03 +01:00
Lauri Ojansivu 70f5326099 Update translations. 2020-01-22 08:06:26 +02:00
Lauri Ojansivu a64204a0c4 Update ChangeLog. 2020-01-20 16:53:38 +02:00
Lauri Ojansivu ea73d9a424 Merge pull request #2891 from tsia/master
Show System Wide Announcement in one line
2020-01-20 16:50:26 +02:00
tsia 3e2415631f Update header.styl 2020-01-20 11:23:08 +01:00
Lauri Ojansivu f425fbfcdf v3.72 2020-01-19 15:29:28 +02:00
Lauri Ojansivu a0e9f4615c v3.71 2020-01-18 20:23:10 +02:00
Lauri Ojansivu 5e5ab95410 Try to fix Wekan at Sandstorm by using Meteor 1.8.x and Node 8.17.0 at Sandstorm.
Thanks to xet7 !
2020-01-18 20:19:20 +02:00
Lauri Ojansivu d7663316f1 v3.70 API docs. 2020-01-18 16:53:27 +02:00
Lauri Ojansivu a0b6b6dac7 v3.70 2020-01-18 16:51:26 +02:00
Lauri Ojansivu 62328c2d2a Merge branch 'master' of github.com:wekan/wekan 2020-01-18 16:42:07 +02:00
Lauri Ojansivu 48f8050c25 Add Snap Meteor 1.8.x files to lint ignore files. 2020-01-18 16:41:23 +02:00
Lauri Ojansivu 135b770400 Add Snap Meteor 1.8.x files to ignore lists. 2020-01-18 16:40:49 +02:00
Lauri Ojansivu f20c2e4565 Update translations. 2020-01-18 16:39:47 +02:00
Lauri Ojansivu 9b905c2833 Try to fix Node 12 Buffer() deprecation errors.
Thanks to xet7 !
2020-01-18 16:01:02 +02:00
Lauri Ojansivu 7efbf30af6 Update translations. 2020-01-17 19:17:43 +02:00
Lauri Ojansivu d9778c1220 Update translations. 2020-01-16 07:57:35 +02:00
Lauri Ojansivu 10f142a1a0 Add LD_LIBRARY_PATH. 2020-01-16 06:49:16 +02:00
Lauri Ojansivu 6a01170d86 Use Meteor 1.8.x on Snap. 2020-01-16 06:15:10 +02:00
Lauri Ojansivu 55a2aa90cb Use Meteor 1.8.x on Snap. 2020-01-16 06:07:22 +02:00
Lauri Ojansivu feafa1827a Try to fix Snap. 2020-01-16 05:52:11 +02:00
Lauri Ojansivu 116e63aa03 Try to fix Snap. 2020-01-14 20:04:47 +02:00
Lauri Ojansivu 1159199e90 Try to fix Snap. 2020-01-14 19:43:25 +02:00
Lauri Ojansivu 0009c4c650 Try to fix Snap. 2020-01-14 12:29:05 +02:00
Lauri Ojansivu b591727b64 Try to fix Snap. 2020-01-14 11:17:23 +02:00
Lauri Ojansivu 218b44d476 Try to fix Snap. 2020-01-14 11:16:43 +02:00
Lauri Ojansivu cc020485c1 Try to fix Snap. 2020-01-12 22:37:37 +02:00
Lauri Ojansivu fc6149d232 Try to fix Snap. 2020-01-12 15:47:01 +02:00
Lauri Ojansivu 8090b6f701 Try to fix Snap. 2020-01-11 23:29:23 +02:00
Lauri Ojansivu 7fc2f2e2e5 Try to fix Snap. 2020-01-11 23:23:27 +02:00
Lauri Ojansivu 76c7fa67d8 Try to fix Snap. 2020-01-11 21:41:09 +02:00
Lauri Ojansivu 4d73eccaa8 Try to fix Snap. 2020-01-11 20:47:26 +02:00
Lauri Ojansivu 634706ca6a Try to fix Snap. 2020-01-11 20:44:46 +02:00
Lauri Ojansivu 88ef717793 Try to fix Snap. 2020-01-11 20:19:23 +02:00
Lauri Ojansivu 870dfc4489 Try to fix snap. 2020-01-11 13:35:27 +02:00
Lauri Ojansivu e01f4dbf13 Change Buffer to Buffer.alloc on Node v12. Try to fix Snap. 2020-01-11 13:08:29 +02:00
Lauri Ojansivu 5bece0dd1e Fix prettier. 2020-01-10 04:09:40 +02:00
Lauri Ojansivu 83e4d214cd Update Travis. 2020-01-10 04:06:54 +02:00
Lauri Ojansivu 891fd60a6c v3.69 2020-01-10 04:03:41 +02:00
Lauri Ojansivu ecb76842fc Fix docker-compose.yml 2020-01-10 03:59:33 +02:00
Lauri Ojansivu da8ae71963 v3.68 2020-01-10 03:57:19 +02:00
Lauri Ojansivu 031df54a2e Try to fix Snap.
Thanks to xet7 !
2020-01-10 03:51:57 +02:00
Lauri Ojansivu 6849bb8af9 v3.67 2020-01-10 02:54:10 +02:00
Lauri Ojansivu 2b382b940b Try to fix Snap. 2020-01-10 02:49:38 +02:00
Lauri Ojansivu 8a64531ad9 v3.66 2020-01-10 01:35:51 +02:00
Lauri Ojansivu 39bf1e375e Try to fix Snap. 2020-01-10 01:31:48 +02:00
Lauri Ojansivu b3e1b8683c Update translations. 2020-01-10 00:18:50 +02:00
Lauri Ojansivu be96f16069 v3.65 2020-01-10 00:16:52 +02:00
Lauri Ojansivu 785f3cf88b Upgrade to Meteor 1.9, Node 12.14.1 and MongoDB 4.2.2.
Thanks to xet7 !
2020-01-10 00:04:43 +02:00
Lauri Ojansivu d16a601c04 More keyboard shortcuts: c for archive card
Thanks to xet7 !

Related #1878
2020-01-09 23:22:27 +02:00
Lauri Ojansivu 5724674e73 Add more issue repo links to GitHub issue template. 2020-01-08 22:09:08 +02:00
Lauri Ojansivu ba0068598c v3.64 2020-01-06 20:33:01 +02:00
Lauri Ojansivu 8f16c8bf8e Update translations. 2020-01-06 20:27:38 +02:00
Lauri Ojansivu 29726453a0 Update ChangeLog. 2020-01-06 20:19:56 +02:00
Lauri Ojansivu 5058233509 Wider sidebar.
Thanks to vjrj !

Closes #2218
2020-01-06 20:12:35 +02:00
Lauri Ojansivu 8442e1e83b Fix typo. 2020-01-06 11:20:11 +02:00
Lauri Ojansivu ef0689baf7 Update ChangeLog. 2020-01-06 11:04:58 +02:00
Lauri Ojansivu ddce0ada09 Removed Custom HTML feature that does not work.
Thanks to xet7 !

Closes #2218
2020-01-06 11:01:38 +02:00
Lauri Ojansivu 60df1fa1ee v3.63 2020-01-06 10:14:00 +02:00
Lauri Ojansivu 8e93912389 Update translations. 2020-01-06 10:09:14 +02:00
Lauri Ojansivu 064e4a2cf9 Update ChangeLog. 2020-01-06 10:07:02 +02:00
Lauri Ojansivu 8ce9939217 Fix: Unable to find Archive Card/List/Swimlane in board settings.
Thanks to neobradley and xet7 !

Closes #2872
2020-01-06 10:02:37 +02:00
Lauri Ojansivu 41a28df430 v3.62 2020-01-05 21:43:44 +02:00
Lauri Ojansivu c36a74b956 Update ChangeLog. 2020-01-05 21:36:28 +02:00
Lauri Ojansivu f6f7705f23 Add Worker role.
This was originally added at Wekan v3.58,
reverted at Wekan v3.60 because of bugs,
and now after fixes added back.

Thanks to xet7 !

Closes #2788
2020-01-05 21:28:14 +02:00
Lauri Ojansivu 6209b792aa Update translations. 2020-01-05 19:48:11 +02:00
Lauri Ojansivu f8873b009b Fix typo. 2020-01-03 17:20:04 +02:00
Lauri Ojansivu 1a1f610977 v3.61 2020-01-03 17:13:56 +02:00
Lauri Ojansivu 3d7871ebfc Update ChangeLog. 2020-01-03 17:09:49 +02:00
Lauri Ojansivu cd253522a3 Add more Font Awesome icons. This was originally added
at Wekan v3.58, removed at Wekan v3.60, and now
added back at Wekan v3.61.

Thanks to xet7 !
2020-01-03 17:02:27 +02:00
Lauri Ojansivu cc70773fd5 v3.60 2020-01-03 16:10:17 +02:00
Lauri Ojansivu 27943796ad Revert to Wekan v3.57 version of client and models directories,
removing Worker role temporarily, because Worker role changes
broke saving card.

Thanks to xet7 !
2020-01-03 16:02:31 +02:00
Lauri Ojansivu 9b28847592 v3.59 2020-01-03 13:34:55 +02:00
Lauri Ojansivu 5376bc7b79 Fix not being able to edit received date.
Thanks to xet7 !
2020-01-03 13:27:27 +02:00
Lauri Ojansivu 19d42c675b v3.58 2020-01-03 07:07:12 +02:00
Lauri Ojansivu be3e4af643 Fix typos. 2020-01-03 07:01:39 +02:00
Lauri Ojansivu 96b3352b2b Update translations. 2020-01-03 06:57:10 +02:00
Lauri Ojansivu 62408fcbf7 Update ChangeLog. 2020-01-03 06:54:39 +02:00
Lauri Ojansivu 2bf004120d Add Worker role.
Add more Font Awesome icons.
Fix browser console errors when editing user profile name etc.

Thanks to xet7 !

Closes #2788
2020-01-03 06:49:35 +02:00
Lauri Ojansivu 0709b5abc8 Update ChangeLog. 2020-01-02 14:07:35 +02:00
Lauri Ojansivu ac43b4c1e6 Merge pull request #2867 from jiangytcn/helm
fix: k8s templates update for helm
2020-01-02 14:02:43 +02:00
Jiang Yitao 5aeb88ee6c fix: k8s templates update for helm
Why:

This change addresses the need by:

1. upgrade mongo replica version
2. access mongo via service url
3. change the expose servicePort to numeric

Signed-off-by: Jiang Yitao <jiangyt.cn@gmail.com>
2020-01-02 11:52:34 +08:00
Lauri Ojansivu c25162aac7 Update translations. 2019-12-31 21:46:23 +02:00
Lauri Ojansivu da6df62042 Update translations. 2019-12-31 21:40:32 +02:00
Lauri Ojansivu 458b9eddb2 Update translations. 2019-12-30 18:25:42 +02:00
391 changed files with 20682 additions and 19220 deletions

No files matched your search

+1
View File
@@ -1 +1,2 @@
packages/*
.snap-meteor-1.8/*
+1
View File
@@ -145,6 +145,7 @@
"allowIsBoardMemberByCard": true,
"allowIsBoardMemberCommentOnly": true,
"allowIsBoardMemberNoComments": true,
"allowIsBoardMemberWorker": true,
"Emoji": true,
"Checklists": true,
"Settings": true,
+2
View File
@@ -2,6 +2,8 @@
Add these issues to elsewhere:
- Snap: https://github.com/wekan/wekan-snap/issues
- LDAP: https://github.com/wekan/wekan-ldap/issues
- UCS: https://github.com/wekan/univention/issues
Other Wekan issues can be added here.
+10 -9
View File
@@ -6,10 +6,11 @@
meteor-base@1.4.0
# Build system
ecmascript@0.13.2
standard-minifier-css@1.5.4
standard-minifier-js@2.5.2
ecmascript@0.14.2
standard-minifier-css@1.6.0
standard-minifier-js@2.6.0
mquandalle:jade
coffeescript@2.4.1!
# Polyfills
es5-shim@4.8.0
@@ -22,7 +23,7 @@ dburles:collection-helpers
idmontie:migrations
matb33:collection-hooks
matteodem:easy-search
mongo@1.7.0
mongo@1.9.0
mquandalle:collection-mutations
# Account system
@@ -37,13 +38,12 @@ wekan-accounts-oidc
# Utilities
check@1.3.1
jquery@1.11.10
random@1.1.0
random@1.2.0
reactive-dict@1.3.0
session@1.2.0
tracker@1.2.0
underscore@1.0.10
3stack:presence
alethes:pages
arillo:flow-router-helpers
audit-argument-checks@1.0.7
kadira:blaze-layout
@@ -67,7 +67,7 @@ templates:tabs
verron:autosize
simple:json-routes
rajit:bootstrap3-datepicker
shell-server@0.4.0
shell-server@0.5.0
simple:rest-accounts-password
useraccounts:core
email@1.2.3
@@ -75,7 +75,7 @@ horka:swipebox
dynamic-import@0.5.1
staringatlights:fast-render
accounts-password@1.5.2
accounts-password@1.6.0
cfs:gridfs
rzymek:fullcalendar
momentjs:moment@2.22.2
@@ -85,7 +85,8 @@ msavin:usercache
wekan-scrollbar
mquandalle:perfect-scrollbar
mdg:meteor-apm-agent@3.2.0-rc.0!
coagmano:stylus
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
coagmano:stylus@1.1.0!
lucasantoniassi:accounts-lockout
meteorhacks:subs-manager
meteorhacks:picker
+1 -1
View File
@@ -1 +1 @@
METEOR@1.8.3
METEOR@1.10.1
+34 -34
View File
@@ -1,29 +1,28 @@
3stack:presence@1.1.2
accounts-base@1.4.5
accounts-oauth@1.1.16
accounts-password@1.5.2
accounts-base@1.6.0
accounts-oauth@1.2.0
accounts-password@1.6.0
aldeed:collection2@2.10.0
aldeed:collection2-core@1.2.0
aldeed:schema-deny@1.1.0
aldeed:schema-index@1.1.1
aldeed:simple-schema@1.5.4
alethes:pages@1.8.6
allow-deny@1.1.0
arillo:flow-router-helpers@0.5.2
audit-argument-checks@1.0.7
autoupdate@1.6.0
babel-compiler@7.4.2
babel-runtime@1.4.0
babel-compiler@7.5.2
babel-runtime@1.5.0
base64@1.0.12
binary-heap@1.0.11
blaze@2.3.4
blaze-tools@1.0.10
boilerplate-generator@1.6.0
boilerplate-generator@1.7.0
browser-policy-common@1.0.11
browser-policy-framing@1.1.0
caching-compiler@1.2.1
caching-html-compiler@1.1.3
callback-hook@1.2.0
callback-hook@1.3.0
cfs:access-point@0.1.49
cfs:base-package@0.0.30
cfs:collection@0.5.5
@@ -44,23 +43,24 @@ cfs:upload-http@0.0.20
cfs:worker@0.1.5
check@1.3.1
chuangbo:cookie@1.1.0
coagmano:stylus@2.0.0
coffeescript@1.0.17
coagmano:stylus@1.1.0
coffeescript@2.4.1
coffeescript-compiler@2.4.1
cottz:publish-relations@2.0.8
dburles:collection-helpers@1.1.0
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.3.0
ddp-server@2.3.1
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.1
easylogic:summernote@0.8.8
ecmascript@0.13.2
ecmascript@0.14.2
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.9.0
ecmascript-runtime-server@0.8.0
ecmascript-runtime-client@0.10.0
ecmascript-runtime-server@0.9.0
ejson@1.1.1
email@1.2.3
es5-shim@4.8.0
@@ -75,7 +75,7 @@ htmljs@1.0.11
http@1.4.2
id-map@1.1.0
idmontie:migrations@1.0.3
inter-process-messaging@0.1.0
inter-process-messaging@0.1.1
jquery@1.11.11
kadira:blaze-layout@2.3.0
kadira:dochead@1.5.0
@@ -84,7 +84,7 @@ kenton:accounts-sandstorm@0.7.0
konecty:mongo-counter@0.0.5_3
lamhieu:meteorx@2.1.1
lamhieu:unblock@1.0.0
launch-screen@1.1.1
launch-screen@1.2.0
livedata@1.0.18
localstorage@1.2.0
logging@1.1.20
@@ -101,16 +101,16 @@ meteorhacks:collection-utils@1.2.0
meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.4
meteorspark:util@0.2.0
minifier-css@1.4.3
minifier-js@2.5.1
minifier-css@1.5.0
minifier-js@2.6.0
minifiers@1.1.8-faster-rebuild.0
minimongo@1.4.5
mobile-status-bar@1.0.14
modern-browsers@0.1.4
modules@0.14.0
modules-runtime@0.11.0
minimongo@1.5.0
mobile-status-bar@1.1.0
modern-browsers@0.1.5
modules@0.15.0
modules-runtime@0.12.0
momentjs:moment@2.24.0
mongo@1.7.0
mongo@1.9.0
mongo-decimal@0.1.1
mongo-dev-server@1.1.0
mongo-id@1.0.7
@@ -127,13 +127,13 @@ mquandalle:mousetrap-bindglobal@0.0.1
mquandalle:perfect-scrollbar@0.6.5_2
msavin:usercache@1.8.0
npm-bcrypt@0.9.3
npm-mongo@3.2.0
oauth@1.2.8
oauth2@1.2.1
npm-mongo@3.7.0
oauth@1.3.0
oauth2@1.3.0
observe-sequence@1.0.16
ongoworks:speakingurl@1.1.0
ordered-dict@1.1.0
ostrio:cookies@2.5.0
ostrio:cookies@2.6.0
peerlibrary:assert@0.3.0
peerlibrary:base-component@0.16.0
peerlibrary:blaze-components@0.15.1
@@ -144,7 +144,7 @@ promise@0.11.2
raix:eventemitter@0.1.3
raix:handlebar-helpers@0.2.5
rajit:bootstrap3-datepicker@1.7.1_1
random@1.1.0
random@1.2.0
rate-limit@1.0.9
reactive-dict@1.3.0
reactive-var@1.0.11
@@ -156,19 +156,19 @@ server-render@0.3.1
service-configuration@1.0.11
session@1.2.0
sha@1.0.9
shell-server@0.4.0
shell-server@0.5.0
simple:authenticate-user-by-token@1.0.1
simple:json-routes@2.1.0
simple:rest-accounts-password@1.1.2
simple:rest-bearer-token-parser@1.0.1
simple:rest-json-error-handler@1.0.1
socket-stream-client@0.2.2
socket-stream-client@0.2.3
softwarerero:accounts-t9n@1.3.11
spacebars@1.0.15
spacebars-compiler@1.1.3
srp@1.0.12
standard-minifier-css@1.5.4
standard-minifier-js@2.5.2
standard-minifier-css@1.6.0
standard-minifier-js@2.6.0
staringatlights:fast-render@3.2.0
staringatlights:inject-data@2.3.0
tap:i18n@1.8.2
@@ -186,7 +186,7 @@ useraccounts:core@1.14.2
useraccounts:flow-routing@1.14.2
useraccounts:unstyled@1.14.2
verron:autosize@3.0.8
webapp@1.7.5
webapp@1.9.0
webapp-hashing@1.0.9
wekan-accounts-cas@0.1.0
wekan-accounts-oidc@1.0.10
+1
View File
@@ -5,3 +5,4 @@ node_modules/
.vscode/
.tx/
.github/
.snap-meteor-1.8/
@@ -0,0 +1,20 @@
# This file contains information which helps Meteor properly upgrade your
# app when you run 'meteor update'. You should check it into version control
# with your project.
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
1.3.0-split-minifiers-package
1.3.5-remove-old-dev-bundle-link
1.4.0-remove-old-dev-bundle-link
1.4.1-add-shell-server-package
1.4.3-split-account-service-packages
1.5-add-dynamic-import-package
1.7-split-underscore-from-meteor-base
1.8.3-split-jquery-from-blaze
+2
View File
@@ -0,0 +1,2 @@
dev_bundle
local
+7
View File
@@ -0,0 +1,7 @@
# This file contains a token that is unique to your project.
# Check it into your repository along with the rest of this directory.
# It can be used for purposes such as:
# - ensuring you don't accidentally deploy one app on top of another
# - providing package authors with aggregated statistics
dvyihgykyzec6y1dpg
Whitespace-only changes.
+100
View File
@@ -0,0 +1,100 @@
# Meteor packages used by this project, one per line.
#
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.
meteor-base@1.4.0
# Build system
ecmascript@0.13.2
standard-minifier-css@1.5.4
standard-minifier-js@2.5.2
mquandalle:jade
# Polyfills
es5-shim@4.8.0
# Collections
aldeed:collection2
cfs:standard-packages
cottz:publish-relations
dburles:collection-helpers
idmontie:migrations
matb33:collection-hooks
matteodem:easy-search
mongo@1.7.0
mquandalle:collection-mutations
# Account system
kenton:accounts-sandstorm
service-configuration@1.0.11
useraccounts:unstyled
useraccounts:flow-routing
wekan-ldap
wekan-accounts-cas
wekan-accounts-oidc
# Utilities
check@1.3.1
jquery@1.11.10
random@1.1.0
reactive-dict@1.3.0
session@1.2.0
tracker@1.2.0
underscore@1.0.10
3stack:presence
alethes:pages
arillo:flow-router-helpers
audit-argument-checks@1.0.7
kadira:blaze-layout
kadira:dochead
mquandalle:autofocus
ongoworks:speakingurl
raix:handlebar-helpers
tap:i18n
http@1.4.2
# UI components
blaze
reactive-var@1.0.11
fortawesome:fontawesome
mousetrap:mousetrap
mquandalle:jquery-textcomplete
mquandalle:jquery-ui-drag-drop-sort
mquandalle:mousetrap-bindglobal
peerlibrary:blaze-components@=0.15.1
templates:tabs
verron:autosize
simple:json-routes
rajit:bootstrap3-datepicker
shell-server@0.4.0
simple:rest-accounts-password
useraccounts:core
email@1.2.3
horka:swipebox
dynamic-import@0.5.1
staringatlights:fast-render
accounts-password@1.5.2
cfs:gridfs
rzymek:fullcalendar
momentjs:moment@2.22.2
browser-policy-framing@1.1.0
mquandalle:moment
msavin:usercache
wekan-scrollbar
mquandalle:perfect-scrollbar
mdg:meteor-apm-agent@3.2.0-rc.0!
# Keep stylus in 1.1.0, because building v2 takes extra 52 minutes.
coagmano:stylus@1.1.0!
lucasantoniassi:accounts-lockout
meteorhacks:subs-manager
meteorhacks:picker
lamhieu:unblock
meteorhacks:aggregate@1.3.0
wekan-markdown
konecty:mongo-counter
percolate:synced-cron
easylogic:summernote
cfs:filesystem
ostrio:cookies
+2
View File
@@ -0,0 +1,2 @@
server
browser
+1
View File
@@ -0,0 +1 @@
METEOR@1.8.3
+198
View File
@@ -0,0 +1,198 @@
3stack:presence@1.1.2
accounts-base@1.4.5
accounts-oauth@1.1.16
accounts-password@1.5.2
aldeed:collection2@2.10.0
aldeed:collection2-core@1.2.0
aldeed:schema-deny@1.1.0
aldeed:schema-index@1.1.1
aldeed:simple-schema@1.5.4
alethes:pages@1.8.6
allow-deny@1.1.0
arillo:flow-router-helpers@0.5.2
audit-argument-checks@1.0.7
autoupdate@1.6.0
babel-compiler@7.4.2
babel-runtime@1.4.0
base64@1.0.12
binary-heap@1.0.11
blaze@2.3.4
blaze-tools@1.0.10
boilerplate-generator@1.6.0
browser-policy-common@1.0.11
browser-policy-framing@1.1.0
caching-compiler@1.2.1
caching-html-compiler@1.1.3
callback-hook@1.2.0
cfs:access-point@0.1.49
cfs:base-package@0.0.30
cfs:collection@0.5.5
cfs:collection-filters@0.2.4
cfs:data-man@0.0.6
cfs:file@0.1.17
cfs:filesystem@0.1.2
cfs:gridfs@0.0.34
cfs:http-methods@0.0.32
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.10
cfs:storage-adapter@0.2.4
cfs:tempstore@0.1.6
cfs:upload-http@0.0.20
cfs:worker@0.1.5
check@1.3.1
chuangbo:cookie@1.1.0
coagmano:stylus@1.1.0
coffeescript@1.0.17
cottz:publish-relations@2.0.8
dburles:collection-helpers@1.1.0
ddp@1.4.0
ddp-client@2.3.3
ddp-common@1.4.0
ddp-rate-limiter@1.0.7
ddp-server@2.3.0
deps@1.0.12
diff-sequence@1.1.1
dynamic-import@0.5.1
easylogic:summernote@0.8.8
ecmascript@0.13.2
ecmascript-runtime@0.7.0
ecmascript-runtime-client@0.9.0
ecmascript-runtime-server@0.8.0
ejson@1.1.1
email@1.2.3
es5-shim@4.8.0
fastclick@1.0.13
fetch@0.1.1
fortawesome:fontawesome@4.7.0
geojson-utils@1.0.10
horka:swipebox@1.0.2
hot-code-push@1.0.4
html-tools@1.0.11
htmljs@1.0.11
http@1.4.2
id-map@1.1.0
idmontie:migrations@1.0.3
inter-process-messaging@0.1.0
jquery@1.11.11
kadira:blaze-layout@2.3.0
kadira:dochead@1.5.0
kadira:flow-router@2.12.1
kenton:accounts-sandstorm@0.7.0
konecty:mongo-counter@0.0.5_3
lamhieu:meteorx@2.1.1
lamhieu:unblock@1.0.0
launch-screen@1.1.1
livedata@1.0.18
localstorage@1.2.0
logging@1.1.20
lucasantoniassi:accounts-lockout@1.0.0
matb33:collection-hooks@0.9.1
matteodem:easy-search@1.6.4
mdg:meteor-apm-agent@3.2.5
mdg:validation-error@0.5.1
meteor@1.9.3
meteor-base@1.4.0
meteor-platform@1.2.6
meteorhacks:aggregate@1.3.0
meteorhacks:collection-utils@1.2.0
meteorhacks:picker@1.0.3
meteorhacks:subs-manager@1.6.4
meteorspark:util@0.2.0
minifier-css@1.4.3
minifier-js@2.5.1
minifiers@1.1.8-faster-rebuild.0
minimongo@1.4.5
mobile-status-bar@1.0.14
modern-browsers@0.1.4
modules@0.14.0
modules-runtime@0.11.0
momentjs:moment@2.24.0
mongo@1.7.0
mongo-decimal@0.1.1
mongo-dev-server@1.1.0
mongo-id@1.0.7
mongo-livedata@1.0.12
mousetrap:mousetrap@1.4.6_1
mquandalle:autofocus@1.0.0
mquandalle:collection-mutations@0.1.0
mquandalle:jade@0.4.9
mquandalle:jade-compiler@0.4.5
mquandalle:jquery-textcomplete@0.8.0_1
mquandalle:jquery-ui-drag-drop-sort@0.2.0
mquandalle:moment@1.0.1
mquandalle:mousetrap-bindglobal@0.0.1
mquandalle:perfect-scrollbar@0.6.5_2
msavin:usercache@1.8.0
npm-bcrypt@0.9.3
npm-mongo@3.2.0
oauth@1.2.8
oauth2@1.2.1
observe-sequence@1.0.16
ongoworks:speakingurl@1.1.0
ordered-dict@1.1.0
ostrio:cookies@2.5.0
peerlibrary:assert@0.3.0
peerlibrary:base-component@0.16.0
peerlibrary:blaze-components@0.15.1
peerlibrary:computed-field@0.10.0
peerlibrary:reactive-field@0.6.0
percolate:synced-cron@1.3.2
promise@0.11.2
raix:eventemitter@0.1.3
raix:handlebar-helpers@0.2.5
rajit:bootstrap3-datepicker@1.7.1_1
random@1.1.0
rate-limit@1.0.9
reactive-dict@1.3.0
reactive-var@1.0.11
reload@1.3.0
retry@1.1.0
routepolicy@1.1.0
rzymek:fullcalendar@3.8.0
server-render@0.3.1
service-configuration@1.0.11
session@1.2.0
sha@1.0.9
shell-server@0.4.0
simple:authenticate-user-by-token@1.0.1
simple:json-routes@2.1.0
simple:rest-accounts-password@1.1.2
simple:rest-bearer-token-parser@1.0.1
simple:rest-json-error-handler@1.0.1
socket-stream-client@0.2.2
softwarerero:accounts-t9n@1.3.11
spacebars@1.0.15
spacebars-compiler@1.1.3
srp@1.0.12
standard-minifier-css@1.5.4
standard-minifier-js@2.5.2
staringatlights:fast-render@3.2.0
staringatlights:inject-data@2.3.0
tap:i18n@1.8.2
templates:tabs@2.3.0
templating@1.3.2
templating-compiler@1.3.3
templating-runtime@1.3.2
templating-tools@1.1.2
tracker@1.2.0
twbs:bootstrap@3.3.6
ui@1.0.13
underscore@1.0.10
url@1.2.0
useraccounts:core@1.14.2
useraccounts:flow-routing@1.14.2
useraccounts:unstyled@1.14.2
verron:autosize@3.0.8
webapp@1.7.5
webapp-hashing@1.0.9
wekan-accounts-cas@0.1.0
wekan-accounts-oidc@1.0.10
wekan-ldap@0.0.2
wekan-markdown@1.0.7
wekan-oidc@1.0.12
wekan-scrollbar@3.1.3
yasaricli:slugify@0.0.7
zimme:active-route@2.3.2
+914
View File
@@ -0,0 +1,914 @@
(function () {
/* Imports */
var Meteor = Package.meteor.Meteor;
var global = Package.meteor.global;
var meteorEnv = Package.meteor.meteorEnv;
var FS = Package['cfs:base-package'].FS;
var check = Package.check.check;
var Match = Package.check.Match;
var EJSON = Package.ejson.EJSON;
var HTTP = Package['cfs:http-methods'].HTTP;
/* Package-scope variables */
var rootUrlPathPrefix, baseUrl, getHeaders, getHeadersByCollection, _existingMountPoints, mountUrls;
(function(){
///////////////////////////////////////////////////////////////////////
// //
// packages/cfs_access-point/packages/cfs_access-point.js //
// //
///////////////////////////////////////////////////////////////////////
//
(function () {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/cfs:access-point/access-point-common.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
rootUrlPathPrefix = __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || ""; // 1
// Adjust the rootUrlPathPrefix if necessary // 2
if (rootUrlPathPrefix.length > 0) { // 3
if (rootUrlPathPrefix.slice(0, 1) !== '/') { // 4
rootUrlPathPrefix = '/' + rootUrlPathPrefix; // 5
} // 6
if (rootUrlPathPrefix.slice(-1) === '/') { // 7
rootUrlPathPrefix = rootUrlPathPrefix.slice(0, -1); // 8
} // 9
} // 10
// 11
// prepend ROOT_URL when isCordova // 12
if (Meteor.isCordova) { // 13
rootUrlPathPrefix = Meteor.absoluteUrl(rootUrlPathPrefix.replace(/^\/+/, '')).replace(/\/+$/, ''); // 14
} // 15
// 16
baseUrl = '/cfs'; // 17
FS.HTTP = FS.HTTP || {}; // 18
// 19
// Note the upload URL so that client uploader packages know what it is // 20
FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 21
// 22
/** // 23
* @method FS.HTTP.setBaseUrl // 24
* @public // 25
* @param {String} newBaseUrl - Change the base URL for the HTTP GET and DELETE endpoints. // 26
* @returns {undefined} // 27
*/ // 28
FS.HTTP.setBaseUrl = function setBaseUrl(newBaseUrl) { // 29
// 30
// Adjust the baseUrl if necessary // 31
if (newBaseUrl.slice(0, 1) !== '/') { // 32
newBaseUrl = '/' + newBaseUrl; // 33
} // 34
if (newBaseUrl.slice(-1) === '/') { // 35
newBaseUrl = newBaseUrl.slice(0, -1); // 36
} // 37
// 38
// Update the base URL // 39
baseUrl = newBaseUrl; // 40
// 41
// Change the upload URL so that client uploader packages know what it is // 42
FS.HTTP.uploadUrl = rootUrlPathPrefix + baseUrl + '/files'; // 43
// 44
// Remount URLs with the new baseUrl, unmounting the old, on the server only. // 45
// If existingMountPoints is empty, then we haven't run the server startup // 46
// code yet, so this new URL will be used at that point for the initial mount. // 47
if (Meteor.isServer && !FS.Utility.isEmpty(_existingMountPoints)) { // 48
mountUrls(); // 49
} // 50
}; // 51
// 52
/* // 53
* FS.File extensions // 54
*/ // 55
// 56
/** // 57
* @method FS.File.prototype.url Construct the file url // 58
* @public // 59
* @param {Object} [options] // 60
* @param {String} [options.store] Name of the store to get from. If not defined, the first store defined in `options.stores` for the collection on the client is used.
* @param {Boolean} [options.auth=null] Add authentication token to the URL query string? By default, a token for the current logged in user is added on the client. Set this to `false` to omit the token. Set this to a string to provide your own token. Set this to a number to specify an expiration time for the token in seconds.
* @param {Boolean} [options.download=false] Should headers be set to force a download? Typically this means that clicking the link with this URL will download the file to the user's Downloads folder instead of displaying the file in the browser.
* @param {Boolean} [options.brokenIsFine=false] Return the URL even if we know it's currently a broken link because the file hasn't been saved in the requested store yet.
* @param {Boolean} [options.metadata=false] Return the URL for the file metadata access point rather than the file itself.
* @param {String} [options.uploading=null] A URL to return while the file is being uploaded. // 66
* @param {String} [options.storing=null] A URL to return while the file is being stored. // 67
* @param {String} [options.filename=null] Override the filename that should appear at the end of the URL. By default it is the name of the file in the requested store.
* // 69
* Returns the HTTP URL for getting the file or its metadata. // 70
*/ // 71
FS.File.prototype.url = function(options) { // 72
var self = this; // 73
options = options || {}; // 74
options = FS.Utility.extend({ // 75
store: null, // 76
auth: null, // 77
download: false, // 78
metadata: false, // 79
brokenIsFine: false, // 80
uploading: null, // return this URL while uploading // 81
storing: null, // return this URL while storing // 82
filename: null // override the filename that is shown to the user // 83
}, options.hash || options); // check for "hash" prop if called as helper // 84
// 85
// Primarily useful for displaying a temporary image while uploading an image // 86
if (options.uploading && !self.isUploaded()) { // 87
return options.uploading; // 88
} // 89
// 90
if (self.isMounted()) { // 91
// See if we've stored in the requested store yet // 92
var storeName = options.store || self.collection.primaryStore.name; // 93
if (!self.hasStored(storeName)) { // 94
if (options.storing) { // 95
return options.storing; // 96
} else if (!options.brokenIsFine) { // 97
// We want to return null if we know the URL will be a broken // 98
// link because then we can avoid rendering broken links, broken // 99
// images, etc. // 100
return null; // 101
} // 102
} // 103
// 104
// Add filename to end of URL if we can determine one // 105
var filename = options.filename || self.name({store: storeName}); // 106
if (typeof filename === "string" && filename.length) { // 107
filename = '/' + filename; // 108
} else { // 109
filename = ''; // 110
} // 111
// 112
// TODO: Could we somehow figure out if the collection requires login? // 113
var authToken = ''; // 114
if (Meteor.isClient && typeof Accounts !== "undefined" && typeof Accounts._storedLoginToken === "function") { // 115
if (options.auth !== false) { // 116
// Add reactive deps on the user // 117
Meteor.userId(); // 118
// 119
var authObject = { // 120
authToken: Accounts._storedLoginToken() || '' // 121
}; // 122
// 123
// If it's a number, we use that as the expiration time (in seconds) // 124
if (options.auth === +options.auth) { // 125
authObject.expiration = FS.HTTP.now() + options.auth * 1000; // 126
} // 127
// 128
// Set the authToken // 129
var authString = JSON.stringify(authObject); // 130
authToken = FS.Utility.btoa(authString); // 131
} // 132
} else if (typeof options.auth === "string") { // 133
// If the user supplies auth token the user will be responsible for // 134
// updating // 135
authToken = options.auth; // 136
} // 137
// 138
// Construct query string // 139
var params = {}; // 140
if (authToken !== '') { // 141
params.token = authToken; // 142
} // 143
if (options.download) { // 144
params.download = true; // 145
} // 146
if (options.store) { // 147
// We use options.store here instead of storeName because we want to omit the queryString // 148
// whenever possible, allowing users to have "clean" URLs if they want. The server will // 149
// assume the first store defined on the server, which means that we are assuming that // 150
// the first on the client is also the first on the server. If that's not the case, the // 151
// store option should be supplied. // 152
params.store = options.store; // 153
} // 154
var queryString = FS.Utility.encodeParams(params); // 155
if (queryString.length) { // 156
queryString = '?' + queryString; // 157
} // 158
// 159
// Determine which URL to use // 160
var area; // 161
if (options.metadata) { // 162
area = '/record'; // 163
} else { // 164
area = '/files'; // 165
} // 166
// 167
// Construct and return the http method url // 168
return rootUrlPathPrefix + baseUrl + area + '/' + self.collection.name + '/' + self._id + filename + queryString; // 169
} // 170
// 171
}; // 172
// 173
// 174
// 175
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/cfs:access-point/access-point-handlers.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
getHeaders = []; // 1
getHeadersByCollection = {}; // 2
// 3
FS.HTTP.Handlers = {}; // 4
// 5
/** // 6
* @method FS.HTTP.Handlers.Del // 7
* @public // 8
* @returns {any} response // 9
* // 10
* HTTP DEL request handler // 11
*/ // 12
FS.HTTP.Handlers.Del = function httpDelHandler(ref) { // 13
var self = this; // 14
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 15
// 16
// If DELETE request, validate with 'remove' allow/deny, delete the file, and return // 17
FS.Utility.validateAction(ref.collection.files._validators['remove'], ref.file, self.userId); // 18
// 19
/* // 20
* From the DELETE spec: // 21
* A successful response SHOULD be 200 (OK) if the response includes an // 22
* entity describing the status, 202 (Accepted) if the action has not // 23
* yet been enacted, or 204 (No Content) if the action has been enacted // 24
* but the response does not include an entity. // 25
*/ // 26
self.setStatusCode(200); // 27
// 28
return { // 29
deleted: !!ref.file.remove() // 30
}; // 31
}; // 32
// 33
/** // 34
* @method FS.HTTP.Handlers.GetList // 35
* @public // 36
* @returns {Object} response // 37
* // 38
* HTTP GET file list request handler // 39
*/ // 40
FS.HTTP.Handlers.GetList = function httpGetListHandler() { // 41
// Not Yet Implemented // 42
// Need to check publications and return file list based on // 43
// what user is allowed to see // 44
}; // 45
// 46
/* // 47
requestRange will parse the range set in request header - if not possible it // 48
will throw fitting errors and autofill range for both partial and full ranges // 49
// 50
throws error or returns the object: // 51
{ // 52
start // 53
end // 54
length // 55
unit // 56
partial // 57
} // 58
*/ // 59
var requestRange = function(req, fileSize) { // 60
if (req) { // 61
if (req.headers) { // 62
var rangeString = req.headers.range; // 63
// 64
// Make sure range is a string // 65
if (rangeString === ''+rangeString) { // 66
// 67
// range will be in the format "bytes=0-32767" // 68
var parts = rangeString.split('='); // 69
var unit = parts[0]; // 70
// 71
// Make sure parts consists of two strings and range is of type "byte" // 72
if (parts.length == 2 && unit == 'bytes') { // 73
// Parse the range // 74
var range = parts[1].split('-'); // 75
var start = Number(range[0]); // 76
var end = Number(range[1]); // 77
// 78
// Fix invalid ranges? // 79
if (range[0] != start) start = 0; // 80
if (range[1] != end || !end) end = fileSize - 1; // 81
// 82
// Make sure range consists of a start and end point of numbers and start is less than end // 83
if (start < end) { // 84
// 85
var partSize = 0 - start + end + 1; // 86
// 87
// Return the parsed range // 88
return { // 89
start: start, // 90
end: end, // 91
length: partSize, // 92
size: fileSize, // 93
unit: unit, // 94
partial: (partSize < fileSize) // 95
}; // 96
// 97
} else { // 98
throw new Meteor.Error(416, "Requested Range Not Satisfiable"); // 99
} // 100
// 101
} else { // 102
// The first part should be bytes // 103
throw new Meteor.Error(416, "Requested Range Unit Not Satisfiable"); // 104
} // 105
// 106
} else { // 107
// No range found // 108
} // 109
// 110
} else { // 111
// throw new Error('No request headers set for _parseRange function'); // 112
} // 113
} else { // 114
throw new Error('No request object passed to _parseRange function'); // 115
} // 116
// 117
return { // 118
start: 0, // 119
end: fileSize - 1, // 120
length: fileSize, // 121
size: fileSize, // 122
unit: 'bytes', // 123
partial: false // 124
}; // 125
}; // 126
// 127
/** // 128
* @method FS.HTTP.Handlers.Get // 129
* @public // 130
* @returns {any} response // 131
* // 132
* HTTP GET request handler // 133
*/ // 134
FS.HTTP.Handlers.Get = function httpGetHandler(ref) { // 135
var self = this; // 136
// Once we have the file, we can test allow/deny validators // 137
// XXX: pass on the "share" query eg. ?share=342hkjh23ggj for shared url access? // 138
FS.Utility.validateAction(ref.collection._validators['download'], ref.file, self.userId /*, self.query.shareId*/); // 139
// 140
var storeName = ref.storeName; // 141
// 142
// If no storeName was specified, use the first defined storeName // 143
if (typeof storeName !== "string") { // 144
// No store handed, we default to primary store // 145
storeName = ref.collection.primaryStore.name; // 146
} // 147
// 148
// Get the storage reference // 149
var storage = ref.collection.storesLookup[storeName]; // 150
// 151
if (!storage) { // 152
throw new Meteor.Error(404, "Not Found", 'There is no store "' + storeName + '"'); // 153
} // 154
// 155
// Get the file // 156
var copyInfo = ref.file.copies[storeName]; // 157
// 158
if (!copyInfo) { // 159
throw new Meteor.Error(404, "Not Found", 'This file was not stored in the ' + storeName + ' store'); // 160
} // 161
// 162
// Set the content type for file // 163
if (typeof copyInfo.type === "string") { // 164
self.setContentType(copyInfo.type); // 165
} else { // 166
self.setContentType('application/octet-stream'); // 167
} // 168
// 169
// Add 'Content-Disposition' header if requested a download/attachment URL // 170
if (typeof ref.download !== "undefined") { // 171
var filename = ref.filename || copyInfo.name; // 172
self.addHeader('Content-Disposition', 'attachment; filename="' + filename + '"'); // 173
} else { // 174
self.addHeader('Content-Disposition', 'inline'); // 175
} // 176
// 177
// Get the contents range from request // 178
var range = requestRange(self.request, copyInfo.size); // 179
// 180
// Some browsers cope better if the content-range header is // 181
// still included even for the full file being returned. // 182
self.addHeader('Content-Range', range.unit + ' ' + range.start + '-' + range.end + '/' + range.size); // 183
// 184
// If a chunk/range was requested instead of the whole file, serve that' // 185
if (range.partial) { // 186
self.setStatusCode(206, 'Partial Content'); // 187
} else { // 188
self.setStatusCode(200, 'OK'); // 189
} // 190
// 191
// Add any other global custom headers and collection-specific custom headers // 192
FS.Utility.each(getHeaders.concat(getHeadersByCollection[ref.collection.name] || []), function(header) { // 193
self.addHeader(header[0], header[1]); // 194
}); // 195
// 196
// Inform clients about length (or chunk length in case of ranges) // 197
self.addHeader('Content-Length', range.length); // 198
// 199
// Last modified header (updatedAt from file info) // 200
self.addHeader('Last-Modified', copyInfo.updatedAt.toUTCString()); // 201
// 202
// Inform clients that we accept ranges for resumable chunked downloads // 203
self.addHeader('Accept-Ranges', range.unit); // 204
// 205
if (FS.debug) console.log('Read file "' + (ref.filename || copyInfo.name) + '" ' + range.unit + ' ' + range.start + '-' + range.end + '/' + range.size);
// 207
var readStream = storage.adapter.createReadStream(ref.file, {start: range.start, end: range.end}); // 208
// 209
readStream.on('error', function(err) { // 210
// Send proper error message on get error // 211
if (err.message && err.statusCode) { // 212
self.Error(new Meteor.Error(err.statusCode, err.message)); // 213
} else { // 214
self.Error(new Meteor.Error(503, 'Service unavailable')); // 215
} // 216
}); // 217
// 218
readStream.pipe(self.createWriteStream()); // 219
}; // 220
const originalHandler = FS.HTTP.Handlers.Get;
FS.HTTP.Handlers.Get = function (ref) {
//console.log(ref.filename);
try {
var userAgent = (this.requestHeaders['user-agent']||'').toLowerCase();
if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('trident') >= 0 || userAgent.indexOf('chrome') >= 0) {
ref.filename = encodeURIComponent(ref.filename);
} else if(userAgent.indexOf('firefox') >= 0) {
ref.filename = new Buffer(ref.filename).toString('binary');
} else {
/* safari*/
ref.filename = new Buffer(ref.filename).toString('binary');
}
} catch (ex){
ref.filename = 'tempfix';
}
return originalHandler.call(this, ref);
};
// 221
/** // 222
* @method FS.HTTP.Handlers.PutInsert // 223
* @public // 224
* @returns {Object} response object with _id property // 225
* // 226
* HTTP PUT file insert request handler // 227
*/ // 228
FS.HTTP.Handlers.PutInsert = function httpPutInsertHandler(ref) { // 229
var self = this; // 230
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 231
// 232
FS.debug && console.log("HTTP PUT (insert) handler"); // 233
// 234
// Create the nice FS.File // 235
var fileObj = new FS.File(); // 236
// 237
// Set its name // 238
fileObj.name(opts.filename || null); // 239
// 240
// Attach the readstream as the file's data // 241
fileObj.attachData(self.createReadStream(), {type: self.requestHeaders['content-type'] || 'application/octet-stream'});
// 243
// Validate with insert allow/deny // 244
FS.Utility.validateAction(ref.collection.files._validators['insert'], fileObj, self.userId); // 245
// 246
// Insert file into collection, triggering readStream storage // 247
ref.collection.insert(fileObj); // 248
// 249
// Send response // 250
self.setStatusCode(200); // 251
// 252
// Return the new file id // 253
return {_id: fileObj._id}; // 254
}; // 255
// 256
/** // 257
* @method FS.HTTP.Handlers.PutUpdate // 258
* @public // 259
* @returns {Object} response object with _id and chunk properties // 260
* // 261
* HTTP PUT file update chunk request handler // 262
*/ // 263
FS.HTTP.Handlers.PutUpdate = function httpPutUpdateHandler(ref) { // 264
var self = this; // 265
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 266
// 267
var chunk = parseInt(opts.chunk, 10); // 268
if (isNaN(chunk)) chunk = 0; // 269
// 270
FS.debug && console.log("HTTP PUT (update) handler received chunk: ", chunk); // 271
// 272
// Validate with insert allow/deny; also mounts and retrieves the file // 273
FS.Utility.validateAction(ref.collection.files._validators['insert'], ref.file, self.userId); // 274
// 275
self.createReadStream().pipe( FS.TempStore.createWriteStream(ref.file, chunk) ); // 276
// 277
// Send response // 278
self.setStatusCode(200); // 279
// 280
return { _id: ref.file._id, chunk: chunk }; // 281
}; // 282
// 283
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function () {
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/cfs:access-point/access-point-server.js //
// //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var path = Npm.require("path"); // 1
// 2
HTTP.publishFormats({ // 3
fileRecordFormat: function (input) { // 4
// Set the method scope content type to json // 5
this.setContentType('application/json'); // 6
if (FS.Utility.isArray(input)) { // 7
return EJSON.stringify(FS.Utility.map(input, function (obj) { // 8
return FS.Utility.cloneFileRecord(obj); // 9
})); // 10
} else { // 11
return EJSON.stringify(FS.Utility.cloneFileRecord(input)); // 12
} // 13
} // 14
}); // 15
// 16
/** // 17
* @method FS.HTTP.setHeadersForGet // 18
* @public // 19
* @param {Array} headers - List of headers, where each is a two-item array in which item 1 is the header name and item 2 is the header value.
* @param {Array|String} [collections] - Which collections the headers should be added for. Omit this argument to add the header for all collections.
* @returns {undefined} // 22
*/ // 23
FS.HTTP.setHeadersForGet = function setHeadersForGet(headers, collections) { // 24
if (typeof collections === "string") { // 25
collections = [collections]; // 26
} // 27
if (collections) { // 28
FS.Utility.each(collections, function(collectionName) { // 29
getHeadersByCollection[collectionName] = headers || []; // 30
}); // 31
} else { // 32
getHeaders = headers || []; // 33
} // 34
}; // 35
// 36
/** // 37
* @method FS.HTTP.publish // 38
* @public // 39
* @param {FS.Collection} collection // 40
* @param {Function} func - Publish function that returns a cursor. // 41
* @returns {undefined} // 42
* // 43
* Publishes all documents returned by the cursor at a GET URL // 44
* with the format baseUrl/record/collectionName. The publish // 45
* function `this` is similar to normal `Meteor.publish`. // 46
*/ // 47
FS.HTTP.publish = function fsHttpPublish(collection, func) { // 48
var name = baseUrl + '/record/' + collection.name; // 49
// Mount collection listing URL using http-publish package // 50
HTTP.publish({ // 51
name: name, // 52
defaultFormat: 'fileRecordFormat', // 53
collection: collection, // 54
collectionGet: true, // 55
collectionPost: false, // 56
documentGet: true, // 57
documentPut: false, // 58
documentDelete: false // 59
}, func); // 60
// 61
FS.debug && console.log("Registered HTTP method GET URLs:\n\n" + name + '\n' + name + '/:id\n'); // 62
}; // 63
// 64
/** // 65
* @method FS.HTTP.unpublish // 66
* @public // 67
* @param {FS.Collection} collection // 68
* @returns {undefined} // 69
* // 70
* Unpublishes a restpoint created by a call to `FS.HTTP.publish` // 71
*/ // 72
FS.HTTP.unpublish = function fsHttpUnpublish(collection) { // 73
// Mount collection listing URL using http-publish package // 74
HTTP.unpublish(baseUrl + '/record/' + collection.name); // 75
}; // 76
// 77
_existingMountPoints = {}; // 78
// 79
/** // 80
* @method defaultSelectorFunction // 81
* @private // 82
* @returns { collection, file } // 83
* // 84
* This is the default selector function // 85
*/ // 86
var defaultSelectorFunction = function() { // 87
var self = this; // 88
// Selector function // 89
// // 90
// This function will have to return the collection and the // 91
// file. If file not found undefined is returned - if null is returned the // 92
// search was not possible // 93
var opts = FS.Utility.extend({}, self.query || {}, self.params || {}); // 94
// 95
// Get the collection name from the url // 96
var collectionName = opts.collectionName; // 97
// 98
// Get the id from the url // 99
var id = opts.id; // 100
// 101
// Get the collection // 102
var collection = FS._collections[collectionName]; // 103
// 104
// Get the file if possible else return null // 105
var file = (id && collection)? collection.findOne({ _id: id }): null; // 106
// 107
// Return the collection and the file // 108
return { // 109
collection: collection, // 110
file: file, // 111
storeName: opts.store, // 112
download: opts.download, // 113
filename: opts.filename // 114
}; // 115
}; // 116
// 117
/* // 118
* @method FS.HTTP.mount // 119
* @public // 120
* @param {array of string} mountPoints mount points to map rest functinality on // 121
* @param {function} selector_f [selector] function returns `{ collection, file }` for mount points to work with // 122
* // 123
*/ // 124
FS.HTTP.mount = function(mountPoints, selector_f) { // 125
// We take mount points as an array and we get a selector function // 126
var selectorFunction = selector_f || defaultSelectorFunction; // 127
// 128
var accessPoint = { // 129
'stream': true, // 130
'auth': expirationAuth, // 131
'post': function(data) { // 132
// Use the selector for finding the collection and file reference // 133
var ref = selectorFunction.call(this); // 134
// 135
// We dont support post - this would be normal insert eg. of filerecord? // 136
throw new Meteor.Error(501, "Not implemented", "Post is not supported"); // 137
}, // 138
'put': function(data) { // 139
// Use the selector for finding the collection and file reference // 140
var ref = selectorFunction.call(this); // 141
// 142
// Make sure we have a collection reference // 143
if (!ref.collection) // 144
throw new Meteor.Error(404, "Not Found", "No collection found"); // 145
// 146
// Make sure we have a file reference // 147
if (ref.file === null) { // 148
// No id supplied so we will create a new FS.File instance and // 149
// insert the supplied data. // 150
return FS.HTTP.Handlers.PutInsert.apply(this, [ref]); // 151
} else { // 152
if (ref.file) { // 153
return FS.HTTP.Handlers.PutUpdate.apply(this, [ref]); // 154
} else { // 155
throw new Meteor.Error(404, "Not Found", 'No file found'); // 156
} // 157
} // 158
}, // 159
'get': function(data) { // 160
// Use the selector for finding the collection and file reference // 161
var ref = selectorFunction.call(this); // 162
// 163
// Make sure we have a collection reference // 164
if (!ref.collection) // 165
throw new Meteor.Error(404, "Not Found", "No collection found"); // 166
// 167
// Make sure we have a file reference // 168
if (ref.file === null) { // 169
// No id supplied so we will return the published list of files ala // 170
// http.publish in json format // 171
return FS.HTTP.Handlers.GetList.apply(this, [ref]); // 172
} else { // 173
if (ref.file) { // 174
return FS.HTTP.Handlers.Get.apply(this, [ref]); // 175
} else { // 176
throw new Meteor.Error(404, "Not Found", 'No file found'); // 177
} // 178
} // 179
}, // 180
'delete': function(data) { // 181
// Use the selector for finding the collection and file reference // 182
var ref = selectorFunction.call(this); // 183
// 184
// Make sure we have a collection reference // 185
if (!ref.collection) // 186
throw new Meteor.Error(404, "Not Found", "No collection found"); // 187
// 188
// Make sure we have a file reference // 189
if (ref.file) { // 190
return FS.HTTP.Handlers.Del.apply(this, [ref]); // 191
} else { // 192
throw new Meteor.Error(404, "Not Found", 'No file found'); // 193
} // 194
} // 195
}; // 196
// 197
var accessPoints = {}; // 198
// 199
// Add debug message // 200
FS.debug && console.log('Registered HTTP method URLs:'); // 201
// 202
FS.Utility.each(mountPoints, function(mountPoint) { // 203
// Couple mountpoint and accesspoint // 204
accessPoints[mountPoint] = accessPoint; // 205
// Remember our mountpoints // 206
_existingMountPoints[mountPoint] = mountPoint; // 207
// Add debug message // 208
FS.debug && console.log(mountPoint); // 209
}); // 210
// 211
// XXX: HTTP:methods should unmount existing mounts in case of overwriting? // 212
HTTP.methods(accessPoints); // 213
// 214
}; // 215
// 216
/** // 217
* @method FS.HTTP.unmount // 218
* @public // 219
* @param {string | array of string} [mountPoints] Optional, if not specified all mountpoints are unmounted // 220
* // 221
*/ // 222
FS.HTTP.unmount = function(mountPoints) { // 223
// The mountPoints is optional, can be string or array if undefined then // 224
// _existingMountPoints will be used // 225
var unmountList; // 226
// Container for the mount points to unmount // 227
var unmountPoints = {}; // 228
// 229
if (typeof mountPoints === 'undefined') { // 230
// Use existing mount points - unmount all // 231
unmountList = _existingMountPoints; // 232
} else if (mountPoints === ''+mountPoints) { // 233
// Got a string // 234
unmountList = [mountPoints]; // 235
} else if (mountPoints.length) { // 236
// Got an array // 237
unmountList = mountPoints; // 238
} // 239
// 240
// If we have a list to unmount // 241
if (unmountList) { // 242
// Iterate over each item // 243
FS.Utility.each(unmountList, function(mountPoint) { // 244
// Check _existingMountPoints to make sure the mount point exists in our // 245
// context / was created by the FS.HTTP.mount // 246
if (_existingMountPoints[mountPoint]) { // 247
// Mark as unmount // 248
unmountPoints[mountPoint] = false; // 249
// Release // 250
delete _existingMountPoints[mountPoint]; // 251
} // 252
}); // 253
FS.debug && console.log('FS.HTTP.unmount:'); // 254
FS.debug && console.log(unmountPoints); // 255
// Complete unmount // 256
HTTP.methods(unmountPoints); // 257
} // 258
}; // 259
// 260
// ### FS.Collection maps on HTTP pr. default on the following restpoints: // 261
// * // 262
// baseUrl + '/files/:collectionName/:id/:filename', // 263
// baseUrl + '/files/:collectionName/:id', // 264
// baseUrl + '/files/:collectionName' // 265
// // 266
// Change/ replace the existing mount point by: // 267
// ```js // 268
// // unmount all existing // 269
// FS.HTTP.unmount(); // 270
// // Create new mount point // 271
// FS.HTTP.mount([ // 272
// '/cfs/files/:collectionName/:id/:filename', // 273
// '/cfs/files/:collectionName/:id', // 274
// '/cfs/files/:collectionName' // 275
// ]); // 276
// ``` // 277
// // 278
mountUrls = function mountUrls() { // 279
// We unmount first in case we are calling this a second time // 280
FS.HTTP.unmount(); // 281
// 282
FS.HTTP.mount([ // 283
baseUrl + '/files/:collectionName/:id/:filename', // 284
baseUrl + '/files/:collectionName/:id', // 285
baseUrl + '/files/:collectionName' // 286
]); // 287
}; // 288
// 289
// Returns the userId from URL token // 290
var expirationAuth = function expirationAuth() { // 291
var self = this; // 292
// 293
// Read the token from '/hello?token=base64' // 294
var encodedToken = self.query.token; // 295
// 296
FS.debug && console.log("token: "+encodedToken); // 297
// 298
if (!encodedToken || !Meteor.users) return false; // 299
// 300
// Check the userToken before adding it to the db query // 301
// Set the this.userId // 302
var tokenString = FS.Utility.atob(encodedToken); // 303
// 304
var tokenObject; // 305
try { // 306
tokenObject = JSON.parse(tokenString); // 307
} catch(err) { // 308
throw new Meteor.Error(400, 'Bad Request'); // 309
} // 310
// 311
// XXX: Do some check here of the object // 312
var userToken = tokenObject.authToken; // 313
if (userToken !== ''+userToken) { // 314
throw new Meteor.Error(400, 'Bad Request'); // 315
} // 316
// 317
// If we have an expiration token we should check that it's still valid // 318
if (tokenObject.expiration != null) { // 319
// check if its too old // 320
var now = Date.now(); // 321
if (tokenObject.expiration < now) { // 322
FS.debug && console.log('Expired token: ' + tokenObject.expiration + ' is less than ' + now); // 323
throw new Meteor.Error(500, 'Expired token'); // 324
} // 325
} // 326
// 327
// We are not on a secure line - so we have to look up the user... // 328
var user = Meteor.users.findOne({ // 329
$or: [ // 330
{'services.resume.loginTokens.hashedToken': Accounts._hashLoginToken(userToken)}, // 331
{'services.resume.loginTokens.token': userToken} // 332
] // 333
}); // 334
// 335
// Set the userId in the scope // 336
return user && user._id; // 337
}; // 338
// 339
HTTP.methods( // 340
{'/cfs/servertime': { // 341
get: function(data) { // 342
return Date.now().toString(); // 343
} // 344
} // 345
}); // 346
// 347
// Unify client / server api // 348
FS.HTTP.now = function() { // 349
return Date.now(); // 350
}; // 351
// 352
// Start up the basic mount points // 353
Meteor.startup(function () { // 354
mountUrls(); // 355
}); // 356
// 357
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
///////////////////////////////////////////////////////////////////////
}).call(this);
/* Exports */
if (typeof Package === 'undefined') Package = {};
Package['cfs:access-point'] = {};
})();
+238
View File
@@ -0,0 +1,238 @@
/* global JsonRoutes */
if (Meteor.isServer) {
// todo XXX once we have a real API in place, move that route there
// todo XXX also share the route definition between the client and the server
// so that we could use something like
// `ApiRoutes.path('boards/export', boardId)``
// on the client instead of copy/pasting the route path manually between the
// client and the server.
/**
* @operation export
* @tag Boards
*
* @summary This route is used to export the board.
*
* @description If user is already logged-in, pass loginToken as param
* "authToken": '/api/boards/:boardId/export?authToken=:token'
*
* See https://blog.kayla.com.au/server-side-route-authentication-in-meteor/
* for detailed explanations
*
* @param {string} boardId the ID of the board we are exporting
* @param {string} authToken the loginToken
*/
JsonRoutes.add('get', '/api/boards/:boardId/export', function(req, res) {
const boardId = req.params.boardId;
let user = null;
const loginToken = req.query.authToken;
if (loginToken) {
const hashToken = Accounts._hashLoginToken(loginToken);
user = Meteor.users.findOne({
'services.resume.loginTokens.hashedToken': hashToken,
});
} else if (!Meteor.settings.public.sandstorm) {
Authentication.checkUserId(req.userId);
user = Users.findOne({ _id: req.userId, isAdmin: true });
}
const exporter = new Exporter(boardId);
if (exporter.canExport(user)) {
JsonRoutes.sendResult(res, {
code: 200,
data: exporter.build(),
});
} else {
// we could send an explicit error message, but on the other hand the only
// way to get there is by hacking the UI so let's keep it raw.
JsonRoutes.sendResult(res, 403);
}
});
}
// exporter maybe is broken since Gridfs introduced, add fs and path
export class Exporter {
constructor(boardId) {
this._boardId = boardId;
}
build() {
const fs = Npm.require('fs');
const os = Npm.require('os');
const path = Npm.require('path');
const byBoard = { boardId: this._boardId };
const byBoardNoLinked = {
boardId: this._boardId,
linkedId: { $in: ['', null] },
};
// we do not want to retrieve boardId in related elements
const noBoardId = {
fields: {
boardId: 0,
},
};
const result = {
_format: 'wekan-board-1.0.0',
};
_.extend(
result,
Boards.findOne(this._boardId, {
fields: {
stars: 0,
},
}),
);
result.lists = Lists.find(byBoard, noBoardId).fetch();
result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
result.customFields = CustomFields.find(
{ boardIds: { $in: [this.boardId] } },
{ fields: { boardId: 0 } },
).fetch();
result.comments = CardComments.find(byBoard, noBoardId).fetch();
result.activities = Activities.find(byBoard, noBoardId).fetch();
result.rules = Rules.find(byBoard, noBoardId).fetch();
result.checklists = [];
result.checklistItems = [];
result.subtaskItems = [];
result.triggers = [];
result.actions = [];
result.cards.forEach(card => {
result.checklists.push(
...Checklists.find({
cardId: card._id,
}).fetch(),
);
result.checklistItems.push(
...ChecklistItems.find({
cardId: card._id,
}).fetch(),
);
result.subtaskItems.push(
...Cards.find({
parentId: card._id,
}).fetch(),
);
});
result.rules.forEach(rule => {
result.triggers.push(
...Triggers.find(
{
_id: rule.triggerId,
},
noBoardId,
).fetch(),
);
result.actions.push(
...Actions.find(
{
_id: rule.actionId,
},
noBoardId,
).fetch(),
);
});
// [Old] for attachments we only export IDs and absolute url to original doc
// [New] Encode attachment to base64
const getBase64Data = function(doc, callback) {
let buffer = new Buffer(0);
// callback has the form function (err, res) {}
const tmpFile = path.join(
os.tmpdir(),
`tmpexport${process.pid}${Math.random()}`,
);
const tmpWriteable = fs.createWriteStream(tmpFile);
const readStream = doc.createReadStream();
readStream.on('data', function(chunk) {
buffer = Buffer.concat([buffer, chunk]);
});
readStream.on('error', function(err) {
callback(err, null);
});
readStream.on('end', function() {
// done
fs.unlink(tmpFile, () => {
//ignored
});
callback(null, buffer.toString('base64'));
});
readStream.pipe(tmpWriteable);
};
const getBase64DataSync = Meteor.wrapAsync(getBase64Data);
result.attachments = Attachments.find(byBoard)
.fetch()
.map(attachment => {
return {
_id: attachment._id,
cardId: attachment.cardId,
// url: FlowRouter.url(attachment.url()),
file: getBase64DataSync(attachment),
name: attachment.original.name,
type: attachment.original.type,
};
});
// we also have to export some user data - as the other elements only
// include id but we have to be careful:
// 1- only exports users that are linked somehow to that board
// 2- do not export any sensitive information
const users = {};
result.members.forEach(member => {
users[member.userId] = true;
});
result.lists.forEach(list => {
users[list.userId] = true;
});
result.cards.forEach(card => {
users[card.userId] = true;
if (card.members) {
card.members.forEach(memberId => {
users[memberId] = true;
});
}
});
result.comments.forEach(comment => {
users[comment.userId] = true;
});
result.activities.forEach(activity => {
users[activity.userId] = true;
});
result.checklists.forEach(checklist => {
users[checklist.userId] = true;
});
const byUserIds = {
_id: {
$in: Object.getOwnPropertyNames(users),
},
};
// we use whitelist to be sure we do not expose inadvertently
// some secret fields that gets added to User later.
const userFields = {
fields: {
_id: 1,
username: 1,
'profile.fullname': 1,
'profile.initials': 1,
'profile.avatarUrl': 1,
},
};
result.users = Users.find(byUserIds, userFields)
.fetch()
.map(user => {
// user avatar is stored as a relative url, we export absolute
if ((user.profile || {}).avatarUrl) {
user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
}
return user;
});
return result;
}
canExport(user) {
const board = Boards.findOne(this._boardId);
return board && board.isVisibleBy(user);
}
}
+155
View File
@@ -0,0 +1,155 @@
name: wekan
version: git
summary: The open-source kanban
description: |
Wekan is an open-source and collaborative kanban board application.
Whether you’re maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
Depending on target environment, some configuration settings might need to be adjusted.
For full list of configuration options call:
$ wekan.help
confinement: strict
grade: stable
base: core18
architectures:
- amd64
plugs:
mongodb-plug:
interface: content
target: $SNAP_DATA/shared
hooks:
configure:
plugs:
- network
- network-bind
slots:
mongodb-slot:
interface: content
write:
- $SNAP_DATA/share
apps:
wekan:
command: wekan-control
daemon: simple
plugs: [network, network-bind]
mongodb:
command: mongodb-control
daemon: simple
plugs: [network, network-bind]
caddy:
command: caddy-control
daemon: simple
plugs: [network, network-bind]
help:
command: wekan-help
database-backup:
command: mongodb-backup
plugs: [network, network-bind]
database-list-backups:
command: ls -al $SNAP_COMMON/db-backups/
database-restore:
command: mongodb-restore
plugs: [network, network-bind]
parts:
mongodb:
source: https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.2/multiverse/binary-amd64/mongodb-org-server_4.2.2_amd64.deb
#https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-4.0.14.tgz
#https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.22.tgz
plugin: dump
stage-packages: [libssl1.0.0, libcurl3]
filesets:
mongo:
- usr
- bin
- lib
stage:
- $mongo
prime:
- $mongo
wekan:
source: .
plugin: nodejs
node-engine: 12.14.1
node-packages:
- node-gyp
- node-pre-gyp
- fibers
build-packages:
- ca-certificates
- apt-utils
- build-essential
- python
- python3
- g++
- capnproto
- curl
- libcurl3
- execstack
- nodejs
- npm
stage-packages:
- libfontconfig1
override-build: |
echo "Cleaning environment first"
rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
rm -rf .build
echo "Installing meteor"
curl https://install.meteor.com/ -o install_meteor.sh
chmod +x install_meteor.sh
sh install_meteor.sh
rm install_meteor.sh
rm -rf .build
meteor add standard-minifier-js --allow-superuser
meteor npm install --allow-superuser
meteor npm install --allow-superuser --save babel-runtime
meteor build .build --directory --allow-superuser
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
cd .build/bundle/programs/server
npm install
npm install --allow-superuser --save babel-runtime
# Change back to Wekan source directory
cd ../../../..
cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
organize:
README: README.wekan
prime:
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
helpers:
source: snap-src
plugin: dump
caddy:
plugin: dump
source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
source-type: tar
organize:
caddy: bin/caddy
CHANGES.txt: CADDY_CHANGES.txt
EULA.txt: CADDY_EULA.txt
LICENSES.txt: CADDY_LICENSES.txt
README.txt: CADDY_README.txt
stage:
- -init
+640
View File
@@ -0,0 +1,640 @@
import ldapjs from 'ldapjs';
import util from 'util';
import Bunyan from 'bunyan';
import { log_debug, log_info, log_warn, log_error } from './logger';
export default class LDAP {
constructor() {
this.ldapjs = ldapjs;
this.connected = false;
this.options = {
host: this.constructor.settings_get('LDAP_HOST'),
port: this.constructor.settings_get('LDAP_PORT'),
Reconnect: this.constructor.settings_get('LDAP_RECONNECT'),
timeout: this.constructor.settings_get('LDAP_TIMEOUT'),
connect_timeout: this.constructor.settings_get('LDAP_CONNECT_TIMEOUT'),
idle_timeout: this.constructor.settings_get('LDAP_IDLE_TIMEOUT'),
encryption: this.constructor.settings_get('LDAP_ENCRYPTION'),
ca_cert: this.constructor.settings_get('LDAP_CA_CERT'),
reject_unauthorized:
this.constructor.settings_get('LDAP_REJECT_UNAUTHORIZED') || false,
Authentication: this.constructor.settings_get('LDAP_AUTHENTIFICATION'),
Authentication_UserDN: this.constructor.settings_get(
'LDAP_AUTHENTIFICATION_USERDN',
),
Authentication_Password: this.constructor.settings_get(
'LDAP_AUTHENTIFICATION_PASSWORD',
),
Authentication_Fallback: this.constructor.settings_get(
'LDAP_LOGIN_FALLBACK',
),
BaseDN: this.constructor.settings_get('LDAP_BASEDN'),
Internal_Log_Level: this.constructor.settings_get('INTERNAL_LOG_LEVEL'),
User_Authentication: this.constructor.settings_get(
'LDAP_USER_AUTHENTICATION',
),
User_Authentication_Field: this.constructor.settings_get(
'LDAP_USER_AUTHENTICATION_FIELD',
),
User_Attributes: this.constructor.settings_get('LDAP_USER_ATTRIBUTES'),
User_Search_Filter: this.constructor.settings_get(
'LDAP_USER_SEARCH_FILTER',
),
User_Search_Scope: this.constructor.settings_get(
'LDAP_USER_SEARCH_SCOPE',
),
User_Search_Field: this.constructor.settings_get(
'LDAP_USER_SEARCH_FIELD',
),
Search_Page_Size: this.constructor.settings_get('LDAP_SEARCH_PAGE_SIZE'),
Search_Size_Limit: this.constructor.settings_get(
'LDAP_SEARCH_SIZE_LIMIT',
),
group_filter_enabled: this.constructor.settings_get(
'LDAP_GROUP_FILTER_ENABLE',
),
group_filter_object_class: this.constructor.settings_get(
'LDAP_GROUP_FILTER_OBJECTCLASS',
),
group_filter_group_id_attribute: this.constructor.settings_get(
'LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE',
),
group_filter_group_member_attribute: this.constructor.settings_get(
'LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE',
),
group_filter_group_member_format: this.constructor.settings_get(
'LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT',
),
group_filter_group_name: this.constructor.settings_get(
'LDAP_GROUP_FILTER_GROUP_NAME',
),
};
}
static settings_get(name, ...args) {
let value = process.env[name];
if (value !== undefined) {
if (value === 'true' || value === 'false') {
value = JSON.parse(value);
} else if (value !== '' && !isNaN(value)) {
value = Number(value);
}
return value;
} else {
log_warn(`Lookup for unset variable: ${name}`);
}
}
connectSync(...args) {
if (!this._connectSync) {
this._connectSync = Meteor.wrapAsync(this.connectAsync, this);
}
return this._connectSync(...args);
}
searchAllSync(...args) {
if (!this._searchAllSync) {
this._searchAllSync = Meteor.wrapAsync(this.searchAllAsync, this);
}
return this._searchAllSync(...args);
}
connectAsync(callback) {
log_info('Init setup');
let replied = false;
const connectionOptions = {
url: `${this.options.host}:${this.options.port}`,
timeout: this.options.timeout,
connectTimeout: this.options.connect_timeout,
idleTimeout: this.options.idle_timeout,
reconnect: this.options.Reconnect,
};
if (this.options.Internal_Log_Level !== 'disabled') {
connectionOptions.log = new Bunyan({
name: 'ldapjs',
component: 'client',
stream: process.stderr,
level: this.options.Internal_Log_Level,
});
}
const tlsOptions = {
rejectUnauthorized: this.options.reject_unauthorized,
};
if (this.options.ca_cert && this.options.ca_cert !== '') {
// Split CA cert into array of strings
const chainLines = this.constructor
.settings_get('LDAP_CA_CERT')
.split('\n');
let cert = [];
const ca = [];
chainLines.forEach(line => {
cert.push(line);
if (line.match(/-END CERTIFICATE-/)) {
ca.push(cert.join('\n'));
cert = [];
}
});
tlsOptions.ca = ca;
}
if (this.options.encryption === 'ssl') {
connectionOptions.url = `ldaps://${connectionOptions.url}`;
connectionOptions.tlsOptions = tlsOptions;
} else {
connectionOptions.url = `ldap://${connectionOptions.url}`;
}
log_info('Connecting', connectionOptions.url);
log_debug(`connectionOptions${util.inspect(connectionOptions)}`);
this.client = ldapjs.createClient(connectionOptions);
this.bindSync = Meteor.wrapAsync(this.client.bind, this.client);
this.client.on('error', error => {
log_error('connection', error);
if (replied === false) {
replied = true;
callback(error, null);
}
});
this.client.on('idle', () => {
log_info('Idle');
this.disconnect();
});
this.client.on('close', () => {
log_info('Closed');
});
if (this.options.encryption === 'tls') {
// Set host parameter for tls.connect which is used by ldapjs starttls. This shouldn't be needed in newer nodejs versions (e.g v5.6.0).
// https://github.com/RocketChat/Rocket.Chat/issues/2035
// https://github.com/mcavage/node-ldapjs/issues/349
tlsOptions.host = this.options.host;
log_info('Starting TLS');
log_debug('tlsOptions', tlsOptions);
this.client.starttls(tlsOptions, null, (error, response) => {
if (error) {
log_error('TLS connection', error);
if (replied === false) {
replied = true;
callback(error, null);
}
return;
}
log_info('TLS connected');
this.connected = true;
if (replied === false) {
replied = true;
callback(null, response);
}
});
} else {
this.client.on('connect', response => {
log_info('LDAP connected');
this.connected = true;
if (replied === false) {
replied = true;
callback(null, response);
}
});
}
setTimeout(() => {
if (replied === false) {
log_error('connection time out', connectionOptions.connectTimeout);
replied = true;
callback(new Error('Timeout'));
}
}, connectionOptions.connectTimeout);
}
getUserFilter(username) {
const filter = [];
if (this.options.User_Search_Filter !== '') {
if (this.options.User_Search_Filter[0] === '(') {
filter.push(`${this.options.User_Search_Filter}`);
} else {
filter.push(`(${this.options.User_Search_Filter})`);
}
}
const usernameFilter = this.options.User_Search_Field.split(',').map(
item => `(${item}=${username})`,
);
if (usernameFilter.length === 0) {
log_error('LDAP_LDAP_User_Search_Field not defined');
} else if (usernameFilter.length === 1) {
filter.push(`${usernameFilter[0]}`);
} else {
filter.push(`(|${usernameFilter.join('')})`);
}
return `(&${filter.join('')})`;
}
bindUserIfNecessary(username, password) {
if (this.domainBinded === true) {
return;
}
if (!this.options.User_Authentication) {
return;
}
if (!this.options.BaseDN) throw new Error('BaseDN is not provided');
const userDn = `${this.options.User_Authentication_Field}=${username},${this.options.BaseDN}`;
this.bindSync(userDn, password);
this.domainBinded = true;
}
bindIfNecessary() {
if (this.domainBinded === true) {
return;
}
if (this.options.Authentication !== true) {
return;
}
log_info('Binding UserDN', this.options.Authentication_UserDN);
this.bindSync(
this.options.Authentication_UserDN,
this.options.Authentication_Password,
);
this.domainBinded = true;
}
searchUsersSync(username, page) {
this.bindIfNecessary();
const searchOptions = {
filter: this.getUserFilter(username),
scope: this.options.User_Search_Scope || 'sub',
sizeLimit: this.options.Search_Size_Limit,
};
if (!!this.options.User_Attributes)
searchOptions.attributes = this.options.User_Attributes.split(',');
if (this.options.Search_Page_Size > 0) {
searchOptions.paged = {
pageSize: this.options.Search_Page_Size,
pagePause: !!page,
};
}
log_info('Searching user', username);
log_debug('searchOptions', searchOptions);
log_debug('BaseDN', this.options.BaseDN);
if (page) {
return this.searchAllPaged(this.options.BaseDN, searchOptions, page);
}
return this.searchAllSync(this.options.BaseDN, searchOptions);
}
getUserByIdSync(id, attribute) {
this.bindIfNecessary();
const Unique_Identifier_Field = this.constructor
.settings_get('LDAP_UNIQUE_IDENTIFIER_FIELD')
.split(',');
let filter;
if (attribute) {
filter = new this.ldapjs.filters.EqualityFilter({
attribute,
value: new Buffer(id, 'hex'),
});
} else {
const filters = [];
Unique_Identifier_Field.forEach(item => {
filters.push(
new this.ldapjs.filters.EqualityFilter({
attribute: item,
value: new Buffer(id, 'hex'),
}),
);
});
filter = new this.ldapjs.filters.OrFilter({ filters });
}
const searchOptions = {
filter,
scope: 'sub',
};
log_info('Searching by id', id);
log_debug('search filter', searchOptions.filter.toString());
log_debug('BaseDN', this.options.BaseDN);
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return;
}
if (result.length > 1) {
log_error('Search by id', id, 'returned', result.length, 'records');
}
return result[0];
}
getUserByUsernameSync(username) {
this.bindIfNecessary();
const searchOptions = {
filter: this.getUserFilter(username),
scope: this.options.User_Search_Scope || 'sub',
};
log_info('Searching user', username);
log_debug('searchOptions', searchOptions);
log_debug('BaseDN', this.options.BaseDN);
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return;
}
if (result.length > 1) {
log_error(
'Search by username',
username,
'returned',
result.length,
'records',
);
}
return result[0];
}
getUserGroups(username, ldapUser) {
if (!this.options.group_filter_enabled) {
return true;
}
const filter = ['(&'];
if (this.options.group_filter_object_class !== '') {
filter.push(`(objectclass=${this.options.group_filter_object_class})`);
}
if (this.options.group_filter_group_member_attribute !== '') {
const format_value =
ldapUser[this.options.group_filter_group_member_format];
if (format_value) {
filter.push(
`(${this.options.group_filter_group_member_attribute}=${format_value})`,
);
}
}
filter.push(')');
const searchOptions = {
filter: filter.join('').replace(/#{username}/g, username),
scope: 'sub',
};
log_debug('Group list filter LDAP:', searchOptions.filter);
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return [];
}
const grp_identifier = this.options.group_filter_group_id_attribute || 'cn';
const groups = [];
result.map(item => {
groups.push(item[grp_identifier]);
});
log_debug(`Groups: ${groups.join(', ')}`);
return groups;
}
isUserInGroup(username, ldapUser) {
if (!this.options.group_filter_enabled) {
return true;
}
const grps = this.getUserGroups(username, ldapUser);
const filter = ['(&'];
if (this.options.group_filter_object_class !== '') {
filter.push(`(objectclass=${this.options.group_filter_object_class})`);
}
if (this.options.group_filter_group_member_attribute !== '') {
const format_value =
ldapUser[this.options.group_filter_group_member_format];
if (format_value) {
filter.push(
`(${this.options.group_filter_group_member_attribute}=${format_value})`,
);
}
}
if (this.options.group_filter_group_id_attribute !== '') {
filter.push(
`(${this.options.group_filter_group_id_attribute}=${this.options.group_filter_group_name})`,
);
}
filter.push(')');
const searchOptions = {
filter: filter.join('').replace(/#{username}/g, username),
scope: 'sub',
};
log_debug('Group filter LDAP:', searchOptions.filter);
const result = this.searchAllSync(this.options.BaseDN, searchOptions);
if (!Array.isArray(result) || result.length === 0) {
return false;
}
return true;
}
extractLdapEntryData(entry) {
const values = {
_raw: entry.raw,
};
Object.keys(values._raw).forEach(key => {
const value = values._raw[key];
if (!['thumbnailPhoto', 'jpegPhoto'].includes(key)) {
if (value instanceof Buffer) {
values[key] = value.toString();
} else {
values[key] = value;
}
}
});
return values;
}
searchAllPaged(BaseDN, options, page) {
this.bindIfNecessary();
const processPage = ({ entries, title, end, next }) => {
log_info(title);
// Force LDAP idle to wait the record processing
this.client._updateIdle(true);
page(null, entries, {
end,
next: () => {
// Reset idle timer
this.client._updateIdle();
next && next();
},
});
};
this.client.search(BaseDN, options, (error, res) => {
if (error) {
log_error(error);
page(error);
return;
}
res.on('error', error => {
log_error(error);
page(error);
return;
});
let entries = [];
const internalPageSize =
options.paged && options.paged.pageSize > 0
? options.paged.pageSize * 2
: 500;
res.on('searchEntry', entry => {
entries.push(this.extractLdapEntryData(entry));
if (entries.length >= internalPageSize) {
processPage({
entries,
title: 'Internal Page',
end: false,
});
entries = [];
}
});
res.on('page', (result, next) => {
if (!next) {
this.client._updateIdle(true);
processPage({
entries,
title: 'Final Page',
end: true,
});
} else if (entries.length) {
log_info('Page');
processPage({
entries,
title: 'Page',
end: false,
next,
});
entries = [];
}
});
res.on('end', () => {
if (entries.length) {
processPage({
entries,
title: 'Final Page',
end: true,
});
entries = [];
}
});
});
}
searchAllAsync(BaseDN, options, callback) {
this.bindIfNecessary();
this.client.search(BaseDN, options, (error, res) => {
if (error) {
log_error(error);
callback(error);
return;
}
res.on('error', error => {
log_error(error);
callback(error);
return;
});
const entries = [];
res.on('searchEntry', entry => {
entries.push(this.extractLdapEntryData(entry));
});
res.on('end', () => {
log_info('Search result count', entries.length);
callback(null, entries);
});
});
}
authSync(dn, password) {
log_info('Authenticating', dn);
try {
if (password === '') {
throw new Error('Password is not provided');
}
this.bindSync(dn, password);
log_info('Authenticated', dn);
return true;
} catch (error) {
log_info('Not authenticated', dn);
log_debug('error', error);
return false;
}
}
disconnect() {
this.connected = false;
this.domainBinded = false;
log_info('Disconecting');
this.client.unbind();
}
}
+163
View File
@@ -0,0 +1,163 @@
Oidc = {};
OAuth.registerService('oidc', 2, null, function(query) {
var debug = process.env.DEBUG || false;
var token = getToken(query);
if (debug) console.log('XXX: register token:', token);
var accessToken = token.access_token || token.id_token;
var expiresAt = +new Date() + 1000 * parseInt(token.expires_in, 10);
var userinfo = getUserInfo(accessToken);
if (debug) console.log('XXX: userinfo:', userinfo);
var serviceData = {};
serviceData.id = userinfo[process.env.OAUTH2_ID_MAP]; // || userinfo["id"];
serviceData.username = userinfo[process.env.OAUTH2_USERNAME_MAP]; // || userinfo["uid"];
serviceData.fullname = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
serviceData.accessToken = accessToken;
serviceData.expiresAt = expiresAt;
serviceData.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
if (accessToken) {
var tokenContent = getTokenContent(accessToken);
var fields = _.pick(
tokenContent,
getConfiguration().idTokenWhitelistFields,
);
_.extend(serviceData, fields);
}
if (token.refresh_token) serviceData.refreshToken = token.refresh_token;
if (debug) console.log('XXX: serviceData:', serviceData);
var profile = {};
profile.name = userinfo[process.env.OAUTH2_FULLNAME_MAP]; // || userinfo["displayName"];
profile.email = userinfo[process.env.OAUTH2_EMAIL_MAP]; // || userinfo["email"];
if (debug) console.log('XXX: profile:', profile);
return {
serviceData: serviceData,
options: { profile: profile },
};
});
var userAgent = 'Meteor';
if (Meteor.release) {
userAgent += '/' + Meteor.release;
}
var getToken = function(query) {
var debug = process.env.DEBUG || false;
var config = getConfiguration();
if (config.tokenEndpoint.includes('https://')) {
var serverTokenEndpoint = config.tokenEndpoint;
} else {
var serverTokenEndpoint = config.serverUrl + config.tokenEndpoint;
}
var requestPermissions = config.requestPermissions;
var response;
try {
response = HTTP.post(serverTokenEndpoint, {
headers: {
Accept: 'application/json',
'User-Agent': userAgent,
},
params: {
code: query.code,
client_id: config.clientId,
client_secret: OAuth.openSecret(config.secret),
redirect_uri: OAuth._redirectUri('oidc', config),
grant_type: 'authorization_code',
scope: requestPermissions,
state: query.state,
},
});
} catch (err) {
throw _.extend(
new Error(
'Failed to get token from OIDC ' +
serverTokenEndpoint +
': ' +
err.message,
),
{ response: err.response },
);
}
if (response.data.error) {
// if the http response was a json object with an error attribute
throw new Error(
'Failed to complete handshake with OIDC ' +
serverTokenEndpoint +
': ' +
response.data.error,
);
} else {
if (debug) console.log('XXX: getToken response: ', response.data);
return response.data;
}
};
var getUserInfo = function(accessToken) {
var debug = process.env.DEBUG || false;
var config = getConfiguration();
// Some userinfo endpoints use a different base URL than the authorization or token endpoints.
// This logic allows the end user to override the setting by providing the full URL to userinfo in their config.
if (config.userinfoEndpoint.includes('https://')) {
var serverUserinfoEndpoint = config.userinfoEndpoint;
} else {
var serverUserinfoEndpoint = config.serverUrl + config.userinfoEndpoint;
}
var response;
try {
response = HTTP.get(serverUserinfoEndpoint, {
headers: {
'User-Agent': userAgent,
Authorization: 'Bearer ' + accessToken,
},
});
} catch (err) {
throw _.extend(
new Error(
'Failed to fetch userinfo from OIDC ' +
serverUserinfoEndpoint +
': ' +
err.message,
),
{ response: err.response },
);
}
if (debug) console.log('XXX: getUserInfo response: ', response.data);
return response.data;
};
var getConfiguration = function() {
var config = ServiceConfiguration.configurations.findOne({ service: 'oidc' });
if (!config) {
throw new ServiceConfiguration.ConfigError('Service oidc not configured.');
}
return config;
};
var getTokenContent = function(token) {
var content = null;
if (token) {
try {
var parts = token.split('.');
var header = JSON.parse(new Buffer(parts[0], 'base64').toString());
content = JSON.parse(new Buffer(parts[1], 'base64').toString());
var signature = new Buffer(parts[2], 'base64');
var signed = parts[0] + '.' + parts[1];
} catch (err) {
this.content = {
exp: 0,
};
}
}
return content;
};
Oidc.retrieveCredential = function(credentialToken, credentialSecret) {
return OAuth.retrieveCredential(credentialToken, credentialSecret);
};
File diff suppressed because it is too large. Load diff
+73
View File
@@ -0,0 +1,73 @@
{
"name": "wekan",
"version": "v3.86.0",
"description": "Open-Source kanban",
"private": true,
"scripts": {
"lint": "eslint --cache --ext .js --ignore-path .eslintignore .",
"lint:eslint:fix": "eslint --ext .js --ignore-path .eslintignore --fix .",
"lint:staged": "lint-staged",
"prettify": "prettier --write '**/*.js' '**/*.jsx'",
"test": "npm run lint"
},
"lint-staged": {
"*.js": [
"meteor npm run prettify",
"meteor npm run lint:eslint:fix",
"git add --force"
],
"*.jsx": [
"meteor npm run prettify",
"meteor npm run lint:eslint:fix",
"git add --force"
],
"*.json": [
"prettier --write",
"git add --force"
]
},
"pre-commit": "lint:staged",
"eslintConfig": {
"extends": "@meteorjs/eslint-config-meteor"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wekan/wekan.git"
},
"license": "MIT",
"bugs": {
"url": "https://github.com/wekan/wekan/issues"
},
"homepage": "https://wekan.github.io",
"devDependencies": {
"eslint": "^6.8.0",
"eslint-config-meteor": "^0.1.1",
"eslint-config-prettier": "^6.10.0",
"eslint-import-resolver-meteor": "^0.4.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-meteor": "^6.0.0",
"eslint-plugin-prettier": "^3.1.2",
"lint-staged": "^10.0.8",
"pre-commit": "^1.2.2",
"prettier": "^1.19.1",
"prettier-eslint": "^9.0.1"
},
"dependencies": {
"@babel/runtime": "^7.8.7",
"ajv": "^6.12.0",
"babel-runtime": "^6.26.0",
"bcrypt": "^4.0.1",
"bson": "^4.0.3",
"bunyan": "^1.8.12",
"es6-promise": "^4.2.8",
"gridfs-stream": "^1.1.1",
"ldapjs": "^1.0.2",
"meteor-node-stubs": "^1.0.0",
"mongodb": "^3.5.5",
"os": "^0.1.1",
"page": "^1.11.5",
"qs": "^6.9.1",
"source-map-support": "^0.5.16",
"xss": "^1.0.6"
}
}
+244
View File
@@ -0,0 +1,244 @@
name: wekan
version: 0
version-script: git describe --tags | cut -c 2-
summary: The open-source kanban
description: |
Wekan is an open-source and collaborative kanban board application.
Whether you’re maintaining a personal todo list, planning your holidays with some friends, or working in a team on your next revolutionary idea, Kanban boards are an unbeatable tool to keep your things organized. They give you a visual overview of the current state of your project, and make you productive by allowing you to focus on the few items that matter the most.
Depending on target environment, some configuration settings might need to be adjusted.
For full list of configuration options call:
$ wekan.help
confinement: strict
grade: stable
architectures:
- amd64
plugs:
mongodb-plug:
interface: content
target: $SNAP_DATA/shared
hooks:
configure:
plugs:
- network
- network-bind
slots:
mongodb-slot:
interface: content
write:
- $SNAP_DATA/share
apps:
wekan:
command: wekan-control
daemon: simple
plugs: [network, network-bind]
mongodb:
command: mongodb-control
daemon: simple
plugs: [network, network-bind]
caddy:
command: caddy-control
daemon: simple
plugs: [network, network-bind]
help:
command: wekan-help
database-backup:
command: mongodb-backup
plugs: [network, network-bind]
database-list-backups:
command: ls -al $SNAP_COMMON/db-backups/
database-restore:
command: mongodb-restore
plugs: [network, network-bind]
parts:
mongodb:
source: https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-ubuntu1604-3.2.22.tgz
plugin: dump
stage-packages: [libssl1.0.0]
filesets:
mongo:
- usr
- bin
- lib
stage:
- $mongo
prime:
- $mongo
wekan:
source: .
plugin: nodejs
node-engine: 8.17.0
node-packages:
- node-gyp
- node-pre-gyp
- fibers@2.0.0
build-packages:
- ca-certificates
- apt-utils
- python
# - python3
- g++
- capnproto
- curl
- execstack
- nodejs
- npm
stage-packages:
- libfontconfig1
override-build: |
echo "Cleaning environment first"
rm -rf ~/.meteor ~/.npm /usr/local/lib/node_modules
# Create the OpenAPI specification
rm -rf .build
#mkdir -p .build/python
#cd .build/python
#git clone --depth 1 -b master https://github.com/Kronuz/esprima-python
#cd esprima-python
#python3 setup.py install
#cd ../../..
#mkdir -p ./public/api
#python3 ./openapi/generate_openapi.py --release $(git describe --tags --abbrev=0) > ./public/api/wekan.yml
# we temporary need api2html and mkdirp
#npm install -g api2html@0.3.0
#npm install -g mkdirp
#api2html -c ./public/logo-header.png -o ./public/api/wekan.html ./public/api/wekan.yml
#npm uninstall -g mkdirp
#npm uninstall -g api2html
# Node Fibers 100% CPU usage issue:
# https://github.com/wekan/wekan-mongodb/issues/2#issuecomment-381453161
# https://github.com/meteor/meteor/issues/9796#issuecomment-381676326
# https://github.com/sandstorm-io/sandstorm/blob/0f1fec013fe7208ed0fd97eb88b31b77e3c61f42/shell/server/00-startup.js#L99-L129
# Also see beginning of wekan/server/authentication.js
# import Fiber from "fibers";
# Fiber.poolSize = 1e9;
# OLD: Download node version 8.12.0 prerelease build => Official node 8.12.0 has been released
# Description at https://releases.wekan.team/node.txt
##echo "375bd8db50b9c692c0bbba6e96d4114cd29bee3770f901c1ff2249d1038f1348 node" >> node-SHASUMS256.txt.asc
##curl https://releases.wekan.team/node -o node
# Verify Fibers patched node authenticity
##echo "Fibers 100% CPU issue patched node authenticity:"
##grep node node-SHASUMS256.txt.asc | shasum -a 256 -c -
##rm -f node-SHASUMS256.txt.asc
##chmod +x node
##mv node `which node`
# DOES NOT WORK: paxctl fix.
# Removed from build-packages: - paxctl
#echo "Applying paxctl fix for alpine linux: https://github.com/wekan/wekan/issues/1303"
#paxctl -mC `which node`
#echo "Installing npm"
#curl -L https://www.npmjs.com/install.sh | sh
echo "Installing meteor"
curl https://install.meteor.com/ -o install_meteor.sh
#sed -i "s|RELEASE=.*|RELEASE=\"1.8.1-beta.0\"|g" install_meteor.sh
chmod +x install_meteor.sh
sh install_meteor.sh
rm install_meteor.sh
# REPOS BELOW ARE INCLUDED TO WEKAN REPO
#if [ ! -d "packages" ]; then
# mkdir packages
#fi
#if [ ! -d "packages/kadira-flow-router" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/flow-router.git kadira-flow-router
# cd ..
#fi
#if [ ! -d "packages/meteor-useraccounts-core" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/meteor-useraccounts/core.git meteor-useraccounts-core
# sed -i 's/api\.versionsFrom/\/\/api.versionsFrom/' meteor-useraccounts-core/package.js
# cd ..
#fi
#if [ ! -d "packages/meteor-accounts-cas" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-cas.git meteor-accounts-cas
# cd ..
#fi
#if [ ! -d "packages/wekan-ldap" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/wekan-ldap.git
# cd ..
#fi
#if [ ! -d "packages/wekan-scrollbar" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/wekan-scrollbar.git
# cd ..
#fi
#if [ ! -d "packages/wekan_accounts-oidc" ]; then
# cd packages
# git clone --depth 1 -b master https://github.com/wekan/meteor-accounts-oidc.git
# mv meteor-accounts-oidc/packages/switch_accounts-oidc wekan-accounts-oidc
# mv meteor-accounts-oidc/packages/switch_oidc wekan-oidc
# rm -rf meteor-accounts-oidc
# cd ..
#fi
#if [ ! -d "packages/markdown" ]; then
# cd packages
# git clone --depth 1 -b master --recurse-submodules https://github.com/wekan/markdown.git
# cd ..
#fi
rm -rf .build
meteor add standard-minifier-js --allow-superuser
meteor npm install --allow-superuser
meteor npm install --allow-superuser --save babel-runtime
meteor build .build --directory --allow-superuser
cp -f fix-download-unicode/cfs_access-point.txt .build/bundle/programs/server/packages/cfs_access-point.js
#Removed binary version of bcrypt because of security vulnerability that is not fixed yet.
#https://github.com/wekan/wekan/commit/4b2010213907c61b0e0482ab55abb06f6a668eac
#https://github.com/wekan/wekan/commit/7eeabf14be3c63fae2226e561ef8a0c1390c8d3c
#cd .build/bundle/programs/server/npm/node_modules/meteor/npm-bcrypt
#rm -rf node_modules/bcrypt
#meteor npm install --save bcrypt
# Change from npm-bcrypt directory back to .build/bundle/programs/server directory.
#cd ../../../../
# Change to directory .build/bundle/programs/server
cd .build/bundle/programs/server
npm install
npm install --allow-superuser --save babel-runtime
#meteor npm install --save bcrypt
# Change back to Wekan source directory
cd ../../../..
cp -r .build/bundle/* $SNAPCRAFT_PART_INSTALL/
cp .build/bundle/.node_version.txt $SNAPCRAFT_PART_INSTALL/
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/wekan
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/meteor/rajit_bootstrap3-datepicker/lib/bootstrap-datepicker/node_modules/phantomjs-prebuilt/lib/phantom/bin/phantomjs
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/npm/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
rm -f $SNAPCRAFT_PART_INSTALL/lib/node_modules/node-gyp/node_modules/tar/lib/.mkdir.js.swp
# Meteor 1.8.x additional .swp remove
rm -f $SNAPCRAFT_PART_INSTALL/programs/server/node_modules/node-pre-gyp/node_modules/tar/lib/.mkdir.js.swp
organize:
README: README.wekan
prime:
- -lib/node_modules/node-pre-gyp/node_modules/tar/lib/.unpack.js.swp
helpers:
source: snap-src
plugin: dump
caddy:
plugin: dump
source: https://caddyserver.com/download/linux/amd64?license=personal&telemetry=off
source-type: tar
organize:
caddy: bin/caddy
CHANGES.txt: CADDY_CHANGES.txt
EULA.txt: CADDY_EULA.txt
LICENSES.txt: CADDY_LICENSES.txt
README.txt: CADDY_README.txt
stage:
- -init
+853
View File
@@ -0,0 +1,853 @@
const DateString = Match.Where(function(dateAsString) {
check(dateAsString, String);
return moment(dateAsString, moment.ISO_8601).isValid();
});
export class WekanCreator {
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 Wekan id
// (so we only parse actions once!)
this.createdAt = {
board: null,
cards: {},
lists: {},
swimlanes: {},
};
// The object creator Wekan Id, indexed by the object Wekan id
// (so we only parse actions once!)
this.createdBy = {
cards: {}, // only cards have a field for that
};
// Map of labels Wekan ID => Wekan ID
this.labels = {};
// Map of swimlanes Wekan ID => Wekan ID
this.swimlanes = {};
// Map of lists Wekan ID => Wekan ID
this.lists = {};
// Map of cards Wekan ID => Wekan ID
this.cards = {};
// Map of comments Wekan ID => Wekan ID
this.commentIds = {};
// Map of attachments Wekan ID => Wekan ID
this.attachmentIds = {};
// Map of checklists Wekan ID => Wekan ID
this.checklists = {};
// Map of checklistItems Wekan ID => Wekan ID
this.checklistItems = {};
// The comments, indexed by Wekan card id (to map when importing cards)
this.comments = {};
// Map of rules Wekan ID => Wekan ID
this.rules = {};
// the members, indexed by Wekan member id => Wekan user ID
this.members = data.membersMapping ? data.membersMapping : {};
// Map of triggers Wekan ID => Wekan ID
this.triggers = {};
// Map of actions Wekan ID => Wekan ID
this.actions = {};
// maps a wekanCardId to an array of wekanAttachments
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 wekanUserId is provided and we have a mapping,
* return it.
* Otherwise return current logged user.
* @param wekanUserId
* @private
*/
_user(wekanUserId) {
if (wekanUserId && this.members[wekanUserId]) {
return this.members[wekanUserId];
}
return Meteor.userId();
}
checkActivities(wekanActivities) {
check(wekanActivities, [
Match.ObjectIncluding({
activityType: String,
createdAt: DateString,
}),
]);
// XXX we could perform more thorough checks based on action type
}
checkBoard(wekanBoard) {
check(
wekanBoard,
Match.ObjectIncluding({
archived: Boolean,
title: String,
// XXX refine control by validating 'color' against a list of
// allowed values (is it worth the maintenance?)
color: String,
permission: Match.Where(value => {
return ['private', 'public'].indexOf(value) >= 0;
}),
}),
);
}
checkCards(wekanCards) {
check(wekanCards, [
Match.ObjectIncluding({
archived: Boolean,
dateLastActivity: DateString,
labelIds: [String],
title: String,
sort: Number,
}),
]);
}
checkLabels(wekanLabels) {
check(wekanLabels, [
Match.ObjectIncluding({
// XXX refine control by validating 'color' against a list of allowed
// values (is it worth the maintenance?)
color: String,
}),
]);
}
checkLists(wekanLists) {
check(wekanLists, [
Match.ObjectIncluding({
archived: Boolean,
title: String,
}),
]);
}
checkSwimlanes(wekanSwimlanes) {
check(wekanSwimlanes, [
Match.ObjectIncluding({
archived: Boolean,
title: String,
}),
]);
}
checkChecklists(wekanChecklists) {
check(wekanChecklists, [
Match.ObjectIncluding({
cardId: String,
title: String,
}),
]);
}
checkChecklistItems(wekanChecklistItems) {
check(wekanChecklistItems, [
Match.ObjectIncluding({
cardId: String,
title: String,
}),
]);
}
checkRules(wekanRules) {
check(wekanRules, [
Match.ObjectIncluding({
triggerId: String,
actionId: String,
title: String,
}),
]);
}
checkTriggers(wekanTriggers) {
// XXX More check based on trigger type
check(wekanTriggers, [
Match.ObjectIncluding({
activityType: String,
desc: String,
}),
]);
}
getMembersToMap(data) {
// 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 = data.members;
const users = data.users;
// auto-map based on username
membersToMap.forEach(importedMember => {
importedMember.id = importedMember.userId;
delete importedMember.userId;
const user = users.filter(user => {
return user._id === importedMember.id;
})[0];
if (user.profile && user.profile.fullname) {
importedMember.fullName = user.profile.fullname;
}
importedMember.username = user.username;
const wekanUser = Users.findOne({ username: importedMember.username });
if (wekanUser) {
importedMember.wekanId = wekanUser._id;
}
});
return membersToMap;
}
checkActions(wekanActions) {
// XXX More check based on action type
check(wekanActions, [
Match.ObjectIncluding({
actionType: String,
desc: String,
}),
]);
}
// You must call parseActions before calling this one.
createBoardAndLabels(boardToImport) {
const boardToCreate = {
archived: boardToImport.archived,
color: boardToImport.color,
// very old boards won't have a creation activity so no creation date
createdAt: this._now(boardToImport.createdAt),
labels: [],
members: [
{
userId: Meteor.userId(),
wekanId: Meteor.userId(),
isActive: true,
isAdmin: true,
isNoComments: false,
isCommentOnly: false,
swimlaneId: false,
},
],
// Standalone Export has modifiedAt missing, adding modifiedAt to fix it
modifiedAt: this._now(boardToImport.modifiedAt),
permission: boardToImport.permission,
slug: getSlug(boardToImport.title) || 'board',
stars: 0,
title: boardToImport.title,
};
// now add other members
if (boardToImport.members) {
boardToImport.members.forEach(wekanMember => {
// do we already have it in our list?
if (
!boardToCreate.members.some(
member => member.wekanId === wekanMember.wekanId,
)
)
boardToCreate.members.push({
...wekanMember,
userId: wekanMember.wekanId,
});
});
}
boardToImport.labels.forEach(label => {
const labelToCreate = {
_id: Random.id(6),
color: label.color,
name: label.name,
};
// We need to remember them by Wekan 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: boardToImport.id,
system: 'Wekan',
},
// 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 Wekan cards,
* as well as all linked data: activities, comments, and attachments
* @param wekanCards
* @param boardId
* @returns {Array}
*/
createCards(wekanCards, boardId) {
const result = [];
wekanCards.forEach(card => {
const cardToCreate = {
archived: card.archived,
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.description,
listId: this.lists[card.listId],
swimlaneId: this.swimlanes[card.swimlaneId],
sort: card.sort,
title: card.title,
// we attribute the card to its creator if available
userId: this._user(this.createdBy.cards[card._id]),
isOvertime: card.isOvertime || false,
startAt: card.startAt ? this._now(card.startAt) : null,
dueAt: card.dueAt ? this._now(card.dueAt) : null,
spentTime: card.spentTime || null,
};
// add labels
if (card.labelIds) {
cardToCreate.labelIds = card.labelIds.map(wekanId => {
return this.labels[wekanId];
});
}
// add members {
if (card.members) {
const wekanMembers = [];
// we can't just map, as some members may not have been mapped
card.members.forEach(sourceMemberId => {
if (this.members[sourceMemberId]) {
const wekanId = this.members[sourceMemberId];
// we may map multiple Wekan 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;
}
}
// set color
if (card.color) {
cardToCreate.color = card.color;
}
// insert card
const cardId = Cards.direct.insert(cardToCreate);
// keep track of Wekan id => Wekan id
this.cards[card._id] = cardId;
// // log activity
// Activities.direct.insert({
// activityType: 'importCard',
// boardId,
// cardId,
// createdAt: this._now(),
// listId: cardToCreate.listId,
// source: {
// id: card._id,
// system: 'Wekan',
// },
// // 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.createdAt),
text: comment.text,
// we attribute the comment to the original author, default to current user
userId: this._user(comment.userId),
};
// dateLastActivity will be set from activity insert, no need to
// update it ourselves
const commentId = CardComments.direct.insert(commentToCreate);
this.commentIds[comment._id] = commentId;
// 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 wekanCoverId = card.coverId;
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.
const self = this;
if (Meteor.isServer) {
if (att.url) {
file.attachData(att.url, function(error) {
file.boardId = boardId;
file.cardId = cardId;
file.userId = self._user(att.userId);
// The field source will only be used to prevent adding
// attachments' related activities automatically
file.source = 'import';
if (error) {
throw error;
} else {
const wekanAtt = Attachments.insert(file, () => {
// we do nothing
});
self.attachmentIds[att._id] = wekanAtt._id;
//
if (wekanCoverId === att._id) {
Cards.direct.update(cardId, {
$set: {
coverId: wekanAtt._id,
},
});
}
}
});
} else if (att.file) {
file.attachData(
new Buffer(att.file, 'base64'),
{
type: att.type,
},
error => {
file.name(att.name);
file.boardId = boardId;
file.cardId = cardId;
file.userId = self._user(att.userId);
// The field source will only be used to prevent adding
// attachments' related activities automatically
file.source = 'import';
if (error) {
throw error;
} else {
const wekanAtt = Attachments.insert(file, () => {
// we do nothing
});
this.attachmentIds[att._id] = wekanAtt._id;
//
if (wekanCoverId === 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(wekanLabels, board) {
wekanLabels.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(wekanLists, boardId) {
wekanLists.forEach((list, listIndex) => {
const listToCreate = {
archived: list.archived,
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
// Wekan boards (eg from 2013) that didn't log the 'createList' action
// we require.
createdAt: this._now(this.createdAt.lists[list.id]),
title: list.title,
sort: list.sort ? list.sort : listIndex,
};
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: 'Wekan',
// },
// // We attribute the import to current user,
// // not the creator of the original object
// userId: this._user(),
// });
});
}
createSwimlanes(wekanSwimlanes, boardId) {
wekanSwimlanes.forEach((swimlane, swimlaneIndex) => {
const swimlaneToCreate = {
archived: swimlane.archived,
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
// Wekan boards (eg from 2013) that didn't log the 'createList' action
// we require.
createdAt: this._now(this.createdAt.swimlanes[swimlane._id]),
title: swimlane.title,
sort: swimlane.sort ? swimlane.sort : swimlaneIndex,
};
// set color
if (swimlane.color) {
swimlaneToCreate.color = swimlane.color;
}
const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
Swimlanes.direct.update(swimlaneId, {
$set: {
updatedAt: this._now(),
},
});
this.swimlanes[swimlane._id] = swimlaneId;
});
}
createChecklists(wekanChecklists) {
const result = [];
wekanChecklists.forEach((checklist, checklistIndex) => {
// Create the checklist
const checklistToCreate = {
cardId: this.cards[checklist.cardId],
title: checklist.title,
createdAt: checklist.createdAt,
sort: checklist.sort ? checklist.sort : checklistIndex,
};
const checklistId = Checklists.direct.insert(checklistToCreate);
this.checklists[checklist._id] = checklistId;
result.push(checklistId);
});
return result;
}
createTriggers(wekanTriggers, boardId) {
wekanTriggers.forEach(trigger => {
if (trigger.hasOwnProperty('labelId')) {
trigger.labelId = this.labels[trigger.labelId];
}
if (trigger.hasOwnProperty('memberId')) {
trigger.memberId = this.members[trigger.memberId];
}
trigger.boardId = boardId;
const oldId = trigger._id;
delete trigger._id;
this.triggers[oldId] = Triggers.direct.insert(trigger);
});
}
createActions(wekanActions, boardId) {
wekanActions.forEach(action => {
if (action.hasOwnProperty('labelId')) {
action.labelId = this.labels[action.labelId];
}
if (action.hasOwnProperty('memberId')) {
action.memberId = this.members[action.memberId];
}
action.boardId = boardId;
const oldId = action._id;
delete action._id;
this.actions[oldId] = Actions.direct.insert(action);
});
}
createRules(wekanRules, boardId) {
wekanRules.forEach(rule => {
// Create the rule
rule.boardId = boardId;
rule.triggerId = this.triggers[rule.triggerId];
rule.actionId = this.actions[rule.actionId];
delete rule._id;
Rules.direct.insert(rule);
});
}
createChecklistItems(wekanChecklistItems) {
wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => {
// Create the checklistItem
const checklistItemTocreate = {
title: checklistitem.title,
checklistId: this.checklists[checklistitem.checklistId],
cardId: this.cards[checklistitem.cardId],
sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex,
isFinished: checklistitem.isFinished,
};
const checklistItemId = ChecklistItems.direct.insert(
checklistItemTocreate,
);
this.checklistItems[checklistitem._id] = checklistItemId;
});
}
parseActivities(wekanBoard) {
wekanBoard.activities.forEach(activity => {
switch (activity.activityType) {
case 'addAttachment': {
// We have to be cautious, because the attachment could have been removed later.
// In that case Wekan still reports its addition, but removes its 'url' field.
// So we test for that
const wekanAttachment = wekanBoard.attachments.filter(attachment => {
return attachment._id === activity.attachmentId;
})[0];
if (typeof wekanAttachment !== 'undefined' && wekanAttachment) {
if (wekanAttachment.url || wekanAttachment.file) {
// 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 wekanCardId = activity.cardId;
if (!this.attachments[wekanCardId]) {
this.attachments[wekanCardId] = [];
}
this.attachments[wekanCardId].push(wekanAttachment);
}
}
break;
}
case 'addComment': {
const wekanComment = wekanBoard.comments.filter(comment => {
return comment._id === activity.commentId;
})[0];
const id = activity.cardId;
if (!this.comments[id]) {
this.comments[id] = [];
}
this.comments[id].push(wekanComment);
break;
}
case 'createBoard': {
this.createdAt.board = activity.createdAt;
break;
}
case 'createCard': {
const cardId = activity.cardId;
this.createdAt.cards[cardId] = activity.createdAt;
this.createdBy.cards[cardId] = activity.userId;
break;
}
case 'createList': {
const listId = activity.listId;
this.createdAt.lists[listId] = activity.createdAt;
break;
}
case 'createSwimlane': {
const swimlaneId = activity.swimlaneId;
this.createdAt.swimlanes[swimlaneId] = activity.createdAt;
break;
}
}
});
}
importActivities(activities, boardId) {
activities.forEach(activity => {
switch (activity.activityType) {
// Board related activities
// TODO: addBoardMember, removeBoardMember
case 'createBoard': {
Activities.direct.insert({
userId: this._user(activity.userId),
type: 'board',
activityTypeId: boardId,
activityType: activity.activityType,
boardId,
createdAt: this._now(activity.createdAt),
});
break;
}
// List related activities
// TODO: removeList, archivedList
case 'createList': {
Activities.direct.insert({
userId: this._user(activity.userId),
type: 'list',
activityType: activity.activityType,
listId: this.lists[activity.listId],
boardId,
createdAt: this._now(activity.createdAt),
});
break;
}
// Card related activities
// TODO: archivedCard, restoredCard, joinMember, unjoinMember
case 'createCard': {
Activities.direct.insert({
userId: this._user(activity.userId),
activityType: activity.activityType,
listId: this.lists[activity.listId],
cardId: this.cards[activity.cardId],
boardId,
createdAt: this._now(activity.createdAt),
});
break;
}
case 'moveCard': {
Activities.direct.insert({
userId: this._user(activity.userId),
oldListId: this.lists[activity.oldListId],
activityType: activity.activityType,
listId: this.lists[activity.listId],
cardId: this.cards[activity.cardId],
boardId,
createdAt: this._now(activity.createdAt),
});
break;
}
// Comment related activities
case 'addComment': {
Activities.direct.insert({
userId: this._user(activity.userId),
activityType: activity.activityType,
cardId: this.cards[activity.cardId],
commentId: this.commentIds[activity.commentId],
boardId,
createdAt: this._now(activity.createdAt),
});
break;
}
// Attachment related activities
case 'addAttachment': {
Activities.direct.insert({
userId: this._user(activity.userId),
type: 'card',
activityType: activity.activityType,
attachmentId: this.attachmentIds[activity.attachmentId],
cardId: this.cards[activity.cardId],
boardId,
createdAt: this._now(activity.createdAt),
});
break;
}
// Checklist related activities
case 'addChecklist': {
Activities.direct.insert({
userId: this._user(activity.userId),
activityType: activity.activityType,
cardId: this.cards[activity.cardId],
checklistId: this.checklists[activity.checklistId],
boardId,
createdAt: this._now(activity.createdAt),
});
break;
}
case 'addChecklistItem': {
Activities.direct.insert({
userId: this._user(activity.userId),
activityType: activity.activityType,
cardId: this.cards[activity.cardId],
checklistId: this.checklists[activity.checklistId],
checklistItemId: activity.checklistItemId.replace(
activity.checklistId,
this.checklists[activity.checklistId],
),
boardId,
createdAt: this._now(activity.createdAt),
});
break;
}
}
});
}
//check(board) {
check() {
//try {
// check(data, {
// membersMapping: Match.Optional(Object),
// });
// this.checkActivities(board.activities);
// this.checkBoard(board);
// this.checkLabels(board.labels);
// this.checkLists(board.lists);
// this.checkSwimlanes(board.swimlanes);
// this.checkCards(board.cards);
//this.checkChecklists(board.checklists);
// this.checkRules(board.rules);
// this.checkActions(board.actions);
//this.checkTriggers(board.triggers);
//this.checkChecklistItems(board.checklistItems);
//} catch (e) {
// throw new Meteor.Error('error-json-schema');
// }
}
create(board, currentBoardId) {
// TODO : Make isSandstorm variable global
const isSandstorm =
Meteor.settings &&
Meteor.settings.public &&
Meteor.settings.public.sandstorm;
if (isSandstorm && currentBoardId) {
const currentBoard = Boards.findOne(currentBoardId);
currentBoard.archive();
}
this.parseActivities(board);
const boardId = this.createBoardAndLabels(board);
this.createLists(board.lists, boardId);
this.createSwimlanes(board.swimlanes, boardId);
this.createCards(board.cards, boardId);
this.createChecklists(board.checklists);
this.createChecklistItems(board.checklistItems);
this.importActivities(board.activities, boardId);
this.createTriggers(board.triggers, boardId);
this.createActions(board.actions, boardId);
this.createRules(board.rules, boardId);
// XXX add members
return boardId;
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
dist: disco
dist: eoan
sudo: required
env:
TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
TRAVIS_NODE_VERSION: 8.17.0
TRAVIS_NODE_VERSION: 12.15.0
TRAVIS_NPM_VERSION: latest
before_install:
+452
View File
@@ -1,3 +1,455 @@
# v3.86 2020-03-24 Wekan release
This release fixes the following bugs:
- [Fix Rich editor can not be disabled, regression from changes yesterday at Wekan v3.85](https://github.com/wekan/wekan/commit/12ab8fac5db9c5ac8069d0ca2bca340d6004a25b).
Thanks to uusijani, vjrj and xet7.
- [1) Fix Pasting text into a card is adding a line before and after
(and multiplies by pasting more) by changing paste "p" to "br".
2) Fixes to summernote and markdown comment editors, related
to keeping them open when adding comments, having
@member mention not close card, and disabling clicking of
@member mention](https://github.com/wekan/wekan/commit/b9099a8b7ea6f63c79bdcbb871cb993b2cb7e325).
Thanks to xet7 !
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.85 2020-03-23 Wekan release
This release fixes the following CRITICAL SECURITY VULNERABILITIES:
- [Fix XSS bug reported today 4 hours ago by Cyb3rjunky](https://github.com/wekan/wekan/commit/482682e50079d70c5113169020d6834013b57c11).
Logged in users could run javascript in input fields.
This affects Wekan versions v3.12-v3.84.
In [Wekan v3.12](https://github.com/wekan/wekan/blob/master/CHANGELOG.md#v312-2019-08-09-wekan-release)
there was [changes for XSS filter to allow inserting images, videos etc
on comment WYSIWYG editor](https://github.com/wekan/wekan/pull/2593)
so features related to that are now removed.
After this fix, Javascript in input fields is not executed.
Thanks to Cyb3rjunky and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.84 2020-03-16 Wekan release
This release adds the following features:
- Add settings for mouse wheel scroll inertia and scroll
amount [Part1](https://github.com/wekan/wekan/commit/9d13001b903f9ec50f5fa3a4bdbacae32b27ac65)
and [Part2](https://github.com/wekan/wekan/commit/aaecac091209e90c0c2123830728f5e7a835ccb4).
For example: sudo snap set wekan scrollinertia='200' , sudo snap set wekan scrollamount='200' .
Thanks to danger89 and xet7.
and adds the following updates:
- [Upgrade to Meteor 1.10.1](https://github.com/wekan/wekan/commit/e16c65babc1f021c35a3d46bc61e649ec94d1e82).
Thanks to xet7.
- [Update markdown](https://github.com/wekan/wekan/commit/6e0fa78022ea487176eb0a32ec5a4a441f8e0c3c).
Thanks to xet7.
- [Update minimist](https://github.com/wekan/wekan/commit/ea6baa5c2b956ee28b0a7e63f988e2fc1998201a).
Thanks to xet7.
- [Update acorn](https://github.com/wekan/wekan/commit/369a29707bbec3bf89717c16e8b698fb4666087a).
Thanks to xet7.
- [Update prettier-eslint](https://github.com/wekan/wekan/commit/8183b7bdaa01d2ce53ac7215beafd5efe21373e8).
Thanks to xet7.
- [Update ostrio:cookies](https://github.com/wekan/wekan/commit/14b8610837117616d436e2bac6a9dc653e315662).
Thanks to xet7.
- [Add build time profiling to build script](https://github.com/wekan/wekan/commit/f968109e7390139e50375ee29bc7bc3cf1e1ab41).
Thanks to zodern.
and fixes the following bugs:
- [Downgrade stylus to v1.1.0 to speed up building Wekan](https://github.com/wekan/wekan/commit/fca4cdcebf1cc6642aefeb78b911cb5b95ebe473).
This is because building newer stylus v2 takes 52 minutes. After this change, building Wekan takes 3 minutes.
Thanks to zodern.
- [Fix: Error when retrieve token from some OIDC due to not necessary scope
parameter](https://github.com/wekan/wekan/pull/2955).
Thanks to benoitm76.
- [Fix: img tag did not allow width and height. Removed swipebox from markdown editor
img tag and updated marked markdown to newest version](https://github.com/wekan/wekan/commit/2b26bbe78a1a2b8b427963a6c44c3853efdb737e).
Thanks to hradec and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.83 2020-03-01 Wekan release
This release tries to revert remaining the following changes:
- [Revert](https://github.com/wekan/wekan/88573ad2cdb8596b795a82ef40a0662180e8a7d7) change made at Wekan v3.81,
because building did not work: [Try to make Meteor build time shorter
by excluding legacy and cordova. This was made possible by
Meteor 1.10-rc.2](https://github.com/wekan/wekan/commit/0d3002f69d97e646fa7368bfdade4f78c51e9884).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.82 2020-03-01 Wekan release
This release reverts the following changes:
- Revert change made at Wekan v3.81, because building did not work: [Try to make Meteor build time shorter
by excluding legacy and cordova. This was made possible by
Meteor 1.10-rc.2](https://github.com/wekan/wekan/commit/0d3002f69d97e646fa7368bfdade4f78c51e9884).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.81 2020-03-01 Wekan release
This release [fixes](https://github.com/wekan/wekan/commit/aac7c380c8c389b0683b2bd64e2cc856993f0e30) the following CRITICAL SECURITY VULNERABILITIES and other bugs:
- Fix critical and moderate security vulnerabilities reported at 2020-02-26 with
responsible disclosure by [Dejan Zelic](https://twitter.com/dejandayoff),
Justin Benjamin and others at [Offensive Security](https://twitter.com/offsectraining),
that follow standard 90 days before public disclosure.
Thanks to xet7.
- Fix webhook error that prevented some card etc deleting from web UI of board.
Thanks to xet7.
- Add missing Font Awesome icon to Board Settings Menu.
Thanks to xet7.
- Remove autofocus from many form input boxes so that they would not cause warnings.
Thanks to xet7.
and does the following upgrades:
- [Upgrade Meteor to 1.10-rc.2](https://github.com/wekan/wekan/commit/26b521e86e6ac40b7ba25bbe8dac7bf4d48d43ce).
Thanks to xet7.
- [Try to make Meteor build time shorter by excluding legacy and cordova. This was made possible by
Meteor 1.10-rc.2](https://github.com/wekan/wekan/commit/0d3002f69d97e646fa7368bfdade4f78c51e9884).
Thanks to xet7.
and fixes the following bugs:
- [Try to fix afterwards loading of cards by adding fallback when requestIdleCallback is not
available](https://github.com/wekan/wekan/commit/2b9540ce02de604bf84ea082f2dcb1d01673708c).
Thanks to xet7.
- [Make profile.initials available in publications](https://github.com/wekan/wekan/pull/2948).
Thanks to NicoP-S.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.80 2020-02-22 Wekan release
This release adds the following features:
- [Create New User in Admin Panel](https://github.com/wekan/wekan/commit/e0ca960a35cf006880019ba28fc82aa30f289a71).
Works, but does not save fullname yet, so currently it's needed to edit add fullname later.
Thanks to xet7.
and adds the following updates:
- [Update to Meteor 1.9.1, Node 12.16.1 etc newest dependencies](https://github.com/wekan/wekan/commit/cbbb5deff7d84a91c40becc9caaf70f5b6738b63).
Thanks to xet7.
- [Update to Meteor 1.9.2](https://github.com/wekan/wekan/commit/9be3f3714ae680ff9fc1855c960c9831e84c2b07).
Thanks to xet7.
and fixes the following bugs:
- [Update Sandstorm release build script](https://github.com/wekan/wekan/commit/a4ff6cc0af8545ca4d3e97fa2cabbe7981c025b2).
Thanks to xet7.
- [Fix docker-compose link](https://github.com/wekan/wekan/pull/2937).
Thanks to pbek.
- [Remove alethes:pages package, that had some indentation error.
Package is about pagination, but I did not find any pagination related code in Wekan
yet](https://github.com/wekan/wekan/commit/ec012060305bc16fbf8d2ac218f5c847e02c4301).
Thanks to xet7 !
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.79 2020-02-13 Wekan release
This release fixes the following bugs:
- [Fix Card Opened Webhook can not be disabled](https://github.com/wekan/wekan/commit/178f376e2138b5522c2e92ddfd2babb113df8d9f).
Thanks to mvanvoorden and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.78 2020-02-12 Wekan release
This release adds the following features:
- [Card Settings / Show on Card: Description Title and Description Text](https://github.com/wekan/wekan/commit/e89965f6422fd95b4ad2112ae407b1dde4853510).
Thanks to e-stoniauk, 2020product and xet7.
and fixes the following bugs:
- [Remove card element grouping to create compact card layout](https://github.com/wekan/wekan/commit/e89965f6422fd95b4ad2112ae407b1dde4853510).
Thanks to e-stoniauk, 2020product and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.77 2020-02-10 Wekan release
This release removes the following features:
- [Remove hiding comments and activities](https://github.com/wekan/wekan/commit/2a54218f3f68547032bd53a04a968b233be21e15).
Thanks to xet7.
and fixes the following bugs:
- Fix Copy Card Link to Clipboard button at card title did not
work [Part 1](https://github.com/wekan/wekan/commit/9a21b0a1c933e7f778e4e57a8258e150ccea1620)
and [Part2](https://github.com/wekan/wekan/commit/4467a68b97a3fbf0fbae7f05177d978f2aa80287).
Thanks to 2020product and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.76 2020-02-07 Wekan release
This release adds the following updates:
- [Use Meteor 1.9 and Node.js 12.15.0 on Snap and Docker](https://github.com/wekan/wekan/commit/8384d68a060ef8f2c202ce2fa6064c5c823d28dc).
This also fixes bug that exporting some boards was not possible, downloading export file failed.
Thanks to xet7.
and fixes the following bugs:
- [Fix Bug enable/disable Comments in Card Settings](https://github.com/wekan/wekan/issues/2923).
Thanks to warnt, mdurokov and xet7.
- [Try to disable dragging Swimlanes/Lists/Cards/Checklists/Subtasks on small mobile smartphones webbrowsers,
and hide drag handles on mobile web](https://github.com/wekan/wekan/commit/bf78b093bad7d463ee325ad96e8b485264d4a3be).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.75 2020-02-05 Wekan release
This release adds the following new features:
- [Fix](https://github.com/wekan/wekan/commit/f22785dbcde42e425c9ead209ec224aef6e11c16)
[adding comments](https://github.com/wekan/wekan/issues/2918).
Thanks to xet7.
and fixes the following bugs:
- [Added some working layout changes like activities using less space from https://github.com/wekan/wekan/pull/2920](https://github.com/wekan/wekan/commit/f22785dbcde42e425c9ead209ec224aef6e11c16).
Thanks to 2020product.
- [Fixed Card Settings not working at Sandstorm](https://github.com/wekan/wekan/commit/f22785dbcde42e425c9ead209ec224aef6e11c16).
Thanks to xet7.
- Add [Card Description title](https://github.com/wekan/wekan/issues/2918#issuecomment-582346577)
[back](https://github.com/wekan/wekan/commit/f22785dbcde42e425c9ead209ec224aef6e11c16).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.74 2020-02-05 Wekan release
This release adds the following new features:
- [For BoardAdmin, add way to hide parts of a card, at Board Settings/Card Settings/Show on Card: Received, Start, ... etc.
Add to card title bar Copy card to Clipboard button](https://github.com/wekan/wekan/pull/2915).
Thanks to 2020product and xet7.
- [Set default to RICHER_CARD_COMMENT_EDITOR=false](https://github.com/wekan/wekan/commit/65fa2f626f503b8089e0d982901cffb3990426cb).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.73 2020-01-29 Wekan release
This release adds the following new features:
- [Login to Wekan with Nextcloud](https://github.com/wekan/wekan/pull/2897).
Thanks to bogie.
- [Add rule action to move cards to other boards](https://github.com/wekan/wekan/pull/2899).
Thanks to peterverraedt.
and fixes the following bugs:
- [Show System Wide Announcement in one line](https://github.com/wekan/wekan/pull/2891).
Thanks to tsia.
- [Fixed board export with attachment in Wekan Meteor 1.9.x version](https://github.com/wekan/wekan/pull/2898).
Thanks to izadpoor.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.72 2020-01-19 Sandstorm-only Wekan release
This release fixes the following bugs:
- Try to fix Wekan at Sandstorm.
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.71 2020-01-18 Sandstorm-only Wekan release
This release fixes the following bugs:
- [Try to fix Wekan at Sandstorm by using Meteor 1.8.x and Node 8.17.0 at Sandstorm](https://github.com/wekan/wekan/commit/5e5ab95410c715a4379631456fc5547c497898b0).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.70 2020-01-18 Wekan release
This release fixes the following bugs:
- [Add missing LD_LIBRARY_PATH to use libssl and libcurl](https://github.com/wekan/wekan/10f142a1a05acb98a175ccb0326fb0c1d3e3713f).
Thanks to xet7.
- [Use Meteor 1.8.x](https://github.com/wekan/wekan/commit/55a2aa90cbbf44200e9b0b9f4bd08b6177f1bb95)
[on Snap](https://github.com/wekan/wekan/commit/6a01170d8696322462c4065ce0cf4a637a058975), because
Snap builds do not work yet for Meteor 1.9, Node 12.14.1 and MongoDB 4.2.2.
Docker version works with Meteor 1.9.
Thanks to xet7.
- [Try to fix Node 12 Buffer() deprecation errors](https://github.com/wekan/wekan/commit/9b905c2833d54cf34d1875148075b2bf756d943a).
Thanks to xet7.
- [Add Snap Meteor 1.8.x files to lint ignore files](https://github.com/wekan/wekan/commit/48f8050c25e40f737dfdd3a98923cb87cd4e77e2).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.69 2020-01-10 Wekan release
This release fixes the following bugs:
- [Fix docker-compose.yml to not use --smallfiles that is not supported in
MongoDB 4.x](https://github.com/wekan/wekan/commit/ecb76842fcbd81701afcab8db0ed106e6be0fdec).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.68 2020-01-10 Wekan release
This release tries to fix the following bugs:
- [Try to fix Snap by removing MongoDB option --smallfiles that is not supported
in MongoDB 4.x](https://github.com/wekan/wekan/commit/031df54a2e0a03dcb7a2586667e60e5bd4eef706)
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.67 2020-01-10 Wekan release
This release tries to fix the following bugs:
- [Try to fix Snap](https://github.com/wekan/wekan/commit/2b382b940be9af575fab4c2e955702d8cde55ae9).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.66 2020-01-10 Wekan release
This release tries to fix the following bugs:
- [Try to fix Snap](https://github.com/wekan/wekan/commit/39bf1e375e2962f824e6f8cfa425ea51aa4efa24).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.65 2020-01-10 Wekan release
This release adds the following features:
- [More keyboard shortcuts: c for archive card](https://github.com/wekan/wekan/commit/d16a601c04aeb1d3550c5c541be02a67276a34cf).
Thanks to xet7.
and adds the following updates:
- [Upgrade to Meteor 1.9, Node 12.14.1 and MongoDB 4.2.2](https://github.com/wekan/wekan/commit/785f3cf88b61f687ef5ad4a529768221d1a54c86).
Thanks to xet7.
- [Add more issue repo links to GitHub issue template](https://github.com/wekan/wekan/commit/5724674e73246f4e52843a6d6906c0ecdd85cccc).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.64 2020-01-06 Wekan release
This release adds the following warning for CentOS 7 users:
- [WARNING: DO NOT USE SNAP ON CENTOS 7, THERE IS UPDATE BUG](https://github.com/wekan/wekan-snap/wiki/CentOS-7).
Thanks to andy-twosticks and xet7.
and adds the following features:
- [Wider sidebar](https://github.com/wekan/wekan/commit/5058233509e44916296e38fb8a6c5dd591c46d8b).
Thanks to vjrj.
and removes the following features:
- [Removed Custom HTML feature that does not work](https://github.com/wekan/wekan/commit/ddce0ada094e6450be260b4cda21fdfa09ae0133).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.63 2020-01-06 Wekan release
This release fixes the following bugs:
- [Fix: Unable to find Archive Card/List/Swimlane in board
settings](https://github.com/wekan/wekan/commit/8ce993921718f3e10c2daa5fabb145b939d789dd).
Thanks to neobradley and xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.62 2020-01-05 Wekan release
This release adds the following features:
- [Add Worker role](https://github.com/wekan/wekan/issues/2788).
This was originally added at Wekan v3.58, reverted at Wekan v3.60 because of bugs,
and now after fixes added back.
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.61 2020-01-03 Wekan release
This release adds the following features:
- [Add more Font Awesome icons. This was originally added
at Wekan v3.58, removed at Wekan v3.60, and now
added back at Wekan v3.61](https://github.com/wekan/wekan/commit/cd253522a305523e3e36bb73313e8c4db500a717).
Thanks to xet7.
and fixes the following bugs:
- [Fix browser javascript console errors when editing profile. This was originally added
at Wekan v3.58, removed at Wekan v3.60, and now added back at
Wekan v3.61](https://github.com/wekan/wekan/commit/cd253522a305523e3e36bb73313e8c4db500a717).
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.60 2020-01-03 Wekan release
This release fixes the following bugs:
- [Revert to Wekan v3.57 version of client and models directories,
removing Worker role temporarily, because Worker role changes
broke saving card](https://github.com/wekan/wekan/commit/27943796ade78ca3c503637a1340918bf06a1267).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.59 2020-01-03 Wekan release
This release fixes the following bugs:
- [Fix not being able to edit received date](https://github.com/wekan/wekan/commit/5376bc7b7905c0dd99fae1aeae3f63b4583a3e3f).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.58 2020-01-03 Wekan release
This release adds the following features:
- [Add Worker role](https://github.com/wekan/wekan/issues/2788). Thanks to xet7.
- [Add more Font Awesome icons](https://github.com/wekan/wekan/commit/2bf004120d5a43cd3c3c060fc7c0c30d1b01f220).
Thanks to xet7.
and fixes the following bugs:
- [Fix: k8s templates update for helm](https://github.com/wekan/wekan/pull/2867).
1. Upgrade mongo replica version.
2. Access mongo via service url.
3. Change the expose servicePort to numeric.
Thanks to jiangytcn.
- [Fix browser console errors when editing user profile name](https://github.com/wekan/wekan/commit/2bf004120d5a43cd3c3c060fc7c0c30d1b01f220).
Thanks to xet7.
Thanks to above GitHub users for their contributions and translators for their translations.
# v3.57 2019-12-22 Wekan release
This release adds the following features:
+6 -4
View File
@@ -6,8 +6,8 @@ LABEL maintainer="wekan"
# ENV BUILD_DEPS="paxctl"
ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-essential git ca-certificates python3" \
DEBUG=false \
NODE_VERSION=v8.17.0 \
METEOR_RELEASE=1.8.1 \
NODE_VERSION=v12.16.1 \
METEOR_RELEASE=1.10-rc.2 \
USE_EDGE=false \
METEOR_EDGE=1.5-beta.17 \
NPM_VERSION=latest \
@@ -21,7 +21,7 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURES_BERORE=3 \
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_LOCKOUT_PERIOD=60 \
ACCOUNTS_LOCKOUT_UNKNOWN_USERS_FAILURE_WINDOW=15 \
RICHER_CARD_COMMENT_EDITOR=true \
RICHER_CARD_COMMENT_EDITOR=false \
CARD_OPENED_WEBHOOK_ENABLED=false \
ATTACHMENTS_STORE_PATH="" \
MAX_IMAGE_PIXEL="" \
@@ -110,7 +110,9 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
CORS="" \
CORS_ALLOW_HEADERS="" \
CORS_EXPOSE_HEADERS="" \
DEFAULT_AUTHENTICATION_METHOD=""
DEFAULT_AUTHENTICATION_METHOD="" \
SCROLLINERTIA="0" \
SCROLLAMOUNT="auto"
# Copy the app to the image
COPY ${SRC_PATH} /home/wekan/app
+1 -1
View File
@@ -61,7 +61,7 @@ that by providing one-click installation on various platforms.
[Mac](https://github.com/wekan/wekan/wiki/Mac) / [Windows](https://github.com/wekan/wekan/wiki/Install-Wekan-from-source-on-Windows).
[More Platforms](https://github.com/wekan/wekan/wiki/Platforms), bundle for RasPi3 ARM and other CPUs where Node.js and MongoDB exists.
- 1 GB RAM minimum free for Wekan. Production server should have minimum total 4 GB RAM.
For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/devel/docker-compose.yml): 3 frontend servers,
For thousands of users, for example with [Docker](https://github.com/wekan/wekan/blob/master/docker-compose.yml): 3 frontend servers,
each having 2 CPU and 2 wekan-app containers. One backend wekan-db server with many CPUs.
- Enough disk space and alerts about low disk space. If you run out disk space, MongoDB database gets corrupted.
- SECURITY: Updating to newest Wekan version very often. Please check you do not have automatic updates of Sandstorm or Snap turned off.
+1 -1
View File
@@ -1,5 +1,5 @@
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
appVersion: "v3.57.0"
appVersion: "v3.86.0"
files:
userUploads:
- README.md
+3 -3
View File
@@ -201,20 +201,20 @@ template(name="cardActivities")
.activity-checklist(href="{{ card.absoluteUrl }}")
+viewer
= checklistItem.title
if(currentData.timeKey)
| {{{_ activityType }}}
= ' '
i(title=currentData.timeValue).activity-meta {{ moment currentData.timeValue 'LLL' }}
if (currentData.timeOldValue)
= ' '
= ' '
| {{{_ "previous_as" }}}
= ' '
i(title=currentData.timeOldValue).activity-meta {{ moment currentData.timeOldValue 'LLL' }}
= ' @'
else if(currentData.timeValue)
| {{{_ activityType currentData.timeValue}}}
if($eq activityType 'deleteComment')
| {{{_ 'activity-deleteComment' currentData.commentId}}}.
+1 -1
View File
@@ -9,7 +9,7 @@
clear: both
.activity
margin: 10px 0
margin: 0.5px 0
display: flex
.member
+7
View File
@@ -33,6 +33,13 @@ BlazeComponent.extendComponent({
cardId,
});
resetCommentInput(input);
// With Richer editor is in use, and comment is submitted,
// clear comment form with JQuery. Id #summernote is defined
// at client/components/main/editor.jade where it previously was
// id=id, now it is id="summernote".
if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === 'true') {
$('#summernote').summernote('code', '');
}
Tracker.flush();
autosize.update(input);
input.trigger('submitted');
@@ -46,3 +46,23 @@
&:is-open
cursor: auto
.comment-item
background-color: #fff
border: 0
box-shadow: 0 1px 2px rgba(0, 0, 0, .23)
color: #8c8c8c
height: 36px
margin: 4px 4px 6px 0
width: 92%
&:hover
background: darken(white, 12%)
&.add-comment
display: flex
margin: 5px
a
display: block
margin: auto
+4 -6
View File
@@ -205,21 +205,19 @@ BlazeComponent.extendComponent({
} else {
showDesktopDragHandles = false;
}
if (
Utils.isMiniScreen() ||
(!Utils.isMiniScreen() && showDesktopDragHandles)
) {
if (!Utils.isMiniScreen() && showDesktopDragHandles) {
$swimlanesDom.sortable({
handle: '.js-swimlane-header-handle',
});
} else {
} else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
$swimlanesDom.sortable({
handle: '.swimlane-header',
});
}
// Disable drag-dropping if the current user is not a board member or is comment only
// Disable drag-dropping if the current user is not a board member or is miniscreen
$swimlanesDom.sortable('option', 'disabled', !userIsMember());
$swimlanesDom.sortable('option', 'disabled', Utils.isMiniScreen());
});
function userIsMember() {
+1
View File
@@ -30,6 +30,7 @@ Template.boardMenuPopup.events({
'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
'click .js-card-settings': Popup.open('boardCardSettings'),
});
Template.boardMenuPopup.helpers({
+16 -12
View File
@@ -38,18 +38,22 @@ template(name="attachmentsGalery")
| {{_ 'download'}}
if currentUser.isBoardMember
unless currentUser.isCommentOnly
if isImage
a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}")
i.fa.fa-thumb-tack
if($eq ../coverId _id)
| {{_ 'remove-cover'}}
else
| {{_ 'add-cover'}}
a.js-confirm-delete
i.fa.fa-close
| {{_ 'delete'}}
unless currentUser.isWorker
if isImage
a(class="{{#if $eq ../coverId _id}}js-remove-cover{{else}}js-add-cover{{/if}}")
i.fa.fa-thumb-tack
if($eq ../coverId _id)
| {{_ 'remove-cover'}}
else
| {{_ 'add-cover'}}
a.js-confirm-delete
i.fa.fa-close
| {{_ 'delete'}}
if currentUser.isBoardMember
unless currentUser.isCommentOnly
li.attachment-item.add-attachment
a.js-add-attachment {{_ 'add-attachment' }}
unless currentUser.isWorker
//li.attachment-item.add-attachment
a.js-add-attachment
i.fa.fa-paperclip
| {{_ 'add-attachment' }}
+2 -1
View File
@@ -97,7 +97,8 @@ Template.dateBadge.helpers({
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
});
+290 -168
View File
@@ -8,16 +8,23 @@ template(name="cardDetails")
a.fa.fa-times-thin.close-card-details.js-close-card-details
if currentUser.isBoardMember
a.fa.fa-navicon.card-details-menu.js-open-card-details-menu
input.inline-input(type="text" id="cardURL_copy" value="{{ absoluteUrl }}")
a.fa.fa-link.card-copy-button.js-copy-link(
class="fa-link"
title="{{_ 'copy-card-link-to-clipboard'}}"
value="{{ absoluteUrl }}"
)
if isMiniScreen
a.fa.fa-times-thin.close-card-details-mobile-web.js-close-card-details
if currentUser.isBoardMember
a.fa.fa-navicon.card-details-menu-mobile-web.js-open-card-details-menu
a.fa.fa-link.card-copy-mobile-button
h2.card-details-title.js-card-title(
class="{{#if canModifyCard}}js-open-inlined-form is-editable{{/if}}")
+viewer
= getTitle
if isWatching
i.fa.fa-eye.card-details-watch
if isWatching
i.card-details-watch.fa.fa-eye
.card-details-path
each parentList
| &nbsp; &gt; &nbsp;
@@ -36,70 +43,105 @@ template(name="cardDetails")
p.warning {{_ 'card-archived'}}
.card-details-items
.card-details-item.card-details-item-received
h3.card-details-item-title {{_ 'card-received'}}
if getReceived
+cardReceivedDate
else
if currentBoard.allowsReceivedDate
.card-details-item.card-details-item-received
h3
i.fa.fa-sign-out
card-details-item-title {{_ 'card-received'}}
if getReceived
+cardReceivedDate
else
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-received-date
i.fa.fa-plus
if currentBoard.allowsStartDate
.card-details-item.card-details-item-start
h3
i.fa.fa-hourglass-start
card-details-item-title {{_ 'card-start'}}
if getStart
+cardStartDate
else
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-start-date
i.fa.fa-plus
if currentBoard.allowsDueDate
.card-details-item.card-details-item-due
h3
i.fa.fa-sign-in
card-details-item-title {{_ 'card-due'}}
if getDue
+cardDueDate
else
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-due-date
i.fa.fa-plus
if currentBoard.allowsEndDate
.card-details-item.card-details-item-end
h3
i.fa.fa-hourglass-end
card-details-item-title {{_ 'card-end'}}
if getEnd
+cardEndDate
else
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-end-date
i.fa.fa-plus
//.card-details-items
if currentBoard.allowsMembers
.card-details-item.card-details-item-members
h3
i.fa.fa-users
card-details-item-title {{_ 'members'}}
each getMembers
+userAvatar(userId=this cardId=../_id)
| {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard
a.js-received-date {{_ 'add'}}
unless currentUser.isWorker
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
i.fa.fa-plus
.card-details-item.card-details-item-start
h3.card-details-item-title {{_ 'card-start'}}
if getStart
+cardStartDate
else
//if assigneeSelected
if currentBoard.allowsAssignee
.card-details-item.card-details-item-assignees
h3
i.fa.fa-user
card-details-item-title {{_ 'assignee'}}
each getAssignees
+userAvatarAssignee(userId=this cardId=../_id)
| {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard
a.js-start-date {{_ 'add'}}
.card-details-item.card-details-item-due
h3.card-details-item-title {{_ 'card-due'}}
if getDue
+cardDueDate
else
if canModifyCard
a.js-due-date {{_ 'add'}}
.card-details-item.card-details-item-end
h3.card-details-item-title {{_ 'card-end'}}
if getEnd
+cardEndDate
else
if canModifyCard
a.js-end-date {{_ 'add'}}
.card-details-items
.card-details-item.card-details-item-members
h3.card-details-item-title {{_ 'members'}}
each getMembers
+userAvatar(userId=this cardId=../_id)
| {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard
a.member.add-member.card-details-item-add-button.js-add-members(title="{{_ 'card-members-title'}}")
i.fa.fa-plus
.card-details-item.card-details-item-assignees
h3.card-details-item-title {{_ 'assignee'}}
each getAssignees
+userAvatarAssignee(userId=this cardId=../_id)
| {{! XXX Hack to hide syntaxic coloration /// }}
if canModifyCard
unless assigneeSelected
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
i.fa.fa-plus
if currentUser.isWorker
unless assigneeSelected
a.assignee.add-assignee.card-details-item-add-button.js-add-assignees(title="{{_ 'assignee'}}")
i.fa.fa-plus
.card-details-item.card-details-item-labels
h3.card-details-item-title {{_ 'labels'}}
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
each labels
span.card-label(class="card-label-{{color}}" title=name)
+viewer
= name
if canModifyCard
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
i.fa.fa-plus
if currentBoard.allowsLabels
.card-details-item.card-details-item-labels
h3
i.fa.fa-tags
card-details-item-title {{_ 'labels'}}
a(class="{{#if canModifyCard}}js-add-labels{{else}}is-disabled{{/if}}" title="{{_ 'card-labels-title'}}")
each labels
span.card-label(class="card-label-{{color}}" title=name)
+viewer
= name
if canModifyCard
unless currentUser.isWorker
a.card-label.add-label.js-add-labels(title="{{_ 'card-labels-title'}}")
i.fa.fa-plus
.card-details-items
//.card-details-items
each customFieldsWD
.card-details-item.card-details-item-customfield
h3.card-details-item-title
@@ -107,7 +149,7 @@ template(name="cardDetails")
= definition.name
+cardCustomField
.card-details-items
//.card-details-items
if getSpentTime
.card-details-item.card-details-item-spent
if getIsOvertime
@@ -116,84 +158,103 @@ template(name="cardDetails")
h3.card-details-item-title {{_ 'spent-time-hours'}}
+cardSpentTime
//.card-details-items
if currentBoard.allowsRequestedBy
.card-details-item.card-details-item-name
h3
i.fa.fa-shopping-cart
card-details-item-title {{_ 'requested-by'}}
if canModifyCard
unless currentUser.isWorker
+inlinedForm(classNames="js-card-details-requester")
+editCardRequesterForm
else
a.js-open-inlined-form
if getRequestedBy
+viewer
= getRequestedBy
else
| {{_ 'add'}}
else if getRequestedBy
+viewer
= getRequestedBy
if currentBoard.allowsAssignedBy
.card-details-item.card-details-item-name
h3
i.fa.fa-user-plus
card-details-item-title {{_ 'assigned-by'}}
if canModifyCard
unless currentUser.isWorker
+inlinedForm(classNames="js-card-details-assigner")
+editCardAssignerForm
else
a.js-open-inlined-form
if getAssignedBy
+viewer
= getAssignedBy
else
| {{_ 'add'}}
else if getRequestedBy
+viewer
= getAssignedBy
//- XXX We should use "editable" to avoid repetiting ourselves
if canModifyCard
h3.card-details-item-title {{_ 'description'}}
+inlinedCardDescription(classNames="card-description js-card-description")
+editor(autofocus=true)
| {{getUnsavedValue 'cardDescription' _id getDescription}}
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
else
a.js-open-inlined-form
if getDescription
+viewer
= getDescription
unless currentUser.isWorker
if currentBoard.allowsDescriptionTitle
h3
i.fa.fa-align-left
card-details-item-title {{_ 'description'}}
if currentBoard.allowsDescriptionText
+inlinedCardDescription(classNames="card-description js-card-description")
+editor(autofocus=true)
| {{getUnsavedValue 'cardDescription' _id getDescription}}
.edit-controls.clearfix
button.primary(type="submit") {{_ 'save'}}
a.fa.fa-times-thin.js-close-inlined-form
else
| {{_ 'edit'}}
if (hasUnsavedValue 'cardDescription' _id)
p.quiet
| {{_ 'unsaved-description'}}
a.js-open-inlined-form {{_ 'view-it'}}
= ' - '
a.js-close-inlined-form {{_ 'discard'}}
if currentBoard.allowsDescriptionText
a.js-open-inlined-form
if getDescription
+viewer
= getDescription
else
| {{_ 'edit'}}
if (hasUnsavedValue 'cardDescription' _id)
p.quiet
| {{_ 'unsaved-description'}}
a.js-open-inlined-form {{_ 'view-it'}}
= ' - '
a.js-close-inlined-form {{_ 'discard'}}
else if getDescription
h3.card-details-item-title {{_ 'description'}}
+viewer
= getDescription
if currentBoard.allowsDescriptionTitle
h3.card-details-item-title {{_ 'description'}}
if currentBoard.allowsDescriptionText
+viewer
= getDescription
.card-details-items
.card-details-item.card-details-item-name
h3.card-details-item-title {{_ 'requested-by'}}
if canModifyCard
+inlinedForm(classNames="js-card-details-requester")
+editCardRequesterForm
else
a.js-open-inlined-form
if getRequestedBy
+viewer
= getRequestedBy
else
| {{_ 'add'}}
else if getRequestedBy
+viewer
= getRequestedBy
.card-details-item.card-details-item-name
h3.card-details-item-title {{_ 'assigned-by'}}
if canModifyCard
+inlinedForm(classNames="js-card-details-assigner")
+editCardAssignerForm
else
a.js-open-inlined-form
if getAssignedBy
+viewer
= getAssignedBy
else
| {{_ 'add'}}
else if getRequestedBy
+viewer
= getAssignedBy
hr
+checklists(cardId = _id)
if currentBoard.allowsSubtasks
hr
+subtasks(cardId = _id)
hr
h3
i.fa.fa-paperclip
| {{_ 'attachments'}}
+attachmentsGalery
.card-checklist-attachmentGalerys
.card-checklist-attachmentGalery.card-checklists
if currentBoard.allowsChecklists
+checklists(cardId = _id)
if currentBoard.allowsSubtasks
hr
+subtasks(cardId = _id)
if currentBoard.allowsAttachments
//- hr
//- h3
//- i.fa.fa-paperclip
//- | {{_ 'attachments'}}
.card-checklist-attachmentGalery.card-attachmentGalery
+attachmentsGalery
hr
unless currentUser.isNoComments
.activity-title
h3 {{ _ 'activity'}}
h3
i.fa.fa-history
| {{ _ 'activity'}}
if currentUser.isBoardMember
.material-toggle-switch
span.toggle-switch-title {{_ 'hide-system-messages'}}
@@ -202,9 +263,10 @@ template(name="cardDetails")
else
input.toggle-switch(type="checkbox" id="toggleButton")
label.toggle-label(for="toggleButton")
if currentUser.isBoardMember
unless currentUser.isNoComments
+commentForm
if currentBoard.allowsComments
if currentUser.isBoardMember
unless currentUser.isNoComments
+commentForm
unless currentUser.isNoComments
if isLoaded.get
if isLinkedCard
@@ -235,32 +297,79 @@ template(name="editCardAssignerForm")
template(name="cardDetailsActionsPopup")
ul.pop-over-list
li: a.js-toggle-watch-card {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
li
a.js-toggle-watch-card
if isWatching
i.fa.fa-eye
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| {{_ 'watch'}}
if canModifyCard
hr
unless currentUser.isWorker
hr
ul.pop-over-list
//li: a.js-members {{_ 'card-edit-members'}}
//li: a.js-labels {{_ 'card-edit-labels'}}
//li: a.js-attachments {{_ 'card-edit-attachments'}}
li
a.js-custom-fields
i.fa.fa-list-alt
| {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
//li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
li
a.js-spent-time
i.fa.fa-clock-o
| {{_ 'editCardSpentTimePopup-title'}}
li
a.js-set-card-color
i.fa.fa-paint-brush
| {{_ 'setCardColorPopup-title'}}
hr
ul.pop-over-list
//li: a.js-members {{_ 'card-edit-members'}}
//li: a.js-labels {{_ 'card-edit-labels'}}
//li: a.js-attachments {{_ 'card-edit-attachments'}}
li: a.js-custom-fields {{_ 'card-edit-custom-fields'}}
//li: a.js-received-date {{_ 'editCardReceivedDatePopup-title'}}
//li: a.js-start-date {{_ 'editCardStartDatePopup-title'}}
//li: a.js-due-date {{_ 'editCardDueDatePopup-title'}}
//li: a.js-end-date {{_ 'editCardEndDatePopup-title'}}
li: a.js-spent-time {{_ 'editCardSpentTimePopup-title'}}
li: a.js-set-card-color {{_ 'setCardColorPopup-title'}}
hr
ul.pop-over-list
li: a.js-move-card-to-top {{_ 'moveCardToTop-title'}}
li: a.js-move-card-to-bottom {{_ 'moveCardToBottom-title'}}
hr
ul.pop-over-list
li: a.js-move-card {{_ 'moveCardPopup-title'}}
li: a.js-copy-card {{_ 'copyCardPopup-title'}}
li: a.js-copy-checklist-cards {{_ 'copyChecklistToManyCardsPopup-title'}}
li
a.js-move-card-to-top
i.fa.fa-arrow-up
| {{_ 'moveCardToTop-title'}}
li
a.js-move-card-to-bottom
i.fa.fa-arrow-down
| {{_ 'moveCardToBottom-title'}}
unless currentUser.isWorker
hr
ul.pop-over-list
li
a.js-move-card
i.fa.fa-arrow-right
| {{_ 'moveCardPopup-title'}}
li
a.js-copy-card
i.fa.fa-copy
| {{_ 'copyCardPopup-title'}}
hr
ul.pop-over-list
li
a.js-copy-checklist-cards
i.fa.fa-list
i.fa.fa-copy
| {{_ 'copyChecklistToManyCardsPopup-title'}}
unless archived
li: a.js-archive {{_ 'archive-card'}}
li: a.js-more {{_ 'cardMorePopup-title'}}
hr
ul.pop-over-list
li
a.js-archive
i.fa.fa-arrow-right
i.fa.fa-archive
| {{_ 'archive-card'}}
hr
ul.pop-over-list
li
a.js-more
i.fa.fa-link
| {{_ 'cardMorePopup-title'}}
template(name="moveCardPopup")
+boardsAndLists
@@ -312,16 +421,27 @@ template(name="cardMembersPopup")
i.fa.fa-check
template(name="cardAssigneesPopup")
ul.pop-over-list.js-card-assignee-list
each board.activeMembers
li.item(class="{{#if isCardAssignee}}active{{/if}}")
a.name.js-select-assignee(href="#")
+userAvatar(userId=user._id)
span.full-name
= user.profile.fullname
| (<span class="username">{{ user.username }}</span>)
if isCardAssignee
i.fa.fa-check
unless currentUser.isWorker
ul.pop-over-list.js-card-assignee-list
each board.activeMembers
li.item(class="{{#if isCardAssignee}}active{{/if}}")
a.name.js-select-assignee(href="#")
+userAvatar(userId=user._id)
span.full-name
= user.profile.fullname
| (<span class="username">{{ user.username }}</span>)
if isCardAssignee
i.fa.fa-check
if currentUser.isWorker
ul.pop-over-list.js-card-assignee-list
li.item(class="{{#if currentUser.isCardAssignee}}active{{/if}}")
a.name.js-select-assignee(href="#")
+userAvatar(userId=currentUser._id)
span.full-name
= currentUser.profile.fullname
| (<span class="username">{{ currentUser.username }}</span>)
if currentUser.isCardAssignee
i.fa.fa-check
template(name="userAvatarAssignee")
a.assignee.js-assignee(title="{{userData.profile.fullname}} ({{userData.username}})")
@@ -349,11 +469,13 @@ template(name="cardAssigneePopup")
p.quiet @{{ user.username }}
ul.pop-over-list
if currentUser.isNotCommentOnly
unless currentUser.isWorker
li: a.js-remove-assignee {{_ 'remove-member-from-card'}}
if $eq currentUser._id user._id
with currentUser
li: a.js-edit-profile {{_ 'edit-profile'}}
unless currentUser.isWorker
if $eq currentUser._id user._id
with currentUser
li: a.js-edit-profile {{_ 'edit-profile'}}
template(name="userAvatarAssigneeInitials")
svg.avatar.avatar-assignee-initials(viewBox="0 0 {{viewPortWidth}} 15")
+95 -1
View File
@@ -51,7 +51,8 @@ BlazeComponent.extendComponent({
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
@@ -252,6 +253,12 @@ BlazeComponent.extendComponent({
if ($subtasksDom.data('sortable')) {
$subtasksDom.sortable('option', 'disabled', !userIsMember());
}
if ($checklistsDom.data('sortable')) {
$checklistsDom.sortable('option', 'disabled', Utils.isMiniScreen());
}
if ($subtasksDom.data('sortable')) {
$subtasksDom.sortable('option', 'disabled', Utils.isMiniScreen());
}
});
},
@@ -278,6 +285,29 @@ BlazeComponent.extendComponent({
'click .js-close-card-details'() {
Utils.goBoardId(this.data().boardId);
},
'click .js-copy-link'() {
StringToCopyElement = document.getElementById('cardURL_copy');
StringToCopyElement.select();
if (document.execCommand('copy')) {
StringToCopyElement.blur();
} else {
document.getElementById('cardURL_copy').selectionStart = 0;
document.getElementById('cardURL_copy').selectionEnd = 999;
document.execCommand('copy');
if (window.getSelection) {
if (window.getSelection().empty) {
// Chrome
window.getSelection().empty();
} else if (window.getSelection().removeAllRanges) {
// Firefox
window.getSelection().removeAllRanges();
}
} else if (document.selection) {
// IE?
document.selection.empty();
}
}
},
'click .js-open-card-details-menu': Popup.open('cardDetailsActions'),
'submit .js-card-description'(event) {
event.preventDefault();
@@ -370,6 +400,54 @@ Template.cardDetails.helpers({
});
},
receivedSelected() {
if (this.getReceived().length === 0) {
return false;
} else {
return true;
}
},
startSelected() {
if (this.getStart().length === 0) {
return false;
} else {
return true;
}
},
endSelected() {
if (this.getEnd().length === 0) {
return false;
} else {
return true;
}
},
dueSelected() {
if (this.getDue().length === 0) {
return false;
} else {
return true;
}
},
memberSelected() {
if (this.getMembers().length === 0) {
return false;
} else {
return true;
}
},
labelSelected() {
if (this.getLabels().length === 0) {
return false;
} else {
return true;
}
},
assigneeSelected() {
if (this.getAssignees().length === 0) {
return false;
@@ -378,6 +456,22 @@ Template.cardDetails.helpers({
}
},
requestBySelected() {
if (this.getRequestBy().length === 0) {
return false;
} else {
return true;
}
},
assigneeBySelected() {
if (this.getAssigneeBy().length === 0) {
return false;
} else {
return true;
}
},
memberType() {
const user = Users.findOne(this.userId);
return user && user.isBoardAdmin() ? 'admin' : 'normal';
+18
View File
@@ -4,6 +4,12 @@
avatar-radius = 50%
#cardURL_copy
// Have clipboard text not visible by moving it to far left
position: absolute
left: -2000px
top: 0px
.assignee
border-radius: 3px
display: block
@@ -108,6 +114,8 @@ avatar-radius = 50%
.close-card-details,
.card-details-menu,
.card-copy-button,
.card-copy-mobile-button,
.close-card-details-mobile-web,
.card-details-menu-mobile-web
float: right
@@ -122,6 +130,16 @@ avatar-radius = 50%
padding: 5px
margin-right: 40px
.card-copy-button
font-size: 17px
padding: 10px
margin-right: 10px
.card-copy-mobile-button
font-size: 17px
padding: 10px
margin-right: 10px
.card-details-menu
font-size: 17px
padding: 10px
+3 -1
View File
@@ -1,5 +1,7 @@
template(name="checklists")
h3 {{_ 'checklists'}}
h3
i.fa.fa-check
| {{_ 'checklists'}}
if toggleDeleteDialog.get
.board-overlay#card-details-overlay
+checklistDeleteDialog(checklist = checklistToDelete)
+9 -3
View File
@@ -60,6 +60,9 @@ BlazeComponent.extendComponent({
if ($itemsDom.data('sortable')) {
$(self.itemsDom).sortable('option', 'disabled', !userIsMember());
}
if ($itemsDom.data('sortable')) {
$(self.itemsDom).sortable('option', 'disabled', Utils.isMiniScreen());
}
});
},
@@ -67,7 +70,8 @@ BlazeComponent.extendComponent({
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
}).register('checklistDetail');
@@ -120,7 +124,8 @@ BlazeComponent.extendComponent({
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
@@ -228,7 +233,8 @@ Template.checklistItemDetail.helpers({
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
});
+2 -2
View File
@@ -4,8 +4,8 @@ template(name="minicard")
class="{{#if isLinkedBoard}}linked-board{{/if}}"
class="minicard-{{colorClass}}")
if isMiniScreen
.handle
.fa.fa-arrows
//.handle
// .fa.fa-arrows
unless isMiniScreen
if showDesktopDragHandles
.handle
+6 -10
View File
@@ -36,24 +36,20 @@ Template.minicard.helpers({
currentUser = Meteor.user();
if (currentUser) {
return (currentUser.profile || {}).showDesktopDragHandles;
} else if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
return false;
}
return false;
}
},
hiddenMinicardLabelText() {
currentUser = Meteor.user();
if (currentUser) {
return (currentUser.profile || {}).hiddenMinicardLabelText;
} else if (cookies.has('hiddenMinicardLabelText')) {
return true;
} else {
if (cookies.has('hiddenMinicardLabelText')) {
return true;
} else {
return false;
}
return false;
}
},
});
+3 -1
View File
@@ -1,5 +1,7 @@
template(name="subtasks")
h3 {{_ 'subtasks'}}
h3
i.fa.fa-sitemap
| {{_ 'subtasks'}}
if toggleDeleteDialog.get
.board-overlay#card-details-overlay
+subtaskDeleteDialog(subtask = subtaskToDelete)
+6 -3
View File
@@ -3,7 +3,8 @@ BlazeComponent.extendComponent({
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
}).register('subtaskDetail');
@@ -55,7 +56,8 @@ BlazeComponent.extendComponent({
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
@@ -154,7 +156,8 @@ Template.subtaskItemDetail.helpers({
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
});
+25 -8
View File
@@ -133,14 +133,33 @@ BlazeComponent.extendComponent({
$cards.sortable({
handle: '.handle',
});
} else {
} else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
$cards.sortable({
handle: '.minicard',
});
}
// Disable drag-dropping if the current user is not a board member or is comment only
$cards.sortable('option', 'disabled', !userIsMember());
if ($cards.data('sortable')) {
$cards.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member/is miniscreen
!userIsMember(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !userIsMember(),
);
}
if ($cards.data('sortable')) {
$cards.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member/is miniscreen
Utils.isMiniScreen(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !userIsMember(),
);
}
});
// We want to re-run this function any time a card is added.
@@ -176,12 +195,10 @@ Template.list.helpers({
currentUser = Meteor.user();
if (currentUser) {
return (currentUser.profile || {}).showDesktopDragHandles;
} else if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
return false;
}
return false;
}
},
});
+19 -2
View File
@@ -189,7 +189,8 @@ BlazeComponent.extendComponent({
!this.reachedWipLimit() &&
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
@@ -742,9 +743,25 @@ BlazeComponent.extendComponent({
},
updateList() {
// Use fallback when requestIdleCallback is not available on iOS and Safari
// https://www.afasterweb.com/2017/11/20/utilizing-idle-moments/
checkIdleTime =
window.requestIdleCallback ||
function(handler) {
const startTime = Date.now();
return setTimeout(function() {
handler({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50.0 - (Date.now() - startTime));
},
});
}, 1);
};
if (this.spinnerInView()) {
this.cardlimit.set(this.cardlimit.get() + InfiniteScrollIter);
window.requestIdleCallback(() => this.updateList());
checkIdleTime(() => this.updateList());
}
},
+38 -15
View File
@@ -10,7 +10,7 @@ template(name="listHeader")
a.list-header-left-icon.fa.fa-angle-left.js-unselect-list
h2.list-header-name(
title="{{ moment modifiedAt 'LLL' }}"
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}js-open-inlined-form is-editable{{/unless}}{{/if}}")
class="{{#if currentUser.isBoardMember}}{{#unless currentUser.isCommentOnly}}{{#unless currentUser.isWorker}}js-open-inlined-form is-editable{{/unless}}{{/unless}}{{/if}}")
+viewer
= title
if wipLimit.enabled
@@ -30,10 +30,10 @@ template(name="listHeader")
if canSeeAddCard
a.js-add-card.fa.fa-plus.list-header-plus-icon
a.fa.fa-navicon.js-open-list-menu
a.list-header-handle.handle.fa.fa-arrows.js-list-handle
//a.list-header-handle.handle.fa.fa-arrows.js-list-handle
else
a.list-header-menu-icon.fa.fa-angle-right.js-select-list
a.list-header-handle.handle.fa.fa-arrows.js-list-handle
//a.list-header-handle.handle.fa.fa-arrows.js-list-handle
else if currentUser.isBoardMember
if isWatching
i.list-header-watch-icon.fa.fa-eye
@@ -56,25 +56,47 @@ template(name="editListTitleForm")
template(name="listActionPopup")
ul.pop-over-list
li: a.js-toggle-watch-list {{#if isWatching}}{{_ 'unwatch'}}{{else}}{{_ 'watch'}}{{/if}}
li
a.js-toggle-watch-list
if isWatching
i.fa.fa-eye
| {{_ 'unwatch'}}
else
i.fa.fa-eye-slash
| {{_ 'watch'}}
unless currentUser.isCommentOnly
hr
ul.pop-over-list
li: a.js-set-color-list {{_ 'set-color-list'}}
hr
unless currentUser.isWorker
ul.pop-over-list
li
a.js-set-color-list
i.fa.fa-paint-brush
| {{_ 'set-color-list'}}
ul.pop-over-list
if cards.count
li: a.js-select-cards {{_ 'list-select-cards'}}
hr
li
a.js-select-cards
i.fa.fa-check-square
| {{_ 'list-select-cards'}}
if currentUser.isBoardAdmin
ul.pop-over-list
li: a.js-set-wip-limit {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
li
a.js-set-wip-limit
i.fa.fa-ban
| {{#if isWipLimitEnabled }}{{_ 'edit-wip-limit'}}{{else}}{{_ 'setWipLimitPopup-title'}}{{/if}}
unless currentUser.isWorker
hr
ul.pop-over-list
li: a.js-close-list {{_ 'archive-list'}}
ul.pop-over-list
li
a.js-close-list
i.fa.fa-arrow-right
i.fa.fa-archive
| {{_ 'archive-list'}}
hr
ul.pop-over-list
li: a.js-more {{_ 'listMorePopup-title'}}
li
a.js-more
i.fa.fa-link
| {{_ 'listMorePopup-title'}}
template(name="boardLists")
ul.pop-over-list
@@ -94,7 +116,8 @@ template(name="listMorePopup")
input.inline-input(type="text" readonly value="{{ rootUrl }}")
| {{_ 'added'}}
span.date(title=list.createdAt) {{ moment createdAt 'LLL' }}
a.js-delete {{_ 'delete'}}
unless currentUser.isWorker
a.js-delete {{_ 'delete'}}
template(name="listDeletePopup")
p {{_ "list-delete-pop"}}
+7 -8
View File
@@ -9,9 +9,10 @@ BlazeComponent.extendComponent({
canSeeAddCard() {
const list = Template.currentData();
return (
!list.getWipLimit('enabled') ||
list.getWipLimit('soft') ||
!this.reachedWipLimit()
(!list.getWipLimit('enabled') ||
list.getWipLimit('soft') ||
!this.reachedWipLimit()) &&
!Meteor.user().isWorker()
);
},
@@ -109,12 +110,10 @@ Template.listHeader.helpers({
currentUser = Meteor.user();
if (currentUser) {
return (currentUser.profile || {}).showDesktopDragHandles;
} else if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
return false;
}
return false;
}
},
});
+6 -1
View File
@@ -1,8 +1,13 @@
template(name="editor")
// With Richer editor is in use, and comment is submitted,
// clear comment form with JQuery Comment at
// client/components/activities/comments.js . Id #summernote is defined
// here at client/components/main/editor.jade where it previously was
// id=id, now it is id="summernote".
textarea.editor(
dir="auto"
class="{{class}}"
id=id
id="summernote"
autofocus=autofocus
placeholder="{{_ 'comment-placeholder'}}")
+Template.contentBlock
+99 -135
View File
@@ -1,87 +1,3 @@
import _sanitizeXss from 'xss';
const ASIS = 'asis';
const sanitizeXss = (input, options) => {
const defaultAllowedIframeSrc = /^(https:){0,1}\/\/.*?(youtube|vimeo|dailymotion|youku)/i;
const allowedIframeSrcRegex = (function() {
let reg = defaultAllowedIframeSrc;
const SAFE_IFRAME_SRC_PATTERN =
Meteor.settings.public.SAFE_IFRAME_SRC_PATTERN;
try {
if (SAFE_IFRAME_SRC_PATTERN !== undefined) {
reg = new RegExp(SAFE_IFRAME_SRC_PATTERN, 'i');
}
} catch (e) {
/*eslint no-console: ["error", { allow: ["warn", "error"] }] */
console.error('Wrong pattern specified', SAFE_IFRAM_SRC_PATTERN, e);
}
return reg;
})();
const targetWindow = '_blank';
const getHtmlDOM = html => {
const i = document.createElement('i');
i.innerHTML = html;
return i.firstChild;
};
options = {
onTag(tag, html, options) {
const htmlDOM = getHtmlDOM(html);
const getAttr = attr => {
return htmlDOM && attr && htmlDOM.getAttribute(attr);
};
if (tag === 'iframe') {
const clipCls = 'note-vide-clip';
if (!options.isClosing) {
const iframeCls = getAttr('class');
let safe = iframeCls.indexOf(clipCls) > -1;
const src = getAttr('src');
if (allowedIframeSrcRegex.exec(src)) {
safe = true;
}
if (safe)
return `<iframe src='${src}' class="${clipCls}" width=100% height=auto allowfullscreen></iframe>`;
} else {
// remove </iframe> tag
return '';
}
} else if (tag === 'a') {
if (!options.isClosing) {
if (getAttr(ASIS) === 'true') {
// if has a ASIS attribute, don't do anything, it's a member id
return html;
} else {
const href = getAttr('href');
if (href.match(/^((http(s){0,1}:){0,1}\/\/|\/)/)) {
// a valid url
return `<a href=${href} target=${targetWindow}>`;
}
}
}
} else if (tag === 'img') {
if (!options.isClosing) {
const src = getAttr('src');
if (src) {
return `<a href='${src}' class='swipebox'><img src='${src}' class="attachment-image-preview mCS_img_loaded"></a>`;
}
}
}
return undefined;
},
onTagAttr(tag, name, value) {
if (tag === 'img' && name === 'src') {
if (value && value.substr(0, 5) === 'data:') {
// allow image with dataURI src
return `${name}='${value}'`;
}
} else if (tag === 'a' && name === 'target') {
return `${name}='${targetWindow}'`; // always change a href target to a new window
}
return undefined;
},
...options,
};
return _sanitizeXss(input, options);
};
Template.editor.onRendered(() => {
const textareaSelector = 'textarea';
const mentions = [
@@ -94,13 +10,7 @@ Template.editor.onRendered(() => {
currentBoard
.activeMembers()
.map(member => {
const user = Users.findOne(member.userId);
if (user._id === Meteor.userId()) {
return null;
}
const value = user.username;
const username =
value && value.match(/\s+/) ? `"${value}"` : value;
const username = Users.findOne(member.userId).username;
return username.includes(term) ? username : null;
})
.filter(Boolean),
@@ -120,16 +30,15 @@ Template.editor.onRendered(() => {
autosize($textarea);
$textarea.escapeableTextComplete(mentions);
};
if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR !== false) {
if (Meteor.settings.public.RICHER_CARD_COMMENT_EDITOR === 'true') {
const isSmall = Utils.isMiniScreen();
const toolbar = isSmall
? [
['view', ['fullscreen']],
['table', ['table']],
['font', ['bold']],
['color', ['color']],
['insert', ['video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
['font', ['bold', 'underline']],
//['fontsize', ['fontsize']],
['color', ['color']],
]
: [
['style', ['style']],
@@ -139,11 +48,47 @@ Template.editor.onRendered(() => {
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
//['insert', ['link', 'picture', 'video']], // iframe tag will be sanitized TODO if iframe[class=note-video-clip] can be added into safe list, insert video can be enabled
//['insert', ['link', 'picture']], // modal popup has issue somehow :(
['view', ['fullscreen', 'help']],
];
const cleanPastedHTML = sanitizeXss;
const cleanPastedHTML = function(input) {
const badTags = [
'style',
'script',
'applet',
'embed',
'noframes',
'noscript',
'meta',
'link',
'button',
'form',
].join('|');
const badPatterns = new RegExp(
`(?:${[
`<(${badTags})s*[^>][\\s\\S]*?<\\/\\1>`,
`<(${badTags})[^>]*?\\/>`,
].join('|')})`,
'gi',
);
let output = input;
// remove bad Tags
output = output.replace(badPatterns, '');
// remove attributes ' style="..."'
const badAttributes = new RegExp(
`(?:${[
'on\\S+=([\'"]?).*?\\1',
'href=([\'"]?)javascript:.*?\\2',
'style=([\'"]?).*?\\3',
'target=\\S+',
].join('|')})`,
'gi',
);
output = output.replace(badAttributes, '');
output = output.replace(/(<a )/gi, '$1target=_ '); // always to new target
return output;
};
const editor = '.editor';
const selectors = [
`.js-new-comment-form ${editor}`,
@@ -163,11 +108,27 @@ Template.editor.onRendered(() => {
}
return undefined;
};
// Prevent @member mentions on Add Comment input field
// from closing card, part 1.
let popupShown = false;
inputs.each(function(idx, input) {
mSummernotes[idx] = $(input).summernote({
placeholder,
// Prevent @member mentions on Add Comment input field
// from closing card, part 2.
onKeydown(e) {
if (popupShown) {
e.preventDefault();
}
},
onKeyup(e) {
if (popupShown) {
e.preventDefault();
}
},
callbacks: {
// Prevent @member mentions on Add Comment input field
// from closing card, part 3.
onKeydown(e) {
if (popupShown) {
e.preventDefault();
@@ -180,28 +141,19 @@ Template.editor.onRendered(() => {
},
onInit(object) {
const originalInput = this;
const setAutocomplete = function(jEditor) {
if (jEditor !== undefined) {
jEditor.escapeableTextComplete(mentions).on({
'textComplete:show'() {
popupShown = true;
},
'textComplete:hide'() {
popupShown = false;
},
});
}
};
$(originalInput).on('submitted', function() {
// resetCommentInput has been called
$(originalInput).on('input', function() {
// when comment is submitted, the original textarea will be set to '', so shall we
if (!this.value) {
const sn = getSummernote(this);
sn && sn.summernote('code', '');
sn && sn.summernote('reset');
object && object.editingArea.find('.note-placeholder').show();
}
});
const jEditor = object && object.editable;
const toolbar = object && object.toolbar;
setAutocomplete(jEditor);
if (jEditor !== undefined) {
jEditor.escapeableTextComplete(mentions);
}
if (toolbar !== undefined) {
const fBtn = toolbar.find('.btn-fullscreen');
fBtn.on('click', function() {
@@ -289,9 +241,15 @@ Template.editor.onRendered(() => {
const thisNote = this;
const updatePastedText = function(object) {
const someNote = getSummernote(object);
// Fix Pasting text into a card is adding a line before and after
// (and multiplies by pasting more) by changing paste "p" to "br".
// Fixes https://github.com/wekan/wekan/2890 .
// == Fix Start ==
someNote.execCommand('defaultParagraphSeparator', false, 'br');
// == Fix End ==
const original = someNote.summernote('code');
const cleaned = cleanPastedHTML(original); //this is where to call whatever clean function you want. I have mine in a different file, called CleanPastedHTML.
someNote.summernote('code', ''); //clear original
someNote.summernote('reset'); //clear original
someNote.summernote('pasteHTML', cleaned); //this sets the displayed content editor to the cleaned pasted code.
};
setTimeout(function() {
@@ -331,6 +289,8 @@ Template.editor.onRendered(() => {
}
});
import sanitizeXss from 'xss';
// XXX I believe we should compute a HTML rendered field on the server that
// would handle markdown and user mentions. We can simply have two
// fields, one source, and one compiled version (in HTML) and send only the
@@ -352,23 +312,28 @@ Blaze.Template.registerHelper(
}
return member;
});
const mentionRegex = /\B@(?:(?:"([\w.\s]*)")|([\w.]+))/gi; // including space in username
const mentionRegex = /\B@([\w.]*)/gi;
let currentMention;
while ((currentMention = mentionRegex.exec(content)) !== null) {
const [fullMention, quoteduser, simple] = currentMention;
const username = quoteduser || simple;
const [fullMention, username] = currentMention;
const knowedUser = _.findWhere(knowedUsers, { username });
if (!knowedUser) {
continue;
}
const linkValue = [' ', at, knowedUser.username];
let linkClass = 'atMention js-open-member';
//let linkClass = 'atMention js-open-member';
let linkClass = 'atMention';
if (knowedUser.userId === Meteor.userId()) {
linkClass += ' me';
}
const link = HTML.A(
// This @user mention link generation did open same Wekan
// window in new tab, so now A is changed to U so it's
// underlined and there is no link popup. This way also
// text can be selected more easily.
//const link = HTML.A(
const link = HTML.U(
{
class: linkClass,
// XXX Hack. Since we stringify this render function result below with
@@ -376,42 +341,41 @@ Blaze.Template.registerHelper(
// `userId` to the popup as usual, and we need to store it in the DOM
// using a data attribute.
'data-userId': knowedUser.userId,
[ASIS]: 'true',
},
linkValue,
);
content = content.replace(fullMention, Blaze.toHTML(link));
}
return HTML.Raw(sanitizeXss(content));
}),
);
Template.viewer.events({
// Viewer sometimes have click-able wrapper around them (for instance to edit
// the corresponding text). Clicking a link shouldn't fire these actions, stop
// we stop these event at the viewer component level.
'click a'(event, templateInstance) {
let prevent = true;
event.stopPropagation();
// XXX We hijack the build-in browser action because we currently don't have
// `_blank` attributes in viewer links, and the transformer function is
// handled by a third party package that we can't configure easily. Fix that
// by using directly `_blank` attribute in the rendered HTML.
event.preventDefault();
const userId = event.currentTarget.dataset.userid;
if (userId) {
Popup.open('member').call({ userId }, event, templateInstance);
// Prevent @member mentions on Add Comment input field
// from closing card, part 4.
PopupNoClose.open('member').call({ userId }, event, templateInstance);
event.preventDefault();
} else {
const href = event.currentTarget.href;
const child = event.currentTarget.firstElementChild;
if (child && child.tagName === 'IMG') {
prevent = false;
} else if (href) {
if (href) {
window.open(href, '_blank');
}
}
if (prevent) {
event.stopPropagation();
// XXX We hijack the build-in browser action because we currently don't have
// `_blank` attributes in viewer links, and the transformer function is
// handled by a third party package that we can't configure easily. Fix that
// by using directly `_blank` attribute in the rendered HTML.
event.preventDefault();
}
},
});
+3
View File
@@ -218,6 +218,9 @@
padding: 10px
margin: -10px 0 -10px -10px
.announcement .viewer
display: inline-block
.announcement,
.offline-warning
width: 100%
@@ -1,29 +1,42 @@
template(name="boardActions")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-move-card-to'}}
div.trigger-dropdown
select(id="move-gen-action")
option(value="top") {{_'r-top-of'}}
option(value="bottom") {{_'r-bottom-of'}}
div.trigger-text
div.trigger-text
| {{_'r-its-list'}}
div.trigger-button.js-add-gen-move-action.js-goto-rules
i.fa.fa-plus
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-move-card-to'}}
div.trigger-dropdown
select(id="move-spec-action")
option(value="top") {{_'r-top-of'}}
option(value="bottom") {{_'r-bottom-of'}}
div.trigger-text
| {{_'r-list'}}
div.trigger-text
| {{_'r-the-board'}}
div.trigger-dropdown
select(id="board-id")
each boards
if $eq _id currentBoard._id
option(value="{{_id}}" selected) {{_ 'current'}}
else
option(value="{{_id}}") {{title}}
div.trigger-text
| {{_'r-in-list'}}
div.trigger-dropdown
input(id="listName",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
| {{_'r-in-swimlane'}}
div.trigger-dropdown
input(id="swimlaneName",type=text,placeholder="{{_'r-name'}}")
div.trigger-button.js-add-spec-move-action.js-goto-rules
i.fa.fa-plus
@@ -33,14 +46,14 @@ template(name="boardActions")
select(id="arch-action")
option(value="archive") {{_'r-archive'}}
option(value="unarchive") {{_'r-unarchive'}}
div.trigger-text
div.trigger-text
| {{_'r-card'}}
div.trigger-button.js-add-arch-action.js-goto-rules
i.fa.fa-plus
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-add-swimlane'}}
div.trigger-dropdown
input(id="swimlane-name",type=text,placeholder="{{_'r-name'}}")
@@ -49,15 +62,15 @@ template(name="boardActions")
div.trigger-item
div.trigger-content
div.trigger-text
div.trigger-text
| {{_'r-create-card'}}
div.trigger-dropdown
input(id="card-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
div.trigger-text
| {{_'r-in-list'}}
div.trigger-dropdown
input(id="list-name",type=text,placeholder="{{_'r-name'}}")
div.trigger-text
div.trigger-text
| {{_'r-in-swimlane'}}
div.trigger-dropdown
input(id="swimlane-name2",type=text,placeholder="{{_'r-name'}}")
@@ -65,8 +78,8 @@ template(name="boardActions")
i.fa.fa-plus
@@ -1,6 +1,22 @@
BlazeComponent.extendComponent({
onCreated() {},
boards() {
const boards = Boards.find(
{
archived: false,
'members.userId': Meteor.userId(),
_id: {
$ne: Meteor.user().getTemplatesBoardId(),
},
},
{
sort: ['title'],
},
);
return boards;
},
events() {
return [
{
@@ -52,15 +68,18 @@ BlazeComponent.extendComponent({
const ruleName = this.data().ruleName.get();
const trigger = this.data().triggerVar.get();
const actionSelected = this.find('#move-spec-action').value;
const listTitle = this.find('#listName').value;
const swimlaneName = this.find('#swimlaneName').value;
const listName = this.find('#listName').value;
const boardId = Session.get('currentBoard');
const destBoardId = this.find('#board-id').value;
const desc = Utils.getTriggerActionDesc(event, this);
if (actionSelected === 'top') {
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToTop',
listTitle,
boardId,
listName,
swimlaneName,
boardId: destBoardId,
desc,
});
Rules.insert({
@@ -74,8 +93,9 @@ BlazeComponent.extendComponent({
const triggerId = Triggers.insert(trigger);
const actionId = Actions.insert({
actionType: 'moveCardToBottom',
listTitle,
boardId,
listName,
swimlaneName,
boardId: destBoardId,
desc,
});
Rules.insert({
@@ -4,12 +4,16 @@ template(name='information')
| {{_ 'error-notAuthorized'}}
else
.content-title
span {{_ 'info'}}
span
i.fa.fa-info-circle
| {{_ 'info'}}
.content-body
.side-menu
ul
li.active
a.js-setting-menu(data-id="information-display") {{_ 'info'}}
a.js-setting-menu(data-id="information-display")
i.fa.fa-info-circle
| {{_ 'info'}}
.main-body
+statistics
+63 -4
View File
@@ -5,16 +5,22 @@ template(name="people")
else
.content-title.ext-box
.ext-box-left
span {{_ 'people'}}
span
i.fa.fa-users
| {{_ 'people'}}
input#searchInput(placeholder="{{_ 'search'}}")
button#searchButton {{_ 'search'}}
button#searchButton
i.fa.fa-search
| {{_ 'search'}}
.ext-box-right
span {{_ 'people-number'}} #{peopleNumber}
.content-body
.side-menu
ul
li.active
a.js-setting-menu(data-id="people-setting") {{_ 'people'}}
a.js-setting-menu(data-id="people-setting")
i.fa.fa-users
| {{_ 'people'}}
.main-body
if loading.get
+spinner
@@ -34,9 +40,15 @@ template(name="peopleGeneral")
th {{_ 'active'}}
th {{_ 'authentication-method'}}
th
+newUserRow
each user in peopleList
+peopleRow(userId=user._id)
template(name="newUserRow")
a.new-user
i.fa.fa-edit
| {{_ 'new'}}
template(name="peopleRow")
tr
if userData.loginDisabled
@@ -90,6 +102,7 @@ template(name="peopleRow")
td {{_ userData.authenticationMethod }}
td
a.edit-user
i.fa.fa-edit
| {{_ 'edit'}}
template(name="editUserPopup")
@@ -97,7 +110,7 @@ template(name="editUserPopup")
label.hide.userId(type="text" value=user._id)
label
| {{_ 'fullname'}}
input.js-profile-fullname(type="text" value=user.profile.fullname autofocus)
input.js-profile-fullname(type="text" value=user.profile.fullname)
label
| {{_ 'username'}}
span.error.hide.username-taken
@@ -141,3 +154,49 @@ template(name="editUserPopup")
// div
// input#deleteButton.primary.wide(type="button" value="{{_ 'delete'}}")
template(name="newUserPopup")
form
//label.hide.userId(type="text" value=user._id)
label
| {{_ 'fullname'}}
input.js-profile-fullname(type="text" value="")
label
| {{_ 'username'}}
span.error.hide.username-taken
| {{_ 'error-username-taken'}}
//if isLdap
// input.js-profile-username(type="text" value=user.username readonly)
//else
input.js-profile-username(type="text" value="")
label
| {{_ 'email'}}
span.error.hide.email-taken
| {{_ 'error-email-taken'}}
//if isLdap
// input.js-profile-email(type="email" value="{{user.emails.[0].address}}" readonly)
//else
input.js-profile-email(type="email" value="")
label
| {{_ 'admin'}}
select.select-role.js-profile-isadmin
option(value="false" selected="selected") {{_ 'no'}}
option(value="true") {{_ 'yes'}}
label
| {{_ 'active'}}
select.select-active.js-profile-isactive
option(value="false" selected="selected") {{_ 'yes'}}
option(value="true") {{_ 'no'}}
label
| {{_ 'authentication-type'}}
select.select-authenticationMethod.js-authenticationMethod
each authentications
if isSelected value
option(value="{{value}}" selected) {{_ value}}
else
option(value="{{value}}") {{_ value}}
hr
label
| {{_ 'password'}}
input.js-profile-password(type="password")
div.buttonsContainer
input.primary.wide(type="submit" value="{{_ 'save'}}")
+95
View File
@@ -39,6 +39,9 @@ BlazeComponent.extendComponent({
this.filterPeople();
}
},
'click #newUserButton'() {
Popup.open('newUser');
},
},
];
},
@@ -141,6 +144,47 @@ Template.editUserPopup.helpers({
},
});
Template.newUserPopup.onCreated(function() {
this.authenticationMethods = new ReactiveVar([]);
this.errorMessage = new ReactiveVar('');
Meteor.call('getAuthenticationsEnabled', (_, result) => {
if (result) {
// TODO : add a management of different languages
// (ex {value: ldap, text: TAPi18n.__('ldap', {}, T9n.getLanguage() || 'en')})
this.authenticationMethods.set([
{ value: 'password' },
// Gets only the authentication methods availables
...Object.entries(result)
.filter(e => e[1])
.map(e => ({ value: e[0] })),
]);
}
});
});
Template.newUserPopup.helpers({
//user() {
// return Users.findOne(this.userId);
//},
authentications() {
return Template.instance().authenticationMethods.get();
},
//isSelected(match) {
// const userId = Template.instance().data.userId;
// const selected = Users.findOne(userId).authenticationMethod;
// return selected === match;
//},
//isLdap() {
// const userId = Template.instance().data.userId;
// const selected = Users.findOne(userId).authenticationMethod;
// return selected === 'ldap';
//},
errorMessage() {
return Template.instance().errorMessage.get();
},
});
BlazeComponent.extendComponent({
onCreated() {},
user() {
@@ -155,6 +199,16 @@ BlazeComponent.extendComponent({
},
}).register('peopleRow');
BlazeComponent.extendComponent({
events() {
return [
{
'click a.new-user': Popup.open('newUser'),
},
];
},
}).register('newUserRow');
Template.editUserPopup.events({
submit(event, templateInstance) {
event.preventDefault();
@@ -248,3 +302,44 @@ Template.editUserPopup.events({
Popup.close();
}),
});
Template.newUserPopup.events({
submit(event, templateInstance) {
event.preventDefault();
const fullname = templateInstance.find('.js-profile-fullname').value.trim();
const username = templateInstance.find('.js-profile-username').value.trim();
const password = templateInstance.find('.js-profile-password').value;
const isAdmin = templateInstance.find('.js-profile-isadmin').value.trim();
const isActive = templateInstance.find('.js-profile-isactive').value.trim();
const email = templateInstance.find('.js-profile-email').value.trim();
Meteor.call(
'setCreateUser',
fullname,
username,
password,
isAdmin,
isActive,
email.toLowerCase(),
function(error) {
const usernameMessageElement = templateInstance.$('.username-taken');
const emailMessageElement = templateInstance.$('.email-taken');
if (error) {
const errorElement = error.error;
if (errorElement === 'username-already-taken') {
usernameMessageElement.show();
emailMessageElement.hide();
} else if (errorElement === 'email-already-taken') {
usernameMessageElement.hide();
emailMessageElement.show();
}
} else {
usernameMessageElement.hide();
emailMessageElement.hide();
Popup.close();
}
},
);
Popup.close();
},
});
+1 -1
View File
@@ -33,7 +33,7 @@ table
padding: 0;
button
min-width: 60px;
min-width: 90px;
.content-wrapper
margin-top: 10px
+19 -12
View File
@@ -4,22 +4,35 @@ template(name="setting")
| {{_ 'error-notAuthorized'}}
else
.content-title
i.fa.fa-cog
span {{_ 'settings'}}
.content-body
.side-menu
ul
li.active
a.js-setting-menu(data-id="registration-setting") {{_ 'registration'}}
a.js-setting-menu(data-id="registration-setting")
i.fa.fa-sign-in
| {{_ 'registration'}}
li
a.js-setting-menu(data-id="email-setting") {{_ 'email'}}
a.js-setting-menu(data-id="email-setting")
i.fa.fa-envelope
| {{_ 'email'}}
li
a.js-setting-menu(data-id="account-setting") {{_ 'accounts'}}
a.js-setting-menu(data-id="account-setting")
i.fa.fa-users
| {{_ 'accounts'}}
li
a.js-setting-menu(data-id="announcement-setting") {{_ 'admin-announcement'}}
a.js-setting-menu(data-id="announcement-setting")
i.fa.fa-bullhorn
| {{_ 'admin-announcement'}}
li
a.js-setting-menu(data-id="layout-setting") {{_ 'layout'}}
a.js-setting-menu(data-id="layout-setting")
i.fa.fa-object-group
| {{_ 'layout'}}
li
a.js-setting-menu(data-id="webhook-setting") {{_ 'global-webhook'}}
a.js-setting-menu(data-id="webhook-setting")
i.fa.fa-globe
| {{_ 'global-webhook'}}
.main-body
if loading.get
+spinner
@@ -171,12 +184,6 @@ template(name='layoutSettings')
.title {{_ 'custom-product-name'}}
.form-group
input.wekan-form-control#product-name(type="text", placeholder="" value="{{currentSetting.productName}}")
li.layout-form
.title {{_ 'add-custom-html-after-body-start'}}
textarea#customHTMLafterBodyStart.wekan-form-control= currentSetting.customHTMLafterBodyStart
li.layout-form
.title {{_ 'add-custom-html-before-body-end'}}
textarea#customHTMLbeforeBodyEnd.wekan-form-control= currentSetting.customHTMLbeforeBodyEnd
li
button.js-save-layout.primary {{_ 'save'}}
@@ -171,20 +171,12 @@ BlazeComponent.extendComponent({
const displayAuthenticationMethod =
$('input[name=displayAuthenticationMethod]:checked').val() === 'true';
const defaultAuthenticationMethod = $('#defaultAuthenticationMethod').val();
const customHTMLafterBodyStart = $('#customHTMLafterBodyStart')
.val()
.trim();
const customHTMLbeforeBodyEnd = $('#customHTMLbeforeBodyEnd')
.val()
.trim();
try {
Settings.update(Settings.findOne()._id, {
$set: {
productName,
hideLogo: hideLogoChange,
customHTMLafterBodyStart,
customHTMLbeforeBodyEnd,
displayAuthenticationMethod,
defaultAuthenticationMethod,
},
+4 -1
View File
@@ -41,15 +41,18 @@
&:hover
background #fff
box-shadow 0 1px 2px rgba(0,0,0,0.15);
a
@extends .flex
padding: 1rem 0 1rem 1rem
width: 100% - 5rem
span
font-size: 13px
i
margin-right: 20px
.main-body
padding: 0.1em 1em
-webkit-user-select: text // Safari 3.1+
+194 -29
View File
@@ -37,11 +37,12 @@ template(name='homeSidebar')
template(name="membersWidget")
.board-widget.board-widget-members
h3
i.fa.fa-user
i.fa.fa-users
| {{_ 'members'}}
unless currentUser.isCommentOnly
a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right
i.board-header-btn-icon.fa.fa-cog
unless currentUser.isWorker
a.board-header-btn.js-open-board-menu(title="{{_ 'boardMenuPopup-title'}}").right
i.board-header-btn-icon.fa.fa-cog
.board-widget-content
each currentBoard.activeMembers
@@ -71,6 +72,108 @@ template(name="boardChangeColorPopup")
if isSelected
i.fa.fa-check
template(name="boardCardSettingsPopup")
form.board-card-settings
h3 {{_ 'show-on-card'}}
div.check-div
a.flex.js-field-has-receiveddate(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsReceivedDate}}is-checked{{/if}}")
span
i.fa.fa-sign-out
| {{_ 'card-received'}}
div.check-div
a.flex.js-field-has-startdate(class="{{#if allowsStartDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsStartDate}}is-checked{{/if}}")
span
i.fa.fa-hourglass-start
| {{_ 'card-start'}}
div.check-div
a.flex.js-field-has-duedate(class="{{#if allowsDueDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDueDate}}is-checked{{/if}}")
span
i.fa.fa-sign-in
| {{_ 'card-due'}}
div.check-div
a.flex.js-field-has-enddate(class="{{#if allowsEndDate}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsEndDate}}is-checked{{/if}}")
span
i.fa.fa-hourglass-end
| {{_ 'card-end'}}
div.check-div
a.flex.js-field-has-members(class="{{#if allowsMembers}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsMembers}}is-checked{{/if}}")
span
i.fa.fa-users
| {{_ 'members'}}
div.check-div
a.flex.js-field-has-assignee(class="{{#if allowsAssignee}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAssignee}}is-checked{{/if}}")
span
i.fa.fa-user
| {{_ 'assignee'}}
div.check-div
a.flex.js-field-has-assigned-by(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAssignedBy}}is-checked{{/if}}")
span
i.fa.fa-shopping-cart
| {{_ 'assigned-by'}}
div.check-div
a.flex.js-field-has-requested-by(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsRequestedBy}}is-checked{{/if}}")
span
i.fa.fa-user-plus
| {{_ 'requested-by'}}
div.check-div
a.flex.js-field-has-labels(class="{{#if allowsLabels}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsLabels}}is-checked{{/if}}")
span
i.fa.fa-tags
| {{_ 'labels'}}
div.check-div
a.flex.js-field-has-description-title(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDescriptionTitle}}is-checked{{/if}}")
span
i.fa.fa-align-left
| {{_ 'description'}}
| {{_ 'title'}}
div.check-div
a.flex.js-field-has-description-text(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsDescriptionText}}is-checked{{/if}}")
span
i.fa.fa-align-left
| {{_ 'description'}}
| {{_ 'custom-field-text'}}
div.check-div
a.flex.js-field-has-checklists(class="{{#if allowsChecklists}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsChecklists}}is-checked{{/if}}")
span
i.fa.fa-check
| {{_ 'checklists'}}
div.check-div
a.flex.js-field-has-subtasks(class="{{#if allowsSubtasks}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsSubtasks}}is-checked{{/if}}")
span
i.fa.fa-sitemap
| {{_ 'subtasks'}}
div.check-div
a.flex.js-field-has-attachments(class="{{#if allowsAttachments}}is-checked{{/if}}")
.materialCheckBox(class="{{#if allowsAttachments}}is-checked{{/if}}")
span
i.fa.fa-paperclip
| {{_ 'attachments'}}
//div.check-div
// a.flex.js-field-has-comments(class="{{#if allowsComments}}is-checked{{/if}}")
// .materialCheckBox(class="{{#if allowsComments}}is-checked{{/if}}")
// span
// i.fa.fa-comment-o
// | {{_ 'comment'}}
//div.check-div
// a.flex.js-field-has-activities(class="{{#if allowsActivities}}is-checked{{/if}}")
// .materialCheckBox(class="{{#if allowsActivities}}is-checked{{/if}}")
// span
// i.fa.fa-history
// | {{_ 'activities'}}
template(name="boardSubtaskSettingsPopup")
form.board-subtask-settings
h3 {{_ 'show-parent-in-minicard'}}
@@ -130,7 +233,9 @@ template(name="chooseBoardSource")
template(name="archiveBoardPopup")
p {{_ 'close-board-pop'}}
button.js-confirm.negate.full(type="submit") {{_ 'archive'}}
button.js-confirm.negate.full(type="submit")
i.fa.fa-archive
| {{_ 'archive'}}
template(name="outgoingWebhooksPopup")
each integrations
@@ -140,7 +245,7 @@ template(name="outgoingWebhooksPopup")
b &nbsp;
.materialCheckBox(class="{{#unless enabled}}is-checked{{/unless}}")
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" value=title)
input.js-outgoing-webhooks-url(type="text" name="url" value=url autofocus)
input.js-outgoing-webhooks-url(type="text" name="url" value=url)
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" value=token name="token")
select.js-outgoing-webhooks-type(name="type")
each _type in types
@@ -152,7 +257,7 @@ template(name="outgoingWebhooksPopup")
input(type="hidden" value=_id name="id")
input.primary.wide(type="submit" value="{{_ 'save'}}")
form.integration-form
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title" autofocus)
input.js-outgoing-webhooks-title(placeholder="{{_ 'webhook-title'}}" type="text" name="title")
input.js-outgoing-webhooks-url(placeholder="{{_ 'URL' }}" type="text" name="url")
input.js-outgoing-webhooks-token(placeholder="{{_ 'webhook-token' }}" type="text" name="token")
select.js-outgoing-webhooks-type(name="type")
@@ -162,38 +267,92 @@ template(name="outgoingWebhooksPopup")
template(name="boardMenuPopup")
ul.pop-over-list
li: a.js-custom-fields {{_ 'custom-fields'}}
li: a.js-open-archives {{_ 'archived-items'}}
li
a.js-custom-fields
i.fa.fa-list-alt
| {{_ 'custom-fields'}}
li
a.js-open-archives
i.fa.fa-archive
| {{_ 'archived-items'}}
if currentUser.isBoardAdmin
li: a.js-change-board-color {{_ 'board-change-color'}}
li
a.js-change-board-color
i.fa.fa-paint-brush
| {{_ 'board-change-color'}}
//-
XXX Language should be handled by sandstorm, but for now display a
language selection link in the board menu. This link is normally present
in the header bar that is not displayed on sandstorm.
if isSandstorm
li: a.js-change-language {{_ 'language'}}
li
a.js-change-language
i.fa.fa-flag
| {{_ 'language'}}
unless isSandstorm
if currentUser.isBoardAdmin
hr
ul.pop-over-list
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
unless currentBoard.isTemplatesBoard
li: a.js-archive-board {{_ 'archive-board'}}
li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
hr
ul.pop-over-list
li: a.js-subtask-settings {{_ 'subtask-settings'}}
li
a(href="{{exportUrl}}", download="{{exportFilename}}")
i.fa.fa-share-alt
| {{_ 'export-board'}}
li
a.js-outgoing-webhooks
i.fa.fa-globe
| {{_ 'outgoing-webhooks'}}
li
a.js-card-settings
i.fa.fa-id-card-o
| {{_ 'card-settings'}}
li
a.js-subtask-settings
i.fa.fa-sitemap
| {{_ 'subtask-settings'}}
unless currentBoard.isTemplatesBoard
hr
ul.pop-over-list
li
a.js-archive-board
i.fa.fa-arrow-right
i.fa.fa-archive
| {{_ 'archive-board'}}
if isSandstorm
hr
ul.pop-over-list
li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}}
li: a.js-import-board {{_ 'import-board-c'}}
li: a.js-archive-board {{_ 'archive-board'}}
li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}}
li
a(href="{{exportUrl}}", download="{{exportFilename}}")
i.fa.fa-share-alt
i.fa.fa-sign-out
| {{_ 'export-board'}}
li
a.js-import-board
i.fa.fa-share-alt
i.fa.fa-sign-in
| {{_ 'import-board-c'}}
li
a.js-archive-board
i.fa.fa-arrow-right
i.fa.fa-archive
| {{_ 'archive-board'}}
li
a.js-outgoing-webhooks
i.fa.fa-globe
| {{_ 'outgoing-webhooks'}}
hr
ul.pop-over-list
li: a.js-subtask-settings {{_ 'subtask-settings'}}
li
a.js-card-settings
i.fa.fa-id-card-o
| {{_ 'card-settings'}}
hr
ul.pop-over-list
li
a.js-subtask-settings
i.fa.fa-sitemap
| {{_ 'subtask-settings'}}
template(name="labelsWidget")
.board-widget.board-widget-labels
@@ -203,7 +362,7 @@ template(name="labelsWidget")
.board-widget-content
each currentBoard.labels
a.card-label(class="card-label-{{color}}"
class="{{#if currentUser.isNotCommentOnly}}js-label{{/if}}")
class="{{#if currentUser.isNotCommentOnly}}{{#if currentUser.isNotWorker}}js-label{{/if}}{{/if}}")
span.card-label-name
+viewer
= name
@@ -232,12 +391,12 @@ template(name="memberPopup")
a.js-change-role
| {{_ 'change-permissions'}}
span.quiet (#{memberType})
li
if $eq currentUser._id userId
a.js-leave-member {{_ 'leave-board'}}
else if currentUser.isBoardAdmin
a.js-remove-member {{_ 'remove-from-board'}}
unless currentUser.isWorker
li
if $eq currentUser._id userId
a.js-leave-member {{_ 'leave-board'}}
else if currentUser.isBoardAdmin
a.js-remove-member {{_ 'remove-from-board'}}
template(name="removeMemberPopup")
p {{_ 'remove-member-pop' name=user.profile.fullname username=user.username boardTitle=board.title}}
@@ -301,6 +460,12 @@ template(name="changePermissionsPopup")
if isCommentOnly
i.fa.fa-check
span.sub-name {{_ 'comment-only-desc'}}
li
a(class="{{#if isLastAdmin}}disabled{{else}}js-set-worker{{/if}}")
| {{_ 'worker'}}
if isWorker
i.fa.fa-check
span.sub-name {{_ 'worker-desc'}}
if isLastAdmin
hr
p.quiet.bottom {{_ 'last-admin-desc'}}
+387 -12
View File
@@ -112,12 +112,10 @@ BlazeComponent.extendComponent({
currentUser = Meteor.user();
if (currentUser) {
Meteor.call('toggleMinicardLabelText');
} else if (cookies.has('hiddenMinicardLabelText')) {
cookies.remove('hiddenMinicardLabelText');
} else {
if (cookies.has('hiddenMinicardLabelText')) {
cookies.remove('hiddenMinicardLabelText');
} else {
cookies.set('hiddenMinicardLabelText', 'true');
}
cookies.set('hiddenMinicardLabelText', 'true');
}
},
'click .js-shortcuts'() {
@@ -135,12 +133,10 @@ Template.homeSidebar.helpers({
currentUser = Meteor.user();
if (currentUser) {
return (currentUser.profile || {}).hiddenMinicardLabelText;
} else if (cookies.has('hiddenMinicardLabelText')) {
return true;
} else {
if (cookies.has('hiddenMinicardLabelText')) {
return true;
} else {
return false;
}
return false;
}
},
});
@@ -165,10 +161,13 @@ Template.memberPopup.helpers({
const currentBoard = Boards.findOne(Session.get('currentBoard'));
const commentOnly = currentBoard.hasCommentOnly(this.userId);
const noComments = currentBoard.hasNoComments(this.userId);
const worker = currentBoard.hasWorker(this.userId);
if (commentOnly) {
return TAPi18n.__('comment-only').toLowerCase();
} else if (noComments) {
return TAPi18n.__('no-comments').toLowerCase();
} else if (worker) {
return TAPi18n.__('worker').toLowerCase();
} else {
return TAPi18n.__(type).toLowerCase();
}
@@ -209,6 +208,7 @@ Template.boardMenuPopup.events({
'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'),
'click .js-import-board': Popup.open('chooseBoardSource'),
'click .js-subtask-settings': Popup.open('boardSubtaskSettings'),
'click .js-card-settings': Popup.open('boardCardSettings'),
});
Template.boardMenuPopup.helpers({
@@ -271,6 +271,14 @@ Template.membersWidget.helpers({
const user = Meteor.user();
return user && user.isInvitedTo(Session.get('currentBoard'));
},
isWorker() {
const user = Meteor.user();
if (user) {
return Meteor.call(Boards.hasWorker(user.memberId));
} else {
return false;
}
},
});
Template.membersWidget.events({
@@ -465,6 +473,10 @@ BlazeComponent.extendComponent({
return this.currentBoard.allowsSubtasks;
},
allowsReceivedDate() {
return this.currentBoard.allowsReceivedDate;
},
isBoardSelected() {
return this.currentBoard.subtasksDefaultBoardId === this.currentData()._id;
},
@@ -578,6 +590,359 @@ BlazeComponent.extendComponent({
},
}).register('boardSubtaskSettingsPopup');
BlazeComponent.extendComponent({
onCreated() {
this.currentBoard = Boards.findOne(Session.get('currentBoard'));
},
allowsReceivedDate() {
return this.currentBoard.allowsReceivedDate;
},
allowsStartDate() {
return this.currentBoard.allowsStartDate;
},
allowsDueDate() {
return this.currentBoard.allowsDueDate;
},
allowsEndDate() {
return this.currentBoard.allowsEndDate;
},
allowsSubtasks() {
return this.currentBoard.allowsSubtasks;
},
allowsMembers() {
return this.currentBoard.allowsMembers;
},
allowsAssignee() {
return this.currentBoard.allowsAssignee;
},
allowsAssignedBy() {
return this.currentBoard.allowsAssignedBy;
},
allowsRequestedBy() {
return this.currentBoard.allowsRequestedBy;
},
allowsLabels() {
return this.currentBoard.allowsLabels;
},
allowsChecklists() {
return this.currentBoard.allowsChecklists;
},
allowsAttachments() {
return this.currentBoard.allowsAttachments;
},
allowsComments() {
return this.currentBoard.allowsComments;
},
allowsDescriptionTitle() {
return this.currentBoard.allowsDescriptionTitle;
},
allowsDescriptionText() {
return this.currentBoard.allowsDescriptionText;
},
isBoardSelected() {
return this.currentBoard.dateSettingsDefaultBoardID;
},
isNullBoardSelected() {
return (
this.currentBoard.dateSettingsDefaultBoardId === null ||
this.currentBoard.dateSettingsDefaultBoardId === undefined
);
},
boards() {
return Boards.find(
{
archived: false,
'members.userId': Meteor.userId(),
},
{
sort: ['title'],
},
);
},
lists() {
return Lists.find(
{
boardId: this.currentBoard._id,
archived: false,
},
{
sort: ['title'],
},
);
},
hasLists() {
return this.lists().count() > 0;
},
isListSelected() {
return (
this.currentBoard.dateSettingsDefaultBoardId === this.currentData()._id
);
},
events() {
return [
{
'click .js-field-has-receiveddate'(evt) {
evt.preventDefault();
this.currentBoard.allowsReceivedDate = !this.currentBoard
.allowsReceivedDate;
this.currentBoard.setAllowsReceivedDate(
this.currentBoard.allowsReceivedDate,
);
$(`.js-field-has-receiveddate ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsReceivedDate,
);
$('.js-field-has-receiveddate').toggleClass(
CKCLS,
this.currentBoard.allowsReceivedDate,
);
},
'click .js-field-has-startdate'(evt) {
evt.preventDefault();
this.currentBoard.allowsStartDate = !this.currentBoard
.allowsStartDate;
this.currentBoard.setAllowsStartDate(
this.currentBoard.allowsStartDate,
);
$(`.js-field-has-startdate ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsStartDate,
);
$('.js-field-has-startdate').toggleClass(
CKCLS,
this.currentBoard.allowsStartDate,
);
},
'click .js-field-has-enddate'(evt) {
evt.preventDefault();
this.currentBoard.allowsEndDate = !this.currentBoard.allowsEndDate;
this.currentBoard.setAllowsEndDate(this.currentBoard.allowsEndDate);
$(`.js-field-has-enddate ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsEndDate,
);
$('.js-field-has-enddate').toggleClass(
CKCLS,
this.currentBoard.allowsEndDate,
);
},
'click .js-field-has-duedate'(evt) {
evt.preventDefault();
this.currentBoard.allowsDueDate = !this.currentBoard.allowsDueDate;
this.currentBoard.setAllowsDueDate(this.currentBoard.allowsDueDate);
$(`.js-field-has-duedate ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsDueDate,
);
$('.js-field-has-duedate').toggleClass(
CKCLS,
this.currentBoard.allowsDueDate,
);
},
'click .js-field-has-subtasks'(evt) {
evt.preventDefault();
this.currentBoard.allowsSubtasks = !this.currentBoard.allowsSubtasks;
this.currentBoard.setAllowsSubtasks(this.currentBoard.allowsSubtasks);
$(`.js-field-has-subtasks ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsSubtasks,
);
$('.js-field-has-subtasks').toggleClass(
CKCLS,
this.currentBoard.allowsSubtasks,
);
},
'click .js-field-has-members'(evt) {
evt.preventDefault();
this.currentBoard.allowsMembers = !this.currentBoard.allowsMembers;
this.currentBoard.setAllowsMembers(this.currentBoard.allowsMembers);
$(`.js-field-has-members ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsMembers,
);
$('.js-field-has-members').toggleClass(
CKCLS,
this.currentBoard.allowsMembers,
);
},
'click .js-field-has-assignee'(evt) {
evt.preventDefault();
this.currentBoard.allowsAssignee = !this.currentBoard.allowsAssignee;
this.currentBoard.setAllowsAssignee(this.currentBoard.allowsAssignee);
$(`.js-field-has-assignee ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAssignee,
);
$('.js-field-has-assignee').toggleClass(
CKCLS,
this.currentBoard.allowsAssignee,
);
},
'click .js-field-has-assigned-by'(evt) {
evt.preventDefault();
this.currentBoard.allowsAssignedBy = !this.currentBoard
.allowsAssignedBy;
this.currentBoard.setAllowsAssignedBy(
this.currentBoard.allowsAssignedBy,
);
$(`.js-field-has-assigned-by ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAssignedBy,
);
$('.js-field-has-assigned-by').toggleClass(
CKCLS,
this.currentBoard.allowsAssignedBy,
);
},
'click .js-field-has-requested-by'(evt) {
evt.preventDefault();
this.currentBoard.allowsRequestedBy = !this.currentBoard
.allowsRequestedBy;
this.currentBoard.setAllowsRequestedBy(
this.currentBoard.allowsRequestedBy,
);
$(`.js-field-has-requested-by ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsRequestedBy,
);
$('.js-field-has-requested-by').toggleClass(
CKCLS,
this.currentBoard.allowsRequestedBy,
);
},
'click .js-field-has-labels'(evt) {
evt.preventDefault();
this.currentBoard.allowsLabels = !this.currentBoard.allowsLabels;
this.currentBoard.setAllowsLabels(this.currentBoard.allowsLabels);
$(`.js-field-has-labels ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAssignee,
);
$('.js-field-has-labels').toggleClass(
CKCLS,
this.currentBoard.allowsLabels,
);
},
'click .js-field-has-description-title'(evt) {
evt.preventDefault();
this.currentBoard.allowsDescriptionTitle = !this.currentBoard
.allowsDescriptionTitle;
this.currentBoard.setAllowsDescriptionTitle(
this.currentBoard.allowsDescriptionTitle,
);
$(`.js-field-has-description-title ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionTitle,
);
$('.js-field-has-description-title').toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionTitle,
);
},
'click .js-field-has-description-text'(evt) {
evt.preventDefault();
this.currentBoard.allowsDescriptionText = !this.currentBoard
.allowsDescriptionText;
this.currentBoard.setAllowsDescriptionText(
this.currentBoard.allowsDescriptionText,
);
$(`.js-field-has-description-text ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionText,
);
$('.js-field-has-description-text').toggleClass(
CKCLS,
this.currentBoard.allowsDescriptionText,
);
},
'click .js-field-has-checklists'(evt) {
evt.preventDefault();
this.currentBoard.allowsChecklists = !this.currentBoard
.allowsChecklists;
this.currentBoard.setAllowsChecklists(
this.currentBoard.allowsChecklists,
);
$(`.js-field-has-checklists ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsChecklists,
);
$('.js-field-has-checklists').toggleClass(
CKCLS,
this.currentBoard.allowsChecklists,
);
},
'click .js-field-has-attachments'(evt) {
evt.preventDefault();
this.currentBoard.allowsAttachments = !this.currentBoard
.allowsAttachments;
this.currentBoard.setAllowsAttachments(
this.currentBoard.allowsAttachments,
);
$(`.js-field-has-attachments ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsAttachments,
);
$('.js-field-has-attachments').toggleClass(
CKCLS,
this.currentBoard.allowsAttachments,
);
},
'click .js-field-has-comments'(evt) {
evt.preventDefault();
this.currentBoard.allowsComments = !this.currentBoard.allowsComments;
this.currentBoard.setAllowsComments(this.currentBoard.allowsComments);
$(`.js-field-has-comments ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsComments,
);
$('.js-field-has-comments').toggleClass(
CKCLS,
this.currentBoard.allowsComments,
);
},
'click .js-field-has-activities'(evt) {
evt.preventDefault();
this.currentBoard.allowsActivities = !this.currentBoard
.allowsActivities;
this.currentBoard.setAllowsActivities(
this.currentBoard.allowsActivities,
);
$(`.js-field-has-activities ${MCB}`).toggleClass(
CKCLS,
this.currentBoard.allowsActivities,
);
$('.js-field-has-activities').toggleClass(
CKCLS,
this.currentBoard.allowsActivities,
);
},
},
];
},
}).register('boardCardSettingsPopup');
BlazeComponent.extendComponent({
onCreated() {
this.error = new ReactiveVar('');
@@ -648,7 +1013,7 @@ BlazeComponent.extendComponent({
}).register('addMemberPopup');
Template.changePermissionsPopup.events({
'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only'(
'click .js-set-admin, click .js-set-normal, click .js-set-no-comments, click .js-set-comment-only, click .js-set-worker'(
event,
) {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
@@ -658,11 +1023,13 @@ Template.changePermissionsPopup.events({
'js-set-comment-only',
);
const isNoComments = $(event.currentTarget).hasClass('js-set-no-comments');
const isWorker = $(event.currentTarget).hasClass('js-set-worker');
currentBoard.setMemberPermission(
memberId,
isAdmin,
isNoComments,
isCommentOnly,
isWorker,
);
Popup.back(1);
},
@@ -679,7 +1046,8 @@ Template.changePermissionsPopup.helpers({
return (
!currentBoard.hasAdmin(this.userId) &&
!currentBoard.hasNoComments(this.userId) &&
!currentBoard.hasCommentOnly(this.userId)
!currentBoard.hasCommentOnly(this.userId) &&
!currentBoard.hasWorker(this.userId)
);
},
@@ -699,6 +1067,13 @@ Template.changePermissionsPopup.helpers({
);
},
isWorker() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
return (
!currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
);
},
isLastAdmin() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
return (
+1 -1
View File
@@ -109,7 +109,7 @@
color: darken(white, 40%)
.board-sidebar
width: 248px
width: 548px
right: -@width
transition: top .1s, right .1s, width .1s
+30 -24
View File
@@ -2,54 +2,60 @@ template(name="archivesSidebar")
if isArchiveReady.get
+basicTabs(tabs=tabs)
+tabContent(slug="cards")
p.quiet
a.js-restore-all-cards {{_ 'restore-all'}}
| -
a.js-delete-all-cards {{_ 'delete-all'}}
unless isWorker
p.quiet
a.js-restore-all-cards {{_ 'restore-all'}}
| -
a.js-delete-all-cards {{_ 'delete-all'}}
each archivedCards
.minicard-wrapper.js-minicard
+minicard(this)
if currentUser.isBoardMember
p.quiet
a.js-restore-card {{_ 'restore'}}
| -
a.js-delete-card {{_ 'delete'}}
unless isWorker
p.quiet
a.js-restore-card {{_ 'restore'}}
| -
a.js-delete-card {{_ 'delete'}}
if cardIsInArchivedList
p.quiet.small ({{_ 'warn-list-archived'}})
else
p.no-items-message {{_ 'no-archived-cards'}}
+tabContent(slug="lists")
p.quiet
a.js-restore-all-lists {{_ 'restore-all'}}
| -
a.js-delete-all-lists {{_ 'delete-all'}}
unless isWorker
p.quiet
a.js-restore-all-lists {{_ 'restore-all'}}
| -
a.js-delete-all-lists {{_ 'delete-all'}}
ul.archived-lists
each archivedLists
li.archived-lists-item
= title
if currentUser.isBoardMember
p.quiet
a.js-restore-list {{_ 'restore'}}
| -
a.js-delete-list {{_ 'delete'}}
unless isWorker
p.quiet
a.js-restore-list {{_ 'restore'}}
| -
a.js-delete-list {{_ 'delete'}}
else
li.no-items-message {{_ 'no-archived-lists'}}
+tabContent(slug="swimlanes")
p.quiet
a.js-restore-all-swimlanes {{_ 'restore-all'}}
| -
a.js-delete-all-swimlanes {{_ 'delete-all'}}
unless isWorker
p.quiet
a.js-restore-all-swimlanes {{_ 'restore-all'}}
| -
a.js-delete-all-swimlanes {{_ 'delete-all'}}
ul.archived-lists
each archivedSwimlanes
li.archived-lists-item
= title
if currentUser.isBoardMember
p.quiet
a.js-restore-swimlane {{_ 'restore'}}
| -
a.js-delete-swimlane {{_ 'delete'}}
unless isWorker
p.quiet
a.js-restore-swimlane {{_ 'restore'}}
| -
a.js-delete-swimlane {{_ 'delete'}}
else
li.no-items-message {{_ 'no-archived-swimlanes'}}
else
@@ -139,3 +139,12 @@ BlazeComponent.extendComponent({
];
},
}).register('archivesSidebar');
Template.archivesSidebar.helpers({
isWorker() {
const currentBoard = Boards.findOne(Session.get('currentBoard'));
return (
!currentBoard.hasAdmin(this.userId) && currentBoard.hasWorker(this.userId)
);
},
});
@@ -117,13 +117,14 @@ template(name="multiselectionSidebar")
i.fa.fa-check
else if someSelectedElementHave 'member' _id
i.fa.fa-ellipsis-h
hr
a.sidebar-btn.js-move-selection
i.fa.fa-share
span {{_ 'move-selection'}}
a.sidebar-btn.js-archive-selection
i.fa.fa-archive
span {{_ 'archive-selection'}}
unless currentUser.isWorker
hr
a.sidebar-btn.js-move-selection
i.fa.fa-share
span {{_ 'move-selection'}}
a.sidebar-btn.js-archive-selection
i.fa.fa-archive
span {{_ 'archive-selection'}}
template(name="disambiguateMultiLabelPopup")
p {{_ 'what-to-do'}}
@@ -35,12 +35,10 @@ Template.swimlaneHeader.helpers({
currentUser = Meteor.user();
if (currentUser) {
return (currentUser.profile || {}).showDesktopDragHandles;
} else if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
return false;
}
return false;
}
},
});
+17 -16
View File
@@ -43,19 +43,20 @@ template(name="listsGroup")
+addListForm
template(name="addListForm")
.list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
+inlinedForm(autoclose=false)
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
autocomplete="off" autofocus)
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-list-template {{_ 'template'}}
else
a.open-list-composer.js-open-inlined-form
i.fa.fa-plus
| {{_ 'add-list'}}
unless currentUser.isWorker
.list.list-composer.js-list-composer(class="{{#if isMiniScreen}}mini-list{{/if}}")
.list-header-add
+inlinedForm(autoclose=false)
input.list-name-input.full-line(type="text" placeholder="{{_ 'add-list'}}"
autocomplete="off" autofocus)
.edit-controls.clearfix
button.primary.confirm(type="submit") {{_ 'save'}}
unless currentBoard.isTemplatesBoard
unless currentBoard.isTemplateBoard
span.quiet
| {{_ 'or'}}
a.js-list-template {{_ 'template'}}
else
a.open-list-composer.js-open-inlined-form
i.fa.fa-plus
| {{_ 'add-list'}}
+37 -19
View File
@@ -94,7 +94,8 @@ function initSortable(boardComponent, $listsDom) {
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
}
@@ -104,19 +105,17 @@ function initSortable(boardComponent, $listsDom) {
if (currentUser) {
showDesktopDragHandles = (currentUser.profile || {})
.showDesktopDragHandles;
} else if (cookies.has('showDesktopDragHandles')) {
showDesktopDragHandles = true;
} else {
if (cookies.has('showDesktopDragHandles')) {
showDesktopDragHandles = true;
} else {
showDesktopDragHandles = false;
}
showDesktopDragHandles = false;
}
if (!Utils.isMiniScreen() && showDesktopDragHandles) {
$listsDom.sortable({
handle: '.js-list-handle',
});
} else {
} else if (!Utils.isMiniScreen() && !showDesktopDragHandles) {
$listsDom.sortable({
handle: '.js-list-header',
});
@@ -127,12 +126,34 @@ function initSortable(boardComponent, $listsDom) {
$listsDom.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member
// Disable drag-dropping when user is not member/is worker/is miniscreen
!userIsMember(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !userIsMember(),
);
}
if ($listDom.data('sortable')) {
$listsDom.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member/is worker/is miniscreen
Meteor.user().isWorker(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !userIsMember(),
);
}
if ($listDom.data('sortable')) {
$listsDom.sortable(
'option',
'disabled',
// Disable drag-dropping when user is not member/is worker/is miniscreen
Utils.isMiniScreen(),
// Not disable drag-dropping while in multi-selection mode
// MultiSelection.isActive() || !userIsMember(),
);
}
});
}
@@ -182,12 +203,10 @@ BlazeComponent.extendComponent({
if (currentUser) {
showDesktopDragHandles = (currentUser.profile || {})
.showDesktopDragHandles;
} else if (cookies.has('showDesktopDragHandles')) {
showDesktopDragHandles = true;
} else {
if (cookies.has('showDesktopDragHandles')) {
showDesktopDragHandles = true;
} else {
showDesktopDragHandles = false;
}
showDesktopDragHandles = false;
}
const noDragInside = ['a', 'input', 'textarea', 'p'].concat(
@@ -276,19 +295,18 @@ Template.swimlane.helpers({
currentUser = Meteor.user();
if (currentUser) {
return (currentUser.profile || {}).showDesktopDragHandles;
} else if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
if (cookies.has('showDesktopDragHandles')) {
return true;
} else {
return false;
}
return false;
}
},
canSeeAddList() {
return (
Meteor.user() &&
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly()
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
);
},
});
+1
View File
@@ -73,6 +73,7 @@ template(name="cardMemberPopup")
p.quiet @{{ user.username }}
ul.pop-over-list
if currentUser.isNotCommentOnly
if currentUser.isNotWorker
li: a.js-remove-member {{_ 'remove-member-from-card'}}
if $eq currentUser._id user._id
+47 -18
View File
@@ -13,21 +13,46 @@ template(name="headerUserBar")
template(name="memberMenuPopup")
ul.pop-over-list
with currentUser
li: a.js-edit-profile {{_ 'edit-profile'}}
li: a.js-change-settings {{_ 'change-settings'}}
li: a.js-change-avatar {{_ 'edit-avatar'}}
li
a.js-edit-profile
i.fa.fa-user
| {{_ 'edit-profile'}}
li
a.js-change-settings
i.fa.fa-cog
| {{_ 'change-settings'}}
li
a.js-change-avatar
i.fa.fa-picture-o
| {{_ 'edit-avatar'}}
unless isSandstorm
li: a.js-change-password {{_ 'changePasswordPopup-title'}}
li: a.js-change-language {{_ 'changeLanguagePopup-title'}}
li
a.js-change-password
i.fa.fa-key
| {{_ 'changePasswordPopup-title'}}
li
a.js-change-language
i.fa.fa-flag
| {{_ 'changeLanguagePopup-title'}}
if currentUser.isAdmin
li: a.js-go-setting(href="{{pathFor 'setting'}}") {{_ 'admin-panel'}}
hr
ul.pop-over-list
li: a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}") {{_ 'templates'}}
li
a.js-go-setting(href="{{pathFor 'setting'}}")
i.fa.fa-lock
| {{_ 'admin-panel'}}
unless currentUser.isWorker
hr
ul.pop-over-list
li
a(href="{{pathFor 'board' id=templatesBoardId slug=templatesBoardSlug}}")
i.fa.fa-clone
| {{_ 'templates'}}
unless isSandstorm
hr
ul.pop-over-list
li: a.js-logout {{_ 'log-out'}}
li
a.js-logout
i.fa.fa-sign-out
| {{_ 'log-out'}}
template(name="editProfilePopup")
form
@@ -75,21 +100,25 @@ template(name="changeSettingsPopup")
ul.pop-over-list
li
a.js-toggle-system-messages
i.fa.fa-comments-o
| {{_ 'hide-system-messages'}}
if hiddenSystemMessages
i.fa.fa-check
li
a.js-toggle-desktop-drag-handles
i.fa.fa-arrows
| {{_ 'show-desktop-drag-handles'}}
if showDesktopDragHandles
i.fa.fa-check
li
label.bold
| {{_ 'show-cards-minimum-count'}}
input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
unless currentUser.isWorker
li
label.bold
i.fa.fa-sort-numeric-asc
| {{_ 'show-cards-minimum-count'}}
input#show-cards-count-at.inline-input.left(type="number" value="#{showCardsCountAt}" min="0" max="99" onkeydown="return false")
input.js-apply-show-cards-at.left(type="submit" value="{{_ 'apply'}}")
template(name="userDeletePopup")
p {{_ 'delete-user-confirm-popup'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
unless currentUser.isWorker
p {{_ 'delete-user-confirm-popup'}}
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
+21 -3
View File
@@ -45,13 +45,31 @@ Template.memberMenuPopup.events({
Template.editProfilePopup.helpers({
allowEmailChange() {
return AccountSettings.findOne('accounts-allowEmailChange').booleanValue;
Meteor.call('AccountSettings.allowEmailChange', (_, result) => {
if (result) {
return true;
} else {
return false;
}
});
},
allowUserNameChange() {
return AccountSettings.findOne('accounts-allowUserNameChange').booleanValue;
Meteor.call('AccountSettings.allowUserNameChange', (_, result) => {
if (result) {
return true;
} else {
return false;
}
});
},
allowUserDelete() {
return AccountSettings.findOne('accounts-allowUserDelete').booleanValue;
Meteor.call('AccountSettings.allowUserDelete', (_, result) => {
if (result) {
return true;
} else {
return false;
}
});
},
});
+28
View File
@@ -70,6 +70,30 @@ Mousetrap.bind('space', evt => {
}
});
// XXX This shortcut should also work when hovering over a card in board view
Mousetrap.bind('c', evt => {
if (!Session.get('currentCard')) {
return;
}
const currentUserId = Meteor.userId();
if (currentUserId === null) {
return;
}
if (
Meteor.user().isBoardMember() &&
!Meteor.user().isCommentOnly() &&
!Meteor.user().isWorker()
) {
const card = Cards.findOne(Session.get('currentCard'));
card.archive();
// 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: [
{
@@ -104,5 +128,9 @@ Template.keyboardShortcuts.helpers({
keys: ['SPACE'],
action: 'shortcut-assign-self',
},
{
keys: ['C'],
action: 'archive-card',
},
],
});
+204
View File
@@ -206,3 +206,207 @@ escapeActions.forEach(actionName => {
},
);
});
// Prevent @member mentions on Add Comment input field
// from closing card, part 5.
// This duplicate below of above popup function is needed, because at
// wekan/components/main/editor.js at bottom is popping up visible
// @member mention, and it seems to trigger closing also card popup,
// so in below closing popup is disabled.
window.PopupNoClose = new (class {
constructor() {
// The template we use to render popups
this.template = Template.popup;
// We only want to display one popup at a time and we keep the view object
// in this `Popup.current` variable. If there is no popup currently opened
// the value is `null`.
this.current = null;
// It's possible to open a sub-popup B from a popup A. In that case we keep
// the data of popup A so we can return back to it. Every time we open a new
// popup the stack grows, every time we go back the stack decrease, and if
// we close the popup the stack is reseted to the empty stack [].
this._stack = [];
// We invalidate this internal dependency every time the top of the stack
// has changed and we want to re-render a popup with the new top-stack data.
this._dep = new Tracker.Dependency();
}
/// This function returns a callback that can be used in an event map:
/// Template.tplName.events({
/// 'click .elementClass': Popup.open("popupName"),
/// });
/// The popup inherit the data context of its parent.
open(name) {
const self = this;
const popupName = `${name}Popup`;
function clickFromPopup(evt) {
return $(evt.target).closest('.js-pop-over').length !== 0;
}
return function(evt) {
// If a popup is already opened, clicking again on the opener element
// should close it -- and interrupt the current `open` function.
/*
if (self.isOpen()) {
const previousOpenerElement = self._getTopStack().openerElement;
if (previousOpenerElement === evt.currentTarget) {
self.close();
return;
} else {
$(previousOpenerElement).removeClass('is-active');
}
}
*/
// We determine the `openerElement` (the DOM element that is being clicked
// and the one we take in reference to position the popup) from the event
// if the popup has no parent, or from the parent `openerElement` if it
// has one. This allows us to position a sub-popup exactly at the same
// position than its parent.
let openerElement;
if (clickFromPopup(evt)) {
openerElement = self._getTopStack().openerElement;
} else {
self._stack = [];
openerElement = evt.currentTarget;
}
$(openerElement).addClass('is-active');
evt.preventDefault();
// We push our popup data to the stack. The top of the stack is always
// used as the data source for our current popup.
self._stack.push({
popupName,
openerElement,
hasPopupParent: clickFromPopup(evt),
title: self._getTitle(popupName),
depth: self._stack.length,
offset: self._getOffset(openerElement),
dataContext: (this && this.currentData && this.currentData()) || this,
});
// If there are no popup currently opened we use the Blaze API to render
// one into the DOM. We use a reactive function as the data parameter that
// return the complete along with its top element and depends on our
// internal dependency that is being invalidated every time the top
// element of the stack has changed and we want to update the popup.
//
// Otherwise if there is already a popup open we just need to invalidate
// our internal dependency, and since we just changed the top element of
// our internal stack, the popup will be updated with the new data.
if (!self.isOpen()) {
self.current = Blaze.renderWithData(
self.template,
() => {
self._dep.depend();
return { ...self._getTopStack(), stack: self._stack };
},
document.body,
);
} else {
self._dep.changed();
}
};
}
/// This function returns a callback that can be used in an event map:
/// Template.tplName.events({
/// 'click .elementClass': Popup.afterConfirm("popupName", function() {
/// // What to do after the user has confirmed the action
/// }),
/// });
afterConfirm(name, action) {
const self = this;
return function(evt, tpl) {
const context = (this.currentData && this.currentData()) || this;
context.__afterConfirmAction = action;
self.open(name).call(context, evt, tpl);
};
}
/// The public reactive state of the popup.
isOpen() {
this._dep.changed();
return Boolean(this.current);
}
/// In case the popup was opened from a parent popup we can get back to it
/// with this `Popup.back()` function. You can go back several steps at once
/// by providing a number to this function, e.g. `Popup.back(2)`. In this case
/// intermediate popup won't even be rendered on the DOM. If the number of
/// steps back is greater than the popup stack size, the popup will be closed.
back(n = 1) {
if (this._stack.length > n) {
_.times(n, () => this._stack.pop());
this._dep.changed();
}
// else {
// this.close();
//}
}
/// Close the current opened popup.
/*
close() {
if (this.isOpen()) {
Blaze.remove(this.current);
this.current = null;
const openerElement = this._getTopStack().openerElement;
$(openerElement).removeClass('is-active');
this._stack = [];
}
}
*/
getOpenerComponent() {
const { openerElement } = Template.parentData(4);
return BlazeComponent.getComponentForElement(openerElement);
}
// An utility fonction that returns the top element of the internal stack
_getTopStack() {
return this._stack[this._stack.length - 1];
}
// We automatically calculate the popup offset from the reference element
// position and dimensions. We also reactively use the window dimensions to
// ensure that the popup is always visible on the screen.
_getOffset(element) {
const $element = $(element);
return () => {
Utils.windowResizeDep.depend();
if (Utils.isMiniScreen()) return { left: 0, top: 0 };
const offset = $element.offset();
const popupWidth = 300 + 15;
return {
left: Math.min(offset.left, $(window).width() - popupWidth),
top: offset.top + $element.outerHeight(),
};
};
}
// We get the title from the translation files. Instead of returning the
// result, we return a function that compute the result and since `TAPi18n.__`
// is a reactive data source, the title will be changed reactively.
_getTitle(popupName) {
return () => {
const translationKey = `${popupName}-title`;
// XXX There is no public API to check if there is an available
// translation for a given key. So we try to translate the key and if the
// translation output equals the key input we deduce that no translation
// was available and returns `false`. There is a (small) risk a false
// positives.
const title = TAPi18n.__(translationKey);
// when popup showed as full of small screen, we need a default header to clearly see [X] button
const defaultTitle = Utils.isMiniScreen() ? '' : false;
return title !== translationKey ? title : defaultTitle;
};
}
})();
+8 -3
View File
@@ -48,6 +48,11 @@ $.fn.escapeableTextComplete = function(strategies, options, ...otherArgs) {
return this;
};
EscapeActions.register('textcomplete', () => {}, () => dropdownMenuIsOpened, {
noClickEscapeOn: '.textcomplete-dropdown',
});
EscapeActions.register(
'textcomplete',
() => {},
() => dropdownMenuIsOpened,
{
noClickEscapeOn: '.textcomplete-dropdown',
},
);
+48 -22
View File
@@ -24,18 +24,14 @@ Utils = {
currentUser = Meteor.user();
if (currentUser) {
return (currentUser.profile || {}).boardView;
} else if (cookies.get('boardView') === 'board-view-lists') {
return 'board-view-lists';
} else if (cookies.get('boardView') === 'board-view-swimlanes') {
return 'board-view-swimlanes';
} else if (cookies.get('boardView') === 'board-view-cal') {
return 'board-view-cal';
} else {
if (cookies.get('boardView') === 'board-view-lists') {
return 'board-view-lists';
} else if (
cookies.get('boardView') === 'board-view-swimlanes'
) {
return 'board-view-swimlanes';
} else if (cookies.get('boardView') === 'board-view-cal') {
return 'board-view-cal';
} else {
return false;
}
return false;
}
},
@@ -43,8 +39,8 @@ Utils = {
goBoardId(_id) {
const board = Boards.findOne(_id);
return (
board
&& FlowRouter.go('board', {
board &&
FlowRouter.go('board', {
id: board._id,
slug: board.slug,
})
@@ -55,8 +51,8 @@ Utils = {
const card = Cards.findOne(_id);
const board = Boards.findOne(card.boardId);
return (
board
&& FlowRouter.go('card', {
board &&
FlowRouter.go('card', {
cardId: card._id,
boardId: board._id,
slug: board.slug,
@@ -151,8 +147,38 @@ Utils = {
// in a small window (even on desktop), Wekan run in compact mode.
// we can easily debug with a small window of desktop browser. :-)
isMiniScreen() {
// OLD WINDOW WIDTH DETECTION:
this.windowResizeDep.depend();
return $(window).width() <= 800;
// NEW TOUCH DEVICE DETECTION:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
/*
var hasTouchScreen = false;
if ("maxTouchPoints" in navigator) {
hasTouchScreen = navigator.maxTouchPoints > 0;
} else if ("msMaxTouchPoints" in navigator) {
hasTouchScreen = navigator.msMaxTouchPoints > 0;
} else {
var mQ = window.matchMedia && matchMedia("(pointer:coarse)");
if (mQ && mQ.media === "(pointer:coarse)") {
hasTouchScreen = !!mQ.matches;
} else if ('orientation' in window) {
hasTouchScreen = true; // deprecated, but good fallback
} else {
// Only as a last resort, fall back to user agent sniffing
var UA = navigator.userAgent;
hasTouchScreen = (
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(UA) ||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(UA)
);
}
}
*/
//if (hasTouchScreen)
// document.getElementById("exampleButton").style.padding="1em";
//return false;
},
calculateIndexData(prevData, nextData, nItems = 1) {
@@ -227,8 +253,8 @@ Utils = {
};
if (
'ontouchstart' in window
|| (window.DocumentTouch && document instanceof window.DocumentTouch)
'ontouchstart' in window ||
(window.DocumentTouch && document instanceof window.DocumentTouch)
) {
return true;
}
@@ -249,8 +275,8 @@ Utils = {
calculateTouchDistance(touchA, touchB) {
return Math.sqrt(
Math.pow(touchA.screenX - touchB.screenX, 2)
+ Math.pow(touchA.screenY - touchB.screenY, 2),
Math.pow(touchA.screenX - touchB.screenX, 2) +
Math.pow(touchA.screenY - touchB.screenY, 2),
);
},
@@ -267,9 +293,9 @@ Utils = {
});
$(document).on('touchend', selector, function(e) {
if (
touchStart
&& lastTouch
&& Utils.calculateTouchDistance(touchStart, lastTouch) <= 20
touchStart &&
lastTouch &&
Utils.calculateTouchDistance(touchStart, lastTouch) <= 20
) {
e.preventDefault();
const clickEvent = document.createEvent('MouseEvents');
+7 -3
View File
@@ -38,9 +38,13 @@ AccountsTemplates.configure({
},
});
['signIn', 'signUp', 'resetPwd', 'forgotPwd', 'enrollAccount'].forEach(
routeName => AccountsTemplates.configureRoute(routeName),
);
[
'signIn',
'signUp',
'resetPwd',
'forgotPwd',
'enrollAccount',
].forEach(routeName => AccountsTemplates.configureRoute(routeName));
// We display the form to change the password in a popup window that already
// have a title, so we unset the title automatically displayed by useraccounts.
+38 -4
View File
@@ -93,14 +93,14 @@ services:
#-------------------------------------------------------------------------------------
# ==== MONGODB AND METEOR VERSION ====
# a) For Wekan Meteor 1.8.x version at master branch, use mongo 4.x
image: mongo:4.0.12
image: mongo:latest
# b) For Wekan Meteor 1.6.x version at devel branch.
# Only for Snap and Sandstorm while they are not upgraded yet to Meteor 1.8.x
#image: mongo:3.2.21
#-------------------------------------------------------------------------------------
container_name: wekan-db
restart: always
command: mongod --smallfiles --oplogSize 128
command: mongod --oplogSize 128
networks:
- wekan-tier
expose:
@@ -238,7 +238,12 @@ services:
#---------------------------------------------------------------
# ==== RICH TEXT EDITOR IN CARD COMMENTS ====
# https://github.com/wekan/wekan/pull/2560
- RICHER_CARD_COMMENT_EDITOR=true
- RICHER_CARD_COMMENT_EDITOR=false
#---------------------------------------------------------------
# ==== MOUSE SCROLL ====
# https://github.com/wekan/wekan/issues/2949
- SCROLLINERTIA=0
- SCROLLAMOUNT=auto
#---------------------------------------------------------------
# ==== CARD OPENED, SEND WEBHOOK MESSAGE ====
# https://github.com/wekan/wekan/issues/2518
@@ -342,6 +347,31 @@ services:
# Tthe claim name you want to map to the email field:
#- OAUTH2_EMAIL_MAP=email
#-----------------------------------------------------------------
# ==== OAUTH2 Nextcloud ====
# 1) Register the application with Nextcloud: https://your.nextcloud/settings/admin/security
# Make sure you capture the application ID as well as generate a secret key.
# 2) Configure the environment variables. This differs slightly
# by installation type, but make sure you have the following:
#- OAUTH2_ENABLED=true
# OAuth2 login style: popup or redirect.
#- OAUTH2_LOGIN_STYLE=redirect
# Application GUID captured during app registration:
#- OAUTH2_CLIENT_ID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx
# Secret key generated during app registration:
#- OAUTH2_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#- OAUTH2_SERVER_URL=https://your-nextcloud.tld
#- OAUTH2_AUTH_ENDPOINT=/index.php/apps/oauth2/authorize
#- OAUTH2_USERINFO_ENDPOINT=/ocs/v2.php/cloud/user?format=json
#- OAUTH2_TOKEN_ENDPOINT=/index.php/apps/oauth2/api/v1/token
# The claim name you want to map to the unique ID field:
#- OAUTH2_ID_MAP=id
# The claim name you want to map to the username field:
#- OAUTH2_USERNAME_MAP=id
# The claim name you want to map to the full name field:
#- OAUTH2_FULLNAME_MAP=display-name
# Tthe claim name you want to map to the email field:
#- OAUTH2_EMAIL_MAP=email
#-----------------------------------------------------------------
# ==== OAUTH2 KEYCLOAK ====
# https://github.com/wekan/wekan/wiki/Keycloak <== MAPPING INFO, REQUIRED
#- OAUTH2_ENABLED=true
@@ -479,18 +509,22 @@ services:
# The limit number of entries (0=unlimited)
#- LDAP_SEARCH_SIZE_LIMIT=0
#
# Enable group filtering
# Enable group filtering. Note the authenticated ldap user must be able to query all relevant group data with own login data from ldap.
#- LDAP_GROUP_FILTER_ENABLE=false
#
# The object class for filtering. Example: group
#- LDAP_GROUP_FILTER_OBJECTCLASS=
#
# The attribute of a group identifying it. Example: cn
#- LDAP_GROUP_FILTER_GROUP_ID_ATTRIBUTE=
#
# The attribute inside a group object listing its members. Example: member
#- LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE=
#
# The format of the value of LDAP_GROUP_FILTER_GROUP_MEMBER_ATTRIBUTE. Example: 'dn' if the users dn ist saved as value into the attribute.
#- LDAP_GROUP_FILTER_GROUP_MEMBER_FORMAT=
#
# The group name (id) that matches all users.
#- LDAP_GROUP_FILTER_GROUP_NAME=
#
# LDAP_UNIQUE_IDENTIFIER_FIELD : This field is sometimes class GUID (Globally Unique Identifier). Example: guid
+4 -4
View File
@@ -451,14 +451,14 @@ FS.HTTP.Handlers.Get = function (ref) {
if(userAgent.indexOf('msie') >= 0 || userAgent.indexOf('trident') >= 0 || userAgent.indexOf('chrome') >= 0) {
ref.filename = encodeURIComponent(ref.filename);
} else if(userAgent.indexOf('firefox') >= 0) {
ref.filename = new Buffer(ref.filename).toString('binary');
ref.filename = Buffer.from(ref.filename).toString('binary');
} else {
/* safari*/
ref.filename = new Buffer(ref.filename).toString('binary');
}
ref.filename = Buffer.from(ref.filename).toString('binary');
}
} catch (ex){
ref.filename = 'tempfix';
}
}
return originalHandler.call(this, ref);
};
// 221
+1 -1
View File
@@ -1,5 +1,5 @@
dependencies:
- name: mongodb-replicaset
version: 3.6.x
version: 3.11.x
repository: "https://kubernetes-charts.storage.googleapis.com/"
condition: mongodb-replicaset.enabled
+1 -1
View File
@@ -75,7 +75,7 @@ else use user-provided URL.
{{- if (index .Values "mongodb-replicaset" "enabled") -}}
{{- $count := (int (index .Values "mongodb-replicaset" "replicas")) -}}
{{- $release := .Release.Name -}}
mongodb://{{- range $v := until $count }}{{ $release }}-mongodb-replicaset-{{ $v }}.{{ $release }}-mongodb-replicaset:27017{{ if ne $v (sub $count 1) }},{{- end -}}{{- end -}}?replicaSet={{ index .Values "mongodb-replicaset" "replicaSetName" }}
mongodb://{{ $release }}-mongodb-replicaset:27017/admin?replicaSet={{ index .Values "mongodb-replicaset" "replicaSetName" }}
{{- else -}}
{{- index .Values "mongodb-replicaset" "url" -}}
{{- end -}}
+1 -1
View File
@@ -35,6 +35,6 @@ spec:
- path: {{ $ingressPath }}
backend:
serviceName: {{ $fullName }}
servicePort: http
servicePort: 80
{{- end }}
{{- end }}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "по подразбиране",
"queue": "Опашка",
"subtask-settings": "Настройки на Подзадачите",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Настройки за Подзадачите за това Табло",
"show-subtasks-field": "Картата може да има подзадачи",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+17 -11
View File
@@ -111,8 +111,8 @@
"restore-board": "Restaura Tauler",
"no-archived-boards": "No hi han Taulers al Arxiu.",
"archives": "Desa",
"template": "Template",
"templates": "Templates",
"template": "Plantilla",
"templates": "Plantilles",
"assign-member": "Assignar membre",
"attached": "adjuntat",
"attachment": "Adjunt",
@@ -131,13 +131,13 @@
"boardChangeTitlePopup-title": "Canvia el nom tauler",
"boardChangeVisibilityPopup-title": "Canvia visibilitat",
"boardChangeWatchPopup-title": "Canvia seguiment",
"boardMenuPopup-title": "Board Settings",
"boardMenuPopup-title": "Configuració del tauler",
"boardChangeViewPopup-title": "Visió del tauler",
"boards": "Taulers",
"board-view": "Visió del tauler",
"board-view-cal": "Calendari",
"board-view-swimlanes": "Carrils de Natació",
"board-view-collapse": "Collapse",
"board-view-collapse": "Contraure",
"board-view-lists": "Llistes",
"bucket-example": "Igual que “Bucket List”, per exemple",
"cancel": "Cancel·la",
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Configuració de targeta",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Configuració de targeta",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -625,7 +626,7 @@
"r-moved-from": "Moved from",
"r-archived": "Moved to Archive",
"r-unarchived": "Restored from Archive",
"r-a-card": "a card",
"r-a-card": "una targeta",
"r-when-a-label-is": "When a label is",
"r-when-the-label": "When the label",
"r-list-name": "list name",
@@ -647,7 +648,7 @@
"r-its-list": "its list",
"r-archive": "Moure al arxiu",
"r-unarchive": "Restore from Archive",
"r-card": "card",
"r-card": "targeta",
"r-add": "Afegeix",
"r-remove": "Remove",
"r-label": "label",
@@ -678,7 +679,7 @@
"r-d-unarchive": "Restore card from Archive",
"r-d-add-label": "Add label",
"r-d-remove-label": "Remove label",
"r-create-card": "Create new card",
"r-create-card": "Crea una targeta nova",
"r-in-list": "in list",
"r-in-swimlane": "in swimlane",
"r-d-add-member": "Add member",
@@ -699,7 +700,7 @@
"r-swimlane-name": "swimlane name",
"r-board-note": "Note: leave a field empty to match every possible value.",
"r-checklist-note": "Note: checklist's items have to be written as comma separated values.",
"r-when-a-card-is-moved": "When a card is moved to another list",
"r-when-a-card-is-moved": "Quan una targeta es mou a una altra llista",
"r-set": "Set",
"r-update": "Update",
"r-datefield": "date field",
@@ -752,6 +753,11 @@
"accounts-allowUserDelete": "Allow users to self delete their account",
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"assignee": "Assignat",
"cardAssigneesPopup-title": "Assignat",
"addmore-detail": "Afegiu una descripció més detallada",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Výchozí",
"queue": "Fronta",
"subtask-settings": "Nastavení podúkolů",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Nastavení podúkolů tabla",
"show-subtasks-field": "Karty mohou mít podúkoly",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Vložit podúkoly do tohoto tabla",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Ukázat předka na minikartě",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+9 -3
View File
@@ -224,7 +224,7 @@
"no-comments": "Keine Kommentare",
"no-comments-desc": "Kann keine Kommentare und Aktivitäten sehen.",
"worker": "Arbeiter",
"worker-desc": "Kann nur Karten verschieben, sich selbst zuweisen und kommentieren.",
"worker-desc": "Kann Karten nur verschieben, sich selbst zuweisen und kommentieren.",
"computer": "Computer",
"confirm-subtask-delete-dialog": "Wollen Sie die Teilaufgabe wirklich löschen?",
"confirm-checklist-delete-dialog": "Wollen Sie die Checkliste wirklich löschen?",
@@ -583,8 +583,9 @@
"default": "Standard",
"queue": "Warteschlange",
"subtask-settings": "Einstellungen für Teilaufgaben",
"card-settings": "Karten-Einstellungen",
"boardSubtaskSettingsPopup-title": "Boardeinstellungen für Teilaufgaben",
"show-subtasks-field": "Karten können Teilaufgaben haben",
"boardCardSettingsPopup-title": "Karten-Einstellungen",
"deposit-subtasks-board": "Teilaufgaben in diesem Board ablegen:",
"deposit-subtasks-list": "Zielliste für hier abgelegte Teilaufgaben:",
"show-parent-in-minicard": "Übergeordnetes Element auf Minikarte anzeigen:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Labeltext auf Minikarte ausblenden",
"show-desktop-drag-handles": "Desktop-Ziehpunkte anzeigen",
"assignee": "Zugewiesen",
"cardAssigneesPopup-title": "Zugewiesen"
"cardAssigneesPopup-title": "Zugewiesen",
"addmore-detail": "Eine detailliertere Beschreibung hinzufügen",
"show-on-card": "Zeige auf Karte",
"new": "Neu",
"editUserPopup-title": "Benutzer ändern",
"newUserPopup-title": "Neuer Benutzer"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -756,5 +757,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+36 -30
View File
@@ -128,7 +128,7 @@
"board-private-info": "This board will be <strong>private</strong>.",
"board-public-info": "This board will be <strong>public</strong>.",
"boardChangeColorPopup-title": "Change Board Background",
"boardChangeTitlePopup-title": "Rename Board",
"boardChangeTitlePopup-title": "Renomi tavolon",
"boardChangeVisibilityPopup-title": "Change Visibility",
"boardChangeWatchPopup-title": "Change Watch",
"boardMenuPopup-title": "Board Settings",
@@ -250,8 +250,8 @@
"custom-field-dropdown-options": "List Options",
"custom-field-dropdown-options-placeholder": "Press enter to add more options",
"custom-field-dropdown-unknown": "(unknown)",
"custom-field-number": "Number",
"custom-field-text": "Text",
"custom-field-number": "Nombro",
"custom-field-text": "Teksto",
"custom-fields": "Custom Fields",
"date": "Dato",
"decline": "Decline",
@@ -327,13 +327,13 @@
"filter-to-selection": "Filter to selection",
"advanced-filter-label": "Advanced Filter",
"advanced-filter-description": "Advanced Filter allows to write a string containing following operators: == != <= >= && || ( ) A space is used as a separator between the Operators. You can filter for all Custom Fields by typing their names and values. For Example: Field1 == Value1. Note: If fields or values contains spaces, you need to encapsulate them into single quotes. For Example: 'Field 1' == 'Value 1'. For single control characters (' \\/) to be skipped, you can use \\. For example: Field1 == I\\'m. Also you can combine multiple conditions. For Example: F1 == V1 || F1 == V2. Normally all operators are interpreted from left to right. You can change the order by placing brackets. For Example: F1 == V1 && ( F2 == V2 || F2 == V3 ). Also you can search text fields using regex: F1 == /Tes.*/i",
"fullname": "Full Name",
"fullname": "Plena nomo",
"header-logo-title": "Go back to your boards page.",
"hide-system-messages": "Hide system messages",
"headerBarCreateBoardPopup-title": "Krei tavolon",
"home": "Hejmo",
"import": "Importi",
"link": "Link",
"link": "Ligilo",
"import-board": "import board",
"import-board-c": "Import board",
"import-board-title-trello": "Import board from Trello",
@@ -435,11 +435,11 @@
"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": "Renomi",
"rename-board": "Rename Board",
"rename-board": "Renomi tavolon",
"restore": "Forigi",
"save": "Savi",
"search": "Serĉi",
"rules": "Rules",
"rules": "Reguloj",
"search-cards": "Search from card/list titles and descriptions on this board",
"search-example": "Text to search for?",
"select-color": "Select Color",
@@ -474,7 +474,7 @@
"title": "Titolo",
"tracking": "Tracking",
"tracking-info": "You will be notified of any changes to those cards you are involved as creator or member.",
"type": "Type",
"type": "Tipo",
"unassign-member": "Unassign member",
"unsaved-description": "You have an unsaved description.",
"unwatch": "Unwatch",
@@ -555,8 +555,8 @@
"show-field-on-card": "Show this field on card",
"automatically-field-on-card": "Auto create field to all cards",
"showLabel-field-on-card": "Show field label on minicard",
"yes": "Yes",
"no": "No",
"yes": "Jes",
"no": "Ne",
"accounts": "Accounts",
"accounts-allowEmailChange": "Allow Email Change",
"accounts-allowUserNameChange": "Allow Username Change",
@@ -580,11 +580,12 @@
"boardDeletePopup-title": "Delete Board?",
"delete-board": "Delete Board",
"default-subtasks-board": "Subtasks for __board__ board",
"default": "Default",
"default": "Defaŭlto",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -604,13 +605,13 @@
"activity-delete-attach-card": "deleted an attachment",
"activity-set-customfield": "set custom field '%s' to '%s' in %s",
"activity-unset-customfield": "unset custom field '%s' in %s",
"r-rule": "Rule",
"r-rule": "Regulo",
"r-add-trigger": "Add trigger",
"r-add-action": "Add action",
"r-board-rules": "Board rules",
"r-add-rule": "Add rule",
"r-add-rule": "Aldoni regulon",
"r-view-rule": "View rule",
"r-delete-rule": "Delete rule",
"r-delete-rule": "Forigi regulon",
"r-new-rule-name": "New rule title",
"r-no-rules": "No rules",
"r-when-a-card": "When a card",
@@ -619,7 +620,7 @@
"r-added-to": "added to",
"r-removed-from": "Removed from",
"r-the-board": "the board",
"r-list": "list",
"r-list": "listo",
"set-filter": "Set Filter",
"r-moved-to": "Moved to",
"r-moved-from": "Moved from",
@@ -631,7 +632,7 @@
"r-list-name": "list name",
"r-when-a-member": "When a member is",
"r-when-the-member": "When the member",
"r-name": "name",
"r-name": "nomo",
"r-when-a-attach": "When an attachment",
"r-when-a-checklist": "When a checklist is",
"r-when-the-checklist": "When the checklist",
@@ -649,12 +650,12 @@
"r-unarchive": "Restore from Archive",
"r-card": "card",
"r-add": "Aldoni",
"r-remove": "Remove",
"r-remove": "Forigi",
"r-label": "label",
"r-member": "member",
"r-member": "membro",
"r-remove-all": "Remove all members from the card",
"r-set-color": "Set color to",
"r-checklist": "checklist",
"r-checklist": "kontrololisto",
"r-check-all": "Check all",
"r-uncheck-all": "Uncheck all",
"r-items-check": "items of checklist",
@@ -664,7 +665,7 @@
"r-of-checklist": "of checklist",
"r-send-email": "Send an email",
"r-to": "to",
"r-subject": "subject",
"r-subject": "temo",
"r-rule-details": "Rule details",
"r-d-move-to-top-gen": "Move card to top of its list",
"r-d-move-to-top-spec": "Move card to top of list",
@@ -672,8 +673,8 @@
"r-d-move-to-bottom-spec": "Move card to bottom of list",
"r-d-send-email": "Send email",
"r-d-send-email-to": "to",
"r-d-send-email-subject": "subject",
"r-d-send-email-message": "message",
"r-d-send-email-subject": "temo",
"r-d-send-email-message": "mesaĝo",
"r-d-archive": "Move card to Archive",
"r-d-unarchive": "Restore card from Archive",
"r-d-add-label": "Add label",
@@ -681,18 +682,18 @@
"r-create-card": "Create new card",
"r-in-list": "in list",
"r-in-swimlane": "in swimlane",
"r-d-add-member": "Add member",
"r-d-remove-member": "Remove member",
"r-d-remove-all-member": "Remove all member",
"r-d-add-member": "Aldoni membron",
"r-d-remove-member": "Forigi membron",
"r-d-remove-all-member": "Forigi ĉiujn membrojn",
"r-d-check-all": "Check all items of a list",
"r-d-uncheck-all": "Uncheck all items of a list",
"r-d-check-one": "Check item",
"r-d-uncheck-one": "Uncheck item",
"r-d-check-of-list": "of checklist",
"r-d-add-checklist": "Add checklist",
"r-d-remove-checklist": "Remove checklist",
"r-d-add-checklist": "Aldoni kontrololiston",
"r-d-remove-checklist": "Forigi kontrololiston",
"r-by": "by",
"r-add-checklist": "Add checklist",
"r-add-checklist": "Aldoni kontrololiston",
"r-with-items": "with items",
"r-items-list": "item1,item2,item3",
"r-add-swimlane": "Add swimlane",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+10 -4
View File
@@ -223,8 +223,8 @@
"comment-only-desc": "Solo puedes comentar en las tarjetas.",
"no-comments": "No hay comentarios",
"no-comments-desc": "No se pueden mostrar comentarios ni actividades.",
"worker": "Worker",
"worker-desc": "Can only move cards, assign itself to card and comment.",
"worker": "Trabajador",
"worker-desc": "Solo puede mover tarjetas, asignarse a la tarjeta y comentar.",
"computer": "el ordenador",
"confirm-subtask-delete-dialog": "¿Seguro que quieres eliminar la subtarea?",
"confirm-checklist-delete-dialog": "¿Seguro que quieres eliminar la lista de verificación?",
@@ -583,8 +583,9 @@
"default": "Por defecto",
"queue": "Cola",
"subtask-settings": "Preferencias de las subtareas",
"card-settings": "Preferencias de la tarjeta",
"boardSubtaskSettingsPopup-title": "Preferencias de las subtareas del tablero",
"show-subtasks-field": "Las tarjetas pueden tener subtareas",
"boardCardSettingsPopup-title": "Preferencias de la tarjeta",
"deposit-subtasks-board": "Depositar subtareas en este tablero:",
"deposit-subtasks-list": "Lista de destino para subtareas depositadas aquí:",
"show-parent-in-minicard": "Mostrar el padre en una minitarjeta:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Ocultar el texto de la etiqueta de la minitarjeta",
"show-desktop-drag-handles": "Mostrar los controles de arrastre del escritorio",
"assignee": "Asignado",
"cardAssigneesPopup-title": "Asignado"
"cardAssigneesPopup-title": "Asignado",
"addmore-detail": "Añadir una descripción detallada",
"show-on-card": "Mostrar en la tarjeta",
"new": "Nuevo",
"editUserPopup-title": "Editar el usuario",
"newUserPopup-title": "Nuevo usuario"
}
+8 -2
View File
@@ -583,8 +583,9 @@
"default": "Default",
"queue": "Queue",
"subtask-settings": "Subtasks Settings",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "Board Subtasks Settings",
"show-subtasks-field": "Cards can have subtasks",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "Deposit subtasks to this board:",
"deposit-subtasks-list": "Landing list for subtasks deposited here:",
"show-parent-in-minicard": "Show parent in minicard:",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+30 -24
View File
@@ -137,7 +137,7 @@
"board-view": "نمایش برد",
"board-view-cal": "تقویم",
"board-view-swimlanes": "Swimlanes",
"board-view-collapse": "Collapse",
"board-view-collapse": "بستن",
"board-view-lists": "فهرست‌ها",
"bucket-example": "برای مثال چیزی شبیه \"لیست سبدها\"",
"cancel": "انصراف",
@@ -223,8 +223,8 @@
"comment-only-desc": "فقط می‌تواند روی کارت‌ها نظر دهد.",
"no-comments": "هیچ کامنتی موجود نیست",
"no-comments-desc": "نظرات و فعالیت ها را نمی توان دید.",
"worker": "Worker",
"worker-desc": "Can only move cards, assign itself to card and comment.",
"worker": "کارگر",
"worker-desc": "تنها میتوانید کارت ها را جابجا کنید، این را به یک کارت اضافه کنید.",
"computer": "رایانه",
"confirm-subtask-delete-dialog": "از حذف این زیر وظیفه اطمینان دارید؟",
"confirm-checklist-delete-dialog": "مطمئنا چک لیست پاک شود؟",
@@ -306,22 +306,22 @@
"export-board": "انتقال به بیرون برد",
"sort": "مرتب سازی",
"sort-desc": "برای مرتب سازی لیست کلیک کنید",
"list-sort-by": "Sort the List By:",
"list-label-modifiedAt": "Last Access Time",
"list-label-title": "Name of the List",
"list-label-sort": "Your Manual Order",
"list-sort-by": "مرتب سازی لیست بر اساس:",
"list-label-modifiedAt": "زمان دسترسی قبلی",
"list-label-title": "نام لیست",
"list-label-sort": "دلخواه شما",
"list-label-short-modifiedAt": "(L)",
"list-label-short-title": "(N)",
"list-label-short-sort": "(M)",
"filter": "صافی ـ فیلتر ـ",
"filter-cards": "Filter Cards or Lists",
"list-filter-label": "Filter List by Title",
"filter-cards": "فیلتر کارت‌ها یا لیست‌ها",
"list-filter-label": "فیلتر لیست بر اساس عنوان",
"filter-clear": "حذف صافی ـ فیلتر ـ",
"filter-no-label": "بدون برچسب",
"filter-no-member": "بدون عضو",
"filter-no-custom-fields": "هیچ فیلدشخصی ای وجود ندارد",
"filter-show-archive": "Show archived lists",
"filter-hide-empty": "Hide empty lists",
"filter-show-archive": "نمایش لیست‌های آرشیو شده",
"filter-hide-empty": "مخفی کردن لیست‌های خالی",
"filter-on": "صافی ـ فیلتر ـ فعال است",
"filter-on-desc": "شما درحال صافی ـ فیلتر ـ کارت‌های این برد هستید. برای ویرایش فیلتر کلیک نمایید.",
"filter-to-selection": "صافی ـ فیلتر ـ برای موارد انتخابی",
@@ -440,7 +440,7 @@
"save": "ذخیره",
"search": "جستجو",
"rules": "قوانین",
"search-cards": "Search from card/list titles and descriptions on this board",
"search-cards": "جتستجو از عنوان لیست/کارت ها و توضیحات در این برد",
"search-example": "متن مورد جستجو؟",
"select-color": "انتخاب رنگ",
"set-wip-limit-value": "تعیین بیشینه تعداد وظایف در این فهرست",
@@ -524,21 +524,21 @@
"email-smtp-test-text": "با موفقیت، یک رایانامه را فرستادید",
"error-invitation-code-not-exist": "چنین کد دعوتی یافت نشد",
"error-notAuthorized": "شما مجاز به دیدن این صفحه نیستید.",
"webhook-title": "Webhook Name",
"webhook-token": "Token (Optional for Authentication)",
"webhook-title": "نام وب‌هوک",
"webhook-token": "توکن",
"outgoing-webhooks": "Outgoing Webhooks",
"bidirectional-webhooks": "Two-Way Webhooks",
"bidirectional-webhooks": "وب‌هوک two-way",
"outgoingWebhooksPopup-title": "Outgoing Webhooks",
"boardCardTitlePopup-title": "فیلتر موضوع کارت",
"disable-webhook": "Disable This Webhook",
"global-webhook": "Global Webhooks",
"disable-webhook": "حذف این وب‌هوک",
"global-webhook": "وب‌هوک‌های سراسری",
"new-outgoing-webhook": "New Outgoing Webhook",
"no-name": "(ناشناخته)",
"Node_version": "نسخه Node",
"Meteor_version": "Meteor version",
"MongoDB_version": "MongoDB version",
"MongoDB_storage_engine": "MongoDB storage engine",
"MongoDB_Oplog_enabled": "MongoDB Oplog enabled",
"MongoDB_version": "ورژن MongoDB",
"MongoDB_storage_engine": "موتور ذخیره سازی MongoDB",
"MongoDB_Oplog_enabled": "MongoDB Oplog فعال",
"OS_Arch": "OS Arch",
"OS_Cpus": "OS CPU Count",
"OS_Freemem": "OS Free Memory",
@@ -583,8 +583,9 @@
"default": "پیش‌فرض",
"queue": "صف",
"subtask-settings": "تنظیمات ریزکارها",
"card-settings": "Card Settings",
"boardSubtaskSettingsPopup-title": "تنظیمات ریزکار های برد",
"show-subtasks-field": "کارت می تواند ریزکار داشته باشد",
"boardCardSettingsPopup-title": "Card Settings",
"deposit-subtasks-board": "افزودن ریزکار به برد:",
"deposit-subtasks-list": "لیست برای ریزکار های افزوده شده",
"show-parent-in-minicard": "نمایش خانواده در ریز کارت",
@@ -701,8 +702,8 @@
"r-checklist-note": "نکته: چک‌لیست‌ها باید توسط کاما از یک‌دیگر جدا شوند.",
"r-when-a-card-is-moved": "زمانی که یک کارت به لیست دیگری منتقل شد",
"r-set": "Set",
"r-update": "Update",
"r-datefield": "date field",
"r-update": "به روز رسانی",
"r-datefield": "تاریخ",
"r-df-start-at": "شروع",
"r-df-due-at": "ناشی از",
"r-df-end-at": "پایان",
@@ -753,5 +754,10 @@
"hide-minicard-label-text": "Hide minicard label text",
"show-desktop-drag-handles": "Show desktop drag handles",
"assignee": "Assignee",
"cardAssigneesPopup-title": "Assignee"
"cardAssigneesPopup-title": "Assignee",
"addmore-detail": "Add a more detailed description",
"show-on-card": "Show on Card",
"new": "New",
"editUserPopup-title": "Edit User",
"newUserPopup-title": "New User"
}
+40 -34
View File
@@ -4,10 +4,10 @@
"act-addAttachment": "lisätty liite __attachment__ kortille __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-deleteAttachment": "poistettu liite __attachment__ kortilla __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-addSubtask": "lisätty alitehtävä __subtask__ kortille __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-addLabel": "Lisätty tunniste __label__ kortille __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-addedLabel": "Lisätty tunniste __label__ kortille __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-removeLabel": "Poistettu tunniste __label__ kortilta __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-removedLabel": "Poistettu tunniste __label__ kortilta __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-addLabel": "Lisätty nimilappu __label__ kortille __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-addedLabel": "Lisätty nimilappu __label__ kortille __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-removeLabel": "Poistettu nimilappu __label__ kortilta __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-removedLabel": "Poistettu nimilappu __label__ kortilta __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-addChecklist": "lisätty tarkistuslista __checklist__ kortille __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-addChecklistItem": "lisätty tarkistuslistan kohta __checklistItem__ tarkistuslistalle __checklist__ kortilla __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
"act-removeChecklist": "poistettu tarkistuslista __checklist__ kortilta __card__ listalla __list__ swimlanella __swimlane__ taululla __board__",
@@ -83,7 +83,7 @@
"add-checklist": "Lisää tarkistuslista",
"add-checklist-item": "Lisää kohta tarkistuslistaan",
"add-cover": "Lisää kansi",
"add-label": "Lisää tunniste",
"add-label": "Lisää nimilappu",
"add-list": "Lisää lista",
"add-members": "Lisää jäseniä",
"added": "Lisätty",
@@ -152,9 +152,9 @@
"card-spent": "Käytetty aika",
"card-edit-attachments": "Muokkaa liitetiedostoja",
"card-edit-custom-fields": "Muokkaa mukautettuja kenttiä",
"card-edit-labels": "Muokkaa tunnisteita",
"card-edit-labels": "Muokkaa nimilappuja",
"card-edit-members": "Muokkaa jäseniä",
"card-labels-title": "Muokkaa kortin tunnisteita.",
"card-labels-title": "Muokkaa kortin nimilappuja.",
"card-members-title": "Lisää tai poista taulun jäseniä tältä kortilta.",
"card-start": "Alkaa",
"card-start-on": "Alkaa",
@@ -163,7 +163,7 @@
"cardCustomFieldsPopup-title": "Muokkaa mukautettuja kenttiä",
"cardDeletePopup-title": "Poista kortti?",
"cardDetailsActionsPopup-title": "Korttitoimet",
"cardLabelsPopup-title": "Tunnisteet",
"cardLabelsPopup-title": "Nimilaput",
"cardMembersPopup-title": "Jäsenet",
"cardMorePopup-title": "Lisää",
"cardTemplatePopup-title": "Luo malli",
@@ -224,7 +224,7 @@
"no-comments": "Ei kommentteja",
"no-comments-desc": "Ei voi nähdä kommentteja ja toimintaa.",
"worker": "Työntekijä",
"worker-desc": "Voi vain siirtää kortteja, valtuuttaa itsensä kortille ja kommentoida.",
"worker-desc": "Voi vain siirtää kortteja, ilmoittautua kortin käsittelijäksi ja kommentoida.",
"computer": "Tietokone",
"confirm-subtask-delete-dialog": "Haluatko varmasti poistaa alitehtävän?",
"confirm-checklist-delete-dialog": "Haluatko varmasti poistaa tarkistuslistan?",
@@ -238,7 +238,7 @@
"create": "Luo",
"createBoardPopup-title": "Luo taulu",
"chooseBoardSourcePopup-title": "Tuo taulu",
"createLabelPopup-title": "Luo tunniste",
"createLabelPopup-title": "Luo nimilappu",
"createCustomField": "Luo kenttä",
"createCustomFieldPopup-title": "Luo kenttä",
"current": "nykyinen",
@@ -258,9 +258,9 @@
"default-avatar": "Oletusprofiilikuva",
"delete": "Poista",
"deleteCustomFieldPopup-title": "Poista mukautettu kenttä?",
"deleteLabelPopup-title": "Poista tunniste?",
"deleteLabelPopup-title": "Poista nimilappu?",
"description": "Kuvaus",
"disambiguateMultiLabelPopup-title": "Yksikäsitteistä tunnistetoiminta",
"disambiguateMultiLabelPopup-title": "Yksikäsitteistä nimilapputoiminta",
"disambiguateMultiMemberPopup-title": "Yksikäsitteistä jäsentoiminta",
"discard": "Hylkää",
"done": "Valmis",
@@ -274,7 +274,7 @@
"editCardDueDatePopup-title": "Muokkaa eräpäivää",
"editCustomFieldPopup-title": "Muokkaa kenttää",
"editCardSpentTimePopup-title": "Muuta käytettyä aikaa",
"editLabelPopup-title": "Muokkaa tunnistetta",
"editLabelPopup-title": "Muokkaa nimilappua",
"editNotificationPopup-title": "Muokkaa ilmoituksia",
"editProfilePopup-title": "Muokkaa profiilia",
"email": "Sähköposti",
@@ -317,7 +317,7 @@
"filter-cards": "Suodata kortit tai listat",
"list-filter-label": "Suodata listat otsikon mukaan",
"filter-clear": "Poista suodatin",
"filter-no-label": "Ei tunnistetta",
"filter-no-label": "Ei nimilappua",
"filter-no-member": "Ei jäseniä",
"filter-no-custom-fields": "Ei mukautettuja kenttiä",
"filter-show-archive": "Näytä arkistoidut listat",
@@ -359,10 +359,10 @@
"joined": "liittyi",
"just-invited": "Sinut on juuri kutsuttu tälle taululle",
"keyboard-shortcuts": "Pikanäppäimet",
"label-create": "Luo tunniste",
"label-default": "%s tunniste (oletus)",
"label-delete-pop": "Tätä ei voi peruuttaa. Tämä poistaa tämän tunnisteen kaikista korteista ja tuhoaa sen historian.",
"labels": "Tunnisteet",
"label-create": "Luo nimilappu",
"label-default": "%s nimilappu (oletus)",
"label-delete-pop": "Tätä ei voi peruuttaa. Tämä poistaa tämän nimilapun kaikista korteista ja tuhoaa sen historian.",
"labels": "Nimilaput",
"language": "Kieli",
"last-admin-desc": "Et voi vaihtaa rooleja koska täytyy olla olemassa ainakin yksi ylläpitäjä.",
"leave-board": "Jää pois taululta",
@@ -428,7 +428,7 @@
"quick-access-description": "Merkkaa taulu tähdellä lisätäksesi pikavalinta tähän palkkiin.",
"remove-cover": "Poista kansi",
"remove-from-board": "Poista taululta",
"remove-label": "Poista tunniste",
"remove-label": "Poista nimilappu",
"listDeletePopup-title": "Poista lista?",
"remove-member": "Poista jäsen",
"remove-member-from-card": "Poista kortilta",
@@ -554,7 +554,7 @@
"seconds": "sekuntia",
"show-field-on-card": "Näytä tämä kenttä kortilla",
"automatically-field-on-card": "Luo kenttä automaattisesti kaikille korteille",
"showLabel-field-on-card": "Näytä kentän tunniste minikortilla",
"showLabel-field-on-card": "Näytä kentän nimilappu minikortilla",
"yes": "Kyllä",
"no": "Ei",
"accounts": "Tilit",
@@ -576,15 +576,16 @@
"assigned-by": "Tehtävänantaja",
"requested-by": "Pyytäjä",
"board-delete-notice": "Poistaminen on lopullista. Menetät kaikki listat, kortit ja toimet tällä taululla.",
"delete-board-confirm-popup": "Kaikki listat, kortit, tunnisteet ja toimet poistetaan ja et pysty palauttamaan taulun sisältöä. Tätä ei voi peruuttaa.",
"delete-board-confirm-popup": "Kaikki listat, kortit, nimilaput ja toimet poistetaan ja et pysty palauttamaan taulun sisältöä. Tätä ei voi peruuttaa.",
"boardDeletePopup-title": "Poista taulu?",
"delete-board": "Poista taulu",
"default-subtasks-board": "Alitehtävät taululle __board__",
"default": "Oletus",
"queue": "Jono",
"subtask-settings": "Alitehtävä-asetukset",
"card-settings": "Kortin asetukset",
"boardSubtaskSettingsPopup-title": "Taulualitehtävien asetukset",
"show-subtasks-field": "Korteilla voi olla alitehtäviä",
"boardCardSettingsPopup-title": "Kortin asetukset",
"deposit-subtasks-board": "Talleta alitehtävät tälle taululle:",
"deposit-subtasks-list": "Laskeutumislista alatehtäville tallennettu tänne:",
"show-parent-in-minicard": "Näytä ylätehtävä minikortilla:",
@@ -596,11 +597,11 @@
"parent-card": "Ylätehtäväkortti",
"source-board": "Lähdetaulu",
"no-parent": "Älä näytä ylätehtävää",
"activity-added-label": "lisätty tunniste '%s' kohteeseen %s",
"activity-removed-label": "poistettu tunniste '%s' kohteesta %s",
"activity-added-label": "lisätty nimilappu '%s' kohteeseen %s",
"activity-removed-label": "poistettu nimilappu '%s' kohteesta %s",
"activity-delete-attach": "poistettu liitetiedosto kohteesta %s",
"activity-added-label-card": "lisätty tunniste '%s'",
"activity-removed-label-card": "poistettu tunniste '%s'",
"activity-added-label-card": "lisätty nimilappu '%s'",
"activity-removed-label-card": "poistettu nimilappu '%s'",
"activity-delete-attach-card": "poistettu liitetiedosto",
"activity-set-customfield": "asetettu mukautettu kentän '%s' sisällöksi '%s' kortilla %s",
"activity-unset-customfield": "poistettu mukautettu kenttä '%s' kortilla %s",
@@ -626,8 +627,8 @@
"r-archived": "Siirretty Arkistoon",
"r-unarchived": "Palautettu Arkistosta",
"r-a-card": "kortti",
"r-when-a-label-is": "Kun tunniste on",
"r-when-the-label": "Kun tunniste on",
"r-when-a-label-is": "Kun nimilappu on",
"r-when-the-label": "Kun nimilappu on",
"r-list-name": "listan nimi",
"r-when-a-member": "Kun jäsen on",
"r-when-the-member": "Kun käyttäjä",
@@ -650,7 +651,7 @@
"r-card": "kortti",
"r-add": "Lisää",
"r-remove": "Poista",
"r-label": "tunniste",
"r-label": "nimilappu",
"r-member": "jäsen",
"r-remove-all": "Poista kaikki jäsenet kortilta",
"r-set-color": "Aseta väriksi",
@@ -676,8 +677,8 @@
"r-d-send-email-message": "viesti",
"r-d-archive": "Siirrä kortti Arkistoon",
"r-d-unarchive": "Palauta kortti Arkistosta",
"r-d-add-label": "Lisää tunniste",
"r-d-remove-label": "Poista tunniste",
"r-d-add-label": "Lisää nimilappu",
"r-d-remove-label": "Poista nimilappu",
"r-create-card": "Luo uusi kortti",
"r-in-list": "listassa",
"r-in-swimlane": "swimlanessa",
@@ -750,8 +751,13 @@
"act-atUserComment": "Sinut mainittiin [__board__] __list__/__card__",
"delete-user-confirm-popup": "Haluatko varmasti poistaa tämän käyttäjätilin? Tätä ei voi peruuttaa.",
"accounts-allowUserDelete": "Salli käyttäjien poistaa tilinsä itse",
"hide-minicard-label-text": "Piilota minikortin tunniste teksti",
"hide-minicard-label-text": "Piilota minikortin nimilappu teksti",
"show-desktop-drag-handles": "Näytä työpöydän vedon kahvat",
"assignee": "Valtuutettu",
"cardAssigneesPopup-title": "Valtuutettu"
"assignee": "Käsittelijä",
"cardAssigneesPopup-title": "Käsittelijä",
"addmore-detail": "Lisää tarkempi kuvaus",
"show-on-card": "Näytä kortilla",
"new": "Uusi",
"editUserPopup-title": "Muokkaa käyttäjää",
"newUserPopup-title": "Uusi käyttäjä"
}
Loaded 100 of 391 files, more files were not shown because too many files have changed in this diff. Show more