Browse Source

Merge branch 'develop' of github.com:tracim/tracim_v2 into develop

AlexiCauvin 5 years ago
parent
commit
64f55e7c72
37 changed files with 950 additions and 464 deletions
  1. 1 0
      .gitignore
  2. 7 0
      backend/development.ini.sample
  3. 3 0
      backend/setup.py
  4. 9 3
      backend/tests_configs.ini
  5. 9 4
      backend/tracim_backend/__init__.py
  6. 31 0
      backend/tracim_backend/config.py
  7. 4 0
      backend/tracim_backend/exceptions.py
  8. 1 1
      backend/tracim_backend/lib/core/notifications.py
  9. 1 1
      backend/tracim_backend/lib/core/user.py
  10. 1 1
      backend/tracim_backend/lib/core/userworkspace.py
  11. 1 1
      backend/tracim_backend/lib/core/workspace.py
  12. 37 17
      backend/tracim_backend/lib/mail_notifier/notifier.py
  13. 29 0
      backend/tracim_backend/lib/utils/utils.py
  14. 6 0
      backend/tracim_backend/models/applications.py
  15. 23 2
      backend/tracim_backend/models/context_models.py
  16. 3 1
      backend/tracim_backend/templates/mail/content_update_body_html.mak
  17. 1 2
      backend/tracim_backend/templates/mail/created_account_body_html.mak
  18. 1 0
      backend/tracim_backend/tests/library/test_webdav.py
  19. 1 1
      backend/tracim_backend/views/core_api/schemas.py
  20. 0 1
      backend/tracim_backend/views/core_api/user_controller.py
  21. 67 0
      backend/tracim_backend/views/frontend.py
  22. 0 68
      frontend/dist/index.html
  23. 80 0
      frontend/dist/index.mak
  24. 53 3
      frontend/src/action-creator.async.js
  25. 9 4
      frontend/src/action-creator.sync.js
  26. 3 1
      frontend/src/component/Dashboard/ContentTypeBtn.styl
  27. 58 21
      frontend/src/component/Dashboard/MemberList.jsx
  28. 56 1
      frontend/src/component/Dashboard/MemberList.styl
  29. 66 0
      frontend/src/component/Dashboard/MoreInfo.jsx
  30. 29 0
      frontend/src/component/Dashboard/MoreInfo.styl
  31. 19 10
      frontend/src/component/Dashboard/RecentActivity.jsx
  32. 67 0
      frontend/src/component/Dashboard/RecentActivity.styl
  33. 75 0
      frontend/src/component/Dashboard/UserStatus.jsx
  34. 30 0
      frontend/src/component/Dashboard/UserStatus.styl
  35. 144 157
      frontend/src/container/Dashboard.jsx
  36. 6 163
      frontend/src/css/Dashboard.styl
  37. 19 1
      frontend/src/reducer/currentWorkspace.js

+ 1 - 0
.gitignore View File

@@ -5,3 +5,4 @@ frontend/dist/asset/tracim/tracim.vendor.bundle.js
5 5
 frontend_app_html-document/dist/html-document.app.js
6 6
 frontend_lib/dist/tracim_frontend_lib.js
7 7
 npm-debug.log
8
+package-lock.json

+ 7 - 0
backend/development.ini.sample View File

@@ -186,6 +186,13 @@ wsgidav.config_path = %(here)s/wsgidav.conf
186 186
 ## return error
187 187
 # preview.jpg.restricted_dims = True
188 188
 
189
+### Frontend
190
+frontend.serve = True
191
+# You can set dist folder of tracim frontend. by default, system
192
+# will try to get it automatically according to tracim_v2 repository
193
+# organisation.
194
+# frontend.dist_folder_path = /home/user/tracim_v2/frontend/dist
195
+
189 196
 ###
190 197
 # wsgi server configuration
191 198
 ###

+ 3 - 0
backend/setup.py View File

@@ -41,6 +41,9 @@ requires = [
41 41
     'lxml',
42 42
     'redis',
43 43
     'rq',
44
+    # frontend file serve
45
+    'pyramid_mako',
46
+    'spectra',
44 47
 ]
45 48
 
46 49
 tests_require = [

+ 9 - 3
backend/tests_configs.ini View File

@@ -4,7 +4,7 @@ depot_storage_name = test
4 4
 depot_storage_dir = /tmp/test/depot
5 5
 user.auth_token.validity = 604800
6 6
 preview_cache_dir = /tmp/test/preview_cache
7
-
7
+website.base_url = http://localhost:6543
8 8
 [app:command_test]
9 9
 use = egg:tracim_backend
10 10
 sqlalchemy.url = sqlite:///tracim_test.sqlite
@@ -12,6 +12,7 @@ depot_storage_name = test
12 12
 depot_storage_dir = /tmp/test/depot
13 13
 user.auth_token.validity = 604800
14 14
 preview_cache_dir = /tmp/test/preview_cache
15
+website.base_url = http://localhost:6543
15 16
 
16 17
 [mail_test]
17 18
 sqlalchemy.url = sqlite:///:memory:
@@ -37,6 +38,7 @@ email.notification.smtp.server = 127.0.0.1
37 38
 email.notification.smtp.port = 1025
38 39
 email.notification.smtp.user = test_user
39 40
 email.notification.smtp.password = just_a_password
41
+website.base_url = http://localhost:6543
40 42
 
41 43
 [mail_test_async]
42 44
 sqlalchemy.url = sqlite:///:memory:
@@ -63,6 +65,7 @@ email.notification.smtp.server = 127.0.0.1
63 65
 email.notification.smtp.port = 1025
64 66
 email.notification.smtp.user = test_user
65 67
 email.notification.smtp.password = just_a_password
68
+website.base_url = http://localhost:6543
66 69
 
67 70
 [functional_test]
68 71
 sqlalchemy.url = sqlite:///tracim_test.sqlite
@@ -72,6 +75,7 @@ user.auth_token.validity = 604800
72 75
 preview_cache_dir = /tmp/test/preview_cache
73 76
 preview.jpg.restricted_dims = True
74 77
 email.notification.activated = false
78
+website.base_url = http://localhost:6543
75 79
 
76 80
 [functional_test_no_db]
77 81
 sqlalchemy.url = sqlite://
@@ -81,6 +85,7 @@ user.auth_token.validity = 604800
81 85
 preview_cache_dir = /tmp/test/preview_cache
82 86
 preview.jpg.restricted_dims = True
83 87
 email.notification.activated = false
88
+website.base_url = http://localhost:6543
84 89
 
85 90
 [functional_test_with_mail_test_sync]
86 91
 sqlalchemy.url = sqlite:///tracim_test.sqlite
@@ -105,7 +110,7 @@ email.notification.smtp.server = 127.0.0.1
105 110
 email.notification.smtp.port = 1025
106 111
 email.notification.smtp.user = test_user
107 112
 email.notification.smtp.password = just_a_password
108
-
113
+website.base_url = http://localhost:6543
109 114
 
110 115
 [functional_test_with_mail_test_async]
111 116
 sqlalchemy.url = sqlite:///tracim_test.sqlite
@@ -129,4 +134,5 @@ email.notification.processing_mode = async
129 134
 email.notification.smtp.server = 127.0.0.1
130 135
 email.notification.smtp.port = 1025
131 136
 email.notification.smtp.user = test_user
132
-email.notification.smtp.password = just_a_password
137
+email.notification.smtp.password = just_a_password
138
+website.base_url = http://localhost:6543

+ 9 - 4
backend/tracim_backend/__init__.py View File

@@ -1,6 +1,4 @@
1 1
 # -*- coding: utf-8 -*-
2
-
3
-
4 2
 try:  # Python 3.5+
5 3
     from http import HTTPStatus
6 4
 except ImportError:
@@ -9,7 +7,6 @@ except ImportError:
9 7
 from pyramid.config import Configurator
10 8
 from pyramid.authentication import BasicAuthAuthenticationPolicy
11 9
 from hapic.ext.pyramid import PyramidContext
12
-from pyramid.exceptions import NotFound
13 10
 from sqlalchemy.exc import OperationalError
14 11
 
15 12
 from tracim_backend.extensions import hapic
@@ -30,8 +27,10 @@ from tracim_backend.views.core_api.user_controller import UserController
30 27
 from tracim_backend.views.core_api.workspace_controller import WorkspaceController
31 28
 from tracim_backend.views.contents_api.comment_controller import CommentController
32 29
 from tracim_backend.views.contents_api.file_controller import FileController
30
+from tracim_backend.views.frontend import FrontendController
33 31
 from tracim_backend.views.errors import ErrorSchema
34 32
 from tracim_backend.exceptions import NotAuthenticated
33
+from tracim_backend.exceptions import PageNotFound
35 34
 from tracim_backend.exceptions import UserNotActive
36 35
 from tracim_backend.exceptions import InvalidId
37 36
 from tracim_backend.exceptions import InsufficientUserProfile
@@ -86,7 +85,7 @@ def web(global_config, **local_settings):
86 85
     hapic.set_context(context)
87 86
     # INFO - G.M - 2018-07-04 - global-context exceptions
88 87
     # Not found
89
-    context.handle_exception(NotFound, HTTPStatus.NOT_FOUND)
88
+    context.handle_exception(PageNotFound, HTTPStatus.NOT_FOUND)
90 89
     # Bad request
91 90
     context.handle_exception(WorkspaceNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
92 91
     context.handle_exception(UserNotFoundInTracimRequest, HTTPStatus.BAD_REQUEST)  # nopep8
@@ -106,6 +105,7 @@ def web(global_config, **local_settings):
106 105
     context.handle_exception(OperationalError, HTTPStatus.INTERNAL_SERVER_ERROR)
107 106
     context.handle_exception(Exception, HTTPStatus.INTERNAL_SERVER_ERROR)
108 107
 
108
+
109 109
     # Add controllers
110 110
     session_controller = SessionController()
111 111
     system_controller = SystemController()
@@ -124,6 +124,11 @@ def web(global_config, **local_settings):
124 124
     configurator.include(thread_controller.bind, route_prefix=BASE_API_V2)
125 125
     configurator.include(file_controller.bind, route_prefix=BASE_API_V2)
126 126
 
127
+    if app_config.FRONTEND_SERVE:
128
+        configurator.include('pyramid_mako')
129
+        frontend_controller = FrontendController(app_config.FRONTEND_DIST_FOLDER_PATH)  # nopep8
130
+        configurator.include(frontend_controller.bind)
131
+
127 132
     hapic.add_documentation_view(
128 133
         '/api/v2/doc',
129 134
         'Tracim v2 API',

+ 31 - 0
backend/tracim_backend/config.py View File

@@ -1,5 +1,7 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 from urllib.parse import urlparse
3
+
4
+import os
3 5
 from paste.deploy.converters import asbool
4 6
 from tracim_backend.lib.utils.logger import logger
5 7
 from depot.manager import DepotManager
@@ -79,6 +81,12 @@ class CFG(object):
79 81
             'website.base_url',
80 82
             '',
81 83
         )
84
+        if not self.WEBSITE_BASE_URL:
85
+            raise Exception(
86
+                'website.base_url is needed in order to have correct path in'
87
+                'few place like in email.'
88
+                'You should set it with frontend root url.'
89
+            )
82 90
 
83 91
         # TODO - G.M - 26-03-2018 - [Cleanup] These params seems deprecated for tracimv2,  # nopep8
84 92
         # Verify this
@@ -435,6 +443,29 @@ class CFG(object):
435 443
 
436 444
         self.PREVIEW_JPG_ALLOWED_DIMS = allowed_dims
437 445
 
446
+        self.FRONTEND_SERVE = asbool(settings.get(
447
+            'frontend.serve', False
448
+        ))
449
+
450
+        # INFO - G.M - 2018-08-06 - we pretend that frontend_dist_folder
451
+        # is probably in frontend subfolder
452
+        # of tracim_v2 parent of both backend and frontend
453
+        backend_folder = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))  # nopep8
454
+        tracim_v2_folder = os.path.dirname(backend_folder)
455
+        frontend_dist_folder = os.path.join(tracim_v2_folder, 'frontend', 'dist')  # nopep8
456
+
457
+        self.FRONTEND_DIST_FOLDER_PATH = settings.get(
458
+            'frontend.dist_folder_path', frontend_dist_folder
459
+        )
460
+
461
+        # INFO - G.M - 2018-08-06 - We check dist folder existence
462
+        if self.FRONTEND_SERVE and not os.path.isdir(self.FRONTEND_DIST_FOLDER_PATH):  # nopep8
463
+            raise Exception(
464
+                'ERROR: {} folder does not exist as folder. '
465
+                'please set frontend.dist_folder.path'
466
+                'with a correct value'.format(self.FRONTEND_DIST_FOLDER_PATH)
467
+            )
468
+
438 469
     def configure_filedepot(self):
439 470
         depot_storage_name = self.DEPOT_STORAGE_NAME
440 471
         depot_storage_path = self.DEPOT_STORAGE_DIR

+ 4 - 0
backend/tracim_backend/exceptions.py View File

@@ -207,3 +207,7 @@ class PreviewDimNotAllowed(TracimException):
207 207
 
208 208
 class TooShortAutocompleteString(TracimException):
209 209
     pass
210
+
211
+
212
+class PageNotFound(TracimException):
213
+    pass

+ 1 - 1
backend/tracim_backend/lib/core/notifications.py View File

@@ -1,7 +1,7 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 from sqlalchemy.orm import Session
3 3
 
4
-from tracim_backend import CFG
4
+from tracim_backend.config import CFG
5 5
 from tracim_backend.lib.utils.logger import logger
6 6
 from tracim_backend.models.auth import User
7 7
 from tracim_backend.models.data import Content

+ 1 - 1
backend/tracim_backend/lib/core/user.py View File

@@ -9,7 +9,7 @@ from sqlalchemy.orm import Session
9 9
 from sqlalchemy.orm import Query
10 10
 from sqlalchemy.orm.exc import NoResultFound
11 11
 
12
-from tracim_backend import CFG
12
+from tracim_backend.config import CFG
13 13
 from tracim_backend.models.auth import User
14 14
 from tracim_backend.models.auth import Group
15 15
 from tracim_backend.exceptions import NoUserSetted

+ 1 - 1
backend/tracim_backend/lib/core/userworkspace.py View File

@@ -1,7 +1,7 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import typing
3 3
 
4
-from tracim_backend import CFG
4
+from tracim_backend.config import CFG
5 5
 from tracim_backend.models.context_models import UserRoleWorkspaceInContext
6 6
 from tracim_backend.models.roles import WorkspaceRoles
7 7
 

+ 1 - 1
backend/tracim_backend/lib/core/workspace.py View File

@@ -5,7 +5,7 @@ from sqlalchemy.orm import Query
5 5
 from sqlalchemy.orm import Session
6 6
 from sqlalchemy.orm.exc import NoResultFound
7 7
 
8
-from tracim_backend import CFG
8
+from tracim_backend.config import CFG
9 9
 from tracim_backend.exceptions import EmptyLabelNotAllowed
10 10
 from tracim_backend.exceptions import WorkspaceNotFound
11 11
 from tracim_backend.lib.utils.translation import fake_translator as _

+ 37 - 17
backend/tracim_backend/lib/mail_notifier/notifier.py View File

@@ -10,16 +10,19 @@ from lxml.html.diff import htmldiff
10 10
 from mako.template import Template
11 11
 from sqlalchemy.orm import Session
12 12
 
13
-from tracim_backend import CFG
13
+from tracim_backend.config import CFG
14 14
 from tracim_backend.lib.core.notifications import INotifier
15 15
 from tracim_backend.lib.mail_notifier.sender import EmailSender
16 16
 from tracim_backend.lib.mail_notifier.utils import SmtpConfiguration, EST
17 17
 from tracim_backend.lib.mail_notifier.sender import send_email_through
18 18
 from tracim_backend.lib.core.workspace import WorkspaceApi
19 19
 from tracim_backend.lib.utils.logger import logger
20
-from tracim_backend.models import User
20
+from tracim_backend.lib.utils.utils import get_login_frontend_url
21
+from tracim_backend.lib.utils.utils import get_email_logo_frontend_url
21 22
 from tracim_backend.models.auth import User
22 23
 from tracim_backend.models.contents import CONTENT_TYPES
24
+from tracim_backend.models.context_models import ContentInContext
25
+from tracim_backend.models.context_models import WorkspaceInContext
23 26
 from tracim_backend.models.data import ActionDescription
24 27
 from tracim_backend.models.data import Content
25 28
 from tracim_backend.models.data import UserRoleInWorkspace
@@ -234,7 +237,13 @@ class EmailManager(object):
234 237
             show_archived=True,
235 238
             show_deleted=True,
236 239
         ).get_one(event_content_id, CONTENT_TYPES.Any_SLUG)
237
-        main_content = content.parent if content.type == CONTENT_TYPES.Comment.slug else content
240
+        workspace_api = WorkspaceApi(
241
+            session=self.session,
242
+            current_user=user,
243
+            config=self.config,
244
+        )
245
+        workpace_in_context = workspace_api.get_workspace_with_context(workspace_api.get_one(content.workspace_id))  # nopep8
246
+        main_content = content.parent if content.type == CONTENT_TYPES.Comment.slug else content  # nopep8
238 247
         notifiable_roles = WorkspaceApi(
239 248
             current_user=user,
240 249
             session=self.session,
@@ -265,7 +274,7 @@ class EmailManager(object):
265 274
             # INFO - G.M - 2017-11-15 - set content_id in header to permit reply
266 275
             # references can have multiple values, but only one in this case.
267 276
             replyto_addr = self.config.EMAIL_NOTIFICATION_REPLY_TO_EMAIL.replace( # nopep8
268
-                '{content_id}',str(content.content_id)
277
+                '{content_id}', str(content.content_id)
269 278
             )
270 279
 
271 280
             reference_addr = self.config.EMAIL_NOTIFICATION_REFERENCES_EMAIL.replace( #nopep8
@@ -297,8 +306,21 @@ class EmailManager(object):
297 306
             # To link this email to a content we create a virtual parent
298 307
             # in reference who contain the content_id.
299 308
             message['References'] = formataddr(('',reference_addr))
300
-            body_text = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
301
-            body_html = self._build_email_body_for_content(self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML, role, content, user)
309
+            content_in_context = content_api.get_content_in_context(content)
310
+            body_text = self._build_email_body_for_content(
311
+                self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT,
312
+                role,
313
+                content_in_context,
314
+                workpace_in_context,
315
+                user
316
+            )
317
+            body_html = self._build_email_body_for_content(
318
+                self.config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML,
319
+                role,
320
+                content_in_context,
321
+                workpace_in_context,
322
+                user
323
+            )
302 324
 
303 325
             part1 = MIMEText(body_text, 'plain', 'utf-8')
304 326
             part2 = MIMEText(body_html, 'html', 'utf-8')
@@ -362,9 +384,9 @@ class EmailManager(object):
362 384
             'user': user,
363 385
             'password': password,
364 386
             # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
365
-            'logo_url': '',
387
+            'logo_url': get_email_logo_frontend_url(self.config),
366 388
             # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for login_url  # nopep8
367
-            'login_url': self.config.WEBSITE_BASE_URL,
389
+            'login_url': get_login_frontend_url(self.config),
368 390
         }
369 391
         body_text = self._render_template(
370 392
             mako_template_filepath=text_template_file_path,
@@ -415,8 +437,9 @@ class EmailManager(object):
415 437
             self,
416 438
             mako_template_filepath: str,
417 439
             role: UserRoleInWorkspace,
418
-            content: Content,
419
-            actor: User
440
+            content_in_context: ContentInContext,
441
+            workspace_in_context: WorkspaceInContext,
442
+            actor: User,
420 443
     ) -> str:
421 444
         """
422 445
         Build an email body and return it as a string
@@ -424,24 +447,21 @@ class EmailManager(object):
424 447
         :param role: the role related to user to whom the email must be sent. The role is required (and not the user only) in order to show in the mail why the user receive the notification
425 448
         :param content: the content item related to the notification
426 449
         :param actor: the user at the origin of the action / notification (for example the one who wrote a comment
427
-        :param config: the global configuration
428 450
         :return: the built email body as string. In case of multipart email, this method must be called one time for text and one time for html
429 451
         """
430 452
         logger.debug(self, 'Building email content from MAKO template {}'.format(mako_template_filepath))
431
-
453
+        content = content_in_context.content
432 454
         main_title = content.label
433 455
         content_intro = ''
434 456
         content_text = ''
435 457
         call_to_action_text = ''
436 458
 
437
-        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for call_to_action_url  # nopep8
438
-        call_to_action_url =''
459
+        call_to_action_url = content_in_context.frontend_url
439 460
         # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for status_icon_url  # nopep8
440 461
         status_icon_url = ''
441
-        # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for workspace_url  # nopep8
442
-        workspace_url = ''
462
+        workspace_url = workspace_in_context.frontend_url
443 463
         # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for logo_url  # nopep8
444
-        logo_url = ''
464
+        logo_url = get_email_logo_frontend_url(self.config)
445 465
 
446 466
         action = content.get_last_action().id
447 467
         if ActionDescription.COMMENT == action:

+ 29 - 0
backend/tracim_backend/lib/utils/utils.py View File

@@ -2,6 +2,8 @@
2 2
 import datetime
3 3
 import random
4 4
 import string
5
+from enum import Enum
6
+
5 7
 from redis import Redis
6 8
 from rq import Queue
7 9
 
@@ -10,6 +12,32 @@ from tracim_backend.config import CFG
10 12
 DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
11 13
 DEFAULT_WEBDAV_CONFIG_FILE = "wsgidav.conf"
12 14
 DEFAULT_TRACIM_CONFIG_FILE = "development.ini"
15
+CONTENT_FRONTEND_URL_SCHEMA = 'workspaces/{workspace_id}/contents/{content_type}/{content_id}'  # nopep8
16
+WORKSPACE_FRONTEND_URL_SCHEMA = 'workspaces/{workspace_id}'  # nopep8
17
+
18
+
19
+def get_root_frontend_url(config: CFG) -> str:
20
+    """
21
+    Return website base url with always '/' at the end
22
+    """
23
+    base_url = ''
24
+    if config.WEBSITE_BASE_URL[-1] == '/':
25
+        base_url = config.WEBSITE_BASE_URL
26
+    else:
27
+        base_url = config.WEBSITE_BASE_URL + '/'
28
+    return base_url
29
+
30
+
31
+def get_login_frontend_url(config: CFG):
32
+    """
33
+    Return login page url
34
+    """
35
+    return get_root_frontend_url(config) + 'login'
36
+
37
+
38
+def get_email_logo_frontend_url(config: CFG):
39
+    # TODO - G.M - 11-06-2018 - [emailTemplateURL] correct value for email_logo_frontend_url  # nopep8
40
+    return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH4QUTDjMSlsws9AAAB89JREFUaN7tmWtwlFcZx3/PeXeXXLmKQJEGsFZApIUBnaqjUm4VgdYyCUib0cJoKLmHW5EWmlGoQkMICURgEPuhtiYDU7XcyVgKVugULZVCQAwUYyENYAmEXHb3PH7IFjYQNgnZHfnA+bKz+7573v//PPf/C/fWvdWhJWHfMbO4P479OpCAigeowWglPq3ARWfU9AJ7Cb/3MAXZn95dBLKKHsfIQmAUIq7rv6t6gU+BKCAGaEBkN9h88tLe7sgjTdjAJ5Y4OGYMIo80Aw8g4kakJyLxiDiIxABPoMwj5+XP3R0ESpP8WC1H1YuqBj4bUWwIB5gA0d/6/xFQFUpKHBJLHADyU9chTEc1BdEZqJ0O+ofb4xcPyuBIxoBQuHc4ov3w2zrwH0Nd0aBDccwgVPsjEg/4Uc4jegKfvkfMmQ9JSfECkFP0HGJeuu0TrJZhSacg9Xh4Caze2wsXC1B9GugO+BAqUIkC/QIinhYsYoEqYDt+fY2s8WXkFE5CnBIg+vaWtJuQukzy5te2l4Bz2ysTkxchshCRWERMU2BKT4RuiLT0vwagHsENPIyR7zAx2eGybws09AUZEQLHcNS5wMEdB8NjgaKyBNTuQuTLoWOAs6AHUd5DpRzxX0KMBe0M+gA4D4Hdw5mT72LJQORxkM6IfgxSC3wjyHpHEN8M8jKPtYeAq2VgvhHg9A4RvPUIKzCyBVe346SM9LZw1y5KShzOxfUhL60SmMfcNUXgisXYS/iNA/IzhNmBgH4I3E8Cxzpmgby9ffFoMSKTQxA4jZjRpI35qENZbF5Rb1SWgcwM7HsQr28KhZnVd55GPfqTkOCbaB+i2lR1uHa8nHYeda0EygJWGILHGXzndaBgz3CQ5Fb8/hSqr5E7uj4sBXBVSjlqN6FaixKHlS8BxCYtHhs/bUly9JT597U9C30/eRYiU4Nc5Q1gH+AG/gvsxJBH2vg3w9pDPTjqFDGxX0FkKOih2PtHOcbIBiPMMi5XT9eDo9/xlu+rDW2BpX+MAQnOCudRWcWFA3OwncYhrke5VpuK1SrW7+sTVgKv5NYjsr2p6TMY1QEGvhi4mmw89unWXaiX6QQEN1YHuHjpELm5lsxvV2MbhxAdWwCSiNfnCXsb7mc/Ih8hXED8J1T1k6awEAG+R+LCLqEJOL2vInwQOP46VHaSm9QYiI1nMU4xwv2oa3OHs0+LiaHuHKr/wOo/r1yuOQwcDEqVw2KdqITQBFJGelFWojITPz8kY+wmAAp3pWLkJVT7oGwkY3R5REar/Ln1qO7AyIfsLGxQ1eB60E3UDmi9kKWPOwmcvP69cM8kROYCXVB5FW18I4LToeLSbTGnKh2T9MIChLHBOI2lT9srMUDB9s7AM8CApj7H/yaZExsiOd9GnbngcjzOciPyVPOuW0RFB7ZvHnDck4FJgbOpwPoPRHpAdxs782bwQSwei3ty0aC2EVh61ANMutEy63Gi/l0VUfSJC7sIjAnhYYON4x7eNgI9KgeifDPIOyuuDygRWnGuKDcQF6roWrGxbSNgzDAgqBuV6ki7z9WT5y4DR0LdoqoVbSOgZhAi7hv4bXzEFarDG7yqbPmsgN2aovhdLdf+2jqBorIE4JGbIugZCvcsijQHr5ojN1tbVaustUWNNP6S0vy60AR+ut4Ndj7CYzdlgL7AixTtmRnRLOToVNAhQeDfR+yPr+iJrIbf/+pM62l0WP8RKNNCSCCTySuJjgT42MTnv4rqjEDvg6J+VH5T8/qynZSW+ts40EgCQtcQDcsDEN2d9IKe4U2hiR5jmG1ERt4QKrS4pkqK2zmRSQMaUsj6D5cuu3C5ppCY6IQLf2dn8CIjZk4Q+N961VnJvlxf+4Z6v/cojvsDYEQL4BXRHVTXfkysjqPfd3tA6YqOnXx293gnLgvV5xFBVa2iv/b6dEX91p+fbf9ImTnxX6gWAadbILARX+0rbEjxohwCFpBVlH1nyJea2KTFY+OduHUGeaFJoNN3gcwr1smu37qsze16y7pQ4a7pYFYj0iuAvho0jbTxJQBkrB6Ky70VGIDqamA9+WmnWtwrZ20/fN4uQA1rss52Slw8wIOZKg4/QIlTKBfY32DttobSZafDJS0Ka3YXYEx6UCXZid8/n6wJRwHIXrsEI7kB6+xHtQTDn8hLPQuipBb2oJOkgSQBPREuAlupq98cfa6mXsXnOFpfV5uQMBRxlMpP/kJpbmP4tNHC3SNBNiLycJAbleLlOXLGVTB33edRLUBkepAuegyRd1D7Pphh10Wr5mJuMT5fNo67L45NQSUlIBqsYlXqkqaiGy51es3eWQh5CDfmUeufTcaE9QBkFY7AyIuImXxT3bfI7doUvQayFbUDMSZYRHiLq64n2JByOXzvBzLGbkJ1GarnP4OPmBt60Or0v+E381Fdj+ILOhYT4p1ADOhTzcA3EdjeXvCtEwDIGNc0J6vNB/0FDXXbmk9uc05wMSYLq+nAgYArtWJ3keYW0eVUVheE9/3ArbYXkND+mbO2H8gk0AnAlGZAb3WlWpQ/I/o6q9JevWMxIyJdWWZ+V4xnOUaebQH4MVS2I/59eO3bFGbWdEiNiVhrmVncH+NfhMg0IA4RH1bfQnQlq9LKwiYnRbTB/9HmKLrWfg2j94HUoI1/Z3XOOe6te+vuWf8DkM0cb7DOQZgAAAAASUVORK5CYII='  # nopep8'
13 41
 
14 42
 
15 43
 def get_redis_connection(config: CFG) -> Redis:
@@ -96,3 +124,4 @@ def password_generator(
96 124
     :return: password as string
97 125
     """
98 126
     return ''.join(random.choice(chars) for char_number in range(length))
127
+

+ 6 - 0
backend/tracim_backend/models/applications.py View File

@@ -36,6 +36,12 @@ class Application(object):
36 36
         self.config = config
37 37
         self.main_route = main_route
38 38
 
39
+    # TODO - G.M - 2018-08-07 - Refactor slug coherence issue like this one.
40
+    # we probably should not have 2 kind of slug
41
+    @property
42
+    def minislug(self):
43
+        return self.slug.replace('contents/', '')
44
+
39 45
 
40 46
 # default apps
41 47
 calendar = Application(

+ 23 - 2
backend/tracim_backend/models/context_models.py View File

@@ -5,8 +5,11 @@ from enum import Enum
5 5
 
6 6
 from slugify import slugify
7 7
 from sqlalchemy.orm import Session
8
-from tracim_backend import CFG
8
+from tracim_backend.config import CFG
9 9
 from tracim_backend.config import PreviewDim
10
+from tracim_backend.lib.utils.utils import get_root_frontend_url
11
+from tracim_backend.lib.utils.utils import CONTENT_FRONTEND_URL_SCHEMA
12
+from tracim_backend.lib.utils.utils import WORKSPACE_FRONTEND_URL_SCHEMA
10 13
 from tracim_backend.models import User
11 14
 from tracim_backend.models.auth import Profile
12 15
 from tracim_backend.models.data import Content
@@ -14,7 +17,7 @@ from tracim_backend.models.data import ContentRevisionRO
14 17
 from tracim_backend.models.data import Workspace
15 18
 from tracim_backend.models.data import UserRoleInWorkspace
16 19
 from tracim_backend.models.roles import WorkspaceRoles
17
-from tracim_backend.models.workspace_menu_entries import default_workspace_menu_entry
20
+from tracim_backend.models.workspace_menu_entries import default_workspace_menu_entry  # nopep8
18 21
 from tracim_backend.models.workspace_menu_entries import WorkspaceMenuEntry
19 22
 from tracim_backend.models.contents import CONTENT_TYPES
20 23
 
@@ -462,6 +465,14 @@ class WorkspaceInContext(object):
462 465
         # apps)
463 466
         return default_workspace_menu_entry(self.workspace)
464 467
 
468
+    @property
469
+    def frontend_url(self):
470
+        root_frontend_url = get_root_frontend_url(self.config)
471
+        workspace_frontend_url = WORKSPACE_FRONTEND_URL_SCHEMA.format(
472
+            workspace_id=self.workspace_id,
473
+        )
474
+        return root_frontend_url + workspace_frontend_url
475
+
465 476
 
466 477
 class UserRoleWorkspaceInContext(object):
467 478
     """
@@ -660,6 +671,16 @@ class ContentInContext(object):
660 671
         assert self._user
661 672
         return not self.content.has_new_information_for(self._user)
662 673
 
674
+    @property
675
+    def frontend_url(self):
676
+        root_frontend_url = get_root_frontend_url(self.config)
677
+        content_frontend_url = CONTENT_FRONTEND_URL_SCHEMA.format(
678
+            workspace_id=self.workspace_id,
679
+            content_type=self.content_type,
680
+            content_id=self.content_id,
681
+        )
682
+        return root_frontend_url + content_frontend_url
683
+
663 684
 
664 685
 class RevisionInContext(object):
665 686
     """

+ 3 - 1
backend/tracim_backend/templates/mail/content_update_body_html.mak View File

@@ -45,7 +45,7 @@
45 45
             ${main_title}
46 46
             &mdash;&nbsp;<span style="font-weight: bold; color: #999; font-weight: bold;">
47 47
               ${status_label|n}
48
-              <img alt="status_icon" src="${status_icon_url}" style="vertical-align: middle;">
48
+              <img alt="" src="${status_icon_url}" style="vertical-align: middle;">
49 49
             </span>
50 50
         </td>
51 51
       </tr>
@@ -55,6 +55,8 @@
55 55
     <div id="content-body">
56 56
         <div>${content_text|n}</div>
57 57
         <div href='' id="call-to-action-container">
58
+            <span style=""> <a href="${call_to_action_url}"
59
+            id="call-to-action-button">${call_to_action_text}</a> </span> </div>
58 60
         </div>
59 61
     </div>
60 62
     

+ 1 - 2
backend/tracim_backend/templates/mail/created_account_body_html.mak View File

@@ -68,10 +68,9 @@
68 68
         </div>
69 69
         <div id="call-to-action-container">
70 70
 
71
-            ${_('To go to {website_title}, please click on following link'.format(
71
+            ${_('To go to {website_title}, please click on following link :'.format(
72 72
                 website_title=config.WEBSITE_TITLE
73 73
             ))}
74
-
75 74
             <span style="">
76 75
                 <a href="${login_url}" id='call-to-action-button'>${login_url}</a>
77 76
             </span>

+ 1 - 0
backend/tracim_backend/tests/library/test_webdav.py View File

@@ -29,6 +29,7 @@ class TestWebdavFactory(StandardTest):
29 29
         :return:
30 30
         """
31 31
         tracim_settings = {
32
+            'website.base_url': 'http://localhost:6543',
32 33
             'sqlalchemy.url': 'sqlite:///:memory:',
33 34
             'user.auth_token.validity': '604800',
34 35
             'depot_storage_dir': '/tmp/test/depot',

+ 1 - 1
backend/tracim_backend/views/core_api/schemas.py View File

@@ -48,7 +48,7 @@ class UserDigestSchema(marshmallow.Schema):
48 48
     user_id = marshmallow.fields.Int(dump_only=True, example=3)
49 49
     avatar_url = marshmallow.fields.Url(
50 50
         allow_none=True,
51
-        example="/api/v2/assets/avatars/suri-cate.jpg",
51
+        example="/api/v2/asset/avatars/suri-cate.jpg",
52 52
         description="avatar_url is the url to the image file. "
53 53
                     "If no avatar, then set it to null "
54 54
                     "(and frontend will interpret this with a default avatar)",

+ 0 - 1
backend/tracim_backend/views/core_api/user_controller.py View File

@@ -39,7 +39,6 @@ from tracim_backend.models.contents import CONTENT_TYPES
39 39
 SWAGGER_TAG__USER_ENDPOINTS = 'Users'
40 40
 
41 41
 
42
-
43 42
 class UserController(Controller):
44 43
 
45 44
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])

+ 67 - 0
backend/tracim_backend/views/frontend.py View File

@@ -0,0 +1,67 @@
1
+import os
2
+
3
+from pyramid.renderers import render_to_response
4
+from pyramid.config import Configurator
5
+from tracim_backend.exceptions import PageNotFound
6
+from tracim_backend.models.applications import applications
7
+from tracim_backend.views import BASE_API_V2
8
+from tracim_backend.lib.utils.request import TracimRequest
9
+from tracim_backend.views.controllers import Controller
10
+import spectra
11
+
12
+INDEX_PAGE_NAME = 'index.mak'
13
+APP_FRONTEND_PATH = 'app/{minislug}.app.js'
14
+
15
+
16
+class FrontendController(Controller):
17
+
18
+    def __init__(self, dist_folder_path: str):
19
+        self.dist_folder_path = dist_folder_path
20
+
21
+    def _get_index_file_path(self):
22
+        index_file_path = os.path.join(self.dist_folder_path, INDEX_PAGE_NAME)
23
+        if not os.path.exists(index_file_path):
24
+            raise FileNotFoundError()
25
+        return index_file_path
26
+
27
+    def not_found_view(self, context, request: TracimRequest):
28
+
29
+        if request.path.startswith(BASE_API_V2):
30
+            raise PageNotFound('{} is not a valid path'.format(request.path)) from context  # nopep8
31
+        return self.index(context, request)
32
+
33
+    def index(self, context, request: TracimRequest):
34
+        app_config = request.registry.settings['CFG']
35
+        # TODO - G.M - 2018-08-07 - Refactor autogen valid app list for frontend
36
+        frontend_apps = []
37
+        for app in applications:
38
+            app_frontend_path = APP_FRONTEND_PATH.replace('{minislug}',
39
+                                                          app.minislug)  # nopep8
40
+            app_path = os.path.join(self.dist_folder_path,
41
+                                    app_frontend_path)  # nopep8
42
+            if os.path.exists(app_path):
43
+                frontend_apps.append(app)
44
+        return render_to_response(
45
+            self._get_index_file_path(),
46
+            {
47
+                'colors': {
48
+                    'primary': spectra.html('#7d4e24'),
49
+                },
50
+                'applications': frontend_apps,
51
+            }
52
+        )
53
+
54
+    def bind(self, configurator: Configurator) -> None:
55
+
56
+        configurator.add_notfound_view(self.not_found_view)
57
+        # index.html for /index.html and /
58
+        configurator.add_route('root', '/', request_method='GET')
59
+        configurator.add_view(self.index, route_name='root')
60
+        configurator.add_route('index', INDEX_PAGE_NAME, request_method='GET')
61
+        configurator.add_view(self.index, route_name='index')
62
+
63
+        for dirname in os.listdir(self.dist_folder_path):
64
+            configurator.add_static_view(
65
+                name=dirname,
66
+                path=os.path.join(self.dist_folder_path, dirname)
67
+            )

+ 0 - 68
frontend/dist/index.html View File

@@ -1,68 +0,0 @@
1
-<!DOCTYPE html>
2
-<html>
3
-  <head>
4
-    <meta charset='utf-8' />
5
-    <meta name="viewport" content="width=device-width, user-scalable=no">
6
-    <title>Tracim</title>
7
-    <link rel='shortcut icon' type='image/x-icon' href='/asset/favicon.ico' >
8
-
9
-    <link rel="stylesheet" type="text/css" href="/asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
-    <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
-    <!--
12
-    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
13
-    -->
14
-    <link rel="stylesheet" type="text/css" href="/asset/hamburger/hamburgers.min.css">
15
-    <link rel="stylesheet" type="text/css" href="/asset/bootstrap/bootstrap-4.0.0-beta.css">
16
-
17
-    <style>
18
-      /* code bellow will be generated by backend */
19
-      .primaryColorFont { color: #7d4e24; }
20
-      .primaryColorFontDarken { color: #572800; }
21
-      .primaryColorFontLighten { color: #a3744a; }
22
-
23
-      .primaryColorFontHover:hover { color: #7d4e24; }
24
-      .primaryColorFontDarkenHover:hover { color: #572800; }
25
-      .primaryColorFontLightenHover:hover { color: #a3744a; }
26
-
27
-      .primaryColorBg { background-color: #7d4e24; }
28
-      .primaryColorBgDarken { background-color: #572800; }
29
-      .primaryColorBgLighten { background-color: #a3744a; }
30
-
31
-      .primaryColorBgHover:hover { background-color: #7d4e24; }
32
-      .primaryColorBgDarkenHover:hover { background-color: #572800; }
33
-      .primaryColorBgLightenHover:hover { background-color: #a3744a; }
34
-
35
-      .primaryColorBorder { border-color: #7d4e24; }
36
-      .primaryColorBorderDarken { border-color: #572800; }
37
-      .primaryColorBorderLighten { border-color: #a3744a; }
38
-
39
-      .primaryColorBorderHover:hover { border-color: #7d4e24; }
40
-      .primaryColorBorderDarkenHover:hover { border-color: #572800; }
41
-      .primaryColorBorderLightenHover:hover { border-color: #a3744a; }
42
-
43
-    </style>
44
-  </head>
45
-
46
-  <body>
47
-    <div id='content'></div>
48
-
49
-    <script type='text/javascript' src='/asset/tracim.vendor.bundle.js'></script>
50
-    <script type='text/javascript' src='/asset/tracim.app.entry.js'></script>
51
-
52
-    <script type='text/javascript' src='/app/workspace.app.js'></script>
53
-    <script type='text/javascript' src='/app/html-document.app.js'></script>
54
-    <script type='text/javascript' src='/app/thread.app.js'></script>
55
-    <!-- <script type='text/javascript' src='/app/file.app.js'></script> -->
56
-    <script type='text/javascript' src='/app/admin_workspace_user.app.js'></script>
57
-
58
-    <script type='text/javascript' src='/asset/bootstrap/jquery-3.2.1.js'></script>
59
-    <script type='text/javascript' src='/asset/bootstrap/popper-1.12.3.js'></script>
60
-    <script type='text/javascript' src='/asset/bootstrap/bootstrap-4.0.0-beta.2.js'></script>
61
-
62
-    <script type='text/javascript' src='/asset/tinymce/js/tinymce/jquery.tinymce.min.js'></script>
63
-    <script type='text/javascript' src='/asset/tinymce/js/tinymce/tinymce.min.js'></script>
64
-
65
-    <script type='text/javascript' src='/asset/tracim/appInterface.js'></script>
66
-    <script type='text/javascript' src='/asset/tracim/tinymceInit.js'></script>
67
-  </body>
68
-</html>

+ 80 - 0
frontend/dist/index.mak View File

@@ -0,0 +1,80 @@
1
+<!DOCTYPE html>
2
+<html>
3
+  <head>
4
+    <meta charset='utf-8' />
5
+    <meta name="viewport" content="width=device-width, user-scalable=no">
6
+    <title>Tracim</title>
7
+    <link rel='shortcut icon' type='image/x-icon' href='/asset/favicon.ico' >
8
+
9
+    <link rel="stylesheet" type="text/css" href="/asset/font/font-awesome-4.7.0/css/font-awesome.css">
10
+    <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
+    <!--
12
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
13
+    -->
14
+    <link rel="stylesheet" type="text/css" href="/asset/hamburger/hamburgers.min.css">
15
+    <link rel="stylesheet" type="text/css" href="/asset/bootstrap/bootstrap-4.0.0-beta.css">
16
+
17
+    <style>
18
+      <%
19
+        primary = colors['primary']
20
+        html_class = '.primaryColorFont{state}'
21
+        param = 'color'
22
+        color_change_value = 15
23
+      %>
24
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
25
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
26
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
27
+       <% html_class = '.primaryColorFont{state}Hover.hover' %>
28
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
29
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
30
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
31
+
32
+      <%
33
+        html_class = '.primaryColorBg{state}'
34
+        param = 'background-color'
35
+      %>
36
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
37
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
38
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
39
+      <% html_class = '.primaryColorBg{state}Hover.hover'%>
40
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
41
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
42
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
43
+
44
+      <%
45
+        param = 'border-color'
46
+        html_class = '.primaryColorBorder{state}'
47
+      %>
48
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
49
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
50
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
51
+      <% html_class = '.primaryColorBorder{state}Hover.hover' %>
52
+      ${html_class.replace('{state}', '')} { ${param}: ${primary.hexcode}; }
53
+      ${html_class.replace('{state}', 'Darken')} { ${param}: ${primary.darken(color_change_value).hexcode}; }
54
+      ${html_class.replace('{state}', 'Lighten')} { ${param}: ${primary.brighten(color_change_value).hexcode}; }
55
+    </style>
56
+  </head>
57
+
58
+  <body>
59
+    <div id='content'></div>
60
+
61
+    <script type='text/javascript' src='/asset/tracim.vendor.bundle.js'></script>
62
+    <script type='text/javascript' src='/asset/tracim.app.entry.js'></script>
63
+
64
+    <script type='text/javascript' src='/app/workspace.app.js'></script>
65
+    % for app in applications:
66
+    <script type='text/javascript' src='/app/${app.minislug}.app.js'></script>
67
+    %endfor
68
+    <script type='text/javascript' src='/app/admin_workspace_user.app.js'></script>
69
+
70
+    <script type='text/javascript' src='/asset/bootstrap/jquery-3.2.1.js'></script>
71
+    <script type='text/javascript' src='/asset/bootstrap/popper-1.12.3.js'></script>
72
+    <script type='text/javascript' src='/asset/bootstrap/bootstrap-4.0.0-beta.2.js'></script>
73
+
74
+    <script type='text/javascript' src='/asset/tinymce/js/tinymce/jquery.tinymce.min.js'></script>
75
+    <script type='text/javascript' src='/asset/tinymce/js/tinymce/tinymce.min.js'></script>
76
+
77
+    <script type='text/javascript' src='/asset/tracim/appInterface.js'></script>
78
+    <script type='text/javascript' src='/asset/tracim/tinymceInit.js'></script>
79
+  </body>
80
+</html>

+ 53 - 3
frontend/src/action-creator.async.js View File

@@ -6,11 +6,13 @@ import {
6 6
   USER_LOGOUT,
7 7
   USER_ROLE,
8 8
   USER_CONNECTED,
9
+  USER_KNOWN_MEMBER_LIST,
9 10
   setUserRole,
10 11
   WORKSPACE,
11 12
   WORKSPACE_LIST,
12 13
   WORKSPACE_DETAIL,
13 14
   WORKSPACE_MEMBER_LIST,
15
+  WORKSPACE_MEMBER_ADD,
14 16
   FOLDER,
15 17
   setFolderData,
16 18
   APP_LIST,
@@ -148,6 +150,36 @@ export const getUserRole = user => async dispatch => {
148 150
   if (fetchGetUserRole.status === 200) dispatch(setUserRole(fetchGetUserRole.json))
149 151
 }
150 152
 
153
+export const getUserKnownMember = (user, userNameToSearch) => dispatch => {
154
+  return fetchWrapper({
155
+    url: `${FETCH_CONFIG.apiUrl}/users/${user.user_id}/known_members?acp=${userNameToSearch}`,
156
+    param: {
157
+      headers: {
158
+        ...FETCH_CONFIG.headers,
159
+        'Authorization': 'Basic ' + user.auth
160
+      },
161
+      method: 'GET'
162
+    },
163
+    actionName: USER_KNOWN_MEMBER_LIST,
164
+    dispatch
165
+  })
166
+}
167
+
168
+export const putUserWorkspaceRead = (user, idWorkspace) => dispatch => {
169
+  return fetchWrapper({
170
+    url: `${FETCH_CONFIG.apiUrl}/users/${user.user_id}/workspaces/${idWorkspace}/read`,
171
+    param: {
172
+      headers: {
173
+        ...FETCH_CONFIG.headers,
174
+        'Authorization': 'Basic ' + user.auth
175
+      },
176
+      method: 'PUT'
177
+    },
178
+    actionName: USER_KNOWN_MEMBER_LIST,
179
+    dispatch
180
+  })
181
+}
182
+
151 183
 export const getWorkspaceList = user => dispatch => {
152 184
   return fetchWrapper({
153 185
     url: `${FETCH_CONFIG.apiUrl}/users/${user.user_id}/workspaces`,
@@ -238,6 +270,26 @@ export const getWorkspaceReadStatusList = (user, idWorkspace) => dispatch => {
238 270
   })
239 271
 }
240 272
 
273
+export const postWorkspaceMember = (user, idWorkspace, newMember) => dispatch => {
274
+  return fetchWrapper({
275
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/members`,
276
+    param: {
277
+      headers: {
278
+        ...FETCH_CONFIG.headers,
279
+        'Authorization': 'Basic ' + user.auth
280
+      },
281
+      method: 'POST',
282
+      body: JSON.stringify({
283
+        user_id: newMember.id,
284
+        user_email_or_public_name: newMember.name,
285
+        role: newMember.role
286
+      })
287
+    },
288
+    actionName: WORKSPACE_MEMBER_ADD,
289
+    dispatch
290
+  })
291
+}
292
+
241 293
 export const getFolderContent = (idWorkspace, idFolder) => async dispatch => {
242 294
   const fetchGetFolderContent = await fetchWrapper({
243 295
     url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents/?parent_id=${idFolder}`,
@@ -252,7 +304,6 @@ export const getFolderContent = (idWorkspace, idFolder) => async dispatch => {
252 304
 }
253 305
 
254 306
 export const getAppList = user => dispatch => {
255
-  console.log(user)
256 307
   return fetchWrapper({
257 308
     url: `${FETCH_CONFIG.apiUrl}/system/applications`,
258 309
     param: {
@@ -260,8 +311,7 @@ export const getAppList = user => dispatch => {
260 311
         ...FETCH_CONFIG.headers,
261 312
         'Authorization': 'Basic ' + user.auth
262 313
       },
263
-      method: 'GET',
264
-      'Authorization': 'Basic ' + user.auth
314
+      method: 'GET'
265 315
     },
266 316
     actionName: APP_LIST,
267 317
     dispatch

+ 9 - 4
frontend/src/action-creator.sync.js View File

@@ -17,18 +17,20 @@ export const removeFlashMessage = msg => ({ type: `${REMOVE}/${FLASH_MESSAGE}`,
17 17
 export const USER = 'User'
18 18
 export const USER_LOGIN = `${USER}/Login`
19 19
 export const USER_LOGOUT = `${USER}/Logout`
20
-export const USER_DATA = `${USER}/Data`
21
-export const USER_ROLE = `${USER}/Role`
22 20
 export const USER_CONNECTED = `${USER}/Connected`
23 21
 export const USER_DISCONNECTED = `${USER}/Disconnected`
24
-export const USER_LANG = `${USER}/Lang`
25 22
 export const setUserConnected = user => ({ type: `${SET}/${USER}/Connected`, user })
26 23
 export const setUserDisconnected = () => ({ type: `${SET}/${USER}/Disconnected` })
24
+export const USER_DATA = `${USER}/Data`
27 25
 export const updateUserData = userData => ({ type: `${UPDATE}/${USER}/Data`, data: userData })
26
+export const USER_ROLE = `${USER}/Role`
28 27
 export const setUserRole = userRole => ({ type: `${SET}/${USER}/Role`, userRole }) // this actually update workspaceList state
29
-export const setUserLang = lang => ({ type: `${SET}/${USER}/Lang`, lang })
30 28
 export const updateUserWorkspaceSubscriptionNotif = (workspaceId, subscriptionNotif) =>
31 29
   ({ type: `${UPDATE}/${USER_ROLE}/SubscriptionNotif`, workspaceId, subscriptionNotif })
30
+export const USER_LANG = `${USER}/Lang`
31
+export const setUserLang = lang => ({ type: `${SET}/${USER}/Lang`, lang })
32
+export const USER_KNOWN_MEMBER = `${USER}/KnownMember`
33
+export const USER_KNOWN_MEMBER_LIST = `${USER_KNOWN_MEMBER}/List`
32 34
 
33 35
 export const WORKSPACE = 'Workspace'
34 36
 export const WORKSPACE_CONTENT = `${WORKSPACE}/Content`
@@ -45,10 +47,13 @@ export const setWorkspaceDetail = workspaceDetail => ({ type: `${SET}/${WORKSPAC
45 47
 export const WORKSPACE_MEMBER = `${WORKSPACE}/Member`
46 48
 export const WORKSPACE_MEMBER_LIST = `${WORKSPACE_MEMBER}/List`
47 49
 export const setWorkspaceMemberList = workspaceMemberList => ({ type: `${SET}/${WORKSPACE_MEMBER_LIST}`, workspaceMemberList })
50
+export const WORKSPACE_MEMBER_ADD = `${WORKSPACE_MEMBER}/${ADD}`
48 51
 
49 52
 export const WORKSPACE_RECENT_ACTIVITY = `${WORKSPACE}/RecentActivity/List`
50 53
 export const WORKSPACE_RECENT_ACTIVITY_LIST = `${WORKSPACE_RECENT_ACTIVITY}/List`
51 54
 export const setWorkspaceRecentActivityList = workspaceRecentActivityList => ({ type: `${SET}/${WORKSPACE_RECENT_ACTIVITY_LIST}`, workspaceRecentActivityList })
55
+export const WORKSPACE_RECENT_ACTIVITY_FOR_USER_LIST = `${WORKSPACE_RECENT_ACTIVITY}/ForUser/List`
56
+export const setWorkspaceRecentActivityForUserList = workspaceRecentActivityForUserList => ({ type: `${SET}/${WORKSPACE_RECENT_ACTIVITY_FOR_USER_LIST}`, workspaceRecentActivityForUserList })
52 57
 
53 58
 export const WORKSPACE_READ_STATUS = `${WORKSPACE}/ReadStatus`
54 59
 export const WORKSPACE_READ_STATUS_LIST = `${WORKSPACE_READ_STATUS}/List`

+ 3 - 1
frontend/src/component/Dashboard/ContentTypeBtn.styl View File

@@ -1,8 +1,10 @@
1
+@import "../../../node_modules/tracim_frontend_lib/src/css/Variable.styl"
2
+
1 3
 .contentTypeBtn
2 4
   display flex
3 5
   flex-direction column
4 6
   justify-content center
5
-  margin 0 15px
7
+  margin 15px
6 8
   border-radius 10px
7 9
   padding 15px
8 10
   width 230px

+ 58 - 21
frontend/src/component/Dashboard/MemberList.jsx View File

@@ -1,16 +1,14 @@
1 1
 import React from 'react'
2 2
 import PropTypes from 'prop-types'
3
-import { Checkbox } from 'tracim_frontend_lib'
3
+// import { Checkbox } from 'tracim_frontend_lib'
4 4
 
5 5
 require('./MemberList.styl')
6 6
 
7 7
 export class MemberList extends React.Component {
8 8
   constructor (props) {
9 9
     super(props)
10
-
11 10
     this.state = {
12
-      displayNewMemberList: true,
13
-      createAccountCheckbox: false
11
+      displayNewMemberList: true
14 12
     }
15 13
   }
16 14
 
@@ -21,7 +19,12 @@ export class MemberList extends React.Component {
21 19
   handleClickCheckboxCreateAccount = e => {
22 20
     e.preventDefault()
23 21
     e.stopPropagation()
24
-    this.setState(prev => ({createAccountCheckbox: !prev.createAccountCheckbox}))
22
+    this.props.onChangeCreateAccount(!this.props.createAccount)
23
+  }
24
+
25
+  handleClickBtnValidate = () => {
26
+    this.props.onClickValidateNewMember()
27
+    this.setState({displayNewMemberList: true})
25 28
   }
26 29
 
27 30
   render () {
@@ -40,7 +43,7 @@ export class MemberList extends React.Component {
40 43
               <div>
41 44
                 <ul className='memberlist__list'>
42 45
                   {props.memberList.map(m =>
43
-                    <li className='memberlist__list__item primaryColorBgLightenHover' key={m.id}>
46
+                    <li className='memberlist__list__item' key={m.id}>
44 47
                       <div className='memberlist__list__item__avatar'>
45 48
                         {m.avatarUrl ? <img src={m.avatarUrl} /> : <img src='NYI' />}
46 49
                       </div>
@@ -78,7 +81,7 @@ export class MemberList extends React.Component {
78 81
               </div>
79 82
             )
80 83
             : (
81
-              <form className='memberlist__form'>
84
+              <div className='memberlist__form'>
82 85
                 <div className='memberlist__form__close d-flex justify-content-end'>
83 86
                   <i className='fa fa-times' onClick={this.handleClickCloseAddMemberBtn} />
84 87
                 </div>
@@ -94,10 +97,34 @@ export class MemberList extends React.Component {
94 97
                       className='name__input form-control'
95 98
                       id='addmember'
96 99
                       placeholder='Nom ou Email'
97
-                      onChange={props.onChangeName}
100
+                      value={props.nameOrEmail}
101
+                      onChange={e => props.onChangeNameOrEmail(e.target.value)}
102
+                      autoComplete='off'
98 103
                     />
104
+
105
+                    {props.searchedKnownMemberList.length > 0 &&
106
+                      <div className='autocomplete primaryColorBorder'>
107
+                        {props.searchedKnownMemberList.filter((u, i) => i < 5).map(u => // only displays the first 5
108
+                          <div
109
+                            className='autocomplete__item primaryColorBgHover'
110
+                            onClick={() => props.onClickKnownMember(u)}
111
+                            key={u.user_id}
112
+                          >
113
+                            <div className='autocomplete__item__avatar primaryColorBorder'>
114
+                              <img src={u.avatar_url} />
115
+                            </div>
116
+
117
+                            <div className='autocomplete__item__name'>
118
+                              {u.public_name}
119
+                            </div>
120
+                          </div>
121
+                        )}
122
+                      </div>
123
+                    }
99 124
                   </div>
100 125
 
126
+                  {/*
127
+                  // @TODO validate with DA that this checkbox is useless since the backend handle everything
101 128
                   <div className='memberlist__form__member__create'>
102 129
                     <div className='memberlist__form__member__create__checkbox mr-3'>
103 130
                       <Checkbox
@@ -111,6 +138,7 @@ export class MemberList extends React.Component {
111 138
                       {props.t('Create an account')}
112 139
                     </div>
113 140
                   </div>
141
+                  */}
114 142
                 </div>
115 143
 
116 144
                 <div className='memberlist__form__role'>
@@ -120,20 +148,29 @@ export class MemberList extends React.Component {
120 148
 
121 149
                   <ul className='memberlist__form__role__list'>
122 150
                     {props.roleList.map(r =>
123
-                      <li className='memberlist__form__role__list__item' key={r.slug}>
124
-                        <div className='item__radiobtn mr-3'>
125
-                          <input type='radio' name='role' value={r.slug} />
126
-                        </div>
127
-
128
-                        <div className='item__text'>
129
-                          <div className='item_text_icon mr-2' style={{color: r.hexcolor}}>
130
-                            <i className={`fa fa-${r.faIcon}`} />
151
+                      <li key={r.slug}>
152
+                        <label className='memberlist__form__role__list__item' htmlFor={r.slug}>
153
+                          <div className='item__radiobtn mr-3'>
154
+                            <input
155
+                              id={r.slug}
156
+                              type='radio'
157
+                              name='role'
158
+                              value={r.slug}
159
+                              checked={r.slug === props.role}
160
+                              onChange={() => props.onChangeRole(r.slug)}
161
+                            />
131 162
                           </div>
132 163
 
133
-                          <div className='item__text__name'>
134
-                            {r.label}
164
+                          <div className='item__text'>
165
+                            <div className='item__text__icon mr-2' style={{color: r.hexcolor}}>
166
+                              <i className={`fa fa-${r.faIcon}`} />
167
+                            </div>
168
+
169
+                            <div className='item__text__name'>
170
+                              {r.label}
171
+                            </div>
135 172
                           </div>
136
-                        </div>
173
+                        </label>
137 174
                       </li>
138 175
                     )}
139 176
 
@@ -141,11 +178,11 @@ export class MemberList extends React.Component {
141 178
                 </div>
142 179
 
143 180
                 <div className='memberlist__form__submitbtn'>
144
-                  <button className='btn btn-outline-primary'>
181
+                  <button className='btn btn-outline-primary' onClick={this.handleClickBtnValidate}>
145 182
                     {props.t('Validate')}
146 183
                   </button>
147 184
                 </div>
148
-              </form>
185
+              </div>
149 186
             )
150 187
           }
151 188
         </div>

+ 56 - 1
frontend/src/component/Dashboard/MemberList.styl View File

@@ -1,3 +1,5 @@
1
+@import "../../../node_modules/tracim_frontend_lib/src/css/Variable.styl"
2
+
1 3
 .memberlist
2 4
   margin 0 0 50px 0
3 5
   width 35%
@@ -76,11 +78,38 @@
76 78
         cursor pointer
77 79
     &__member
78 80
       &__name
81
+        position relative
82
+        margin 0 0 20px 0
79 83
         .name__label
80 84
           margin 30px 0 20px 0
81 85
           label()
86
+        .autocomplete
87
+          position absolute
88
+          min-width 300px
89
+          background-color off-white
90
+          border-radius 10px
91
+          border-width 1px
92
+          border-style solid
93
+          &__item
94
+            display flex
95
+            align-items center
96
+            cursor pointer
97
+            padding 5px 8px
98
+            &:first-child
99
+              border-top-left-radius 10px
100
+              border-top-right-radius 10px
101
+            &:last-child
102
+              border-bottom-left-radius 10px
103
+              border-bottom-right-radius 10px
104
+            &__avatar
105
+              width 45px
106
+              height 45px
107
+              border-radius 50%
108
+              border-width 1px
109
+              border-style solid
110
+            &__name
111
+              margin-left 15px
82 112
       .name__input
83
-        margin-bottom 20px
84 113
         border 1px solid grey
85 114
         border-radius 10px
86 115
         padding 10px
@@ -106,6 +135,7 @@
106 135
           display flex
107 136
           align-items center
108 137
           margin 10px 25px 10px 0
138
+          cursor pointer
109 139
           .item
110 140
             &__text
111 141
               display flex
@@ -115,3 +145,28 @@
115 145
       & > button
116 146
         padding 8px 30px
117 147
         cursor pointer
148
+
149
+@media (min-width min-sm) and (max-width: max-lg)
150
+  .memberlist
151
+    width 50%
152
+
153
+@media (min-width: min-sm) and (max-width: max-sm)
154
+  .memberlist
155
+    margin 50px 0
156
+    width 90%
157
+
158
+@media (max-width: max-xs)
159
+  .memberlist
160
+    margin 50px 0
161
+    width 100%
162
+    &__title
163
+      margin-left 10px
164
+    &__wrapper
165
+      height auto
166
+    &__list
167
+      height auto
168
+      overflow-Y visible
169
+      &__item:nth-last-child(1)
170
+        border-bottom 1px solid grey
171
+    &__btnadd
172
+      border-top 0

+ 66 - 0
frontend/src/component/Dashboard/MoreInfo.jsx View File

@@ -0,0 +1,66 @@
1
+import React from 'react'
2
+
3
+require('./MoreInfo.styl')
4
+
5
+export const MoreInfo = props =>
6
+  <div className='moreinfo'>
7
+    <div className='moreinfo__webdav genericBtnInfoDashboard'>
8
+      <div
9
+        className='moreinfo__webdav__btn genericBtnInfoDashboard__btn'
10
+        onClick={props.onClickToggleWebdav}
11
+      >
12
+        <div className='moreinfo__webdav__btn__icon genericBtnInfoDashboard__btn__icon'>
13
+          <i className='fa fa-windows' />
14
+        </div>
15
+
16
+        <div className='moreinfo__webdav__btn__text genericBtnInfoDashboard__btn__text'>
17
+          {props.t('Implement Tracim in your explorer')}
18
+        </div>
19
+      </div>
20
+
21
+      {props.displayWebdavBtn === true &&
22
+      <div className='moreinfo__webdav__information genericBtnInfoDashboard__info'>
23
+        <div className='moreinfo__webdav__information__text genericBtnInfoDashboard__info__text'>
24
+          {props.t('Find all your documents deposited online directly on your computer via the workstation, without going through the software.')}'
25
+        </div>
26
+
27
+        <div className='moreinfo__webdav__information__link genericBtnInfoDashboard__info__link'>
28
+          http://algoo.trac.im/webdav/
29
+        </div>
30
+      </div>
31
+      }
32
+    </div>
33
+
34
+    <div className='moreinfo__calendar genericBtnInfoDashboard'>
35
+      <div className='moreinfo__calendar__wrapperBtn'>
36
+        <div
37
+          className='moreinfo__calendar__btn genericBtnInfoDashboard__btn'
38
+          onClick={props.onClickToggleCalendar}
39
+        >
40
+          <div className='moreinfo__calendar__btn__icon genericBtnInfoDashboard__btn__icon'>
41
+            <i className='fa fa-calendar' />
42
+          </div>
43
+
44
+          <div className='moreinfo__calendar__btn__text genericBtnInfoDashboard__btn__text'>
45
+            {props.t('Workspace Calendar')}
46
+          </div>
47
+        </div>
48
+      </div>
49
+
50
+      <div className='moreinfo__calendar__wrapperText'>
51
+        {props.displayCalendarBtn === true &&
52
+        <div className='moreinfo__calendar__information genericBtnInfoDashboard__info'>
53
+          <div className='moreinfo__calendar__information__text genericBtnInfoDashboard__info__text'>
54
+            {props.t('Each workspace has its own calendar.')}
55
+          </div>
56
+
57
+          <div className='moreinfo__calendar__information__link genericBtnInfoDashboard__info__link'>
58
+            http://algoo.trac.im/calendar/
59
+          </div>
60
+        </div>
61
+        }
62
+      </div>
63
+    </div>
64
+  </div>
65
+
66
+export default MoreInfo

+ 29 - 0
frontend/src/component/Dashboard/MoreInfo.styl View File

@@ -0,0 +1,29 @@
1
+.moreinfo
2
+  display flex
3
+  justify-content space-between
4
+  flexwrap wrap
5
+  &__webdav
6
+    margin 0 15px 40px 0
7
+    &__btn
8
+      width 300px
9
+    &__information
10
+      width 300px
11
+  &__calendar
12
+    margin-bottom 100px
13
+    &__wrapperBtn
14
+      margin-right 290px
15
+    &__btn
16
+      width 300px
17
+    &__information
18
+      width 300px
19
+
20
+@media (min-width: min-sm) and (max-width: max-sm)
21
+  .moreinfo__webdav__information
22
+    width 500px
23
+
24
+@media (max-width: max-xs)
25
+  .moreinfo
26
+    &__webdav
27
+      margin-left 10px
28
+      &__information
29
+        width 350px

+ 19 - 10
frontend/src/component/Dashboard/RecentActivity.jsx View File

@@ -2,36 +2,45 @@ import React from 'react'
2 2
 import PropTypes from 'prop-types'
3 3
 import classnames from 'classnames'
4 4
 
5
+require('./RecentActivity.styl')
6
+
5 7
 export const RecentActivity = props =>
6
-  <div className={props.customClass}>
7
-    <div className={`${props.customClass}__header`}>
8
-      <div className={classnames(`${props.customClass}__header__title`, 'subTitle')}>
8
+  <div className='activity'>
9
+    <div className='activity__header'>
10
+      <div className={classnames('activity__header__title', 'subTitle')}>
9 11
         {props.t('Recent activity')}
10 12
       </div>
11 13
 
12
-      <div className={classnames(`${props.customClass}__header__allread`, 'btn btn-outline-primary')}>
14
+      <div
15
+        className={classnames('activity__header__allread', 'btn btn-outline-primary')}
16
+        onClick={props.onClickEverythingAsRead}
17
+      >
13 18
         {props.t('Mark everything as read')}
14 19
       </div>
15 20
     </div>
16 21
 
17
-    <div className={`${props.customClass}__wrapper`}>
22
+    <div className='activity__wrapper'>
18 23
       {props.recentActivityFilteredForUser.map(content => {
19 24
         const contentType = props.contentTypeList.find(ct => ct.slug === content.type)
20 25
         return (
21
-          <div className={`${props.customClass}__workspace`} key={content.id}>
22
-            <div className={`${props.customClass}__workspace__icon`} style={{color: contentType.hexcolor}}>
26
+          <div
27
+            className='activity__workspace'
28
+            onClick={() => props.onClickRecentContent(content.id, content.type)}
29
+            key={content.id}
30
+          >
31
+            <div className='activity__workspace__icon' style={{color: contentType.hexcolor}}>
23 32
               <i className={`fa fa-${contentType.faIcon}`} />
24 33
             </div>
25
-            <div className={`${props.customClass}__workspace__name`}>
34
+            <div className='activity__workspace__name'>
26 35
               {content.label}
27 36
             </div>
28 37
           </div>
29 38
         )
30 39
       })}
31 40
 
32
-      <div className={classnames(`${props.customClass}__more`, 'd-flex flex-row-reverse')}>
41
+      <div className={classnames('activity__more', 'd-flex flex-row-reverse')}>
33 42
         <div
34
-          className={classnames(`${props.customClass}__more__btn`, 'btn btn-outline-primary')}
43
+          className={classnames('activity__more__btn', 'btn btn-outline-primary')}
35 44
           onClick={props.onClickSeeMore}
36 45
         >
37 46
           {props.t('See more')}

+ 67 - 0
frontend/src/component/Dashboard/RecentActivity.styl View File

@@ -0,0 +1,67 @@
1
+@import "../../../node_modules/tracim_frontend_lib/src/css/Variable.styl"
2
+
3
+.activity
4
+  margin 0 35px 50px 0
5
+  width 60%
6
+  &__wrapper
7
+    border 1px solid grey
8
+    height 480px
9
+    overflow-y scroll
10
+  &__header
11
+    display flex
12
+    justify-content space-between
13
+    align-items center
14
+    margin-bottom 20px
15
+    height 44px
16
+    &__allread
17
+      padding 10px 25px
18
+      font-size 18px
19
+      cursor pointer
20
+  &__workspace
21
+    display flex
22
+    align-items center
23
+    border-bottom 1px solid grey
24
+    padding 15px
25
+    cursor pointer
26
+    &:hover
27
+      background-color fourthColor
28
+    &:nth-child(even)
29
+      background-color grey-hover
30
+      &:hover
31
+        background-color fourthColor
32
+    &__icon
33
+      margin 0 25px
34
+      font-size 25px
35
+    &__name
36
+      font-size 18px
37
+      font-weight 500
38
+      span
39
+        font-weight 400
40
+  &__more
41
+    &__btn
42
+      margin 15px
43
+      padding 10px 25px
44
+      cursor pointer
45
+
46
+@media (min-width min-sm) and (max-width: max-lg)
47
+  .activity
48
+    width 100%
49
+
50
+@media (min-width: min-md) and (max-width: max-md)
51
+  .activity
52
+    margin 25px 15px 25px 0
53
+
54
+@media (min-width: min-sm) and (max-width: max-sm)
55
+  .activity
56
+    margin 25px 15px 25px 0
57
+
58
+@media (max-width: max-xs)
59
+  .activity
60
+    margin 25px 0
61
+    width 100%
62
+    &__header
63
+      display block
64
+      height auto
65
+      margin 0 15px 20px 15px
66
+      &__title
67
+        margin-bottom 20px

+ 75 - 0
frontend/src/component/Dashboard/UserStatus.jsx View File

@@ -0,0 +1,75 @@
1
+import React from 'react'
2
+import {ROLE} from '../../helper.js'
3
+
4
+require('./UserStatus.styl')
5
+
6
+// @TODO Côme - 2018/08/07 - since api yet doesn't handle notification subscriptions, this file is WIP
7
+export const UserStatus = props =>
8
+  <div className='userstatus'>
9
+    <div className='userstatus__role'>
10
+      <div className='userstatus__role__msg'>
11
+        {props.t('Hi {{name}} ! Currently, you are ', {name: props.user.public_name})}
12
+      </div>
13
+
14
+      {(() => {
15
+        const myself = props.curWs.memberList.find(m => m.id === props.user.user_id)
16
+        if (myself === undefined) return
17
+
18
+        const myRole = ROLE.find(r => r.slug === myself.role)
19
+
20
+        return (
21
+          <div className='userstatus__role__definition'>
22
+            <div className='userstatus__role__definition__icon'>
23
+              <i className={`fa fa-${myRole.faIcon}`} />
24
+            </div>
25
+
26
+            <div className='userstatus__role__definition__text'>
27
+              {myRole.label}
28
+            </div>
29
+          </div>
30
+        )
31
+      })()}
32
+    </div>
33
+
34
+    <div className='userstatus__notification'>
35
+      <div className='userstatus__notification__text'>
36
+        {props.t("You have subscribed to this workspace's notifications")} (NYI)
37
+      </div>
38
+
39
+      {props.displayNotifBtn
40
+        ? (
41
+          <div className='userstatus__notification__subscribe dropdown'>
42
+            <button
43
+              className='userstatus__notification__subscribe__btn btn btn-outline-primary dropdown-toggle'
44
+              type='button'
45
+              id='dropdownMenuButton'
46
+              data-toggle='dropdown'
47
+              aria-haspopup='true'
48
+              aria-expanded='false'
49
+            >
50
+              {props.t('subscribed')}
51
+            </button>
52
+
53
+            <div className='userstatus__notification__subscribe__submenu dropdown-menu'>
54
+              <div className='userstatus__notification__subscribe__submenu__item dropdown-item'>
55
+                {props.t('subscriber')}
56
+              </div>
57
+              <div className='userstatus__notification__subscribe__submenu__item dropdown-item dropdown-item'>
58
+                {props.t('unsubscribed')}
59
+              </div>
60
+            </div>
61
+          </div>
62
+        )
63
+        : (
64
+          <div
65
+            className='userstatus__notification__btn btn btn-outline-primary'
66
+            onClick={props.onClickToggleNotifBtn}
67
+          >
68
+            {props.t('Change your status')}
69
+          </div>
70
+        )
71
+      }
72
+    </div>
73
+  </div>
74
+
75
+export default UserStatus

+ 30 - 0
frontend/src/component/Dashboard/UserStatus.styl View File

@@ -0,0 +1,30 @@
1
+@import "../../../node_modules/tracim_frontend_lib/src/css/Variable.styl"
2
+
3
+.userstatus
4
+  width 35%
5
+  &__role
6
+    margin 20px 0
7
+    font-size 18px
8
+    &__msg
9
+      margin-right 15px
10
+    &__definition
11
+      display flex
12
+      &__icon
13
+        margin-right 15px
14
+        color gestionnaire
15
+  &__notification
16
+    font-size 18px
17
+    &__btn
18
+      margin 20px 0
19
+      border 1px solid thirdColor
20
+      padding 10px 15px
21
+      cursor pointer
22
+    &__subscribe
23
+      &__btn
24
+        margin 20px 0
25
+        border 1px solid thirdColor
26
+        padding 10px 15px
27
+      &__submenu
28
+        padding 0
29
+        &__item
30
+          padding 10px

+ 144 - 157
frontend/src/container/Dashboard.jsx View File

@@ -10,25 +10,39 @@ import {
10 10
   getWorkspaceDetail,
11 11
   getWorkspaceMemberList,
12 12
   getWorkspaceRecentActivityList,
13
-  getWorkspaceReadStatusList
13
+  getWorkspaceReadStatusList,
14
+  getUserKnownMember,
15
+  postWorkspaceMember,
16
+  putUserWorkspaceRead
14 17
 } from '../action-creator.async.js'
15 18
 import {
16 19
   newFlashMessage,
17 20
   setWorkspaceDetail,
18 21
   setWorkspaceMemberList,
19 22
   setWorkspaceRecentActivityList,
23
+  setWorkspaceRecentActivityForUserList,
20 24
   setWorkspaceReadStatusList
21 25
 } from '../action-creator.sync.js'
22
-import { ROLE } from '../helper.js'
26
+import { ROLE, PAGE } from '../helper.js'
27
+import UserStatus from '../component/Dashboard/UserStatus.jsx'
23 28
 import ContentTypeBtn from '../component/Dashboard/ContentTypeBtn.jsx'
24 29
 import RecentActivity from '../component/Dashboard/RecentActivity.jsx'
25 30
 import MemberList from '../component/Dashboard/MemberList.jsx'
31
+import MoreInfo from '../component/Dashboard/MoreInfo.jsx'
26 32
 
27 33
 class Dashboard extends React.Component {
28 34
   constructor (props) {
29 35
     super(props)
30 36
     this.state = {
31 37
       workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null, // this is used to avoid handling the parseInt everytime
38
+      newMember: {
39
+        id: '',
40
+        avatarUrl: '',
41
+        nameOrEmail: '',
42
+        // createAccount: false, // @TODO ask DA about this checkbox if it is still usefull (since backend handles it all)
43
+        role: ''
44
+      },
45
+      searchedKnownMemberList: [],
32 46
       displayNewMemberDashboard: false,
33 47
       displayNotifBtn: false,
34 48
       displayWebdavBtn: false,
@@ -41,52 +55,126 @@ class Dashboard extends React.Component {
41 55
 
42 56
     const fetchWorkspaceDetail = await props.dispatch(getWorkspaceDetail(props.user, state.workspaceIdInUrl))
43 57
     switch (fetchWorkspaceDetail.status) {
44
-      case 200:
45
-        props.dispatch(setWorkspaceDetail(fetchWorkspaceDetail.json)); break
46
-      default:
47
-        props.dispatch(newFlashMessage(props.t('An error has happened while fetching workspace detail'), 'warning')); break
58
+      case 200: props.dispatch(setWorkspaceDetail(fetchWorkspaceDetail.json)); break
59
+      default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('workspace detail')}`, 'warning')); break
48 60
     }
61
+    this.loadMemberList()
62
+    this.loadRecentActivity()
63
+  }
64
+
65
+  loadMemberList = async () => {
66
+    const { props, state } = this
49 67
 
50 68
     const fetchWorkspaceMemberList = await props.dispatch(getWorkspaceMemberList(props.user, state.workspaceIdInUrl))
51 69
     switch (fetchWorkspaceMemberList.status) {
52
-      case 200:
53
-        props.dispatch(setWorkspaceMemberList(fetchWorkspaceMemberList.json)); break
54
-      default:
55
-        props.dispatch(newFlashMessage(props.t('An error has happened while fetching member list'), 'warning')); break
70
+      case 200: props.dispatch(setWorkspaceMemberList(fetchWorkspaceMemberList.json)); break
71
+      default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('member list')}`, 'warning')); break
56 72
     }
73
+  }
74
+
75
+  loadRecentActivity = async () => {
76
+    const { props, state } = this
57 77
 
58 78
     const fetchWorkspaceRecentActivityList = await props.dispatch(getWorkspaceRecentActivityList(props.user, state.workspaceIdInUrl))
79
+    const fetchWorkspaceReadStatusList = await props.dispatch(getWorkspaceReadStatusList(props.user, state.workspaceIdInUrl))
80
+
59 81
     switch (fetchWorkspaceRecentActivityList.status) {
60
-      case 200:
61
-        props.dispatch(setWorkspaceRecentActivityList(fetchWorkspaceRecentActivityList.json)); break
62
-      default:
63
-        props.dispatch(newFlashMessage(props.t('An error has happened while fetching recent activity list'), 'warning')); break
82
+      case 200: props.dispatch(setWorkspaceRecentActivityList(fetchWorkspaceRecentActivityList.json)); break
83
+      default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('recent activity list')}`, 'warning')); break
64 84
     }
65 85
 
66
-    const fetchWorkspaceReadStatusList = await props.dispatch(getWorkspaceReadStatusList(props.user, state.workspaceIdInUrl))
67 86
     switch (fetchWorkspaceReadStatusList.status) {
87
+      case 200: props.dispatch(setWorkspaceReadStatusList(fetchWorkspaceReadStatusList.json)); break
88
+      default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('read status list')}`, 'warning')); break
89
+    }
90
+
91
+    const readStatusForUserList = fetchWorkspaceReadStatusList.json.filter(c => c.read_by_user).map(c => c.content_id)
92
+    const recentActivityForUserList = fetchWorkspaceRecentActivityList.json.filter(content => !readStatusForUserList.includes(content.content_id))
93
+
94
+    props.dispatch(setWorkspaceRecentActivityForUserList(recentActivityForUserList))
95
+  }
96
+
97
+  handleToggleNewMemberDashboard = () => this.setState(prevState => ({displayNewMemberDashboard: !prevState.displayNewMemberDashboard}))
98
+
99
+  handleToggleNotifBtn = () => this.setState(prevState => ({displayNotifBtn: !prevState.displayNotifBtn}))
100
+
101
+  handleToggleWebdavBtn = () => this.setState(prevState => ({displayWebdavBtn: !prevState.displayWebdavBtn}))
102
+
103
+  handleToggleCalendarBtn = () => this.setState(prevState => ({displayCalendarBtn: !prevState.displayCalendarBtn}))
104
+
105
+  handleClickRecentContent = (idContent, typeContent) => this.props.history.push(PAGE.WORKSPACE.CONTENT(this.props.curWs.id, typeContent, idContent))
106
+
107
+  handleClickMarkRecentActivityAsRead = async () => {
108
+    const { props } = this
109
+    const fetchUserWorkspaceAllRead = await props.dispatch(putUserWorkspaceRead(props.user, props.curWs.id))
110
+    switch (fetchUserWorkspaceAllRead.status) {
111
+      case 204: this.loadRecentActivity(); break
112
+      default: props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching "mark all as read"')}`, 'warning')); break
113
+    }
114
+  }
115
+
116
+  handleClickSeeMore = async () => {
117
+    console.log('nyi')
118
+  }
119
+
120
+  handleSearchUser = async userNameToSearch => {
121
+    const { props } = this
122
+    const fetchUserKnownMemberList = await props.dispatch(getUserKnownMember(props.user, userNameToSearch))
123
+    switch (fetchUserKnownMemberList.status) {
68 124
       case 200:
69
-        props.dispatch(setWorkspaceReadStatusList(fetchWorkspaceReadStatusList.json)); break
125
+        this.setState({searchedKnownMemberList: fetchUserKnownMemberList.json}); break
70 126
       default:
71
-        props.dispatch(newFlashMessage(props.t('An error has happened while fetching read status list'), 'warning')); break
127
+        props.dispatch(newFlashMessage(`${props.t('An error has happened while fetching')} ${props.t('known members list')}`, 'warning')); break
72 128
     }
73 129
   }
74 130
 
75
-  handleToggleNewMemberDashboard = () => this.setState(prevState => ({
76
-    displayNewMemberDashboard: !prevState.displayNewMemberDashboard
77
-  }))
131
+  handleChangeNewMemberNameOrEmail = newNameOrEmail => {
132
+    if (newNameOrEmail.length >= 2) this.handleSearchUser(newNameOrEmail)
133
+    this.setState(prev => ({newMember: {...prev.newMember, nameOrEmail: newNameOrEmail}}))
134
+  }
135
+
136
+  handleClickKnownMember = knownMember => {
137
+    this.setState(prev => ({
138
+      newMember: {
139
+        ...prev.newMember,
140
+        id: knownMember.user_id,
141
+        nameOrEmail: knownMember.public_name,
142
+        avatarUrl: knownMember.avatar_url
143
+      },
144
+      searchedKnownMemberList: []
145
+    }))
146
+  }
147
+
148
+  // handleChangeNewMemberCreateAccount = newCreateAccount => this.setState(prev => ({newMember: {...prev.newMember, createAccount: newCreateAccount}}))
149
+
150
+  handleChangeNewMemberRole = newRole => this.setState(prev => ({newMember: {...prev.newMember, role: newRole}}))
151
+
152
+  handleClickValidateNewMember = async () => {
153
+    const { props, state } = this
154
+
155
+    if (state.newMember.nameOrEmail === '') {
156
+      props.dispatch(newFlashMessage(props.t('Please set a name or email'), 'warning'))
157
+      return
158
+    }
78 159
 
79
-  handleToggleNotifBtn = () => this.setState(prevState => ({
80
-    displayNotifBtn: !prevState.displayNotifBtn
81
-  }))
160
+    if (state.newMember.role === '') {
161
+      props.dispatch(newFlashMessage(props.t('Please set a role'), 'warning'))
162
+      return
163
+    }
82 164
 
83
-  handleToggleWebdavBtn = () => this.setState(prevState => ({
84
-    displayWebdavBtn: !prevState.displayWebdavBtn
85
-  }))
165
+    const fetchWorkspaceNewMember = await props.dispatch(postWorkspaceMember(props.user, props.curWs.id, {
166
+      id: state.newMember.id,
167
+      name: state.newMember.nameOrEmail,
168
+      role: state.newMember.role
169
+    }))
86 170
 
87
-  handleToggleCalendarBtn = () => this.setState(prevState => ({
88
-    displayCalendarBtn: !prevState.displayCalendarBtn
89
-  }))
171
+    switch (fetchWorkspaceNewMember.status) {
172
+      case 200:
173
+        this.loadMemberList(); break
174
+      default:
175
+        props.dispatch(newFlashMessage(props.t('An error has happened while adding the member'), 'warning')); break
176
+    }
177
+  }
90 178
 
91 179
   render () {
92 180
     const { props, state } = this
@@ -118,72 +206,13 @@ class Dashboard extends React.Component {
118 206
                 </div>
119 207
               </div>
120 208
 
121
-              <div className='dashboard__userstatut'>
122
-                <div className='dashboard__userstatut__role'>
123
-                  <div className='dashboard__userstatut__role__msg'>
124
-                    {props.t('Hi {{name}} ! Currently, you are ', {name: props.user.public_name})}
125
-                  </div>
126
-
127
-                  {(() => {
128
-                    const myself = props.curWs.memberList.find(m => m.id === props.user.user_id)
129
-                    if (myself === undefined) return
130
-
131
-                    const myRole = ROLE.find(r => r.slug === myself.role)
132
-
133
-                    return (
134
-                      <div className='dashboard__userstatut__role__definition'>
135
-                        <div className='dashboard__userstatut__role__definition__icon'>
136
-                          <i className={`fa fa-${myRole.faIcon}`} />
137
-                        </div>
138
-
139
-                        <div className='dashboard__userstatut__role__definition__text'>
140
-                          {myRole.label}
141
-                        </div>
142
-                      </div>
143
-                    )
144
-                  })()}
145
-                </div>
146
-
147
-                <div className='dashboard__userstatut__notification'>
148
-                  <div className='dashboard__userstatut__notification__text'>
149
-                    {props.t("You have subscribed to this workspace's notifications")} (nyi)
150
-                  </div>
151
-
152
-                  {state.displayNotifBtn
153
-                    ? (
154
-                      <div className='dashboard__userstatut__notification__subscribe dropdown'>
155
-                        <button
156
-                          className='dashboard__userstatut__notification__subscribe__btn btn btn-outline-primary dropdown-toggle'
157
-                          type='button'
158
-                          id='dropdownMenuButton'
159
-                          data-toggle='dropdown'
160
-                          aria-haspopup='true'
161
-                          aria-expanded='false'
162
-                        >
163
-                          {props.t('subscriber')}
164
-                        </button>
165
-
166
-                        <div className='dashboard__userstatut__notification__subscribe__submenu dropdown-menu'>
167
-                          <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item'>
168
-                            {props.t('subscriber')}
169
-                          </div>
170
-                          <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item dropdown-item'>
171
-                            {props.t('unsubscribed')}
172
-                          </div>
173
-                        </div>
174
-                      </div>
175
-                    )
176
-                    : (
177
-                      <div
178
-                        className='dashboard__userstatut__notification__btn btn btn-outline-primary'
179
-                        onClick={this.handleToggleNotifBtn}
180
-                      >
181
-                        {props.t('Change your status')}
182
-                      </div>
183
-                    )
184
-                  }
185
-                </div>
186
-              </div>
209
+              <UserStatus
210
+                user={props.user}
211
+                curWs={props.curWs}
212
+                displayNotifBtn={state.displayNotifBtn}
213
+                onClickToggleNotifBtn={this.handleToggleNotifBtn}
214
+                t={props.t}
215
+              />
187 216
             </div>
188 217
 
189 218
             <div className='dashboard__calltoaction justify-content-xl-center'>
@@ -202,9 +231,11 @@ class Dashboard extends React.Component {
202 231
             <div className='dashboard__workspaceInfo'>
203 232
               <RecentActivity
204 233
                 customClass='dashboard__activity'
205
-                recentActivityFilteredForUser={props.curWs.recentActivityList.filter(content => !props.curWs.contentReadStatusList.includes(content.id))}
234
+                recentActivityFilteredForUser={props.curWs.recentActivityForUserList}
206 235
                 contentTypeList={props.contentType}
207
-                onClickSeeMore={() => {}}
236
+                onClickRecentContent={this.handleClickRecentContent}
237
+                onClickEverythingAsRead={this.handleClickMarkRecentActivityAsRead}
238
+                onClickSeeMore={this.handleClickSeeMore}
208 239
                 t={props.t}
209 240
               />
210 241
 
@@ -212,70 +243,26 @@ class Dashboard extends React.Component {
212 243
                 customClass='dashboard__memberlist'
213 244
                 memberList={props.curWs.memberList}
214 245
                 roleList={ROLE}
246
+                searchedKnownMemberList={state.searchedKnownMemberList}
247
+                nameOrEmail={state.newMember.nameOrEmail}
248
+                onChangeNameOrEmail={this.handleChangeNewMemberNameOrEmail}
249
+                onClickKnownMember={this.handleClickKnownMember}
250
+                // createAccount={state.newMember.createAccount}
251
+                // onChangeCreateAccount={this.handleChangeNewMemberCreateAccount}
252
+                role={state.newMember.role}
253
+                onChangeRole={this.handleChangeNewMemberRole}
254
+                onClickValidateNewMember={this.handleClickValidateNewMember}
215 255
                 t={props.t}
216 256
               />
217 257
             </div>
218 258
 
219
-            <div className='dashboard__moreinfo'>
220
-              <div className='dashboard__moreinfo__webdav genericBtnInfoDashboard'>
221
-                <div
222
-                  className='dashboard__moreinfo__webdav__btn genericBtnInfoDashboard__btn'
223
-                  onClick={this.handleToggleWebdavBtn}
224
-                >
225
-                  <div className='dashboard__moreinfo__webdav__btn__icon genericBtnInfoDashboard__btn__icon'>
226
-                    <i className='fa fa-windows' />
227
-                  </div>
228
-
229
-                  <div className='dashboard__moreinfo__webdav__btn__text genericBtnInfoDashboard__btn__text'>
230
-                    {this.props.t('Implement Tracim in your explorer')}
231
-                  </div>
232
-                </div>
233
-                {this.state.displayWebdavBtn === true &&
234
-                <div>
235
-                  <div className='dashboard__moreinfo__webdav__information genericBtnInfoDashboard__info'>
236
-                    <div className='dashboard__moreinfo__webdav__information__text genericBtnInfoDashboard__info__text'>
237
-                      {this.props.t('Find all your documents deposited online directly on your computer via the workstation, without going through the software.')}'
238
-                    </div>
239
-
240
-                    <div className='dashboard__moreinfo__webdav__information__link genericBtnInfoDashboard__info__link'>
241
-                      http://algoo.trac.im/webdav/
242
-                    </div>
243
-                  </div>
244
-                </div>
245
-                }
246
-              </div>
247
-              <div className='dashboard__moreinfo__calendar genericBtnInfoDashboard'>
248
-                <div className='dashboard__moreinfo__calendar__wrapperBtn'>
249
-                  <div
250
-                    className='dashboard__moreinfo__calendar__btn genericBtnInfoDashboard__btn'
251
-                    onClick={this.handleToggleCalendarBtn}
252
-                  >
253
-                    <div className='dashboard__moreinfo__calendar__btn__icon genericBtnInfoDashboard__btn__icon'>
254
-                      <i className='fa fa-calendar' />
255
-                    </div>
256
-
257
-                    <div className='dashboard__moreinfo__calendar__btn__text genericBtnInfoDashboard__btn__text'>
258
-                      {this.props.t('Workspace Calendar')}
259
-                    </div>
260
-                  </div>
261
-                </div>
262
-                <div className='dashboard__moreinfo__calendar__wrapperText'>
263
-                  {this.state.displayCalendarBtn === true &&
264
-                  <div>
265
-                    <div className='dashboard__moreinfo__calendar__information genericBtnInfoDashboard__info'>
266
-                      <div className='dashboard__moreinfo__calendar__information__text genericBtnInfoDashboard__info__text'>
267
-                        {this.props.t('Each workspace has its own calendar.')}
268
-                      </div>
269
-
270
-                      <div className='dashboard__moreinfo__calendar__information__link genericBtnInfoDashboard__info__link'>
271
-                        http://algoo.trac.im/calendar/
272
-                      </div>
273
-                    </div>
274
-                  </div>
275
-                  }
276
-                </div>
277
-              </div>
278
-            </div>
259
+            <MoreInfo
260
+              onClickToggleWebdav={this.handleToggleWebdavBtn}
261
+              displayWebdavBtn={state.displayWebdavBtn}
262
+              onClickToggleCalendar={this.handleToggleCalendarBtn}
263
+              displayCalendarBtn={state.displayCalendarBtn}
264
+              t={props.t}
265
+            />
279 266
           </PageContent>
280 267
         </PageWrapper>
281 268
       </div>

+ 6 - 163
frontend/src/css/Dashboard.styl View File

@@ -1,35 +1,9 @@
1
+@import "../../node_modules/tracim_frontend_lib/src/css/Variable.styl"
2
+
1 3
 flexwrap()
2 4
   display flex
3 5
   flex-wrap wrap
4 6
 
5
-btnNotification()
6
-  margin 20px 0
7
-  border 1px solid thirdColor
8
-  padding 10px 15px
9
-
10
-hoverfocus()
11
-  background-color thirdColor
12
-  color white
13
-
14
-bgandcolor()
15
-  background-color transparent
16
-  color thirdColor
17
-
18
-label()
19
-  font-weight 500
20
-  font-size 18px
21
-  color thirdColor
22
-
23
-coloricon()
24
-  .fa-gavel
25
-    color responsable
26
-  .fa-graduation-cap
27
-    color gestionnaire
28
-  .fa-pencil
29
-    color contributeur
30
-  .fa-eye
31
-    color lecteur
32
-
33 7
 .dashboard
34 8
   width 100%
35 9
   &__header
@@ -55,30 +29,6 @@ coloricon()
55 29
     &__detail
56 30
       margin-bottom 20px
57 31
       font-size 18px
58
-  &__userstatut
59
-    width 35%
60
-    &__role
61
-      margin 20px 0
62
-      font-size 18px
63
-      &__msg
64
-        margin-right 15px
65
-      &__definition
66
-        display flex
67
-        &__icon
68
-          margin-right 15px
69
-          color gestionnaire
70
-    &__notification
71
-      font-size 18px
72
-      &__btn
73
-        btnNotification()
74
-        cursor pointer
75
-      &__subscribe
76
-        &__btn
77
-          btnNotification()
78
-        &__submenu
79
-          padding 0
80
-          &__item
81
-            padding 10px
82 32
   &__calltoaction
83 33
     flexwrap()
84 34
     margin 100px 0
@@ -93,66 +43,6 @@ coloricon()
93 43
           font-size 18px
94 44
   &__workspaceInfo
95 45
     flexwrap()
96
-  &__activity
97
-    margin 0 35px 50px 0
98
-    width 60%
99
-    &__wrapper
100
-      border 1px solid grey
101
-      height 480px
102
-      overflow-y scroll
103
-    &__header
104
-      display flex
105
-      justify-content space-between
106
-      align-items center
107
-      margin-bottom 20px
108
-      height 44px
109
-      &__allread
110
-        padding 10px 25px
111
-        font-size 18px
112
-        cursor pointer
113
-    &__workspace
114
-      display flex
115
-      align-items center
116
-      border-bottom 1px solid grey
117
-      padding 15px
118
-      cursor pointer
119
-      &:hover
120
-        background-color fourthColor
121
-      &:nth-child(even)
122
-        background-color grey-hover
123
-        &:hover
124
-          background-color fourthColor
125
-      &__icon
126
-        margin 0 25px
127
-        font-size 25px
128
-      &__name
129
-        font-size 18px
130
-        font-weight 500
131
-        span
132
-          font-weight 400
133
-    &__more
134
-      &__btn
135
-        margin 15px
136
-        padding 10px 25px
137
-        cursor pointer
138
-  &__moreinfo
139
-    display flex
140
-    justify-content space-between
141
-    flexwrap wrap
142
-    &__webdav
143
-      margin 0 15px 40px 0
144
-      &__btn
145
-        width 300px
146
-      &__information
147
-        width 300px
148
-    &__calendar
149
-      margin-bottom 100px
150
-      &__wrapperBtn
151
-        margin-right 290px
152
-      &__btn
153
-        width 300px
154
-      &__information
155
-        width 300px
156 46
 
157 47
 /**** MEDIAQUERIES *****/
158 48
 
@@ -169,10 +59,6 @@ coloricon()
169 59
       width auto
170 60
     &__calltoaction
171 61
       justify-content center
172
-    &__activity
173
-      width 100%
174
-    &__memberlist
175
-      width 50%
176 62
 
177 63
 /**** MEDIA 992px & 1199px ****/
178 64
 
@@ -181,14 +67,6 @@ coloricon()
181 67
   .dashboard
182 68
     margin-left 15px
183 69
 
184
-/**** MEDIA 768px & 991px ****/
185
-
186
-@media (min-width: min-md) and (max-width: max-md)
187
-
188
-  .dashboard
189
-    &__activity
190
-      margin 25px 15px 25px 0
191
-
192 70
 /**** MEDIA 576px & 767px ****/
193 71
 
194 72
 @media (min-width: min-sm) and (max-width: max-sm)
@@ -196,57 +74,22 @@ coloricon()
196 74
   .dashboard
197 75
     &__activity
198 76
       margin 25px 15px 25px 0
199
-    &__memberlist
200
-      margin 50px 0
201
-      width 90%
202
-    &__moreinfo__webdav__information
203
-      width 500px
204 77
 
205 78
 /**** MEDIA 575px ****/
206 79
 
207 80
 @media (max-width: max-xs)
208 81
 
209
-  position()
210
-    margin-left 10px
211
-    width auto
212
-
213 82
   .dashboard
214 83
     margin-left 0
215 84
     &__title
216 85
       margin-left 10px
217 86
     &__workspace
218
-      position()
87
+      margin-left 10px
88
+      width auto
219 89
     &__userstatut
220
-     position()
90
+      margin-left 10px
91
+      width auto
221 92
     &__calltoaction
222 93
       justify-content center
223 94
       &__button
224 95
         margin 10px
225
-    &__activity
226
-      margin 25px 0
227
-      width 100%
228
-      &__header
229
-        display block
230
-        height auto
231
-        margin 0 15px 20px 15px
232
-        &__title
233
-          margin-bottom 20px
234
-    &__memberlist
235
-      margin 50px 0
236
-      width 100%
237
-      &__title
238
-        margin-left 10px
239
-      &__wrapper
240
-        height auto
241
-      &__list
242
-        height auto
243
-        overflow-Y visible
244
-        &__item:nth-last-child(1)
245
-          border-bottom 1px solid grey
246
-      &__btnadd
247
-        border-top 0
248
-    &__moreinfo
249
-      &__webdav
250
-        margin-left 10px
251
-        &__information
252
-          width 350px

+ 19 - 1
frontend/src/reducer/currentWorkspace.js View File

@@ -2,7 +2,7 @@ import {
2 2
   SET,
3 3
   WORKSPACE_DETAIL,
4 4
   WORKSPACE_MEMBER_LIST,
5
-  WORKSPACE_READ_STATUS_LIST,
5
+  WORKSPACE_READ_STATUS_LIST, WORKSPACE_RECENT_ACTIVITY_FOR_USER_LIST,
6 6
   WORKSPACE_RECENT_ACTIVITY_LIST
7 7
 } from '../action-creator.sync.js'
8 8
 import { handleRouteFromApi } from '../helper.js'
@@ -15,6 +15,7 @@ const defaultWorkspace = {
15 15
   sidebarEntryList: [],
16 16
   memberList: [],
17 17
   recentActivityList: [],
18
+  recentActivityForUserList: [],
18 19
   contentReadStatusList: []
19 20
 }
20 21
 
@@ -65,6 +66,23 @@ export default function currentWorkspace (state = defaultWorkspace, action) {
65 66
         }))
66 67
       }
67 68
 
69
+    case `${SET}/${WORKSPACE_RECENT_ACTIVITY_FOR_USER_LIST}`:
70
+      return {
71
+        ...state,
72
+        recentActivityForUserList: action.workspaceRecentActivityForUserList.map(ra => ({
73
+          id: ra.content_id,
74
+          slug: ra.slug,
75
+          label: ra.label,
76
+          type: ra.content_type,
77
+          idParent: ra.parent_id,
78
+          showInUi: ra.show_in_ui,
79
+          isArchived: ra.is_archived,
80
+          isDeleted: ra.is_deleted,
81
+          statusSlug: ra.status,
82
+          subContentTypeSlug: ra.sub_content_types
83
+        }))
84
+      }
85
+
68 86
     case `${SET}/${WORKSPACE_READ_STATUS_LIST}`:
69 87
       return {
70 88
         ...state,