ソースを参照

Merge branch 'develop' into feature/650_folder_missing_endpoint_about_edit

inkhey 6 年 前
コミット
d92de6d920
No account linked to committer's email
共有50 個のファイルを変更した2055 個の追加492 個の削除を含む
  1. 12 0
      backend/doc/roles.md
  2. 1 1
      backend/tracim_backend/lib/core/content.py
  3. 9 10
      backend/tracim_backend/models/contents.py
  4. 2 2
      backend/tracim_backend/views/core_api/schemas.py
  5. 18 0
      build_full_frontend.sh
  6. 2 0
      frontend/dist/appInterface.js
  7. 1 0
      frontend/dist/index.html
  8. 1 0
      frontend/package.json
  9. 40 8
      frontend/src/action-creator.async.js
  10. 24 14
      frontend/src/action-creator.sync.js
  11. 2 0
      frontend/src/component/Workspace/ContentItem.jsx
  12. 3 3
      frontend/src/component/Workspace/OpenContentApp.jsx
  13. 1 1
      frontend/src/component/Workspace/OpenCreateContentApp.jsx
  14. 4 1
      frontend/src/component/common/Input/SubDropdownCreateButton.jsx
  15. 3 3
      frontend/src/container/AppFullscreenManager.jsx
  16. 178 377
      frontend/src/container/Dashboard.jsx
  17. 571 0
      frontend/src/container/Dashboard_old.jsx
  18. 38 18
      frontend/src/container/Sidebar.jsx
  19. 22 2
      frontend/src/container/Tracim.jsx
  20. 12 19
      frontend/src/container/WorkspaceContent.jsx
  21. 8 22
      frontend/src/css/Dashboard.styl
  22. 5 1
      frontend/src/css/Generic.styl
  23. 11 0
      frontend/src/helper.js
  24. 0 0
      frontend/src/reducer/appList.js
  25. 46 0
      frontend/src/reducer/currentWorkspace.js
  26. 4 3
      frontend/src/reducer/root.js
  27. 4 3
      frontend/src/reducer/workspaceContentList.js
  28. 2 3
      frontend/src/reducer/workspaceList.js
  29. 13 0
      frontend_app_workspace/.editorconfig
  30. 8 0
      frontend_app_workspace/.gitignore
  31. 3 0
      frontend_app_workspace/README.md
  32. 17 0
      frontend_app_workspace/build_workspace.sh
  33. 1 0
      frontend_app_workspace/dist/asset
  34. 1 0
      frontend_app_workspace/dist/dev
  35. 1 0
      frontend_app_workspace/dist/font
  36. 110 0
      frontend_app_workspace/dist/index.html
  37. 14 0
      frontend_app_workspace/i18next.scanner.js
  38. 6 0
      frontend_app_workspace/i18next.scanner/en/translation.json
  39. 6 0
      frontend_app_workspace/i18next.scanner/fr/translation.json
  40. 63 0
      frontend_app_workspace/package.json
  41. 14 0
      frontend_app_workspace/src/action.async.js
  42. 121 0
      frontend_app_workspace/src/container/PopupCreateWorkspace.jsx
  43. 401 0
      frontend_app_workspace/src/container/Workspace.jsx
  44. 2 0
      frontend_app_workspace/src/css/index.styl
  45. 97 0
      frontend_app_workspace/src/helper.js
  46. 21 0
      frontend_app_workspace/src/i18n.js
  47. 16 0
      frontend_app_workspace/src/index.dev.js
  48. 28 0
      frontend_app_workspace/src/index.js
  49. 87 0
      frontend_app_workspace/webpack.config.js
  50. 1 1
      frontend_lib/src/component/CardPopup/CardPopupCreateContent.jsx

+ 12 - 0
backend/doc/roles.md ファイルの表示

@@ -7,6 +7,11 @@ The other is workspace related and is called "workspace role".
7 7
 
8 8
 ## Global profile
9 9
 
10
+|                               | Normal User | Managers    | Admin          |
11
+|-------------------------------|-------------|-------------|----------------|
12
+| slug                            | users       | managers    | administrators |
13
+|-------------------------------|-------------|-------------|---------|
14
+
10 15
 
11 16
 |                               | Normal User | Managers    | Admin   |
12 17
 |-------------------------------|-------------|-------------|---------|
@@ -22,11 +27,18 @@ The other is workspace related and is called "workspace role".
22 27
 | access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
23 28
 
24 29
 
30
+
31
+
25 32
 ## Workspace Roles
26 33
 
27 34
 
28 35
 |                              | Reader | Contributor | Content Manager | Workspace Manager |
29 36
 |------------------------------|--------|-------------|-----------------|-------------------|
37
+| slug                         | reader | contributor | content-manager |  workspace-manager|
38
+|------------------------------|--------|-------------|-----------------|-------------------|
39
+
40
+|                              | Reader | Contributor | Content Manager | Workspace Manager |
41
+|------------------------------|--------|-------------|-----------------|-------------------|
30 42
 | read content                 |  yes   | yes         | yes             | yes               |
31 43
 |------------------------------|--------|-------------|-----------------|-------------------|
32 44
 | create content               |  no    | yes         | yes             | yes               |

+ 1 - 1
backend/tracim_backend/lib/core/content.py ファイルの表示

@@ -1145,7 +1145,7 @@ class ContentApi(object):
1145 1145
             content.properties = properties
1146 1146
 
1147 1147
     def set_status(self, content: Content, new_status: str):
1148
-        if new_status in CONTENT_STATUS.allowed_slugs_values():
1148
+        if new_status in CONTENT_STATUS.get_all_slugs_values():
1149 1149
             content.status = new_status
1150 1150
             content.revision_type = ActionDescription.STATUS_UPDATE
1151 1151
         else:

+ 9 - 10
backend/tracim_backend/models/contents.py ファイルの表示

@@ -85,11 +85,11 @@ class ContentStatusList(object):
85 85
                 return item
86 86
         raise ContentStatusNotExist()
87 87
 
88
-    def allowed_slugs_values(self) -> typing.List[str]:
88
+    def get_all_slugs_values(self) -> typing.List[str]:
89 89
         """ Get alls slugs"""
90 90
         return [item.slug for item in self._content_status]
91 91
 
92
-    def allowed(self) -> typing.List[ContentStatus]:
92
+    def get_all(self) -> typing.List[ContentStatus]:
93 93
         """ Get all status"""
94 94
         return [item for item in self._content_status]
95 95
 
@@ -139,7 +139,7 @@ thread_type = ContentType(
139 139
     hexcolor=thread.hexcolor,
140 140
     label='Thread',
141 141
     creation_label='Discuss about a topic',
142
-    available_statuses=CONTENT_STATUS.allowed(),
142
+    available_statuses=CONTENT_STATUS.get_all(),
143 143
 )
144 144
 
145 145
 file_type = ContentType(
@@ -148,7 +148,7 @@ file_type = ContentType(
148 148
     hexcolor=_file.hexcolor,
149 149
     label='File',
150 150
     creation_label='Upload a file',
151
-    available_statuses=CONTENT_STATUS.allowed(),
151
+    available_statuses=CONTENT_STATUS.get_all(),
152 152
 )
153 153
 
154 154
 markdownpluspage_type = ContentType(
@@ -157,7 +157,7 @@ markdownpluspage_type = ContentType(
157 157
     hexcolor=markdownpluspage.hexcolor,
158 158
     label='Rich Markdown File',
159 159
     creation_label='Create a Markdown document',
160
-    available_statuses=CONTENT_STATUS.allowed(),
160
+    available_statuses=CONTENT_STATUS.get_all(),
161 161
 )
162 162
 
163 163
 html_documents_type = ContentType(
@@ -166,7 +166,7 @@ html_documents_type = ContentType(
166 166
     hexcolor=html_documents.hexcolor,
167 167
     label='Text Document',
168 168
     creation_label='Write a document',
169
-    available_statuses=CONTENT_STATUS.allowed(),
169
+    available_statuses=CONTENT_STATUS.get_all(),
170 170
     slug_alias=['page']
171 171
 )
172 172
 
@@ -177,8 +177,7 @@ folder_type = ContentType(
177 177
     hexcolor=thread.hexcolor,
178 178
     label='Folder',
179 179
     creation_label='Create collection of any documents',
180
-    available_statuses=CONTENT_STATUS.allowed(),
181
-    allow_sub_content=True,
180
+    available_statuses=CONTENT_STATUS.get_all(),
182 181
 )
183 182
 
184 183
 
@@ -189,7 +188,7 @@ event_type = ContentType(
189 188
     hexcolor=thread.hexcolor,
190 189
     label='Event',
191 190
     creation_label='Event',
192
-    available_statuses=CONTENT_STATUS.allowed(),
191
+    available_statuses=CONTENT_STATUS.get_all(),
193 192
 )
194 193
 
195 194
 # TODO - G.M - 31-05-2018 - Set Better Event params
@@ -199,7 +198,7 @@ comment_type = ContentType(
199 198
     hexcolor=thread.hexcolor,
200 199
     label='Comment',
201 200
     creation_label='Comment',
202
-    available_statuses=CONTENT_STATUS.allowed(),
201
+    available_statuses=CONTENT_STATUS.get_all(),
203 202
 )
204 203
 
205 204
 

+ 2 - 2
backend/tracim_backend/views/core_api/schemas.py ファイルの表示

@@ -697,7 +697,7 @@ class ContentDigestSchema(marshmallow.Schema):
697 697
     )
698 698
     status = marshmallow.fields.Str(
699 699
         example='closed-deprecated',
700
-        validate=OneOf(CONTENT_STATUS.allowed_slugs_values()),
700
+        validate=OneOf(CONTENT_STATUS.get_all_slugs_values()),
701 701
         description='this slug is found in content_type available statuses',
702 702
         default=open_status
703 703
     )
@@ -858,7 +858,7 @@ class FileContentModifySchema(TextBasedContentModifySchema):
858 858
 class SetContentStatusSchema(marshmallow.Schema):
859 859
     status = marshmallow.fields.Str(
860 860
         example='closed-deprecated',
861
-        validate=OneOf(CONTENT_STATUS.allowed_slugs_values()),
861
+        validate=OneOf(CONTENT_STATUS.get_all_slugs_values()),
862 862
         description='this slug is found in content_type available statuses',
863 863
         default=open_status,
864 864
         required=True,

+ 18 - 0
build_full_frontend.sh ファイルの表示

@@ -53,6 +53,24 @@ log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/thread_fr_trans
53 53
 cp i18next.scanner/fr/translation.json ../frontend/dist/app/thread_fr_translation.json
54 54
 cd -
55 55
 
56
+# app Workspace
57
+
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
63
+
64
+log "cp dist/workspace.app.js"
65
+cp dist/workspace.app.js ../frontend/dist/app
66
+
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 -
73
+
56 74
 # app Admin Workspace User
57 75
 
58 76
 log "cd frontend_app_admin_workspace_user"

+ 2 - 0
frontend/dist/appInterface.js ファイルの表示

@@ -3,6 +3,8 @@
3 3
 
4 4
   getSelectedApp = name => {
5 5
     switch (name) {
6
+      case 'workspace':
7
+        return appWorkspace
6 8
       case 'html-document':
7 9
         return appHtmlDocument
8 10
       case 'thread':

+ 1 - 0
frontend/dist/index.html ファイルの表示

@@ -51,6 +51,7 @@
51 51
     <script src='/tracim.vendor.bundle.js'></script>
52 52
     <script src='/tracim.app.entry.js'></script>
53 53
 
54
+    <script src='/app/workspace.app.js'></script>
54 55
     <script src='/app/html-document.app.js'></script>
55 56
     <script src='/app/thread.app.js'></script>
56 57
     <!-- <script src='/app/file.app.js'></script> -->

+ 1 - 0
frontend/package.json ファイルの表示

@@ -32,6 +32,7 @@
32 32
     "js-cookie": "^2.2.0",
33 33
     "prop-types": "^15.6.0",
34 34
     "query-string": "^6.1.0",
35
+    "radium": "^0.24.1",
35 36
     "react": "^16.0.0",
36 37
     "react-animate-height": "^0.10.10",
37 38
     "react-dom": "^16.0.0",

+ 40 - 8
frontend/src/action-creator.async.js ファイルの表示

@@ -11,6 +11,8 @@ import {
11 11
   setUserRole,
12 12
   WORKSPACE,
13 13
   WORKSPACE_LIST,
14
+  WORKSPACE_DETAIL,
15
+  WORKSPACE_MEMBER_LIST,
14 16
   FOLDER,
15 17
   setFolderData,
16 18
   APP_LIST,
@@ -57,8 +59,20 @@ const fetchWrapper = async ({url, param, actionName, dispatch, debug = false}) =
57 59
   })()
58 60
   if (debug) console.log(`fetch ${param.method}/${actionName} result: `, fetchResult)
59 61
 
60
-  if ([200, 204, 304].includes(fetchResult.status)) dispatch({type: `${param.method}/${actionName}/SUCCESS`, data: fetchResult.json})
61
-  else if ([400, 404, 500].includes(fetchResult.status)) dispatch({type: `${param.method}/${actionName}/FAILED`, data: fetchResult.json})
62
+  // if ([200, 204, 304].includes(fetchResult.status)) dispatch({type: `${param.method}/${actionName}/SUCCESS`, data: fetchResult.json})
63
+  // else if ([400, 404, 500].includes(fetchResult.status)) dispatch({type: `${param.method}/${actionName}/FAILED`, data: fetchResult.json})
64
+  switch (fetchResult.status) {
65
+    case 200:
66
+    case 204:
67
+    case 304:
68
+      dispatch({type: `${param.method}/${actionName}/SUCCESS`, data: fetchResult.json})
69
+      break
70
+    case 400:
71
+    case 404:
72
+    case 500:
73
+      dispatch({type: `${param.method}/${actionName}/FAILED`, data: fetchResult.json})
74
+      break
75
+  }
62 76
 
63 77
   return fetchResult
64 78
 }
@@ -161,9 +175,9 @@ export const getWorkspaceList = user => dispatch => {
161 175
   })
162 176
 }
163 177
 
164
-export const getWorkspaceContentList = (user, idWorkspace, idParent) => dispatch => {
178
+export const getWorkspaceDetail = (user, idWorkspace) => dispatch => {
165 179
   return fetchWrapper({
166
-    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents?parent_id=${idParent}`,
180
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}`,
167 181
     param: {
168 182
       headers: {
169 183
         ...FETCH_CONFIG.headers,
@@ -171,16 +185,34 @@ export const getWorkspaceContentList = (user, idWorkspace, idParent) => dispatch
171 185
       },
172 186
       method: 'GET'
173 187
     },
174
-    actionName: WORKSPACE,
188
+    actionName: WORKSPACE_DETAIL,
175 189
     dispatch
176 190
   })
177 191
 }
178 192
 
179
-export const getWorkspaceContent = (idWorkspace, idContent) => dispatch => {
193
+export const getWorkspaceMemberList = (user, idWorkspace) => dispatch => {
180 194
   return fetchWrapper({
181
-    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents/${idContent}`,
195
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/members`,
182 196
     param: {
183
-      headers: {...FETCH_CONFIG.headers},
197
+      headers: {
198
+        ...FETCH_CONFIG.headers,
199
+        'Authorization': 'Basic ' + user.auth
200
+      },
201
+      method: 'GET'
202
+    },
203
+    actionName: WORKSPACE_MEMBER_LIST,
204
+    dispatch
205
+  })
206
+}
207
+
208
+export const getWorkspaceContentList = (user, idWorkspace, idParent) => dispatch => {
209
+  return fetchWrapper({
210
+    url: `${FETCH_CONFIG.apiUrl}/workspaces/${idWorkspace}/contents?parent_id=${idParent}`,
211
+    param: {
212
+      headers: {
213
+        ...FETCH_CONFIG.headers,
214
+        'Authorization': 'Basic ' + user.auth
215
+      },
184 216
       method: 'GET'
185 217
     },
186 218
     actionName: WORKSPACE,

+ 24 - 14
frontend/src/action-creator.sync.js ファイルの表示

@@ -15,13 +15,13 @@ export const addFlashMessage = msg => ({ type: `${ADD}/${FLASH_MESSAGE}`, msg })
15 15
 export const removeFlashMessage = msg => ({ type: `${REMOVE}/${FLASH_MESSAGE}`, msg })
16 16
 
17 17
 export const USER = 'User'
18
-export const USER_LOGIN = 'User/Login'
19
-export const USER_LOGOUT = 'User/Logout'
20
-export const USER_DATA = 'User/Data'
21
-export const USER_ROLE = 'User/Role'
22
-export const USER_CONNECTED = 'User/Connected'
23
-export const USER_DISCONNECTED = 'User/Disconnected'
24
-export const USER_LANG = 'User/Lang'
18
+export const USER_LOGIN = `${USER}/Login`
19
+export const USER_LOGOUT = `${USER}/Logout`
20
+export const USER_DATA = `${USER}/Data`
21
+export const USER_ROLE = `${USER}/Role`
22
+export const USER_CONNECTED = `${USER}/Connected`
23
+export const USER_DISCONNECTED = `${USER}/Disconnected`
24
+export const USER_LANG = `${USER}/Lang`
25 25
 export const setUserConnected = user => ({ type: `${SET}/${USER}/Connected`, user })
26 26
 export const setUserDisconnected = () => ({ type: `${SET}/${USER}/Disconnected` })
27 27
 export const updateUserData = userData => ({ type: `${UPDATE}/${USER}/Data`, data: userData })
@@ -31,20 +31,30 @@ export const updateUserWorkspaceSubscriptionNotif = (workspaceId, subscriptionNo
31 31
   ({ type: `${UPDATE}/${USER_ROLE}/SubscriptionNotif`, workspaceId, subscriptionNotif })
32 32
 
33 33
 export const WORKSPACE = 'Workspace'
34
-export const setWorkspaceContent = (workspaceContent, filterStr = '') => ({ type: `${SET}/${WORKSPACE}/Content`, workspaceContent, filterStr })
34
+export const WORKSPACE_CONTENT = `${WORKSPACE}/Content`
35
+export const setWorkspaceContentList = (workspaceContentList, filterStr = '') => ({ type: `${SET}/${WORKSPACE_CONTENT}`, workspaceContentList, filterStr })
35 36
 export const updateWorkspaceFilter = filterList => ({ type: `${UPDATE}/${WORKSPACE}/Filter`, filterList })
36 37
 
37
-export const FOLDER = 'Folder'
38
-export const setFolderData = (folderId, content) => ({ type: `${SET}/${WORKSPACE}/${FOLDER}/Content`, folderId, content })
39
-
40
-export const WORKSPACE_LIST = 'WorkspaceList'
38
+export const WORKSPACE_LIST = `${WORKSPACE}/List`
41 39
 export const updateWorkspaceListData = workspaceList => ({ type: `${UPDATE}/${WORKSPACE_LIST}`, workspaceList })
42 40
 export const setWorkspaceListIsOpenInSidebar = (workspaceId, isOpenInSidebar) => ({ type: `${SET}/${WORKSPACE_LIST}/isOpenInSidebar`, workspaceId, isOpenInSidebar })
43 41
 
44
-export const APP_LIST = 'App/List'
42
+export const WORKSPACE_DETAIL = `${WORKSPACE}/Detail`
43
+export const setWorkspaceDetail = workspaceDetail => ({ type: `${SET}/${WORKSPACE_DETAIL}`, workspaceDetail })
44
+
45
+export const WORKSPACE_MEMBER = `${WORKSPACE}/Member`
46
+export const WORKSPACE_MEMBER_LIST = `${WORKSPACE_MEMBER}/List`
47
+export const setWorkspaceMemberList = workspaceMemberList => ({ type: `${SET}/${WORKSPACE_MEMBER_LIST}`, workspaceMemberList })
48
+
49
+export const FOLDER = 'Folder'
50
+export const setFolderData = (folderId, content) => ({ type: `${SET}/${WORKSPACE}/${FOLDER}/Content`, folderId, content })
51
+
52
+export const APP = 'App'
53
+export const APP_LIST = `${APP}/List`
45 54
 export const setAppList = appList => ({ type: `${SET}/${APP_LIST}`, appList })
46 55
 
47
-export const CONTENT_TYPE_LIST = 'ContentType/List'
56
+export const CONTENT_TYPE = 'ContentType'
57
+export const CONTENT_TYPE_LIST = `${CONTENT_TYPE}/List`
48 58
 export const setContentTypeList = contentTypeList => ({ type: `${SET}/${CONTENT_TYPE_LIST}`, contentTypeList })
49 59
 
50 60
 export const LANG = 'Lang'

+ 2 - 0
frontend/src/component/Workspace/ContentItem.jsx ファイルの表示

@@ -4,6 +4,8 @@ import classnames from 'classnames'
4 4
 import BtnExtandedAction from './BtnExtandedAction.jsx'
5 5
 
6 6
 const ContentItem = props => {
7
+  if (props.contentType === null) return null // this means the endpoint system/content_type hasn't responded yet
8
+
7 9
   const status = props.contentType.availableStatuses.find(s => s.slug === props.statusSlug)
8 10
   return (
9 11
     <div className={classnames('content', 'align-items-center', {'item-last': props.isLast}, props.customClass)} onClick={props.onClickItem}>

+ 3 - 3
frontend/src/component/Workspace/OpenContentApp.jsx ファイルの表示

@@ -6,11 +6,11 @@ import appFactory from '../../appFactory.js'
6 6
 // @FIXME Côme - 2018/07/31 - should this be in a component like AppFeatureManager ?
7 7
 export class OpenContentApp extends React.Component {
8 8
   openContentApp = () => {
9
-    const { idWorkspace, appOpenedType, user, workspaceContent, contentType, renderAppFeature, dispatchCustomEvent, match } = this.props
9
+    const { idWorkspace, appOpenedType, user, workspaceContentList, contentType, renderAppFeature, dispatchCustomEvent, match } = this.props
10 10
 
11 11
     if (isNaN(idWorkspace) || idWorkspace === -1) return
12 12
 
13
-    if (['type', 'idcts'].every(p => p in match.params) && match.params.type !== 'contents' && workspaceContent.length) {
13
+    if (['type', 'idcts'].every(p => p in match.params) && match.params.type !== 'contents' && workspaceContentList.length) {
14 14
       if (isNaN(match.params.idcts) || !contentType.map(c => c.slug).includes(match.params.type)) return
15 15
 
16 16
       const contentToOpen = {
@@ -57,5 +57,5 @@ export class OpenContentApp extends React.Component {
57 57
   }
58 58
 }
59 59
 
60
-const mapStateToProps = ({ user, workspaceContent, contentType }) => ({ user, workspaceContent, contentType })
60
+const mapStateToProps = ({ user, workspaceContentList, contentType }) => ({ user, workspaceContentList, contentType })
61 61
 export default withRouter(connect(mapStateToProps)(appFactory(OpenContentApp)))

+ 1 - 1
frontend/src/component/Workspace/OpenCreateContentApp.jsx ファイルの表示

@@ -39,5 +39,5 @@ export class OpenCreateContentApp extends React.Component {
39 39
   }
40 40
 }
41 41
 
42
-const mapStateToProps = ({ user, workspaceContent, contentType }) => ({ user, workspaceContent, contentType })
42
+const mapStateToProps = ({ user, contentType }) => ({ user, contentType })
43 43
 export default withRouter(connect(mapStateToProps)(appFactory(OpenCreateContentApp)))

+ 4 - 1
frontend/src/component/common/Input/SubDropdownCreateButton.jsx ファイルの表示

@@ -10,7 +10,10 @@ const SubDropdownCreateButton = props => {
10 10
         <div className='subdropdown__link dropdown-item' onClick={e => props.onClickCreateContent(e, props.idFolder, app.slug)} key={app.slug}>
11 11
           <div className={`subdropdown__link__${app.slug} d-flex align-items-center`}>
12 12
             <div className={`subdropdown__link__${app.slug}__icon mr-3`}>
13
-              <i className={`fa fa-fw fa-${app.faIcon}`} />
13
+              <i
14
+                className={`fa fa-fw fa-${app.faIcon}`}
15
+                style={{color: app.hexcolor}}
16
+              />
14 17
             </div>
15 18
             <div className='subdropdown__link__folder__text'>
16 19
               {app.creationLabel}

+ 3 - 3
frontend/src/container/AppFullscreenManager.jsx ファイルの表示

@@ -17,7 +17,7 @@ class AppFullscreenManager extends React.Component {
17 17
   componentDidMount = () => this.setState({AmIMounted: true})
18 18
 
19 19
   render () {
20
-    const { user, renderAppFullscreen } = this.props
20
+    const { props } = this
21 21
 
22 22
     return (
23 23
       <div className='sidebarpagecontainer'>
@@ -28,12 +28,12 @@ class AppFullscreenManager extends React.Component {
28 28
         {this.state.AmIMounted && (// we must wait for the component to be fully mounted to be sure the div#appFullscreenContainer exists in DOM
29 29
           <div className='emptyDiForRoute'>
30 30
             <Route path={PAGE.ADMIN.WORKSPACE} render={() => {
31
-              renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'workspace'}, user, {})
31
+              props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'workspace'}, props.user, {})
32 32
               return null
33 33
             }} />
34 34
 
35 35
             <Route path={PAGE.ADMIN.USER} render={() => {
36
-              renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'user'}, user, {})
36
+              props.renderAppFullscreen({slug: 'admin_workspace_user', hexcolor: '#7d4e24', type: 'user'}, props.user, {})
37 37
               return null
38 38
             }} />
39 39
           </div>

+ 178 - 377
frontend/src/container/Dashboard.jsx ファイルの表示

@@ -2,15 +2,23 @@ import React from 'react'
2 2
 import { connect } from 'react-redux'
3 3
 import Sidebar from './Sidebar.jsx'
4 4
 import imgProfil from '../img/imgProfil.png'
5
+import { translate } from 'react-i18next'
6
+import Radium from 'radium'
7
+import color from 'color'
8
+import {
9
+  PageWrapper,
10
+  PageTitle,
11
+  PageContent
12
+} from 'tracim_frontend_lib'
5 13
 import {
6
-  getAppList,
7
-  getContentTypeList, getWorkspaceList
14
+  getWorkspaceDetail,
15
+  getWorkspaceMemberList
8 16
 } from '../action-creator.async.js'
9 17
 import {
10
-  setAppList,
11
-  setContentTypeList, setWorkspaceListIsOpenInSidebar, updateWorkspaceListData
18
+  addFlashMessage,
19
+  setWorkspaceDetail,
20
+  setWorkspaceMemberList
12 21
 } from '../action-creator.sync.js'
13
-import { translate } from 'react-i18next'
14 22
 
15 23
 class Dashboard extends React.Component {
16 24
   constructor (props) {
@@ -25,28 +33,28 @@ class Dashboard extends React.Component {
25 33
   }
26 34
 
27 35
   async componentDidMount () {
28
-    const { workspaceIdInUrl } = this.state
29
-    const { user, workspaceList, app, contentType, dispatch } = this.props
30
-
31
-    console.log('<Dashboard> componentDidMount')
32
-
33
-    if (app.length === 0) {
34
-      const fetchGetAppList = await dispatch(getAppList(user))
35
-      if (fetchGetAppList.status === 200) dispatch(setAppList(fetchGetAppList.json))
36
-    }
37
-
38
-    if (contentType.length === 0) {
39
-      const fetchGetContentTypeList = await dispatch(getContentTypeList(user))
40
-      if (fetchGetContentTypeList.status === 200) dispatch(setContentTypeList(fetchGetContentTypeList.json))
36
+    const { props, state } = this
37
+
38
+    const fetchWorkspaceDetail = await props.dispatch(getWorkspaceDetail(props.user, state.workspaceIdInUrl))
39
+    switch (fetchWorkspaceDetail.status) {
40
+      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
41 47
     }
42 48
 
43
-    if (user.user_id !== -1 && workspaceList.length === 0) {
44
-      const fetchGetWorkspaceList = await dispatch(getWorkspaceList(user))
45
-
46
-      if (fetchGetWorkspaceList.status === 200) {
47
-        dispatch(updateWorkspaceListData(fetchGetWorkspaceList.json))
48
-        dispatch(setWorkspaceListIsOpenInSidebar(workspaceIdInUrl || fetchGetWorkspaceList.json[0].workspace_id, true))
49
-      }
49
+    const fetchWorkspaceMemberList = await props.dispatch(getWorkspaceMemberList(props.user, state.workspaceIdInUrl))
50
+    switch (fetchWorkspaceMemberList.status) {
51
+      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
50 58
     }
51 59
   }
52 60
 
@@ -67,152 +75,118 @@ class Dashboard extends React.Component {
67 75
   }))
68 76
 
69 77
   render () {
78
+    const { props, state } = this
79
+
70 80
     return (
71 81
       <div className='sidebarpagecontainer'>
72 82
         <Sidebar />
73 83
 
74
-        <div className='dashboard'>
75
-          <div className='container-fluid nopadding'>
76
-            <div className='dashboard__header mb-5'>
77
-              <div className='pageTitleGeneric dashboard__header__title d-flex align-items-center'>
78
-                <div className='pageTitleGeneric__title dashboard__header__title__text mr-3'>
79
-                  {this.props.t('Dashboard')}
80
-                </div>
81
-                <div className='dashboard__header__acces' />
82
-              </div>
83
-              <div className='dashboard__header__advancedmode mr-3'>
84
-                <button type='button' className='btn btn-primary'>
85
-                  {this.props.t('Active advanced Dashboard')}
86
-                </button>
87
-              </div>
84
+        <PageWrapper customeClass='dashboard'>
85
+          <PageTitle
86
+            parentClass='dashboard__header'
87
+            title={props.t('Dashboard')}
88
+            subtitle={''}
89
+          >
90
+            <div className='dashboard__header__advancedmode mr-3'>
91
+              <button type='button' className='btn btn-primary'>
92
+                {props.t('Active advanced Dashboard')}
93
+              </button>
88 94
             </div>
95
+          </PageTitle>
89 96
 
90
-            <div className='dashboard__wkswrapper'>
97
+          <PageContent>
98
+            <div className='dashboard__workspace-wrapper'>
91 99
               <div className='dashboard__workspace'>
92 100
                 <div className='dashboard__workspace__title'>
93
-                  Développement tracim
101
+                  {props.curWs.label}
94 102
                 </div>
95 103
 
96 104
                 <div className='dashboard__workspace__detail'>
97
-                  Ligne directive pour le prochain design de Tracim et des futurs fonctionnalités.
105
+                  {props.curWs.description}
98 106
                 </div>
99 107
               </div>
100
-              <div className='dashboard__userstatut'>
101 108
 
109
+              <div className='dashboard__userstatut'>
102 110
                 <div className='dashboard__userstatut__role'>
103
-                  <div className='dashboard__userstatut__role__text'>
104
-                    Hi ! Alexi, vous êtes actuellement
111
+                  <div className='dashboard__userstatut__role__msg'>
112
+                    {props.t(`Hi ! ${props.user.public_name}, vous êtes actuellement`)}
105 113
                   </div>
106
-                  <div className='dashboard__userstatut__role__rank'>
107
-                    <div className='dashboard__userstatut__role__rank__icon'>
114
+
115
+                  <div className='dashboard__userstatut__role__definition'>
116
+                    <div className='dashboard__userstatut__role__definition__icon'>
108 117
                       <i className='fa fa-graduation-cap' />
109 118
                     </div>
110
-                    <div className='dashboard__userstatut__role__rank__rolename'>
111
-                      Gestionnaire de projet
119
+
120
+                    <div className='dashboard__userstatut__role__definition__text'>
121
+                      {(member => member ? member.role : '')(props.curWs.member.find(m => m.id === props.user.user_id))}
112 122
                     </div>
113 123
                   </div>
114 124
                 </div>
115 125
 
116 126
                 <div className='dashboard__userstatut__notification'>
117 127
                   <div className='dashboard__userstatut__notification__text'>
118
-                    Vous êtes abonné(e) aux notifications de ce workspace
128
+                    {props.t("You have subscribed to this workspace's notifications")} (nyi)
119 129
                   </div>
120
-                  {this.state.displayNotifBtn === false &&
121
-                    <div
122
-                      className='dashboard__userstatut__notification__btn btn btn-outline-primary'
123
-                      onClick={this.handleToggleNotifBtn}
124
-                    >
125
-                      {this.props.t('Change your status')}
126
-                    </div>
127
-                  }
128 130
 
129
-                  {this.state.displayNotifBtn === true &&
130
-                    <div className='dashboard__userstatut__notification__subscribe dropdown'>
131
-                      <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'>
132
-                        Abonné(e)
133
-                      </button>
134
-                      <div className='dashboard__userstatut__notification__subscribe__submenu dropdown-menu'>
135
-                        <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item'>
136
-                          {this.props.t('subscriber')}
137
-                        </div>
138
-                        <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item dropdown-item'>
139
-                          {this.props.t('unsubscribed')}
131
+                  {state.displayNotifBtn
132
+                    ? (
133
+                      <div className='dashboard__userstatut__notification__subscribe dropdown'>
134
+                        <button
135
+                          className='dashboard__userstatut__notification__subscribe__btn btn btn-outline-primary dropdown-toggle'
136
+                          type='button'
137
+                          id='dropdownMenuButton'
138
+                          data-toggle='dropdown'
139
+                          aria-haspopup='true'
140
+                          aria-expanded='false'
141
+                        >
142
+                          Abonné(e)
143
+                        </button>
144
+
145
+                        <div className='dashboard__userstatut__notification__subscribe__submenu dropdown-menu'>
146
+                          <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item'>
147
+                            {props.t('subscriber')}
148
+                          </div>
149
+                          <div className='dashboard__userstatut__notification__subscribe__submenu__item dropdown-item dropdown-item'>
150
+                            {props.t('unsubscribed')}
151
+                          </div>
140 152
                         </div>
141 153
                       </div>
142
-                    </div>
154
+                    )
155
+                    : (
156
+                      <div
157
+                        className='dashboard__userstatut__notification__btn btn btn-outline-primary'
158
+                        onClick={this.handleToggleNotifBtn}
159
+                      >
160
+                        {props.t('Change your status')}
161
+                      </div>
162
+                    )
143 163
                   }
144 164
                 </div>
145 165
               </div>
146 166
             </div>
147 167
 
148 168
             <div className='dashboard__calltoaction justify-content-xl-center'>
149
-              <div className='dashboard__calltoaction__button btnaction btnthread'>
150
-                <div className='dashboard__calltoaction__button__text'>
151
-                  <div className='dashboard__calltoaction__button__text__icon'>
152
-                    <i className='fa fa-comments-o' />
153
-                  </div>
154
-                  <div className='dashboard__calltoaction__button__text__title'>
155
-                    {this.props.t('Start a new Thread')}
156
-                  </div>
157
-                </div>
158
-              </div>
159
-
160
-              <div className='dashboard__calltoaction__button btnaction writefile'>
161
-                <div className='dashboard__calltoaction__button__text'>
162
-                  <div className='dashboard__calltoaction__button__text__icon'>
163
-                    <i className='fa fa-file-text-o' />
164
-                  </div>
165
-                  <div className='dashboard__calltoaction__button__text__title'>
166
-                    {this.props.t('Writing a document')}
167
-                  </div>
168
-                </div>
169
-              </div>
170
-
171
-              <div className='dashboard__calltoaction__button btnaction importfile'>
172
-                <div className='dashboard__calltoaction__button__text'>
173
-                  <div className='dashboard__calltoaction__button__text__icon'>
174
-                    <i className='fa fa-paperclip' />
175
-                  </div>
176
-                  <div className='dashboard__calltoaction__button__text__title'>
177
-                    {this.props.t('Upload a file')}
178
-                  </div>
179
-                </div>
180
-              </div>
181
-
182
-              {/*
183
-                <div className='dashboard__calltoaction__button btnaction visioconf'>
184
-                  <div className='dashboard__calltoaction__button__text'>
185
-                    <div className='dashboard__calltoaction__button__text__icon'>
186
-                      <i className='fa fa-video-camera' />
187
-                    </div>
188
-                    <div className='dashboard__calltoaction__button__text__title'>
189
-                      {this.props.t('Start a videoconference')}
190
-                    </div>
191
-                  </div>
192
-                </div>
193
-
194
-                <div className='dashboard__calltoaction__button btnaction calendar'>
169
+              {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
+                  }}
178
+                  key={ct.label}
179
+                >
195 180
                   <div className='dashboard__calltoaction__button__text'>
196 181
                     <div className='dashboard__calltoaction__button__text__icon'>
197
-                      <i className='fa fa-calendar' />
182
+                      <i className={`fa fa-${ct.faIcon}`} />
198 183
                     </div>
199 184
                     <div className='dashboard__calltoaction__button__text__title'>
200
-                      {this.props.t('View the Calendar')}
185
+                      {ct.creationLabel}
201 186
                     </div>
202 187
                   </div>
203 188
                 </div>
204
-              */ }
205
-
206
-              <div className='dashboard__calltoaction__button btnaction explore'>
207
-                <div className='dashboard__calltoaction__button__text'>
208
-                  <div className='dashboard__calltoaction__button__text__icon'>
209
-                    <i className='fa fa-folder-open-o' />
210
-                  </div>
211
-                  <div className='dashboard__calltoaction__button__text__title'>
212
-                    {this.props.t('Explore the workspace')}
213
-                  </div>
214
-                </div>
215
-              </div>
189
+              )}
216 190
             </div>
217 191
 
218 192
             <div className='dashboard__wksinfo'>
@@ -227,57 +201,13 @@ class Dashboard extends React.Component {
227 201
                   </div>
228 202
                 </div>
229 203
                 <div className='dashboard__activity__wrapper'>
230
-                  <div className='dashboard__activity__workspace'>
231
-                    <div className='dashboard__activity__workspace__icon'>
232
-                      <i className='fa fa-comments-o' />
233
-                    </div>
234
-                    <div className='dashboard__activity__workspace__name'>
235
-                      <span>Développement Tracim</span>
236
-                    </div>
237
-                  </div>
238
-
239
-                  <div className='dashboard__activity__workspace'>
240
-                    <div className='dashboard__activity__workspace__icon'>
241
-                      <i className='fa fa-list-ul' />
242
-                    </div>
243
-                    <div className='dashboard__activity__workspace__name'>
244
-                      Mission externe
245
-                    </div>
246
-                  </div>
247
-
248
-                  <div className='dashboard__activity__workspace'>
249
-                    <div className='dashboard__activity__workspace__icon'>
250
-                      <i className='fa fa-list-ul' />
251
-                    </div>
252
-                    <div className='dashboard__activity__workspace__name'>
253
-                      Recherche et developpement
254
-                    </div>
255
-                  </div>
256
-
257
-                  <div className='dashboard__activity__workspace'>
258
-                    <div className='dashboard__activity__workspace__icon'>
259
-                      <i className='fa fa-file-text-o' />
260
-                    </div>
261
-                    <div className='dashboard__activity__workspace__name'>
262
-                      <span>Marketing</span>
263
-                    </div>
264
-                  </div>
265 204
 
266 205
                   <div className='dashboard__activity__workspace'>
267 206
                     <div className='dashboard__activity__workspace__icon'>
268 207
                       <i className='fa fa-comments-o' />
269 208
                     </div>
270 209
                     <div className='dashboard__activity__workspace__name'>
271
-                      <span>Évolution</span>
272
-                    </div>
273
-                  </div>
274
-
275
-                  <div className='dashboard__activity__workspace'>
276
-                    <div className='dashboard__activity__workspace__icon'>
277
-                      <i className='fa fa-file-text-o' />
278
-                    </div>
279
-                    <div className='dashboard__activity__workspace__name'>
280
-                      Commercialisation
210
+                      <span>Développement Tracim</span>
281 211
                     </div>
282 212
                   </div>
283 213
 
@@ -299,6 +229,7 @@ class Dashboard extends React.Component {
299 229
                   {this.state.displayNewMemberDashboard === false &&
300 230
                     <div>
301 231
                       <ul className='dashboard__memberlist__list'>
232
+
302 233
                         <li className='dashboard__memberlist__list__item'>
303 234
                           <div className='dashboard__memberlist__list__item__avatar'>
304 235
                             <img src={imgProfil} alt='avatar' />
@@ -316,107 +247,6 @@ class Dashboard extends React.Component {
316 247
                           </div>
317 248
                         </li>
318 249
 
319
-                        <li className='dashboard__memberlist__list__item'>
320
-                          <div className='dashboard__memberlist__list__item__avatar'>
321
-                            <img src={imgProfil} alt='avatar' />
322
-                          </div>
323
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
324
-                            <div className='dashboard__memberlist__list__item__info__name'>
325
-                              Aldwin Vinel
326
-                            </div>
327
-                            <div className='dashboard__memberlist__list__item__info__role'>
328
-                              Lecteur
329
-                            </div>
330
-                          </div>
331
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
332
-                            <i className='fa fa-trash-o' />
333
-                          </div>
334
-                        </li>
335
-
336
-                        <li className='dashboard__memberlist__list__item'>
337
-                          <div className='dashboard__memberlist__list__item__avatar'>
338
-                            <img src={imgProfil} alt='avatar' />
339
-                          </div>
340
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
341
-                            <div className='dashboard__memberlist__list__item__info__name'>
342
-                              William Himme
343
-                            </div>
344
-                            <div className='dashboard__memberlist__list__item__info__role'>
345
-                              Contributeur
346
-                            </div>
347
-                          </div>
348
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
349
-                            <i className='fa fa-trash-o' />
350
-                          </div>
351
-                        </li>
352
-
353
-                        <li className='dashboard__memberlist__list__item'>
354
-                          <div className='dashboard__memberlist__list__item__avatar'>
355
-                            <img src={imgProfil} alt='avatar' />
356
-                          </div>
357
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
358
-                            <div className='dashboard__memberlist__list__item__info__name'>
359
-                              Yacine Lite
360
-                            </div>
361
-                            <div className='dashboard__memberlist__list__item__info__role'>
362
-                              Contributeur
363
-                            </div>
364
-                          </div>
365
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
366
-                            <i className='fa fa-trash-o' />
367
-                          </div>
368
-                        </li>
369
-
370
-                        <li className='dashboard__memberlist__list__item'>
371
-                          <div className='dashboard__memberlist__list__item__avatar'>
372
-                            <img src={imgProfil} alt='avatar' />
373
-                          </div>
374
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
375
-                            <div className='dashboard__memberlist__list__item__info__name'>
376
-                              Alexi Falcin
377
-                            </div>
378
-                            <div className='dashboard__memberlist__list__item__info__role'>
379
-                              Gestionnaire
380
-                            </div>
381
-                          </div>
382
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
383
-                            <i className='fa fa-trash-o' />
384
-                          </div>
385
-                        </li>
386
-
387
-                        <li className='dashboard__memberlist__list__item'>
388
-                          <div className='dashboard__memberlist__list__item__avatar'>
389
-                            <img src={imgProfil} alt='avatar' />
390
-                          </div>
391
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
392
-                            <div className='dashboard__memberlist__list__item__info__name'>
393
-                              Mickaël Fonati
394
-                            </div>
395
-                            <div className='dashboard__memberlist__list__item__info__role'>
396
-                              Gestionnaire
397
-                            </div>
398
-                          </div>
399
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
400
-                            <i className='fa fa-trash-o' />
401
-                          </div>
402
-                        </li>
403
-
404
-                        <li className='dashboard__memberlist__list__item'>
405
-                          <div className='dashboard__memberlist__list__item__avatar'>
406
-                            <img src={imgProfil} alt='avatar' />
407
-                          </div>
408
-                          <div className='dashboard__memberlist__list__item__info mr-auto'>
409
-                            <div className='dashboard__memberlist__list__item__info__name'>
410
-                              Eva Lonbard
411
-                            </div>
412
-                            <div className='dashboard__memberlist__list__item__info__role'>
413
-                              Gestionnaire
414
-                            </div>
415
-                          </div>
416
-                          <div className='dashboard__memberlist__list__item__delete d-flex justify-content-end'>
417
-                            <i className='fa fa-trash-o' />
418
-                          </div>
419
-                        </li>
420 250
                       </ul>
421 251
 
422 252
                       <div
@@ -440,93 +270,64 @@ class Dashboard extends React.Component {
440 270
                   }
441 271
 
442 272
                   {this.state.displayNewMemberDashboard === true &&
443
-                    <form className='dashboard__memberlist__form'>
444
-                      <div
445
-                        className='dashboard__memberlist__form__close d-flex justify-content-end'
446
-                      >
447
-                        <i className='fa fa-times' onClick={this.handleToggleNewMemberDashboard} />
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' />
448 286
                       </div>
449
-                      <div className='dashboard__memberlist__form__member'>
450
-                        <div className='dashboard__memberlist__form__member__name'>
451
-                          <label className='name__label' htmlFor='addmember'>
452
-                            {this.props.t('Enter the name or email of the member')}
453
-                          </label>
454
-                          <input type='text' id='addmember' className='name__input form-control' placeholder='Nom ou Email' />
287
+
288
+                      <div className='dashboard__memberlist__form__member__create'>
289
+                        <div className='create__radiobtn mr-3'>
290
+                          <input type='radio' />
455 291
                         </div>
456
-                        <div className='dashboard__memberlist__form__member__create'>
457
-                          <div className='create__radiobtn mr-3'>
458
-                            <input type='radio' />
459
-                          </div>
460
-                          <div className='create__text'>
461
-                            {this.props.t('Create an account')}
462
-                          </div>
292
+
293
+                        <div className='create__text'>
294
+                          {this.props.t('Create an account')}
463 295
                         </div>
464 296
                       </div>
465
-                      <div className='dashboard__memberlist__form__role'>
466
-                        <div className='dashboard__memberlist__form__role__text'>
467
-                          {this.props.t('Choose the role of the member')}
468
-                        </div>
469
-                        <ul className='dashboard__memberlist__form__role__list'>
470
-                          <li className='dashboard__memberlist__form__role__list__item'>
471
-                            <div className='item__radiobtn mr-3'>
472
-                              <input type='radio' name='role' value='responsable' />
473
-                            </div>
474
-                            <div className='item__text'>
475
-                              <div className='item_text_icon mr-2'>
476
-                                <i className='fa fa-gavel' />
477
-                              </div>
478
-                              <div className='item__text__name'>
479
-                                {this.props.t('Supervisor')}
480
-                              </div>
481
-                            </div>
482
-                          </li>
483
-                          <li className='dashboard__memberlist__form__role__list__item'>
484
-                            <div className='item__radiobtn mr-3'>
485
-                              <input type='radio' name='role' value='gestionnaire' />
486
-                            </div>
487
-                            <div className='item__text'>
488
-                              <div className='item_text_icon mr-2'>
489
-                                <i className='fa fa-graduation-cap' />
490
-                              </div>
491
-                              <div className='item__text__name'>
492
-                                {this.props.t('Content Manager')}
493
-                              </div>
494
-                            </div>
495
-                          </li>
496
-                          <li className='dashboard__memberlist__form__role__list__item'>
497
-                            <div className='item__radiobtn mr-3'>
498
-                              <input type='radio' name='role' value='contributeur' />
499
-                            </div>
500
-                            <div className='item__text'>
501
-                              <div className='item_text_icon mr-2'>
502
-                                <i className='fa fa-pencil' />
503
-                              </div>
504
-                              <div className='item__text__name'>
505
-                                {this.props.t('Contributor')}
506
-                              </div>
507
-                            </div>
508
-                          </li>
509
-                          <li className='dashboard__memberlist__form__role__list__item'>
510
-                            <div className='item__radiobtn mr-3'>
511
-                              <input type='radio' name='role' value='lecteur' />
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' />
512 314
                             </div>
513
-                            <div className='item__text'>
514
-                              <div className='item_text_icon mr-2'>
515
-                                <i className='fa fa-eye' />
516
-                              </div>
517
-                              <div className='item__text__name'>
518
-                                {this.props.t('Reader')}
519
-                              </div>
315
+
316
+                            <div className='item__text__name'>
317
+                              {this.props.t('Supervisor')}
520 318
                             </div>
521
-                          </li>
522
-                        </ul>
523
-                      </div>
524
-                      <div className='dashboard__memberlist__form__submitbtn'>
525
-                        <button className='btn btn-outline-primary'>
526
-                          {this.props.t('Validate')}
527
-                        </button>
528
-                      </div>
529
-                    </form>
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>
530 331
                   }
531 332
                 </div>
532 333
               </div>
@@ -547,17 +348,17 @@ class Dashboard extends React.Component {
547 348
                   </div>
548 349
                 </div>
549 350
                 {this.state.displayWebdavBtn === true &&
550
-                  <div>
551
-                    <div className='dashboard__moreinfo__webdav__information genericBtnInfoDashboard__info'>
552
-                      <div className='dashboard__moreinfo__webdav__information__text genericBtnInfoDashboard__info__text'>
553
-                        {this.props.t('Find all your documents deposited online directly on your computer via the workstation, without going through the software.')}'
554
-                      </div>
351
+                <div>
352
+                  <div className='dashboard__moreinfo__webdav__information genericBtnInfoDashboard__info'>
353
+                    <div className='dashboard__moreinfo__webdav__information__text genericBtnInfoDashboard__info__text'>
354
+                      {this.props.t('Find all your documents deposited online directly on your computer via the workstation, without going through the software.')}'
355
+                    </div>
555 356
 
556
-                      <div className='dashboard__moreinfo__webdav__information__link genericBtnInfoDashboard__info__link'>
557
-                        http://algoo.trac.im/webdav/
558
-                      </div>
357
+                    <div className='dashboard__moreinfo__webdav__information__link genericBtnInfoDashboard__info__link'>
358
+                      http://algoo.trac.im/webdav/
559 359
                     </div>
560 360
                   </div>
361
+                </div>
561 362
                 }
562 363
               </div>
563 364
               <div className='dashboard__moreinfo__calendar genericBtnInfoDashboard'>
@@ -577,27 +378,27 @@ class Dashboard extends React.Component {
577 378
                 </div>
578 379
                 <div className='dashboard__moreinfo__calendar__wrapperText'>
579 380
                   {this.state.displayCalendarBtn === true &&
580
-                    <div>
581
-                      <div className='dashboard__moreinfo__calendar__information genericBtnInfoDashboard__info'>
582
-                        <div className='dashboard__moreinfo__calendar__information__text genericBtnInfoDashboard__info__text'>
583
-                          {this.props.t('Each workspace has its own calendar.')}
584
-                        </div>
381
+                  <div>
382
+                    <div className='dashboard__moreinfo__calendar__information genericBtnInfoDashboard__info'>
383
+                      <div className='dashboard__moreinfo__calendar__information__text genericBtnInfoDashboard__info__text'>
384
+                        {this.props.t('Each workspace has its own calendar.')}
385
+                      </div>
585 386
 
586
-                        <div className='dashboard__moreinfo__calendar__information__link genericBtnInfoDashboard__info__link'>
587
-                          http://algoo.trac.im/calendar/
588
-                        </div>
387
+                      <div className='dashboard__moreinfo__calendar__information__link genericBtnInfoDashboard__info__link'>
388
+                        http://algoo.trac.im/calendar/
589 389
                       </div>
590 390
                     </div>
391
+                  </div>
591 392
                   }
592 393
                 </div>
593 394
               </div>
594 395
             </div>
595
-          </div>
596
-        </div>
396
+          </PageContent>
397
+        </PageWrapper>
597 398
       </div>
598 399
     )
599 400
   }
600 401
 }
601 402
 
602
-const mapStateToProps = ({ user, app, contentType, workspaceList }) => ({ user, app, contentType, workspaceList })
603
-export default connect(mapStateToProps)(translate()(Dashboard))
403
+const mapStateToProps = ({ user, contentType, currentWorkspace }) => ({ user, contentType, curWs: currentWorkspace })
404
+export default connect(mapStateToProps)(translate()(Radium(Dashboard)))

+ 571 - 0
frontend/src/container/Dashboard_old.jsx ファイルの表示

@@ -0,0 +1,571 @@
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))

+ 38 - 18
frontend/src/container/Sidebar.jsx ファイルの表示

@@ -3,6 +3,7 @@ import { connect } from 'react-redux'
3 3
 import { withRouter } from 'react-router'
4 4
 import classnames from 'classnames'
5 5
 import { translate } from 'react-i18next'
6
+import appFactory from '../appFactory.js'
6 7
 import WorkspaceListItem from '../component/Sidebar/WorkspaceListItem.jsx'
7 8
 import {
8 9
   setWorkspaceListIsOpenInSidebar,
@@ -12,7 +13,7 @@ import {
12 13
 import {
13 14
   getWorkspaceList
14 15
 } from '../action-creator.async.js'
15
-import { PAGE } from '../helper.js'
16
+import { PAGE, workspaceConfig } from '../helper.js'
16 17
 
17 18
 const qs = require('query-string')
18 19
 
@@ -23,22 +24,23 @@ class Sidebar extends React.Component {
23 24
       sidebarClose: false,
24 25
       workspaceIdInUrl: props.match.params.idws ? parseInt(props.match.params.idws) : null
25 26
     }
26
-  }
27
-
28
-  async componentDidMount () {
29
-    const { workspaceIdInUrl } = this.state
30
-    const { user, workspaceList, dispatch } = this.props
31 27
 
32
-    if (user.user_id !== -1 && workspaceList.length === 0) {
33
-      const fetchGetWorkspaceList = await dispatch(getWorkspaceList(user))
28
+    document.addEventListener('appCustomEvent', this.customEventReducer)
29
+  }
34 30
 
35
-      if (fetchGetWorkspaceList.status === 200) {
36
-        dispatch(updateWorkspaceListData(fetchGetWorkspaceList.json))
37
-        dispatch(setWorkspaceListIsOpenInSidebar(workspaceIdInUrl || fetchGetWorkspaceList.json[0].workspace_id, true))
38
-      }
31
+  customEventReducer = async ({ detail: { type, data } }) => {
32
+    switch (type) {
33
+      case 'refreshWorkspaceList':
34
+        console.log('%c<Sidebar> Custom event', 'color: #28a745', type, data)
35
+        this.loadWorkspaceList()
36
+        break
39 37
     }
40 38
   }
41 39
 
40
+  componentDidMount () {
41
+    this.loadWorkspaceList()
42
+  }
43
+
42 44
   componentDidUpdate (prevProps, prevState) {
43 45
     // console.log('%c<Sidebar> Did Update', 'color: #c17838')
44 46
     if (this.props.match.params.idws === undefined || isNaN(this.props.match.params.idws)) return
@@ -47,6 +49,20 @@ class Sidebar extends React.Component {
47 49
     if (prevState.workspaceIdInUrl !== newWorkspaceId) this.setState({workspaceIdInUrl: newWorkspaceId})
48 50
   }
49 51
 
52
+  loadWorkspaceList = async () => {
53
+    const { workspaceIdInUrl } = this.state
54
+    const { user, dispatch } = this.props
55
+
56
+    if (user.user_id !== -1) {
57
+      const fetchGetWorkspaceList = await dispatch(getWorkspaceList(user))
58
+
59
+      if (fetchGetWorkspaceList.status === 200) {
60
+        dispatch(updateWorkspaceListData(fetchGetWorkspaceList.json))
61
+        dispatch(setWorkspaceListIsOpenInSidebar(workspaceIdInUrl || fetchGetWorkspaceList.json[0].workspace_id, true))
62
+      }
63
+    }
64
+  }
65
+
50 66
   handleClickWorkspace = (idWs, newIsOpenInSidebar) => this.props.dispatch(setWorkspaceListIsOpenInSidebar(idWs, newIsOpenInSidebar))
51 67
 
52 68
   handleClickAllContent = idWs => {
@@ -63,13 +79,15 @@ class Sidebar extends React.Component {
63 79
 
64 80
     history.push(`${PAGE.WORKSPACE.CONTENT_LIST(idWs)}?type=${newFilter.join(';')}`) // workspace.filter gets updated on react redraw from match.params
65 81
 
66
-    // obviously, it's ugly to use custom event to tell WorkspaceContent to refresh, but since WorkspaceContent
82
+    // obviously, it's ugly to use custom event to tell WorkspaceContentList to refresh, but since WorkspaceContentList
67 83
     // will end up being an App, it'll have to be that way. So it's fine
68 84
     GLOBAL_dispatchEvent({ type: 'refreshContentList', data: {} })
69 85
   }
70 86
 
71 87
   handleClickToggleSidebar = () => this.setState(prev => ({sidebarClose: !prev.sidebarClose}))
72 88
 
89
+  handleClickNewWorkspace = () => this.props.renderAppPopupCreation(workspaceConfig, this.props.user, null, null)
90
+
73 91
   render () {
74 92
     const { sidebarClose, workspaceIdInUrl } = this.state
75 93
     const { activeLang, workspaceList, t } = this.props
@@ -103,7 +121,10 @@ class Sidebar extends React.Component {
103 121
             </nav>
104 122
 
105 123
             <div className='sidebar__btnnewworkspace'>
106
-              <button className='sidebar__btnnewworkspace__btn btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover mb-5'>
124
+              <button
125
+                className='sidebar__btnnewworkspace__btn btn btn-primary primaryColorBg primaryColorBorder primaryColorBorderDarkenHover mb-5'
126
+                onClick={this.handleClickNewWorkspace}
127
+              >
107 128
                 {t('Create a workspace')}
108 129
               </button>
109 130
             </div>
@@ -124,11 +145,10 @@ class Sidebar extends React.Component {
124 145
   }
125 146
 }
126 147
 
127
-const mapStateToProps = ({ lang, user, workspace, workspaceList, app }) => ({
148
+const mapStateToProps = ({ lang, user, workspace, workspaceList }) => ({
128 149
   activeLang: lang.find(l => l.active) || {id: 'en'},
129 150
   user,
130 151
   workspace,
131
-  workspaceList,
132
-  app
152
+  workspaceList
133 153
 })
134
-export default withRouter(connect(mapStateToProps)(translate()(Sidebar)))
154
+export default withRouter(connect(mapStateToProps)(appFactory(translate()(Sidebar))))

+ 22 - 2
frontend/src/container/Tracim.jsx ファイルの表示

@@ -16,16 +16,33 @@ import PrivateRoute from './PrivateRoute.jsx'
16 16
 import { COOKIE, PAGE } from '../helper.js'
17 17
 import {
18 18
   getAppList,
19
-  getUserIsConnected
19
+  getUserIsConnected,
20
+  getContentTypeList
20 21
 } from '../action-creator.async.js'
21 22
 import {
22 23
   removeFlashMessage,
23 24
   setAppList,
24
-  setUserConnected
25
+  setUserConnected,
26
+  setContentTypeList
25 27
 } from '../action-creator.sync.js'
26 28
 import Cookies from 'js-cookie'
27 29
 
28 30
 class Tracim extends React.Component {
31
+  constructor (props) {
32
+    super(props)
33
+
34
+    document.addEventListener('appCustomEvent', this.customEventReducer)
35
+  }
36
+
37
+  customEventReducer = async ({ detail: { type, data } }) => {
38
+    switch (type) {
39
+      case 'redirect':
40
+        console.log('%c<Tracim> Custom event', 'color: #28a745', type, data)
41
+        this.props.history.push(data.url)
42
+        break
43
+    }
44
+  }
45
+
29 46
   async componentDidMount () {
30 47
     const { dispatch } = this.props
31 48
 
@@ -47,6 +64,9 @@ class Tracim extends React.Component {
47 64
 
48 65
         const fetchGetAppList = await dispatch(getAppList(userLogged))
49 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))
50 70
         break
51 71
 
52 72
       case 401:

+ 12 - 19
frontend/src/container/WorkspaceContent.jsx ファイルの表示

@@ -16,14 +16,12 @@ import {
16 16
   PageContent
17 17
 } from 'tracim_frontend_lib'
18 18
 import {
19
-  getContentTypeList,
20 19
   getWorkspaceContentList,
21 20
   getFolderContent
22 21
 } from '../action-creator.async.js'
23 22
 import {
24 23
   newFlashMessage,
25
-  setContentTypeList,
26
-  setWorkspaceContent
24
+  setWorkspaceContentList
27 25
 } from '../action-creator.sync.js'
28 26
 
29 27
 const qs = require('query-string')
@@ -61,15 +59,10 @@ class WorkspaceContent extends React.Component {
61 59
   }
62 60
 
63 61
   async componentDidMount () {
64
-    const { user, workspaceList, contentType, match, dispatch } = this.props
62
+    const { workspaceList, match } = this.props
65 63
 
66 64
     console.log('%c<WorkspaceContent> componentDidMount', 'color: #c17838')
67 65
 
68
-    if (contentType.length === 0) {
69
-      const fetchGetContentTypeList = await dispatch(getContentTypeList(user))
70
-      if (fetchGetContentTypeList.status === 200) dispatch(setContentTypeList(fetchGetContentTypeList.json))
71
-    }
72
-
73 66
     let wsToLoad = null
74 67
 
75 68
     if (match.params.idws === undefined) {
@@ -109,7 +102,7 @@ class WorkspaceContent extends React.Component {
109 102
 
110 103
     const wsContent = await dispatch(getWorkspaceContentList(user, idWorkspace, 0))
111 104
 
112
-    if (wsContent.status === 200) dispatch(setWorkspaceContent(wsContent.json, qs.parse(location.search).type))
105
+    if (wsContent.status === 200) dispatch(setWorkspaceContentList(wsContent.json, qs.parse(location.search).type))
113 106
     else dispatch(newFlashMessage('Error while loading workspace', 'danger'))
114 107
   }
115 108
 
@@ -155,7 +148,7 @@ class WorkspaceContent extends React.Component {
155 148
   handleUpdateAppOpenedType = openedAppType => this.setState({appOpenedType: openedAppType})
156 149
 
157 150
   render () {
158
-    const { workspaceContent, contentType } = this.props
151
+    const { workspaceContentList, contentType } = this.props
159 152
 
160 153
     const filterWorkspaceContent = (contentList, filter) => {
161 154
       return filter.length === 0
@@ -168,8 +161,8 @@ class WorkspaceContent extends React.Component {
168 161
 
169 162
     const urlFilter = qs.parse(this.props.location.search).type
170 163
 
171
-    const filteredWorkspaceContent = workspaceContent.length > 0
172
-      ? filterWorkspaceContent(workspaceContent, urlFilter ? [urlFilter] : [])
164
+    const filteredWorkspaceContentList = workspaceContentList.length > 0
165
+      ? filterWorkspaceContent(workspaceContentList, urlFilter ? [urlFilter] : [])
173 166
       : []
174 167
 
175 168
     return (
@@ -196,7 +189,7 @@ class WorkspaceContent extends React.Component {
196 189
             parentClass='workspace__header'
197 190
             customClass='justify-content-between'
198 191
             title='Liste des Contenus'
199
-            subtitle={workspaceContent.label ? workspaceContent.label : ''}
192
+            subtitle={workspaceContentList.label ? workspaceContentList.label : ''}
200 193
           >
201 194
             <DropdownCreateButton
202 195
               parentClass='workspace__header__btnaddworkspace'
@@ -212,7 +205,7 @@ class WorkspaceContent extends React.Component {
212 205
             <div className='workspace__content__fileandfolder folder__content active'>
213 206
               <ContentItemHeader />
214 207
 
215
-              { filteredWorkspaceContent.map((c, i) => c.type === 'folder'
208
+              { filteredWorkspaceContentList.map((c, i) => c.type === 'folder'
216 209
                 ? (
217 210
                   <Folder
218 211
                     availableApp={contentType}
@@ -227,7 +220,7 @@ class WorkspaceContent extends React.Component {
227 220
                     }}
228 221
                     onClickFolder={this.handleClickFolder}
229 222
                     onClickCreateContent={this.handleClickCreateContent}
230
-                    isLast={i === filteredWorkspaceContent.length - 1}
223
+                    isLast={i === filteredWorkspaceContentList.length - 1}
231 224
                     key={c.id}
232 225
                   />
233 226
                 )
@@ -237,7 +230,7 @@ class WorkspaceContent extends React.Component {
237 230
                     type={c.type}
238 231
                     faIcon={contentType.length ? contentType.find(a => a.slug === c.type).faIcon : ''}
239 232
                     statusSlug={c.statusSlug}
240
-                    contentType={contentType.find(ct => ct.slug === c.type)}
233
+                    contentType={contentType.length ? contentType.find(ct => ct.slug === c.type) : null}
241 234
                     onClickItem={() => this.handleClickContentItem(c)}
242 235
                     onClickExtendedAction={{
243 236
                       edit: e => this.handleClickEditContentItem(e, c),
@@ -247,7 +240,7 @@ class WorkspaceContent extends React.Component {
247 240
                       delete: e => this.handleClickDeleteContentItem(e, c)
248 241
                     }}
249 242
                     onClickCreateContent={this.handleClickCreateContent}
250
-                    isLast={i === filteredWorkspaceContent.length - 1}
243
+                    isLast={i === filteredWorkspaceContentList.length - 1}
251 244
                     key={c.id}
252 245
                   />
253 246
                 )
@@ -268,5 +261,5 @@ class WorkspaceContent extends React.Component {
268 261
   }
269 262
 }
270 263
 
271
-const mapStateToProps = ({ user, workspaceContent, workspaceList, app, contentType }) => ({ user, workspaceContent, workspaceList, app, contentType })
264
+const mapStateToProps = ({ user, workspaceContentList, workspaceList, contentType }) => ({ user, workspaceContentList, workspaceList, contentType })
272 265
 export default withRouter(connect(mapStateToProps)(appFactory(WorkspaceContent)))

+ 8 - 22
frontend/src/css/Dashboard.styl ファイルの表示

@@ -43,7 +43,7 @@ coloricon()
43 43
         margin 0
44 44
     &__advancedmode
45 45
       cursor pointer
46
-  &__wkswrapper
46
+  &__workspace-wrapper
47 47
     flexwrap()
48 48
   &__workspace
49 49
     margin-right 20px
@@ -62,9 +62,9 @@ coloricon()
62 62
       flexwrap()
63 63
       margin 20px 0
64 64
       font-size 18px
65
-      &__text
65
+      &__msg
66 66
         margin-right 15px
67
-      &__rank
67
+      &__definition
68 68
         display flex
69 69
         &__icon
70 70
           margin-right 15px
@@ -88,25 +88,11 @@ coloricon()
88 88
       &:active
89 89
         box-shadow inset 0px 0px 5px 2px #656565
90 90
       &__text
91
+        color white
91 92
         &__icon
92 93
           font-size 30px
93
-          .fa-comments-o, .fa-paperclip, .fa-calendar, .fa-file-text-o, .fa-folder-open-o, .fa-video-camera
94
-              color white
95 94
         &__title
96 95
           font-size 18px
97
-          color white
98
-    .btnthread
99
-      background-color threadColor
100
-    .writefile
101
-      background-color writefile
102
-    .importfile
103
-      background-color importfile
104
-    .visioconf
105
-      background-color grey-hover
106
-    .calendar
107
-      background-color calendar
108
-    .explore
109
-      background-color explore
110 96
   &__wksinfo
111 97
     flexwrap()
112 98
     margin-top 150px
@@ -270,19 +256,19 @@ coloricon()
270 256
     justify-content space-between
271 257
     flexwrap wrap
272 258
     &__webdav
273
-      margin-bottom 40px
259
+      margin 0 15px 40px 0
274 260
       &__btn
275 261
         width 300px
276 262
       &__information
277
-        width 550px
263
+        width 300px
278 264
     &__calendar
279 265
       margin-bottom 100px
280 266
       &__wrapperBtn
281
-        margin-right 300px
267
+        margin-right 290px
282 268
       &__btn
283 269
         width 300px
284 270
       &__information
285
-        width 550px
271
+        width 300px
286 272
 
287 273
 /**** MEDIAQUERIES *****/
288 274
 

+ 5 - 1
frontend/src/css/Generic.styl ファイルの表示

@@ -202,7 +202,7 @@ a
202 202
   display flex
203 203
   flex-direction column
204 204
   justify-content center
205
-  margin 20px 30px 20px 0
205
+  margin 0 15px
206 206
   border-radius 10px
207 207
   padding 15px
208 208
   width 230px
@@ -210,6 +210,10 @@ a
210 210
   box-shadow shadow-all
211 211
   text-align center
212 212
   cursor pointer
213
+  &:nth-child(1)
214
+    margin-left 0
215
+  &:nth-last-child
216
+    margin-right 0
213 217
 
214 218
 .genericBtnInfoDashboard
215 219
   &__btn

+ 11 - 0
frontend/src/helper.js ファイルの表示

@@ -12,6 +12,15 @@ export const COOKIE = {
12 12
   USER_AUTH: 'user_auth'
13 13
 }
14 14
 
15
+// Côme - 2018/08/02 - shouldn't this come from api ?
16
+export const workspaceConfig = {
17
+  slug: 'workspace',
18
+  faIcon: 'space-shuttle',
19
+  hexcolor: '#7d4e24',
20
+  creationLabel: 'Create a workspace',
21
+  domContainer: 'appFeatureContainer'
22
+}
23
+
15 24
 export const PAGE = {
16 25
   HOME: '/',
17 26
   WORKSPACE: {
@@ -55,3 +64,5 @@ export const ROLE = [{
55 64
   icon: 'fa-gavel',
56 65
   translationKey: 'role.manager'
57 66
 }]
67
+
68
+export const handleRouteFromApi = route => route.startsWith('/#') ? route.slice(2) : route

frontend/src/reducer/app.js → frontend/src/reducer/appList.js ファイルの表示


+ 46 - 0
frontend/src/reducer/currentWorkspace.js ファイルの表示

@@ -0,0 +1,46 @@
1
+import {SET, WORKSPACE_DETAIL, WORKSPACE_MEMBER_LIST} from '../action-creator.sync.js'
2
+import { handleRouteFromApi } from '../helper.js'
3
+
4
+const defaultWorkspace = {
5
+  id: 0,
6
+  slug: '',
7
+  label: '',
8
+  description: '',
9
+  sidebarEntries: [],
10
+  member: []
11
+}
12
+
13
+export default function currentWorkspace (state = defaultWorkspace, action) {
14
+  switch (action.type) {
15
+    case `${SET}/${WORKSPACE_DETAIL}`:
16
+      return {
17
+        ...state,
18
+        id: action.workspaceDetail.workspace_id,
19
+        slug: action.workspaceDetail.slug,
20
+        label: action.workspaceDetail.label,
21
+        description: action.workspaceDetail.description,
22
+        sidebarEntries: action.workspaceDetail.sidebar_entries.map(sbe => ({
23
+          slug: sbe.slug,
24
+          route: handleRouteFromApi(sbe.route),
25
+          faIcon: sbe.fa_icon,
26
+          hexcolor: sbe.hexcolor,
27
+          label: sbe.label
28
+        }))
29
+      }
30
+
31
+    case `${SET}/${WORKSPACE_MEMBER_LIST}`:
32
+      return {
33
+        ...state,
34
+        member: action.workspaceMemberList.map(m => ({
35
+          id: m.user_id,
36
+          publicName: m.user.public_name,
37
+          avatarUrl: m.user.avatar_url,
38
+          role: m.role,
39
+          isActive: m.is_active,
40
+        }))
41
+      }
42
+
43
+    default:
44
+      return state
45
+  }
46
+}

+ 4 - 3
frontend/src/reducer/root.js ファイルの表示

@@ -2,12 +2,13 @@ import { combineReducers } from 'redux'
2 2
 import lang from './lang.js'
3 3
 import flashMessage from './flashMessage.js'
4 4
 import user from './user.js'
5
-import workspaceContent from './workspaceContent.js'
5
+import currentWorkspace from './currentWorkspace.js'
6
+import workspaceContentList from './workspaceContentList.js'
6 7
 import workspaceList from './workspaceList.js'
7
-import app from './app.js'
8
+import appList from './appList.js'
8 9
 import contentType from './contentType.js'
9 10
 import timezone from './timezone.js'
10 11
 
11
-const rootReducer = combineReducers({ lang, flashMessage, user, workspaceContent, workspaceList, app, contentType, timezone })
12
+const rootReducer = combineReducers({ lang, flashMessage, user, currentWorkspace, workspaceContentList, workspaceList, appList, contentType, timezone })
12 13
 
13 14
 export default rootReducer

frontend/src/reducer/workspaceContent.js → frontend/src/reducer/workspaceContentList.js ファイルの表示

@@ -2,13 +2,14 @@ import {
2 2
   SET,
3 3
   UPDATE,
4 4
   WORKSPACE,
5
+  WORKSPACE_CONTENT,
5 6
   FOLDER
6 7
 } from '../action-creator.sync.js'
7 8
 
8
-export default function workspace (state = [], action) {
9
+export default function workspaceContentList (state = [], action) {
9 10
   switch (action.type) {
10
-    case `${SET}/${WORKSPACE}/Content`:
11
-      return action.workspaceContent.map(wsc => ({
11
+    case `${SET}/${WORKSPACE_CONTENT}`:
12
+      return action.workspaceContentList.map(wsc => ({
12 13
         id: wsc.content_id,
13 14
         label: wsc.label,
14 15
         slug: wsc.slug,

+ 2 - 3
frontend/src/reducer/workspaceList.js ファイルの表示

@@ -4,8 +4,7 @@ import {
4 4
   WORKSPACE_LIST,
5 5
   USER_ROLE
6 6
 } from '../action-creator.sync.js'
7
-
8
-const handleRouteFromApi = route => route.startsWith('/#') ? route.slice(2) : route
7
+import { handleRouteFromApi } from '../helper.js'
9 8
 
10 9
 export function workspaceList (state = [], action) {
11 10
   switch (action.type) {
@@ -14,7 +13,7 @@ export function workspaceList (state = [], action) {
14 13
         id: ws.workspace_id,
15 14
         label: ws.label,
16 15
         slug: ws.slug,
17
-        description: ws.description,
16
+        // description: ws.description, // not returned by /api/v2/users/:idUser/workspaces
18 17
         sidebarEntry: ws.sidebar_entries.map(sbe => ({
19 18
           slug: sbe.slug,
20 19
           route: handleRouteFromApi(sbe.route),

+ 13 - 0
frontend_app_workspace/.editorconfig ファイルの表示

@@ -0,0 +1,13 @@
1
+# doc here : https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties
2
+root = true
3
+
4
+[*]
5
+indent_style = space
6
+indent_size = 2
7
+end_of_line = lf
8
+charset = utf-8
9
+insert_final_newline = true
10
+trim_trailing_whitespace = true
11
+
12
+[*.py]
13
+indent_size = 4

+ 8 - 0
frontend_app_workspace/.gitignore ファイルの表示

@@ -0,0 +1,8 @@
1
+# Created by .ignore support plugin (hsz.mobi)
2
+.idea/
3
+.git/
4
+node_modules/
5
+dist/workspace.app.js
6
+dist/workspace.app.js.map
7
+dist/workspace.app.dev.js
8
+dist/workspace.app.dev.js.map

+ 3 - 0
frontend_app_workspace/README.md ファイルの表示

@@ -0,0 +1,3 @@
1
+# App Workspace for Tracim
2
+
3
+This repo is an app loaded by Tracim.

+ 17 - 0
frontend_app_workspace/build_workspace.sh ファイルの表示

@@ -0,0 +1,17 @@
1
+#!/bin/bash
2
+
3
+. ../bash_library.sh # source bash_library.sh
4
+
5
+windoz=""
6
+if [[ $1 = "-w" ]]; then
7
+    windoz="windoz"
8
+fi
9
+
10
+log "npm run build$windoz"
11
+npm run build$windoz
12
+log "cp dist/workspace.app.js ../frontend/dist/app"
13
+cp dist/workspace.app.js ../frontend/dist/app
14
+log "cp i18next.scanner/en/translation.json ../frontend/dist/app/workspace_en_translation.json"
15
+cp i18next.scanner/en/translation.json ../frontend/dist/app/workspace_en_translation.json
16
+log "cp i18next.scanner/fr/translation.json ../frontend/dist/app/workspace_fr_translation.json"
17
+cp i18next.scanner/fr/translation.json ../frontend/dist/app/workspace_fr_translation.json

+ 1 - 0
frontend_app_workspace/dist/asset ファイルの表示

@@ -0,0 +1 @@
1
+../../frontend/dist/asset/

+ 1 - 0
frontend_app_workspace/dist/dev ファイルの表示

@@ -0,0 +1 @@
1
+../../frontend/dist/dev/

+ 1 - 0
frontend_app_workspace/dist/font ファイルの表示

@@ -0,0 +1 @@
1
+../../frontend/dist/font/

+ 110 - 0
frontend_app_workspace/dist/index.html ファイルの表示

@@ -0,0 +1,110 @@
1
+<!DOCTYPE html>
2
+<html>
3
+<head>
4
+  <meta charset='utf-8' />
5
+  <meta name="viewport" content="width=device-width, user-scalable=no" />
6
+  <title>Workspace App Tracim</title>
7
+  <link rel='shortcut icon' href='favicon.ico'>
8
+
9
+  <link rel="stylesheet" type="text/css" href="./font/font-awesome-4.7.0/css/font-awesome.css">
10
+  <link href="https://fonts.googleapis.com/css?family=Quicksand:300,400,500,700" rel="stylesheet">
11
+  <link rel="stylesheet" type="text/css" href="./dev/bootstrap-4.0.0-beta.css">
12
+</head>
13
+
14
+<body>
15
+  <script src="./dev/jquery-3.2.1.js"></script>
16
+  <script src="./dev/popper-1.12.3.js"></script>
17
+  <script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
18
+
19
+  <script type="text/javascript" src="/asset/tinymce/js/tinymce/jquery.tinymce.min.js"></script>
20
+  <script type="text/javascript" src="/asset/tinymce/js/tinymce/tinymce.min.js"></script>
21
+
22
+  <div id='content'></div>
23
+
24
+  <script type='text/javascript'>
25
+    (function () {
26
+      wysiwyg = function (selector, handleOnChange) {
27
+        function base64EncodeAndTinyMceInsert (files) {
28
+          for (var i = 0; i < files.length; i++) {
29
+            if (files[i].size > 1000000)
30
+              files[i].allowed = confirm(files[i].name + " fait plus de 1mo et peut prendre du temps à insérer, voulez-vous continuer ?")
31
+          }
32
+
33
+          for (var i = 0; i < files.length; i++) {
34
+            if (files[i].allowed !== false && files[i].type.match('image.*')) {
35
+              var img = document.createElement('img')
36
+
37
+              var fr = new FileReader()
38
+
39
+              fr.readAsDataURL(files[i])
40
+
41
+              fr.onloadend = function (e) {
42
+                img.src = e.target.result
43
+                tinymce.activeEditor.execCommand('mceInsertContent', false, img.outerHTML)
44
+              }
45
+            }
46
+          }
47
+        }
48
+
49
+        // HACK: The tiny mce source code modal contain a textarea, but we
50
+        // can't edit it (like it's readonly). The following solution
51
+        // solve the bug: https://stackoverflow.com/questions/36952148/tinymce-code-editor-is-readonly-in-jtable-grid
52
+        $(document).on('focusin', function(e) {
53
+          if ($(e.target).closest(".mce-window").length) {
54
+            e.stopImmediatePropagation();
55
+          }
56
+        });
57
+
58
+        tinymce.init({
59
+          selector: selector,
60
+          menubar: false,
61
+          resize: false,
62
+          skin: "lightgray",
63
+          plugins:'advlist autolink lists link image charmap print preview anchor textcolor searchreplace visualblocks code fullscreen insertdatetime media table contextmenu paste code help',
64
+          toolbar: 'insert | formatselect | bold italic underline strikethrough forecolor backcolor | link | alignleft aligncenter alignright alignjustify  | numlist bullist outdent indent  | table | code ',
65
+          content_style: "div {height: 100%;}",
66
+          setup: function ($editor) {
67
+            $editor.on('change', function(e) {
68
+              handleOnChange({target: {value: $editor.getContent()}}) // target.value to emulate a js event so the react handler can expect one
69
+            })
70
+
71
+            //////////////////////////////////////////////
72
+            // add custom btn to handle image by selecting them with system explorer
73
+            $editor.addButton('customInsertImage', {
74
+              icon: 'mce-ico mce-i-image',
75
+              title: 'Image',
76
+              onclick: function () {
77
+                if ($('#hidden_tinymce_fileinput').length > 0) $('#hidden_tinymce_fileinput').remove()
78
+
79
+                fileTag = document.createElement('input')
80
+                fileTag.id = 'hidden_tinymce_fileinput'
81
+                fileTag.type = 'file'
82
+                $('body').append(fileTag)
83
+
84
+                $('#hidden_tinymce_fileinput').on('change', function () {
85
+                  base64EncodeAndTinyMceInsert($(this)[0].files)
86
+                })
87
+
88
+                $('#hidden_tinymce_fileinput').click()
89
+              }
90
+            })
91
+
92
+            //////////////////////////////////////////////
93
+            // Handle drag & drop image into TinyMce by encoding them in base64 (to avoid uploading them somewhere and keep saving comment in string format)
94
+            $editor
95
+              .on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
96
+                e.preventDefault()
97
+                e.stopPropagation()
98
+              })
99
+              .on('drop', function(e) {
100
+                base64EncodeAndTinyMceInsert(e.dataTransfer.files)
101
+              })
102
+          }
103
+        })
104
+      }
105
+    })()
106
+  </script>
107
+
108
+  <script src='./workspace.app.dev.js'></script>
109
+</body>
110
+</html>

+ 14 - 0
frontend_app_workspace/i18next.scanner.js ファイルの表示

@@ -0,0 +1,14 @@
1
+const scanner = require('i18next-scanner')
2
+const vfs = require('vinyl-fs')
3
+
4
+const option = require('../i18next.option.js')
5
+
6
+// --------------------
7
+// 2018/07/27 - currently, last version is 2.6.5 but a bug is spaming log with errors. So I'm using 2.6.1
8
+// this issue seems related : https://github.com/i18next/i18next-scanner/issues/88
9
+// --------------------
10
+
11
+vfs.src(['./src/**/*.jsx'])
12
+// .pipe(sort()) // Sort files in stream by path
13
+  .pipe(scanner(option))
14
+  .pipe(vfs.dest('./i18next.scanner'))

+ 6 - 0
frontend_app_workspace/i18next.scanner/en/translation.json ファイルの表示

@@ -0,0 +1,6 @@
1
+{
2
+  "Last version": "Last version",
3
+  "Validate and create": "Validate and create",
4
+  "Document's title": "Document's title",
5
+  "New Document": "New Document"
6
+}

+ 6 - 0
frontend_app_workspace/i18next.scanner/fr/translation.json ファイルの表示

@@ -0,0 +1,6 @@
1
+{
2
+  "Last version": "Dernière version",
3
+  "Validate and create": "Valider et créer",
4
+  "Document's title": "Titre du document",
5
+  "New Document": "Nouveau document"
6
+}

+ 63 - 0
frontend_app_workspace/package.json ファイルの表示

@@ -0,0 +1,63 @@
1
+{
2
+  "name": "tracim_app_workspace",
3
+  "version": "1.0.0",
4
+  "description": "",
5
+  "main": "index.js",
6
+  "scripts": {
7
+    "servdev": "NODE_ENV=development webpack-dev-server --watch --colors --inline --hot --progress",
8
+    "servdevwindoz": "set NODE_ENV=development&& webpack-dev-server --watch --colors --inline --hot --progress",
9
+    "servdev-dashboard": "NODE_ENV=development webpack-dashboard -m -p 9874 -- webpack-dev-server --watch --colors --inline --hot --progress",
10
+    "build": "NODE_ENV=production webpack -p",
11
+    "build-translation": "node i18next.scanner.js",
12
+    "buildwindoz": "set NODE_ENV=production&& webpack -p",
13
+    "test": "echo \"Error: no test specified\" && exit 1"
14
+  },
15
+  "author": "",
16
+  "license": "ISC",
17
+  "dependencies": {
18
+    "babel-core": "^6.26.0",
19
+    "babel-eslint": "^8.2.1",
20
+    "babel-loader": "^7.1.2",
21
+    "babel-plugin-transform-class-properties": "^6.24.1",
22
+    "babel-plugin-transform-object-assign": "^6.22.0",
23
+    "babel-plugin-transform-object-rest-spread": "^6.26.0",
24
+    "babel-polyfill": "^6.26.0",
25
+    "babel-preset-env": "^1.6.1",
26
+    "babel-preset-react": "^6.24.1",
27
+    "classnames": "^2.2.5",
28
+    "css-loader": "^0.28.7",
29
+    "file-loader": "^1.1.5",
30
+    "i18next": "^10.5.0",
31
+    "prop-types": "^15.6.0",
32
+    "react": "^16.0.0",
33
+    "react-dom": "^16.0.0",
34
+    "react-i18next": "^7.5.0",
35
+    "standard": "^11.0.0",
36
+    "standard-loader": "^6.0.1",
37
+    "style-loader": "^0.19.0",
38
+    "stylus": "^0.54.5",
39
+    "stylus-loader": "^3.0.1",
40
+    "url-loader": "^0.6.2",
41
+    "webpack": "^3.8.1",
42
+    "whatwg-fetch": "^2.0.3"
43
+  },
44
+  "devDependencies": {
45
+    "i18next-scanner": "^2.6.1",
46
+    "webpack-dashboard": "^1.1.1",
47
+    "webpack-dev-server": "^2.9.2"
48
+  },
49
+  "standard": {
50
+    "globals": [
51
+      "fetch",
52
+      "history",
53
+      "btoa",
54
+      "wysiwyg",
55
+      "tinymce",
56
+      "GLOBAL_renderAppFeature",
57
+      "GLOBAL_unmountApp",
58
+      "GLOBAL_dispatchEvent"
59
+    ],
60
+    "parser": "babel-eslint",
61
+    "ignore": []
62
+  }
63
+}

+ 14 - 0
frontend_app_workspace/src/action.async.js ファイルの表示

@@ -0,0 +1,14 @@
1
+import { FETCH_CONFIG } from './helper.js'
2
+
3
+export const postWorkspace = (user, apiUrl, newWorkspaceName) =>
4
+  fetch(`${apiUrl}/workspaces`, {
5
+    headers: {
6
+      'Authorization': 'Basic ' + user.auth,
7
+      ...FETCH_CONFIG.headers
8
+    },
9
+    method: 'POST',
10
+    body: JSON.stringify({
11
+      label: newWorkspaceName,
12
+      description: ''
13
+    })
14
+  })

+ 121 - 0
frontend_app_workspace/src/container/PopupCreateWorkspace.jsx ファイルの表示

@@ -0,0 +1,121 @@
1
+import React from 'react'
2
+import { translate } from 'react-i18next'
3
+import {
4
+  CardPopupCreateContent,
5
+  handleFetchResult,
6
+  addAllResourceI18n
7
+} from 'tracim_frontend_lib'
8
+import { postWorkspace } from '../action.async.js'
9
+import i18n from '../i18n.js'
10
+
11
+const debug = { // outdated
12
+  config: {
13
+    slug: 'workspace',
14
+    faIcon: 'space-shuttle',
15
+    hexcolor: '#7d4e24',
16
+    creationLabel: 'Create a workspace',
17
+    domContainer: 'appFeatureContainer',
18
+    apiUrl: 'http://localhost:3001',
19
+    apiHeader: {
20
+      'Accept': 'application/json',
21
+      'Content-Type': 'application/json',
22
+      'Authorization': 'Basic ' + btoa(`${'admin@admin.admin'}:${'admin@admin.admin'}`)
23
+    },
24
+    translation: {
25
+      en: {},
26
+      fr: {}
27
+    }
28
+  },
29
+  loggedUser: {
30
+    id: 5,
31
+    username: 'Smoi',
32
+    firstname: 'Côme',
33
+    lastname: 'Stoilenom',
34
+    email: 'osef@algoo.fr',
35
+    avatar: 'https://avatars3.githubusercontent.com/u/11177014?s=460&v=4'
36
+  },
37
+  idWorkspace: 1,
38
+  idFolder: null
39
+}
40
+
41
+class PopupCreateWorkspace extends React.Component {
42
+  constructor (props) {
43
+    super(props)
44
+    this.state = {
45
+      appName: 'workspace',
46
+      config: props.data ? props.data.config : debug.config,
47
+      loggedUser: props.data ? props.data.loggedUser : debug.loggedUser,
48
+      newWorkspaceName: ''
49
+    }
50
+
51
+    // i18n has been init, add resources from frontend
52
+    addAllResourceI18n(i18n, this.state.config.translation)
53
+    i18n.changeLanguage(this.state.loggedUser.lang)
54
+
55
+    document.addEventListener('appCustomEvent', this.customEventReducer)
56
+  }
57
+
58
+  customEventReducer = ({ detail: { type, data } }) => { // action: { type: '', data: {} }
59
+    switch (type) {
60
+      case 'allApp_changeLang':
61
+        console.log('%c<PopupCreateWorkspace> Custom event', 'color: #28a745', type, data)
62
+        this.setState(prev => ({
63
+          loggedUser: {
64
+            ...prev.loggedUser,
65
+            lang: data
66
+          }
67
+        }))
68
+        i18n.changeLanguage(data)
69
+        break
70
+    }
71
+  }
72
+
73
+  handleChangeNewWorkspaceName = e => this.setState({newWorkspaceName: e.target.value})
74
+
75
+  handleClose = () => GLOBAL_dispatchEvent({
76
+    type: 'hide_popupCreateWorkspace', // handled by tracim_front:dist/index.html
77
+    data: {
78
+      name: this.state.appName
79
+    }
80
+  })
81
+
82
+  handleValidate = async () => {
83
+    const { loggedUser, config, newWorkspaceName } = this.state
84
+
85
+    const fetchSaveNewWorkspace = postWorkspace(loggedUser, config.apiUrl, newWorkspaceName)
86
+
87
+    handleFetchResult(await fetchSaveNewWorkspace)
88
+      .then(resSave => {
89
+        if (resSave.apiResponse.status === 200) {
90
+          this.handleClose()
91
+
92
+          GLOBAL_dispatchEvent({ type: 'refreshWorkspaceList', data: {} })
93
+
94
+          GLOBAL_dispatchEvent({
95
+            type: 'redirect',
96
+            data: {
97
+              url: `/workspaces/${resSave.body.workspace_id}/dashboard`
98
+            }
99
+          })
100
+        }
101
+      })
102
+  }
103
+
104
+  render () {
105
+    return (
106
+      <CardPopupCreateContent
107
+        onClose={this.handleClose}
108
+        onValidate={this.handleValidate}
109
+        label={this.props.t('New workspace')} // @TODO get the lang of user
110
+        customColor={this.state.config.hexcolor}
111
+        faIcon={this.state.config.faIcon}
112
+        contentName={this.state.newWorkspaceName}
113
+        onChangeContentName={this.handleChangeNewWorkspaceName}
114
+        btnValidateLabel={this.props.t('Validate and create')}
115
+        inputPlaceholder={this.props.t("Workspace's name")}
116
+      />
117
+    )
118
+  }
119
+}
120
+
121
+export default translate()(PopupCreateWorkspace)

+ 401 - 0
frontend_app_workspace/src/container/Workspace.jsx ファイルの表示

@@ -0,0 +1,401 @@
1
+import React from 'react'
2
+import { translate } from 'react-i18next'
3
+import i18n from '../i18n.js'
4
+import {
5
+  addAllResourceI18n,
6
+  handleFetchResult,
7
+  PopinFixed,
8
+  PopinFixedHeader,
9
+  PopinFixedOption,
10
+  PopinFixedContent,
11
+  Timeline,
12
+  NewVersionBtn,
13
+  ArchiveDeleteContent,
14
+  SelectStatus
15
+} from 'tracim_frontend_lib'
16
+import { MODE, debug } from '../helper.js'
17
+import {
18
+  getHtmlDocContent,
19
+  getHtmlDocComment,
20
+  getHtmlDocRevision,
21
+  postHtmlDocNewComment,
22
+  putHtmlDocContent,
23
+  putHtmlDocStatus
24
+} from '../action.async.js'
25
+
26
+class Workspace extends React.Component {
27
+  constructor (props) {
28
+    super(props)
29
+    this.state = {
30
+      appName: 'html-document',
31
+      isVisible: true,
32
+      config: props.data ? props.data.config : debug.config,
33
+      loggedUser: props.data ? props.data.loggedUser : debug.loggedUser,
34
+      content: props.data ? props.data.content : debug.content,
35
+      rawContentBeforeEdit: '',
36
+      timeline: props.data ? [] : [], // debug.timeline,
37
+      newComment: '',
38
+      timelineWysiwyg: false,
39
+      mode: MODE.VIEW
40
+    }
41
+
42
+    // i18n has been init, add resources from frontend
43
+    addAllResourceI18n(i18n, this.state.config.translation)
44
+    i18n.changeLanguage(this.state.loggedUser.lang)
45
+
46
+    document.addEventListener('appCustomEvent', this.customEventReducer)
47
+  }
48
+
49
+  customEventReducer = ({ detail: { type, data } }) => { // action: { type: '', data: {} }
50
+    switch (type) {
51
+      case 'html-document_showApp':
52
+        console.log('%c<Workspace> Custom event', 'color: #28a745', type, data)
53
+        this.setState({isVisible: true})
54
+        break
55
+      case 'html-document_hideApp':
56
+        console.log('%c<Workspace> Custom event', 'color: #28a745', type, data)
57
+        this.setState({isVisible: false})
58
+        break
59
+      case 'html-document_reloadContent':
60
+        console.log('%c<Workspace> Custom event', 'color: #28a745', type, data)
61
+        this.setState(prev => ({content: {...prev.content, ...data}, isVisible: true}))
62
+        break
63
+      case 'allApp_changeLang':
64
+        console.log('%c<Workspace> Custom event', 'color: #28a745', type, data)
65
+        this.setState(prev => ({
66
+          loggedUser: {
67
+            ...prev.loggedUser,
68
+            lang: data
69
+          }
70
+        }))
71
+        i18n.changeLanguage(data)
72
+        break
73
+    }
74
+  }
75
+
76
+  componentDidMount () {
77
+    console.log('%c<Workspace> did mount', `color: ${this.state.config.hexcolor}`)
78
+
79
+    this.loadContent()
80
+  }
81
+
82
+  componentDidUpdate (prevProps, prevState) {
83
+    const { state } = this
84
+
85
+    console.log('%c<Workspace> did update', `color: ${this.state.config.hexcolor}`, prevState, state)
86
+
87
+    if (!prevState.content || !state.content) return
88
+
89
+    if (prevState.content.content_id !== state.content.content_id) this.loadContent()
90
+
91
+    if (state.mode === MODE.EDIT && prevState.mode !== state.mode) {
92
+      tinymce.remove('#wysiwygNewVersion')
93
+      wysiwyg('#wysiwygNewVersion', this.handleChangeText)
94
+    }
95
+
96
+    if (!prevState.timelineWysiwyg && state.timelineWysiwyg) wysiwyg('#wysiwygTimelineComment', this.handleChangeNewComment)
97
+    else if (prevState.timelineWysiwyg && !state.timelineWysiwyg) tinymce.remove('#wysiwygTimelineComment')
98
+  }
99
+
100
+  componentWillUnmount () {
101
+    console.log('%c<Workspace> will Unmount', `color: ${this.state.config.hexcolor}`)
102
+    document.removeEventListener('appCustomEvent', this.customEventReducer)
103
+  }
104
+
105
+  loadContent = async () => {
106
+    const { loggedUser, content, config } = this.state
107
+
108
+    const fetchResultHtmlDocument = getHtmlDocContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
109
+    const fetchResultComment = getHtmlDocComment(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
110
+    const fetchResultRevision = getHtmlDocRevision(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
111
+
112
+    handleFetchResult(await fetchResultHtmlDocument)
113
+      .then(resHtmlDocument => this.setState({content: resHtmlDocument.body}))
114
+      .catch(e => console.log('Error loading content.', e))
115
+
116
+    Promise.all([
117
+      handleFetchResult(await fetchResultComment),
118
+      handleFetchResult(await fetchResultRevision)
119
+    ])
120
+      .then(([resComment, resRevision]) => {
121
+        const resCommentWithProperDate = resComment.body.map(c => ({...c, created: (new Date(c.created)).toLocaleString()}))
122
+
123
+        const revisionWithComment = resRevision.body
124
+          .map((r, i) => ({
125
+            ...r,
126
+            created: (new Date(r.created)).toLocaleString(),
127
+            timelineType: 'revision',
128
+            commentList: r.comment_ids.map(ci => ({
129
+              timelineType: 'comment',
130
+              ...resCommentWithProperDate.find(c => c.content_id === ci)
131
+            })),
132
+            number: i + 1
133
+          }))
134
+          .reduce((acc, rev) => [
135
+            ...acc,
136
+            rev,
137
+            ...rev.commentList.map(comment => ({
138
+              ...comment,
139
+              customClass: '',
140
+              loggedUser: this.state.config.loggedUser
141
+            }))
142
+          ], [])
143
+
144
+        this.setState({
145
+          timeline: revisionWithComment,
146
+          mode: resRevision.body.length === 1 ? MODE.EDIT : MODE.VIEW // first time editing the doc, open in edit mode
147
+        })
148
+      })
149
+      .catch(e => {
150
+        console.log('Error loading Timeline.', e)
151
+        this.setState({timeline: []})
152
+      })
153
+  }
154
+
155
+  handleClickBtnCloseApp = () => {
156
+    this.setState({ isVisible: false })
157
+    GLOBAL_dispatchEvent({type: 'appClosed', data: {}}) // handled by tracim_front::src/container/WorkspaceContent.jsx
158
+  }
159
+
160
+  handleSaveEditTitle = async newTitle => {
161
+    const { loggedUser, config, content } = this.state
162
+
163
+    const fetchResultSaveHtmlDoc = putHtmlDocContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newTitle, content.raw_content)
164
+
165
+    handleFetchResult(await fetchResultSaveHtmlDoc)
166
+      .then(resSave => {
167
+        if (resSave.apiResponse.status === 200) {
168
+          this.loadContent()
169
+          GLOBAL_dispatchEvent({ type: 'refreshContentList', data: {} })
170
+        } else {
171
+          console.warn('Error saving html-document. Result:', resSave, 'content:', content, 'config:', config)
172
+        }
173
+      })
174
+  }
175
+
176
+  handleClickNewVersion = () => this.setState(prev => ({
177
+    rawContentBeforeEdit: prev.content.raw_content,
178
+    mode: MODE.EDIT
179
+  }))
180
+
181
+  handleCloseNewVersion = () => {
182
+    tinymce.remove('#wysiwygNewVersion')
183
+    this.setState(prev => ({
184
+      content: {
185
+        ...prev.content,
186
+        raw_content: prev.rawContentBeforeEdit
187
+      },
188
+      mode: MODE.VIEW
189
+    }))
190
+  }
191
+
192
+  handleSaveHtmlDocument = async () => {
193
+    const { loggedUser, content, config } = this.state
194
+
195
+    const fetchResultSaveHtmlDoc = putHtmlDocContent(loggedUser, config.apiUrl, content.workspace_id, content.content_id, content.label, content.raw_content)
196
+
197
+    handleFetchResult(await fetchResultSaveHtmlDoc)
198
+      .then(resSave => {
199
+        if (resSave.apiResponse.status === 200) {
200
+          this.handleCloseNewVersion()
201
+          this.loadContent()
202
+        } else {
203
+          console.warn('Error saving html-document. Result:', resSave, 'content:', content, 'config:', config)
204
+        }
205
+      })
206
+  }
207
+
208
+  handleChangeText = e => {
209
+    const newText = e.target.value // because SyntheticEvent is pooled (react specificity)
210
+    this.setState(prev => ({content: {...prev.content, raw_content: newText}}))
211
+  }
212
+
213
+  handleChangeNewComment = e => {
214
+    const newComment = e.target.value
215
+    this.setState({newComment})
216
+  }
217
+
218
+  handleClickValidateNewCommentBtn = async () => {
219
+    const { loggedUser, config, content, newComment } = this.state
220
+
221
+    const fetchResultSaveNewComment = await postHtmlDocNewComment(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newComment)
222
+
223
+    handleFetchResult(await fetchResultSaveNewComment)
224
+      .then(resSave => {
225
+        if (resSave.apiResponse.status === 200) {
226
+          this.setState({newComment: ''})
227
+          if (this.state.timelineWysiwyg) tinymce.get('wysiwygTimelineComment').setContent('')
228
+          this.loadContent()
229
+        } else {
230
+          console.warn('Error saving html-document comment. Result:', resSave, 'content:', content, 'config:', config)
231
+        }
232
+      })
233
+  }
234
+
235
+  handleToggleWysiwyg = () => this.setState(prev => ({timelineWysiwyg: !prev.timelineWysiwyg}))
236
+
237
+  handleChangeStatus = async newStatus => {
238
+    const { loggedUser, config, content } = this.state
239
+
240
+    const fetchResultSaveEditStatus = putHtmlDocStatus(loggedUser, config.apiUrl, content.workspace_id, content.content_id, newStatus)
241
+
242
+    handleFetchResult(await fetchResultSaveEditStatus)
243
+      .then(resSave => {
244
+        if (resSave.status !== 204) { // 204 no content so dont take status from resSave.apiResponse.status
245
+          console.warn('Error saving html-document comment. Result:', resSave, 'content:', content, 'config:', config)
246
+        } else {
247
+          this.loadContent()
248
+        }
249
+      })
250
+  }
251
+
252
+  handleClickArchive = async () => {
253
+    console.log('archive')
254
+    // const { config, content } = this.state
255
+    //
256
+    // const fetchResultArchive = await fetch(`${config.apiUrl}/workspaces/${content.workspace_id}/contents/${content.content_id}/archive`, {
257
+    //   ...FETCH_CONFIG,
258
+    //   method: 'PUT'
259
+    // })
260
+  }
261
+
262
+  handleClickDelete = async () => {
263
+    console.log('delete')
264
+    // const { config, content } = this.state
265
+    // const fetchResultDelete = await fetch(`${config.apiUrl}/workspaces/${content.workspace_id}/contents/${content.content_id}/delete`, {
266
+    //   ...FETCH_CONFIG,
267
+    //   method: 'PUT'
268
+    // })
269
+  }
270
+
271
+  handleClickShowRevision = revision => {
272
+    const { mode, timeline } = this.state
273
+
274
+    const revisionArray = timeline.filter(t => t.timelineType === 'revision')
275
+    const isLastRevision = revision.revision_id === revisionArray[revisionArray.length - 1].revision_id
276
+
277
+    if (mode === MODE.REVISION && isLastRevision) {
278
+      this.handleClickLastVersion()
279
+      return
280
+    }
281
+
282
+    if (mode === MODE.VIEW && isLastRevision) return
283
+
284
+    this.setState(prev => ({
285
+      content: {
286
+        ...prev.content,
287
+        label: revision.label,
288
+        raw_content: revision.raw_content,
289
+        number: revision.number,
290
+        status: revision.status
291
+      },
292
+      mode: MODE.REVISION
293
+    }))
294
+  }
295
+
296
+  handleClickLastVersion = () => {
297
+    this.loadContent()
298
+    this.setState({mode: MODE.VIEW})
299
+  }
300
+
301
+  render () {
302
+    const { isVisible, loggedUser, content, timeline, newComment, timelineWysiwyg, config, mode } = this.state
303
+    const { t } = this.props
304
+
305
+    if (!isVisible) return null
306
+
307
+    return (
308
+      <PopinFixed
309
+        customClass={`${config.slug}`}
310
+        customColor={config.hexcolor}
311
+      >
312
+        <PopinFixedHeader
313
+          customClass={`${config.slug}`}
314
+          customColor={config.hexcolor}
315
+          faIcon={config.faIcon}
316
+          title={content.label}
317
+          onClickCloseBtn={this.handleClickBtnCloseApp}
318
+          onValidateChangeTitle={this.handleSaveEditTitle}
319
+        />
320
+
321
+        <PopinFixedOption
322
+          customColor={config.hexcolor}
323
+          customClass={`${config.slug}`}
324
+          i18n={i18n}
325
+        >
326
+          <div /* this div in display flex, justify-content space-between */>
327
+            <div className='d-flex'>
328
+              <NewVersionBtn
329
+                customColor={config.hexcolor}
330
+                onClickNewVersionBtn={this.handleClickNewVersion}
331
+                disabled={mode !== MODE.VIEW}
332
+              />
333
+
334
+              {mode === MODE.REVISION &&
335
+                <button
336
+                  className='wsContentGeneric__option__menu__lastversion html-document__lastversionbtn btn'
337
+                  onClick={this.handleClickLastVersion}
338
+                  style={{backgroundColor: config.hexcolor, color: '#fdfdfd'}}
339
+                >
340
+                  <i className='fa fa-code-fork' />
341
+                  {t('Last version')}
342
+                </button>
343
+              }
344
+            </div>
345
+
346
+            <div className='d-flex'>
347
+              <SelectStatus
348
+                selectedStatus={config.availableStatuses.find(s => s.slug === content.status)}
349
+                availableStatus={config.availableStatuses}
350
+                onChangeStatus={this.handleChangeStatus}
351
+                disabled={mode === MODE.REVISION}
352
+              />
353
+
354
+              <ArchiveDeleteContent
355
+                customColor={config.hexcolor}
356
+                onClickArchiveBtn={this.handleClickArchive}
357
+                onClickDeleteBtn={this.handleClickDelete}
358
+                disabled={mode === MODE.REVISION}
359
+              />
360
+            </div>
361
+          </div>
362
+        </PopinFixedOption>
363
+
364
+        <PopinFixedContent
365
+          customClass={`${config.slug}__contentpage`}
366
+          showRightPartOnLoad={mode === MODE.VIEW}
367
+        >
368
+          <HtmlDocumentComponent
369
+            mode={mode}
370
+            customColor={config.hexcolor}
371
+            wysiwygNewVersion={'wysiwygNewVersion'}
372
+            onClickCloseEditMode={this.handleCloseNewVersion}
373
+            onClickValidateBtn={this.handleSaveHtmlDocument}
374
+            version={content.number}
375
+            lastVersion={timeline.filter(t => t.timelineType === 'revision').length}
376
+            text={content.raw_content}
377
+            onChangeText={this.handleChangeText}
378
+            key={'html-document'}
379
+          />
380
+
381
+          <Timeline
382
+            customClass={`${config.slug}__contentpage`}
383
+            customColor={config.hexcolor}
384
+            loggedUser={loggedUser}
385
+            timelineData={timeline}
386
+            newComment={newComment}
387
+            disableComment={mode === MODE.REVISION}
388
+            wysiwyg={timelineWysiwyg}
389
+            onChangeNewComment={this.handleChangeNewComment}
390
+            onClickValidateNewCommentBtn={this.handleClickValidateNewCommentBtn}
391
+            onClickWysiwygBtn={this.handleToggleWysiwyg}
392
+            onClickRevisionBtn={this.handleClickShowRevision}
393
+            shouldScrollToBottom={mode !== MODE.REVISION}
394
+          />
395
+        </PopinFixedContent>
396
+      </PopinFixed>
397
+    )
398
+  }
399
+}
400
+
401
+export default translate()(Workspace)

+ 2 - 0
frontend_app_workspace/src/css/index.styl ファイルの表示

@@ -0,0 +1,2 @@
1
+@import "../../node_modules/tracim_frontend_lib/src/css/Variable.styl"
2
+

+ 97 - 0
frontend_app_workspace/src/helper.js ファイルの表示

@@ -0,0 +1,97 @@
1
+export const FETCH_CONFIG = {
2
+  headers: {
3
+    'Accept': 'application/json',
4
+    'Content-Type': 'application/json'
5
+  }
6
+}
7
+
8
+export const debug = {
9
+  config: {
10
+    label: 'Workspace',
11
+    slug: 'workspace',
12
+    faIcon: 'file-text-o',
13
+    hexcolor: '#3f52e3',
14
+    creationLabel: 'Create a workspace',
15
+    domContainer: 'appFeatureContainer',
16
+    apiUrl: 'http://localhost:6543/api/v2',
17
+    apiHeader: {
18
+      'Accept': 'application/json',
19
+      'Content-Type': 'application/json'
20
+      // 'Authorization': 'Basic ' + btoa(`${'admin@admin.admin'}:${'admin@admin.admin'}`)
21
+    },
22
+    availableStatuses: [{
23
+      label: 'Open',
24
+      slug: 'open',
25
+      faIcon: 'square-o',
26
+      hexcolor: '#3f52e3',
27
+      globalStatus: 'open'
28
+    }, {
29
+      label: 'Validated',
30
+      slug: 'closed-validated',
31
+      faIcon: 'check-square-o',
32
+      hexcolor: '#008000',
33
+      globalStatus: 'closed'
34
+    }, {
35
+      label: 'Cancelled',
36
+      slug: 'closed-unvalidated',
37
+      faIcon: 'close',
38
+      hexcolor: '#f63434',
39
+      globalStatus: 'closed'
40
+    }, {
41
+      label: 'Deprecated',
42
+      slug: 'closed-deprecated',
43
+      faIcon: 'warning',
44
+      hexcolor: '#ababab',
45
+      globalStatus: 'closed'
46
+    }],
47
+    translation: {
48
+      en: {
49
+        translation: {
50
+          'Last version': 'Last version debug en'
51
+        }
52
+      },
53
+      fr: {
54
+        translation: {
55
+          'Last version': 'Dernière version debug fr'
56
+        }
57
+      }
58
+    }
59
+  },
60
+  loggedUser: { // @FIXME this object is outdated
61
+    user_id: 5,
62
+    username: 'Smoi',
63
+    firstname: 'Côme',
64
+    lastname: 'Stoilenom',
65
+    email: 'osef@algoo.fr',
66
+    lang: 'en',
67
+    avatar_url: 'https://avatars3.githubusercontent.com/u/11177014?s=460&v=4',
68
+    auth: btoa(`${'admin@admin.admin'}:${'admin@admin.admin'}`)
69
+  },
70
+  content: {
71
+    author: {
72
+      avatar_url: null,
73
+      public_name: 'Global manager',
74
+      user_id: 1 // -1 or 1 for debug
75
+    },
76
+    content_id: 22, // 1 or 22 for debug
77
+    content_type: 'html-document',
78
+    created: '2018-06-18T14:59:26Z',
79
+    current_revision_id: 11,
80
+    is_archived: false,
81
+    is_deleted: false,
82
+    label: 'Current Menu',
83
+    last_modifier: {
84
+      avatar_url: null,
85
+      public_name: 'Global manager',
86
+      user_id: 1
87
+    },
88
+    modified: '2018-06-18T14:59:26Z',
89
+    parent_id: 2,
90
+    raw_content: '<div>bonjour, je suis un lapin.</div>',
91
+    show_in_ui: true,
92
+    slug: 'current-menu',
93
+    status: 'open',
94
+    sub_content_types: ['thread', 'html-document', 'file', 'folder'],
95
+    workspace_id: 1
96
+  }
97
+}

+ 21 - 0
frontend_app_workspace/src/i18n.js ファイルの表示

@@ -0,0 +1,21 @@
1
+import i18n from 'i18next'
2
+import { reactI18nextModule } from 'react-i18next'
3
+
4
+i18n
5
+  .use(reactI18nextModule)
6
+  .init({
7
+    fallbackLng: 'fr',
8
+    // have a common namespace used around the full app
9
+    ns: ['translation'], // namespace
10
+    defaultNS: 'translation',
11
+    debug: true,
12
+    // interpolation: {
13
+    //   escapeValue: false, // not needed for react!!
14
+    // },
15
+    react: {
16
+      wait: true
17
+    },
18
+    resources: {} // init with empty resources, they will come from frontend in app constructor
19
+  })
20
+
21
+export default i18n

+ 16 - 0
frontend_app_workspace/src/index.dev.js ファイルの表示

@@ -0,0 +1,16 @@
1
+import React from 'react'
2
+import ReactDOM from 'react-dom'
3
+// import Workspace from './container/Workspace.jsx'
4
+import PopupCreateWorkspace from './container/PopupCreateWorkspace.jsx'
5
+
6
+require('./css/index.styl')
7
+
8
+// ReactDOM.render(
9
+//   <Workspace data={undefined} />
10
+//   , document.getElementById('content')
11
+// )
12
+
13
+ReactDOM.render(
14
+  <PopupCreateWorkspace />
15
+  , document.getElementById('content')
16
+)

+ 28 - 0
frontend_app_workspace/src/index.js ファイルの表示

@@ -0,0 +1,28 @@
1
+import React from 'react'
2
+import ReactDOM from 'react-dom'
3
+// import Workspace from './container/Workspace.jsx'
4
+import PopupCreateWorkspace from './container/PopupCreateWorkspace.jsx'
5
+
6
+require('./css/index.styl')
7
+
8
+const appInterface = {
9
+  name: 'html-document',
10
+  isRendered: false,
11
+  renderAppFeature: data => {
12
+    return ReactDOM.render(
13
+      null // <Workspace data={data} />
14
+      , document.getElementById(data.config.domContainer)
15
+    )
16
+  },
17
+  unmountApp: domId => {
18
+    return ReactDOM.unmountComponentAtNode(document.getElementById(domId)) // returns bool
19
+  },
20
+  renderAppPopupCreation: data => {
21
+    return ReactDOM.render(
22
+      <PopupCreateWorkspace data={data} />
23
+      , document.getElementById(data.config.domContainer)
24
+    )
25
+  }
26
+}
27
+
28
+module.exports = appInterface

+ 87 - 0
frontend_app_workspace/webpack.config.js ファイルの表示

@@ -0,0 +1,87 @@
1
+const webpack = require('webpack')
2
+const path = require('path')
3
+const isProduction = process.env.NODE_ENV === 'production'
4
+
5
+console.log('isProduction : ', isProduction)
6
+
7
+module.exports = {
8
+  entry: isProduction
9
+    ? './src/index.js' // only one instance of babel-polyfill is allowed
10
+    : ['babel-polyfill', './src/index.dev.js'],
11
+  output: {
12
+    path: path.resolve(__dirname, 'dist'),
13
+    filename: isProduction ? 'workspace.app.js' : 'workspace.app.dev.js',
14
+    pathinfo: !isProduction,
15
+    library: isProduction ? 'appWorkspace' : undefined,
16
+    libraryTarget: isProduction ? 'var' : undefined
17
+  },
18
+  externals: {},
19
+  // isProduction ? { // Côme - since plugins are imported through <script>, cannot externalize libraries
20
+  //   react: {commonjs: 'react', commonjs2: 'react', amd: 'react', root: '_'},
21
+  //   'react-dom': {commonjs: 'react-dom', commonjs2: 'react-dom', amd: 'react-dom', root: '_'},
22
+  //   classnames: {commonjs: 'classnames', commonjs2: 'classnames', amd: 'classnames', root: '_'},
23
+  //   'prop-types': {commonjs: 'prop-types', commonjs2: 'prop-types', amd: 'prop-types', root: '_'},
24
+  //   tracim_lib: {commonjs: 'tracim_lib', commonjs2: 'tracim_lib', amd: 'tracim_lib', root: '_'}
25
+  // }
26
+  // : {},
27
+  devServer: {
28
+    contentBase: path.join(__dirname, 'dist/'),
29
+    port: 8074,
30
+    hot: true,
31
+    noInfo: true,
32
+    overlay: {
33
+      warnings: false,
34
+      errors: true
35
+    },
36
+    historyApiFallback: true
37
+    // headers: {
38
+    //   'Access-Control-Allow-Origin': '*'
39
+    // }
40
+  },
41
+  devtool: isProduction ? false : 'cheap-module-source-map',
42
+  module: {
43
+    rules: [{
44
+      test: /\.jsx?$/,
45
+      enforce: 'pre',
46
+      use: 'standard-loader',
47
+      exclude: [/node_modules/]
48
+    }, {
49
+      test: [/\.js$/, /\.jsx$/],
50
+      loader: 'babel-loader',
51
+      options: {
52
+        presets: ['env', 'react'],
53
+        plugins: ['transform-object-rest-spread', 'transform-class-properties', 'transform-object-assign']
54
+      },
55
+      exclude: [/node_modules/]
56
+    }, {
57
+      test: /\.css$/,
58
+      use: ['style-loader', 'css-loader']
59
+    }, {
60
+      test: /\.styl$/,
61
+      use: ['style-loader', 'css-loader', 'stylus-loader']
62
+    }, {
63
+      test: /\.(jpg|png|svg)$/,
64
+      loader: 'url-loader',
65
+      options: {
66
+        limit: 25000
67
+      }
68
+    }]
69
+  },
70
+  resolve: {
71
+    extensions: ['.js', '.jsx']
72
+  },
73
+  plugins: [
74
+    ...[], // generic plugins always present
75
+    ...(isProduction
76
+      ? [ // production specific plugins
77
+        new webpack.DefinePlugin({
78
+          'process.env': { 'NODE_ENV': JSON.stringify('production') }
79
+        }),
80
+        new webpack.optimize.UglifyJsPlugin({
81
+          compress: { warnings: false }
82
+        })
83
+      ]
84
+      : [] // development specific plugins
85
+    )
86
+  ]
87
+}

+ 1 - 1
frontend_lib/src/component/CardPopup/CardPopupCreateContent.jsx ファイルの表示

@@ -27,7 +27,7 @@ const PopupCreateContent = props => {
27 27
           <input
28 28
             type='text'
29 29
             className='createcontent__form__input'
30
-            placeHolder={props.inputPlaceholder}
30
+            placeholder={props.inputPlaceholder}
31 31
             value={props.contentName}
32 32
             onChange={props.onChangeContentName}
33 33
           />