Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1a9e30956 | ||
|
|
4b202aaa08 | ||
|
|
5ef1a92366 | ||
|
|
1a836969e1 | ||
|
|
079b3da6ef | ||
|
|
13563e20e7 | ||
|
|
4f1330c777 | ||
|
|
94b558cf43 | ||
|
|
e22e24d917 | ||
|
|
55545a52ca | ||
|
|
e0cb5a0c7a | ||
|
|
efd3faf9ef | ||
|
|
2fd577d2c0 | ||
|
|
659e8418f7 | ||
|
|
18610d2fe6 | ||
|
|
1389ed0fce | ||
|
|
c39364af83 | ||
|
|
3e817257ef | ||
|
|
c5f782976b | ||
|
|
b841f798f8 | ||
|
|
35ae07e2a6 | ||
|
|
a68ecc079d | ||
|
|
b2d546579c | ||
|
|
4bc6ad3b4c | ||
|
|
7f558fede0 | ||
|
|
544bc1d6ad | ||
|
|
89d25f52fa | ||
|
|
2fd71d5be8 | ||
|
|
dc643f3d43 | ||
|
|
4469b12203 | ||
|
|
9cacaafb8f | ||
|
|
269382869e | ||
|
|
8648e32108 | ||
|
|
edf52bc438 | ||
|
|
58ce859368 | ||
|
|
e23aacedb3 | ||
|
|
7f02990626 | ||
|
|
c86155f1aa | ||
|
|
3fae5355d4 | ||
|
|
20e88159ae | ||
|
|
a81921382a | ||
|
|
8f28a409c7 | ||
|
|
7566f05b97 | ||
|
|
268d7fcb32 | ||
|
|
70edd44a01 | ||
|
|
6db717b9b3 | ||
|
|
f389fd95fb | ||
|
|
363bcab98b | ||
|
|
c4a7e53661 | ||
|
|
c6afe8d007 | ||
|
|
93eccffd44 | ||
|
|
fe66169415 | ||
|
|
6ced47aa8c | ||
|
|
e661d03e8d | ||
|
|
f09219cbfd | ||
|
|
7c25542976 | ||
|
|
121686a928 | ||
|
|
3563e0145d | ||
|
|
c630714245 | ||
|
|
5ebb47cb0e | ||
|
|
115701fa65 | ||
|
|
a182dde11f | ||
|
|
1e20e2601f | ||
|
|
4d066b1f30 | ||
|
|
7856692946 | ||
|
|
7e7c8fd001 | ||
|
|
feb602ecd4 | ||
|
|
6ed26e373a | ||
|
|
4fba0fb1a1 | ||
|
|
968ae122b0 | ||
|
|
06c32cb497 | ||
|
|
d2a4925cae | ||
|
|
9c97c301bd | ||
|
|
d4a91d30ff | ||
|
|
6a56a495ad | ||
|
|
3330aff12c | ||
|
|
43bdd56a41 | ||
|
|
a602f4b9ec | ||
|
|
053e47173f | ||
|
|
fe285c62e1 | ||
|
|
a3d44b0774 | ||
|
|
0e79bd25ed | ||
|
|
71f7a680b6 | ||
|
|
411b8d3f56 | ||
|
|
2bbc312ad0 | ||
|
|
14de17b667 | ||
|
|
9083c341c1 | ||
|
|
e0930c32a6 | ||
|
|
f438d04c42 | ||
|
|
d798f6e3ef | ||
|
|
c96ca4e875 | ||
|
|
8a71fe39c5 | ||
|
|
e4ec5c38b7 | ||
|
|
b849d4c2ba | ||
|
|
db9c895da8 | ||
|
|
a938c386cf | ||
|
|
a376fb2fab | ||
|
|
06c2ff0904 | ||
|
|
76e832c3a6 | ||
|
|
b7fd79aef0 | ||
|
|
28a9ab7131 | ||
|
|
8e0b1cc056 | ||
|
|
b424b34cab | ||
|
|
45a83da7be | ||
|
|
6e86292b99 | ||
|
|
e47ff25d45 | ||
|
|
89fe030fa3 | ||
|
|
1e0b53eb9b | ||
|
|
a5f2ae497a | ||
|
|
6441ebec83 | ||
|
|
7c609d402a | ||
|
|
e34f240c35 | ||
|
|
8ea86ae0b9 | ||
|
|
f4d0791c6c | ||
|
|
fcb1def355 | ||
|
|
56667112d7 | ||
|
|
d0003a334f | ||
|
|
35427063ff | ||
|
|
f5182e6328 | ||
|
|
0c911694c8 | ||
|
|
2eed1ca0eb | ||
|
|
99e5293ef5 | ||
|
|
c60a092fc0 | ||
|
|
b33f44e23f | ||
|
|
a65e39563b | ||
|
|
703efa5216 | ||
|
|
6b902bdb36 | ||
|
|
d599fc12ff | ||
|
|
e7d2343593 | ||
|
|
c8ec5d5f62 | ||
|
|
033d671047 | ||
|
|
6ef3f8a1c8 | ||
|
|
3546d7aa02 | ||
|
|
9819c9f801 | ||
|
|
7fa9603f9d | ||
|
|
c41c1e5293 | ||
|
|
29d62440a5 | ||
|
|
5bd0459cc2 | ||
|
|
8b118c1939 | ||
|
|
d7a0ab3c15 | ||
|
|
8609e0e82d | ||
|
|
b9099a8b7e | ||
|
|
87a81789d1 | ||
|
|
12ab8fac5d | ||
|
|
10b09b2da1 | ||
|
|
7c5f4a3849 | ||
|
|
ec71849d84 | ||
|
|
482682e500 | ||
|
|
3a6303e5c2 | ||
|
|
81635d9217 | ||
|
|
e8f89a8c4c | ||
|
|
393482315f | ||
|
|
cb67af02d7 | ||
|
|
a064b8d643 | ||
|
|
556621e60e | ||
|
|
0f95a513bf | ||
|
|
dce624486f | ||
|
|
64e76af8de | ||
|
|
acf2ede94e | ||
|
|
b3a7c4dc88 | ||
|
|
8d83c641e3 | ||
|
|
c1aa0047c6 | ||
|
|
7af4fe75ba | ||
|
|
ea6baa5c2b | ||
|
|
8183b7bdaa | ||
|
|
51514ae22c | ||
|
|
14b8610837 | ||
|
|
369a29707b | ||
|
|
ae240af323 | ||
|
|
2a715ec6c6 | ||
|
|
fca4cdcebf | ||
|
|
49bbfef3ec | ||
|
|
f968109e73 | ||
|
|
c0349c1ff9 | ||
|
|
e009bcf06e | ||
|
|
8aae812aa4 | ||
|
|
0b8f1cabef | ||
|
|
e80d3ac79a | ||
|
|
ed9ba169fe | ||
|
|
4720a6fbc3 | ||
|
|
ad45f82e27 | ||
|
|
2174936613 | ||
|
|
a6d7388919 | ||
|
|
f4e2bc0d32 | ||
|
|
2f13141907 | ||
|
|
365eded5c4 | ||
|
|
ed365c3d41 | ||
|
|
e16c65babc | ||
|
|
6e0fa78022 | ||
|
|
06fb201d80 | ||
|
|
62e5622523 | ||
|
|
2b26bbe78a | ||
|
|
a6d702d1ab | ||
|
|
aaecac0912 | ||
|
|
08b35e1fe7 | ||
|
|
78cf4618d5 | ||
|
|
9d13001b90 | ||
|
|
965c3fb07f | ||
|
|
c43366b7ac | ||
|
|
3a50556656 | ||
|
|
b26e65c70c | ||
|
|
75073bbdb2 | ||
|
|
88573ad2cd | ||
|
|
058180687a | ||
|
|
897506eeac | ||
|
|
aac7c380c8 | ||
|
|
fc35c234a7 | ||
|
|
0d3002f69d | ||
|
|
7d920e938b | ||
|
|
26b521e86e | ||
|
|
83b042701f | ||
|
|
21b75edcdd | ||
|
|
493d71a198 | ||
|
|
27f9c6b925 | ||
|
|
fa07dcf422 | ||
|
|
2b9540ce02 | ||
|
|
906c25d5e8 |
No files matched your search
File renamed without changes.
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
FROM gitpod/workspace-mongodb
|
||||
|
||||
USER gitpod
|
||||
|
||||
# Install custom tools, runtime, etc. using apt-get
|
||||
# For example, the command below would install "bastet" - a command line tetris clone:
|
||||
#
|
||||
# RUN sudo apt-get -q update && # sudo apt-get install -yq bastet && # sudo rm -rf /var/lib/apt/lists/*
|
||||
#
|
||||
# More information: https://www.gitpod.io/docs/config-docker/
|
||||
@@ -0,0 +1,4 @@
|
||||
tasks:
|
||||
- init: npm install
|
||||
image:
|
||||
file: .gitpod.Dockerfile
|
||||
+6
-5
@@ -23,7 +23,7 @@ dburles:collection-helpers
|
||||
idmontie:migrations
|
||||
matb33:collection-hooks
|
||||
matteodem:easy-search
|
||||
mongo@1.8.0
|
||||
mongo@1.9.0
|
||||
mquandalle:collection-mutations
|
||||
|
||||
# Account system
|
||||
@@ -38,7 +38,7 @@ 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
|
||||
@@ -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
@@ -1 +1 @@
|
||||
METEOR@1.9.2
|
||||
METEOR@1.10.1
|
||||
+21
-21
@@ -1,7 +1,7 @@
|
||||
3stack:presence@1.1.2
|
||||
accounts-base@1.5.0
|
||||
accounts-oauth@1.1.16
|
||||
accounts-password@1.5.3
|
||||
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
|
||||
@@ -11,16 +11,16 @@ 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.5.2
|
||||
babel-compiler@7.5.3
|
||||
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-compiler@1.2.2
|
||||
caching-html-compiler@1.1.3
|
||||
callback-hook@1.3.0
|
||||
cfs:access-point@0.1.49
|
||||
@@ -43,7 +43,7 @@ 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
|
||||
coagmano:stylus@1.1.0
|
||||
coffeescript@2.4.1
|
||||
coffeescript-compiler@2.4.1
|
||||
cottz:publish-relations@2.0.8
|
||||
@@ -55,9 +55,9 @@ ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.3.1
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.1
|
||||
dynamic-import@0.5.1
|
||||
dynamic-import@0.5.2
|
||||
easylogic:summernote@0.8.8
|
||||
ecmascript@0.14.2
|
||||
ecmascript@0.14.3
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.10.0
|
||||
ecmascript-runtime-server@0.9.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
|
||||
@@ -104,13 +104,13 @@ meteorspark:util@0.2.0
|
||||
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
|
||||
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.8.1
|
||||
mongo@1.9.1
|
||||
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.3.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,7 +156,7 @@ 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
|
||||
@@ -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.8.2
|
||||
webapp@1.9.1
|
||||
webapp-hashing@1.0.9
|
||||
wekan-accounts-cas@0.1.0
|
||||
wekan-accounts-oidc@1.0.10
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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
|
||||
@@ -1,2 +0,0 @@
|
||||
dev_bundle
|
||||
local
|
||||
@@ -1,7 +0,0 @@
|
||||
# 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.
@@ -1,99 +0,0 @@
|
||||
# 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!
|
||||
coagmano:stylus
|
||||
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
|
||||
@@ -1,2 +0,0 @@
|
||||
server
|
||||
browser
|
||||
@@ -1 +0,0 @@
|
||||
METEOR@1.8.3
|
||||
@@ -1,198 +0,0 @@
|
||||
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@2.0.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
|
||||
@@ -1,914 +0,0 @@
|
||||
(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'] = {};
|
||||
|
||||
})();
|
||||
@@ -1,238 +0,0 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,584 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
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);
|
||||
};
|
||||
Generated
-5184
File diff suppressed because it is too large.
Load diff
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"name": "wekan",
|
||||
"version": "v3.80.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": "^5.16.0",
|
||||
"eslint-config-meteor": "0.0.9",
|
||||
"eslint-config-prettier": "^3.6.0",
|
||||
"eslint-import-resolver-meteor": "^0.4.0",
|
||||
"eslint-plugin-import": "^2.18.0",
|
||||
"eslint-plugin-meteor": "^5.1.0",
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"lint-staged": "^7.3.0",
|
||||
"pre-commit": "^1.2.2",
|
||||
"prettier": "^1.18.2",
|
||||
"prettier-eslint": "^8.8.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.2",
|
||||
"ajv": "^5.0.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"bcrypt": "^3.0.2",
|
||||
"bson": "^4.0.0",
|
||||
"bunyan": "^1.8.12",
|
||||
"es6-promise": "^4.2.4",
|
||||
"gridfs-stream": "^0.5.3",
|
||||
"ldapjs": "^1.0.2",
|
||||
"meteor-node-stubs": "^0.4.1",
|
||||
"mongodb": "^3.3.3",
|
||||
"os": "^0.1.1",
|
||||
"page": "^1.8.6",
|
||||
"qs": "^6.8.0",
|
||||
"source-map-support": "^0.5.12",
|
||||
"xss": "^1.0.6"
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
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
|
||||
@@ -1,853 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ sudo: required
|
||||
|
||||
env:
|
||||
TRAVIS_DOCKER_COMPOSE_VERSION: 1.24.0
|
||||
TRAVIS_NODE_VERSION: 12.15.0
|
||||
TRAVIS_NODE_VERSION: 12.16.2
|
||||
TRAVIS_NPM_VERSION: latest
|
||||
|
||||
before_install:
|
||||
|
||||
+317
@@ -1,3 +1,320 @@
|
||||
# v3.96 2020-04-15 Wekan release
|
||||
|
||||
This release adds the following Sandstorm updates:
|
||||
|
||||
- This is the first Sandstorm Wekan release that uses newest Meteor 1.10.1 and Node 12.x.
|
||||
Now all Wekan platforms use newest Meteor and Node 12.x LTS.
|
||||
Thanks to kentonv and xet7.
|
||||
- [Fix capnp workaround to work with newest Meteor and
|
||||
Node 12.x](https://github.com/wekan/wekan/commit/b2d546579c4957352c29b36c0c8a4a08b944dbb4).
|
||||
Thanks to kentonv.
|
||||
- [Update Sandstorm release script for newest Meteor and
|
||||
Node 12.x](https://github.com/wekan/wekan/commit/c5f782976b971fa3f2323e80a013bbf6a49c0596).
|
||||
Thanks to xet7.
|
||||
- [Remove Meteor 1.8.x files because Sandstorm Wekan now uses newest
|
||||
Meteor](https://github.com/wekan/wekan/commit/1a836969e10215bad47ac56a9b0d9de801b66fd2).
|
||||
Thanks to xet7.
|
||||
|
||||
and adds the following new features:
|
||||
|
||||
- [Hide password auth with environment variable PASSWORD_LOGIN_ENABLED=false](https://github.com/wekan/wekan/pull/3014).
|
||||
Snap example: `sudo snap set wekan password-login-enabled='false'` .
|
||||
Thanks to salleman33.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Board admins can not clone or archive their boards at All Boards
|
||||
page](https://github.com/wekan/wekan/pull/3013).
|
||||
Thanks to salleman33.
|
||||
- [Fix `<p>` margin in card labels](https://github.com/wekan/wekan/pull/3015).
|
||||
Thanks to boeserwolf.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.95 2020-04-12 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Add gitpod config](https://github.com/wekan/wekan/pull/3009).
|
||||
This adds support for Gitpod.io, a free automated
|
||||
dev environment that makes contributing and generally working on GitHub
|
||||
projects much easier. It allows anyone to start a ready-to-code dev
|
||||
environment for any branch, issue and pull request with a single click.
|
||||
Thanks to juniormendonca.
|
||||
- [Public boards overview](https://github.com/wekan/wekan/pull/3008).
|
||||
Thanks to NicoP-S.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix styling issue in notifications drawer](https://github.com/wekan/wekan/pull/3012).
|
||||
Thanks to boeserwolf.
|
||||
- [Fix error in notifications cleanup cron](https://github.com/wekan/wekan/pull/3010).
|
||||
Thanks to jtbairdsr.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.94 2020-04-12 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Public vote](https://github.com/wekan/wekan/pull/3006).
|
||||
Thanks to NicoP-S.
|
||||
- [Add robots.txt disallow all](https://github.com/wekan/wekan/commit/3fae5355d40055757bf4a5f0c503581195609720).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.93 2020-04-10 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Trello vote import & hide export button if with_api is
|
||||
disabled](https://github.com/wekan/wekan/pull/3000).
|
||||
Thanks to NicoP-S.
|
||||
- [When adding a user to a board that has subtasks, also add user to the subtask
|
||||
board](https://github.com/wekan/wekan/pull/3004).
|
||||
Thanks to slvrpdr.
|
||||
|
||||
and adds the following updates:
|
||||
|
||||
- Upgrade to Node v12.16.2 [Part1](https://github.com/wekan/wekan/commit/6db717b9b384fe1491063e507b80e67791a07e3a)
|
||||
and [Part2](https://github.com/wekan/wekan/commit/268d7fcb32186a902a84e7f6d80c50b1f3790bad).
|
||||
Thanks to Node developers and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix bug that prevents editing or deleting
|
||||
comments](https://github.com/wekan/wekan/pull/3005).
|
||||
Thanks to jtbairdsr.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.92 2020-04-09 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Scheduler to clean up read notifications. Also added a button to manually remove all
|
||||
read notifications, and a fix to prevent users form getting notifications for their own
|
||||
actions](https://github.com/wekan/wekan/pull/2998).
|
||||
Thanks to jtbairdsr.
|
||||
- [Add setting](https://github.com/wekan/wekan/commit/5ebb47cb0ec7272894a37d99579ede872251f55c)
|
||||
default [NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE=2](https://github.com/wekan/wekan/pull/2998)
|
||||
to all Wekan platforms.
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.91 2020-04-08 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [OpenShift: Route template added to helm chart for Openshift v4x
|
||||
cluster](https://github.com/wekan/wekan/pull/2996).
|
||||
Thanks to ckavili.
|
||||
- [Filter by Assignee](https://github.com/wekan/wekan/pull/2997).
|
||||
Thanks to daniel-eder.
|
||||
- [Vote on Card](https://github.com/wekan/wekan/pull/2994).
|
||||
Thanks to NicoP-S and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.90 2020-04-06 Wekan release
|
||||
|
||||
This release makes the following updates:
|
||||
|
||||
- [Update dependencies](https://github.com/wekan/wekan/commit/d798f6e3ef09595ce4f1d1fbc053eec70fc91fb9).
|
||||
|
||||
and updates the following translations:
|
||||
|
||||
- [Update layouts.js for zh-TW language name](https://github.com/wekan/wekan/pull/2988).
|
||||
Thanks to doggy8088.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.89 2020-04-05 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Create subtasks in parenttask swimlane](https://github.com/wekan/wekan/issues/1953).
|
||||
Thanks to TOSCom-DanielEder.
|
||||
- [When searching cards in a board, also search from Custom Fields](https://github.com/wekan/wekan/pull/2985).
|
||||
Thanks to slvrpdr.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.88 2020-04-02 Wekan release
|
||||
|
||||
This release adds the following new features:
|
||||
|
||||
- [Notification drawer](https://github.com/wekan/wekan/pull/2975) [like Trello](https://github.com/wekan/wekan/issues/2471).
|
||||
Thanks to jtbairdsr and xet7.
|
||||
|
||||
and makes the following UI changes:
|
||||
|
||||
- [Minicard labels on the top and title on bottom](https://github.com/wekan/wekan/issues/2980).
|
||||
Thanks to helioguardabaxo and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix start-wekan.sh MongoDB port to 27017](https://github.com/wekan/wekan/commit/c60a092fc0ed9fe15c417bcb443b1e3e3aaedf7e).
|
||||
Thanks to Keelan and xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# v3.87 2020-04-01 Wekan release
|
||||
|
||||
This release makes the following UI changes:
|
||||
|
||||
- [Move "Rules" from "Board View" to "Board Settings"](https://github.com/wekan/wekan/issues/2973).
|
||||
Thanks to helioguardabaxo and xet7.
|
||||
- [Improvements on card details visualization](https://github.com/wekan/wekan/issues/2974).
|
||||
Thanks to helioguardabaxo and xet7.
|
||||
- [Hide duplicate "Hide system messages" at Change Settings/Member Settings, because it's also on card
|
||||
slider](https://github.com/wekan/wekan/issues/2837).
|
||||
Thanks to notohiro and xet7.
|
||||
|
||||
and fixes the following bugs:
|
||||
|
||||
- [Fix Browser always reload the whole page when I change one of the card
|
||||
color](https://github.com/wekan/wekan/commit/3546d7aa02bc65cf1183cb493adeb543ba51945d).
|
||||
Fixed by making label colors and text again editable.
|
||||
Regression from [Wekan v3.86 2)](https://github.com/wekan/wekan/commit/b9099a8b7ea6f63c79bdcbb871cb993b2cb7e325).
|
||||
Thanks to javen9881 and xet7.
|
||||
- [Fix richer editor submit did not clear edit area](https://github.com/wekan/wekan/commit/033d6710470b2ecd7a0ec0b2f0741ff459e68b32).
|
||||
Thanks to xet7.
|
||||
|
||||
Thanks to above GitHub users for their contributions and translators for their translations.
|
||||
|
||||
# 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:
|
||||
|
||||
+7
-3
@@ -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=v12.16.1 \
|
||||
METEOR_RELEASE=1.9.0 \
|
||||
NODE_VERSION=v12.16.2 \
|
||||
METEOR_RELEASE=1.10-rc.2 \
|
||||
USE_EDGE=false \
|
||||
METEOR_EDGE=1.5-beta.17 \
|
||||
NPM_VERSION=latest \
|
||||
@@ -26,6 +26,7 @@ ENV BUILD_DEPS="apt-utils libarchive-tools gnupg gosu wget curl bzip2 g++ build-
|
||||
ATTACHMENTS_STORE_PATH="" \
|
||||
MAX_IMAGE_PIXEL="" \
|
||||
IMAGE_COMPRESS_RATIO="" \
|
||||
NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE="" \
|
||||
BIGEVENTS_PATTERN=NONE \
|
||||
NOTIFY_DUE_DAYS_BEFORE_AND_AFTER="" \
|
||||
NOTIFY_DUE_AT_HOUR_OF_DAY="" \
|
||||
@@ -110,7 +111,10 @@ 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" \
|
||||
PASSWORD_LOGIN_ENABLED=true
|
||||
|
||||
# Copy the app to the image
|
||||
COPY ${SRC_PATH} /home/wekan/app
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
[](https://gitpod.io/#https://github.com/wekan/wekan)
|
||||
|
||||
# Wekan - Open Source kanban
|
||||
|
||||
[](https://github.com/wekan/wekan/graphs/contributors)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
appId: wekan-public/apps/77b94f60-dec9-0136-304e-16ff53095928
|
||||
appVersion: "v3.80.0"
|
||||
appVersion: "v3.96.0"
|
||||
files:
|
||||
userUploads:
|
||||
- README.md
|
||||
|
||||
@@ -8,234 +8,201 @@ template(name="activities")
|
||||
+cardActivities
|
||||
|
||||
template(name="boardActivities")
|
||||
each currentBoard.activities
|
||||
.activity
|
||||
+userAvatar(userId=user._id)
|
||||
p.activity-desc
|
||||
+memberName(user=user)
|
||||
|
||||
if($eq activityType 'deleteAttachment')
|
||||
| {{{_ 'activity-delete-attach' cardLink}}}.
|
||||
|
||||
if($eq activityType 'addAttachment')
|
||||
| {{{_ 'activity-attached' attachmentLink cardLink}}}.
|
||||
|
||||
if($eq activityType 'addBoardMember')
|
||||
| {{{_ 'activity-added' memberLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'addComment')
|
||||
| {{{_ 'activity-on' cardLink}}}
|
||||
a.activity-comment(href="{{ card.absoluteUrl }}")
|
||||
+viewer
|
||||
= comment.text
|
||||
|
||||
if($eq activityType 'addChecklist')
|
||||
| {{{_ 'activity-checklist-added' cardLink}}}.
|
||||
.activity-checklist(href="{{ card.absoluteUrl }}")
|
||||
+viewer
|
||||
= checklist.title
|
||||
if($eq activityType 'removeChecklist')
|
||||
| {{{_ 'activity-checklist-removed' cardLink}}}.
|
||||
|
||||
if($eq activityType 'checkedItem')
|
||||
| {{{_ 'activity-checked-item' checkItem checklist.title cardLink}}}.
|
||||
|
||||
if($eq activityType 'uncheckedItem')
|
||||
| {{{_ 'activity-unchecked-item' checkItem checklist.title cardLink}}}.
|
||||
|
||||
if($eq activityType 'checklistCompleted')
|
||||
| {{{_ 'activity-checklist-completed' checklist.title cardLink}}}.
|
||||
|
||||
if($eq activityType 'checklistUncompleted')
|
||||
| {{{_ 'activity-checklist-uncompleted' checklist.title cardLink}}}.
|
||||
|
||||
if($eq activityType 'addChecklistItem')
|
||||
| {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
|
||||
.activity-checklist(href="{{ card.absoluteUrl }}")
|
||||
+viewer
|
||||
= checklistItem.title
|
||||
if($eq activityType 'removedChecklistItem')
|
||||
| {{{_ 'activity-checklist-item-removed' checklist.title cardLink}}}.
|
||||
|
||||
if($eq activityType 'archivedCard')
|
||||
| {{{_ 'activity-archived' cardLink}}}.
|
||||
|
||||
if($eq activityType 'archivedList')
|
||||
| {{_ 'activity-archived' list.title}}.
|
||||
|
||||
if($eq activityType 'archivedSwimlane')
|
||||
| {{_ 'activity-archived' swimlane.title}}.
|
||||
|
||||
if($eq activityType 'createBoard')
|
||||
| {{_ 'activity-created' boardLabel}}.
|
||||
|
||||
if($eq activityType 'createCard')
|
||||
| {{{_ 'activity-added' cardLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'createCustomField')
|
||||
| {{_ 'activity-customfield-created' customField}}.
|
||||
|
||||
if($eq activityType 'createList')
|
||||
| {{_ 'activity-added' list.title boardLabel}}.
|
||||
|
||||
if($eq activityType 'createSwimlane')
|
||||
| {{_ 'activity-added' swimlane.title boardLabel}}.
|
||||
|
||||
if($eq activityType 'removeList')
|
||||
| {{_ 'activity-removed' title boardLabel}}.
|
||||
|
||||
if($eq activityType 'importBoard')
|
||||
| {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
|
||||
|
||||
if($eq activityType 'importCard')
|
||||
| {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
|
||||
|
||||
if($eq activityType 'importList')
|
||||
| {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
|
||||
|
||||
if($eq activityType 'joinMember')
|
||||
if($eq user._id member._id)
|
||||
| {{{_ 'activity-joined' cardLink}}}.
|
||||
else
|
||||
| {{{_ 'activity-added' memberLink cardLink}}}.
|
||||
|
||||
if($eq activityType 'moveCardBoard')
|
||||
| {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
|
||||
|
||||
if($eq activityType 'moveCard')
|
||||
| {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
|
||||
|
||||
if($eq activityType 'removeBoardMember')
|
||||
| {{{_ 'activity-excluded' memberLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'restoredCard')
|
||||
| {{{_ 'activity-sent' cardLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'addedLabel')
|
||||
| {{{_ 'activity-added-label' lastLabel cardLink}}}.
|
||||
|
||||
if($eq activityType 'removedLabel')
|
||||
| {{{_ 'activity-removed-label' lastLabel cardLink}}}.
|
||||
|
||||
if($eq activityType 'setCustomField')
|
||||
| {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
|
||||
|
||||
if($eq activityType 'unsetCustomField')
|
||||
| {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
|
||||
|
||||
if($eq activityType 'unjoinMember')
|
||||
if($eq user._id member._id)
|
||||
| {{{_ 'activity-unjoined' cardLink}}}.
|
||||
else
|
||||
| {{{_ 'activity-removed' memberLink cardLink}}}.
|
||||
|
||||
span(title=createdAt).activity-meta {{ moment createdAt }}
|
||||
each activityData in currentBoard.activities
|
||||
+activity(activity=activityData card=card mode=mode)
|
||||
|
||||
template(name="cardActivities")
|
||||
each currentCard.activities
|
||||
.activity
|
||||
+userAvatar(userId=user._id)
|
||||
p.activity-desc
|
||||
+memberName(user=user)
|
||||
if($eq activityType 'createCard')
|
||||
| {{_ 'activity-added' cardLabel listName}}.
|
||||
if($eq activityType 'importCard')
|
||||
| {{{_ 'activity-imported' cardLabel list.title sourceLink}}}.
|
||||
if($eq activityType 'joinMember')
|
||||
if($eq user._id member._id)
|
||||
| {{_ 'activity-joined' cardLabel}}.
|
||||
else
|
||||
| {{{_ 'activity-added' memberLink cardLabel}}}.
|
||||
if($eq activityType 'unjoinMember')
|
||||
if($eq user._id member._id)
|
||||
| {{_ 'activity-unjoined' cardLabel}}.
|
||||
else
|
||||
| {{{_ 'activity-removed' cardLabel memberLink}}}.
|
||||
if($eq activityType 'archivedCard')
|
||||
| {{_ 'activity-archived' cardLabel}}.
|
||||
each activityData in currentCard.activities
|
||||
+activity(activity=activityData card=card mode=mode)
|
||||
|
||||
if($eq activityType 'addedLabel')
|
||||
| {{{_ 'activity-added-label-card' lastLabel }}}.
|
||||
template(name="activity")
|
||||
.activity
|
||||
+userAvatar(userId=activity.user._id)
|
||||
p.activity-desc
|
||||
+memberName(user=activity.user)
|
||||
|
||||
if($eq activityType 'removedLabel')
|
||||
| {{{_ 'activity-removed-label-card' lastLabel }}}.
|
||||
//- attachment activity -------------------------------------------------
|
||||
if($eq activity.activityType 'deleteAttachment')
|
||||
| {{{_ 'activity-delete-attach' cardLink}}}.
|
||||
|
||||
if($eq activityType 'removeChecklist')
|
||||
| {{{_ 'activity-checklist-removed' cardLabel}}}.
|
||||
if($eq activity.activityType 'addAttachment')
|
||||
| {{{_ 'activity-attached' attachmentLink cardLink}}}.
|
||||
if($neq mode 'board')
|
||||
if activity.attachment.isImage
|
||||
img.attachment-image-preview(src=activity.attachment.url)
|
||||
|
||||
if($eq activityType 'checkedItem')
|
||||
| {{{_ 'activity-checked-item-card' checkItem checklist.title }}}.
|
||||
//- board activity ------------------------------------------------------
|
||||
if($eq mode 'board')
|
||||
if($eq activity.activityType 'createBoard')
|
||||
| {{_ 'activity-created' boardLabel}}.
|
||||
|
||||
if($eq activityType 'uncheckedItem')
|
||||
| {{{_ 'activity-unchecked-item-card' checkItem checklist.title }}}.
|
||||
if($eq activity.activityType 'importBoard')
|
||||
| {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
|
||||
|
||||
if($eq activityType 'checklistCompleted')
|
||||
| {{{_ 'activity-checklist-completed-card' checklist.title }}}.
|
||||
if($eq activity.activityType 'addBoardMember')
|
||||
| {{{_ 'activity-added' memberLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'checklistUncompleted')
|
||||
| {{{_ 'activity-checklist-uncompleted-card' checklist.title }}}.
|
||||
if($eq activity.activityType 'removeBoardMember')
|
||||
| {{{_ 'activity-excluded' memberLink boardLabel}}}.
|
||||
|
||||
if($eq activityType 'restoredCard')
|
||||
| {{_ 'activity-sent' cardLabel boardLabel}}.
|
||||
if($eq activityType 'moveCard')
|
||||
| {{_ 'activity-moved' cardLabel oldList.title list.title}}.
|
||||
//- card activity -------------------------------------------------------
|
||||
if($eq activity.activityType 'createCard')
|
||||
if($eq mode 'card')
|
||||
| {{{_ 'activity-added' cardLabel activity.listName}}}.
|
||||
else
|
||||
| {{{_ 'activity-added' cardLabel boardLabel}}}.
|
||||
|
||||
if($eq activityType 'moveCardBoard')
|
||||
| {{{_ 'activity-moved' cardLink oldBoardName boardName}}}.
|
||||
if($eq activity.activityType 'importCard')
|
||||
| {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
|
||||
|
||||
if($eq activityType 'addAttachment')
|
||||
| {{{_ 'activity-attached' attachmentLink cardLabel}}}.
|
||||
if attachment.isImage
|
||||
img.attachment-image-preview(src=attachment.url)
|
||||
if($eq activityType 'deleteAttachment')
|
||||
| {{{_ 'activity-delete-attach' cardLabel}}}.
|
||||
if($eq activityType 'removedChecklist')
|
||||
| {{{_ 'activity-checklist-removed' cardLabel}}}.
|
||||
if($eq activityType 'addChecklist')
|
||||
| {{{_ 'activity-checklist-added' cardLabel}}}.
|
||||
if($eq activity.activityType 'moveCard')
|
||||
| {{{_ 'activity-moved' cardLabel activity.oldList.title activity.list.title}}}.
|
||||
|
||||
if($eq activity.activityType 'moveCardBoard')
|
||||
| {{{_ 'activity-moved' cardLink activity.oldBoardName activity.boardName}}}.
|
||||
|
||||
if($eq activity.activityType 'archivedCard')
|
||||
| {{{_ 'activity-archived' cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'restoredCard')
|
||||
| {{{_ 'activity-sent' cardLink boardLabel}}}.
|
||||
|
||||
//- checklist activity --------------------------------------------------
|
||||
if($eq activity.activityType 'addChecklist')
|
||||
| {{{_ 'activity-checklist-added' cardLink}}}.
|
||||
if($eq mode 'card')
|
||||
.activity-checklist
|
||||
+viewer
|
||||
= checklist.title
|
||||
if($eq activityType 'addChecklistItem')
|
||||
| {{{_ 'activity-checklist-item-added' checklist.title cardLink}}}.
|
||||
.activity-checklist(href="{{ card.absoluteUrl }}")
|
||||
= activity.checklist.title
|
||||
else
|
||||
a.activity-checklist(href="{{ activity.card.absoluteUrl }}")
|
||||
+viewer
|
||||
= checklistItem.title
|
||||
= activity.checklist.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 activity.activityType 'removedChecklist')
|
||||
| {{{_ 'activity-checklist-removed' cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'completeChecklist')
|
||||
| {{{_ 'activity-checklist-completed' activity.checklist.title cardLink}}}.
|
||||
|
||||
if($eq activityType 'deleteComment')
|
||||
| {{{_ 'activity-deleteComment' currentData.commentId}}}.
|
||||
if($eq activityType 'editComment')
|
||||
| {{{_ 'activity-editComment' currentData.commentId}}}.
|
||||
if($eq activityType 'addComment')
|
||||
if($eq activity.activityType 'uncompleteChecklist')
|
||||
| {{{_ 'activity-checklist-uncompleted' activity.checklist.title cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'checkedItem')
|
||||
| {{{_ 'activity-checked-item' checkItem activity.checklist.title cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'uncheckedItem')
|
||||
| {{{_ 'activity-unchecked-item' checkItem activity.checklist.title cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'addChecklistItem')
|
||||
| {{{_ 'activity-checklist-item-added' activity.checklist.title cardLink}}}.
|
||||
.activity-checklist(href="{{ activity.card.absoluteUrl }}")
|
||||
+viewer
|
||||
= activity.checklistItem.title
|
||||
|
||||
if($eq activity.activityType 'removedChecklistItem')
|
||||
| {{{_ 'activity-checklist-item-removed' activity.checklist.title cardLink}}}.
|
||||
|
||||
//- comment activity ----------------------------------------------------
|
||||
if($eq mode 'card')
|
||||
//- if we are in card mode we display the comment in a way that it
|
||||
//- can be edited by the owner
|
||||
if($eq activity.activityType 'addComment')
|
||||
+inlinedForm(classNames='js-edit-comment')
|
||||
+editor(autofocus=true)
|
||||
= comment.text
|
||||
= activity.comment.text
|
||||
.edit-controls
|
||||
button.primary(type="submit") {{_ 'edit'}}
|
||||
else
|
||||
.activity-comment
|
||||
+viewer
|
||||
= comment.text
|
||||
span(title=createdAt).activity-meta {{ moment createdAt }}
|
||||
if ($eq currentUser._id comment.userId)
|
||||
= activity.comment.text
|
||||
span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
if ($eq currentUser._id activity.comment.userId)
|
||||
= ' - '
|
||||
a.js-open-inlined-form {{_ "edit"}}
|
||||
= ' - '
|
||||
a.js-delete-comment {{_ "delete"}}
|
||||
|
||||
if($eq activity.activityType 'deleteComment')
|
||||
| {{{_ 'activity-deleteComment' currentData.commentId}}}.
|
||||
|
||||
if($eq activity.activityType 'editComment')
|
||||
| {{{_ 'activity-editComment' currentData.commentId}}}.
|
||||
else
|
||||
//- if we are not in card mode we only display a summary of the comment
|
||||
if($eq activity.activityType 'addComment')
|
||||
| {{{_ 'activity-on' cardLink}}}
|
||||
a.activity-comment(href="{{ activity.card.absoluteUrl }}")
|
||||
+viewer
|
||||
= activity.comment.text
|
||||
|
||||
//- customField activity ------------------------------------------------
|
||||
if($eq mode 'board')
|
||||
if($eq activity.activityType 'createCustomField')
|
||||
| {{_ 'activity-customfield-created' customField}}.
|
||||
|
||||
if($eq activity.activityType 'setCustomField')
|
||||
| {{{_ 'activity-set-customfield' lastCustomField lastCustomFieldValue cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'unsetCustomField')
|
||||
| {{{_ 'activity-unset-customfield' lastCustomField cardLink}}}.
|
||||
|
||||
//- label activity ------------------------------------------------------
|
||||
if($eq activity.activityType 'addedLabel')
|
||||
| {{{_ 'activity-added-label' lastLabel cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'removedLabel')
|
||||
| {{{_ 'activity-removed-label' lastLabel cardLink}}}.
|
||||
|
||||
//- list activity -------------------------------------------------------
|
||||
if($neq mode 'card')
|
||||
if($eq activity.activityType 'createList')
|
||||
| {{{_ 'activity-added' listLabel boardLabel}}}.
|
||||
|
||||
if($eq activity.activityType 'importList')
|
||||
| {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
|
||||
|
||||
if($eq activity.activityType 'removeList')
|
||||
| {{{_ 'activity-removed' activity.title boardLabel}}}.
|
||||
|
||||
if($eq activity.activityType 'archivedList')
|
||||
| {{_ 'activity-archived' listLabel}}.
|
||||
|
||||
//- member activity ----------------------------------------------------
|
||||
if($eq activity.activityType 'joinMember')
|
||||
if($eq user._id activity.member._id)
|
||||
| {{{_ 'activity-joined' cardLink}}}.
|
||||
else
|
||||
span(title=createdAt).activity-meta {{ moment createdAt }}
|
||||
| {{{_ 'activity-added' memberLink cardLink}}}.
|
||||
|
||||
if($eq activity.activityType 'unjoinMember')
|
||||
if($eq user._id activity.member._id)
|
||||
| {{{_ 'activity-unjoined' cardLink}}}.
|
||||
else
|
||||
| {{{_ 'activity-removed' memberLink cardLink}}}.
|
||||
|
||||
//- swimlane activity --------------------------------------------------
|
||||
if($neq mode 'card')
|
||||
if($eq activity.activityType 'createSwimlane')
|
||||
| {{{_ 'activity-added' activity.swimlane.title boardLabel}}}.
|
||||
|
||||
if($eq activity.activityType 'archivedSwimlane')
|
||||
| {{_ 'activity-archived' activity.swimlane.title}}.
|
||||
|
||||
|
||||
//- I don't understand this part ----------------------------------------
|
||||
if(currentData.timeKey)
|
||||
| {{{_ activity.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)
|
||||
| {{{_ activity.activityType currentData.timeValue}}}
|
||||
|
||||
span(title=activity.createdAt).activity-meta {{ moment activity.createdAt }}
|
||||
@@ -41,7 +41,9 @@ BlazeComponent.extendComponent({
|
||||
});
|
||||
});
|
||||
},
|
||||
}).register('activities');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
loadNextPage() {
|
||||
if (this.loadNextPageLocked === false) {
|
||||
this.page.set(this.page.get() + 1);
|
||||
@@ -50,41 +52,37 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
checkItem() {
|
||||
const checkItemId = this.currentData().checklistItemId;
|
||||
const checkItemId = this.currentData().activity.checklistItemId;
|
||||
const checkItem = ChecklistItems.findOne({ _id: checkItemId });
|
||||
return checkItem.title;
|
||||
return checkItem && checkItem.title;
|
||||
},
|
||||
|
||||
boardLabel() {
|
||||
const data = this.currentData();
|
||||
if (data.mode !== 'board') {
|
||||
return createBoardLink(data.activity.board(), data.activity.listName);
|
||||
}
|
||||
return TAPi18n.__('this-board');
|
||||
},
|
||||
|
||||
cardLabel() {
|
||||
const data = this.currentData();
|
||||
if (data.mode !== 'card') {
|
||||
return createCardLink(this.currentData().activity.card());
|
||||
}
|
||||
return TAPi18n.__('this-card');
|
||||
},
|
||||
|
||||
cardLink() {
|
||||
const card = this.currentData().card();
|
||||
return (
|
||||
card &&
|
||||
Blaze.toHTML(
|
||||
HTML.A(
|
||||
{
|
||||
href: card.absoluteUrl(),
|
||||
class: 'action-card',
|
||||
},
|
||||
card.title,
|
||||
),
|
||||
)
|
||||
);
|
||||
return createCardLink(this.currentData().activity.card());
|
||||
},
|
||||
|
||||
lastLabel() {
|
||||
const lastLabelId = this.currentData().labelId;
|
||||
const lastLabelId = this.currentData().activity.labelId;
|
||||
if (!lastLabelId) return null;
|
||||
const lastLabel = Boards.findOne(Session.get('currentBoard')).getLabelById(
|
||||
lastLabelId,
|
||||
);
|
||||
const lastLabel = Boards.findOne(
|
||||
this.currentData().activity.boardId,
|
||||
).getLabelById(lastLabelId);
|
||||
if (lastLabel && (lastLabel.name === undefined || lastLabel.name === '')) {
|
||||
return lastLabel.color;
|
||||
} else {
|
||||
@@ -94,7 +92,7 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastCustomField() {
|
||||
const lastCustomField = CustomFields.findOne(
|
||||
this.currentData().customFieldId,
|
||||
this.currentData().activity.customFieldId,
|
||||
);
|
||||
if (!lastCustomField) return null;
|
||||
return lastCustomField.name;
|
||||
@@ -102,10 +100,10 @@ BlazeComponent.extendComponent({
|
||||
|
||||
lastCustomFieldValue() {
|
||||
const lastCustomField = CustomFields.findOne(
|
||||
this.currentData().customFieldId,
|
||||
this.currentData().activity.customFieldId,
|
||||
);
|
||||
if (!lastCustomField) return null;
|
||||
const value = this.currentData().value;
|
||||
const value = this.currentData().activity.value;
|
||||
if (
|
||||
lastCustomField.settings.dropdownItems &&
|
||||
lastCustomField.settings.dropdownItems.length > 0
|
||||
@@ -122,11 +120,13 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
listLabel() {
|
||||
return this.currentData().list().title;
|
||||
const activity = this.currentData().activity;
|
||||
const list = activity.list();
|
||||
return (list && list.title) || activity.title;
|
||||
},
|
||||
|
||||
sourceLink() {
|
||||
const source = this.currentData().source;
|
||||
const source = this.currentData().activity.source;
|
||||
if (source) {
|
||||
if (source.url) {
|
||||
return Blaze.toHTML(
|
||||
@@ -146,30 +146,31 @@ BlazeComponent.extendComponent({
|
||||
|
||||
memberLink() {
|
||||
return Blaze.toHTMLWithData(Template.memberName, {
|
||||
user: this.currentData().member(),
|
||||
user: this.currentData().activity.member(),
|
||||
});
|
||||
},
|
||||
|
||||
attachmentLink() {
|
||||
const attachment = this.currentData().attachment();
|
||||
const attachment = this.currentData().activity.attachment();
|
||||
// trying to display url before file is stored generates js errors
|
||||
return (
|
||||
attachment &&
|
||||
attachment.url({ download: true }) &&
|
||||
Blaze.toHTML(
|
||||
HTML.A(
|
||||
{
|
||||
href: attachment.url({ download: true }),
|
||||
target: '_blank',
|
||||
},
|
||||
attachment.name(),
|
||||
),
|
||||
)
|
||||
(attachment &&
|
||||
attachment.url({ download: true }) &&
|
||||
Blaze.toHTML(
|
||||
HTML.A(
|
||||
{
|
||||
href: attachment.url({ download: true }),
|
||||
target: '_blank',
|
||||
},
|
||||
attachment.name(),
|
||||
),
|
||||
)) ||
|
||||
this.currentData().activity.attachmentName
|
||||
);
|
||||
},
|
||||
|
||||
customField() {
|
||||
const customField = this.currentData().customField();
|
||||
const customField = this.currentData().activity.customField();
|
||||
if (!customField) return null;
|
||||
return customField.name;
|
||||
},
|
||||
@@ -179,7 +180,7 @@ BlazeComponent.extendComponent({
|
||||
{
|
||||
// XXX We should use Popup.afterConfirmation here
|
||||
'click .js-delete-comment'() {
|
||||
const commentId = this.currentData().commentId;
|
||||
const commentId = this.currentData().activity.commentId;
|
||||
CardComments.remove(commentId);
|
||||
},
|
||||
'submit .js-edit-comment'(evt) {
|
||||
@@ -187,7 +188,7 @@ BlazeComponent.extendComponent({
|
||||
const commentText = this.currentComponent()
|
||||
.getValue()
|
||||
.trim();
|
||||
const commentId = Template.parentData().commentId;
|
||||
const commentId = Template.parentData().activity.commentId;
|
||||
if (commentText) {
|
||||
CardComments.update(commentId, {
|
||||
$set: {
|
||||
@@ -199,4 +200,36 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('activities');
|
||||
}).register('activity');
|
||||
|
||||
function createCardLink(card) {
|
||||
return (
|
||||
card &&
|
||||
Blaze.toHTML(
|
||||
HTML.A(
|
||||
{
|
||||
href: card.absoluteUrl(),
|
||||
class: 'action-card',
|
||||
},
|
||||
card.title,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function createBoardLink(board, list) {
|
||||
let text = board.title;
|
||||
if (list) text += `: ${list}`;
|
||||
return (
|
||||
board &&
|
||||
Blaze.toHTML(
|
||||
HTML.A(
|
||||
{
|
||||
href: board.absoluteUrl(),
|
||||
class: 'action-board',
|
||||
},
|
||||
text,
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -193,20 +193,6 @@ template(name="boardChangeViewPopup")
|
||||
| {{_ 'board-view-cal'}}
|
||||
if $eq Utils.boardView "board-view-cal"
|
||||
i.fa.fa-check
|
||||
if currentUser.isAdmin
|
||||
hr
|
||||
li
|
||||
with "board-view-rules"
|
||||
a.js-open-rules-view(title="{{_ 'rules'}}")
|
||||
i.fa.fa-magic
|
||||
| {{_ 'rules'}}
|
||||
else if currentUser.isBoardAdmin
|
||||
hr
|
||||
li
|
||||
with "board-view-rules"
|
||||
a.js-open-rules-view(title="{{_ 'rules'}}")
|
||||
i.fa.fa-magic
|
||||
| {{_ 'rules'}}
|
||||
|
||||
template(name="createBoard")
|
||||
form
|
||||
|
||||
@@ -33,22 +33,6 @@ Template.boardMenuPopup.events({
|
||||
'click .js-card-settings': Popup.open('boardCardSettings'),
|
||||
});
|
||||
|
||||
Template.boardMenuPopup.helpers({
|
||||
exportUrl() {
|
||||
const params = {
|
||||
boardId: Session.get('currentBoard'),
|
||||
};
|
||||
const queryParams = {
|
||||
authToken: Accounts._storedLoginToken(),
|
||||
};
|
||||
return FlowRouter.path('/api/boards/:boardId/export', params, queryParams);
|
||||
},
|
||||
exportFilename() {
|
||||
const boardId = Session.get('currentBoard');
|
||||
return `wekan-export-board-${boardId}.json`;
|
||||
},
|
||||
});
|
||||
|
||||
Template.boardChangeTitlePopup.events({
|
||||
submit(event, templateInstance) {
|
||||
const newTitle = templateInstance
|
||||
@@ -191,10 +175,6 @@ Template.boardChangeViewPopup.events({
|
||||
Utils.setBoardView('board-view-cal');
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-open-rules-view'() {
|
||||
Modal.openWide('rulesMain');
|
||||
Popup.close();
|
||||
},
|
||||
});
|
||||
|
||||
const CreateBoard = BlazeComponent.extendComponent({
|
||||
|
||||
@@ -39,7 +39,7 @@ template(name="boardList")
|
||||
i.fa.js-archive-board(
|
||||
class="fa-archive"
|
||||
title="{{_ 'archive-board'}}")
|
||||
else if currentUser.isBoardAdmin
|
||||
else if isAdministrable
|
||||
i.fa.js-clone-board(
|
||||
class="fa-clone"
|
||||
title="{{_ 'duplicate-board'}}")
|
||||
@@ -55,7 +55,7 @@ template(name="boardList")
|
||||
title="{{_ 'archive-board'}}")
|
||||
|
||||
template(name="boardListHeaderBar")
|
||||
h1 {{_ 'my-boards'}}
|
||||
h1 {{_ title }}
|
||||
.board-header-btns.right
|
||||
a.board-header-btn.js-open-archived-board
|
||||
i.fa.fa-archive
|
||||
|
||||
@@ -7,6 +7,9 @@ Template.boardListHeaderBar.events({
|
||||
});
|
||||
|
||||
Template.boardListHeaderBar.helpers({
|
||||
title(){
|
||||
return FlowRouter.getRouteName() == 'home' ? 'my-boards' :'public';
|
||||
},
|
||||
templatesBoardId() {
|
||||
return Meteor.user() && Meteor.user().getTemplatesBoardId();
|
||||
},
|
||||
@@ -21,12 +24,17 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
|
||||
boards() {
|
||||
let query = {
|
||||
archived: false,
|
||||
type: 'board',
|
||||
}
|
||||
if (FlowRouter.getRouteName() == 'home')
|
||||
query['members.userId'] = Meteor.userId()
|
||||
else
|
||||
query.permission = 'public'
|
||||
|
||||
return Boards.find(
|
||||
{
|
||||
archived: false,
|
||||
'members.userId': Meteor.userId(),
|
||||
type: 'board',
|
||||
},
|
||||
query,
|
||||
{ sort: ['title'] },
|
||||
);
|
||||
},
|
||||
@@ -34,6 +42,10 @@ BlazeComponent.extendComponent({
|
||||
const user = Meteor.user();
|
||||
return user && user.hasStarred(this.currentData()._id);
|
||||
},
|
||||
isAdministrable() {
|
||||
const user = Meteor.user();
|
||||
return user && user.isBoardAdmin(this.currentData()._id);
|
||||
},
|
||||
|
||||
hasOvertimeCards() {
|
||||
subManager.subscribe('board', this.currentData()._id, false);
|
||||
|
||||
@@ -55,5 +55,5 @@ template(name="attachmentsGalery")
|
||||
unless currentUser.isWorker
|
||||
//li.attachment-item.add-attachment
|
||||
a.js-add-attachment
|
||||
i.fa.fa-paperclip
|
||||
i.fa.fa-plus
|
||||
| {{_ 'add-attachment' }}
|
||||
@@ -199,10 +199,29 @@ template(name="cardDetails")
|
||||
+viewer
|
||||
= getAssignedBy
|
||||
|
||||
if getVoteQuestion
|
||||
hr
|
||||
.vote-title
|
||||
h3
|
||||
i.fa.fa-thumbs-up
|
||||
card-details-item-title {{_ 'vote-question'}}
|
||||
.vote-result
|
||||
if votePublic
|
||||
a.card-label.card-label-green.js-show-positive-votes {{ voteCountPositive }}
|
||||
a.card-label.card-label-red.js-show-negative-votes {{ voteCountNegative }}
|
||||
else
|
||||
.card-label.card-label-green {{ voteCountPositive }}
|
||||
.card-label.card-label-red {{ voteCountNegative }}
|
||||
+viewer
|
||||
= getVoteQuestion
|
||||
button.card-details-green.js-vote.js-vote-positive(class="{{#if voteState}}voted{{/if}}") {{_ 'vote-for-it'}}
|
||||
button.card-details-red.js-vote.js-vote-negative(class="{{#if $eq voteState false}}voted{{/if}}") {{_ 'vote-against'}}
|
||||
|
||||
//- XXX We should use "editable" to avoid repetiting ourselves
|
||||
if canModifyCard
|
||||
unless currentUser.isWorker
|
||||
if currentBoard.allowsDescriptionTitle
|
||||
hr
|
||||
h3
|
||||
i.fa.fa-align-left
|
||||
card-details-item-title {{_ 'description'}}
|
||||
@@ -229,6 +248,7 @@ template(name="cardDetails")
|
||||
a.js-close-inlined-form {{_ 'discard'}}
|
||||
else if getDescription
|
||||
if currentBoard.allowsDescriptionTitle
|
||||
hr
|
||||
h3.card-details-item-title {{_ 'description'}}
|
||||
if currentBoard.allowsDescriptionText
|
||||
+viewer
|
||||
@@ -237,15 +257,16 @@ template(name="cardDetails")
|
||||
.card-checklist-attachmentGalerys
|
||||
.card-checklist-attachmentGalery.card-checklists
|
||||
if currentBoard.allowsChecklists
|
||||
hr
|
||||
+checklists(cardId = _id)
|
||||
if currentBoard.allowsSubtasks
|
||||
hr
|
||||
+subtasks(cardId = _id)
|
||||
if currentBoard.allowsAttachments
|
||||
//- hr
|
||||
//- h3
|
||||
//- i.fa.fa-paperclip
|
||||
//- | {{_ 'attachments'}}
|
||||
hr
|
||||
h3
|
||||
i.fa.fa-paperclip
|
||||
| {{_ 'attachments'}}
|
||||
.card-checklist-attachmentGalery.card-attachmentGalery
|
||||
+attachmentsGalery
|
||||
|
||||
@@ -312,6 +333,16 @@ template(name="cardDetailsActionsPopup")
|
||||
//li: a.js-members {{_ 'card-edit-members'}}
|
||||
//li: a.js-labels {{_ 'card-edit-labels'}}
|
||||
//li: a.js-attachments {{_ 'card-edit-attachments'}}
|
||||
if getVoteQuestion
|
||||
li
|
||||
a.js-cancel-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-cancel-voting'}}
|
||||
else
|
||||
li
|
||||
a.js-start-voting
|
||||
i.fa.fa-thumbs-up
|
||||
| {{_ 'card-start-voting'}}
|
||||
li
|
||||
a.js-custom-fields
|
||||
i.fa.fa-list-alt
|
||||
@@ -535,3 +566,35 @@ template(name="cardDeletePopup")
|
||||
unless archived
|
||||
p {{_ "card-delete-suggest-archive"}}
|
||||
button.js-confirm.negate.full(type="submit") {{_ 'delete'}}
|
||||
|
||||
template(name="cardStartVotingPopup")
|
||||
form.edit-vote-question
|
||||
.fields
|
||||
label(for="vote") {{_ 'vote-question'}}
|
||||
input.js-vote-field#vote(type="text" name="vote" value="{{card.getVoteQuestion}}" autofocus)
|
||||
label(for="vote-public") {{_ 'vote-public'}}
|
||||
a.js-toggle-vote-public
|
||||
.materialCheckBox#vote-public(name="vote-public")
|
||||
|
||||
button.primary.confirm.js-submit {{_ 'save'}}
|
||||
//- button.js-remove-color.negate.wide.right {{_ 'delete'}}
|
||||
|
||||
template(name="positiveVoteMembersPopup")
|
||||
ul.pop-over-list.js-card-member-list
|
||||
each m in voteMemberPositive
|
||||
li.item
|
||||
a.name
|
||||
+userAvatar(userId=m._id)
|
||||
span.full-name
|
||||
= m.profile.fullname
|
||||
| (<span class="username">{{ m.username }}</span>)
|
||||
|
||||
template(name="negativeVoteMembersPopup")
|
||||
ul.pop-over-list.js-card-member-list
|
||||
each m in voteMemberNegative
|
||||
li.item
|
||||
a.name
|
||||
+userAvatar(userId=m._id)
|
||||
span.full-name
|
||||
= m.profile.fullname
|
||||
| (<span class="username">{{ m.username }}</span>)
|
||||
@@ -38,6 +38,38 @@ BlazeComponent.extendComponent({
|
||||
Meteor.subscribe('unsaved-edits');
|
||||
},
|
||||
|
||||
voteState() {
|
||||
const card = this.currentData();
|
||||
const userId = Meteor.userId();
|
||||
let state;
|
||||
if (card.vote) {
|
||||
if (card.vote.positive) {
|
||||
state = _.contains(card.vote.positive, userId);
|
||||
if (state === true) return true;
|
||||
}
|
||||
if (card.vote.negative) {
|
||||
state = _.contains(card.vote.negative, userId);
|
||||
if (state === true) return false;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
votePublic() {
|
||||
const card = this.currentData();
|
||||
if (card.vote)
|
||||
return card.vote.public
|
||||
return null
|
||||
},
|
||||
voteCountPositive() {
|
||||
const card = this.currentData();
|
||||
if (card.vote && card.vote.positive) return card.vote.positive.length;
|
||||
return null;
|
||||
},
|
||||
voteCountNegative() {
|
||||
const card = this.currentData();
|
||||
if (card.vote && card.vote.negative) return card.vote.negative.length;
|
||||
return null;
|
||||
},
|
||||
isWatching() {
|
||||
const card = this.currentData();
|
||||
return card.findWatcher(Meteor.userId());
|
||||
@@ -356,6 +388,8 @@ BlazeComponent.extendComponent({
|
||||
'click .js-start-date': Popup.open('editCardStartDate'),
|
||||
'click .js-due-date': Popup.open('editCardDueDate'),
|
||||
'click .js-end-date': Popup.open('editCardEndDate'),
|
||||
'click .js-show-positive-votes':Popup.open('positiveVoteMembers'),
|
||||
'click .js-show-negative-votes': Popup.open('negativeVoteMembers'),
|
||||
'mouseenter .js-card-details'() {
|
||||
const parentComponent = this.parentComponent().parentComponent();
|
||||
//on mobile view parent is Board, not BoardBody.
|
||||
@@ -379,6 +413,18 @@ BlazeComponent.extendComponent({
|
||||
'click #toggleButton'() {
|
||||
Meteor.call('toggleSystemMessages');
|
||||
},
|
||||
'click .js-vote'(e) {
|
||||
const forIt = $(e.target).hasClass('js-vote-positive');
|
||||
let newState = null;
|
||||
if (
|
||||
this.voteState() == null ||
|
||||
(this.voteState() == false && forIt) ||
|
||||
(this.voteState() == true && !forIt)
|
||||
) {
|
||||
newState = forIt;
|
||||
}
|
||||
this.data().setVote(Meteor.userId(), newState);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
@@ -560,6 +606,7 @@ Template.cardDetailsActionsPopup.events({
|
||||
'click .js-assignees': Popup.open('cardAssignees'),
|
||||
'click .js-labels': Popup.open('cardLabels'),
|
||||
'click .js-attachments': Popup.open('cardAttachments'),
|
||||
'click .js-start-voting': Popup.open('cardStartVoting'),
|
||||
'click .js-custom-fields': Popup.open('cardCustomFields'),
|
||||
'click .js-received-date': Popup.open('editCardReceivedDate'),
|
||||
'click .js-start-date': Popup.open('editCardStartDate'),
|
||||
@@ -570,6 +617,11 @@ Template.cardDetailsActionsPopup.events({
|
||||
'click .js-copy-card': Popup.open('copyCard'),
|
||||
'click .js-copy-checklist-cards': Popup.open('copyChecklistToManyCards'),
|
||||
'click .js-set-card-color': Popup.open('setCardColor'),
|
||||
'click .js-cancel-voting'(event) {
|
||||
event.preventDefault();
|
||||
this.unsetVote();
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-move-card-to-top'(event) {
|
||||
event.preventDefault();
|
||||
const minOrder = _.min(
|
||||
@@ -603,7 +655,7 @@ Template.cardDetailsActionsPopup.events({
|
||||
},
|
||||
});
|
||||
|
||||
Template.editCardTitleForm.onRendered(function() {
|
||||
Template.editCardTitleForm.onRendered(function () {
|
||||
autosize(this.$('.js-edit-card-title'));
|
||||
});
|
||||
|
||||
@@ -617,7 +669,7 @@ Template.editCardTitleForm.events({
|
||||
},
|
||||
});
|
||||
|
||||
Template.editCardRequesterForm.onRendered(function() {
|
||||
Template.editCardRequesterForm.onRendered(function () {
|
||||
autosize(this.$('.js-edit-card-requester'));
|
||||
});
|
||||
|
||||
@@ -630,7 +682,7 @@ Template.editCardRequesterForm.events({
|
||||
},
|
||||
});
|
||||
|
||||
Template.editCardAssignerForm.onRendered(function() {
|
||||
Template.editCardAssignerForm.onRendered(function () {
|
||||
autosize(this.$('.js-edit-card-assigner'));
|
||||
});
|
||||
|
||||
@@ -770,7 +822,7 @@ Template.copyChecklistToManyCardsPopup.events({
|
||||
|
||||
// copy subtasks
|
||||
cursor = Cards.find({ parentId: oldId });
|
||||
cursor.forEach(function() {
|
||||
cursor.forEach(function () {
|
||||
'use strict';
|
||||
const subtask = arguments[0];
|
||||
subtask.parentId = _id;
|
||||
@@ -919,7 +971,7 @@ BlazeComponent.extendComponent({
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .js-delete': Popup.afterConfirm('cardDelete', function() {
|
||||
'click .js-delete': Popup.afterConfirm('cardDelete', function () {
|
||||
Popup.close();
|
||||
Cards.remove(this._id);
|
||||
Utils.goBoardId(this.boardId);
|
||||
@@ -945,6 +997,31 @@ BlazeComponent.extendComponent({
|
||||
},
|
||||
}).register('cardMorePopup');
|
||||
|
||||
BlazeComponent.extendComponent({
|
||||
onCreated() {
|
||||
this.currentCard = this.currentData();
|
||||
this.voteQuestion = new ReactiveVar(this.currentCard.voteQuestion);
|
||||
},
|
||||
|
||||
events() {
|
||||
return [
|
||||
{
|
||||
'submit .edit-vote-question'(evt) {
|
||||
evt.preventDefault();
|
||||
const voteQuestion = evt.target.vote.value;
|
||||
const publicVote = $('#vote-public').hasClass('is-checked');
|
||||
this.currentCard.setVoteQuestion(voteQuestion, publicVote);
|
||||
Popup.close();
|
||||
},
|
||||
'click a.js-toggle-vote-public'(event) {
|
||||
event.preventDefault();
|
||||
$('#vote-public').toggleClass('is-checked');
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}).register('cardStartVotingPopup');
|
||||
|
||||
// Close the card details pane by pressing escape
|
||||
EscapeActions.register(
|
||||
'detailsPane',
|
||||
|
||||
@@ -100,11 +100,11 @@ avatar-radius = 50%
|
||||
|
||||
.ps-scrollbar-y-rail
|
||||
pointer-event: all
|
||||
position: absolute;
|
||||
position: absolute
|
||||
|
||||
.card-details-canvas
|
||||
width: 470px
|
||||
padding-left: 20px;
|
||||
padding-left: 20px
|
||||
|
||||
.card-details-header
|
||||
margin: 0 -20px 5px
|
||||
@@ -241,7 +241,7 @@ input[type="submit"].attachment-add-link-submit
|
||||
|
||||
.card-details-canvas
|
||||
width: 100%
|
||||
padding-left: 0px;
|
||||
padding-left: 0px
|
||||
|
||||
.card-details-header
|
||||
.close-card-details
|
||||
@@ -330,3 +330,13 @@ card-details-color(background, color...)
|
||||
|
||||
.card-details-indigo
|
||||
card-details-color(#4b0082, #ffffff) //White text for better visibility
|
||||
|
||||
.voted
|
||||
opacity: .7
|
||||
.vote-title
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
.vote-result
|
||||
display: flex
|
||||
.js-show-positive-votes
|
||||
cursor: pointer
|
||||
@@ -158,6 +158,8 @@
|
||||
|
||||
.edit-labels-pop-over
|
||||
margin-bottom: 8px
|
||||
.card-label .viewer p
|
||||
margin: 0
|
||||
|
||||
.edit-labels-pop-over .shortcut
|
||||
display: inline-block
|
||||
|
||||
@@ -100,6 +100,10 @@ template(name="minicard")
|
||||
if getDescription
|
||||
.badge.badge-state-image-only(title=getDescription)
|
||||
span.badge-icon.fa.fa-align-left
|
||||
if getVoteQuestion
|
||||
.badge.badge-state-image-only(title=getVoteQuestion)
|
||||
span.badge-icon.fa.fa-thumbs-up
|
||||
span.badge-icon.fa.fa-thumbs-down
|
||||
if attachments.count
|
||||
.badge
|
||||
span.badge-icon.fa.fa-paperclip
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
border-radius: top 2px
|
||||
|
||||
.minicard-labels
|
||||
float: right
|
||||
float: none
|
||||
display: flex
|
||||
flex-wrap: wrap
|
||||
|
||||
|
||||
@@ -20,7 +20,22 @@ BlazeComponent.extendComponent({
|
||||
const crtBoard = Boards.findOne(card.boardId);
|
||||
const targetBoard = crtBoard.getDefaultSubtasksBoard();
|
||||
const listId = targetBoard.getDefaultSubtasksListId();
|
||||
const swimlaneId = targetBoard.getDefaultSwimline()._id;
|
||||
|
||||
//Get the full swimlane data for the parent task.
|
||||
const parentSwimlane = Swimlanes.findOne({
|
||||
boardId: crtBoard._id,
|
||||
_id: card.swimlaneId,
|
||||
});
|
||||
//find the swimlane of the same name in the target board.
|
||||
const targetSwimlane = Swimlanes.findOne({
|
||||
boardId: targetBoard._id,
|
||||
title: parentSwimlane.title,
|
||||
});
|
||||
//If no swimlane with a matching title exists in the target board, fall back to the default swimlane.
|
||||
const swimlaneId =
|
||||
targetSwimlane === undefined
|
||||
? targetBoard.getDefaultSwimline()._id
|
||||
: targetSwimlane._id;
|
||||
|
||||
if (title) {
|
||||
const _id = Cards.insert({
|
||||
|
||||
@@ -743,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());
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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),
|
||||
@@ -126,10 +36,9 @@ Template.editor.onRendered(() => {
|
||||
? [
|
||||
['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,37 +108,14 @@ Template.editor.onRendered(() => {
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
let popupShown = false;
|
||||
inputs.each(function(idx, input) {
|
||||
mSummernotes[idx] = $(input).summernote({
|
||||
placeholder,
|
||||
callbacks: {
|
||||
onKeydown(e) {
|
||||
if (popupShown) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
onKeyup(e) {
|
||||
if (popupShown) {
|
||||
e.preventDefault();
|
||||
}
|
||||
},
|
||||
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
|
||||
// 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', '');
|
||||
@@ -201,7 +123,9 @@ Template.editor.onRendered(() => {
|
||||
});
|
||||
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() {
|
||||
@@ -211,6 +135,7 @@ Template.editor.onRendered(() => {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onImageUpload(files) {
|
||||
const $summernote = getSummernote(this);
|
||||
if (files && files.length > 0) {
|
||||
@@ -289,6 +214,12 @@ 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
|
||||
@@ -331,6 +262,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,7 +285,7 @@ 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) {
|
||||
@@ -368,7 +301,12 @@ Blaze.Template.registerHelper(
|
||||
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,16 +314,17 @@ 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
|
||||
@@ -397,10 +336,7 @@ Template.viewer.events({
|
||||
Popup.open('member').call({ userId }, event, templateInstance);
|
||||
} 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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,11 @@ template(name="header")
|
||||
a(href="{{pathFor 'home'}}")
|
||||
span.fa.fa-home
|
||||
| {{_ 'all-boards'}}
|
||||
li.separator -
|
||||
li
|
||||
a(href="{{pathFor 'public'}}")
|
||||
span.fa.fa-globe
|
||||
| {{_ 'public'}}
|
||||
each currentUser.starredBoards
|
||||
li.separator -
|
||||
li(class="{{#if $.Session.equals 'currentBoard' _id}}current{{/if}}")
|
||||
@@ -35,6 +40,8 @@ template(name="header")
|
||||
a#header-new-board-icon.js-create-board
|
||||
i.fa.fa-plus(title="Create a new board")
|
||||
|
||||
+notifications
|
||||
|
||||
+headerUserBar
|
||||
|
||||
#header(class=currentBoard.colorClass)
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
height: 28px
|
||||
font-size: 12px
|
||||
display: flex
|
||||
z-index: 17
|
||||
z-index: 21
|
||||
|
||||
#header-user-bar,
|
||||
#header-new-board-icon,
|
||||
@@ -127,7 +127,7 @@
|
||||
&.current
|
||||
color: darken(white, 5%)
|
||||
|
||||
&:first-child .fa-home
|
||||
&:first-child .fa-home,&:nth-child(3) .fa-globe
|
||||
margin-right: 5px
|
||||
|
||||
a.js-create-board
|
||||
|
||||
@@ -31,6 +31,11 @@ Template.userFormsLayout.onCreated(function() {
|
||||
return this.stop();
|
||||
},
|
||||
});
|
||||
Meteor.call('isPasswordLoginDisabled', (_, result) => {
|
||||
if (result) {
|
||||
$('.at-pwd-form').hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Template.userFormsLayout.onRendered(() => {
|
||||
@@ -73,6 +78,8 @@ Template.userFormsLayout.helpers({
|
||||
name = 'Igbo';
|
||||
} else if (lang.name === 'oc') {
|
||||
name = 'Occitan';
|
||||
} else if (lang.name === '繁体中文(台湾)') {
|
||||
name = '繁體中文(台灣)';
|
||||
}
|
||||
return { tag, name };
|
||||
}).sort(function(a, b) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
template(name='notification')
|
||||
li.notification(class="{{#if read}}read{{/if}}")
|
||||
.read-status
|
||||
.materialCheckBox(class="{{#if read}}is-checked{{/if}}")
|
||||
+notificationIcon(activityData)
|
||||
.details
|
||||
+activity(activity=activityData mode='none')
|
||||
if read
|
||||
.remove
|
||||
a.fa.fa-trash
|
||||
@@ -0,0 +1,28 @@
|
||||
Template.notification.events({
|
||||
'click .read-status .materialCheckBox'() {
|
||||
const update = {};
|
||||
update[`profile.notifications.${this.index}.read`] = this.read
|
||||
? null
|
||||
: Date.now();
|
||||
Users.update(Meteor.userId(), { $set: update });
|
||||
},
|
||||
'click .remove a'() {
|
||||
Meteor.user().removeNotification(this.activityData._id);
|
||||
},
|
||||
});
|
||||
|
||||
Template.notification.helpers({
|
||||
mode: 'board',
|
||||
isOfActivityType(activityId, type) {
|
||||
const activity = Activities.findOne(activityId);
|
||||
return activity && activity.activityType === type;
|
||||
},
|
||||
activityType(activityId) {
|
||||
const activity = Activities.findOne(activityId);
|
||||
return activity ? activity.activityType : '';
|
||||
},
|
||||
activityUser(activityId) {
|
||||
const activity = Activities.findOne(activityId);
|
||||
return activity && activity.userId;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
#notifications-drawer
|
||||
&.show-read .notification.read
|
||||
display: flex
|
||||
|
||||
.notification
|
||||
display: flex
|
||||
float: none
|
||||
padding: 12px 8px 8px
|
||||
color: black
|
||||
border-bottom: 1px solid #dbdbdb
|
||||
|
||||
&.read
|
||||
display: none
|
||||
|
||||
.read-status
|
||||
width: 30px
|
||||
|
||||
input
|
||||
width: 24px
|
||||
height: 24px
|
||||
|
||||
.activity-type
|
||||
margin: 16px 0 0
|
||||
width: 17px
|
||||
height: 17px
|
||||
font-size: 17px
|
||||
display: block
|
||||
color: #bbb
|
||||
|
||||
.details
|
||||
width: calc(100% - 30px)
|
||||
|
||||
.activity
|
||||
display: flex
|
||||
|
||||
.activity-desc
|
||||
width: 100%;
|
||||
|
||||
.activity-comment
|
||||
display: block
|
||||
width: 100%
|
||||
border-radius: 3px
|
||||
background: #fff
|
||||
text-decoration: none
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.2)
|
||||
margin-top: 5px
|
||||
padding: 5px
|
||||
|
||||
.activity-meta
|
||||
display: block
|
||||
font-size: 0.8em
|
||||
color: #999
|
||||
font-style: italic
|
||||
|
||||
.remove
|
||||
a:hover
|
||||
color #eb4646 !important
|
||||
@@ -0,0 +1,53 @@
|
||||
template(name='notificationIcon')
|
||||
if($in activityType 'deleteAttachment' 'addAttachment')
|
||||
i.fa.fa-paperclip.activity-type(title="attachment")
|
||||
else if($in activityType 'createBoard' 'importBoard')
|
||||
i.fa.fa-chalkboard.activity-type(title="board")
|
||||
|
||||
else if($in activityType 'createCard' 'importCard' 'moveCard')
|
||||
+cardNotificationIcon
|
||||
else if($in activityType 'moveCardBoard' 'archivedCard' 'restoredCard')
|
||||
+cardNotificationIcon
|
||||
//- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
|
||||
//- DRY and consistant
|
||||
|
||||
else if($in activityType 'addChecklist' 'removedChecklist' 'completeChecklist')
|
||||
+checklistNotificationIcon
|
||||
else if($in activityType 'uncompleteChecklist')
|
||||
+checklistNotificationIcon
|
||||
//- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
|
||||
//- DRY and consistant
|
||||
|
||||
else if($in activityType 'checkedItem' 'uncheckedItem' 'addChecklistItem' 'removedChecklistItem')
|
||||
i.fa.fa-check-square.activity-type(title="checklist item")
|
||||
else if($in activityType 'addComment')
|
||||
i.fa.fa-comment-o.activity-type(title="comment")
|
||||
else if($in activityType 'createCustomField' 'setCustomField' 'unsetCustomField')
|
||||
i.fa.fa-code.activity-type(title="custom field")
|
||||
else if($in activityType 'addedLabel' 'removedLabel')
|
||||
i.fa.fa-tag.activity-type(title="label")
|
||||
|
||||
else if($in activityType 'createList' 'removeList' 'archivedList')
|
||||
+listNotificationIcon
|
||||
else if($in activityType 'importList')
|
||||
+listNotificationIcon
|
||||
//- $in can only handle up to 3 cases so we have to break this case over 2 cases... use a simple template to keep it
|
||||
//- DRY and consistant
|
||||
|
||||
//- elswhere in the app we use fa-trello to indicate lists...
|
||||
//- i personally like fa-columns a bit better
|
||||
else if($in activityType 'unjoinMember' 'addBoardMember' 'joinMember' 'removeBoardMember')
|
||||
i.fa.fa-user.activity-type(title="member")
|
||||
else if($in activityType 'createSwimlane' 'archivedSwimlane')
|
||||
i.fa.fa-th-large.activity-type(title="swimlane")
|
||||
else
|
||||
i.fa.fa-bug.activity-type(title="can't find icon for #{activityType}")
|
||||
|
||||
template(name='cardNotificationIcon')
|
||||
i.fa.fa-clone.activity-type(title="card")
|
||||
|
||||
template(name='checklistNotificationIcon')
|
||||
i.fa.fa-list.activity-type(title="checklist")
|
||||
|
||||
template(name='listNotificationIcon')
|
||||
i.fa.fa-columns.activity-type(title="list")
|
||||
@@ -0,0 +1,5 @@
|
||||
template(name='notifications')
|
||||
#notifications.board-header-btns.right
|
||||
a.notifications-drawer-toggle.fa.fa-bell(class="{{#if $gt unreadNotifications 0}}alert{{/if}}")
|
||||
if $.Session.get 'showNotificationsDrawer'
|
||||
+notificationsDrawer(unreadNotifications=unreadNotifications)
|
||||
@@ -0,0 +1,32 @@
|
||||
// this hides the notifications drawer if anyone clicks off of the panel
|
||||
Template.body.events({
|
||||
click(event) {
|
||||
if (
|
||||
!$(event.target).is('#notifications *') &&
|
||||
Session.get('showNotificationsDrawer')
|
||||
) {
|
||||
toggleNotificationsDrawer();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Template.notifications.helpers({
|
||||
unreadNotifications() {
|
||||
const notifications = Users.findOne(Meteor.userId()).notifications();
|
||||
const unreadNotifications = _.filter(notifications, v => !v.read);
|
||||
return unreadNotifications.length;
|
||||
},
|
||||
});
|
||||
|
||||
Template.notifications.events({
|
||||
'click .notifications-drawer-toggle'() {
|
||||
toggleNotificationsDrawer();
|
||||
},
|
||||
});
|
||||
|
||||
export function toggleNotificationsDrawer() {
|
||||
Session.set(
|
||||
'showNotificationsDrawer',
|
||||
!Session.get('showNotificationsDrawer'),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#notifications
|
||||
position: relative
|
||||
|
||||
.notifications-drawer-toggle
|
||||
display: block
|
||||
line-height: 28px
|
||||
color: #f2f2f2
|
||||
margin: 0 10px
|
||||
width: 28px
|
||||
height: 28px
|
||||
text-align: center
|
||||
border: 0
|
||||
padding: 0
|
||||
|
||||
&.alert
|
||||
background-color: #eb4646;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
template(name='notificationsDrawer')
|
||||
section#notifications-drawer(class="{{#if $.Session.get 'showReadNotifications'}}show-read{{/if}}")
|
||||
.header
|
||||
if $.Session.get 'showReadNotifications'
|
||||
a.toggle-read {{_ 'filter-by-unread'}}
|
||||
else
|
||||
a.toggle-read {{_ 'view-all'}}
|
||||
h5 {{_ 'notifications'}}
|
||||
if($gt unreadNotifications 0)
|
||||
|(#{unreadNotifications})
|
||||
a.fa.fa-times-thin.close
|
||||
ul.notifications
|
||||
each transformedProfile.notifications
|
||||
+notification(activityData=activity index=dbIndex read=read)
|
||||
if($gt unreadNotifications 0)
|
||||
a.all-read {{_ 'mark-all-as-read'}}
|
||||
if ($and ($.Session.get 'showReadNotifications') ($gt readNotifications 0))
|
||||
a.remove-read
|
||||
i.fa.fa-trash
|
||||
| {{_ 'remove-all-read'}}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { toggleNotificationsDrawer } from './notifications.js';
|
||||
|
||||
Template.notificationsDrawer.onCreated(function() {
|
||||
Meteor.subscribe('notificationActivities');
|
||||
Meteor.subscribe('notificationCards');
|
||||
Meteor.subscribe('notificationUsers');
|
||||
Meteor.subscribe('notificationsAttachments');
|
||||
Meteor.subscribe('notificationChecklistItems');
|
||||
Meteor.subscribe('notificationChecklists');
|
||||
Meteor.subscribe('notificationComments');
|
||||
Meteor.subscribe('notificationLists');
|
||||
Meteor.subscribe('notificationSwimlanes');
|
||||
});
|
||||
|
||||
Template.notificationsDrawer.helpers({
|
||||
transformedProfile() {
|
||||
return Users.findOne(Meteor.userId());
|
||||
},
|
||||
readNotifications() {
|
||||
const readNotifications = _.filter(
|
||||
Meteor.user().profile.notifications,
|
||||
v => !!v.read,
|
||||
);
|
||||
return readNotifications.length;
|
||||
},
|
||||
});
|
||||
|
||||
Template.notificationsDrawer.events({
|
||||
'click .all-read'() {
|
||||
const notifications = Meteor.user().profile.notifications;
|
||||
for (const index in notifications) {
|
||||
if (notifications.hasOwnProperty(index) && !notifications[index].read) {
|
||||
const update = {};
|
||||
update[`profile.notifications.${index}.read`] = Date.now();
|
||||
Users.update(Meteor.userId(), { $set: update });
|
||||
}
|
||||
}
|
||||
},
|
||||
'click .close'() {
|
||||
toggleNotificationsDrawer();
|
||||
},
|
||||
'click .toggle-read'() {
|
||||
Session.set('showReadNotifications', !Session.get('showReadNotifications'));
|
||||
},
|
||||
'click .remove-read'() {
|
||||
const user = Meteor.user();
|
||||
for (const notification of user.profile.notifications) {
|
||||
if (notification.read) {
|
||||
user.removeNotification(notification.activity);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
belize = #2980b9
|
||||
|
||||
section#notifications-drawer
|
||||
position: fixed
|
||||
top: 28px
|
||||
right: 0
|
||||
width: 400px
|
||||
background-color: #fafafa
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.15)
|
||||
border-radius: 2px
|
||||
max-height: calc(100vh - 28px - 36px)
|
||||
color: black
|
||||
padding-top 36px
|
||||
|
||||
a:hover
|
||||
color: belize !important
|
||||
|
||||
.header
|
||||
position: fixed
|
||||
top 28px
|
||||
right 0
|
||||
width calc(400px - 32px)
|
||||
padding: 8px 16px
|
||||
background: #ededed
|
||||
border-bottom: 1px solid #dbdbdb
|
||||
z-index 2
|
||||
|
||||
.toggle-read
|
||||
position absolute
|
||||
left 16px
|
||||
top calc(50% - 8px)
|
||||
color belize
|
||||
|
||||
h5
|
||||
text-align: center
|
||||
margin: 0
|
||||
|
||||
.close
|
||||
position: absolute
|
||||
top: calc(50% - 12px)
|
||||
right: 12px
|
||||
font-size: 24px
|
||||
height: 24px
|
||||
line-height: 24px
|
||||
opacity 1
|
||||
|
||||
.all-read,
|
||||
.remove-read
|
||||
color belize
|
||||
background-color: #fafafa
|
||||
margin 8px 16px 12px
|
||||
display inline-block
|
||||
|
||||
.remove-read
|
||||
float right
|
||||
|
||||
&:hover
|
||||
color #eb4646 !important
|
||||
|
||||
i.fa
|
||||
color inherit
|
||||
|
||||
|
||||
ul.notifications
|
||||
display: block
|
||||
padding: 0px 16px
|
||||
margin: 0
|
||||
height: calc(100vh - 102px)
|
||||
overflow-y: scroll
|
||||
@@ -110,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
|
||||
@@ -159,7 +159,7 @@ template(name="newUserPopup")
|
||||
//label.hide.userId(type="text" value=user._id)
|
||||
label
|
||||
| {{_ 'fullname'}}
|
||||
input.js-profile-fullname(type="text" value="" autofocus)
|
||||
input.js-profile-fullname(type="text" value="")
|
||||
label
|
||||
| {{_ 'username'}}
|
||||
span.error.hide.username-taken
|
||||
|
||||
@@ -245,7 +245,7 @@ template(name="outgoingWebhooksPopup")
|
||||
b
|
||||
.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
|
||||
@@ -257,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")
|
||||
@@ -267,7 +267,14 @@ template(name="outgoingWebhooksPopup")
|
||||
|
||||
template(name="boardMenuPopup")
|
||||
ul.pop-over-list
|
||||
li: a.js-custom-fields {{_ 'custom-fields'}}
|
||||
li
|
||||
a.js-open-rules-view(title="{{_ 'rules'}}")
|
||||
i.fa.fa-magic
|
||||
| {{_ 'rules'}}
|
||||
li
|
||||
a.js-custom-fields
|
||||
i.fa.fa-list-alt
|
||||
| {{_ 'custom-fields'}}
|
||||
li
|
||||
a.js-open-archives
|
||||
i.fa.fa-archive
|
||||
@@ -291,10 +298,11 @@ template(name="boardMenuPopup")
|
||||
if currentUser.isBoardAdmin
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrl}}", download="{{exportFilename}}")
|
||||
i.fa.fa-share-alt
|
||||
| {{_ 'export-board'}}
|
||||
if withApi
|
||||
li
|
||||
a(href="{{exportUrl}}", download="{{exportFilename}}")
|
||||
i.fa.fa-share-alt
|
||||
| {{_ 'export-board'}}
|
||||
li
|
||||
a.js-outgoing-webhooks
|
||||
i.fa.fa-globe
|
||||
@@ -319,11 +327,12 @@ template(name="boardMenuPopup")
|
||||
if isSandstorm
|
||||
hr
|
||||
ul.pop-over-list
|
||||
li
|
||||
a(href="{{exportUrl}}", download="{{exportFilename}}")
|
||||
i.fa.fa-share-alt
|
||||
i.fa.fa-sign-out
|
||||
| {{_ 'export-board'}}
|
||||
if withApi
|
||||
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
|
||||
|
||||
@@ -182,6 +182,10 @@ Template.memberPopup.helpers({
|
||||
|
||||
Template.boardMenuPopup.events({
|
||||
'click .js-rename-board': Popup.open('boardChangeTitle'),
|
||||
'click .js-open-rules-view'() {
|
||||
Modal.openWide('rulesMain');
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-custom-fields'() {
|
||||
Sidebar.setView('customFields');
|
||||
Popup.close();
|
||||
@@ -192,14 +196,14 @@ Template.boardMenuPopup.events({
|
||||
},
|
||||
'click .js-change-board-color': Popup.open('boardChangeColor'),
|
||||
'click .js-change-language': Popup.open('changeLanguage'),
|
||||
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function() {
|
||||
'click .js-archive-board ': Popup.afterConfirm('archiveBoard', function () {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
currentBoard.archive();
|
||||
// XXX We should have some kind of notification on top of the page to
|
||||
// confirm that the board was successfully archived.
|
||||
FlowRouter.go('home');
|
||||
}),
|
||||
'click .js-delete-board': Popup.afterConfirm('deleteBoard', function() {
|
||||
'click .js-delete-board': Popup.afterConfirm('deleteBoard', function () {
|
||||
const currentBoard = Boards.findOne(Session.get('currentBoard'));
|
||||
Popup.close();
|
||||
Boards.remove(currentBoard._id);
|
||||
@@ -211,7 +215,18 @@ Template.boardMenuPopup.events({
|
||||
'click .js-card-settings': Popup.open('boardCardSettings'),
|
||||
});
|
||||
|
||||
|
||||
Template.boardMenuPopup.onCreated(function () {
|
||||
this.apiEnabled = new ReactiveVar(false);
|
||||
Meteor.call('_isApiEnabled', (e, result) => {
|
||||
this.apiEnabled.set(result)
|
||||
})
|
||||
})
|
||||
|
||||
Template.boardMenuPopup.helpers({
|
||||
withApi() {
|
||||
return Template.instance().apiEnabled.get()
|
||||
},
|
||||
exportUrl() {
|
||||
const params = {
|
||||
boardId: Session.get('currentBoard'),
|
||||
@@ -233,7 +248,7 @@ Template.memberPopup.events({
|
||||
Popup.close();
|
||||
},
|
||||
'click .js-change-role': Popup.open('changePermissions'),
|
||||
'click .js-remove-member': Popup.afterConfirm('removeMember', function() {
|
||||
'click .js-remove-member': Popup.afterConfirm('removeMember', function () {
|
||||
const boardId = Session.get('currentBoard');
|
||||
const memberId = this.userId;
|
||||
Cards.find({ boardId, members: memberId }).forEach(card => {
|
||||
@@ -574,7 +589,7 @@ BlazeComponent.extendComponent({
|
||||
'subtext-with-parent',
|
||||
'no-parent',
|
||||
];
|
||||
options.forEach(function(element) {
|
||||
options.forEach(function (element) {
|
||||
if (element !== value) {
|
||||
$(`#${element} ${MCB}`).toggleClass(CKCLS, false);
|
||||
$(`#${element}`).toggleClass(CKCLS, false);
|
||||
|
||||
@@ -45,6 +45,24 @@ template(name="filterSidebar")
|
||||
if Filter.members.isSelected _id
|
||||
i.fa.fa-check
|
||||
hr
|
||||
ul.sidebar-list
|
||||
li(class="{{#if Filter.assignees.isSelected undefined}}active{{/if}}")
|
||||
a.name.js-toggle-assignee-filter
|
||||
span.sidebar-list-item-description
|
||||
| {{_ 'filter-no-assignee'}}
|
||||
if Filter.assignees.isSelected undefined
|
||||
i.fa.fa-check
|
||||
each currentBoard.activeMembers
|
||||
with getUser userId
|
||||
li(class="{{#if Filter.assignees.isSelected _id}}active{{/if}}")
|
||||
a.name.js-toggle-assignee-filter
|
||||
+userAvatar(userId=this._id)
|
||||
span.sidebar-list-item-description
|
||||
= profile.fullname
|
||||
| (<span class="username">{{ username }}</span>)
|
||||
if Filter.assignees.isSelected _id
|
||||
i.fa.fa-check
|
||||
hr
|
||||
ul.sidebar-list
|
||||
li(class="{{#if Filter.customFields.isSelected undefined}}active{{/if}}")
|
||||
a.name.js-toggle-custom-fields-filter
|
||||
|
||||
@@ -18,6 +18,11 @@ BlazeComponent.extendComponent({
|
||||
Filter.members.toggle(this.currentData()._id);
|
||||
Filter.resetExceptions();
|
||||
},
|
||||
'click .js-toggle-assignee-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.assignees.toggle(this.currentData()._id);
|
||||
Filter.resetExceptions();
|
||||
},
|
||||
'click .js-toggle-archive-filter'(evt) {
|
||||
evt.preventDefault();
|
||||
Filter.archive.toggle(this.currentData()._id);
|
||||
|
||||
@@ -98,12 +98,12 @@ template(name="changeLanguagePopup")
|
||||
|
||||
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-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
|
||||
|
||||
@@ -166,6 +166,8 @@ Template.changeLanguagePopup.helpers({
|
||||
name = 'Igbo';
|
||||
} else if (lang.name === 'oc') {
|
||||
name = 'Occitan';
|
||||
} else if (lang.name === '繁体中文(台湾)') {
|
||||
name = '繁體中文(台灣)';
|
||||
}
|
||||
return { tag, name };
|
||||
}).sort(function(a, b) {
|
||||
|
||||
@@ -459,13 +459,21 @@ Filter = {
|
||||
// before changing the schema.
|
||||
labelIds: new SetFilter(),
|
||||
members: new SetFilter(),
|
||||
assignees: new SetFilter(),
|
||||
archive: new SetFilter(),
|
||||
hideEmpty: new SetFilter(),
|
||||
customFields: new SetFilter('_id'),
|
||||
advanced: new AdvancedFilter(),
|
||||
lists: new AdvancedFilter(), // we need the ability to filter list by name as well
|
||||
|
||||
_fields: ['labelIds', 'members', 'archive', 'hideEmpty', 'customFields'],
|
||||
_fields: [
|
||||
'labelIds',
|
||||
'members',
|
||||
'assignees',
|
||||
'archive',
|
||||
'hideEmpty',
|
||||
'customFields',
|
||||
],
|
||||
|
||||
// We don't filter cards that have been added after the last filter change. To
|
||||
// implement this we keep the id of these cards in this `_exceptions` fields
|
||||
|
||||
@@ -26,6 +26,27 @@ FlowRouter.route('/', {
|
||||
},
|
||||
});
|
||||
|
||||
FlowRouter.route('/public', {
|
||||
name: 'public',
|
||||
triggersEnter: [AccountsTemplates.ensureSignedIn],
|
||||
action() {
|
||||
Session.set('currentBoard', null);
|
||||
Session.set('currentList', null);
|
||||
Session.set('currentCard', null);
|
||||
|
||||
Filter.reset();
|
||||
EscapeActions.executeAll();
|
||||
|
||||
Utils.manageCustomUI();
|
||||
Utils.manageMatomo();
|
||||
|
||||
BlazeLayout.render('defaultLayout', {
|
||||
headerBar: 'boardListHeaderBar',
|
||||
content: 'boardList',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
FlowRouter.route('/b/:id/:slug', {
|
||||
name: 'board',
|
||||
action(params) {
|
||||
|
||||
+18
-1
@@ -240,6 +240,11 @@ services:
|
||||
# https://github.com/wekan/wekan/pull/2560
|
||||
- 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
|
||||
- CARD_OPENED_WEBHOOK_ENABLED=false
|
||||
@@ -249,6 +254,11 @@ services:
|
||||
#-MAX_IMAGE_PIXEL=1024
|
||||
#-IMAGE_COMPRESS_RATIO=80
|
||||
#---------------------------------------------------------------
|
||||
# ==== NOTIFICATION TRAY AFTER READ DAYS BEFORE REMOVE =====
|
||||
# Number of days after a notification is read before we remove it.
|
||||
# Default: 2
|
||||
#- NOTIFICATION_TRAY_AFTER_READ_DAYS_BEFORE_REMOVE=2
|
||||
#---------------------------------------------------------------
|
||||
# ==== BIGEVENTS DUE ETC NOTIFICATIONS =====
|
||||
# https://github.com/wekan/wekan/pull/2541
|
||||
# Introduced a system env var BIGEVENTS_PATTERN default as "NONE",
|
||||
@@ -504,18 +514,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
|
||||
@@ -584,6 +598,9 @@ services:
|
||||
# example : LOGOUT_ON_MINUTES=55
|
||||
#- LOGOUT_ON_MINUTES=
|
||||
#-------------------------------------------------------------------
|
||||
# Hide password login form
|
||||
# - PASSWORD_LOGIN_ENABLED=true
|
||||
#-------------------------------------------------------------------
|
||||
depends_on:
|
||||
- wekandb
|
||||
|
||||
|
||||
@@ -56,3 +56,10 @@ mongodb-replicaset:
|
||||
This section controls the scale of the MongoDB redundant Replica Set.
|
||||
|
||||
**replicas:** This is the number of MongoDB instances to include in the set. You can set this to 1 for a single server - this will still allow you to scale-up later with a helm upgrade.
|
||||
|
||||
### Install OCP route
|
||||
If you use this chart to deploy Wekan on an OCP cluster, you can create route instead of ingress with following command:
|
||||
|
||||
``` bash
|
||||
$ helm template --set route.enabled=true,ingress.enabled=false values.yaml . | oc apply -f-
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
{{- if .Values.route.enabled -}}
|
||||
{{- $fullName := include "wekan.fullname" . -}}
|
||||
apiVersion: route.openshift.io/v1
|
||||
kind: Route
|
||||
metadata:
|
||||
annotations:
|
||||
haproxy.router.openshift.io/timeout: 4m
|
||||
openshift.io/host.generated: "true"
|
||||
labels:
|
||||
app: {{ template "wekan.name" . }}
|
||||
service: {{ template "wekan.name" . }}
|
||||
name: {{ template "wekan.name" . }}
|
||||
spec:
|
||||
port:
|
||||
targetPort: http
|
||||
tls:
|
||||
termination: edge
|
||||
to:
|
||||
kind: Service
|
||||
name: {{ template "wekan.name" . }}
|
||||
weight: 100
|
||||
wildcardPolicy: None
|
||||
{{- end }}
|
||||
@@ -59,6 +59,9 @@ ingress:
|
||||
# hosts:
|
||||
# - wekan-example.local
|
||||
|
||||
route:
|
||||
enabled: false
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: 128Mi
|
||||
|
||||
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+20
-3
@@ -64,7 +64,7 @@
|
||||
"activity-unchecked-item": "unchecked %s in checklist %s of %s",
|
||||
"activity-checklist-added": "added checklist to %s",
|
||||
"activity-checklist-removed": "removed a checklist from %s",
|
||||
"activity-checklist-completed": "completed checklist __checklist__ at card __card__ at list __list__ at swimlane __swimlane__ at board __board__",
|
||||
"activity-checklist-completed": "completed checklist %s of %s",
|
||||
"activity-checklist-uncompleted": "uncompleted the checklist %s of %s",
|
||||
"activity-checklist-item-added": "added checklist item to '%s' in %s",
|
||||
"activity-checklist-item-removed": "removed a checklist item from '%s' in %s",
|
||||
@@ -152,6 +152,8 @@
|
||||
"card-spent": "Spent Time",
|
||||
"card-edit-attachments": "Edit attachments",
|
||||
"card-edit-custom-fields": "Edit custom fields",
|
||||
"card-start-voting": "Start voting",
|
||||
"card-cancel-voting": "Delete voting and all votes",
|
||||
"card-edit-labels": "Edit labels",
|
||||
"card-edit-members": "Edit members",
|
||||
"card-labels-title": "Change the labels for the card.",
|
||||
@@ -161,6 +163,13 @@
|
||||
"cardAttachmentsPopup-title": "Attach From",
|
||||
"cardCustomField-datePopup-title": "Change date",
|
||||
"cardCustomFieldsPopup-title": "Edit custom fields",
|
||||
"cardStartVotingPopup-title": "Start a vote",
|
||||
"positiveVoteMembersPopup-title": "Proponents",
|
||||
"negativeVoteMembersPopup-title": "Opponents",
|
||||
"vote-question": "Voting question",
|
||||
"vote-public": "Public vote",
|
||||
"vote-for-it": "for it",
|
||||
"vote-against": "against",
|
||||
"cardDeletePopup-title": "Delete Card?",
|
||||
"cardDetailsActionsPopup-title": "Card Actions",
|
||||
"cardLabelsPopup-title": "Labels",
|
||||
@@ -319,6 +328,7 @@
|
||||
"filter-clear": "Clear filter",
|
||||
"filter-no-label": "No label",
|
||||
"filter-no-member": "No member",
|
||||
"filter-no-assignee": "No assignee",
|
||||
"filter-no-custom-fields": "No Custom Fields",
|
||||
"filter-show-archive": "Show archived lists",
|
||||
"filter-hide-empty": "Hide empty lists",
|
||||
@@ -440,7 +450,7 @@
|
||||
"save": "Save",
|
||||
"search": "Search",
|
||||
"rules": "Rules",
|
||||
"search-cards": "Search from card/list titles and descriptions on this board",
|
||||
"search-cards": "Search from card/list titles, descriptions and custom fields on this board",
|
||||
"search-example": "Text to search for?",
|
||||
"select-color": "Select Color",
|
||||
"set-wip-limit-value": "Set a limit for the maximum number of tasks in this list",
|
||||
@@ -762,5 +772,12 @@
|
||||
"show-on-card": "Show on Card",
|
||||
"new": "New",
|
||||
"editUserPopup-title": "Edit User",
|
||||
"newUserPopup-title": "New User"
|
||||
"newUserPopup-title": "New User",
|
||||
"notifications": "Notifications",
|
||||
"view-all": "View All",
|
||||
"filter-by-unread": "Filter by Unread",
|
||||
"mark-all-as-read": "Mark all as read",
|
||||
"remove-all-read": "Remove all read",
|
||||
"allow-rename": "Allow Rename",
|
||||
"allowRenamePopup-title": "Allow Rename"
|
||||
}
|
||||
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
+779
-762
File diff suppressed because it is too large.
Load diff
Loaded 100 of 371 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user