Browse Source

Merge branch 'develop' into feature/650_folder_missing_endpoint_about_edit

inkhey 6 years ago
parent
commit
58a917f247
No account linked to committer's email
61 changed files with 2039 additions and 1261 deletions
  1. 0 1
      .travis.yml
  2. 67 0
      backend/tests_configs.ini
  3. 3 1
      backend/tracim_backend/config.py
  4. 3 1
      backend/tracim_backend/exceptions.py
  5. 40 2
      backend/tracim_backend/lib/core/user.py
  6. 24 0
      backend/tracim_backend/lib/utils/utils.py
  7. 14 2
      backend/tracim_backend/models/applications.py
  8. 6 4
      backend/tracim_backend/models/contents.py
  9. 8 0
      backend/tracim_backend/models/context_models.py
  10. 8 11
      backend/tracim_backend/tests/__init__.py
  11. 24 72
      backend/tracim_backend/tests/functional/test_system.py
  12. 412 15
      backend/tracim_backend/tests/functional/test_user.py
  13. 98 27
      backend/tracim_backend/tests/functional/test_workspaces.py
  14. 183 0
      backend/tracim_backend/tests/library/test_user_api.py
  15. 14 0
      backend/tracim_backend/tests/library/tests_utils.py
  16. 6 0
      backend/tracim_backend/views/contents_api/comment_controller.py
  17. 28 0
      backend/tracim_backend/views/contents_api/file_controller.py
  18. 8 0
      backend/tracim_backend/views/contents_api/html_document_controller.py
  19. 8 0
      backend/tracim_backend/views/contents_api/threads_controller.py
  20. 13 0
      backend/tracim_backend/views/core_api/schemas.py
  21. 57 0
      backend/tracim_backend/views/core_api/user_controller.py
  22. 17 2
      backend/tracim_backend/views/core_api/workspace_controller.py
  23. 1 1
      bash_library.sh
  24. 51 54
      build_full_frontend.sh
  25. 1 0
      frontend/dist/appInterface.js
  26. 0 1
      frontend/dist/index.html
  27. 1 0
      frontend/dist/tinymceInit.js
  28. 6 1
      frontend/i18next.scanner/en/translation.json
  29. 6 1
      frontend/i18next.scanner/fr/translation.json
  30. 34 16
      frontend/src/action-creator.async.js
  31. 9 1
      frontend/src/action-creator.sync.js
  32. 1 1
      frontend/src/component/Account/Notification.jsx
  33. 41 0
      frontend/src/component/Dashboard/ContentTypeBtn.jsx
  34. 16 0
      frontend/src/component/Dashboard/ContentTypeBtn.styl
  35. 162 0
      frontend/src/component/Dashboard/MemberList.jsx
  36. 117 0
      frontend/src/component/Dashboard/MemberList.styl
  37. 50 0
      frontend/src/component/Dashboard/RecentActivity.jsx
  38. 2 5
      frontend/src/component/Workspace/OpenContentApp.jsx
  39. 1 4
      frontend/src/container/Account.jsx
  40. 138 0
      frontend/src/container/AdminWorkspacePage.jsx
  41. 1 4
      frontend/src/container/AppFullscreenManager.jsx
  42. 76 193
      frontend/src/container/Dashboard.jsx
  43. 0 571
      frontend/src/container/Dashboard_old.jsx
  44. 5 5
      frontend/src/container/Login.jsx
  45. 18 6
      frontend/src/container/Sidebar.jsx
  46. 53 35
      frontend/src/container/Tracim.jsx
  47. 3 6
      frontend/src/container/WorkspaceContent.jsx
  48. 11 0
      frontend/src/css/AdminWorkspacePage.styl
  49. 5 121
      frontend/src/css/Dashboard.styl
  50. 1 18
      frontend/src/css/Generic.styl
  51. 0 1
      frontend/src/css/Workspace.styl
  52. 19 17
      frontend/src/helper.js
  53. 39 6
      frontend/src/reducer/currentWorkspace.js
  54. 1 1
      frontend/src/reducer/workspaceContentList.js
  55. 1 1
      frontend_lib/dist/index.html
  56. 0 15
      frontend_lib/dist/tracim_lib.js
  57. 2 2
      frontend_lib/package.json
  58. 68 0
      frontend_lib/src/component/Input/Checkbox.jsx
  59. 7 0
      frontend_lib/src/index.dev.js
  60. 6 8
      frontend_lib/src/index.js
  61. 45 28
      install_frontend_dependencies.sh

+ 0 - 1
.travis.yml View File

@@ -3,7 +3,6 @@ matrix:
3 3
     - sudo: false
4 4
       language: python
5 5
       python:
6
-        - "3.4"
7 6
         - "3.5"
8 7
         - "3.6"
9 8
 

+ 67 - 0
backend/tests_configs.ini View File

@@ -63,3 +63,70 @@ email.notification.smtp.server = 127.0.0.1
63 63
 email.notification.smtp.port = 1025
64 64
 email.notification.smtp.user = test_user
65 65
 email.notification.smtp.password = just_a_password
66
+
67
+[functional_test]
68
+sqlalchemy.url = sqlite:///tracim_test.sqlite
69
+depot_storage_name = test
70
+depot_storage_dir = /tmp/test/depot
71
+user.auth_token.validity = 604800
72
+preview_cache_dir = /tmp/test/preview_cache
73
+preview.jpg.restricted_dims = True
74
+email.notification.activated = false
75
+
76
+[functional_test_no_db]
77
+sqlalchemy.url = sqlite://
78
+depot_storage_name = test
79
+depot_storage_dir = /tmp/test/depot
80
+user.auth_token.validity = 604800
81
+preview_cache_dir = /tmp/test/preview_cache
82
+preview.jpg.restricted_dims = True
83
+email.notification.activated = false
84
+
85
+[functional_test_with_mail_test_sync]
86
+sqlalchemy.url = sqlite:///tracim_test.sqlite
87
+depot_storage_name = test
88
+depot_storage_dir = /tmp/test/depot
89
+user.auth_token.validity = 604800
90
+preview_cache_dir = /tmp/test/preview_cache
91
+preview.jpg.restricted_dims = True
92
+email.notification.activated = true
93
+email.notification.from.email = test_user_from+{user_id}@localhost
94
+email.notification.from.default_label = Tracim Notifications
95
+email.notification.reply_to.email = test_user_reply+{content_id}@localhost
96
+email.notification.references.email = test_user_refs+{content_id}@localhost
97
+email.notification.content_update.template.html = %(here)s/tracim_backend/templates/mail/content_update_body_html.mak
98
+email.notification.content_update.template.text = %(here)s/tracim_backend/templates/mail/content_update_body_text.mak
99
+email.notification.created_account.template.html = %(here)s/tracim_backend/templates/mail/created_account_body_html.mak
100
+email.notification.created_account.template.text = %(here)s/tracim_backend/templates/mail/created_account_body_text.mak
101
+email.notification.content_update.subject = [{website_title}] [{workspace_label}] {content_label} ({content_status_label})
102
+email.notification.created_account.subject = [{website_title}] Created account
103
+email.notification.processing_mode = sync
104
+email.notification.smtp.server = 127.0.0.1
105
+email.notification.smtp.port = 1025
106
+email.notification.smtp.user = test_user
107
+email.notification.smtp.password = just_a_password
108
+
109
+
110
+[functional_test_with_mail_test_async]
111
+sqlalchemy.url = sqlite:///tracim_test.sqlite
112
+depot_storage_name = test
113
+depot_storage_dir = /tmp/test/depot
114
+user.auth_token.validity = 604800
115
+preview_cache_dir = /tmp/test/preview_cache
116
+preview.jpg.restricted_dims = True
117
+email.notification.activated = true
118
+email.notification.from.email = test_user_from+{user_id}@localhost
119
+email.notification.from.default_label = Tracim Notifications
120
+email.notification.reply_to.email = test_user_reply+{content_id}@localhost
121
+email.notification.references.email = test_user_refs+{content_id}@localhost
122
+email.notification.content_update.template.html = %(here)s/tracim_backend/templates/mail/content_update_body_html.mak
123
+email.notification.content_update.template.text = %(here)s/tracim_backend/templates/mail/content_update_body_text.mak
124
+email.notification.created_account.template.html = %(here)s/tracim_backend/templates/mail/created_account_body_html.mak
125
+email.notification.created_account.template.text = %(here)s/tracim_backend/templates/mail/created_account_body_text.mak
126
+email.notification.content_update.subject = [{website_title}] [{workspace_label}] {content_label} ({content_status_label})
127
+email.notification.created_account.subject = [{website_title}] Created account
128
+email.notification.processing_mode = async
129
+email.notification.smtp.server = 127.0.0.1
130
+email.notification.smtp.port = 1025
131
+email.notification.smtp.user = test_user
132
+email.notification.smtp.password = just_a_password

+ 3 - 1
backend/tracim_backend/config.py View File

@@ -161,9 +161,11 @@ class CFG(object):
161 161
 
162 162
         self.EMAIL_NOTIFICATION_FROM_EMAIL = settings.get(
163 163
             'email.notification.from.email',
164
+            'noreply+{user_id}@trac.im'
164 165
         )
165 166
         self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = settings.get(
166
-            'email.notification.from.default_label'
167
+            'email.notification.from.default_label',
168
+            'Tracim Notifications'
167 169
         )
168 170
         self.EMAIL_NOTIFICATION_REPLY_TO_EMAIL = settings.get(
169 171
             'email.notification.reply_to.email',

+ 3 - 1
backend/tracim_backend/exceptions.py View File

@@ -204,6 +204,8 @@ class PageOfPreviewNotFound(NotFound):
204 204
 class PreviewDimNotAllowed(TracimException):
205 205
     pass
206 206
 
207
-
208 207
 class UnallowedSubContent(TracimException):
209 208
     pass
209
+
210
+class TooShortAutocompleteString(TracimException):
211
+    pass

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

@@ -3,13 +3,17 @@ from smtplib import SMTPException
3 3
 
4 4
 import transaction
5 5
 import typing as typing
6
+
7
+from sqlalchemy import or_
6 8
 from sqlalchemy.orm import Session
9
+from sqlalchemy.orm import Query
7 10
 from sqlalchemy.orm.exc import NoResultFound
8 11
 
9 12
 from tracim_backend import CFG
10 13
 from tracim_backend.models.auth import User
11 14
 from tracim_backend.models.auth import Group
12 15
 from tracim_backend.exceptions import NoUserSetted
16
+from tracim_backend.exceptions import TooShortAutocompleteString
13 17
 from tracim_backend.exceptions import PasswordDoNotMatch
14 18
 from tracim_backend.exceptions import EmailValidationFailed
15 19
 from tracim_backend.exceptions import UserDoesNotExist
@@ -20,6 +24,7 @@ from tracim_backend.exceptions import UserNotActive
20 24
 from tracim_backend.models.context_models import UserInContext
21 25
 from tracim_backend.lib.mail_notifier.notifier import get_email_manager
22 26
 from tracim_backend.models.context_models import TypeUser
27
+from tracim_backend.models.data import UserRoleInWorkspace
23 28
 
24 29
 
25 30
 class UserApi(object):
@@ -94,8 +99,41 @@ class UserApi(object):
94 99
             raise UserDoesNotExist('There is no current user')
95 100
         return self._user
96 101
 
102
+    def _get_all_query(self) -> Query:
103
+        return self._session.query(User).order_by(User.display_name)
104
+
97 105
     def get_all(self) -> typing.Iterable[User]:
98
-        return self._session.query(User).order_by(User.display_name).all()
106
+        return self._get_all_query().all()
107
+
108
+    def get_known_user(
109
+            self,
110
+            acp: str,
111
+    ) -> typing.Iterable[User]:
112
+        """
113
+        Return list of know user by current UserApi user.
114
+        :param acp: autocomplete filter by name/email
115
+        :return: List of found users
116
+        """
117
+        if len(acp) < 2:
118
+            raise TooShortAutocompleteString(
119
+                '"{acp}" is a too short string, acp string need to have more than one character'.format(acp=acp)  # nopep8
120
+            )
121
+        query = self._get_all_query()
122
+        query = query.filter(or_(User.display_name.ilike('%{}%'.format(acp)), User.email.ilike('%{}%'.format(acp))))  # nopep8
123
+
124
+        # INFO - G.M - 2018-07-27 - if user is set and is simple user, we
125
+        # should show only user in same workspace as user
126
+        if self._user and self._user.profile.id <= Group.TIM_USER:
127
+            user_workspaces_id_query = self._session.\
128
+                query(UserRoleInWorkspace.workspace_id).\
129
+                distinct(UserRoleInWorkspace.workspace_id).\
130
+                filter(UserRoleInWorkspace.user_id == self._user.user_id)
131
+            users_in_workspaces = self._session.\
132
+                query(UserRoleInWorkspace.user_id).\
133
+                distinct(UserRoleInWorkspace.user_id).\
134
+                filter(UserRoleInWorkspace.workspace_id.in_(user_workspaces_id_query.subquery())).subquery()  # nopep8
135
+            query = query.filter(User.user_id.in_(users_in_workspaces))
136
+        return query.all()
99 137
 
100 138
     def find(
101 139
             self,
@@ -196,7 +234,7 @@ class UserApi(object):
196 234
         )
197 235
         if do_save:
198 236
             # TODO - G.M - 2018-07-24 - Check why commit is needed here
199
-            transaction.commit()
237
+            self.save(user)
200 238
         return user
201 239
 
202 240
     def set_email(

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

@@ -1,5 +1,7 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import datetime
3
+import random
4
+import string
3 5
 from redis import Redis
4 6
 from rq import Queue
5 7
 
@@ -72,3 +74,25 @@ def current_date_for_filename() -> str:
72 74
     # webdav utils, it may cause trouble. So, it should be replaced to
73 75
     # a character which will not change in bdd.
74 76
     return datetime.datetime.now().isoformat().replace(':', '.')
77
+
78
+# INFO - G.M - 2018-08-02 - Simple password generator, inspired by
79
+# https://gist.github.com/23maverick23/4131896
80
+
81
+
82
+ALLOWED_AUTOGEN_PASSWORD_CHAR = string.ascii_letters + \
83
+                                string.digits + \
84
+                                string.punctuation
85
+
86
+DEFAULT_PASSWORD_GEN_CHAR_LENGTH = 12
87
+
88
+
89
+def password_generator(
90
+        length: int=DEFAULT_PASSWORD_GEN_CHAR_LENGTH,
91
+        chars: str=ALLOWED_AUTOGEN_PASSWORD_CHAR
92
+) -> str:
93
+    """
94
+    :param length: length of the new password
95
+    :param chars: characters allowed
96
+    :return: password as string
97
+    """
98
+    return ''.join(random.choice(chars) for char_number in range(length))

+ 14 - 2
backend/tracim_backend/models/applications.py View File

@@ -59,6 +59,16 @@ thread = Application(
59 59
 
60 60
 )
61 61
 
62
+folder = Application(
63
+    label='Folder',
64
+    slug='contents/folder',
65
+    fa_icon='folder-open-o',
66
+    hexcolor='#252525',
67
+    is_active=True,
68
+    config={},
69
+    main_route='',
70
+)
71
+
62 72
 _file = Application(
63 73
     label='Files',
64 74
     slug='contents/file',
@@ -92,8 +102,10 @@ html_documents = Application(
92 102
 # List of applications
93 103
 applications = [
94 104
     html_documents,
95
-    markdownpluspage,
105
+    # TODO - G.M - 2018-08-02 - Restore markdownpage app
106
+    # markdownpluspage,
96 107
     _file,
97 108
     thread,
98
-    calendar,
109
+    folder,
110
+    # calendar,
99 111
 ]

+ 6 - 4
backend/tracim_backend/models/contents.py View File

@@ -6,6 +6,7 @@ from tracim_backend.exceptions import ContentTypeNotExist
6 6
 from tracim_backend.exceptions import ContentStatusNotExist
7 7
 from tracim_backend.models.applications import html_documents
8 8
 from tracim_backend.models.applications import _file
9
+from tracim_backend.models.applications import folder
9 10
 from tracim_backend.models.applications import thread
10 11
 from tracim_backend.models.applications import markdownpluspage
11 12
 
@@ -173,10 +174,10 @@ html_documents_type = ContentType(
173 174
 # TODO - G.M - 31-05-2018 - Set Better folder params
174 175
 folder_type = ContentType(
175 176
     slug='folder',
176
-    fa_icon=thread.fa_icon,
177
-    hexcolor=thread.hexcolor,
177
+    fa_icon=folder.fa_icon,
178
+    hexcolor=folder.hexcolor,
178 179
     label='Folder',
179
-    creation_label='Create collection of any documents',
180
+    creation_label='Create a folder',
180 181
     available_statuses=CONTENT_STATUS.get_all(),
181 182
     allow_sub_content=True,
182 183
 )
@@ -283,7 +284,8 @@ CONTENT_TYPES = ContentTypeList(
283 284
     [
284 285
         thread_type,
285 286
         file_type,
286
-        markdownpluspage_type,
287
+        # TODO - G.M - 2018-08-02 - Restore markdown page content
288
+        #    markdownpluspage_type,
287 289
         html_documents_type,
288 290
     ]
289 291
 )

+ 8 - 0
backend/tracim_backend/models/context_models.py View File

@@ -186,6 +186,14 @@ class CommentPath(object):
186 186
         self.comment_id = comment_id
187 187
 
188 188
 
189
+class AutocompleteQuery(object):
190
+    """
191
+    Autocomplete query model
192
+    """
193
+    def __init__(self, acp: str):
194
+        self.acp = acp
195
+
196
+
189 197
 class PageQuery(object):
190 198
     """
191 199
     Page query model

+ 8 - 11
backend/tracim_backend/tests/__init__.py View File

@@ -68,20 +68,17 @@ def create_1000px_png_test_image():
68 68
 class FunctionalTest(unittest.TestCase):
69 69
 
70 70
     fixtures = [BaseFixture]
71
-    sqlalchemy_url = 'sqlite:///tracim_test.sqlite'
71
+    config_uri = 'tests_configs.ini'
72
+    config_section = 'functional_test'
72 73
 
73 74
     def setUp(self):
74 75
         logger._logger.setLevel('WARNING')
76
+
75 77
         DepotManager._clear()
76
-        self.settings = {
77
-            'sqlalchemy.url': self.sqlalchemy_url,
78
-            'user.auth_token.validity': '604800',
79
-            'depot_storage_dir': '/tmp/test/depot',
80
-            'depot_storage_name': 'test',
81
-            'preview_cache_dir': '/tmp/test/preview_cache',
82
-            'preview.jpg.restricted_dims': True,
83
-            'email.notification.activated': 'false',
84
-        }
78
+        self.settings = plaster.get_settings(
79
+            self.config_uri,
80
+            self.config_section
81
+        )
85 82
         hapic.reset_context()
86 83
         self.engine = get_engine(self.settings)
87 84
         DeclarativeBase.metadata.create_all(self.engine)
@@ -127,7 +124,7 @@ class FunctionalTestEmptyDB(FunctionalTest):
127 124
 
128 125
 
129 126
 class FunctionalTestNoDB(FunctionalTest):
130
-    sqlalchemy_url = 'sqlite://'
127
+    config_section = 'functional_test_no_db'
131 128
 
132 129
     def init_database(self, settings):
133 130
         self.engine = get_engine(settings)

+ 24 - 72
backend/tracim_backend/tests/functional/test_system.py View File

@@ -1,5 +1,7 @@
1 1
 # coding=utf-8
2
+from tracim_backend.models.contents import CONTENT_TYPES
2 3
 from tracim_backend.tests import FunctionalTest
4
+from tracim_backend.models.applications import applications
3 5
 
4 6
 """
5 7
 Tests for /api/v2/system subpath endpoints.
@@ -24,41 +26,14 @@ class TestApplicationEndpoint(FunctionalTest):
24 26
         )
25 27
         res = self.testapp.get('/api/v2/system/applications', status=200)
26 28
         res = res.json_body
27
-        application = res[0]
28
-        assert application['label'] == "Text Documents"
29
-        assert application['slug'] == 'contents/html-document'
30
-        assert application['fa_icon'] == 'file-text-o'
31
-        assert application['hexcolor'] == '#3f52e3'
32
-        assert application['is_active'] is True
33
-        assert 'config' in application
34
-        application = res[1]
35
-        assert application['label'] == "Markdown Plus Documents"
36
-        assert application['slug'] == 'contents/markdownpluspage'
37
-        assert application['fa_icon'] == 'file-code-o'
38
-        assert application['hexcolor'] == '#f12d2d'
39
-        assert application['is_active'] is True
40
-        assert 'config' in application
41
-        application = res[2]
42
-        assert application['label'] == "Files"
43
-        assert application['slug'] == 'contents/file'
44
-        assert application['fa_icon'] == 'paperclip'
45
-        assert application['hexcolor'] == '#FF9900'
46
-        assert application['is_active'] is True
47
-        assert 'config' in application
48
-        application = res[3]
49
-        assert application['label'] == "Threads"
50
-        assert application['slug'] == 'contents/thread'
51
-        assert application['fa_icon'] == 'comments-o'
52
-        assert application['hexcolor'] == '#ad4cf9'
53
-        assert application['is_active'] is True
54
-        assert 'config' in application
55
-        application = res[4]
56
-        assert application['label'] == "Calendar"
57
-        assert application['slug'] == 'calendar'
58
-        assert application['fa_icon'] == 'calendar'
59
-        assert application['hexcolor'] == '#757575'
60
-        assert application['is_active'] is True
61
-        assert 'config' in application
29
+        assert len(res) == len(applications)
30
+        for counter, application in enumerate(applications):
31
+            assert res[counter]['label'] == application.label
32
+            assert res[counter]['slug'] == application.slug
33
+            assert res[counter]['fa_icon'] == application.fa_icon
34
+            assert res[counter]['hexcolor'] == application.hexcolor
35
+            assert res[counter]['is_active'] == application.is_active
36
+            assert res[counter]['config'] == application.config
62 37
 
63 38
     def test_api__get_applications__err_401__unregistered_user(self):
64 39
         """
@@ -96,44 +71,21 @@ class TestContentsTypesEndpoint(FunctionalTest):
96 71
         )
97 72
         res = self.testapp.get('/api/v2/system/content_types', status=200)
98 73
         res = res.json_body
74
+        assert len(res) == len(CONTENT_TYPES.endpoint_allowed_types_slug())
75
+        content_types = CONTENT_TYPES.endpoint_allowed_types_slug()
99 76
 
100
-        content_type = res[1]
101
-        assert content_type['slug'] == 'thread'
102
-        assert content_type['fa_icon'] == 'comments-o'
103
-        assert content_type['hexcolor'] == '#ad4cf9'
104
-        assert content_type['label'] == 'Thread'
105
-        assert content_type['creation_label'] == 'Discuss about a topic'
106
-        assert 'available_statuses' in content_type
107
-        assert len(content_type['available_statuses']) == 4
108
-
109
-        content_type = res[2]
110
-        assert content_type['slug'] == 'file'
111
-        assert content_type['fa_icon'] == 'paperclip'
112
-        assert content_type['hexcolor'] == '#FF9900'
113
-        assert content_type['label'] == 'File'
114
-        assert content_type['creation_label'] == 'Upload a file'
115
-        assert 'available_statuses' in content_type
116
-        assert len(content_type['available_statuses']) == 4
117
-
118
-        content_type = res[3]
119
-        assert content_type['slug'] == 'markdownpage'
120
-        assert content_type['fa_icon'] == 'file-code-o'
121
-        assert content_type['hexcolor'] == '#f12d2d'
122
-        assert content_type['label'] == 'Rich Markdown File'
123
-        assert content_type['creation_label'] == 'Create a Markdown document'
124
-        assert 'available_statuses' in content_type
125
-        assert len(content_type['available_statuses']) == 4
126
-
127
-        content_type = res[4]
128
-        assert content_type['slug'] == 'html-document'
129
-        assert content_type['fa_icon'] == 'file-text-o'
130
-        assert content_type['hexcolor'] == '#3f52e3'
131
-        assert content_type['label'] == 'Text Document'
132
-        assert content_type['creation_label'] == 'Write a document'
133
-        assert 'available_statuses' in content_type
134
-        assert len(content_type['available_statuses']) == 4
135
-        # TODO - G.M - 31-05-2018 - Check Folder type
136
-        # TODO - G.M - 29-05-2018 - Better check for available_statuses
77
+        for counter, content_type_slug in enumerate(content_types):
78
+            content_type = CONTENT_TYPES.get_one_by_slug(content_type_slug)
79
+            assert res[counter]['slug'] == content_type.slug
80
+            assert res[counter]['fa_icon'] == content_type.fa_icon
81
+            assert res[counter]['hexcolor'] == content_type.hexcolor
82
+            assert res[counter]['label'] == content_type.label
83
+            assert res[counter]['creation_label'] == content_type.creation_label
84
+            for status_counter, status in enumerate(content_type.available_statuses):
85
+                assert res[counter]['available_statuses'][status_counter]['fa_icon'] == status.fa_icon  # nopep8
86
+                assert res[counter]['available_statuses'][status_counter]['global_status'] == status.global_status  # nopep8
87
+                assert res[counter]['available_statuses'][status_counter]['slug'] == status.slug  # nopep8
88
+                assert res[counter]['available_statuses'][status_counter]['hexcolor'] == status.hexcolor  # nopep8
137 89
 
138 90
     def test_api__get_content_types__err_401__unregistered_user(self):
139 91
         """

+ 412 - 15
backend/tracim_backend/tests/functional/test_user.py View File

@@ -2382,8 +2382,10 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
2382 2382
         assert workspace['workspace_id'] == 1
2383 2383
         assert workspace['label'] == 'Business'
2384 2384
         assert workspace['slug'] == 'business'
2385
-        assert len(workspace['sidebar_entries']) == 7
2385
+        assert len(workspace['sidebar_entries']) == 5
2386 2386
 
2387
+        # TODO - G.M - 2018-08-02 - Better test for sidebar entry, make it
2388
+        # not fixed on active application/content-file
2387 2389
         sidebar_entry = workspace['sidebar_entries'][0]
2388 2390
         assert sidebar_entry['slug'] == 'dashboard'
2389 2391
         assert sidebar_entry['label'] == 'Dashboard'
@@ -2406,32 +2408,19 @@ class TestUserWorkspaceEndpoint(FunctionalTest):
2406 2408
         assert sidebar_entry['fa_icon'] == "file-text-o"
2407 2409
 
2408 2410
         sidebar_entry = workspace['sidebar_entries'][3]
2409
-        assert sidebar_entry['slug'] == 'contents/markdownpluspage'
2410
-        assert sidebar_entry['label'] == 'Markdown Plus Documents'
2411
-        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
2412
-        assert sidebar_entry['hexcolor'] == "#f12d2d"
2413
-        assert sidebar_entry['fa_icon'] == "file-code-o"
2414
-
2415
-        sidebar_entry = workspace['sidebar_entries'][4]
2416 2411
         assert sidebar_entry['slug'] == 'contents/file'
2417 2412
         assert sidebar_entry['label'] == 'Files'
2418 2413
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
2419 2414
         assert sidebar_entry['hexcolor'] == "#FF9900"
2420 2415
         assert sidebar_entry['fa_icon'] == "paperclip"
2421 2416
 
2422
-        sidebar_entry = workspace['sidebar_entries'][5]
2417
+        sidebar_entry = workspace['sidebar_entries'][4]
2423 2418
         assert sidebar_entry['slug'] == 'contents/thread'
2424 2419
         assert sidebar_entry['label'] == 'Threads'
2425 2420
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
2426 2421
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
2427 2422
         assert sidebar_entry['fa_icon'] == "comments-o"
2428 2423
 
2429
-        sidebar_entry = workspace['sidebar_entries'][6]
2430
-        assert sidebar_entry['slug'] == 'calendar'
2431
-        assert sidebar_entry['label'] == 'Calendar'
2432
-        assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
2433
-        assert sidebar_entry['hexcolor'] == "#757575"
2434
-        assert sidebar_entry['fa_icon'] == "calendar"
2435 2424
 
2436 2425
     def test_api__get_user_workspaces__err_403__unallowed_user(self):
2437 2426
         """
@@ -2646,6 +2635,390 @@ class TestUserEndpoint(FunctionalTest):
2646 2635
         )
2647 2636
 
2648 2637
 
2638
+class TestUsersEndpoint(FunctionalTest):
2639
+    # -*- coding: utf-8 -*-
2640
+    """
2641
+    Tests for GET /api/v2/users/{user_id}
2642
+    """
2643
+    fixtures = [BaseFixture]
2644
+
2645
+    def test_api__get_user__ok_200__admin(self):
2646
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2647
+        admin = dbsession.query(models.User) \
2648
+            .filter(models.User.email == 'admin@admin.admin') \
2649
+            .one()
2650
+        uapi = UserApi(
2651
+            current_user=admin,
2652
+            session=dbsession,
2653
+            config=self.app_config,
2654
+        )
2655
+        gapi = GroupApi(
2656
+            current_user=admin,
2657
+            session=dbsession,
2658
+            config=self.app_config,
2659
+        )
2660
+        groups = [gapi.get_one_with_name('users')]
2661
+        test_user = uapi.create_user(
2662
+            email='test@test.test',
2663
+            password='pass',
2664
+            name='bob',
2665
+            groups=groups,
2666
+            timezone='Europe/Paris',
2667
+            do_save=True,
2668
+            do_notify=False,
2669
+        )
2670
+        uapi.save(test_user)
2671
+        transaction.commit()
2672
+        user_id = int(test_user.user_id)
2673
+
2674
+        self.testapp.authorization = (
2675
+            'Basic',
2676
+            (
2677
+                'admin@admin.admin',
2678
+                'admin@admin.admin'
2679
+            )
2680
+        )
2681
+        res = self.testapp.get(
2682
+            '/api/v2/users',
2683
+            status=200
2684
+        )
2685
+        res = res.json_body
2686
+        assert len(res) == 2
2687
+        assert res[0]['user_id'] == admin.user_id
2688
+        assert res[0]['public_name'] == admin.display_name
2689
+        assert res[0]['avatar_url'] is None
2690
+
2691
+        assert res[1]['user_id'] == test_user.user_id
2692
+        assert res[1]['public_name'] == test_user.display_name
2693
+        assert res[1]['avatar_url'] is None
2694
+
2695
+    def test_api__get_user__err_403__normal_user(self):
2696
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2697
+        admin = dbsession.query(models.User) \
2698
+            .filter(models.User.email == 'admin@admin.admin') \
2699
+            .one()
2700
+        uapi = UserApi(
2701
+            current_user=admin,
2702
+            session=dbsession,
2703
+            config=self.app_config,
2704
+        )
2705
+        gapi = GroupApi(
2706
+            current_user=admin,
2707
+            session=dbsession,
2708
+            config=self.app_config,
2709
+        )
2710
+        groups = [gapi.get_one_with_name('users')]
2711
+        test_user = uapi.create_user(
2712
+            email='test@test.test',
2713
+            password='pass',
2714
+            name='bob',
2715
+            groups=groups,
2716
+            timezone='Europe/Paris',
2717
+            do_save=True,
2718
+            do_notify=False,
2719
+        )
2720
+        uapi.save(test_user)
2721
+        transaction.commit()
2722
+        user_id = int(test_user.user_id)
2723
+
2724
+        self.testapp.authorization = (
2725
+            'Basic',
2726
+            (
2727
+                'test@test.test',
2728
+                'pass'
2729
+            )
2730
+        )
2731
+        self.testapp.get(
2732
+            '/api/v2/users',
2733
+            status=403
2734
+        )
2735
+
2736
+
2737
+class TestKnownMembersEndpoint(FunctionalTest):
2738
+    # -*- coding: utf-8 -*-
2739
+    """
2740
+    Tests for GET /api/v2/users/{user_id}
2741
+    """
2742
+    fixtures = [BaseFixture]
2743
+
2744
+    def test_api__get_user__ok_200__admin__by_name(self):
2745
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2746
+        admin = dbsession.query(models.User) \
2747
+            .filter(models.User.email == 'admin@admin.admin') \
2748
+            .one()
2749
+        uapi = UserApi(
2750
+            current_user=admin,
2751
+            session=dbsession,
2752
+            config=self.app_config,
2753
+        )
2754
+        gapi = GroupApi(
2755
+            current_user=admin,
2756
+            session=dbsession,
2757
+            config=self.app_config,
2758
+        )
2759
+        groups = [gapi.get_one_with_name('users')]
2760
+        test_user = uapi.create_user(
2761
+            email='test@test.test',
2762
+            password='pass',
2763
+            name='bob',
2764
+            groups=groups,
2765
+            timezone='Europe/Paris',
2766
+            do_save=True,
2767
+            do_notify=False,
2768
+        )
2769
+        test_user2 = uapi.create_user(
2770
+            email='test2@test2.test2',
2771
+            password='pass',
2772
+            name='bob2',
2773
+            groups=groups,
2774
+            timezone='Europe/Paris',
2775
+            do_save=True,
2776
+            do_notify=False,
2777
+        )
2778
+        uapi.save(test_user)
2779
+        uapi.save(test_user2)
2780
+        transaction.commit()
2781
+        user_id = int(admin.user_id)
2782
+
2783
+        self.testapp.authorization = (
2784
+            'Basic',
2785
+            (
2786
+                'admin@admin.admin',
2787
+                'admin@admin.admin'
2788
+            )
2789
+        )
2790
+        params = {
2791
+            'acp': 'bob',
2792
+        }
2793
+        res = self.testapp.get(
2794
+            '/api/v2/users/{user_id}/known_members'.format(user_id=user_id),
2795
+            status=200,
2796
+            params=params,
2797
+        )
2798
+        res = res.json_body
2799
+        assert len(res) == 2
2800
+        assert res[0]['user_id'] == test_user.user_id
2801
+        assert res[0]['public_name'] == test_user.display_name
2802
+        assert res[0]['avatar_url'] is None
2803
+
2804
+        assert res[1]['user_id'] == test_user2.user_id
2805
+        assert res[1]['public_name'] == test_user2.display_name
2806
+        assert res[1]['avatar_url'] is None
2807
+
2808
+    def test_api__get_user__ok_200__admin__by_email(self):
2809
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2810
+        admin = dbsession.query(models.User) \
2811
+            .filter(models.User.email == 'admin@admin.admin') \
2812
+            .one()
2813
+        uapi = UserApi(
2814
+            current_user=admin,
2815
+            session=dbsession,
2816
+            config=self.app_config,
2817
+        )
2818
+        gapi = GroupApi(
2819
+            current_user=admin,
2820
+            session=dbsession,
2821
+            config=self.app_config,
2822
+        )
2823
+        groups = [gapi.get_one_with_name('users')]
2824
+        test_user = uapi.create_user(
2825
+            email='test@test.test',
2826
+            password='pass',
2827
+            name='bob',
2828
+            groups=groups,
2829
+            timezone='Europe/Paris',
2830
+            do_save=True,
2831
+            do_notify=False,
2832
+        )
2833
+        test_user2 = uapi.create_user(
2834
+            email='test2@test2.test2',
2835
+            password='pass',
2836
+            name='bob2',
2837
+            groups=groups,
2838
+            timezone='Europe/Paris',
2839
+            do_save=True,
2840
+            do_notify=False,
2841
+        )
2842
+        uapi.save(test_user)
2843
+        uapi.save(test_user2)
2844
+        transaction.commit()
2845
+        user_id = int(admin.user_id)
2846
+
2847
+        self.testapp.authorization = (
2848
+            'Basic',
2849
+            (
2850
+                'admin@admin.admin',
2851
+                'admin@admin.admin'
2852
+            )
2853
+        )
2854
+        params = {
2855
+            'acp': 'test',
2856
+        }
2857
+        res = self.testapp.get(
2858
+            '/api/v2/users/{user_id}/known_members'.format(user_id=user_id),
2859
+            status=200,
2860
+            params=params,
2861
+        )
2862
+        res = res.json_body
2863
+        assert len(res) == 2
2864
+        assert res[0]['user_id'] == test_user.user_id
2865
+        assert res[0]['public_name'] == test_user.display_name
2866
+        assert res[0]['avatar_url'] is None
2867
+
2868
+        assert res[1]['user_id'] == test_user2.user_id
2869
+        assert res[1]['public_name'] == test_user2.display_name
2870
+        assert res[1]['avatar_url'] is None
2871
+
2872
+    def test_api__get_user__err_403__admin__too_small_acp(self):
2873
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2874
+        admin = dbsession.query(models.User) \
2875
+            .filter(models.User.email == 'admin@admin.admin') \
2876
+            .one()
2877
+        uapi = UserApi(
2878
+            current_user=admin,
2879
+            session=dbsession,
2880
+            config=self.app_config,
2881
+        )
2882
+        gapi = GroupApi(
2883
+            current_user=admin,
2884
+            session=dbsession,
2885
+            config=self.app_config,
2886
+        )
2887
+        groups = [gapi.get_one_with_name('users')]
2888
+        test_user = uapi.create_user(
2889
+            email='test@test.test',
2890
+            password='pass',
2891
+            name='bob',
2892
+            groups=groups,
2893
+            timezone='Europe/Paris',
2894
+            do_save=True,
2895
+            do_notify=False,
2896
+        )
2897
+        test_user2 = uapi.create_user(
2898
+            email='test2@test2.test2',
2899
+            password='pass',
2900
+            name='bob2',
2901
+            groups=groups,
2902
+            timezone='Europe/Paris',
2903
+            do_save=True,
2904
+            do_notify=False,
2905
+        )
2906
+        uapi.save(test_user)
2907
+        transaction.commit()
2908
+        user_id = int(admin.user_id)
2909
+
2910
+        self.testapp.authorization = (
2911
+            'Basic',
2912
+            (
2913
+                'admin@admin.admin',
2914
+                'admin@admin.admin'
2915
+            )
2916
+        )
2917
+        params = {
2918
+            'acp': 't',
2919
+        }
2920
+        res = self.testapp.get(
2921
+            '/api/v2/users/{user_id}/known_members'.format(user_id=user_id),
2922
+            status=400,
2923
+            params=params
2924
+        )
2925
+
2926
+    def test_api__get_user__ok_200__normal_user_by_email(self):
2927
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2928
+        admin = dbsession.query(models.User) \
2929
+            .filter(models.User.email == 'admin@admin.admin') \
2930
+            .one()
2931
+        uapi = UserApi(
2932
+            current_user=admin,
2933
+            session=dbsession,
2934
+            config=self.app_config,
2935
+        )
2936
+        gapi = GroupApi(
2937
+            current_user=admin,
2938
+            session=dbsession,
2939
+            config=self.app_config,
2940
+        )
2941
+        groups = [gapi.get_one_with_name('users')]
2942
+        test_user = uapi.create_user(
2943
+            email='test@test.test',
2944
+            password='pass',
2945
+            name='bob',
2946
+            groups=groups,
2947
+            timezone='Europe/Paris',
2948
+            do_save=True,
2949
+            do_notify=False,
2950
+        )
2951
+        test_user2 = uapi.create_user(
2952
+            email='test2@test2.test2',
2953
+            password='pass',
2954
+            name='bob2',
2955
+            groups=groups,
2956
+            timezone='Europe/Paris',
2957
+            do_save=True,
2958
+            do_notify=False,
2959
+        )
2960
+        test_user3 = uapi.create_user(
2961
+            email='test3@test3.test3',
2962
+            password='pass',
2963
+            name='bob3',
2964
+            groups=groups,
2965
+            timezone='Europe/Paris',
2966
+            do_save=True,
2967
+            do_notify=False,
2968
+        )
2969
+        uapi.save(test_user)
2970
+        uapi.save(test_user2)
2971
+        uapi.save(test_user3)
2972
+        workspace_api = WorkspaceApi(
2973
+            current_user=admin,
2974
+            session=dbsession,
2975
+            config=self.app_config
2976
+
2977
+        )
2978
+        workspace = WorkspaceApi(
2979
+            current_user=admin,
2980
+            session=dbsession,
2981
+            config=self.app_config,
2982
+        ).create_workspace(
2983
+            'test workspace',
2984
+            save_now=True
2985
+        )
2986
+        role_api = RoleApi(
2987
+            current_user=admin,
2988
+            session=dbsession,
2989
+            config=self.app_config,
2990
+        )
2991
+        role_api.create_one(test_user, workspace, UserRoleInWorkspace.READER, False)
2992
+        role_api.create_one(test_user2, workspace, UserRoleInWorkspace.READER, False)
2993
+        transaction.commit()
2994
+        user_id = int(test_user.user_id)
2995
+
2996
+        self.testapp.authorization = (
2997
+            'Basic',
2998
+            (
2999
+                'test@test.test',
3000
+                'pass'
3001
+            )
3002
+        )
3003
+        params = {
3004
+            'acp': 'test',
3005
+        }
3006
+        res = self.testapp.get(
3007
+            '/api/v2/users/{user_id}/known_members'.format(user_id=user_id),
3008
+            status=200,
3009
+            params=params
3010
+        )
3011
+        res = res.json_body
3012
+        assert len(res) == 2
3013
+        assert res[0]['user_id'] == test_user.user_id
3014
+        assert res[0]['public_name'] == test_user.display_name
3015
+        assert res[0]['avatar_url'] is None
3016
+
3017
+        assert res[1]['user_id'] == test_user2.user_id
3018
+        assert res[1]['public_name'] == test_user2.display_name
3019
+        assert res[1]['avatar_url'] is None
3020
+
3021
+
2649 3022
 class TestSetEmailEndpoint(FunctionalTest):
2650 3023
     # -*- coding: utf-8 -*-
2651 3024
     """
@@ -3025,6 +3398,12 @@ class TestSetPasswordEndpoint(FunctionalTest):
3025 3398
             status=204,
3026 3399
         )
3027 3400
         # Check After
3401
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
3402
+        uapi = UserApi(
3403
+            current_user=admin,
3404
+            session=dbsession,
3405
+            config=self.app_config,
3406
+        )
3028 3407
         user = uapi.get_one(user_id)
3029 3408
         assert not user.validate_password('pass')
3030 3409
         assert user.validate_password('mynewpassword')
@@ -3080,6 +3459,12 @@ class TestSetPasswordEndpoint(FunctionalTest):
3080 3459
             params=params,
3081 3460
             status=403,
3082 3461
         )
3462
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
3463
+        uapi = UserApi(
3464
+            current_user=admin,
3465
+            session=dbsession,
3466
+            config=self.app_config,
3467
+        )
3083 3468
         # Check After
3084 3469
         user = uapi.get_one(user_id)
3085 3470
         assert user.validate_password('pass')
@@ -3138,6 +3523,12 @@ class TestSetPasswordEndpoint(FunctionalTest):
3138 3523
             status=400,
3139 3524
         )
3140 3525
         # Check After
3526
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
3527
+        uapi = UserApi(
3528
+            current_user=admin,
3529
+            session=dbsession,
3530
+            config=self.app_config,
3531
+        )
3141 3532
         user = uapi.get_one(user_id)
3142 3533
         assert user.validate_password('pass')
3143 3534
         assert not user.validate_password('mynewpassword')
@@ -3195,6 +3586,12 @@ class TestSetPasswordEndpoint(FunctionalTest):
3195 3586
             status=204,
3196 3587
         )
3197 3588
         # Check After
3589
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
3590
+        uapi = UserApi(
3591
+            current_user=admin,
3592
+            session=dbsession,
3593
+            config=self.app_config,
3594
+        )
3198 3595
         user = uapi.get_one(user_id)
3199 3596
         assert not user.validate_password('pass')
3200 3597
         assert user.validate_password('mynewpassword')

+ 98 - 27
backend/tracim_backend/tests/functional/test_workspaces.py View File

@@ -2,7 +2,7 @@
2 2
 """
3 3
 Tests for /api/v2/workspaces subpath endpoints.
4 4
 """
5
-
5
+import requests
6 6
 import transaction
7 7
 from depot.io.utils import FileIntent
8 8
 
@@ -41,8 +41,10 @@ class TestWorkspaceEndpoint(FunctionalTest):
41 41
         assert workspace['slug'] == 'business'
42 42
         assert workspace['label'] == 'Business'
43 43
         assert workspace['description'] == 'All importants documents'
44
-        assert len(workspace['sidebar_entries']) == 7
44
+        assert len(workspace['sidebar_entries']) == 5
45 45
 
46
+        # TODO - G.M - 2018-08-02 - Better test for sidebar entry, make it
47
+        # not fixed on active application/content-file
46 48
         sidebar_entry = workspace['sidebar_entries'][0]
47 49
         assert sidebar_entry['slug'] == 'dashboard'
48 50
         assert sidebar_entry['label'] == 'Dashboard'
@@ -65,33 +67,19 @@ class TestWorkspaceEndpoint(FunctionalTest):
65 67
         assert sidebar_entry['fa_icon'] == "file-text-o"
66 68
 
67 69
         sidebar_entry = workspace['sidebar_entries'][3]
68
-        assert sidebar_entry['slug'] == 'contents/markdownpluspage'
69
-        assert sidebar_entry['label'] == 'Markdown Plus Documents'
70
-        assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=markdownpluspage"    # nopep8
71
-        assert sidebar_entry['hexcolor'] == "#f12d2d"
72
-        assert sidebar_entry['fa_icon'] == "file-code-o"
73
-
74
-        sidebar_entry = workspace['sidebar_entries'][4]
75 70
         assert sidebar_entry['slug'] == 'contents/file'
76 71
         assert sidebar_entry['label'] == 'Files'
77 72
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=file"  # nopep8
78 73
         assert sidebar_entry['hexcolor'] == "#FF9900"
79 74
         assert sidebar_entry['fa_icon'] == "paperclip"
80 75
 
81
-        sidebar_entry = workspace['sidebar_entries'][5]
76
+        sidebar_entry = workspace['sidebar_entries'][4]
82 77
         assert sidebar_entry['slug'] == 'contents/thread'
83 78
         assert sidebar_entry['label'] == 'Threads'
84 79
         assert sidebar_entry['route'] == "/#/workspaces/1/contents?type=thread"  # nopep8
85 80
         assert sidebar_entry['hexcolor'] == "#ad4cf9"
86 81
         assert sidebar_entry['fa_icon'] == "comments-o"
87 82
 
88
-        sidebar_entry = workspace['sidebar_entries'][6]
89
-        assert sidebar_entry['slug'] == 'calendar'
90
-        assert sidebar_entry['label'] == 'Calendar'
91
-        assert sidebar_entry['route'] == "/#/workspaces/1/calendar"  # nopep8
92
-        assert sidebar_entry['hexcolor'] == "#757575"
93
-        assert sidebar_entry['fa_icon'] == "calendar"
94
-
95 83
     def test_api__update_workspace__ok_200__nominal_case(self) -> None:
96 84
         """
97 85
         Test update workspace
@@ -118,7 +106,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
118 106
         assert workspace['slug'] == 'business'
119 107
         assert workspace['label'] == 'Business'
120 108
         assert workspace['description'] == 'All importants documents'
121
-        assert len(workspace['sidebar_entries']) == 7
109
+        assert len(workspace['sidebar_entries']) == 5
122 110
 
123 111
         # modify workspace
124 112
         res = self.testapp.put_json(
@@ -132,7 +120,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
132 120
         assert workspace['slug'] == 'superworkspace'
133 121
         assert workspace['label'] == 'superworkspace'
134 122
         assert workspace['description'] == 'mysuperdescription'
135
-        assert len(workspace['sidebar_entries']) == 7
123
+        assert len(workspace['sidebar_entries']) == 5
136 124
 
137 125
         # after
138 126
         res = self.testapp.get(
@@ -145,7 +133,7 @@ class TestWorkspaceEndpoint(FunctionalTest):
145 133
         assert workspace['slug'] == 'superworkspace'
146 134
         assert workspace['label'] == 'superworkspace'
147 135
         assert workspace['description'] == 'mysuperdescription'
148
-        assert len(workspace['sidebar_entries']) == 7
136
+        assert len(workspace['sidebar_entries']) == 5
149 137
 
150 138
     def test_api__update_workspace__err_400__empty_label(self) -> None:
151 139
         """
@@ -612,6 +600,89 @@ class TestWorkspaceMembersEndpoint(FunctionalTest):
612 600
         assert user_role['workspace_id'] == 1
613 601
 
614 602
 
603
+class TestUserInvitationWithMailActivatedSync(FunctionalTest):
604
+
605
+    fixtures = [BaseFixture, ContentFixtures]
606
+    config_section = 'functional_test_with_mail_test_sync'
607
+
608
+    def test_api__create_workspace_member_role__ok_200__new_user(self):  # nopep8
609
+        """
610
+        Create workspace member role
611
+        :return:
612
+        """
613
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')
614
+        self.testapp.authorization = (
615
+            'Basic',
616
+            (
617
+                'admin@admin.admin',
618
+                'admin@admin.admin'
619
+            )
620
+        )
621
+        # create workspace role
622
+        params = {
623
+            'user_id': None,
624
+            'user_email_or_public_name': 'bob@bob.bob',
625
+            'role': 'content-manager',
626
+        }
627
+        res = self.testapp.post_json(
628
+            '/api/v2/workspaces/1/members',
629
+            status=200,
630
+            params=params,
631
+        )
632
+        user_role_found = res.json_body
633
+        assert user_role_found['role'] == 'content-manager'
634
+        assert user_role_found['user_id']
635
+        user_id = user_role_found['user_id']
636
+        assert user_role_found['workspace_id'] == 1
637
+        assert user_role_found['newly_created'] is True
638
+        assert user_role_found['email_sent'] is True
639
+
640
+        # check mail received
641
+        response = requests.get('http://127.0.0.1:8025/api/v1/messages')
642
+        response = response.json()
643
+        assert len(response) == 1
644
+        headers = response[0]['Content']['Headers']
645
+        assert headers['From'][0] == 'Tracim Notifications <test_user_from+0@localhost>'  # nopep8
646
+        assert headers['To'][0] == 'bob <bob@bob.bob>'
647
+        assert headers['Subject'][0] == '[TRACIM] Created account'
648
+
649
+        # TODO - G.M - 2018-08-02 - Place cleanup outside of the test
650
+        requests.delete('http://127.0.0.1:8025/api/v1/messages')
651
+
652
+
653
+class TestUserInvitationWithMailActivatedASync(FunctionalTest):
654
+
655
+    fixtures = [BaseFixture, ContentFixtures]
656
+    config_section = 'functional_test_with_mail_test_async'
657
+
658
+    def test_api__create_workspace_member_role__ok_200__new_user(self):  # nopep8
659
+        """
660
+        Create workspace member role
661
+        :return:
662
+        """
663
+        self.testapp.authorization = (
664
+            'Basic',
665
+            (
666
+                'admin@admin.admin',
667
+                'admin@admin.admin'
668
+            )
669
+        )
670
+        # create workspace role
671
+        params = {
672
+            'user_id': None,
673
+            'user_email_or_public_name': 'bob@bob.bob',
674
+            'role': 'content-manager',
675
+        }
676
+        res = self.testapp.post_json(
677
+            '/api/v2/workspaces/1/members',
678
+            status=200,
679
+            params=params,
680
+        )
681
+        user_role_found = res.json_body
682
+        assert user_role_found['newly_created'] is True
683
+        assert user_role_found['email_sent'] is False
684
+
685
+
615 686
 class TestWorkspaceContents(FunctionalTest):
616 687
     """
617 688
     Tests for /api/v2/workspaces/{workspace_id}/contents endpoint
@@ -1442,7 +1513,7 @@ class TestWorkspaceContents(FunctionalTest):
1442 1513
         params = {
1443 1514
             'parent_id': None,
1444 1515
             'label': 'GenericCreatedContent',
1445
-            'content_type': 'markdownpage',
1516
+            'content_type': 'html-document',
1446 1517
         }
1447 1518
         res = self.testapp.post_json(
1448 1519
             '/api/v2/workspaces/1/contents',
@@ -1453,7 +1524,7 @@ class TestWorkspaceContents(FunctionalTest):
1453 1524
         assert res.json_body
1454 1525
         assert res.json_body['status'] == 'open'
1455 1526
         assert res.json_body['content_id']
1456
-        assert res.json_body['content_type'] == 'markdownpage'
1527
+        assert res.json_body['content_type'] == 'html-document'
1457 1528
         assert res.json_body['is_archived'] is False
1458 1529
         assert res.json_body['is_deleted'] is False
1459 1530
         assert res.json_body['workspace_id'] == 1
@@ -1484,7 +1555,7 @@ class TestWorkspaceContents(FunctionalTest):
1484 1555
         )
1485 1556
         params = {
1486 1557
             'label': 'GenericCreatedContent',
1487
-            'content_type': 'markdownpage',
1558
+            'content_type': 'html-document',
1488 1559
         }
1489 1560
         res = self.testapp.post_json(
1490 1561
             '/api/v2/workspaces/1/contents',
@@ -1495,7 +1566,7 @@ class TestWorkspaceContents(FunctionalTest):
1495 1566
         assert res.json_body
1496 1567
         assert res.json_body['status'] == 'open'
1497 1568
         assert res.json_body['content_id']
1498
-        assert res.json_body['content_type'] == 'markdownpage'
1569
+        assert res.json_body['content_type'] == 'html-document'
1499 1570
         assert res.json_body['is_archived'] is False
1500 1571
         assert res.json_body['is_deleted'] is False
1501 1572
         assert res.json_body['workspace_id'] == 1
@@ -1548,7 +1619,7 @@ class TestWorkspaceContents(FunctionalTest):
1548 1619
         )
1549 1620
         params = {
1550 1621
             'label': 'GenericCreatedContent',
1551
-            'content_type': 'markdownpage',
1622
+            'content_type': 'html-document',
1552 1623
             'parent_id': 10,
1553 1624
         }
1554 1625
         res = self.testapp.post_json(
@@ -1560,7 +1631,7 @@ class TestWorkspaceContents(FunctionalTest):
1560 1631
         assert res.json_body
1561 1632
         assert res.json_body['status'] == 'open'
1562 1633
         assert res.json_body['content_id']
1563
-        assert res.json_body['content_type'] == 'markdownpage'
1634
+        assert res.json_body['content_type'] == 'html-document'
1564 1635
         assert res.json_body['is_archived'] is False
1565 1636
         assert res.json_body['is_deleted'] is False
1566 1637
         assert res.json_body['workspace_id'] == 1
@@ -1591,7 +1662,7 @@ class TestWorkspaceContents(FunctionalTest):
1591 1662
         )
1592 1663
         params = {
1593 1664
             'label': '',
1594
-            'content_type': 'markdownpage',
1665
+            'content_type': 'html-document',
1595 1666
         }
1596 1667
         res = self.testapp.post_json(
1597 1668
             '/api/v2/workspaces/1/contents',

+ 183 - 0
backend/tracim_backend/tests/library/test_user_api.py View File

@@ -1,14 +1,19 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import pytest
3 3
 import transaction
4
+from tracim_backend import models
4 5
 
5 6
 from tracim_backend.exceptions import AuthenticationFailed
7
+from tracim_backend.exceptions import TooShortAutocompleteString
6 8
 from tracim_backend.exceptions import UserDoesNotExist
7 9
 from tracim_backend.exceptions import UserNotActive
8 10
 from tracim_backend.lib.core.group import GroupApi
9 11
 from tracim_backend.lib.core.user import UserApi
12
+from tracim_backend.lib.core.userworkspace import RoleApi
13
+from tracim_backend.lib.core.workspace import WorkspaceApi
10 14
 from tracim_backend.models import User
11 15
 from tracim_backend.models.context_models import UserInContext
16
+from tracim_backend.models.data import UserRoleInWorkspace
12 17
 from tracim_backend.tests import DefaultTest
13 18
 from tracim_backend.tests import eq_
14 19
 
@@ -107,6 +112,184 @@ class TestUserApi(DefaultTest):
107 112
         # u1 + Admin user from BaseFixture
108 113
         assert 2 == len(users)
109 114
 
115
+    def test_unit__get_known__user__admin__too_short_acp_str(self):
116
+        api = UserApi(
117
+            current_user=None,
118
+            session=self.session,
119
+            config=self.config,
120
+        )
121
+        u1 = api.create_user(
122
+            email='email@email',
123
+            name='name',
124
+            do_notify=False,
125
+            do_save=True,
126
+        )
127
+        with pytest.raises(TooShortAutocompleteString):
128
+            api.get_known_user('e')
129
+
130
+    def test_unit__get_known__user__admin__by_email(self):
131
+        api = UserApi(
132
+            current_user=None,
133
+            session=self.session,
134
+            config=self.config,
135
+        )
136
+        u1 = api.create_user(
137
+            email='email@email',
138
+            name='name',
139
+            do_notify=False,
140
+            do_save=True,
141
+        )
142
+
143
+        users = api.get_known_user('email')
144
+        assert len(users) == 1
145
+        assert users[0] == u1
146
+
147
+    def test_unit__get_known__user__user__no_workspace_empty_known_user(self):
148
+        admin = self.session.query(models.User) \
149
+            .filter(models.User.email == 'admin@admin.admin') \
150
+            .one()
151
+        api = UserApi(
152
+            current_user=admin,
153
+            session=self.session,
154
+            config=self.config,
155
+        )
156
+        u1 = api.create_user(
157
+            email='email@email',
158
+            name='name',
159
+            do_notify=False,
160
+            do_save=True,
161
+        )
162
+        api2 = UserApi(
163
+            current_user=u1,
164
+            session=self.session,
165
+            config=self.config,
166
+        )
167
+        users = api2.get_known_user('email')
168
+        assert len(users) == 0
169
+
170
+    def test_unit__get_known__user__same_workspaces_users_by_name(self):
171
+        admin = self.session.query(models.User) \
172
+            .filter(models.User.email == 'admin@admin.admin') \
173
+            .one()
174
+        api = UserApi(
175
+            current_user=None,
176
+            session=self.session,
177
+            config=self.config,
178
+        )
179
+        u1 = api.create_user(
180
+            email='email@email',
181
+            name='name',
182
+            do_notify=False,
183
+            do_save=True,
184
+        )
185
+        u2 = api.create_user(
186
+            email='email2@email2',
187
+            name='name2',
188
+            do_notify=False,
189
+            do_save=True,
190
+        )
191
+        u3 = api.create_user(
192
+            email='notfound@notfound',
193
+            name='notfound',
194
+            do_notify=False,
195
+            do_save=True,
196
+        )
197
+        wapi = WorkspaceApi(
198
+            current_user=admin,
199
+            session=self.session,
200
+            config=self.app_config,
201
+        )
202
+        workspace = wapi.create_workspace(
203
+            'test workspace n°1',
204
+            save_now=True)
205
+        role_api = RoleApi(
206
+            current_user=admin,
207
+            session=self.session,
208
+            config=self.app_config,
209
+        )
210
+        role_api.create_one(u1, workspace, UserRoleInWorkspace.READER, False)
211
+        role_api.create_one(u2, workspace, UserRoleInWorkspace.READER, False)
212
+        role_api.create_one(u3, workspace, UserRoleInWorkspace.READER, False)
213
+        api2 = UserApi(
214
+            current_user=u1,
215
+            session=self.session,
216
+            config=self.config,
217
+        )
218
+        users = api2.get_known_user('name')
219
+        assert len(users) == 2
220
+        assert users[0] == u1
221
+        assert users[1] == u2
222
+
223
+    def test_unit__get_known__user__same_workspaces_users_by_email(self):
224
+        admin = self.session.query(models.User) \
225
+            .filter(models.User.email == 'admin@admin.admin') \
226
+            .one()
227
+        api = UserApi(
228
+            current_user=None,
229
+            session=self.session,
230
+            config=self.config,
231
+        )
232
+        u1 = api.create_user(
233
+            email='email@email',
234
+            name='name',
235
+            do_notify=False,
236
+            do_save=True,
237
+        )
238
+        u2 = api.create_user(
239
+            email='email2@email2',
240
+            name='name2',
241
+            do_notify=False,
242
+            do_save=True,
243
+        )
244
+        u3 = api.create_user(
245
+            email='notfound@notfound',
246
+            name='notfound',
247
+            do_notify=False,
248
+            do_save=True,
249
+        )
250
+        wapi = WorkspaceApi(
251
+            current_user=admin,
252
+            session=self.session,
253
+            config=self.app_config,
254
+        )
255
+        workspace = wapi.create_workspace(
256
+            'test workspace n°1',
257
+            save_now=True)
258
+        role_api = RoleApi(
259
+            current_user=admin,
260
+            session=self.session,
261
+            config=self.app_config,
262
+        )
263
+        role_api.create_one(u1, workspace, UserRoleInWorkspace.READER, False)
264
+        role_api.create_one(u2, workspace, UserRoleInWorkspace.READER, False)
265
+        role_api.create_one(u3, workspace, UserRoleInWorkspace.READER, False)
266
+        api2 = UserApi(
267
+            current_user=u1,
268
+            session=self.session,
269
+            config=self.config,
270
+        )
271
+        users = api2.get_known_user('email')
272
+        assert len(users) == 2
273
+        assert users[0] == u1
274
+        assert users[1] == u2
275
+
276
+    def test_unit__get_known__user__admin__by_name(self):
277
+        api = UserApi(
278
+            current_user=None,
279
+            session=self.session,
280
+            config=self.config,
281
+        )
282
+        u1 = api.create_user(
283
+            email='email@email',
284
+            name='name',
285
+            do_notify=False,
286
+            do_save=True,
287
+        )
288
+
289
+        users = api.get_known_user('nam')
290
+        assert len(users) == 1
291
+        assert users[0] == u1
292
+
110 293
     def test_unit__get_one__ok__nominal_case(self):
111 294
         api = UserApi(
112 295
             current_user=None,

+ 14 - 0
backend/tracim_backend/tests/library/tests_utils.py View File

@@ -0,0 +1,14 @@
1
+import string
2
+
3
+from tracim_backend.lib.utils.utils import password_generator
4
+from tracim_backend.lib.utils.utils import ALLOWED_AUTOGEN_PASSWORD_CHAR
5
+from tracim_backend.lib.utils.utils import DEFAULT_PASSWORD_GEN_CHAR_LENGTH
6
+
7
+
8
+class TestPasswordGenerator(object):
9
+
10
+    def test_password_generator_ok_nominal_case(self):
11
+        password = password_generator()
12
+        assert len(password) == DEFAULT_PASSWORD_GEN_CHAR_LENGTH
13
+        for char in password:
14
+            assert char in ALLOWED_AUTOGEN_PASSWORD_CHAR

+ 6 - 0
backend/tracim_backend/views/contents_api/comment_controller.py View File

@@ -41,6 +41,8 @@ class CommentController(Controller):
41 41
         # login = hapic_data.body
42 42
         app_config = request.registry.settings['CFG']
43 43
         api = ContentApi(
44
+            show_archived=True,
45
+            show_deleted=True,
44 46
             current_user=request.current_user,
45 47
             session=request.dbsession,
46 48
             config=app_config,
@@ -68,6 +70,8 @@ class CommentController(Controller):
68 70
         # login = hapic_data.body
69 71
         app_config = request.registry.settings['CFG']
70 72
         api = ContentApi(
73
+            show_archived=True,
74
+            show_deleted=True,
71 75
             current_user=request.current_user,
72 76
             session=request.dbsession,
73 77
             config=app_config,
@@ -97,6 +101,8 @@ class CommentController(Controller):
97 101
         """
98 102
         app_config = request.registry.settings['CFG']
99 103
         api = ContentApi(
104
+            show_archived=True,
105
+            show_deleted=True,
100 106
             current_user=request.current_user,
101 107
             session=request.dbsession,
102 108
             config=app_config,

+ 28 - 0
backend/tracim_backend/views/contents_api/file_controller.py View File

@@ -61,6 +61,8 @@ class FileController(Controller):
61 61
         """
62 62
         app_config = request.registry.settings['CFG']
63 63
         api = ContentApi(
64
+            show_archived=True,
65
+            show_deleted=True,
64 66
             current_user=request.current_user,
65 67
             session=request.dbsession,
66 68
             config=app_config,
@@ -95,6 +97,8 @@ class FileController(Controller):
95 97
         """
96 98
         app_config = request.registry.settings['CFG']
97 99
         api = ContentApi(
100
+            show_archived=True,
101
+            show_deleted=True,
98 102
             current_user=request.current_user,
99 103
             session=request.dbsession,
100 104
             config=app_config,
@@ -120,6 +124,8 @@ class FileController(Controller):
120 124
         """
121 125
         app_config = request.registry.settings['CFG']
122 126
         api = ContentApi(
127
+            show_archived=True,
128
+            show_deleted=True,
123 129
             current_user=request.current_user,
124 130
             session=request.dbsession,
125 131
             config=app_config,
@@ -154,6 +160,8 @@ class FileController(Controller):
154 160
         """
155 161
         app_config = request.registry.settings['CFG']
156 162
         api = ContentApi(
163
+            show_archived=True,
164
+            show_deleted=True,
157 165
             current_user=request.current_user,
158 166
             session=request.dbsession,
159 167
             config=app_config,
@@ -181,6 +189,8 @@ class FileController(Controller):
181 189
         """
182 190
         app_config = request.registry.settings['CFG']
183 191
         api = ContentApi(
192
+            show_archived=True,
193
+            show_deleted=True,
184 194
             current_user=request.current_user,
185 195
             session=request.dbsession,
186 196
             config=app_config,
@@ -205,6 +215,8 @@ class FileController(Controller):
205 215
         """
206 216
         app_config = request.registry.settings['CFG']
207 217
         api = ContentApi(
218
+            show_archived=True,
219
+            show_deleted=True,
208 220
             current_user=request.current_user,
209 221
             session=request.dbsession,
210 222
             config=app_config,
@@ -238,6 +250,8 @@ class FileController(Controller):
238 250
         """
239 251
         app_config = request.registry.settings['CFG']
240 252
         api = ContentApi(
253
+            show_archived=True,
254
+            show_deleted=True,
241 255
             current_user=request.current_user,
242 256
             session=request.dbsession,
243 257
             config=app_config,
@@ -270,6 +284,8 @@ class FileController(Controller):
270 284
         """
271 285
         app_config = request.registry.settings['CFG']
272 286
         api = ContentApi(
287
+            show_archived=True,
288
+            show_deleted=True,
273 289
             current_user=request.current_user,
274 290
             session=request.dbsession,
275 291
             config=app_config,
@@ -301,6 +317,8 @@ class FileController(Controller):
301 317
         """
302 318
         app_config = request.registry.settings['CFG']
303 319
         api = ContentApi(
320
+            show_archived=True,
321
+            show_deleted=True,
304 322
             current_user=request.current_user,
305 323
             session=request.dbsession,
306 324
             config=app_config,
@@ -334,6 +352,8 @@ class FileController(Controller):
334 352
         """
335 353
         app_config = request.registry.settings['CFG']
336 354
         api = ContentApi(
355
+            show_archived=True,
356
+            show_deleted=True,
337 357
             current_user=request.current_user,
338 358
             session=request.dbsession,
339 359
             config=app_config,
@@ -352,6 +372,8 @@ class FileController(Controller):
352 372
         """
353 373
         app_config = request.registry.settings['CFG']
354 374
         api = ContentApi(
375
+            show_archived=True,
376
+            show_deleted=True,
355 377
             current_user=request.current_user,
356 378
             session=request.dbsession,
357 379
             config=app_config,
@@ -375,6 +397,8 @@ class FileController(Controller):
375 397
         """
376 398
         app_config = request.registry.settings['CFG']
377 399
         api = ContentApi(
400
+            show_archived=True,
401
+            show_deleted=True,
378 402
             current_user=request.current_user,
379 403
             session=request.dbsession,
380 404
             config=app_config,
@@ -413,6 +437,8 @@ class FileController(Controller):
413 437
         """
414 438
         app_config = request.registry.settings['CFG']
415 439
         api = ContentApi(
440
+            show_archived=True,
441
+            show_deleted=True,
416 442
             current_user=request.current_user,
417 443
             session=request.dbsession,
418 444
             config=app_config,
@@ -440,6 +466,8 @@ class FileController(Controller):
440 466
         """
441 467
         app_config = request.registry.settings['CFG']
442 468
         api = ContentApi(
469
+            show_archived=True,
470
+            show_deleted=True,
443 471
             current_user=request.current_user,
444 472
             session=request.dbsession,
445 473
             config=app_config,

+ 8 - 0
backend/tracim_backend/views/contents_api/html_document_controller.py View File

@@ -46,6 +46,8 @@ class HTMLDocumentController(Controller):
46 46
         """
47 47
         app_config = request.registry.settings['CFG']
48 48
         api = ContentApi(
49
+            show_archived=True,
50
+            show_deleted=True,
49 51
             current_user=request.current_user,
50 52
             session=request.dbsession,
51 53
             config=app_config,
@@ -69,6 +71,8 @@ class HTMLDocumentController(Controller):
69 71
         """
70 72
         app_config = request.registry.settings['CFG']
71 73
         api = ContentApi(
74
+            show_archived=True,
75
+            show_deleted=True,
72 76
             current_user=request.current_user,
73 77
             session=request.dbsession,
74 78
             config=app_config,
@@ -107,6 +111,8 @@ class HTMLDocumentController(Controller):
107 111
         """
108 112
         app_config = request.registry.settings['CFG']
109 113
         api = ContentApi(
114
+            show_archived=True,
115
+            show_deleted=True,
110 116
             current_user=request.current_user,
111 117
             session=request.dbsession,
112 118
             config=app_config,
@@ -138,6 +144,8 @@ class HTMLDocumentController(Controller):
138 144
         """
139 145
         app_config = request.registry.settings['CFG']
140 146
         api = ContentApi(
147
+            show_archived=True,
148
+            show_deleted=True,
141 149
             current_user=request.current_user,
142 150
             session=request.dbsession,
143 151
             config=app_config,

+ 8 - 0
backend/tracim_backend/views/contents_api/threads_controller.py View File

@@ -45,6 +45,8 @@ class ThreadController(Controller):
45 45
         """
46 46
         app_config = request.registry.settings['CFG']
47 47
         api = ContentApi(
48
+            show_archived=True,
49
+            show_deleted=True,
48 50
             current_user=request.current_user,
49 51
             session=request.dbsession,
50 52
             config=app_config,
@@ -68,6 +70,8 @@ class ThreadController(Controller):
68 70
         """
69 71
         app_config = request.registry.settings['CFG']
70 72
         api = ContentApi(
73
+            show_archived=True,
74
+            show_deleted=True,
71 75
             current_user=request.current_user,
72 76
             session=request.dbsession,
73 77
             config=app_config,
@@ -106,6 +110,8 @@ class ThreadController(Controller):
106 110
         """
107 111
         app_config = request.registry.settings['CFG']
108 112
         api = ContentApi(
113
+            show_archived=True,
114
+            show_deleted=True,
109 115
             current_user=request.current_user,
110 116
             session=request.dbsession,
111 117
             config=app_config,
@@ -132,6 +138,8 @@ class ThreadController(Controller):
132 138
         """
133 139
         app_config = request.registry.settings['CFG']
134 140
         api = ContentApi(
141
+            show_archived=True,
142
+            show_deleted=True,
135 143
             current_user=request.current_user,
136 144
             session=request.dbsession,
137 145
             config=app_config,

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

@@ -2,6 +2,7 @@
2 2
 import marshmallow
3 3
 from marshmallow import post_load
4 4
 from marshmallow.validate import OneOf
5
+from marshmallow.validate import Length
5 6
 from marshmallow.validate import Range
6 7
 
7 8
 from tracim_backend.lib.utils.utils import DATETIME_FORMAT
@@ -12,6 +13,7 @@ from tracim_backend.models.contents import CONTENT_TYPES
12 13
 from tracim_backend.models.contents import open_status
13 14
 from tracim_backend.models.context_models import ActiveContentFilter
14 15
 from tracim_backend.models.context_models import FolderContentUpdate
16
+from tracim_backend.models.context_models import AutocompleteQuery
15 17
 from tracim_backend.models.context_models import ContentIdsQuery
16 18
 from tracim_backend.models.context_models import UserWorkspaceAndContentPath
17 19
 from tracim_backend.models.context_models import ContentCreation
@@ -293,6 +295,17 @@ class CommentsPathSchema(WorkspaceAndContentIdPathSchema):
293 295
         return CommentPath(**data)
294 296
 
295 297
 
298
+class AutocompleteQuerySchema(marshmallow.Schema):
299
+    acp = marshmallow.fields.Str(
300
+        example='test',
301
+        description='search text to query',
302
+        validate=Length(min=2),
303
+    )
304
+    @post_load
305
+    def make_autocomplete(self, data):
306
+        return AutocompleteQuery(**data)
307
+
308
+
296 309
 class PageQuerySchema(marshmallow.Schema):
297 310
     page = marshmallow.fields.Int(
298 311
         example=2,

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

@@ -18,6 +18,8 @@ from tracim_backend.lib.utils.authorization import require_profile
18 18
 from tracim_backend.exceptions import WrongUserPassword
19 19
 from tracim_backend.exceptions import PasswordDoNotMatch
20 20
 from tracim_backend.views.core_api.schemas import UserSchema
21
+from tracim_backend.views.core_api.schemas import AutocompleteQuerySchema
22
+from tracim_backend.views.core_api.schemas import UserDigestSchema
21 23
 from tracim_backend.views.core_api.schemas import SetEmailSchema
22 24
 from tracim_backend.views.core_api.schemas import SetPasswordSchema
23 25
 from tracim_backend.views.core_api.schemas import UserInfosSchema
@@ -37,6 +39,7 @@ from tracim_backend.models.contents import CONTENT_TYPES
37 39
 SWAGGER_TAG__USER_ENDPOINTS = 'Users'
38 40
 
39 41
 
42
+
40 43
 class UserController(Controller):
41 44
 
42 45
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
@@ -77,6 +80,46 @@ class UserController(Controller):
77 80
         return uapi.get_user_with_context(request.candidate_user)
78 81
 
79 82
     @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
83
+    @require_profile(Group.TIM_ADMIN)
84
+    @hapic.output_body(UserDigestSchema(many=True))
85
+    def users(self, context, request: TracimRequest, hapic_data=None):
86
+        """
87
+        Get all users
88
+        """
89
+        app_config = request.registry.settings['CFG']
90
+        uapi = UserApi(
91
+            current_user=request.current_user,  # User
92
+            session=request.dbsession,
93
+            config=app_config,
94
+        )
95
+        users = uapi.get_all()
96
+        context_users = [
97
+            uapi.get_user_with_context(user) for user in users
98
+        ]
99
+        return context_users
100
+
101
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
102
+    @require_same_user_or_profile(Group.TIM_MANAGER)
103
+    @hapic.input_path(UserIdPathSchema())
104
+    @hapic.input_query(AutocompleteQuerySchema())
105
+    @hapic.output_body(UserDigestSchema(many=True))
106
+    def known_members(self, context, request: TracimRequest, hapic_data=None):
107
+        """
108
+        Get known users list
109
+        """
110
+        app_config = request.registry.settings['CFG']
111
+        uapi = UserApi(
112
+            current_user=request.candidate_user,  # User
113
+            session=request.dbsession,
114
+            config=app_config,
115
+        )
116
+        users = uapi.get_known_user(acp=hapic_data.query.acp)
117
+        context_users = [
118
+            uapi.get_user_with_context(user) for user in users
119
+        ]
120
+        return context_users
121
+
122
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
80 123
     @hapic.handle_exception(WrongUserPassword, HTTPStatus.FORBIDDEN)
81 124
     @require_same_user_or_profile(Group.TIM_ADMIN)
82 125
     @hapic.input_body(SetEmailSchema())
@@ -329,6 +372,8 @@ class UserController(Controller):
329 372
         """
330 373
         app_config = request.registry.settings['CFG']
331 374
         api = ContentApi(
375
+            show_archived=True,
376
+            show_deleted=True,
332 377
             current_user=request.candidate_user,
333 378
             session=request.dbsession,
334 379
             config=app_config,
@@ -346,6 +391,8 @@ class UserController(Controller):
346 391
         """
347 392
         app_config = request.registry.settings['CFG']
348 393
         api = ContentApi(
394
+            show_archived=True,
395
+            show_deleted=True,
349 396
             current_user=request.candidate_user,
350 397
             session=request.dbsession,
351 398
             config=app_config,
@@ -363,6 +410,8 @@ class UserController(Controller):
363 410
         """
364 411
         app_config = request.registry.settings['CFG']
365 412
         api = ContentApi(
413
+            show_archived=True,
414
+            show_deleted=True,
366 415
             current_user=request.candidate_user,
367 416
             session=request.dbsession,
368 417
             config=app_config,
@@ -384,6 +433,14 @@ class UserController(Controller):
384 433
         configurator.add_route('user', '/users/{user_id}', request_method='GET')  # nopep8
385 434
         configurator.add_view(self.user, route_name='user')
386 435
 
436
+        # users lists
437
+        configurator.add_route('users', '/users', request_method='GET')  # nopep8
438
+        configurator.add_view(self.users, route_name='users')
439
+
440
+        # known members lists
441
+        configurator.add_route('known_members', '/users/{user_id}/known_members', request_method='GET')  # nopep8
442
+        configurator.add_view(self.known_members, route_name='known_members')
443
+
387 444
         # set user email
388 445
         configurator.add_route('set_user_email', '/users/{user_id}/email', request_method='PUT')  # nopep8
389 446
         configurator.add_view(self.set_user_email, route_name='set_user_email')

+ 17 - 2
backend/tracim_backend/views/core_api/workspace_controller.py View File

@@ -31,6 +31,7 @@ from tracim_backend.exceptions import ContentNotFound
31 31
 from tracim_backend.exceptions import WorkspacesDoNotMatch
32 32
 from tracim_backend.exceptions import ParentNotFound
33 33
 from tracim_backend.views.controllers import Controller
34
+from tracim_backend.lib.utils.utils import password_generator
34 35
 from tracim_backend.views.core_api.schemas import FilterContentQuerySchema
35 36
 from tracim_backend.views.core_api.schemas import WorkspaceMemberCreationSchema
36 37
 from tracim_backend.views.core_api.schemas import WorkspaceMemberInviteSchema
@@ -213,12 +214,18 @@ class WorkspaceController(Controller):
213 214
                 # TODO - G.M - 2018-07-05 - [UserCreation] Reenable email
214 215
                 # notification for creation
215 216
                 user = uapi.create_user(
216
-                    hapic_data.body.user_email_or_public_name,
217
-                    do_notify=False
217
+                    email=hapic_data.body.user_email_or_public_name,
218
+                    password= password_generator(),
219
+                    do_notify=True
218 220
                 )  # nopep8
219 221
                 newly_created = True
222
+                if app_config.EMAIL_NOTIFICATION_ACTIVATED and \
223
+                        app_config.EMAIL_NOTIFICATION_PROCESSING_MODE.lower() == 'sync':
224
+                    email_sent = True
225
+
220 226
             except EmailValidationFailed:
221 227
                 raise UserCreationFailed('no valid mail given')
228
+
222 229
         role = rapi.create_one(
223 230
             user=user,
224 231
             workspace=request.current_workspace,
@@ -328,6 +335,8 @@ class WorkspaceController(Controller):
328 335
         move_data = hapic_data.body
329 336
 
330 337
         api = ContentApi(
338
+            show_archived=True,
339
+            show_deleted=True,
331 340
             current_user=request.current_user,
332 341
             session=request.dbsession,
333 342
             config=app_config,
@@ -375,6 +384,8 @@ class WorkspaceController(Controller):
375 384
         app_config = request.registry.settings['CFG']
376 385
         path_data = hapic_data.path
377 386
         api = ContentApi(
387
+            show_archived=True,
388
+            show_deleted=True,
378 389
             current_user=request.current_user,
379 390
             session=request.dbsession,
380 391
             config=app_config,
@@ -411,6 +422,7 @@ class WorkspaceController(Controller):
411 422
             session=request.dbsession,
412 423
             config=app_config,
413 424
             show_deleted=True,
425
+            show_archived=True,
414 426
         )
415 427
         content = api.get_one(
416 428
             path_data.content_id,
@@ -440,6 +452,8 @@ class WorkspaceController(Controller):
440 452
         app_config = request.registry.settings['CFG']
441 453
         path_data = hapic_data.path
442 454
         api = ContentApi(
455
+            show_archived=True,
456
+            show_deleted=True,
443 457
             current_user=request.current_user,
444 458
             session=request.dbsession,
445 459
             config=app_config,
@@ -473,6 +487,7 @@ class WorkspaceController(Controller):
473 487
             session=request.dbsession,
474 488
             config=app_config,
475 489
             show_archived=True,
490
+            show_deleted=True,
476 491
         )
477 492
         content = api.get_one(
478 493
             path_data.content_id,

+ 1 - 1
bash_library.sh View File

@@ -5,5 +5,5 @@ BROWN='\033[0;33m'
5 5
 NC='\033[0m' # No Color
6 6
 
7 7
 function log {
8
-    echo -e "\n${YELLOW}[$(date +'%H:%M:%S')]${BROWN} $ $1${NC}\n"
8
+    echo -e "\n${YELLOW}[$(date +'%H:%M:%S')]${BROWN} $ $1${NC}"
9 9
 }

+ 51 - 54
build_full_frontend.sh View File

@@ -1,5 +1,6 @@
1 1
 #!/bin/bash
2 2
 
3
+# shellcheck disable=SC1091
3 4
 . bash_library.sh # source bash_library.sh
4 5
 
5 6
 windoz=""
@@ -10,81 +11,77 @@ fi
10 11
 echo -e "\n${BROWN}/!\ ${NC}this script does not run 'npm install'\n${BROWN}/!\ ${NC}it also assumes your webpack dev server of frontend is running"
11 12
 
12 13
 # Tracim Lib
14
+log "build frontend_lib"
15
+(
16
+  cd frontend_lib || exit
17
+  npm run buildtracimlib$windoz
18
+)
13 19
 
14
-log "cd frontend_lib"
15
-cd frontend_lib
16
-log "npm run buildtracimlib$windoz"
17
-npm run buildtracimlib$windoz
18
-cd -
19 20
 
20 21
 # app Html Document
22
+log "build frontend_app_html-document"
23
+(
24
+  cd frontend_app_html-document || exit
25
+  npm run build$windoz
26
+)
21 27
 
22
-log "cd frontend_app_html-document"
23
-cd frontend_app_html-document
28
+log "copying built file to frontend/"
29
+cp frontend_app_html-document/dist/html-document.app.js frontend/dist/app/
24 30
 
25
-log "npm run build$windoz # for frontend_app_html-document"
26
-npm run build$windoz
31
+log "copying en translation.json"
32
+cp frontend_app_html-document/i18next.scanner/en/translation.json frontend/dist/app/html-document_en_translation.json
27 33
 
28
-log "cp dist/html-document.app.js"
29
-cp dist/html-document.app.js ../frontend/dist/app
34
+log "copying fr translation.json"
35
+cp frontend_app_html-document/i18next.scanner/fr/translation.json frontend/dist/app/html-document_fr_translation.json
30 36
 
31
-log "cp i18next.scanner/en/translation.json ../frontend/dist/app/tml-document_en_translation.json"
32
-cp i18next.scanner/en/translation.json ../frontend/dist/app/html-document_en_translation.json
33
-
34
-log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/html-document_fr_translation.json"
35
-cp i18next.scanner/fr/translation.json ../frontend/dist/app/html-document_fr_translation.json
36
-cd -
37 37
 
38 38
 # app Thread
39
+log "build frontend_app_thread"
40
+(
41
+  cd frontend_app_thread || exit
42
+  npm run build$windoz
43
+)
39 44
 
40
-log "cd frontend_app_thread"
41
-cd frontend_app_thread
42
-
43
-log "npm run build$windoz # for frontend_app_thread"
44
-npm run build$windoz
45
+log "copying built file to frontend/"
46
+cp frontend_app_thread/dist/thread.app.js frontend/dist/app/
45 47
 
46
-log "cp dist/thread.app.js"
47
-cp dist/thread.app.js ../frontend/dist/app
48
+log "copying Thread en translation.json"
49
+cp frontend_app_thread/i18next.scanner/en/translation.json frontend/dist/app/thread_en_translation.json
48 50
 
49
-log "cp i18next.scanner/en/translation.json ../frontend/dist/app/thread_en_translation.json"
50
-cp i18next.scanner/en/translation.json ../frontend/dist/app/thread_en_translation.json
51
+log "copying Thread fr translation.json"
52
+cp frontend_app_thread/i18next.scanner/fr/translation.json frontend/dist/app/thread_fr_translation.json
51 53
 
52
-log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/thread_fr_translation.json"
53
-cp i18next.scanner/fr/translation.json ../frontend/dist/app/thread_fr_translation.json
54
-cd -
55 54
 
56 55
 # app Workspace
56
+log "build frontend_app_workspace"
57
+(
58
+  cd frontend_app_workspace || exit
59
+  npm run build$windoz
60
+)
57 61
 
58
-log "cd frontend_app_workspace"
59
-cd frontend_app_workspace
60
-
61
-log "npm run build$windoz # for frontend_app_workspace"
62
-npm run build$windoz
62
+log "copying built file to frontend/"
63
+cp frontend_app_workspace/dist/workspace.app.js frontend/dist/app/
63 64
 
64
-log "cp dist/workspace.app.js"
65
-cp dist/workspace.app.js ../frontend/dist/app
65
+log "copying Thread en translation.json"
66
+cp frontend_app_workspace/i18next.scanner/en/translation.json frontend/dist/app/workspace_en_translation.json
66 67
 
67
-log "cp i18next.scanner/en/translation.json ../frontend/dist/app/workspace_en_translation.json"
68
-cp i18next.scanner/en/translation.json ../frontend/dist/app/workspace_en_translation.json
69
-
70
-log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/workspace_fr_translation.json"
71
-cp i18next.scanner/fr/translation.json ../frontend/dist/app/workspace_fr_translation.json
72
-cd -
68
+log "copying Thread fr translation.json"
69
+cp frontend_app_workspace/i18next.scanner/fr/translation.json frontend/dist/app/workspace_fr_translation.json
73 70
 
74 71
 # app Admin Workspace User
72
+log "build frontend_app_admin_workspace_user"
73
+(
74
+  cd frontend_app_admin_workspace_user || exit
75
+  npm run build$windoz
76
+)
75 77
 
76
-log "cd frontend_app_admin_workspace_user"
77
-cd frontend_app_admin_workspace_user
78
-
79
-log "npm run build$windoz # for frontend_app_thread"
80
-npm run build$windoz
78
+log "copying built file to frontend/"
79
+cp frontend_app_admin_workspace_user/dist/admin_workspace_user.app.js frontend/dist/app/
81 80
 
82
-log "cp dist/admin_workspace_user.app.js"
83
-cp dist/admin_workspace_user.app.js ../frontend/dist/app
81
+log "copying Thread en translation.json"
82
+cp frontend_app_admin_workspace_user/i18next.scanner/en/translation.json frontend/dist/app/admin_workspace_user_en_translation.json
84 83
 
85
-log "cp i18next.scanner/en/translation.json ../frontend/dist/app/admin_workspace_user_en_translation.json"
86
-cp i18next.scanner/en/translation.json ../frontend/dist/app/admin_workspace_user_en_translation.json
84
+log "copying Thread fr translation.json"
85
+cp frontend_app_admin_workspace_user/i18next.scanner/fr/translation.json frontend/dist/app/admin_workspace_user_fr_translation.json
87 86
 
88
-log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/admin_workspace_user_fr_translation.json"
89
-cp i18next.scanner/fr/translation.json ../frontend/dist/app/admin_workspace_user_fr_translation.json
90
-cd -
87
+log "frontend fully built"

+ 1 - 0
frontend/dist/appInterface.js View File

@@ -76,6 +76,7 @@
76 76
   GLOBAL_eventReducer = ({detail: {type, data}}) => {
77 77
     switch (type) {
78 78
       case 'hide_popupCreateContent':
79
+      case 'hide_popupCreateWorkspace':
79 80
         console.log('%cGLOBAL_eventReducer Custom Event', 'color: #28a745', type, data)
80 81
         getSelectedApp(data.name).unmountApp('popupCreateContentContainer')
81 82
         break

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

@@ -36,7 +36,6 @@
36 36
       .primaryColorBorder { border-color: #7d4e24; }
37 37
       .primaryColorBorderDarken { border-color: #572800; }
38 38
       .primaryColorBorderLighten { border-color: #a3744a; }
39
-      .whiteColorBorder { border-color: #fdfdfd; }
40 39
 
41 40
       .primaryColorBorderHover:hover { border-color: #7d4e24; }
42 41
       .primaryColorBorderDarkenHover:hover { border-color: #572800; }

+ 1 - 0
frontend/dist/tinymceInit.js View File

@@ -32,6 +32,7 @@
32 32
     });
33 33
 
34 34
     tinymce.init({
35
+      forced_root_block : "",
35 36
       selector: selector,
36 37
       menubar: false,
37 38
       resize: false,

+ 6 - 1
frontend/i18next.scanner/en/translation.json View File

@@ -64,5 +64,10 @@
64 64
   "Old password": "Old password",
65 65
   "New password": "New password",
66 66
   "Name:": "Name:",
67
-  "Email Adress:": "Email Adress:"
67
+  "Email Adress:": "Email Adress:",
68
+  "An error has happened": "An error has happened",
69
+  "You have subscribed to this workspace's notifications": "You have subscribed to this workspace's notifications",
70
+  "Connection": "Connection",
71
+  "Forgotten password ?": "Forgotten password ?",
72
+  "currently, you are ": "currently, you are "
68 73
 }

+ 6 - 1
frontend/i18next.scanner/fr/translation.json View File

@@ -64,5 +64,10 @@
64 64
   "Old password": "Ancien mot de passe",
65 65
   "New password": "Nouveau mot de passe",
66 66
   "Name:": "Nom :",
67
-  "Email Adress:": "Adresse mail :"
67
+  "Email Adress:": "Adresse mail :",
68
+  "An error has happened": "__NOT_TRANSLATED__",
69
+  "You have subscribed to this workspace's notifications": "Vous êtes abonné aux notifications de cet espace de travail.",
70
+  "Connection": "Connexion",
71
+  "Forgotten password ?": "Mot de passe oublié ?",
72
+  "currently, you are ": "actuellement, vous êtes "
68 73
 }

+ 34 - 16
frontend/src/action-creator.async.js View File

@@ -2,8 +2,6 @@ import { FETCH_CONFIG } from './helper.js'
2 2
 import {
3 3
   TIMEZONE,
4 4
   setTimezone,
5
-  LANG,
6
-  updateLangList,
7 5
   USER_LOGIN,
8 6
   USER_LOGOUT,
9 7
   USER_ROLE,
@@ -16,7 +14,9 @@ import {
16 14
   FOLDER,
17 15
   setFolderData,
18 16
   APP_LIST,
19
-  CONTENT_TYPE_LIST
17
+  CONTENT_TYPE_LIST,
18
+  WORKSPACE_RECENT_ACTIVITY,
19
+  WORKSPACE_READ_STATUS
20 20
 } from './action-creator.sync.js'
21 21
 
22 22
 /*
@@ -36,6 +36,7 @@ import {
36 36
  * This function create a http async request using whatwg-fetch while dispatching a PENDING and a SUCCESS redux action.
37 37
  * It also adds, to the Response of the fetch request, the json value so that the redux action have access to the status and the data
38 38
  */
39
+// Côme - 2018/08/02 - fetchWrapper should come from tracim_lib so that all apps uses the same
39 40
 const fetchWrapper = async ({url, param, actionName, dispatch, debug = false}) => {
40 41
   dispatch({type: `${param.method}/${actionName}/PENDING`})
41 42
 
@@ -77,19 +78,6 @@ const fetchWrapper = async ({url, param, actionName, dispatch, debug = false}) =
77 78
   return fetchResult
78 79
 }
79 80
 
80
-export const getLangList = () => async dispatch => {
81
-  const fetchGetLangList = await fetchWrapper({
82
-    url: `${FETCH_CONFIG.apiUrl}/lang`,
83
-    param: {
84
-      headers: {...FETCH_CONFIG.headers},
85
-      method: 'GET'
86
-    },
87
-    actionName: LANG,
88
-    dispatch
89
-  })
90
-  if (fetchGetLangList.status === 200) dispatch(updateLangList(fetchGetLangList.json))
91
-}
92
-
93 81
 export const getTimezone = () => async dispatch => {
94 82
   const fetchGetTimezone = await fetchWrapper({
95 83
     url: `${FETCH_CONFIG.apiUrl}/timezone`,
@@ -220,6 +208,36 @@ export const getWorkspaceContentList = (user, idWorkspace, idParent) => dispatch
220 208
   })
221 209
 }
222 210
 
211
+export const getWorkspaceRecentActivityList = (user, idWorkspace) => dispatch => {
212
+  return fetchWrapper({
213
+    url: `${FETCH_CONFIG.apiUrl}/users/${user.user_id}/workspaces/${idWorkspace}/contents/recently_active?limit=10`,
214
+    param: {
215
+      headers: {
216
+        ...FETCH_CONFIG.headers,
217
+        'Authorization': 'Basic ' + user.auth
218
+      },
219
+      method: 'GET'
220
+    },
221
+    actionName: WORKSPACE_RECENT_ACTIVITY,
222
+    dispatch
223
+  })
224
+}
225
+
226
+export const getWorkspaceReadStatusList = (user, idWorkspace) => dispatch => {
227
+  return fetchWrapper({
228
+    url: `${FETCH_CONFIG.apiUrl}/users/${user.user_id}/workspaces/${idWorkspace}/contents/read_status`,
229
+    param: {
230
+      headers: {
231
+        ...FETCH_CONFIG.headers,
232
+        'Authorization': 'Basic ' + user.auth
233
+      },
234
+      method: 'GET'
235
+    },
236
+    actionName: WORKSPACE_READ_STATUS,
237
+    dispatch
238
+  })
239
+}
240
+
223 241
 export const getFolderContent = (idWorkspace, idFolder) => async dispatch => {
224 242
   const fetchGetFolderContent = await fetchWrapper({
225 243
     url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents/?parent_id=${idFolder}`,

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

@@ -32,7 +32,7 @@ export const updateUserWorkspaceSubscriptionNotif = (workspaceId, subscriptionNo
32 32
 
33 33
 export const WORKSPACE = 'Workspace'
34 34
 export const WORKSPACE_CONTENT = `${WORKSPACE}/Content`
35
-export const setWorkspaceContentList = (workspaceContentList, filterStr = '') => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList, filterStr })
35
+export const setWorkspaceContentList = workspaceContentList => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList })
36 36
 export const updateWorkspaceFilter = filterList => ({ type: `${UPDATE}/${WORKSPACE}/Filter`, filterList })
37 37
 
38 38
 export const WORKSPACE_LIST = `${WORKSPACE}/List`
@@ -46,6 +46,14 @@ export const WORKSPACE_MEMBER = `${WORKSPACE}/Member`
46 46
 export const WORKSPACE_MEMBER_LIST = `${WORKSPACE_MEMBER}/List`
47 47
 export const setWorkspaceMemberList = workspaceMemberList => ({ type: `${SET}/${WORKSPACE_MEMBER_LIST}`, workspaceMemberList })
48 48
 
49
+export const WORKSPACE_RECENT_ACTIVITY = `${WORKSPACE}/RecentActivity/List`
50
+export const WORKSPACE_RECENT_ACTIVITY_LIST = `${WORKSPACE_RECENT_ACTIVITY}/List`
51
+export const setWorkspaceRecentActivityList = workspaceRecentActivityList => ({ type: `${SET}/${WORKSPACE_RECENT_ACTIVITY_LIST}`, workspaceRecentActivityList })
52
+
53
+export const WORKSPACE_READ_STATUS = `${WORKSPACE}/ReadStatus`
54
+export const WORKSPACE_READ_STATUS_LIST = `${WORKSPACE_READ_STATUS}/List`
55
+export const setWorkspaceReadStatusList = workspaceReadStatusList => ({ type: `${SET}/${WORKSPACE_READ_STATUS_LIST}`, workspaceReadStatusList })
56
+
49 57
 export const FOLDER = 'Folder'
50 58
 export const setFolderData = (folderId, content) => ({ type: `${SET}/${WORKSPACE}/${FOLDER}/Content`, folderId, content })
51 59
 

+ 1 - 1
frontend/src/component/Account/Notification.jsx View File

@@ -4,7 +4,7 @@ import { BtnSwitch } from 'tracim_frontend_lib'
4 4
 import { ROLE } from '../../helper.js'
5 5
 
6 6
 export const Notification = props => {
7
-  const getRole = role => ROLE.find(r => r.name === role)
7
+  const getRole = role => ROLE.find(r => r.slug === role)
8 8
 
9 9
   return (
10 10
     <div className='account__userpreference__setting__notification'>

+ 41 - 0
frontend/src/component/Dashboard/ContentTypeBtn.jsx View File

@@ -0,0 +1,41 @@
1
+import React from 'react'
2
+import PropTypes from 'prop-types'
3
+import Radium from 'radium'
4
+import color from 'color'
5
+import classnames from 'classnames'
6
+
7
+require('./ContentTypeBtn.styl')
8
+
9
+export const ContentTypeBtn = props =>
10
+  <div
11
+    className={classnames(`${props.customClass}`, 'contentTypeBtn')}
12
+    style={{
13
+      backgroundColor: props.hexcolor,
14
+      ':hover': {
15
+        backgroundColor: color(props.hexcolor).darken(0.15).hexString()
16
+      }
17
+    }}
18
+  >
19
+    <div className={classnames(`${props.customClass}__text`)}>
20
+      <div className={classnames(`${props.customClass}__text__icon`)}>
21
+        <i className={`fa fa-${props.faIcon}`} />
22
+      </div>
23
+      <div className={classnames(`${props.customClass}__text__title`)}>
24
+        {props.creationLabel}
25
+      </div>
26
+    </div>
27
+  </div>
28
+
29
+export default Radium(ContentTypeBtn)
30
+
31
+ContentTypeBtn.propTypes = {
32
+  hexcolor: PropTypes.string.isRequired,
33
+  label: PropTypes.string.isRequired,
34
+  faIcon: PropTypes.string.isRequired,
35
+  creationLabel: PropTypes.string.isRequired,
36
+  customClass: PropTypes.string
37
+}
38
+
39
+ContentTypeBtn.defaultProps = {
40
+  customClass: ''
41
+}

+ 16 - 0
frontend/src/component/Dashboard/ContentTypeBtn.styl View File

@@ -0,0 +1,16 @@
1
+.contentTypeBtn
2
+  display flex
3
+  flex-direction column
4
+  justify-content center
5
+  margin 0 15px
6
+  border-radius 10px
7
+  padding 15px
8
+  width 230px
9
+  height 200px
10
+  box-shadow shadow-all
11
+  text-align center
12
+  cursor pointer
13
+  &:nth-child(1)
14
+    margin-left 0
15
+  &:nth-last-child
16
+    margin-right 0

+ 162 - 0
frontend/src/component/Dashboard/MemberList.jsx View File

@@ -0,0 +1,162 @@
1
+import React from 'react'
2
+import PropTypes from 'prop-types'
3
+import { Checkbox } from 'tracim_frontend_lib'
4
+
5
+require('./MemberList.styl')
6
+
7
+export class MemberList extends React.Component {
8
+  constructor (props) {
9
+    super(props)
10
+
11
+    this.state = {
12
+      displayNewMemberList: true,
13
+      createAccountCheckbox: false
14
+    }
15
+  }
16
+
17
+  handleClickAddMemberBtn = () => this.setState({displayNewMemberList: false})
18
+
19
+  handleClickCloseAddMemberBtn = () => this.setState({displayNewMemberList: true})
20
+
21
+  handleClickCheckboxCreateAccount = e => {
22
+    e.preventDefault()
23
+    e.stopPropagation()
24
+    this.setState(prev => ({createAccountCheckbox: !prev.createAccountCheckbox}))
25
+  }
26
+
27
+  render () {
28
+    const { props, state } = this
29
+
30
+    return (
31
+      <div className='memberlist'>
32
+
33
+        <div className='memberlist__title subTitle'>
34
+          {props.t('Member List')}
35
+        </div>
36
+
37
+        <div className='memberlist__wrapper'>
38
+          {state.displayNewMemberList
39
+            ? (
40
+              <div>
41
+                <ul className='memberlist__list'>
42
+                  {props.memberList.map(m =>
43
+                    <li className='memberlist__list__item primaryColorBgLightenHover' key={m.id}>
44
+                      <div className='memberlist__list__item__avatar'>
45
+                        {m.avatarUrl ? <img src={m.avatarUrl} /> : <img src='NYI' />}
46
+                      </div>
47
+
48
+                      <div className='memberlist__list__item__info mr-auto'>
49
+                        <div className='memberlist__list__item__info__name'>
50
+                          {m.publicName}
51
+                        </div>
52
+
53
+                        <div className='memberlist__list__item__info__role'>
54
+                          {props.roleList.find(r => r.slug === m.role).label}
55
+                        </div>
56
+                      </div>
57
+
58
+                      <div className='memberlist__list__item__delete'>
59
+                        <i className='fa fa-trash-o' />
60
+                      </div>
61
+                    </li>
62
+                  )}
63
+                </ul>
64
+
65
+                <div className='memberlist__btnadd' onClick={this.handleClickAddMemberBtn}>
66
+                  <div className='memberlist__btnadd__button'>
67
+                    <div className='memberlist__btnadd__button__avatar'>
68
+                      <div className='memberlist__btnadd__button__avatar__icon'>
69
+                        <i className='fa fa-plus' />
70
+                      </div>
71
+                    </div>
72
+
73
+                    <div className='memberlist__btnadd__button__text'>
74
+                      {props.t('Add a member')}
75
+                    </div>
76
+                  </div>
77
+                </div>
78
+              </div>
79
+            )
80
+            : (
81
+              <form className='memberlist__form'>
82
+                <div className='memberlist__form__close d-flex justify-content-end'>
83
+                  <i className='fa fa-times' onClick={this.handleClickCloseAddMemberBtn} />
84
+                </div>
85
+
86
+                <div className='memberlist__form__member'>
87
+                  <div className='memberlist__form__member__name'>
88
+                    <label className='name__label' htmlFor='addmember'>
89
+                      {props.t('Enter the name or email of the member')}
90
+                    </label>
91
+
92
+                    <input
93
+                      type='text'
94
+                      className='name__input form-control'
95
+                      id='addmember'
96
+                      placeholder='Nom ou Email'
97
+                      onChange={props.onChangeName}
98
+                    />
99
+                  </div>
100
+
101
+                  <div className='memberlist__form__member__create'>
102
+                    <div className='memberlist__form__member__create__checkbox mr-3'>
103
+                      <Checkbox
104
+                        name='createAccountCheckbox'
105
+                        onClickCheckbox={e => this.handleClickCheckboxCreateAccount(e)}
106
+                        checked={state.createAccountCheckbox}
107
+                      />
108
+                    </div>
109
+
110
+                    <div className='create__text'>
111
+                      {props.t('Create an account')}
112
+                    </div>
113
+                  </div>
114
+                </div>
115
+
116
+                <div className='memberlist__form__role'>
117
+                  <div className='memberlist__form__role__text'>
118
+                    {props.t('Choose the role of the member')}
119
+                  </div>
120
+
121
+                  <ul className='memberlist__form__role__list'>
122
+                    {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}`} />
131
+                          </div>
132
+
133
+                          <div className='item__text__name'>
134
+                            {r.label}
135
+                          </div>
136
+                        </div>
137
+                      </li>
138
+                    )}
139
+
140
+                  </ul>
141
+                </div>
142
+
143
+                <div className='memberlist__form__submitbtn'>
144
+                  <button className='btn btn-outline-primary'>
145
+                    {props.t('Validate')}
146
+                  </button>
147
+                </div>
148
+              </form>
149
+            )
150
+          }
151
+        </div>
152
+      </div>
153
+    )
154
+  }
155
+}
156
+
157
+export default MemberList
158
+
159
+MemberList.propTypes = {
160
+  memberList: PropTypes.array.isRequired,
161
+  onChangeName: PropTypes.func
162
+}

+ 117 - 0
frontend/src/component/Dashboard/MemberList.styl View File

@@ -0,0 +1,117 @@
1
+.memberlist
2
+  margin 0 0 50px 0
3
+  width 35%
4
+  &__title
5
+    margin-bottom 20px
6
+    padding 6px
7
+    height 45px
8
+  &__wrapper
9
+    position relative
10
+    border 1px solid grey
11
+    height 480px
12
+  &__list
13
+    margin 0
14
+    padding 0
15
+    list-style none
16
+    height 400px
17
+    overflow-Y scroll
18
+    &__item
19
+      display flex
20
+      border-bottom 1px solid grey
21
+      padding 10px 15px
22
+      &:hover
23
+        background-color fourthColor
24
+      &:nth-last-child(1)
25
+        border-bottom 0
26
+      &:nth-child(even)
27
+        background-color grey-hover
28
+        &:hover
29
+          background-color fourthColor
30
+      &__avatar
31
+        margin-right 20px
32
+        & > img
33
+          width 50px
34
+          height 50px
35
+      &__info
36
+        &__name
37
+          font-size 20px
38
+        &__role
39
+          font-size 18px
40
+      &__delete
41
+        font-size 20px
42
+        color darkGrey
43
+        cursor pointer
44
+  &__btnadd
45
+    border-top 1px solid grey
46
+    padding 15px
47
+    &__button
48
+      display flex
49
+      align-items center
50
+      &__avatar
51
+        display flex
52
+        justify-content center
53
+        align-items center
54
+        margin-right 20px
55
+        border 2px dashed grey
56
+        border-radius 50%
57
+        width 50px
58
+        height 50px
59
+        cursor pointer
60
+        &__icon
61
+          color grey
62
+          font-size 25px
63
+      &__text
64
+        font-size 18px
65
+        color fontColor
66
+        cursor pointer
67
+  &__form
68
+    padding 15px
69
+    flex-direction column
70
+    height 100%
71
+    width 100%
72
+    background-color off-white
73
+    &__close
74
+      font-size 20px
75
+      & > i
76
+        cursor pointer
77
+    &__member
78
+      &__name
79
+        .name__label
80
+          margin 30px 0 20px 0
81
+          label()
82
+      .name__input
83
+        margin-bottom 20px
84
+        border 1px solid grey
85
+        border-radius 10px
86
+        padding 10px
87
+        width 300px
88
+      &__create
89
+        display flex
90
+        align-items center
91
+        margin 15px 0
92
+        line-height 23px
93
+        &__checkbox
94
+          padding-top 6px
95
+    &__role
96
+      margin-bottom 15px
97
+      coloricon()
98
+      &__text
99
+        margin 15px 0
100
+        label()
101
+      &__list
102
+        margin 0
103
+        padding 0
104
+        list-style none
105
+        &__item
106
+          display flex
107
+          align-items center
108
+          margin 10px 25px 10px 0
109
+          .item
110
+            &__text
111
+              display flex
112
+    &__submitbtn
113
+      display flex
114
+      justify-content flex-end
115
+      & > button
116
+        padding 8px 30px
117
+        cursor pointer

+ 50 - 0
frontend/src/component/Dashboard/RecentActivity.jsx View File

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

+ 2 - 5
frontend/src/component/Workspace/OpenContentApp.jsx View File

@@ -22,13 +22,10 @@ export class OpenContentApp extends React.Component {
22 22
       console.log('%c<OpenContentApp> contentToOpen', 'color: #dcae84', contentToOpen)
23 23
 
24 24
       if (appOpenedType === contentToOpen.type) { // app already open
25
-        dispatchCustomEvent({
26
-          type: `${contentToOpen.type}_reloadContent`, // handled by html-document:src/container/AdminWorkspaceUser.jsx
27
-          data: contentToOpen
28
-        })
25
+        dispatchCustomEvent(`${contentToOpen.type}_reloadContent`, contentToOpen)
29 26
       } else { // open another app
30 27
         // if another app is already visible, hide it
31
-        if (appOpenedType !== false) dispatchCustomEvent({type: `${appOpenedType}_hideApp`})
28
+        if (appOpenedType !== false) dispatchCustomEvent(`${appOpenedType}_hideApp`, {})
32 29
         // open app
33 30
         renderAppFeature(
34 31
           contentType.find(ct => ct.slug === contentToOpen.type),

+ 1 - 4
frontend/src/container/Account.jsx View File

@@ -1,6 +1,5 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3
-import Sidebar from './Sidebar.jsx'
4 3
 import UserInfo from '../component/Account/UserInfo.jsx'
5 4
 import MenuSubComponent from '../component/Account/MenuSubComponent.jsx'
6 5
 import PersonalData from '../component/Account/PersonalData.jsx'
@@ -102,9 +101,7 @@ class Account extends React.Component {
102 101
     })()
103 102
 
104 103
     return (
105
-      <div className='sidebarpagecontainer'>
106
-        <Sidebar />
107
-
104
+      <div className='Account'>
108 105
         <PageWrapper customClass='account'>
109 106
           <PageTitle
110 107
             parentClass={'account'}

+ 138 - 0
frontend/src/container/AdminWorkspacePage.jsx View File

@@ -0,0 +1,138 @@
1
+import React from 'react'
2
+import PropTypes from 'prop-types'
3
+import Sidebar from './Sidebar.jsx'
4
+import {
5
+  Delimiter,
6
+  PageWrapper,
7
+  PageTitle,
8
+  PageContent
9
+} from 'tracim_frontend_lib'
10
+import { translate } from 'react-i18next'
11
+
12
+class AdminWorkspacePage extends React.Component {
13
+  render () {
14
+    return (
15
+      <div className='sidebarpagecontainer'>
16
+        <Sidebar />
17
+
18
+        <PageWrapper customClass='adminWorkspacePage'>
19
+          <PageTitle
20
+            parentClass={'adminWorkspacePage'}
21
+            title={'Workspace management'}
22
+          />
23
+
24
+          <PageContent parentClass='adminWorkspacePage'>
25
+
26
+            <div className='adminWorkspacePage__description'>
27
+              This page informs all workspaces of the instances
28
+            </div>
29
+
30
+            <div className='adminWorkspacePage__createworkspace'>
31
+              <button className='adminWorkspacePage__createworkspace__btncreate btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover'>
32
+                {this.props.t('Create a workspace')}
33
+              </button>
34
+            </div>
35
+
36
+            <Delimiter customClass={'adminWorkspacePage__delimiter'} />
37
+
38
+            <div className='adminWorkspacePage__workspaceTable'>
39
+
40
+              <table class='table'>
41
+                <thead>
42
+                  <tr>
43
+                    <th scope='col'># ID</th>
44
+                    <th scope='col'>Workspace</th>
45
+                    <th scope='col'>Description</th>
46
+                    <th scope='col'>Users</th>
47
+                    <th scope='col'>Calendar</th>
48
+                  </tr>
49
+                </thead>
50
+                <tbody>
51
+                  <tr>
52
+                    <th>#1</th>
53
+                    <td>Design v_2</td>
54
+                    <td>Workspace about tracim v2 design</td>
55
+                    <td>8 users</td>
56
+                    <td className='d-flex align-items-center flex-wrap'>
57
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
58
+                        <i className='fa fa-fw fa-check-square-o' />
59
+                      </div>
60
+                      Enable
61
+                    </td>
62
+                  </tr>
63
+                  <tr>
64
+                    <th>#2</th>
65
+                    <td>New features</td>
66
+                    <td>Add a new features : Annotated files</td>
67
+                    <td>5 users</td>
68
+                    <td className='d-flex align-items-center flex-wrap'>
69
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
70
+                        <i className='fa fa-fw fa-square-o' />
71
+                      </div>
72
+                      Disable
73
+                    </td>
74
+                  </tr>
75
+                  <tr>
76
+                    <th>#3</th>
77
+                    <td>Fix Backend</td>
78
+                    <td>workspace referring to multiple issues on the backend </td>
79
+                    <td>3 users</td>
80
+                    <td className='d-flex align-items-center flex-wrap'>
81
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
82
+                        <i className='fa fa-fw fa-check-square-o' />
83
+                      </div>
84
+                      Enable
85
+                    </td>
86
+                  </tr>
87
+                  <tr>
88
+                    <th>#4</th>
89
+                    <td>Design v_2</td>
90
+                    <td>Workspace about tracim v2 design</td>
91
+                    <td>8 users</td>
92
+                    <td className='d-flex align-items-center flex-wrap'>
93
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
94
+                        <i className='fa fa-fw fa-square-o' />
95
+                      </div>
96
+                      Disable
97
+                    </td>
98
+                  </tr>
99
+                  <tr>
100
+                    <th>#5</th>
101
+                    <td>New features</td>
102
+                    <td>Add a new features : Annotated files</td>
103
+                    <td>5 users</td>
104
+                    <td className='d-flex align-items-center flex-wrap'>
105
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
106
+                        <i className='fa fa-fw fa-square-o' />
107
+                      </div>
108
+                      Disable
109
+                    </td>
110
+                  </tr>
111
+                  <tr>
112
+                    <th>#6</th>
113
+                    <td>Fix Backend</td>
114
+                    <td>workspace referring to multiple issues on the backend </td>
115
+                    <td>3 users</td>
116
+                    <td className='d-flex align-items-center flex-wrap'>
117
+                      <div className='adminWorkspacePage__workspaceTable__calendaricon mr-2'>
118
+                        <i className='fa fa-fw fa-check-square-o' />
119
+                      </div>
120
+                      Enable
121
+                    </td>
122
+                  </tr>
123
+                </tbody>
124
+              </table>
125
+            </div>
126
+
127
+          </PageContent>
128
+        </PageWrapper>
129
+      </div>
130
+    )
131
+  }
132
+}
133
+
134
+AdminWorkspacePage.propTypes = {
135
+  availableApp: PropTypes.array.isRequired
136
+}
137
+
138
+export default translate()(AdminWorkspacePage)

+ 1 - 4
frontend/src/container/AppFullscreenManager.jsx View File

@@ -4,7 +4,6 @@ import { withRouter } from 'react-router'
4 4
 import { Route } from 'react-router-dom'
5 5
 import { PAGE } from '../helper.js'
6 6
 import appFactory from '../appFactory.js'
7
-import Sidebar from './Sidebar.jsx'
8 7
 
9 8
 class AppFullscreenManager extends React.Component {
10 9
   constructor (props) {
@@ -20,9 +19,7 @@ class AppFullscreenManager extends React.Component {
20 19
     const { props } = this
21 20
 
22 21
     return (
23
-      <div className='sidebarpagecontainer'>
24
-        <Sidebar />
25
-
22
+      <div className='AppFullScreenManager'>
26 23
         <div id='appFullscreenContainer' />
27 24
 
28 25
         {this.state.AmIMounted && (// we must wait for the component to be fully mounted to be sure the div#appFullscreenContainer exists in DOM

+ 76 - 193
frontend/src/container/Dashboard.jsx View File

@@ -1,10 +1,6 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3
-import Sidebar from './Sidebar.jsx'
4
-import imgProfil from '../img/imgProfil.png'
5 3
 import { translate } from 'react-i18next'
6
-import Radium from 'radium'
7
-import color from 'color'
8 4
 import {
9 5
   PageWrapper,
10 6
   PageTitle,
@@ -12,13 +8,21 @@ import {
12 8
 } from 'tracim_frontend_lib'
13 9
 import {
14 10
   getWorkspaceDetail,
15
-  getWorkspaceMemberList
11
+  getWorkspaceMemberList,
12
+  getWorkspaceRecentActivityList,
13
+  getWorkspaceReadStatusList
16 14
 } from '../action-creator.async.js'
17 15
 import {
18
-  addFlashMessage,
16
+  newFlashMessage,
19 17
   setWorkspaceDetail,
20
-  setWorkspaceMemberList
18
+  setWorkspaceMemberList,
19
+  setWorkspaceRecentActivityList,
20
+  setWorkspaceReadStatusList
21 21
 } from '../action-creator.sync.js'
22
+import { ROLE } from '../helper.js'
23
+import ContentTypeBtn from '../component/Dashboard/ContentTypeBtn.jsx'
24
+import RecentActivity from '../component/Dashboard/RecentActivity.jsx'
25
+import MemberList from '../component/Dashboard/MemberList.jsx'
22 26
 
23 27
 class Dashboard extends React.Component {
24 28
   constructor (props) {
@@ -38,23 +42,33 @@ class Dashboard extends React.Component {
38 42
     const fetchWorkspaceDetail = await props.dispatch(getWorkspaceDetail(props.user, state.workspaceIdInUrl))
39 43
     switch (fetchWorkspaceDetail.status) {
40 44
       case 200:
41
-        props.dispatch(setWorkspaceDetail(fetchWorkspaceDetail.json))
42
-        break
43
-      case 400:
44
-      case 500:
45
-        props.dispatch(addFlashMessage(props.t('An error has happened'), 'warning'))
46
-        break
45
+        props.dispatch(setWorkspaceDetail(fetchWorkspaceDetail.json)); break
46
+      default:
47
+        props.dispatch(newFlashMessage(props.t('An error has happened when fetching workspace detail'), 'warning')); break
47 48
     }
48 49
 
49 50
     const fetchWorkspaceMemberList = await props.dispatch(getWorkspaceMemberList(props.user, state.workspaceIdInUrl))
50 51
     switch (fetchWorkspaceMemberList.status) {
51 52
       case 200:
52
-        props.dispatch(setWorkspaceMemberList(fetchWorkspaceMemberList.json))
53
-        break
54
-      case 400:
55
-      case 500:
56
-        props.dispatch(addFlashMessage(props.t('An error has happened'), 'warning'))
57
-        break
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
56
+    }
57
+
58
+    const fetchWorkspaceRecentActivityList = await props.dispatch(getWorkspaceRecentActivityList(props.user, state.workspaceIdInUrl))
59
+    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
64
+    }
65
+
66
+    const fetchWorkspaceReadStatusList = await props.dispatch(getWorkspaceReadStatusList(props.user, state.workspaceIdInUrl))
67
+    switch (fetchWorkspaceReadStatusList.status) {
68
+      case 200:
69
+        props.dispatch(setWorkspaceReadStatusList(fetchWorkspaceReadStatusList.json)); break
70
+      default:
71
+        props.dispatch(newFlashMessage(props.t('An error has happened while fetching read status list'), 'warning')); break
58 72
     }
59 73
   }
60 74
 
@@ -78,9 +92,7 @@ class Dashboard extends React.Component {
78 92
     const { props, state } = this
79 93
 
80 94
     return (
81
-      <div className='sidebarpagecontainer'>
82
-        <Sidebar />
83
-
95
+      <div className='Dashboard' style={{width: '100%'}}>
84 96
         <PageWrapper customeClass='dashboard'>
85 97
           <PageTitle
86 98
             parentClass='dashboard__header'
@@ -109,18 +121,27 @@ class Dashboard extends React.Component {
109 121
               <div className='dashboard__userstatut'>
110 122
                 <div className='dashboard__userstatut__role'>
111 123
                   <div className='dashboard__userstatut__role__msg'>
112
-                    {props.t(`Hi ! ${props.user.public_name}, vous êtes actuellement`)}
124
+                    {props.t(`Hi ! ${props.user.public_name} `)}{props.t('currently, you are ')}
113 125
                   </div>
114 126
 
115
-                  <div className='dashboard__userstatut__role__definition'>
116
-                    <div className='dashboard__userstatut__role__definition__icon'>
117
-                      <i className='fa fa-graduation-cap' />
118
-                    </div>
127
+                  {(() => {
128
+                    const myself = props.curWs.memberList.find(m => m.id === props.user.user_id)
129
+                    if (myself === undefined) return
119 130
 
120
-                    <div className='dashboard__userstatut__role__definition__text'>
121
-                      {(member => member ? member.role : '')(props.curWs.member.find(m => m.id === props.user.user_id))}
122
-                    </div>
123
-                  </div>
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
+                  })()}
124 145
                 </div>
125 146
 
126 147
                 <div className='dashboard__userstatut__notification'>
@@ -139,7 +160,7 @@ class Dashboard extends React.Component {
139 160
                           aria-haspopup='true'
140 161
                           aria-expanded='false'
141 162
                         >
142
-                          Abonné(e)
163
+                          {props.t('subscriber')}
143 164
                         </button>
144 165
 
145 166
                         <div className='dashboard__userstatut__notification__subscribe__submenu dropdown-menu'>
@@ -167,170 +188,32 @@ class Dashboard extends React.Component {
167 188
 
168 189
             <div className='dashboard__calltoaction justify-content-xl-center'>
169 190
               {props.contentType.map(ct =>
170
-                <div
171
-                  className='dashboard__calltoaction__button btnaction'
172
-                  style={{
173
-                    backgroundColor: ct.hexcolor,
174
-                    ':hover': {
175
-                      backgroundColor: color(ct.hexcolor).darken(0.15).hexString()
176
-                    }
177
-                  }}
191
+                <ContentTypeBtn
192
+                  customClass='dashboard__calltoaction__button'
193
+                  hexcolor={ct.hexcolor}
194
+                  label={ct.label}
195
+                  faIcon={ct.faIcon}
196
+                  creationLabel={ct.creationLabel}
178 197
                   key={ct.label}
179
-                >
180
-                  <div className='dashboard__calltoaction__button__text'>
181
-                    <div className='dashboard__calltoaction__button__text__icon'>
182
-                      <i className={`fa fa-${ct.faIcon}`} />
183
-                    </div>
184
-                    <div className='dashboard__calltoaction__button__text__title'>
185
-                      {ct.creationLabel}
186
-                    </div>
187
-                  </div>
188
-                </div>
198
+                />
189 199
               )}
190 200
             </div>
191 201
 
192
-            <div className='dashboard__wksinfo'>
193
-              <div className='dashboard__activity'>
194
-                <div className='dashboard__activity__header'>
195
-                  <div className='dashboard__activity__header__title subTitle'>
196
-                    {this.props.t('Recent activity')}
197
-                  </div>
198
-
199
-                  <div className='dashboard__activity__header__allread btn btn-outline-primary'>
200
-                    {this.props.t('Mark everything as read')}
201
-                  </div>
202
-                </div>
203
-                <div className='dashboard__activity__wrapper'>
204
-
205
-                  <div className='dashboard__activity__workspace'>
206
-                    <div className='dashboard__activity__workspace__icon'>
207
-                      <i className='fa fa-comments-o' />
208
-                    </div>
209
-                    <div className='dashboard__activity__workspace__name'>
210
-                      <span>Développement Tracim</span>
211
-                    </div>
212
-                  </div>
213
-
214
-                  <div className='dashboard__activity__more d-flex flex-row-reverse'>
215
-                    <div className='dashboard__activity__more__btn btn btn-outline-primary'>
216
-                      {this.props.t('See more')}
217
-                    </div>
218
-                  </div>
219
-                </div>
220
-              </div>
221
-
222
-              <div className='dashboard__memberlist'>
223
-
224
-                <div className='dashboard__memberlist__title subTitle'>
225
-                  {this.props.t('Member List')}
226
-                </div>
227
-
228
-                <div className='dashboard__memberlist__wrapper'>
229
-                  {this.state.displayNewMemberDashboard === false &&
230
-                    <div>
231
-                      <ul className='dashboard__memberlist__list'>
232
-
233
-                        <li className='dashboard__memberlist__list__item'>
234
-                          <div className='dashboard__memberlist__list__item__avatar'>
235
-                            <img src={imgProfil} alt='avatar' />
236
-                          </div>
237
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
238
-                            <div className='dashboard__memberlist__list__item__info__name'>
239
-                              Jean Dupont
240
-                            </div>
241
-                            <div className='dashboard__memberlist__list__item__info__role'>
242
-                              Responsable
243
-                            </div>
244
-                          </div>
245
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
246
-                            <i className='fa fa-trash-o' />
247
-                          </div>
248
-                        </li>
249
-
250
-                      </ul>
251
-
252
-                      <div
253
-                        className='dashboard__memberlist__btnadd'
254
-                        onClick={this.handleToggleNewMemberDashboard}
255
-                      >
256
-                        <div className='dashboard__memberlist__btnadd__button'>
257
-                          <div className='dashboard__memberlist__btnadd__button__avatar'>
258
-                            <div className='dashboard__memberlist__btnadd__button__avatar__icon'>
259
-                              <i className='fa fa-plus' />
260
-                            </div>
261
-                          </div>
262
-                          <div
263
-                            className='dashboard__memberlist__btnadd__button__text'
264
-                          >
265
-                            {this.props.t('Add a member')}
266
-                          </div>
267
-                        </div>
268
-                      </div>
269
-                    </div>
270
-                  }
271
-
272
-                  {this.state.displayNewMemberDashboard === true &&
273
-                  <form className='dashboard__memberlist__form'>
274
-                    <div
275
-                      className='dashboard__memberlist__form__close d-flex justify-content-end'
276
-                    >
277
-                      <i className='fa fa-times' onClick={this.handleToggleNewMemberDashboard} />
278
-                    </div>
279
-
280
-                    <div className='dashboard__memberlist__form__member'>
281
-                      <div className='dashboard__memberlist__form__member__name'>
282
-                        <label className='name__label' htmlFor='addmember'>
283
-                          {this.props.t('Enter the name or email of the member')}
284
-                        </label>
285
-                        <input type='text' id='addmember' className='name__input form-control' placeholder='Nom ou Email' />
286
-                      </div>
287
-
288
-                      <div className='dashboard__memberlist__form__member__create'>
289
-                        <div className='create__radiobtn mr-3'>
290
-                          <input type='radio' />
291
-                        </div>
292
-
293
-                        <div className='create__text'>
294
-                          {this.props.t('Create an account')}
295
-                        </div>
296
-                      </div>
297
-                    </div>
298
-
299
-                    <div className='dashboard__memberlist__form__role'>
300
-                      <div className='dashboard__memberlist__form__role__text'>
301
-                        {this.props.t('Choose the role of the member')}
302
-                      </div>
303
-
304
-                      <ul className='dashboard__memberlist__form__role__list'>
305
-
306
-                        <li className='dashboard__memberlist__form__role__list__item'>
307
-                          <div className='item__radiobtn mr-3'>
308
-                            <input type='radio' name='role' value='responsable' />
309
-                          </div>
310
-
311
-                          <div className='item__text'>
312
-                            <div className='item_text_icon mr-2'>
313
-                              <i className='fa fa-gavel' />
314
-                            </div>
315
-
316
-                            <div className='item__text__name'>
317
-                              {this.props.t('Supervisor')}
318
-                            </div>
319
-                          </div>
320
-                        </li>
321
-
322
-                      </ul>
323
-                    </div>
324
-
325
-                    <div className='dashboard__memberlist__form__submitbtn'>
326
-                      <button className='btn btn-outline-primary'>
327
-                        {this.props.t('Validate')}
328
-                      </button>
329
-                    </div>
330
-                  </form>
331
-                  }
332
-                </div>
333
-              </div>
202
+            <div className='dashboard__workspaceInfo'>
203
+              <RecentActivity
204
+                customClass='dashboard__activity'
205
+                recentActivityFilteredForUser={props.curWs.recentActivityList.filter(content => !props.curWs.contentReadStatusList.includes(content.id))}
206
+                contentTypeList={props.contentType}
207
+                onClickSeeMore={() => {}}
208
+                t={props.t}
209
+              />
210
+
211
+              <MemberList
212
+                customClass='dashboard__memberlist'
213
+                memberList={props.curWs.memberList}
214
+                roleList={ROLE}
215
+                t={props.t}
216
+              />
334 217
             </div>
335 218
 
336 219
             <div className='dashboard__moreinfo'>
@@ -401,4 +284,4 @@ class Dashboard extends React.Component {
401 284
 }
402 285
 
403 286
 const mapStateToProps = ({ user, contentType, currentWorkspace }) => ({ user, contentType, curWs: currentWorkspace })
404
-export default connect(mapStateToProps)(translate()(Radium(Dashboard)))
287
+export default connect(mapStateToProps)(translate()(Dashboard))

+ 0 - 571
frontend/src/container/Dashboard_old.jsx View File

@@ -1,571 +0,0 @@
1
-import React from 'react'
2
-import { connect } from 'react-redux'
3
-import Sidebar from './Sidebar.jsx'
4
-import imgProfil from '../img/imgProfil.png'
5
-import { translate } from 'react-i18next'
6
-
7
-class Dashboard extends React.Component {
8
-  constructor (props) {
9
-    super(props)
10
-    this.state = {
11
-      workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null, // this is used to avoid handling the parseInt everytime
12
-      displayNewMemberDashboard: false,
13
-      displayNotifBtn: false,
14
-      displayWebdavBtn: false,
15
-      displayCalendarBtn: false
16
-    }
17
-  }
18
-
19
-  handleToggleNewMemberDashboard = () => this.setState(prevState => ({
20
-    displayNewMemberDashboard: !prevState.displayNewMemberDashboard
21
-  }))
22
-
23
-  handleToggleNotifBtn = () => this.setState(prevState => ({
24
-    displayNotifBtn: !prevState.displayNotifBtn
25
-  }))
26
-
27
-  handleToggleWebdavBtn = () => this.setState(prevState => ({
28
-    displayWebdavBtn: !prevState.displayWebdavBtn
29
-  }))
30
-
31
-  handleToggleCalendarBtn = () => this.setState(prevState => ({
32
-    displayCalendarBtn: !prevState.displayCalendarBtn
33
-  }))
34
-
35
-  render () {
36
-    return (
37
-      <div className='sidebarpagecontainer'>
38
-        <Sidebar />
39
-
40
-        <div className='dashboard'>
41
-          <div className='container-fluid nopadding'>
42
-            <div className='dashboard__header mb-5'>
43
-              <div className='pageTitleGeneric dashboard__header__title d-flex align-items-center'>
44
-                <div className='pageTitleGeneric__title dashboard__header__title__text mr-3'>
45
-                  {this.props.t('Dashboard')}
46
-                </div>
47
-                <div className='dashboard__header__acces' />
48
-              </div>
49
-
50
-              <div className='dashboard__header__advancedmode mr-3'>
51
-                <button type='button' className='btn btn-primary'>
52
-                  {this.props.t('Active advanced Dashboard')}
53
-                </button>
54
-              </div>
55
-            </div>
56
-
57
-            <div className='dashboard__workspace-wrapper'>
58
-              <div className='dashboard__workspace'>
59
-                <div className='dashboard__workspace__title'>
60
-                  Développement tracim
61
-                </div>
62
-
63
-                <div className='dashboard__workspace__detail'>
64
-                  Ligne directive pour le prochain design de Tracim et des futurs fonctionnalités.
65
-                </div>
66
-              </div>
67
-
68
-              <div className='dashboard__userstatut'>
69
-
70
-                <div className='dashboard__userstatut__role'>
71
-                  <div className='dashboard__userstatut__role__text'>
72
-                    Hi ! Alexi, vous êtes actuellement
73
-                  </div>
74
-                  <div className='dashboard__userstatut__role__rank'>
75
-                    <div className='dashboard__userstatut__role__rank__icon'>
76
-                      <i className='fa fa-graduation-cap' />
77
-                    </div>
78
-                    <div className='dashboard__userstatut__role__rank__rolename'>
79
-                      Gestionnaire de projet
80
-                    </div>
81
-                  </div>
82
-                </div>
83
-
84
-                <div className='dashboard__userstatut__notification'>
85
-                  <div className='dashboard__userstatut__notification__text'>
86
-                    Vous êtes abonné(e) aux notifications de ce workspace
87
-                  </div>
88
-                  {this.state.displayNotifBtn === false &&
89
-                  <div
90
-                    className='dashboard__userstatut__notification__btn btn btn-outline-primary'
91
-                    onClick={this.handleToggleNotifBtn}
92
-                  >
93
-                    {this.props.t('Change your status')}
94
-                  </div>
95
-                  }
96
-
97
-                  {this.state.displayNotifBtn === true &&
98
-                  <div className='dashboard__userstatut__notification__subscribe dropdown'>
99
-                    <button className='dashboard__userstatut__notification__subscribe__btn btn btn-outline-primary dropdown-toggle' type='button' id='dropdownMenuButton' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>
100
-                      Abonné(e)
101
-                    </button>
102
-                    <div className='dashboard__userstatut__notification__subscribe__submenu dropdown-menu'>
103
-                      <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item'>
104
-                        {this.props.t('subscriber')}
105
-                      </div>
106
-                      <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item dropdown-item'>
107
-                        {this.props.t('unsubscribed')}
108
-                      </div>
109
-                    </div>
110
-                  </div>
111
-                  }
112
-                </div>
113
-              </div>
114
-            </div>
115
-
116
-            <div className='dashboard__calltoaction justify-content-xl-center'>
117
-              <div className='dashboard__calltoaction__button btnaction btnthread'>
118
-                <div className='dashboard__calltoaction__button__text'>
119
-                  <div className='dashboard__calltoaction__button__text__icon'>
120
-                    <i className='fa fa-comments-o' />
121
-                  </div>
122
-                  <div className='dashboard__calltoaction__button__text__title'>
123
-                    {this.props.t('Start a new Thread')}
124
-                  </div>
125
-                </div>
126
-              </div>
127
-
128
-              <div className='dashboard__calltoaction__button btnaction writefile'>
129
-                <div className='dashboard__calltoaction__button__text'>
130
-                  <div className='dashboard__calltoaction__button__text__icon'>
131
-                    <i className='fa fa-file-text-o' />
132
-                  </div>
133
-                  <div className='dashboard__calltoaction__button__text__title'>
134
-                    {this.props.t('Writing a document')}
135
-                  </div>
136
-                </div>
137
-              </div>
138
-
139
-              <div className='dashboard__calltoaction__button btnaction importfile'>
140
-                <div className='dashboard__calltoaction__button__text'>
141
-                  <div className='dashboard__calltoaction__button__text__icon'>
142
-                    <i className='fa fa-paperclip' />
143
-                  </div>
144
-                  <div className='dashboard__calltoaction__button__text__title'>
145
-                    {this.props.t('Upload a file')}
146
-                  </div>
147
-                </div>
148
-              </div>
149
-
150
-              {/*
151
-                <div className='dashboard__calltoaction__button btnaction visioconf'>
152
-                  <div className='dashboard__calltoaction__button__text'>
153
-                    <div className='dashboard__calltoaction__button__text__icon'>
154
-                      <i className='fa fa-video-camera' />
155
-                    </div>
156
-                    <div className='dashboard__calltoaction__button__text__title'>
157
-                      {this.props.t('Start a videoconference')}
158
-                    </div>
159
-                  </div>
160
-                </div>
161
-
162
-                <div className='dashboard__calltoaction__button btnaction calendar'>
163
-                  <div className='dashboard__calltoaction__button__text'>
164
-                    <div className='dashboard__calltoaction__button__text__icon'>
165
-                      <i className='fa fa-calendar' />
166
-                    </div>
167
-                    <div className='dashboard__calltoaction__button__text__title'>
168
-                      {this.props.t('View the Calendar')}
169
-                    </div>
170
-                  </div>
171
-                </div>
172
-              */ }
173
-
174
-              <div className='dashboard__calltoaction__button btnaction explore'>
175
-                <div className='dashboard__calltoaction__button__text'>
176
-                  <div className='dashboard__calltoaction__button__text__icon'>
177
-                    <i className='fa fa-folder-open-o' />
178
-                  </div>
179
-                  <div className='dashboard__calltoaction__button__text__title'>
180
-                    {this.props.t('Explore the workspace')}
181
-                  </div>
182
-                </div>
183
-              </div>
184
-            </div>
185
-
186
-            <div className='dashboard__wksinfo'>
187
-              <div className='dashboard__activity'>
188
-                <div className='dashboard__activity__header'>
189
-                  <div className='dashboard__activity__header__title subTitle'>
190
-                    {this.props.t('Recent activity')}
191
-                  </div>
192
-
193
-                  <div className='dashboard__activity__header__allread btn btn-outline-primary'>
194
-                    {this.props.t('Mark everything as read')}
195
-                  </div>
196
-                </div>
197
-                <div className='dashboard__activity__wrapper'>
198
-                  <div className='dashboard__activity__workspace'>
199
-                    <div className='dashboard__activity__workspace__icon'>
200
-                      <i className='fa fa-comments-o' />
201
-                    </div>
202
-                    <div className='dashboard__activity__workspace__name'>
203
-                      <span>Développement Tracim</span>
204
-                    </div>
205
-                  </div>
206
-
207
-                  <div className='dashboard__activity__workspace'>
208
-                    <div className='dashboard__activity__workspace__icon'>
209
-                      <i className='fa fa-list-ul' />
210
-                    </div>
211
-                    <div className='dashboard__activity__workspace__name'>
212
-                      Mission externe
213
-                    </div>
214
-                  </div>
215
-
216
-                  <div className='dashboard__activity__workspace'>
217
-                    <div className='dashboard__activity__workspace__icon'>
218
-                      <i className='fa fa-list-ul' />
219
-                    </div>
220
-                    <div className='dashboard__activity__workspace__name'>
221
-                      Recherche et developpement
222
-                    </div>
223
-                  </div>
224
-
225
-                  <div className='dashboard__activity__workspace'>
226
-                    <div className='dashboard__activity__workspace__icon'>
227
-                      <i className='fa fa-file-text-o' />
228
-                    </div>
229
-                    <div className='dashboard__activity__workspace__name'>
230
-                      <span>Marketing</span>
231
-                    </div>
232
-                  </div>
233
-
234
-                  <div className='dashboard__activity__workspace'>
235
-                    <div className='dashboard__activity__workspace__icon'>
236
-                      <i className='fa fa-comments-o' />
237
-                    </div>
238
-                    <div className='dashboard__activity__workspace__name'>
239
-                      <span>Évolution</span>
240
-                    </div>
241
-                  </div>
242
-
243
-                  <div className='dashboard__activity__workspace'>
244
-                    <div className='dashboard__activity__workspace__icon'>
245
-                      <i className='fa fa-file-text-o' />
246
-                    </div>
247
-                    <div className='dashboard__activity__workspace__name'>
248
-                      Commercialisation
249
-                    </div>
250
-                  </div>
251
-
252
-                  <div className='dashboard__activity__more d-flex flex-row-reverse'>
253
-                    <div className='dashboard__activity__more__btn btn btn-outline-primary'>
254
-                      {this.props.t('See more')}
255
-                    </div>
256
-                  </div>
257
-                </div>
258
-              </div>
259
-
260
-              <div className='dashboard__memberlist'>
261
-
262
-                <div className='dashboard__memberlist__title subTitle'>
263
-                  {this.props.t('Member List')}
264
-                </div>
265
-
266
-                <div className='dashboard__memberlist__wrapper'>
267
-                  {this.state.displayNewMemberDashboard === false &&
268
-                  <div>
269
-                    <ul className='dashboard__memberlist__list'>
270
-                      <li className='dashboard__memberlist__list__item'>
271
-                        <div className='dashboard__memberlist__list__item__avatar'>
272
-                          <img src={imgProfil} alt='avatar' />
273
-                        </div>
274
-                        <div className='dashboard__memberlist__list__item__info mr-auto'>
275
-                          <div className='dashboard__memberlist__list__item__info__name'>
276
-                            Jean Dupont
277
-                          </div>
278
-                          <div className='dashboard__memberlist__list__item__info__role'>
279
-                            Responsable
280
-                          </div>
281
-                        </div>
282
-                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
283
-                          <i className='fa fa-trash-o' />
284
-                        </div>
285
-                      </li>
286
-
287
-                      <li className='dashboard__memberlist__list__item'>
288
-                        <div className='dashboard__memberlist__list__item__avatar'>
289
-                          <img src={imgProfil} alt='avatar' />
290
-                        </div>
291
-                        <div className='dashboard__memberlist__list__item__info mr-auto'>
292
-                          <div className='dashboard__memberlist__list__item__info__name'>
293
-                            Aldwin Vinel
294
-                          </div>
295
-                          <div className='dashboard__memberlist__list__item__info__role'>
296
-                            Lecteur
297
-                          </div>
298
-                        </div>
299
-                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
300
-                          <i className='fa fa-trash-o' />
301
-                        </div>
302
-                      </li>
303
-
304
-                      <li className='dashboard__memberlist__list__item'>
305
-                        <div className='dashboard__memberlist__list__item__avatar'>
306
-                          <img src={imgProfil} alt='avatar' />
307
-                        </div>
308
-                        <div className='dashboard__memberlist__list__item__info mr-auto'>
309
-                          <div className='dashboard__memberlist__list__item__info__name'>
310
-                            William Himme
311
-                          </div>
312
-                          <div className='dashboard__memberlist__list__item__info__role'>
313
-                            Contributeur
314
-                          </div>
315
-                        </div>
316
-                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
317
-                          <i className='fa fa-trash-o' />
318
-                        </div>
319
-                      </li>
320
-
321
-                      <li className='dashboard__memberlist__list__item'>
322
-                        <div className='dashboard__memberlist__list__item__avatar'>
323
-                          <img src={imgProfil} alt='avatar' />
324
-                        </div>
325
-                        <div className='dashboard__memberlist__list__item__info mr-auto'>
326
-                          <div className='dashboard__memberlist__list__item__info__name'>
327
-                            Yacine Lite
328
-                          </div>
329
-                          <div className='dashboard__memberlist__list__item__info__role'>
330
-                            Contributeur
331
-                          </div>
332
-                        </div>
333
-                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
334
-                          <i className='fa fa-trash-o' />
335
-                        </div>
336
-                      </li>
337
-
338
-                      <li className='dashboard__memberlist__list__item'>
339
-                        <div className='dashboard__memberlist__list__item__avatar'>
340
-                          <img src={imgProfil} alt='avatar' />
341
-                        </div>
342
-                        <div className='dashboard__memberlist__list__item__info mr-auto'>
343
-                          <div className='dashboard__memberlist__list__item__info__name'>
344
-                            Alexi Falcin
345
-                          </div>
346
-                          <div className='dashboard__memberlist__list__item__info__role'>
347
-                            Gestionnaire
348
-                          </div>
349
-                        </div>
350
-                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
351
-                          <i className='fa fa-trash-o' />
352
-                        </div>
353
-                      </li>
354
-
355
-                      <li className='dashboard__memberlist__list__item'>
356
-                        <div className='dashboard__memberlist__list__item__avatar'>
357
-                          <img src={imgProfil} alt='avatar' />
358
-                        </div>
359
-                        <div className='dashboard__memberlist__list__item__info mr-auto'>
360
-                          <div className='dashboard__memberlist__list__item__info__name'>
361
-                            Mickaël Fonati
362
-                          </div>
363
-                          <div className='dashboard__memberlist__list__item__info__role'>
364
-                            Gestionnaire
365
-                          </div>
366
-                        </div>
367
-                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
368
-                          <i className='fa fa-trash-o' />
369
-                        </div>
370
-                      </li>
371
-
372
-                      <li className='dashboard__memberlist__list__item'>
373
-                        <div className='dashboard__memberlist__list__item__avatar'>
374
-                          <img src={imgProfil} alt='avatar' />
375
-                        </div>
376
-                        <div className='dashboard__memberlist__list__item__info mr-auto'>
377
-                          <div className='dashboard__memberlist__list__item__info__name'>
378
-                            Eva Lonbard
379
-                          </div>
380
-                          <div className='dashboard__memberlist__list__item__info__role'>
381
-                            Gestionnaire
382
-                          </div>
383
-                        </div>
384
-                        <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
385
-                          <i className='fa fa-trash-o' />
386
-                        </div>
387
-                      </li>
388
-                    </ul>
389
-
390
-                    <div
391
-                      className='dashboard__memberlist__btnadd'
392
-                      onClick={this.handleToggleNewMemberDashboard}
393
-                    >
394
-                      <div className='dashboard__memberlist__btnadd__button'>
395
-                        <div className='dashboard__memberlist__btnadd__button__avatar'>
396
-                          <div className='dashboard__memberlist__btnadd__button__avatar__icon'>
397
-                            <i className='fa fa-plus' />
398
-                          </div>
399
-                        </div>
400
-                        <div
401
-                          className='dashboard__memberlist__btnadd__button__text'
402
-                        >
403
-                          {this.props.t('Add a member')}
404
-                        </div>
405
-                      </div>
406
-                    </div>
407
-                  </div>
408
-                  }
409
-
410
-                  {this.state.displayNewMemberDashboard === true &&
411
-                  <form className='dashboard__memberlist__form'>
412
-                    <div
413
-                      className='dashboard__memberlist__form__close d-flex justify-content-end'
414
-                    >
415
-                      <i className='fa fa-times' onClick={this.handleToggleNewMemberDashboard} />
416
-                    </div>
417
-                    <div className='dashboard__memberlist__form__member'>
418
-                      <div className='dashboard__memberlist__form__member__name'>
419
-                        <label className='name__label' htmlFor='addmember'>
420
-                          {this.props.t('Enter the name or email of the member')}
421
-                        </label>
422
-                        <input type='text' id='addmember' className='name__input form-control' placeholder='Nom ou Email' />
423
-                      </div>
424
-                      <div className='dashboard__memberlist__form__member__create'>
425
-                        <div className='create__radiobtn mr-3'>
426
-                          <input type='radio' />
427
-                        </div>
428
-                        <div className='create__text'>
429
-                          {this.props.t('Create an account')}
430
-                        </div>
431
-                      </div>
432
-                    </div>
433
-                    <div className='dashboard__memberlist__form__role'>
434
-                      <div className='dashboard__memberlist__form__role__text'>
435
-                        {this.props.t('Choose the role of the member')}
436
-                      </div>
437
-                      <ul className='dashboard__memberlist__form__role__list'>
438
-                        <li className='dashboard__memberlist__form__role__list__item'>
439
-                          <div className='item__radiobtn mr-3'>
440
-                            <input type='radio' name='role' value='responsable' />
441
-                          </div>
442
-                          <div className='item__text'>
443
-                            <div className='item_text_icon mr-2'>
444
-                              <i className='fa fa-gavel' />
445
-                            </div>
446
-                            <div className='item__text__name'>
447
-                              {this.props.t('Supervisor')}
448
-                            </div>
449
-                          </div>
450
-                        </li>
451
-                        <li className='dashboard__memberlist__form__role__list__item'>
452
-                          <div className='item__radiobtn mr-3'>
453
-                            <input type='radio' name='role' value='gestionnaire' />
454
-                          </div>
455
-                          <div className='item__text'>
456
-                            <div className='item_text_icon mr-2'>
457
-                              <i className='fa fa-graduation-cap' />
458
-                            </div>
459
-                            <div className='item__text__name'>
460
-                              {this.props.t('Content Manager')}
461
-                            </div>
462
-                          </div>
463
-                        </li>
464
-                        <li className='dashboard__memberlist__form__role__list__item'>
465
-                          <div className='item__radiobtn mr-3'>
466
-                            <input type='radio' name='role' value='contributeur' />
467
-                          </div>
468
-                          <div className='item__text'>
469
-                            <div className='item_text_icon mr-2'>
470
-                              <i className='fa fa-pencil' />
471
-                            </div>
472
-                            <div className='item__text__name'>
473
-                              {this.props.t('Contributor')}
474
-                            </div>
475
-                          </div>
476
-                        </li>
477
-                        <li className='dashboard__memberlist__form__role__list__item'>
478
-                          <div className='item__radiobtn mr-3'>
479
-                            <input type='radio' name='role' value='lecteur' />
480
-                          </div>
481
-                          <div className='item__text'>
482
-                            <div className='item_text_icon mr-2'>
483
-                              <i className='fa fa-eye' />
484
-                            </div>
485
-                            <div className='item__text__name'>
486
-                              {this.props.t('Reader')}
487
-                            </div>
488
-                          </div>
489
-                        </li>
490
-                      </ul>
491
-                    </div>
492
-                    <div className='dashboard__memberlist__form__submitbtn'>
493
-                      <button className='btn btn-outline-primary'>
494
-                        {this.props.t('Validate')}
495
-                      </button>
496
-                    </div>
497
-                  </form>
498
-                  }
499
-                </div>
500
-              </div>
501
-            </div>
502
-
503
-            <div className='dashboard__moreinfo'>
504
-              <div className='dashboard__moreinfo__webdav genericBtnInfoDashboard'>
505
-                <div
506
-                  className='dashboard__moreinfo__webdav__btn genericBtnInfoDashboard__btn'
507
-                  onClick={this.handleToggleWebdavBtn}
508
-                >
509
-                  <div className='dashboard__moreinfo__webdav__btn__icon genericBtnInfoDashboard__btn__icon'>
510
-                    <i className='fa fa-windows' />
511
-                  </div>
512
-
513
-                  <div className='dashboard__moreinfo__webdav__btn__text genericBtnInfoDashboard__btn__text'>
514
-                    {this.props.t('Implement Tracim in your explorer')}
515
-                  </div>
516
-                </div>
517
-                {this.state.displayWebdavBtn === true &&
518
-                <div>
519
-                  <div className='dashboard__moreinfo__webdav__information genericBtnInfoDashboard__info'>
520
-                    <div className='dashboard__moreinfo__webdav__information__text genericBtnInfoDashboard__info__text'>
521
-                      {this.props.t('Find all your documents deposited online directly on your computer via the workstation, without going through the software.')}'
522
-                    </div>
523
-
524
-                    <div className='dashboard__moreinfo__webdav__information__link genericBtnInfoDashboard__info__link'>
525
-                      http://algoo.trac.im/webdav/
526
-                    </div>
527
-                  </div>
528
-                </div>
529
-                }
530
-              </div>
531
-              <div className='dashboard__moreinfo__calendar genericBtnInfoDashboard'>
532
-                <div className='dashboard__moreinfo__calendar__wrapperBtn'>
533
-                  <div
534
-                    className='dashboard__moreinfo__calendar__btn genericBtnInfoDashboard__btn'
535
-                    onClick={this.handleToggleCalendarBtn}
536
-                  >
537
-                    <div className='dashboard__moreinfo__calendar__btn__icon genericBtnInfoDashboard__btn__icon'>
538
-                      <i className='fa fa-calendar' />
539
-                    </div>
540
-
541
-                    <div className='dashboard__moreinfo__calendar__btn__text genericBtnInfoDashboard__btn__text'>
542
-                      {this.props.t('Workspace Calendar')}
543
-                    </div>
544
-                  </div>
545
-                </div>
546
-                <div className='dashboard__moreinfo__calendar__wrapperText'>
547
-                  {this.state.displayCalendarBtn === true &&
548
-                  <div>
549
-                    <div className='dashboard__moreinfo__calendar__information genericBtnInfoDashboard__info'>
550
-                      <div className='dashboard__moreinfo__calendar__information__text genericBtnInfoDashboard__info__text'>
551
-                        {this.props.t('Each workspace has its own calendar.')}
552
-                      </div>
553
-
554
-                      <div className='dashboard__moreinfo__calendar__information__link genericBtnInfoDashboard__info__link'>
555
-                        http://algoo.trac.im/calendar/
556
-                      </div>
557
-                    </div>
558
-                  </div>
559
-                  }
560
-                </div>
561
-              </div>
562
-            </div>
563
-          </div>
564
-        </div>
565
-      </div>
566
-    )
567
-  }
568
-}
569
-
570
-const mapStateToProps = ({ user, app, contentType, workspaceList }) => ({ user, app, contentType, workspaceList })
571
-export default connect(mapStateToProps)(translate()(Dashboard))

+ 5 - 5
frontend/src/container/Login.jsx View File

@@ -55,8 +55,8 @@ class Login extends React.Component {
55 55
       Cookies.set(COOKIE.USER_LOGIN, inputLogin.value)
56 56
       Cookies.set(COOKIE.USER_AUTH, userAuth)
57 57
 
58
-      history.push(PAGE.HOME)
59
-    } else if (fetchPostUserLogin.status === 400) {
58
+      history.push(PAGE.WORKSPACE.ROOT)
59
+    } else if (fetchPostUserLogin.status === 403) {
60 60
       dispatch(newFlashMessage(t('Email or password invalid'), 'danger'))
61 61
     }
62 62
   }
@@ -74,7 +74,7 @@ class Login extends React.Component {
74 74
               <div className='col-12 col-sm-11 col-md-8 col-lg-6 col-xl-4'>
75 75
 
76 76
                 <Card customClass='loginpage__connection'>
77
-                  <CardHeader customClass='connection__header text-center'>{'Connexion'}</CardHeader>
77
+                  <CardHeader customClass='connection__header text-center'>{this.props.t('Connection')}</CardHeader>
78 78
 
79 79
                   <CardBody formClass='connection__form'>
80 80
                     <div>
@@ -118,7 +118,7 @@ class Login extends React.Component {
118 118
                         <div className='col-12 col-sm-6 col-md-6 col-lg-6 col-xl-6 text-sm-right'>
119 119
                           <LoginBtnForgotPw
120 120
                             customClass='connection__form__pwforgot'
121
-                            label='Mot de passe oublié ?'
121
+                            label={this.props.t('Forgotten password ?')}
122 122
                           />
123 123
                         </div>
124 124
                       </div>
@@ -127,7 +127,7 @@ class Login extends React.Component {
127 127
                         htmlType='button'
128 128
                         bootstrapType='primary'
129 129
                         customClass='connection__form__btnsubmit ml-auto'
130
-                        label='Connexion'
130
+                        label={this.props.t('Connection')}
131 131
                         onClick={this.handleClickSubmit}
132 132
                       />
133 133
                     </div>

+ 18 - 6
frontend/src/container/Sidebar.jsx View File

@@ -6,11 +6,14 @@ import { translate } from 'react-i18next'
6 6
 import appFactory from '../appFactory.js'
7 7
 import WorkspaceListItem from '../component/Sidebar/WorkspaceListItem.jsx'
8 8
 import {
9
+  setAppList,
10
+  setContentTypeList,
9 11
   setWorkspaceListIsOpenInSidebar,
10 12
   updateWorkspaceFilter,
11 13
   updateWorkspaceListData
12 14
 } from '../action-creator.sync.js'
13 15
 import {
16
+  getAppList, getContentTypeList,
14 17
   getWorkspaceList
15 18
 } from '../action-creator.async.js'
16 19
 import { PAGE, workspaceConfig } from '../helper.js'
@@ -22,7 +25,7 @@ class Sidebar extends React.Component {
22 25
     super(props)
23 26
     this.state = {
24 27
       sidebarClose: false,
25
-      workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null
28
+      workspaceIdInUrl: props.match && props.match.params.idws ? parseInt(props.match.params.idws) : null
26 29
     }
27 30
 
28 31
     document.addEventListener('appCustomEvent', this.customEventReducer)
@@ -32,28 +35,37 @@ class Sidebar extends React.Component {
32 35
     switch (type) {
33 36
       case 'refreshWorkspaceList':
34 37
         console.log('%c<Sidebar> Custom event', 'color: #28a745', type, data)
35
-        this.loadWorkspaceList()
38
+        this.loadAppConfigAndWorkspaceList()
36 39
         break
37 40
     }
38 41
   }
39 42
 
40 43
   componentDidMount () {
41
-    this.loadWorkspaceList()
44
+    // console.log('Sidebar Did Mount', this.props)
45
+    this.loadAppConfigAndWorkspaceList()
42 46
   }
43 47
 
44 48
   componentDidUpdate (prevProps, prevState) {
49
+    const { props } = this
50
+
45 51
     // console.log('%c<Sidebar> Did Update', 'color: #c17838')
46
-    if (this.props.match.params.idws === undefined || isNaN(this.props.match.params.idws)) return
52
+    if (!props.match || props.match.params.idws === undefined || isNaN(props.match.params.idws)) return
47 53
 
48
-    const newWorkspaceId = parseInt(this.props.match.params.idws)
54
+    const newWorkspaceId = parseInt(props.match.params.idws)
49 55
     if (prevState.workspaceIdInUrl !== newWorkspaceId) this.setState({workspaceIdInUrl: newWorkspaceId})
50 56
   }
51 57
 
52
-  loadWorkspaceList = async () => {
58
+  loadAppConfigAndWorkspaceList = async () => {
53 59
     const { workspaceIdInUrl } = this.state
54 60
     const { user, dispatch } = this.props
55 61
 
56 62
     if (user.user_id !== -1) {
63
+      const fetchGetAppList = await dispatch(getAppList(user))
64
+      if (fetchGetAppList.status === 200) dispatch(setAppList(fetchGetAppList.json))
65
+
66
+      const fetchGetContentTypeList = await dispatch(getContentTypeList(user))
67
+      if (fetchGetContentTypeList.status === 200) dispatch(setContentTypeList(fetchGetContentTypeList.json))
68
+
57 69
       const fetchGetWorkspaceList = await dispatch(getWorkspaceList(user))
58 70
 
59 71
       if (fetchGetWorkspaceList.status === 200) {

+ 53 - 35
frontend/src/container/Tracim.jsx View File

@@ -1,31 +1,28 @@
1 1
 import React from 'react'
2 2
 import { connect } from 'react-redux'
3 3
 import { translate } from 'react-i18next'
4
+import Sidebar from './Sidebar.jsx'
4 5
 import Header from './Header.jsx'
5 6
 import Login from './Login.jsx'
6
-import Dashboard from './Dashboard.jsx'
7 7
 import Account from './Account.jsx'
8
+import AdminWorkspacePage from './AdminWorkspacePage.jsx'
8 9
 import AppFullscreenManager from './AppFullscreenManager.jsx'
9 10
 import FlashMessage from '../component/FlashMessage.jsx'
10 11
 import WorkspaceContent from './WorkspaceContent.jsx'
11 12
 import WIPcomponent from './WIPcomponent.jsx'
12 13
 import {
13
-  Route, withRouter, Switch
14
+  Route, withRouter, Redirect
14 15
 } from 'react-router-dom'
15
-import PrivateRoute from './PrivateRoute.jsx'
16 16
 import { COOKIE, PAGE } from '../helper.js'
17 17
 import {
18
-  getAppList,
19
-  getUserIsConnected,
20
-  getContentTypeList
18
+  getUserIsConnected
21 19
 } from '../action-creator.async.js'
22 20
 import {
23 21
   removeFlashMessage,
24
-  setAppList,
25
-  setUserConnected,
26
-  setContentTypeList
22
+  setUserConnected
27 23
 } from '../action-creator.sync.js'
28 24
 import Cookies from 'js-cookie'
25
+import Dashboard from './Dashboard.jsx'
29 26
 
30 27
 class Tracim extends React.Component {
31 28
   constructor (props) {
@@ -44,6 +41,7 @@ class Tracim extends React.Component {
44 41
   }
45 42
 
46 43
   async componentDidMount () {
44
+    // console.log('<Tracim> did Mount')
47 45
     const { dispatch } = this.props
48 46
 
49 47
     const userFromCookies = {
@@ -54,24 +52,14 @@ class Tracim extends React.Component {
54 52
     const fetchGetUserIsConnected = await dispatch(getUserIsConnected(userFromCookies))
55 53
     switch (fetchGetUserIsConnected.status) {
56 54
       case 200:
57
-        const userLogged = {
55
+        dispatch(setUserConnected({
58 56
           ...fetchGetUserIsConnected.json,
59 57
           auth: userFromCookies.auth,
60 58
           logged: true
61
-        }
62
-
63
-        dispatch(setUserConnected(userLogged))
64
-
65
-        const fetchGetAppList = await dispatch(getAppList(userLogged))
66
-        if (fetchGetAppList.status === 200) dispatch(setAppList(fetchGetAppList.json))
67
-
68
-        const fetchGetContentTypeList = await dispatch(getContentTypeList(userLogged))
69
-        if (fetchGetContentTypeList.status === 200) dispatch(setContentTypeList(fetchGetContentTypeList.json))
59
+        }))
70 60
         break
71
-
72 61
       case 401:
73 62
         dispatch(setUserConnected({logged: false})); break
74
-
75 63
       default:
76 64
         dispatch(setUserConnected({logged: null})); break
77 65
     }
@@ -80,27 +68,57 @@ class Tracim extends React.Component {
80 68
   handleRemoveFlashMessage = msg => this.props.dispatch(removeFlashMessage(msg))
81 69
 
82 70
   render () {
83
-    const { flashMessage, t } = this.props
71
+    const { props } = this
84 72
 
85 73
     return (
86 74
       <div className='tracim'>
87 75
         <Header />
88
-        <FlashMessage flashMessage={flashMessage} removeFlashMessage={this.handleRemoveFlashMessage} t={t} />
76
+        <FlashMessage flashMessage={props.flashMessage} removeFlashMessage={this.handleRemoveFlashMessage} t={props.t} />
89 77
 
90 78
         <div className='tracim__content'>
91 79
           <Route path={PAGE.LOGIN} component={Login} />
92 80
 
93
-          <PrivateRoute exact path='/' component={WorkspaceContent} />
94
-
95
-          <Switch>
96
-            <PrivateRoute path={PAGE.WORKSPACE.DASHBOARD(':idws')} component={Dashboard} />
97
-            <PrivateRoute path={PAGE.WORKSPACE.CALENDAR(':idws')} component={() => <div><br /><br /><br /><br />NYI</div>} />
98
-            <PrivateRoute path={PAGE.WORKSPACE.CONTENT(':idws', ':type?', ':idcts?')} component={WorkspaceContent} />
99
-          </Switch>
100
-
101
-          <PrivateRoute path={PAGE.ACCOUNT} component={Account} />
102
-          <PrivateRoute path={PAGE.ADMIN.ROOT} component={AppFullscreenManager} />
103
-          <PrivateRoute path={'/wip/:cp'} component={WIPcomponent} /> {/* for testing purpose only */}
81
+          <Route exact path='/' component={() => {
82
+            switch (props.user.logged) {
83
+              case true:
84
+                return <Redirect to={{pathname: PAGE.WORKSPACE.ROOT, state: {from: props.location}}} />
85
+              case false:
86
+                return <Redirect to={{pathname: '/login', state: {from: props.location}}} />
87
+              case null:
88
+                return null
89
+            }
90
+          }} />
91
+
92
+          { props.user.logged
93
+            ? (
94
+              <Route path='/workspaces/:idws?' render={() => // Workspace Router
95
+                <div className='sidebarpagecontainer'>
96
+                  <Sidebar />
97
+
98
+                  <Route exact path={PAGE.WORKSPACE.ROOT} render={() => props.workspaceList.length === 0 // handle '/' and redirect to first workspace
99
+                    ? null
100
+
101
+          <PrivateRoute path='/admin_temp/workspace' component={AdminWorkspacePage} />
102
+
103
+                  <Route exact path={`${PAGE.WORKSPACE.ROOT}/:idws`} render={props2 => // handle '/workspaces/:id' and add '/contents'
104
+                    <Redirect to={{pathname: `/workspaces/${props2.match.params.idws}/contents`, state: {from: props.location}}} />
105
+                  } />
106
+
107
+                  <Route path={PAGE.WORKSPACE.DASHBOARD(':idws')} component={Dashboard} />
108
+                  <Route path={PAGE.WORKSPACE.CALENDAR(':idws')} component={() => <div><br /><br /><br /><br />NYI</div>} />
109
+                  <Route path={PAGE.WORKSPACE.CONTENT(':idws', ':type', ':idcts')} component={WorkspaceContent} />
110
+                  <Route exact path={PAGE.WORKSPACE.CONTENT_LIST(':idws')} component={WorkspaceContent} />
111
+
112
+                  <Route path={PAGE.ACCOUNT} component={Account} />
113
+                  <Route path={PAGE.ADMIN.ROOT} component={AppFullscreenManager} />
114
+                </div>
115
+              } />
116
+            )
117
+            : props.user.logged === false && props.location.pathname !== '/login' &&
118
+              <Redirect to={{pathname: '/login', state: {from: props.location}}} />
119
+          }
120
+
121
+          <Route path={'/wip/:cp'} component={WIPcomponent} /> {/* for testing purpose only */}
104 122
 
105 123
           <div id='appFeatureContainer' />
106 124
         </div>
@@ -110,5 +128,5 @@ class Tracim extends React.Component {
110 128
   }
111 129
 }
112 130
 
113
-const mapStateToProps = ({ flashMessage }) => ({ flashMessage })
131
+const mapStateToProps = ({ user, appList, contentType, workspaceList, flashMessage }) => ({ user, appList, contentType, workspaceList, flashMessage })
114 132
 export default withRouter(connect(mapStateToProps)(translate()(Tracim)))

+ 3 - 6
frontend/src/container/WorkspaceContent.jsx View File

@@ -3,7 +3,6 @@ import { connect } from 'react-redux'
3 3
 import { withRouter, Route } from 'react-router-dom'
4 4
 import appFactory from '../appFactory.js'
5 5
 import { PAGE } from '../helper.js'
6
-import Sidebar from './Sidebar.jsx'
7 6
 import Folder from '../component/Workspace/Folder.jsx'
8 7
 import ContentItem from '../component/Workspace/ContentItem.jsx'
9 8
 import ContentItemHeader from '../component/Workspace/ContentItemHeader.jsx'
@@ -108,7 +107,7 @@ class WorkspaceContent extends React.Component {
108 107
 
109 108
   handleClickContentItem = content => {
110 109
     console.log('%c<WorkspaceContent> content clicked', 'color: #c17838', content)
111
-    this.props.history.push(`/workspaces/${content.idWorkspace}/${content.type}/${content.id}`)
110
+    this.props.history.push(PAGE.WORKSPACE.CONTENT(content.idWorkspace, content.type, content.id))
112 111
   }
113 112
 
114 113
   handleClickEditContentItem = (e, content) => {
@@ -166,9 +165,7 @@ class WorkspaceContent extends React.Component {
166 165
       : []
167 166
 
168 167
     return (
169
-      <div className='sidebarpagecontainer'>
170
-        <Sidebar />
171
-
168
+      <div className='WorkspaceContent' style={{width: '100%'}}>
172 169
         <OpenContentApp
173 170
           // automatically open the app for the idContent in url
174 171
           idWorkspace={this.state.workspaceIdInUrl}
@@ -192,7 +189,7 @@ class WorkspaceContent extends React.Component {
192 189
             subtitle={workspaceContentList.label ? workspaceContentList.label : ''}
193 190
           >
194 191
             <DropdownCreateButton
195
-              parentClass='workspace__header__btnaddworkspace'
192
+              parentClass='workspace__header__btnaddcontent'
196 193
               idFolder={null} // null because it is workspace root content
197 194
               onClickCreateContent={this.handleClickCreateContent}
198 195
               availableApp={contentType}

+ 11 - 0
frontend/src/css/AdminWorkspacePage.styl View File

@@ -0,0 +1,11 @@
1
+.adminWorkspacePage
2
+  &__createworkspace
3
+    &__btncreate
4
+      margin 25px 15px
5
+  &__description
6
+    margin 25px 15px
7
+    font-size 20px
8
+  &__delimiter
9
+    margin 25px auto 65px auto
10
+  &__workspaceTable
11
+    margin 25px 15px

+ 5 - 121
frontend/src/css/Dashboard.styl View File

@@ -31,7 +31,6 @@ coloricon()
31 31
     color lecteur
32 32
 
33 33
 .dashboard
34
-  margin-left 20px
35 34
   width 100%
36 35
   &__header
37 36
     flexwrap()
@@ -59,7 +58,6 @@ coloricon()
59 58
   &__userstatut
60 59
     width 35%
61 60
     &__role
62
-      flexwrap()
63 61
       margin 20px 0
64 62
       font-size 18px
65 63
       &__msg
@@ -93,12 +91,15 @@ coloricon()
93 91
           font-size 30px
94 92
         &__title
95 93
           font-size 18px
96
-  &__wksinfo
94
+  &__workspaceInfo
97 95
     flexwrap()
98
-    margin-top 150px
99 96
   &__activity
100 97
     margin 0 35px 50px 0
101 98
     width 60%
99
+    &__wrapper
100
+      border 1px solid grey
101
+      height 480px
102
+      overflow-y scroll
102 103
     &__header
103 104
       display flex
104 105
       justify-content space-between
@@ -109,9 +110,6 @@ coloricon()
109 110
         padding 10px 25px
110 111
         font-size 18px
111 112
         cursor pointer
112
-    &__wrapper
113
-      border 1px solid grey
114
-      height 480px
115 113
     &__workspace
116 114
       display flex
117 115
       align-items center
@@ -137,120 +135,6 @@ coloricon()
137 135
         margin 15px
138 136
         padding 10px 25px
139 137
         cursor pointer
140
-  &__memberlist
141
-    margin 0 0 50px 0
142
-    width 35%
143
-    &__title
144
-      margin-bottom 20px
145
-      padding 6px
146
-      height 45px
147
-    &__wrapper
148
-      position relative
149
-      border 1px solid grey
150
-      height 480px
151
-    &__list
152
-      margin 0
153
-      padding 0
154
-      list-style none
155
-      height 400px
156
-      overflow-Y scroll
157
-      &__item
158
-        display flex
159
-        border-bottom 1px solid grey
160
-        padding 10px 15px
161
-        &:hover
162
-          background-color fourthColor
163
-        &:nth-last-child(1)
164
-          border-bottom 0
165
-        &:nth-child(even)
166
-          background-color grey-hover
167
-          &:hover
168
-            background-color fourthColor
169
-        &__avatar
170
-          margin-right 20px
171
-          & > img
172
-            width 50px
173
-            height 50px
174
-        &__info
175
-          &__name
176
-            font-size 20px
177
-          &__role
178
-            font-size 18px
179
-        &__delete
180
-          font-size 20px
181
-          color darkGrey
182
-          cursor pointer
183
-    &__btnadd
184
-      border-top 1px solid grey
185
-      padding 15px
186
-      &__button
187
-        display flex
188
-        align-items center
189
-        &__avatar
190
-          display flex
191
-          justify-content center
192
-          align-items center
193
-          margin-right 20px
194
-          border 2px dashed grey
195
-          border-radius 50%
196
-          width 50px
197
-          height 50px
198
-          cursor pointer
199
-          &__icon
200
-            color grey
201
-            font-size 25px
202
-        &__text
203
-          font-size 18px
204
-          color fontColor
205
-          cursor pointer
206
-    &__form
207
-      padding 15px
208
-      flex-direction column
209
-      height 100%
210
-      width 100%
211
-      background-color off-white
212
-      &__close
213
-        font-size 20px
214
-        & > i
215
-          cursor pointer
216
-      &__member
217
-        &__name
218
-          .name__label
219
-            margin 30px 0 20px 0
220
-            label()
221
-          .name__input
222
-            margin-bottom 20px
223
-            border 1px solid grey
224
-            border-radius 10px
225
-            padding 10px
226
-            width 300px
227
-        &__create
228
-          display flex
229
-          align-items center
230
-          margin 15px 0
231
-      &__role
232
-        margin-bottom 15px
233
-        coloricon()
234
-        &__text
235
-          margin 15px 0
236
-          label()
237
-        &__list
238
-          margin 0
239
-          padding 0
240
-          list-style none
241
-          &__item
242
-            display flex
243
-            align-items center
244
-            margin 10px 25px 10px 0
245
-            .item
246
-              &__text
247
-                display flex
248
-      &__submitbtn
249
-        display flex
250
-        justify-content flex-end
251
-        & > button
252
-          padding 8px 30px
253
-          cursor pointer
254 138
   &__moreinfo
255 139
     display flex
256 140
     justify-content space-between

+ 1 - 18
frontend/src/css/Generic.styl View File

@@ -111,7 +111,7 @@ a
111 111
 
112 112
 
113 113
 .pageContentGeneric
114
-  margin 10px 0 0 0
114
+  margin 10px 15px 0 15px
115 115
   width 100%
116 116
   height 100%
117 117
 
@@ -198,23 +198,6 @@ a
198 198
   font-weight 500
199 199
   color thirdColor
200 200
 
201
-.btnaction
202
-  display flex
203
-  flex-direction column
204
-  justify-content center
205
-  margin 0 15px
206
-  border-radius 10px
207
-  padding 15px
208
-  width 230px
209
-  height 200px
210
-  box-shadow shadow-all
211
-  text-align center
212
-  cursor pointer
213
-  &:nth-child(1)
214
-    margin-left 0
215
-  &:nth-last-child
216
-    margin-right 0
217
-
218 201
 .genericBtnInfoDashboard
219 202
   &__btn
220 203
     display flex

+ 0 - 1
frontend/src/css/Workspace.styl View File

@@ -4,7 +4,6 @@
4 4
     flex-wrap wrap
5 5
     margin-right 15px
6 6
   &__content
7
-    margin 0 15px
8 7
     &__button
9 8
       display flex
10 9
       justify-content flex-end

+ 19 - 17
frontend/src/helper.js View File

@@ -24,14 +24,12 @@ export const workspaceConfig = {
24 24
 export const PAGE = {
25 25
   HOME: '/',
26 26
   WORKSPACE: {
27
+    ROOT: '/workspaces',
27 28
     DASHBOARD: (idws = ':idws') => `/workspaces/${idws}/dashboard`,
28
-    NEW: (idws, type) => `/workspaces/${idws}/${type}/new`,
29
+    NEW: (idws, type) => `/workspaces/${idws}/contents/${type}/new`,
29 30
     CALENDAR: (idws = ':idws') => `/workspaces/${idws}/calendar`,
30 31
     CONTENT_LIST: (idws = ':idws') => `/workspaces/${idws}/contents`,
31
-    CONTENT: (idws = ':idws', type = ':type?', idcts = ':idcts?') => `/workspaces/${idws}/${type}/${idcts}`, // @TODO add /contents/ in url and remove <Switch> in <Tracim>
32
-    // CONTENT_NEW: (idws = ':idws', ctstype = ':ctstype') => `/workspaces/${idws}/contents/${ctstype}/new`,
33
-    // CONTENT_EDIT: (idws = ':idws', idcts = ':idcts') => `/workspaces/${idws}/contents/${idcts}/edit`,
34
-    // CONTENT_TITLE_EDIT: (idws = ':idws', idcts = ':idcts') => `/workspaces/${idws}/contents/${idcts}/title/edit`,
32
+    CONTENT: (idws = ':idws', type = ':type', idcts = ':idcts') => `/workspaces/${idws}/contents/${type}/${idcts}`,
35 33
     ADMIN: (idws = ':idws') => `/workspaces/${idws}/admin`
36 34
   },
37 35
   LOGIN: '/login',
@@ -45,24 +43,28 @@ export const PAGE = {
45 43
 
46 44
 export const ROLE = [{
47 45
   id: 0,
48
-  name: 'reader',
49
-  icon: 'fa-eye',
50
-  translationKey: 'role.reader'
46
+  slug: 'reader',
47
+  faIcon: 'eye',
48
+  hexcolor: '#15D948',
49
+  label: 'Reader'
51 50
 }, {
52 51
   id: 1,
53
-  name: 'contributor',
54
-  icon: 'fa-pencil',
55
-  translationKey: 'role.contributor'
52
+  slug: 'contributor',
53
+  faIcon: 'pencil',
54
+  hexcolor: '#3145F7',
55
+  label: 'Contributor'
56 56
 }, {
57 57
   id: 2,
58
-  name: 'content_manager',
59
-  icon: 'fa-graduation-cap',
60
-  translationKey: 'role.content_manager'
58
+  slug: 'content-manager',
59
+  faIcon: 'graduation-cap',
60
+  hexcolor: '#f2af2d',
61
+  label: 'Content manager'
61 62
 }, {
62 63
   id: 3,
63
-  name: 'manager',
64
-  icon: 'fa-gavel',
65
-  translationKey: 'role.manager'
64
+  slug: 'workspace-manager',
65
+  faIcon: 'gavel',
66
+  hexcolor: '#ed0007',
67
+  label: 'Workspace manager'
66 68
 }]
67 69
 
68 70
 export const handleRouteFromApi = route => route.startsWith('/#') ? route.slice(2) : route

+ 39 - 6
frontend/src/reducer/currentWorkspace.js View File

@@ -1,4 +1,10 @@
1
-import {SET, WORKSPACE_DETAIL, WORKSPACE_MEMBER_LIST} from '../action-creator.sync.js'
1
+import {
2
+  SET,
3
+  WORKSPACE_DETAIL,
4
+  WORKSPACE_MEMBER_LIST,
5
+  WORKSPACE_READ_STATUS_LIST,
6
+  WORKSPACE_RECENT_ACTIVITY_LIST
7
+} from '../action-creator.sync.js'
2 8
 import { handleRouteFromApi } from '../helper.js'
3 9
 
4 10
 const defaultWorkspace = {
@@ -6,8 +12,10 @@ const defaultWorkspace = {
6 12
   slug: '',
7 13
   label: '',
8 14
   description: '',
9
-  sidebarEntries: [],
10
-  member: []
15
+  sidebarEntryList: [],
16
+  memberList: [],
17
+  recentActivityList: [],
18
+  contentReadStatusList: []
11 19
 }
12 20
 
13 21
 export default function currentWorkspace (state = defaultWorkspace, action) {
@@ -19,7 +27,7 @@ export default function currentWorkspace (state = defaultWorkspace, action) {
19 27
         slug: action.workspaceDetail.slug,
20 28
         label: action.workspaceDetail.label,
21 29
         description: action.workspaceDetail.description,
22
-        sidebarEntries: action.workspaceDetail.sidebar_entries.map(sbe => ({
30
+        sidebarEntryList: action.workspaceDetail.sidebar_entries.map(sbe => ({
23 31
           slug: sbe.slug,
24 32
           route: handleRouteFromApi(sbe.route),
25 33
           faIcon: sbe.fa_icon,
@@ -31,15 +39,40 @@ export default function currentWorkspace (state = defaultWorkspace, action) {
31 39
     case `${SET}/${WORKSPACE_MEMBER_LIST}`:
32 40
       return {
33 41
         ...state,
34
-        member: action.workspaceMemberList.map(m => ({
42
+        memberList: action.workspaceMemberList.map(m => ({
35 43
           id: m.user_id,
36 44
           publicName: m.user.public_name,
37 45
           avatarUrl: m.user.avatar_url,
38 46
           role: m.role,
39
-          isActive: m.is_active,
47
+          isActive: m.is_active
40 48
         }))
41 49
       }
42 50
 
51
+    case `${SET}/${WORKSPACE_RECENT_ACTIVITY_LIST}`:
52
+      return {
53
+        ...state,
54
+        recentActivityList: action.workspaceRecentActivityList.map(ra => ({
55
+          id: ra.content_id,
56
+          slug: ra.slug,
57
+          label: ra.label,
58
+          type: ra.content_type,
59
+          idParent: ra.parent_id,
60
+          showInUi: ra.show_in_ui,
61
+          isArchived: ra.is_archived,
62
+          isDeleted: ra.is_deleted,
63
+          statusSlug: ra.status,
64
+          subContentTypeSlug: ra.sub_content_types
65
+        }))
66
+      }
67
+
68
+    case `${SET}/${WORKSPACE_READ_STATUS_LIST}`:
69
+      return {
70
+        ...state,
71
+        contentReadStatusList: action.workspaceReadStatusList
72
+          .filter(content => content.read_by_user)
73
+          .map(content => content.content_id)
74
+      }
75
+
43 76
     default:
44 77
       return state
45 78
   }

+ 1 - 1
frontend/src/reducer/workspaceContentList.js View File

@@ -16,7 +16,7 @@ export default function workspaceContentList (state = [], action) {
16 16
         type: wsc.content_type,
17 17
         idWorkspace: wsc.workspace_id,
18 18
         isArchived: wsc.is_archived,
19
-        parentId: wsc.parent_id,
19
+        idParent: wsc.parent_id,
20 20
         isDeleted: wsc.is_deleted,
21 21
         showInUi: wsc.show_in_ui,
22 22
         statusSlug: wsc.status,

+ 1 - 1
frontend_lib/dist/index.html View File

@@ -17,6 +17,6 @@
17 17
 
18 18
   <div id='content'></div>
19 19
 
20
-  <script type='text/javascript' src='./tracim_lib.dev.js'></script>
20
+  <script type='text/javascript' src='./tracim_frontend_lib.dev.js'></script>
21 21
 </body>
22 22
 </html>

File diff suppressed because it is too large
+ 0 - 15
frontend_lib/dist/tracim_lib.js


+ 2 - 2
frontend_lib/package.json View File

@@ -7,8 +7,8 @@
7 7
     "servdev": "NODE_ENV=development webpack-dev-server --watch --colors --inline --hot --progress",
8 8
     "servdevwindoz": "set NODE_ENV=development&& webpack-dev-server --watch --colors --inline --hot --progress",
9 9
     "servdev-dashboard": "NODE_ENV=development webpack-dashboard -m -p 9870 -- webpack-dev-server --watch --colors --inline --hot --progress",
10
-    "buildwindoz": "set NODE_ENV=production&& webpack -p",
11
-    "build": "NODE_ENV=production webpack -p",
10
+    "buildwindoz": "echo 'Only use npm run buildtracimlibwindoz'",
11
+    "build": "echo 'Only use npm run buildtracimlib'",
12 12
     "build-translation": "node i18next.scanner.js",
13 13
     "buildtracimlib": "NODE_ENV=production webpack -p && echo '/* eslint-disable */' | cat - dist/tracim_frontend_lib.js > temp && mv temp dist/tracim_frontend_lib.js && printf '\n/* eslint-enable */\n' >> dist/tracim_frontend_lib.js",
14 14
     "buildtracimlibwindoz": "set NODE_ENV=production&& webpack -p && echo /* eslint-disable */ | cat - dist/tracim_frontend_lib.js > temp && mv temp dist/tracim_frontend_lib.js && echo /* eslint-enable */>> dist/tracim_frontend_lib.js",

+ 68 - 0
frontend_lib/src/component/Input/Checkbox.jsx View File

@@ -0,0 +1,68 @@
1
+import React from 'react'
2
+import PropTypes from 'prop-types'
3
+import classnames from 'classnames'
4
+
5
+const style = {
6
+  label: {
7
+    position: 'relative',
8
+    width: '18px',
9
+    height: '18px',
10
+    border: '1px solid #999',
11
+    borderRadius: '3px',
12
+    backgroundColor: '#eee',
13
+    cursor: 'pointer'
14
+  },
15
+  checked: {
16
+    position: 'absolute',
17
+    top: '-3px',
18
+    left: '2px',
19
+    fontSize: '18px',
20
+    color: '#333'
21
+  },
22
+  input: {
23
+    width: '0',
24
+    height: '0',
25
+    visibility: 'hidden'
26
+  },
27
+  disabled: {
28
+    cursor: 'default'
29
+  }
30
+}
31
+
32
+export const Checkbox = props =>
33
+  <label
34
+    className={classnames('checkboxCustom', {'checked': props.checked})}
35
+    style={{
36
+      ...style.label,
37
+      ...(props.disabled ? style.disabled : {})
38
+    }}
39
+    onClick={props.onClickCheckbox}
40
+  >
41
+    { props.checked && <div className='checboxCustom__checked' style={style.checked}>✔</div> }
42
+    <input
43
+      type='checkbox'
44
+      name={`checkbox-${props.name}`}
45
+      checked={props.checked}
46
+      defaultChecked={props.defaultChecked}
47
+      onChange={() => {}} // to remove warning
48
+      style={style.input}
49
+      disabled={props.disabled}
50
+    />
51
+  </label>
52
+
53
+Checkbox.propTypes = {
54
+  name: PropTypes.string.isRequired,
55
+  onClickCheckbox: PropTypes.func.isRequired,
56
+  checked: PropTypes.bool,
57
+  defaultChecked: PropTypes.bool,
58
+  disabled: PropTypes.bool
59
+}
60
+
61
+Checkbox.defaultProps = {
62
+  name: '',
63
+  onClickCheckbox: () => {},
64
+  checked: false,
65
+  disabled: false
66
+}
67
+
68
+export default Checkbox

+ 7 - 0
frontend_lib/src/index.dev.js View File

@@ -8,6 +8,7 @@ import PopinFixedContent from './component/PopinFixed/PopinFixedContent.jsx'
8 8
 
9 9
 import TextAreaApp from './component/Input/TextAreaApp/TextAreaApp.jsx'
10 10
 import BtnSwitch from './component/Input/BtnSwitch/BtnSwitch.jsx'
11
+import Checkbox from './component/Input/Checkbox.jsx'
11 12
 
12 13
 import Timeline from './component/Timeline/Timeline.jsx'
13 14
 import TimelineDebugData from './component/Timeline/debugData.js'
@@ -17,6 +18,7 @@ import Delimiter from './component/Delimiter/Delimiter.jsx'
17 18
 import CardPopup from './component/CardPopup/CardPopup.jsx'
18 19
 import CardPopupCreateContent from './component/CardPopup/CardPopupCreateContent.jsx'
19 20
 
21
+
20 22
 import NewVersionButton from './component/OptionComponent/NewVersionBtn.jsx'
21 23
 import ArchiveDeleteContent from './component/OptionComponent/ArchiveDeleteContent.jsx'
22 24
 
@@ -78,6 +80,11 @@ ReactDOM.render(
78 80
           <span>Here will be the app content. Style is handled by the app (obviously)</span>
79 81
           <BtnSwitch />
80 82
           {/* <TextAreaApp customClass={'randomClass'} text={'woot'} /> */}
83
+          <Checkbox
84
+            name='osef'
85
+            onClickCheckbox={() => {}}
86
+            checked
87
+          />
81 88
         </div>
82 89
 
83 90
         <Timeline

+ 6 - 8
frontend_lib/src/index.js View File

@@ -1,8 +1,7 @@
1
-import { libAddAllResourceI18n, libHandleFetchResult } from './helper.js'
2
-
3
-// fr and en are deprecated
4
-import fr from './translate/fr.js'
5
-import en from './translate/en.js'
1
+import {
2
+  libAddAllResourceI18n,
3
+  libHandleFetchResult
4
+} from './helper.js'
6 5
 
7 6
 import libPopinFixed from './component/PopinFixed/PopinFixed.jsx'
8 7
 import libPopinFixedHeader from './component/PopinFixed/PopinFixedHeader.jsx'
@@ -13,6 +12,7 @@ import libTimeline from './component/Timeline/Timeline.jsx'
13 12
 
14 13
 import libTextAreaApp from './component/Input/TextAreaApp/TextAreaApp.jsx'
15 14
 import libBtnSwitch from './component/Input/BtnSwitch/BtnSwitch.jsx'
15
+import libCheckbox from './component/Input/Checkbox.jsx'
16 16
 
17 17
 import libPageWrapper from './component/Layout/PageWrapper.jsx'
18 18
 import libPageTitle from './component/Layout/PageTitle.jsx'
@@ -27,9 +27,6 @@ import libNewVersionBtn from './component/OptionComponent/NewVersionBtn.jsx'
27 27
 import libArchiveDeleteContent from './component/OptionComponent/ArchiveDeleteContent.jsx'
28 28
 import libSelectStatus from './component/Input/SelectStatus/SelectStatus.jsx'
29 29
 
30
-export const langFr = fr
31
-export const langEn = en
32
-
33 30
 export const addAllResourceI18n = libAddAllResourceI18n
34 31
 
35 32
 export const handleFetchResult = libHandleFetchResult
@@ -43,6 +40,7 @@ export const Timeline = libTimeline
43 40
 
44 41
 export const TextAreaApp = libTextAreaApp
45 42
 export const BtnSwitch = libBtnSwitch
43
+export const Checkbox = libCheckbox
46 44
 
47 45
 export const PageWrapper = libPageWrapper
48 46
 export const PageTitle = libPageTitle

+ 45 - 28
install_frontend_dependencies.sh View File

@@ -1,51 +1,68 @@
1 1
 #!/bin/bash
2 2
 
3
+# shellcheck disable=SC1091
3 4
 . bash_library.sh # source bash_library.sh
4 5
 
5 6
 # install Tracim Lib
6 7
 
7 8
 log "cd frontend_lib"
8
-cd frontend_lib
9
-log "npm i"
10
-npm i
11
-log "sudo npm link"
12
-sudo npm link
13
-cd -
9
+(
10
+  cd frontend_lib
11
+  log "npm i"
12
+  npm i
13
+  log "sudo npm link"
14
+  sudo npm link
15
+)
14 16
 
15 17
 # install app Html Document
16 18
 
17 19
 log "cd frontend_app_html-document"
18
-cd frontend_app_html-document
19
-log "npm i"
20
-npm i
21
-log "npm link tracim_frontend_lib"
22
-npm link tracim_frontend_lib
23
-cd -
20
+(
21
+  cd frontend_app_html-document
22
+  log "npm i"
23
+  npm i
24
+  log "npm link tracim_frontend_lib"
25
+  npm link tracim_frontend_lib
26
+)
24 27
 
25 28
 # install app Thread
26 29
 
27 30
 log "cd frontend_app_thread"
28
-cd frontend_app_thread
29
-log "npm i"
30
-npm i
31
-log "npm link tracim_frontend_lib"
32
-npm link tracim_frontend_lib
33
-cd -
31
+(
32
+  cd frontend_app_thread
33
+  log "npm i"
34
+  npm i
35
+  log "npm link tracim_frontend_lib"
36
+  npm link tracim_frontend_lib
37
+)
38
+
39
+# install app Workspace
40
+
41
+log "cd frontend_app_workspace"
42
+(
43
+  cd frontend_app_workspace
44
+  log "npm i"
45
+  npm i
46
+  log "npm link tracim_frontend_lib"
47
+  npm link tracim_frontend_lib
48
+)
34 49
 
35 50
 # install app Admin Workspace User
36 51
 
37 52
 log "cd frontend_app_admin_workspace_user"
38
-cd frontend_app_admin_workspace_user
39
-log "npm i"
40
-npm i
41
-log "npm link tracim_frontend_lib"
42
-npm link tracim_frontend_lib
43
-cd -
53
+(
54
+  cd frontend_app_admin_workspace_user
55
+  log "npm i"
56
+  npm i
57
+  log "npm link tracim_frontend_lib"
58
+  npm link tracim_frontend_lib
59
+)
44 60
 
45 61
 # install Tracim Frontend
46 62
 
47 63
 log "cd frontend"
48
-cd frontend
49
-log "npm i"
50
-npm i
51
-cd -
64
+(
65
+  cd frontend
66
+  log "npm i"
67
+  npm i
68
+)