ReportCard.vue 15.6 KB
Newer Older
1
2
3
4
5
6
7
<template>
  <div class="ui fluid report card">
    <div class="content">
      <div class="header">
        <router-link :to="{name: 'manage.moderation.reports.detail', params: {id: obj.uuid}}">
          <translate translate-context="Content/Moderation/Card/Short" :translate-params="{id: obj.uuid.substring(0, 8)}">Report %{ id }</translate>
        </router-link>
8
        <collapse-link class="right floated" v-model="isCollapsed"></collapse-link>
9
10
      </div>
      <div class="content">
11
        <div class="ui hidden divider"></div>
12
13
14
15
16
17
18
19
20
21
        <div class="ui stackable two column grid">
          <div class="column">
            <table class="ui very basic unstackable table">
              <tbody>
                <tr>
                  <td>
                    <translate translate-context="Content/Moderation/*">Submitted by</translate>
                  </td>
                  <td>
                    <div v-if="obj.submitter">
22
                      <actor-link :admin="true" :actor="obj.submitter" />
23
24
25
26
27
28
29
30
31
32
33
                    </div>
                    <div v-else="obj.submitter_email">
                      {{ obj.submitter_email }}
                    </div>
                  </td>
                </tr>
                <tr>
                  <td>
                    <translate translate-context="*/*/*">Category</translate>
                  </td>
                  <td>
34
35
                    <report-category-dropdown
                      :value="obj.type"
36
37
38
39
                      @input="update({type: $event})">
                      &#32;
                      <action-feedback :is-loading="updating.type"></action-feedback>
                    </report-category-dropdown>
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
                  </td>
                </tr>
                <tr>
                  <td>
                    <translate translate-context="*/*/*/Noun">Creation date</translate>
                  </td>
                  <td>
                    <human-date :date="obj.creation_date" :icon="true"></human-date>
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
          <div class="column">
            <table class="ui very basic unstackable table">
              <tbody>
                <tr>
                  <td>
                    <translate translate-context="*/*/*">Status</translate>
                  </td>
                  <td v-if="obj.is_handled">
                    <span v-if="obj.is_handled">
                      <i class="green check icon"></i>
                      <translate translate-context="Content/*/*/Short">Resolved</translate>
                    </span>
                  </td>
                  <td v-else>
                    <i class="red x icon"></i>
                    <translate translate-context="Content/*/*/Short">Unresolved</translate>
                  </td>
                </tr>
                <tr>
                  <td>
Eliot Berriot's avatar
Eliot Berriot committed
73
                    <translate translate-context="Content/Moderation/*">Assigned to</translate>
74
75
76
                  </td>
                  <td>
                    <div v-if="obj.assigned_to">
77
                      <actor-link :admin="true" :actor="obj.assigned_to" />
78
79
80
81
82
83
84
85
86
87
88
89
90
                    </div>
                    <translate v-else translate-context="*/*/*">N/A</translate>
                  </td>
                </tr>
                <tr>
                  <td>
                    <translate translate-context="Content/*/*/Noun">Resolution date</translate>
                  </td>
                  <td>
                    <human-date v-if="obj.handled_date" :date="obj.handled_date" :icon="true"></human-date>
                    <translate v-else translate-context="*/*/*">N/A</translate>
                  </td>
                </tr>
91
92
                <tr>
                  <td>
93
                    <translate translate-context="Content/*/*/Noun">Internal notes</translate>
94
95
96
97
98
99
                  </td>
                  <td>
                    <i class="comment icon"></i>
                    {{ obj.notes.length }}
                  </td>
                </tr>
100
101
102
103
104
105
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
106
    <div class="main content" v-if="!isCollapsed">
107
108
109
110
111
112
113
114
115
116
117
118
119
      <div class="ui stackable two column grid">
        <div class="column">
          <h3>
            <translate translate-context="Content/*/*/Short">Message</translate>
          </h3>
          <expandable-div v-if="obj.summary" class="summary" :content="obj.summary">
            <div v-html="markdown.makeHtml(obj.summary)"></div>
          </expandable-div>
        </div>
        <aside class="column">
          <h3>
            <translate translate-context="Content/*/*/Short">Reported object</translate>
          </h3>
120
121
122
          <div v-if="!obj.target" class="ui warning message">
            <translate translate-context="Content/Moderation/Message">The object associated with this report was deleted.</translate>
          </div>
123
124
125
126
          <router-link class="ui basic button" v-if="target && configs[target.type].urls.getDetail" :to="configs[target.type].urls.getDetail(obj.target_state)">
            <i class="eye icon"></i>
            <translate translate-context="Content/Moderation/Link">View public page</translate>
          </router-link>
127
          <router-link class="ui basic button" v-if="target && configs[target.type].urls.getAdminDetail" :to="configs[target.type].urls.getAdminDetail(obj.target_state)">
128
129
130
131
132
            <i class="wrench icon"></i>
            <translate translate-context="Content/Moderation/Link">Open in moderation interface</translate>
          </router-link>
          <table class="ui very basic unstackable table">
            <tbody>
133
              <tr v-if="target">
134
135
136
                <td>
                  <translate translate-context="*/*/*">Type</translate>
                </td>
137
                <td colspan="2">
138
139
                  <i :class="[configs[target.type].icon, 'icon']"></i>
                  <translate translate-context="*/*/*">{{ configs[target.type].label }}</translate>
140
141
                </td>
              </tr>
142
              <tr v-if="obj.target_owner && (!target || target.type !== 'account')">
143
144
145
146
147
148
                <td>
                  <translate translate-context="*/*/*">Owner</translate>
                </td>
                <td>
                  <actor-link :admin="true" :actor="obj.target_owner"></actor-link>
                </td>
149
                <td>
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
                  <instance-policy-modal
                    v-if="!obj.target_owner.is_local"
                    class="right floated mini basic" type="actor" :target="obj.target_owner.full_username" />
                </td>
              </tr>
              <tr v-if="target && target.type === 'account'">
                <td>
                  <translate translate-context="*/*/*">Account</translate>
                </td>
                <td>
                  <actor-link :admin="true" :actor="obj.target_owner"></actor-link>
                </td>
                <td>
                  <instance-policy-modal
                    v-if="!obj.target_owner.is_local"
                    class="right floated mini basic" type="actor" :target="obj.target_owner.full_username" />
166
                </td>
167
              </tr>
168
169
170
171
              <tr v-if="obj.target_state.is_local">
                <td>
                  <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
                </td>
172
                <td colspan="2">
173
174
175
176
177
178
179
180
181
182
183
184
185
                  <i class="home icon"></i>
                  <translate translate-context="Content/Moderation/*/Short, Noun">Local</translate>
                </td>
              </tr>
              <tr v-else-if="obj.target_state.domain">
                <td>
                  <router-link :to="{name: 'manage.moderation.domains.detail', params: {id: obj.target_state.domain }}">
                    <translate translate-context="Content/Moderation/*/Noun">Domain</translate>
                  </router-link>
                </td>
                <td>
                  {{ obj.target_state.domain }}
                </td>
186
                <td>
187
                  <instance-policy-modal class="right floated mini basic" type="domain" :target="obj.target_state.domain" />
188
                </td>
189
190
191
              </tr>
              <tr v-for="field in targetFields" :key="field.id">
                <td>{{ field.label }}</td>
192
193
194
195
                <td colspan="2" v-if="field.repr">{{ field.repr }}</td>
                <td colspan="2" v-else>
                  <translate translate-context="*/*/*">N/A</translate>
                </td>
196
197
198
199
200
              </tr>
            </tbody>
          </table>
        </aside>
      </div>
201
202
203
204
205
206
207
208
      <div class="ui stackable two column grid">
        <div class="column">
          <h3>
            <translate translate-context="Content/*/*/Noun">Internal notes</translate>
          </h3>
          <notes-thread @deleted="handleRemovedNote($event)" :notes="obj.notes" />
          <note-form @created="obj.notes.push($event)" :target="{type: 'report', uuid: obj.uuid}" />
        </div>
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
        <div class="column">
          <h3>
            <translate translate-context="*/*/*">Actions</translate>
          </h3>
          <div class="ui labelled icon basic buttons">
            <button
              v-if="obj.is_handled === false"
              @click="resolve(true)"
              :class="['ui', {loading: isLoading}, 'button']">
              <i class="green check icon"></i>&nbsp;
              <translate translate-context="Content/*/Button.Label/Verb">Resolve</translate>
            </button>
            <button
              v-if="obj.is_handled === true"
              @click="resolve(false)"
              :class="['ui', {loading: isLoading}, 'button']">
              <i class="yellow redo icon"></i>&nbsp;
              <translate translate-context="Content/*/Button.Label">Unresolve</translate>
            </button>
            <template v-for="action in actions">
              <dangerous-button
230
                v-if="action.dangerous && action.show(obj)"
231
232
233
234
235
236
237
238
239
240
241
242
243
244
                :class="['ui', {loading: isLoading}, 'button']"
                color=""
                :action="action.handler">
                <i :class="[action.iconColor, action.icon, 'icon']"></i>&nbsp;
                {{ action.label }}
                <p slot="modal-header">{{ action.modalHeader}}</p>
                <div slot="modal-content">
                  <p>{{ action.modalContent }}</p>
                </div>
                <p slot="modal-confirm">{{ action.modalConfirmLabel }}</p>
              </dangerous-button>
            </template>
          </div>
        </div>
245
      </div>
246
247
248
249
250
251
252
253
    </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios'
import { diffWordsWithSpace } from 'diff'
254
255
import NoteForm from '@/components/manage/moderation/NoteForm'
import NotesThread from '@/components/manage/moderation/NotesThread'
256
import ReportCategoryDropdown from '@/components/moderation/ReportCategoryDropdown'
257
import InstancePolicyModal from '@/components/manage/moderation/InstancePolicyModal'
258
import entities from '@/entities'
259
import {setUpdate} from '@/utils'
260
261
import showdown from 'showdown'

262

263
264
265
266
267
268
269
270
271
272
273
274
function castValue (value) {
  if (value === null || value === undefined) {
    return ''
  }
  return String(value)
}

export default {
  props: {
    obj: {required: true},
    currentState: {required: false}
  },
275
276
277
  components: {
    NoteForm,
    NotesThread,
278
    ReportCategoryDropdown,
279
    InstancePolicyModal,
280
  },
281
  data () {
282
    return {
283
284
      markdown: new showdown.Converter(),
      isLoading: false,
285
      isCollapsed: false,
286
287
288
      updating: {
        type: false,
      }
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
    }
  },
  computed: {
    configs: entities.getConfigs,
    previousState () {
      if (this.obj.is_applied) {
        // mutation was applied, we use the previous state that is stored
        // on the mutation itself
        return this.obj.previous_state
      }
      // mutation is not applied yet, so we use the current state that was
      // passed to the component, if any
      return this.currentState
    },
    detailUrl () {
304
      if (!this.target) {
305
306
307
        return ''
      }
      let namespace
308
309
      let id = this.target.id
      if (this.target.type === 'track') {
310
311
        namespace = 'library.tracks.edit.detail'
      }
312
      if (this.target.type === 'album') {
313
314
        namespace = 'library.albums.edit.detail'
      }
315
      if (this.target.type === 'artist') {
316
317
318
319
320
321
        namespace = 'library.artists.edit.detail'
      }
      return this.$router.resolve({name: namespace, params: {id, editId: this.obj.uuid}}).href
    },

    targetFields () {
322
323
324
      if (!this.target) {
        return []
      }
325
      let payload = this.obj.target_state
326
      let fields = this.configs[this.target.type].moderatedFields
327
328
329
330
331
332
333
334
335
336
337
338
      let self = this
      return fields.map((fieldConfig) => {
        let dummyRepr = (v) => { return v }
        let getValueRepr = fieldConfig.getValueRepr || dummyRepr
        let d = {
          id: fieldConfig.id,
          label: fieldConfig.label,
          value: payload[fieldConfig.id],
          repr: castValue(getValueRepr(payload[fieldConfig.id])),
        }
        return d
      })
339
340
341
342
343
344
345
    },
    target () {
      if (this.obj.target) {
        return this.obj.target
      } else {
        return this.obj.target_state._target
      }
346
    },
347
348
349
350
351
352
353
354
355
356
357
358
    actions () {
      if (!this.target) {
        return []
      }
      let self = this
      let actions = []
      let typeConfig = this.configs[this.target.type]
      if (typeConfig.getDeleteUrl) {
        let deleteUrl = typeConfig.getDeleteUrl(this.target)
        actions.push({
          label: this.$pgettext('Content/Moderation/Button/Verb', 'Delete reported object'),
          modalHeader: this.$pgettext('Content/Moderation/Popup/Header', 'Delete reported object?'),
359
          modalContent: this.$pgettext('Content/Moderation/Popup,Paragraph', 'This will delete the object associated with this report and mark the report as resolved. The deletion is irreversible.'),
360
361
362
          modalConfirmLabel: this.$pgettext('*/*/*/Verb', 'Delete'),
          icon: 'x',
          iconColor: 'red',
363
          show: (obj) => { return !!obj.target },
364
365
366
367
368
          dangerous: true,
          handler: () => {
            axios.delete(deleteUrl).then((response) => {
              console.log('Target deleted')
              self.obj.target = null
369
              self.resolve(true)
370
371
372
373
374
375
376
377
            }, error => {
              console.log('Error while deleting target')
            })
          }
        })
      }
      return actions
    }
378
379
  },
  methods: {
380
381
382
383
    update (payload) {
      let url = `manage/moderation/reports/${this.obj.uuid}/`
      let self = this
      this.isLoading = true
384
      setUpdate(payload, this.updating, true)
385
386
      axios.patch(url, payload).then((response) => {
        self.$emit('updated', payload)
387
        Object.assign(self.obj, payload)
388
        self.isLoading = false
389
        setUpdate(payload, self.updating, false)
390
391
      }, error => {
        self.isLoading = false
392
        setUpdate(payload, self.updating, false)
393
394
      })
    },
395
396
397
398
399
400
401
    resolve (v) {
      let url = `manage/moderation/reports/${this.obj.uuid}/`
      let self = this
      this.isLoading = true
      axios.patch(url, {is_handled: v}).then((response) => {
        self.$emit('handled', v)
        self.isLoading = false
402
        self.obj.is_handled = v
403
404
        let increment
        if (v) {
405
          self.isCollapsed = true
406
407
408
409
410
411
412
413
414
          increment = -1
        } else {
          increment = 1
        }
        self.$store.commit('ui/incrementNotifications', {count: increment, type: 'pendingReviewReports'})
      }, error => {
        self.isLoading = false
      })
    },
415
416
417
418
    handleRemovedNote (uuid) {
      this.obj.notes = this.obj.notes.filter((note) => {
        return note.uuid != uuid
      })
419
    },
420
421
422
  }
}
</script>