Browse Source

[https://github.com/tracim/tracim/issues/639 and https://github.com/tracim/tracim/issues/640] added archive and delete feature

Skylsmoi 5 years ago
parent
commit
65886601e1

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

@@ -11,7 +11,7 @@ export const newFlashMessage = (msgText = '', msgType = 'info', msgDelay = 5000)
11 11
   msgDelay !== 0 && window.setTimeout(() => dispatch(removeFlashMessage(msgText)), msgDelay)
12 12
   return dispatch(addFlashMessage({message: msgText, type: msgType}))
13 13
 }
14
-export const addFlashMessage = msg => ({ type: `${ADD}/${FLASH_MESSAGE}`, msg })
14
+const addFlashMessage = msg => ({ type: `${ADD}/${FLASH_MESSAGE}`, msg }) // only newFlashMsg should be used by component and app so dont export this
15 15
 export const removeFlashMessage = msg => ({ type: `${REMOVE}/${FLASH_MESSAGE}`, msg })
16 16
 
17 17
 export const USER = 'User'

+ 5 - 0
frontend/src/container/Tracim.jsx View File

@@ -18,6 +18,7 @@ import {
18 18
   getUserIsConnected
19 19
 } from '../action-creator.async.js'
20 20
 import {
21
+  newFlashMessage,
21 22
   removeFlashMessage,
22 23
   setUserConnected
23 24
 } from '../action-creator.sync.js'
@@ -37,6 +38,10 @@ class Tracim extends React.Component {
37 38
         console.log('%c<Tracim> Custom event', 'color: #28a745', type, data)
38 39
         this.props.history.push(data.url)
39 40
         break
41
+      case 'addFlashMsg':
42
+        console.log('%c<Tracim> Custom event', 'color: #28a745', type, data)
43
+        this.props.dispatch(newFlashMessage(data.msg, data.type, data.delay))
44
+        break
40 45
     }
41 46
   }
42 47
 

+ 40 - 0
frontend_app_html-document/src/action.async.js View File

@@ -77,3 +77,43 @@ export const postHtmlDocContent = (user, apiUrl, idWorkspace, idFolder, contentT
77 77
       label: newContentName
78 78
     })
79 79
   })
80
+
81
+export const putHtmlDocIsArchived = (user, apiUrl, idWorkspace, idContent) => {
82
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/archive`, {
83
+    headers: {
84
+      'Authorization': 'Basic ' + user.auth,
85
+      ...FETCH_CONFIG.headers
86
+    },
87
+    method: 'PUT'
88
+  })
89
+}
90
+
91
+export const putHtmlDocIsDeleted = (user, apiUrl, idWorkspace, idContent) => {
92
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/delete`, {
93
+    headers: {
94
+      'Authorization': 'Basic ' + user.auth,
95
+      ...FETCH_CONFIG.headers
96
+    },
97
+    method: 'PUT'
98
+  })
99
+}
100
+
101
+export const putHtmlDocRestoreArchived = (user, apiUrl, idWorkspace, idContent) => {
102
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/unarchive`, {
103
+    headers: {
104
+      'Authorization': 'Basic ' + user.auth,
105
+      ...FETCH_CONFIG.headers
106
+    },
107
+    method: 'PUT'
108
+  })
109
+}
110
+
111
+export const putHtmlDocRestoreDeleted = (user, apiUrl, idWorkspace, idContent) => {
112
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/undelete`, {
113
+    headers: {
114
+      'Authorization': 'Basic ' + user.auth,
115
+      ...FETCH_CONFIG.headers
116
+    },
117
+    method: 'PUT'
118
+  })
119
+}

+ 28 - 0
frontend_app_html-document/src/component/HtmlDocument.jsx View File

@@ -5,6 +5,34 @@ import { MODE } from '../helper.js'
5 5
 const HtmlDocument = props => {
6 6
   return (
7 7
     <div className='wsContentHtmlDocument__contentpage__textnote html-document__contentpage__textnote'>
8
+      {props.isArchived &&
9
+        <div className='html-document__contentpage__textnote__state'>
10
+          <div className='html-document__contentpage__textnote__state__msg'>
11
+            <i className='fa fa-fw fa-archive' />
12
+            This content is archived.
13
+          </div>
14
+
15
+          <button className='html-document__contentpage__textnote__state__btnrestore btn' onClick={props.onClickRestoreArchived}>
16
+            <i className='fa fa-fw fa-archive' />
17
+            Restore
18
+          </button>
19
+        </div>
20
+      }
21
+
22
+      {props.isDeleted &&
23
+        <div className='html-document__contentpage__textnote__state'>
24
+          <div className='html-document__contentpage__textnote__state__msg'>
25
+            <i className='fa fa-fw fa-trash' />
26
+            Ce contenu est supprimé.
27
+          </div>
28
+
29
+          <button className='html-document__contentpage__textnote__state__btnrestore btn' onClick={props.onClickRestoreDeleted}>
30
+            <i className='fa fa-fw fa-trash' />
31
+            Restore
32
+          </button>
33
+        </div>
34
+      }
35
+
8 36
       {(props.mode === MODE.VIEW || props.mode === MODE.REVISION) &&
9 37
         <div>
10 38
           <div className='html-document__contentpage__textnote__version'>

+ 74 - 15
frontend_app_html-document/src/container/HtmlDocument.jsx View File

@@ -22,7 +22,11 @@ import {
22 22
   getHtmlDocRevision,
23 23
   postHtmlDocNewComment,
24 24
   putHtmlDocContent,
25
-  putHtmlDocStatus
25
+  putHtmlDocStatus,
26
+  putHtmlDocIsArchived,
27
+  putHtmlDocIsDeleted,
28
+  putHtmlDocRestoreArchived,
29
+  putHtmlDocRestoreDeleted
26 30
 } from '../action.async.js'
27 31
 
28 32
 class HtmlDocument extends React.Component {
@@ -261,22 +265,71 @@ class HtmlDocument extends React.Component {
261 265
   }
262 266
 
263 267
   handleClickArchive = async () => {
264
-    console.log('archive')
265
-    // const { config, content } = this.state
266
-    //
267
-    // const fetchResultArchive = await fetch(`${config.apiUrl}/workspaces/${content.workspace_id}/contents/${content.content_id}/archive`, {
268
-    //   ...FETCH_CONFIG,
269
-    //   method: 'PUT'
270
-    // })
268
+    const { loggedUser, config, content } = this.state
269
+
270
+    const fetchResultArchive = await putHtmlDocIsArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
271
+    switch (fetchResultArchive.status) {
272
+      case 204: this.setState(prev => ({content: {...prev.content, is_archived: true}})); break
273
+      default: GLOBAL_dispatchEvent({
274
+        type: 'addFlashMsg',
275
+        data: {
276
+          msg: this.props.t('Error while archiving document'),
277
+          type: 'warning',
278
+          delay: undefined
279
+        }
280
+      })
281
+    }
271 282
   }
272 283
 
273 284
   handleClickDelete = async () => {
274
-    console.log('delete')
275
-    // const { config, content } = this.state
276
-    // const fetchResultDelete = await fetch(`${config.apiUrl}/workspaces/${content.workspace_id}/contents/${content.content_id}/delete`, {
277
-    //   ...FETCH_CONFIG,
278
-    //   method: 'PUT'
279
-    // })
285
+    const { loggedUser, config, content } = this.state
286
+
287
+    const fetchResultArchive = await putHtmlDocIsDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
288
+    switch (fetchResultArchive.status) {
289
+      case 204: this.setState(prev => ({content: {...prev.content, is_deleted: true}})); break
290
+      default: GLOBAL_dispatchEvent({
291
+        type: 'addFlashMsg',
292
+        data: {
293
+          msg: this.props.t('Error while deleting document'),
294
+          type: 'warning',
295
+          delay: undefined
296
+        }
297
+      })
298
+    }
299
+  }
300
+
301
+  handleClickRestoreArchived = async () => {
302
+    const { loggedUser, config, content } = this.state
303
+
304
+    const fetchResultRestore = await putHtmlDocRestoreArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
305
+    switch (fetchResultRestore.status) {
306
+      case 204: this.setState(prev => ({content: {...prev.content, is_archived: false}})); break
307
+      default: GLOBAL_dispatchEvent({
308
+        type: 'addFlashMsg',
309
+        data: {
310
+          msg: this.props.t('Error while restoring document'),
311
+          type: 'warning',
312
+          delay: undefined
313
+        }
314
+      })
315
+    }
316
+  }
317
+
318
+  handleClickRestoreDeleted = async () => {
319
+    const { loggedUser, config, content } = this.state
320
+
321
+    const fetchResultRestore = await putHtmlDocRestoreDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
322
+    switch (fetchResultRestore.status) {
323
+      case 204: this.setState(prev => ({content: {...prev.content, is_deleted: false}})); break
324
+      default: GLOBAL_dispatchEvent({
325
+        type: 'addFlashMsg',
326
+        data: {
327
+          msg: this.props.t('Error while restoring document'),
328
+          type: 'warning',
329
+          delay: undefined
330
+        }
331
+      })
332
+    }
280 333
   }
281 334
 
282 335
   handleClickShowRevision = revision => {
@@ -298,7 +351,9 @@ class HtmlDocument extends React.Component {
298 351
         label: revision.label,
299 352
         raw_content: revision.raw_content,
300 353
         number: revision.number,
301
-        status: revision.status
354
+        status: revision.status,
355
+        is_archived: prev.is_archived, // archived and delete should always be taken from last version
356
+        is_deleted: prev.is_deleted
302 357
       },
303 358
       mode: MODE.REVISION
304 359
     }))
@@ -386,6 +441,10 @@ class HtmlDocument extends React.Component {
386 441
             lastVersion={timeline.filter(t => t.timelineType === 'revision').length}
387 442
             text={content.raw_content}
388 443
             onChangeText={this.handleChangeText}
444
+            isArchived={content.is_archived}
445
+            isDeleted={content.is_deleted}
446
+            onClickRestoreArchived={this.handleClickRestoreArchived}
447
+            onClickRestoreDeleted={this.handleClickRestoreDeleted}
389 448
             key={'html-document'}
390 449
           />
391 450
 

+ 14 - 0
frontend_app_html-document/src/css/index.styl View File

@@ -21,6 +21,20 @@
21 21
       height 100%
22 22
       overflow-y auto
23 23
       background-color off-white
24
+      &__state
25
+        display flex
26
+        align-items center
27
+        justify-content space-between
28
+        margin-bottom 15px
29
+        padding 5px 15px
30
+        background-color #fee498
31
+        border-radius 10px
32
+        &__msg > i
33
+          margin-right 8px
34
+        &__btnrestore
35
+          cursor pointer
36
+          & > i
37
+            margin-right 8px
24 38
       &__version
25 39
         display flex
26 40
         justify-content flex-end

+ 40 - 0
frontend_app_thread/src/action.async.js View File

@@ -68,3 +68,43 @@ export const putThreadContent = (user, apiUrl, idWorkspace, idContent, label) =>
68 68
       raw_content: '' // threads have no content
69 69
     })
70 70
   })
71
+
72
+export const putThreadIsArchived = (user, apiUrl, idWorkspace, idContent) => {
73
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/archive`, {
74
+    headers: {
75
+      'Authorization': 'Basic ' + user.auth,
76
+      ...FETCH_CONFIG.headers
77
+    },
78
+    method: 'PUT'
79
+  })
80
+}
81
+
82
+export const putThreadIsDeleted = (user, apiUrl, idWorkspace, idContent) => {
83
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/delete`, {
84
+    headers: {
85
+      'Authorization': 'Basic ' + user.auth,
86
+      ...FETCH_CONFIG.headers
87
+    },
88
+    method: 'PUT'
89
+  })
90
+}
91
+
92
+export const putThreadRestoreArchived = (user, apiUrl, idWorkspace, idContent) => {
93
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/unarchive`, {
94
+    headers: {
95
+      'Authorization': 'Basic ' + user.auth,
96
+      ...FETCH_CONFIG.headers
97
+    },
98
+    method: 'PUT'
99
+  })
100
+}
101
+
102
+export const putThreadRestoreDeleted = (user, apiUrl, idWorkspace, idContent) => {
103
+  return fetch(`${apiUrl}/workspaces/${idWorkspace}/contents/${idContent}/undelete`, {
104
+    headers: {
105
+      'Authorization': 'Basic ' + user.auth,
106
+      ...FETCH_CONFIG.headers
107
+    },
108
+    method: 'PUT'
109
+  })
110
+}

+ 81 - 14
frontend_app_thread/src/container/Thread.jsx View File

@@ -18,7 +18,11 @@ import {
18 18
   getThreadComment,
19 19
   postThreadNewComment,
20 20
   putThreadStatus,
21
-  putThreadContent
21
+  putThreadContent,
22
+  putThreadIsArchived,
23
+  putThreadIsDeleted,
24
+  putThreadRestoreArchived,
25
+  putThreadRestoreDeleted
22 26
 } from '../action.async.js'
23 27
 
24 28
 class Thread extends React.Component {
@@ -77,7 +81,7 @@ class Thread extends React.Component {
77 81
   componentDidUpdate (prevProps, prevState) {
78 82
     const { state } = this
79 83
 
80
-    console.log('%c<Thread> did Mount', `color: ${this.state.config.hexcolor}`, prevState, state)
84
+    console.log('%c<Thread> did Update', `color: ${this.state.config.hexcolor}`, prevState, state)
81 85
 
82 86
     if (!prevState.content || !state.content) return
83 87
 
@@ -173,17 +177,81 @@ class Thread extends React.Component {
173 177
 
174 178
     handleFetchResult(await fetchResultSaveEditStatus)
175 179
       .then(resSave => {
176
-        if (resSave.status !== 204) { // 204 no content so dont take status from resSave.apiResponse.status
177
-          console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
178
-        } else {
180
+        if (resSave.status === 204) { // 204 no content so dont take status from resSave.apiResponse.status
179 181
           this.loadContent()
182
+        } else {
183
+          console.warn('Error saving thread comment. Result:', resSave, 'content:', content, 'config:', config)
184
+        }
185
+      })
186
+  }
187
+
188
+  handleClickArchive = async () => {
189
+    const { loggedUser, config, content } = this.state
190
+
191
+    const fetchResultArchive = await putThreadIsArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
192
+    switch (fetchResultArchive.status) {
193
+      case 204: this.setState(prev => ({content: {...prev.content, is_archived: true}})); break
194
+      default: GLOBAL_dispatchEvent({
195
+        type: 'addFlashMsg',
196
+        data: {
197
+          msg: this.props.t('Error while archiving thread'),
198
+          type: 'warning',
199
+          delay: undefined
200
+        }
201
+      })
202
+    }
203
+  }
204
+
205
+  handleClickDelete = async () => {
206
+    const { loggedUser, config, content } = this.state
207
+
208
+    const fetchResultArchive = await putThreadIsDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
209
+    switch (fetchResultArchive.status) {
210
+      case 204: this.setState(prev => ({content: {...prev.content, is_deleted: true}})); break
211
+      default: GLOBAL_dispatchEvent({
212
+        type: 'addFlashMsg',
213
+        data: {
214
+          msg: this.props.t('Error while deleting thread'),
215
+          type: 'warning',
216
+          delay: undefined
217
+        }
218
+      })
219
+    }
220
+  }
221
+
222
+  handleClickRestoreArchived = async () => {
223
+    const { loggedUser, config, content } = this.state
224
+
225
+    const fetchResultRestore = await putThreadRestoreArchived(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
226
+    switch (fetchResultRestore.status) {
227
+      case 204: this.setState(prev => ({content: {...prev.content, is_archived: false}})); break
228
+      default: GLOBAL_dispatchEvent({
229
+        type: 'addFlashMsg',
230
+        data: {
231
+          msg: this.props.t('Error while restoring thread'),
232
+          type: 'warning',
233
+          delay: undefined
180 234
         }
181 235
       })
236
+    }
182 237
   }
183 238
 
184
-  handleClickArchive = () => console.log('archive nyi')
239
+  handleClickRestoreDeleted = async () => {
240
+    const { loggedUser, config, content } = this.state
185 241
 
186
-  handleClickDelete = () => console.log('delete nyi')
242
+    const fetchResultRestore = await putThreadRestoreDeleted(loggedUser, config.apiUrl, content.workspace_id, content.content_id)
243
+    switch (fetchResultRestore.status) {
244
+      case 204: this.setState(prev => ({content: {...prev.content, is_deleted: false}})); break
245
+      default: GLOBAL_dispatchEvent({
246
+        type: 'addFlashMsg',
247
+        data: {
248
+          msg: this.props.t('Error while restoring thread'),
249
+          type: 'warning',
250
+          delay: undefined
251
+        }
252
+      })
253
+    }
254
+  }
187 255
 
188 256
   render () {
189 257
     const { config, isVisible, loggedUser, content, listMessage, newComment, timelineWysiwyg } = this.state
@@ -191,10 +259,7 @@ class Thread extends React.Component {
191 259
     if (!isVisible) return null
192 260
 
193 261
     return (
194
-      <PopinFixed
195
-        customClass={config.slug}
196
-        customColor={config.hexcolor}
197
-      >
262
+      <PopinFixed customClass={config.slug} customColor={config.hexcolor}>
198 263
         <PopinFixedHeader
199 264
           customClass={`${config.slug}__contentpage`}
200 265
           customColor={config.hexcolor}
@@ -226,9 +291,7 @@ class Thread extends React.Component {
226 291
           </div>
227 292
         </PopinFixedOption>
228 293
 
229
-        <PopinFixedContent
230
-          customClass={`${config.slug}__contentpage`}
231
-        >
294
+        <PopinFixedContent customClass={`${config.slug}__contentpage`}>
232 295
           <Timeline
233 296
             customClass={`${config.slug}__contentpage`}
234 297
             customColor={config.hexcolor}
@@ -243,6 +306,10 @@ class Thread extends React.Component {
243 306
             onClickRevisionBtn={() => {}}
244 307
             shouldScrollToBottom
245 308
             showHeader={false}
309
+            isArchived={content.is_archived}
310
+            onClickRestoreArchived={this.handleClickRestoreArchived}
311
+            isDeleted={content.is_deleted}
312
+            onClickRestoreDeleted={this.handleClickRestoreDeleted}
246 313
           />
247 314
         </PopinFixedContent>
248 315
       </PopinFixed>

+ 5 - 0
frontend_lib/src/component/PopinFixed/PopinFixed.styl View File

@@ -87,6 +87,11 @@
87 87
     // 209 = wsContentGeneric:top wsContentGeneric__header:height - wsContentGeneric__option:height
88 88
     width 200% // to allow transform translateX of right part
89 89
     height calc(100% - 209px)
90
+    &__state
91
+      width 100%
92
+      padding 5px 15px
93
+      border-radius 10px
94
+      background-color #fbe294
90 95
     &__left
91 96
       transition width 0.4s ease
92 97
       width 25%

+ 36 - 2
frontend_lib/src/component/Timeline/Timeline.jsx View File

@@ -46,6 +46,34 @@ class Timeline extends React.Component {
46 46
           </div>
47 47
         }
48 48
 
49
+        {props.isArchived &&
50
+          <div className='timeline__info'>
51
+            <div className='timeline__info__msg'>
52
+              <i className='fa fa-fw fa-archive' />
53
+              This content is archived.
54
+            </div>
55
+
56
+            <button className='timeline__info__btnrestore btn' onClick={props.onClickRestoreArchived}>
57
+              <i className='fa fa-fw fa-archive' />
58
+              Restore
59
+            </button>
60
+          </div>
61
+        }
62
+
63
+        {props.isDeleted &&
64
+          <div className='timeline__info'>
65
+            <div className='timeline__info__msg'>
66
+              <i className='fa fa-fw fa-trash' />
67
+              This content is deleted.
68
+            </div>
69
+
70
+            <button className='timeline__info__btnrestore btn' onClick={props.onClickRestoreDeleted}>
71
+              <i className='fa fa-fw fa-trash' />
72
+              Restore
73
+            </button>
74
+          </div>
75
+        }
76
+
49 77
         <div className='timeline__body'>
50 78
           <ul className={classnames(`${props.customClass}__messagelist`, 'timeline__body__messagelist')}>
51 79
             {props.timelineData.map(content => {
@@ -157,7 +185,11 @@ Timeline.propTypes = {
157 185
   onClickRevisionBtn: PropTypes.func,
158 186
   shouldScrollToBottom: PropTypes.bool,
159 187
   showHeader: PropTypes.bool,
160
-  rightPartOpen: PropTypes.bool // irrelevent if showHeader in false
188
+  rightPartOpen: PropTypes.bool, // irrelevant if showHeader is false
189
+  isArchived: PropTypes.bool,
190
+  onClickRestoreArchived: PropTypes.func,
191
+  isDeleted: PropTypes.bool,
192
+  onClickRestoreDeleted: PropTypes.func,
161 193
 }
162 194
 
163 195
 Timeline.defaultProps = {
@@ -174,5 +206,7 @@ Timeline.defaultProps = {
174 206
   onClickWysiwygBtn: () => {},
175 207
   shouldScrollToBottom: true,
176 208
   showHeader: true,
177
-  rightPartOpen: false
209
+  rightPartOpen: false,
210
+  isArchived: false,
211
+  isDeleted: false
178 212
 }

+ 17 - 0
frontend_lib/src/component/Timeline/Timeline.styl View File

@@ -20,6 +20,23 @@
20 20
       text-align center
21 21
     &__title
22 22
       transform rotate(-90deg)
23
+  &__info
24
+    position absolute
25
+    display flex
26
+    align-items center
27
+    justify-content space-between
28
+    margin 10px
29
+    padding 5px 15px
30
+    width calc(100% - 20px)
31
+    background-color #fee498
32
+    border-radius 10px
33
+    z-index 1
34
+    &__msg > i
35
+      margin-right 8px
36
+    &__btnrestore
37
+      cursor pointer
38
+      & > i
39
+        margin-right 8px
23 40
   &__body
24 41
     display flex
25 42
     flex-direction column

+ 1 - 13
frontend_lib/src/index.dev.js View File

@@ -74,21 +74,9 @@ ReactDOM.render(
74 74
       />
75 75
 
76 76
       <PopinFixedContent customClass={`${'randomClass'}__contentpage`}>
77
-        <div>
78
-          <NewVersionButton customColor='#3f52e3' />
79
-          <ArchiveDeleteContent customColor='#3f52e3' />
80
-          <Delimiter />
81
-          <span>Here will be the app content. Style is handled by the app (obviously)</span>
82
-          <BtnSwitch />
83
-          {/* <TextAreaApp customClass={'randomClass'} text={'woot'} /> */}
84
-          <Checkbox
85
-            name='osef'
86
-            onClickCheckbox={() => {}}
87
-            checked
88
-          />
89
-        </div>
90 77
 
91 78
         <Timeline
79
+          showHeader={false}
92 80
           customClass={`${'randomClass'}__contentpage`}
93 81
           customColor={'#3f52e3'}
94 82
           loggedUser={{