Browse Source

integration of Thread with real api and new app interface

Skylsmoi 5 years ago
parent
commit
abc9c18d35
10 changed files with 456 additions and 265 deletions
  1. 82 131
      dist/index.html
  2. 7 1
      package.json
  3. 42 0
      src/action.async.js
  4. 0 56
      src/component/Thread.jsx
  5. 96 0
      src/container/PopupCreateThread.jsx
  6. 130 75
      src/container/Thread.jsx
  7. 4 0
      src/css/index.styl
  8. 81 1
      src/helper.js
  9. 6 0
      src/index.dev.js
  10. 8 1
      src/index.js

+ 82 - 131
dist/index.html View File

@@ -11,148 +11,99 @@
11 11
   <link rel="stylesheet" type="text/css" href="./dev/bootstrap-4.0.0-beta.css">
12 12
 </head>
13 13
 <body>
14
-  <script src="./dev/jquery-3.2.1.js"></script>
15
-  <script src="./dev/popper-1.12.3.js"></script>
16
-  <script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
17
-
18
-  <div id='content'></div>
19
-
20
-  <script src='./thread.app.dev.js'></script>
21
-
22
-  <script type='text/javascript'>
23
-    GLOBAL_renderApp = app => {
24
-      switch (app.appData.name) {
25
-        case 'PageHtml':
26
-          appPageHtml.renderApp('appContainer'); break
27
-        case 'Thread':
28
-          appThread.renderApp('appContainer'); break
29
-      }
30
-    }
31
-
32
-    GLOBAL_dispatchEvent = (data) => {
33
-      var event = new CustomEvent('appCustomEvent', {detail: data})
34
-      document.dispatchEvent(event)
35
-    }
36
-
37
-    GLOBAL_unmountApp = () => {
38
-      switch (appName) {
39
-        case 'PageHtml':
40
-          appPageHtml.hideApp('appContainer'); break
41
-        case 'Thread':
42
-          appThread.hideApp('appContainer'); break
43
-      }
44
-    }
45
-
46
-    // only usefull if app doesn't handle fileContent himself
47
-    GLOBAL_handleRequireRedraw = () => {
48
-      var rez = appA.destroyApp('app')
49
-      if (rez) GLOBAL_drawApp('redraw')
50
-      else console.log('Erreur, failed at destroying app')
51
-    }
52
-  </script>
53
-
54
-  <script type='text/javascript'>
55
-    // appPageHtml.renderApp('content')
56
-  </script>
57
-
58
-  <script type="text/javascript" src="/asset/tinymce/jquery.tinymce.min.js"></script>
59
-  <script type="text/javascript" src="/asset/tinymce/tinymce.min.js"></script>
60
-
61
-  <script type='text/javascript'>
62
-      if (window.matchMedia("(min-width:1200px)").matches) {
63
-
64
-        var jsScript = document.createElement("script");
65
-        jsScript.type = "text/javascript";
66
-        jsScript.src = "https://cloud.tinymce.com/stable/tinymce.min.js?apiKey=dfdhxdokxj2wagzkbxfgysgh86d6rr9m3dln0172vo3shipc";
67
-
68
-        jsScript.onload = function() {
69
-          function base64EncodeAndTinyMceInsert (files) {
70
-            for (var i = 0; i < files.length; i++) {
71
-              if (files[i].size > 1000000)
72
-                files[i].allowed = confirm(files[i].name + " fait plus de 1mo et peut prendre du temps à insérer, voulez-vous continuer ?")
73
-            }
14
+<script src="./dev/jquery-3.2.1.js"></script>
15
+<script src="./dev/popper-1.12.3.js"></script>
16
+<script src="./dev/bootstrap-4.0.0-beta.2.js"></script>
17
+
18
+<script type="text/javascript" src="/asset/tinymce/js/tinymce/jquery.tinymce.min.js"></script>
19
+<script type="text/javascript" src="/asset/tinymce/js/tinymce/tinymce.min.js"></script>
20
+
21
+<div id='content'></div>
22
+
23
+<script type='text/javascript'>
24
+  (function () {
25
+    wysiwyg = function (selector, handleOnChange) {
26
+      function base64EncodeAndTinyMceInsert (files) {
27
+        for (var i = 0; i < files.length; i++) {
28
+          if (files[i].size > 1000000)
29
+            files[i].allowed = confirm(files[i].name + " fait plus de 1mo et peut prendre du temps à insérer, voulez-vous continuer ?")
30
+        }
74 31
 
75
-            for (var i = 0; i < files.length; i++) {
76
-              if (files[i].allowed !== false && files[i].type.match('image.*')) {
77
-                var img = document.createElement('img')
32
+        for (var i = 0; i < files.length; i++) {
33
+          if (files[i].allowed !== false && files[i].type.match('image.*')) {
34
+            var img = document.createElement('img')
78 35
 
79
-                var fr = new FileReader()
36
+            var fr = new FileReader()
80 37
 
81
-                fr.readAsDataURL(files[i])
38
+            fr.readAsDataURL(files[i])
82 39
 
83
-                fr.onloadend = function (e) {
84
-                  img.src = e.target.result
85
-                  tinymce.activeEditor.execCommand('mceInsertContent', false, img.outerHTML)
86
-                }
87
-              }
40
+            fr.onloadend = function (e) {
41
+              img.src = e.target.result
42
+              tinymce.activeEditor.execCommand('mceInsertContent', false, img.outerHTML)
88 43
             }
89 44
           }
45
+        }
46
+      }
90 47
 
91
-          // HACK: The tiny mce source code modal contain a textarea, but we
92
-          // can't edit it (like it's readonly). The following solution
93
-          // solve the bug: https://stackoverflow.com/questions/36952148/tinymce-code-editor-is-readonly-in-jtable-grid
94
-          $(document).on('focusin', function(e) {
95
-            if ($(e.target).closest(".mce-window").length) {
96
-              e.stopImmediatePropagation();
97
-            }
98
-          });
99
-
100
-          tinymce.init({
101
-            selector: 'textarea',
102
-            height: 130,
103
-            // width: 530,
104
-            menubar: false,
105
-            resize: false,
106
-            plugins: [
107
-              'advlist autolink lists link image charmap print preview anchor textcolor',
108
-              'searchreplace visualblocks code fullscreen',
109
-              'insertdatetime media table contextmenu paste code help'
110
-            ],
111
-            toolbar: 'insert | formatselect | bold italic underline strikethrough forecolor backcolor | link | alignleft aligncenter alignright alignjustify  | numlist bullist outdent indent  | table | code ',
112
-            content_css: [
113
-              '//fonts.googleapis.com/css?family=Lato:300,300i,400,400i',
114
-              '//www.tinymce.com/css/codepen.min.css'
115
-            ],
116
-            setup: function ($editor) {
117
-              //////////////////////////////////////////////
118
-              // add custom btn to handle image by selecting them with system explorer
119
-              $editor.addButton('customInsertImage', {
120
-                icon: 'mce-ico mce-i-image',
121
-                title: 'Image',
122
-                onclick: function () {
123
-                  var hiddenTinyMceInput = $('#hidden_tinymce_fileinput')
124
-
125
-                  if (hiddenTinyMceInput.length > 0) hiddenTinyMceInput.remove()
126
-
127
-                  fileTag = document.createElement('input')
128
-                  fileTag.id = 'hidden_tinymce_fileinput'
129
-                  fileTag.type = 'file'
130
-                  $('body').append(fileTag)
131
-
132
-                  hiddenTinyMceInput.on('change', function () {
133
-                    base64EncodeAndTinyMceInsert($(this)[0].files)
134
-                  })
135
-
136
-                  hiddenTinyMceInput.click()
137
-                }
48
+      // HACK: The tiny mce source code modal contain a textarea, but we
49
+      // can't edit it (like it's readonly). The following solution
50
+      // solve the bug: https://stackoverflow.com/questions/36952148/tinymce-code-editor-is-readonly-in-jtable-grid
51
+      $(document).on('focusin', function(e) {
52
+        if ($(e.target).closest(".mce-window").length) {
53
+          e.stopImmediatePropagation();
54
+        }
55
+      });
56
+
57
+      tinymce.init({
58
+        selector: selector,
59
+        menubar: false,
60
+        resize: false,
61
+        skin: "lightgray",
62
+        plugins:'advlist autolink lists link image charmap print preview anchor textcolor searchreplace visualblocks code fullscreen insertdatetime media table contextmenu paste code help',
63
+        toolbar: 'insert | formatselect | bold italic underline strikethrough forecolor backcolor | link | alignleft aligncenter alignright alignjustify  | numlist bullist outdent indent  | table | code ',
64
+        content_style: "div {height: 100%;}",
65
+        setup: function ($editor) {
66
+          $editor.on('change', function(e) {
67
+            handleOnChange({target: {value: $editor.getContent()}}) // target.value to emulate a js event so the react handler can expect one
68
+          })
69
+
70
+          //////////////////////////////////////////////
71
+          // add custom btn to handle image by selecting them with system explorer
72
+          $editor.addButton('customInsertImage', {
73
+            icon: 'mce-ico mce-i-image',
74
+            title: 'Image',
75
+            onclick: function () {
76
+              if ($('#hidden_tinymce_fileinput').length > 0) $('#hidden_tinymce_fileinput').remove()
77
+
78
+              fileTag = document.createElement('input')
79
+              fileTag.id = 'hidden_tinymce_fileinput'
80
+              fileTag.type = 'file'
81
+              $('body').append(fileTag)
82
+
83
+              $('#hidden_tinymce_fileinput').on('change', function () {
84
+                base64EncodeAndTinyMceInsert($(this)[0].files)
138 85
               })
139 86
 
140
-              //////////////////////////////////////////////
141
-              // Handle drag & drop image into TinyMce by encoding them in base64 (to avoid uploading them somewhere and keep saving comment in string format)
142
-              $editor
143
-                .on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
144
-                  e.preventDefault()
145
-                  e.stopPropagation()
146
-                })
147
-                .on('drop', function(e) {
148
-                  base64EncodeAndTinyMceInsert(e.dataTransfer.files)
149
-                })
87
+              $('#hidden_tinymce_fileinput').click()
150 88
             }
151
-          });
89
+          })
90
+
91
+          //////////////////////////////////////////////
92
+          // Handle drag & drop image into TinyMce by encoding them in base64 (to avoid uploading them somewhere and keep saving comment in string format)
93
+          $editor
94
+            .on('drag dragstart dragend dragover dragenter dragleave drop', function (e) {
95
+              e.preventDefault()
96
+              e.stopPropagation()
97
+            })
98
+            .on('drop', function(e) {
99
+              base64EncodeAndTinyMceInsert(e.dataTransfer.files)
100
+            })
152 101
         }
153
-        document.getElementsByTagName("body")[0].appendChild(jsScript);
154
-      }
155
-    </script>
102
+      })
103
+    }
104
+  })()
105
+</script>
156 106
 
107
+<script src='./thread.app.dev.js'></script>
157 108
 </body>
158 109
 </html>

+ 7 - 1
package.json View File

@@ -5,8 +5,10 @@
5 5
   "main": "index.js",
6 6
   "scripts": {
7 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",
8 9
     "servdev-dashboard": "NODE_ENV=development webpack-dashboard -m -p 9872 -- webpack-dev-server --watch --colors --inline --hot --progress",
9 10
     "build": "NODE_ENV=production webpack -p",
11
+    "buildwindoz": "set NODE_ENV=production&& webpack -p",
10 12
     "test": "echo \"Error: no test specified\" && exit 1"
11 13
   },
12 14
   "author": "",
@@ -47,8 +49,12 @@
47 49
     "globals": [
48 50
       "fetch",
49 51
       "history",
52
+      "btoa",
53
+      "wysiwyg",
54
+      "tinymce",
50 55
       "GLOBAL_renderApp",
51
-      "GLOBAL_unmountApp"
56
+      "GLOBAL_unmountApp",
57
+      "GLOBAL_dispatchEvent"
52 58
     ],
53 59
     "parser": "babel-eslint",
54 60
     "ignore": []

+ 42 - 0
src/action.async.js View File

@@ -0,0 +1,42 @@
1
+import { FETCH_CONFIG } from './helper.js'
2
+
3
+export const getThreadContent = (apiUrl, idWorkspace, idContent) =>
4
+  fetch(`${apiUrl}/workspaces/${idWorkspace}/threads/${idContent}`, {
5
+    ...FETCH_CONFIG,
6
+    method: 'GET'
7
+  })
8
+
9
+export const getThreadComment = (apiUrl, idWorkspace, idContent) =>
10
+  fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/comments`, {
11
+    ...FETCH_CONFIG,
12
+    method: 'GET'
13
+  })
14
+
15
+export const postThreadNewComment = (apiUrl, idWorkspace, idContent, newComment) =>
16
+  fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/comments`, {
17
+    ...FETCH_CONFIG,
18
+    method: 'POST',
19
+    body: JSON.stringify({
20
+      raw_content: newComment
21
+    })
22
+  })
23
+
24
+export const putThreadStatus = (apiUrl, idWorkspace, idContent, newStatus) =>
25
+  fetch(`${apiUrl}/workspaces/${idWorkspace}/threads/${idContent}/status`, {
26
+    ...FETCH_CONFIG,
27
+    method: 'PUT',
28
+    body: JSON.stringify({
29
+      status: newStatus
30
+    })
31
+  })
32
+
33
+export const postThreadContent = (apiUrl, idWorkspace, idFolder, contentType, newContentName) =>
34
+  fetch(`${apiUrl}/workspaces/${idWorkspace}/contents`, {
35
+    ...FETCH_CONFIG,
36
+    method: 'POST',
37
+    body: JSON.stringify({
38
+      parent_id: idFolder,
39
+      content_type: contentType,
40
+      label: newContentName
41
+    })
42
+  })

+ 0 - 56
src/component/Thread.jsx View File

@@ -1,56 +0,0 @@
1
-import React from 'react'
2
-import classnames from 'classnames'
3
-
4
-const Thread = props => {
5
-  return (
6
-    <div className='wsContentThread__app'>
7
-      <ul className='wsContentThread__app__messagelist wsContentGeneric__messagelist'>
8
-        { props.listMessage.map(msg =>
9
-          <li className={classnames('wsContentThread__app__messagelist__item', 'wsContentGeneric__messagelist__item', {
10
-            'sended': props.loggedUser.id === msg.author.id,
11
-            'received': !(props.loggedUser.id === msg.author.id)
12
-          })} key={msg.id}>
13
-            <div className='wsContentThread__app__messagelist__item__avatar wsContentGeneric__messagelist__item__avatar'>
14
-              <img src={msg.author.avatar} alt='avatar' />
15
-            </div>
16
-
17
-            <div className='wsContentThread__app__messagelist__item__createhour wsContentGeneric__messagelist__item__createhour'>
18
-              {msg.createdAt.day} à {msg.createdAt.hour}
19
-            </div>
20
-
21
-            <div className='wsContentThread__app__messagelist__item__content wsContentGeneric__messagelist__item__content'>
22
-              {msg.text}
23
-            </div>
24
-          </li>
25
-        )}
26
-      </ul>
27
-
28
-      <form className='wsContentThread__app__texteditor wsContentGeneric__texteditor d-flex align-items-center justify-content-between flex-wrap'>
29
-        <div className='wsContentThread__app__texteditor__textinput wsContentGeneric__texteditor__textinput'>
30
-          <textarea placeholder='Taper votre message ici'/>
31
-        </div>
32
-
33
-        <div className='wsContentThread__app__texteditor__wrapper'>
34
-
35
-          <div className='wsContentThread__app__texteditor__advancedtext wsContentGeneric__texteditor__advancedtext mb-2'>
36
-            <button type='button' className='wsContentThread__app__texteditor__advancedtext__btn wsContentGeneric__texteditor__advancedtext__btn btn btn-outline-primary'>
37
-              Texte Avancé
38
-            </button>
39
-          </div>
40
-
41
-          <div className='wsContentThread__app__texteditor__submit wsContentGeneric__texteditor__submit mb-2'>
42
-            <button type='submit' className='wsContentThread__app__texteditor__submit__btn wsContentGeneric__texteditor__submit__btn btn btn-primary'>
43
-              Envoyer
44
-              <div className='wsContentThread__app__texteditor__submit__btn__icon wsContentGeneric__texteditor__submit__btn__icon ml-3'>
45
-                <i className='fa fa-paper-plane-o' />
46
-              </div>
47
-            </button>
48
-          </div>
49
-
50
-        </div>
51
-      </form>
52
-    </div>
53
-  )
54
-}
55
-
56
-export default Thread

+ 96 - 0
src/container/PopupCreateThread.jsx View File

@@ -0,0 +1,96 @@
1
+import React from 'react'
2
+import {
3
+  CardPopupCreateContent,
4
+  handleFetchResult
5
+} from 'tracim_lib'
6
+import { postThreadContent } from '../action.async.js'
7
+
8
+const debug = { // outdated
9
+  config: {
10
+    label: 'Thread',
11
+    slug: 'thread',
12
+    faIcon: 'file-text-o',
13
+    hexcolor: '#3f52e3',
14
+    creationLabel: 'Write a thread',
15
+    domContainer: 'appContainer',
16
+    apiUrl: 'http://localhost:3001',
17
+    mockApiUrl: 'http://localhost:8071',
18
+    apiHeader: {
19
+      'Accept': 'application/json',
20
+      'Content-Type': 'application/json',
21
+      'Authorization': 'Basic ' + btoa(`${'admin@admin.admin'}:${'admin@admin.admin'}`)
22
+    }
23
+  },
24
+  loggedUser: {
25
+    id: 5,
26
+    username: 'Smoi',
27
+    firstname: 'Côme',
28
+    lastname: 'Stoilenom',
29
+    email: 'osef@algoo.fr',
30
+    avatar: 'https://avatars3.githubusercontent.com/u/11177014?s=460&v=4'
31
+  },
32
+  idWorkspace: 1,
33
+  idFolder: null
34
+}
35
+
36
+class PopupCreateHtmlDocument extends React.Component {
37
+  constructor (props) {
38
+    super(props)
39
+    this.state = {
40
+      appName: 'thread',
41
+      config: props.data ? props.data.config : debug.config,
42
+      loggedUser: props.data ? props.data.loggedUser : debug.loggedUser,
43
+      idWorkspace: props.data ? props.data.idWorkspace : debug.idWorkspace,
44
+      idFolder: props.data ? props.data.idFolder : debug.idFolder,
45
+      newContentName: ''
46
+    }
47
+  }
48
+
49
+  handleChangeNewContentName = e => this.setState({newContentName: e.target.value})
50
+
51
+  handleClose = () => GLOBAL_dispatchEvent({
52
+    type: 'hide_popupCreateContent', // handled by tracim_front:dist/index.html
53
+    data: {
54
+      name: this.state.appName
55
+    }
56
+  })
57
+
58
+  handleValidate = async () => {
59
+    const { config, appName, idWorkspace, idFolder, newContentName } = this.state
60
+
61
+    const fetchSaveThreadDoc = postThreadContent(config.apiUrl, idWorkspace, idFolder, config.slug, newContentName)
62
+
63
+    handleFetchResult(await fetchSaveThreadDoc)
64
+      .then(resSave => {
65
+        if (resSave.apiResponse.status === 200) {
66
+          this.handleClose()
67
+
68
+          GLOBAL_dispatchEvent({
69
+            type: 'openContentUrl', // handled by tracim_front:src/container/WorkspaceContent.jsx
70
+            data: {
71
+              idWorkspace: resSave.body.workspace_id,
72
+              contentType: appName,
73
+              idContent: resSave.body.content_id
74
+            }
75
+          })
76
+        }
77
+      })
78
+  }
79
+
80
+  render () {
81
+    return (
82
+      <CardPopupCreateContent
83
+        onClose={this.handleClose}
84
+        onValidate={this.handleValidate}
85
+        label={this.state.config.label} // @TODO get the lang of user
86
+        hexcolor={this.state.config.hexcolor}
87
+        faIcon={this.state.config.faIcon}
88
+        contentName={this.state.newContentName}
89
+        onChangeContentName={this.handleChangeNewContentName}
90
+        btnValidateLabel='Valider et créer'
91
+      />
92
+    )
93
+  }
94
+}
95
+
96
+export default PopupCreateHtmlDocument

+ 130 - 75
src/container/Thread.jsx View File

@@ -1,64 +1,35 @@
1 1
 import React from 'react'
2
-import ThreadComponent from '../component/Thread.jsx'
2
+import i18n from '../i18n.js'
3
+import { debug } from '../helper.js'
3 4
 import {
4 5
   handleFetchResult,
5 6
   PopinFixed,
6 7
   PopinFixedHeader,
7 8
   PopinFixedOption,
8
-  PopinFixedContent
9
+  PopinFixedContent,
10
+  Timeline,
11
+  SelectStatus,
12
+  ArchiveDeleteContent
9 13
 } from 'tracim_lib'
10
-import { listMessageDebugData } from '../listMessageDebugData.js'
11
-import { FETCH_CONFIG } from '../helper.js'
12
-import i18n from '../i18n.js'
13
-
14
-const debug = {
15
-  config: {
16
-    name: 'Thread',
17
-    label: {
18
-      fr: 'Discussion',
19
-      en: 'Thread'
20
-    },
21
-    componentLeft: 'Thread',
22
-    componentRight: 'undefined',
23
-    customClass: 'wsContentThread',
24
-    icon: 'fa fa-comments-o',
25
-    color: '#65c7f2',
26
-    domContainer: 'appContainer',
27
-    apiUrl: 'http://localhost:3001',
28
-    mockApiUrl: 'http://localhost:3001'
29
-  },
30
-  loggedUser: {
31
-    id: 5,
32
-    username: 'Stoi',
33
-    firstname: 'John',
34
-    lastname: 'Doe',
35
-    email: 'osef@algoo.fr',
36
-    avatar: 'https://avatars3.githubusercontent.com/u/11177014?s=460&v=4'
37
-  },
38
-  content: {
39
-    id: 2,
40
-    type: 'thread',
41
-    status: 'validated',
42
-    title: 'test debug title',
43
-    workspace: {
44
-      id: 1,
45
-      title: 'Test debug workspace',
46
-      ownerId: 5
47
-    }
48
-  },
49
-  listMessage: listMessageDebugData
50
-}
14
+import {
15
+  getThreadContent,
16
+  getThreadComment,
17
+  postThreadNewComment,
18
+  putThreadStatus
19
+} from '../action.async.js'
51 20
 
52 21
 class Thread extends React.Component {
53 22
   constructor (props) {
54 23
     super(props)
55 24
     this.state = {
56
-      appName: 'Thread',
25
+      appName: 'thread',
57 26
       isVisible: true,
58 27
       config: props.data ? props.data.config : debug.config,
59 28
       loggedUser: props.data ? props.data.loggedUser : debug.loggedUser,
60 29
       content: props.data ? props.data.content : debug.content,
61
-      listMessage: props.data ? [] : debug.listMessage
30
+      listMessage: props.data ? [] : [], // debug.listMessage,
31
+      newComment: '',
32
+      timelineWysiwyg: false
62 33
     }
63 34
 
64 35
     document.addEventListener('appCustomEvent', this.customEventReducer)
@@ -75,55 +46,139 @@ class Thread extends React.Component {
75 46
     }
76 47
   }
77 48
 
78
-  async componentDidMount () {
49
+  componentDidMount () {
50
+    console.log('Thread did Mount')
51
+    this.loadContent()
52
+  }
53
+
54
+  componentDidUpdate (prevProps, prevState) {
55
+    const { state } = this
56
+
57
+    console.log('Thread did Update', prevState, state)
58
+    if (!prevState.content || !state.content) return
59
+
60
+    if (prevState.content.content_id !== state.content.content_id) this.loadContent()
61
+
62
+    if (!prevState.timelineWysiwyg && state.timelineWysiwyg) wysiwyg('#wysiwygTimelineComment', this.handleChangeNewComment)
63
+    else if (prevState.timelineWysiwyg && !state.timelineWysiwyg) tinymce.remove('#wysiwygTimelineComment')
64
+  }
65
+
66
+  loadContent = async () => {
79 67
     const { content, config } = this.state
80
-    if (content.id === '-1') return // debug case
81
-
82
-    const fetchResultThread = await fetch(`${config.mockApiUrl}/workspace/${content.workspace.id}/content/${content.id}`, {
83
-      ...FETCH_CONFIG,
84
-      method: 'GET'
85
-    })
86
-
87
-    fetchResultThread.json = await handleFetchResult(fetchResultThread)
88
-
89
-    this.setState({
90
-      content: {
91
-        id: fetchResultThread.json.id,
92
-        status: fetchResultThread.json.status,
93
-        title: fetchResultThread.json.title,
94
-        type: fetchResultThread.json.type
95
-      },
96
-      listMessage: fetchResultThread.json.message_list
97
-    })
98
-
99
-    // wysiwyg()
68
+
69
+    if (content.content_id === '-1') return // debug case
70
+
71
+    const fetchResultThread = getThreadContent(config.apiUrl, content.workspace_id, content.content_id)
72
+    const fetchResultThreadComment = getThreadComment(config.apiUrl, content.workspace_id, content.content_id)
73
+
74
+    Promise.all([
75
+      handleFetchResult(await fetchResultThread),
76
+      handleFetchResult(await fetchResultThreadComment)
77
+    ])
78
+      .then(([resThread, resComment]) => this.setState({
79
+        content: resThread.body,
80
+        listMessage: resComment.body.map(c => ({
81
+          ...c,
82
+          timelineType: 'comment',
83
+          created: (new Date(c.created)).toLocaleString()
84
+        }))
85
+      }))
86
+      .catch(e => console.log('Error loading Thread data.', e))
100 87
   }
101 88
 
102 89
   handleClickBtnCloseApp = () => {
103 90
     this.setState({ isVisible: false })
91
+    GLOBAL_dispatchEvent({type: 'appClosed', data: {}}) // handled by tracim_front::src/container/WorkspaceContent.jsx
92
+  }
93
+
94
+  handleChangeNewComment = e => {
95
+    const newComment = e.target.value
96
+    this.setState({newComment})
97
+  }
98
+
99
+  handleClickValidateNewCommentBtn = async () => {
100
+    const { config, content, newComment } = this.state
101
+
102
+    const fetchResultSaveNewComment = await postThreadNewComment(config.apiUrl, content.workspace_id, content.content_id, newComment)
103
+
104
+    handleFetchResult(await fetchResultSaveNewComment)
105
+      .then(resSave => {
106
+        if (resSave.apiResponse.status === 200) {
107
+          this.setState({newComment: ''})
108
+          if (this.state.timelineWysiwyg) tinymce.get('wysiwygTimelineComment').setContent('')
109
+          this.loadContent()
110
+        } else {
111
+          console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
112
+        }
113
+      })
114
+  }
115
+
116
+  handleToggleWysiwyg = () => this.setState(prev => ({timelineWysiwyg: !prev.timelineWysiwyg}))
117
+
118
+  handleChangeStatus = async newStatus => {
119
+    const { config, content } = this.state
120
+
121
+    const fetchResultSaveEditStatus = putThreadStatus(config.apiUrl, content.workspace_id, content.content_id, newStatus)
122
+
123
+    handleFetchResult(await fetchResultSaveEditStatus)
124
+      .then(resSave => {
125
+        if (resSave.apiResponse.status !== 204) { // 204 no content so dont take status from resSave.apiResponse.status
126
+          console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
127
+        } else {
128
+          this.loadContent()
129
+        }
130
+      })
104 131
   }
105 132
 
133
+  handleClickArchive = () => console.log('archive nyi')
134
+
135
+  handleClickDelete = () => console.log('delete nyi')
136
+
106 137
   render () {
107
-    const { isVisible, loggedUser, content, listMessage, config } = this.state
138
+    const { config, isVisible, loggedUser, content, listMessage, newComment, timelineWysiwyg } = this.state
108 139
 
109 140
     if (!isVisible) return null
110 141
 
111 142
     return (
112
-      <PopinFixed customClass={`${config.customClass}`}>
143
+      <PopinFixed customClass={`wsContentThread`}>
113 144
         <PopinFixedHeader
114
-          customClass={`${config.customClass}`}
115
-          icon={config.icon}
116
-          name={content.title}
145
+          customClass={`wsContentThread`}
146
+          faIcon={config.faIcon}
147
+          title={content.label}
117 148
           onClickCloseBtn={this.handleClickBtnCloseApp}
118 149
         />
119 150
 
120
-        <PopinFixedOption customClass={`${config.customClass}`} i18n={i18n} />
151
+        <PopinFixedOption customClass={`wsContentThread`} i18n={i18n}>
152
+          <div className='justify-content-end'>
153
+            <SelectStatus
154
+              selectedStatus={config.availableStatuses.find(s => s.slug === content.status)}
155
+              availableStatus={config.availableStatuses}
156
+              onChangeStatus={this.handleChangeStatus}
157
+              disabled={false}
158
+            />
159
+
160
+            <ArchiveDeleteContent
161
+              onClickArchiveBtn={this.handleClickArchive}
162
+              onClickDeleteBtn={this.handleClickDelete}
163
+              disabled={false}
164
+            />
165
+          </div>
166
+        </PopinFixedOption>
121 167
 
122 168
         <PopinFixedContent customClass={`${config.customClass}__contentpage`}>
123
-          <ThreadComponent
124
-            title={content.title}
125
-            listMessage={listMessage}
169
+          <Timeline
170
+            customClass={`${config.slug}__contentpage`}
126 171
             loggedUser={loggedUser}
172
+            timelineData={listMessage}
173
+            newComment={newComment}
174
+            disableComment={false}
175
+            wysiwyg={timelineWysiwyg}
176
+            onChangeNewComment={this.handleChangeNewComment}
177
+            onClickValidateNewCommentBtn={this.handleClickValidateNewCommentBtn}
178
+            onClickWysiwygBtn={this.handleToggleWysiwyg}
179
+            onClickRevisionBtn={() => {}}
180
+            shouldScrollToBottom={false}
181
+            showHeader={false}
127 182
           />
128 183
         </PopinFixedContent>
129 184
       </PopinFixed>

+ 4 - 0
src/css/index.styl View File

@@ -20,6 +20,10 @@ bgandcolorhover()
20 20
     &__icon
21 21
       .fa-comments-o
22 22
         color white
23
+  &__option
24
+    &__menu
25
+      .selectStatus
26
+        margin-right 15px
23 27
   &__app
24 28
     display flex
25 29
     flex-flow column

+ 81 - 1
src/helper.js View File

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

+ 6 - 0
src/index.dev.js View File

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

+ 8 - 1
src/index.js View File

@@ -1,6 +1,7 @@
1 1
 import React from 'react'
2 2
 import ReactDOM from 'react-dom'
3 3
 import Thread from './container/Thread.jsx'
4
+import PopupCreateThread from './container/PopupCreateThread.jsx'
4 5
 
5 6
 require('./css/index.styl')
6 7
 
@@ -13,8 +14,14 @@ const appInterface = {
13 14
       , document.getElementById(data.config.domContainer)
14 15
     )
15 16
   },
16
-  hideApp: domId => {
17
+  unmountApp: domId => {
17 18
     return ReactDOM.unmountComponentAtNode(document.getElementById(domId)) // returns bool
19
+  },
20
+  renderPopupCreation: data => {
21
+    return ReactDOM.render(
22
+      <PopupCreateThread data={data} />
23
+      , document.getElementById(data.config.domContainer)
24
+    )
18 25
   }
19 26
 }
20 27