Browse Source

added app feature workspace ; only contains the creation popup for now

Skylsmoi 5 years ago
parent
commit
26e8d28a1c

+ 13 - 0
frontend_app_workspace/.editorconfig View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

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

+ 1 - 0
frontend_app_workspace/dist/dev View File

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

+ 1 - 0
frontend_app_workspace/dist/font View File

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

+ 110 - 0
frontend_app_workspace/dist/index.html View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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
+  })

+ 122 - 0
frontend_app_workspace/src/container/PopupCreateWorkspace.jsx View File

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

+ 401 - 0
frontend_app_workspace/src/container/Workspace.jsx View File

@@ -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 View File

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

+ 97 - 0
frontend_app_workspace/src/helper.js View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 View File

@@ -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 ? 'workspace' : 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
+}