show mail attachments in frontend #47

Merged
j3d1 merged 1 commit from jedi/dev into live 2024-01-14 17:34:20 +00:00
11 changed files with 252 additions and 23 deletions

View file

@ -11,6 +11,7 @@ from rest_framework.response import Response
from core.settings import MEDIA_ROOT from core.settings import MEDIA_ROOT
from files.models import File from files.models import File
from mail.models import EmailAttachment
@swagger_auto_schema(method='GET', auto_schema=None) @swagger_auto_schema(method='GET', auto_schema=None)
@ -21,7 +22,11 @@ def media_urls(request, hash):
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash: if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash:
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED) return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
file = File.objects.get(hash=hash) file = File.objects.filter(hash=hash).first()
attachment = EmailAttachment.objects.filter(hash=hash).first()
file = file if file else attachment
if not file:
return Response(status=status.HTTP_404_NOT_FOUND)
hash_path = file.file hash_path = file.file
return HttpResponse(status=status.HTTP_200_OK, return HttpResponse(status=status.HTTP_200_OK,
content_type=file.mime_type, content_type=file.mime_type,
@ -33,9 +38,10 @@ def media_urls(request, hash):
'Age': 0, 'Age': 0,
'ETag': file.hash, 'ETag': file.hash,
}) })
except File.DoesNotExist: except File.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
except EmailAttachment.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
@swagger_auto_schema(method='GET', auto_schema=None) @swagger_auto_schema(method='GET', auto_schema=None)
@ -47,7 +53,11 @@ def thumbnail_urls(request, size, hash):
if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash + "_" + str(size): if request.META.get('HTTP_IF_NONE_MATCH') and request.META.get('HTTP_IF_NONE_MATCH') == hash + "_" + str(size):
return HttpResponse(status=status.HTTP_304_NOT_MODIFIED) return HttpResponse(status=status.HTTP_304_NOT_MODIFIED)
try: try:
file = File.objects.get(hash=hash) file = File.objects.filter(hash=hash).first()
attachment = EmailAttachment.objects.filter(hash=hash).first()
file = file if file else attachment
if not file:
return Response(status=status.HTTP_404_NOT_FOUND)
hash_path = file.file hash_path = file.file
if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'): if not os.path.exists(MEDIA_ROOT + f'/thumbnails/{size}/{hash_path}'):
from PIL import Image from PIL import Image
@ -72,6 +82,8 @@ def thumbnail_urls(request, size, hash):
except File.DoesNotExist: except File.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
except EmailAttachment.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
urlpatterns = [ urlpatterns = [

View file

@ -77,6 +77,13 @@ class AbstractFile(models.Model):
objects = FileManager() objects = FileManager()
def save(self, *args, **kwargs):
from django.utils import timezone
if not self.created_at:
self.created_at = timezone.now()
self.updated_at = timezone.now()
super().save(*args, **kwargs)
class Meta: class Meta:
abstract = True abstract = True

View file

@ -1,6 +1,12 @@
from rest_framework import routers, viewsets, serializers from rest_framework import routers, viewsets, serializers
from mail.models import Email from mail.models import Email, EmailAttachment
class AttachmentSerializer(serializers.ModelSerializer):
class Meta:
model = EmailAttachment
fields = ['hash', 'mime_type', 'name']
class EmailSerializer(serializers.ModelSerializer): class EmailSerializer(serializers.ModelSerializer):

View file

@ -26,7 +26,7 @@ class Migration(migrations.Migration):
def generate_email_attachments(apps, schema_editor): def generate_email_attachments(apps, schema_editor):
for email in Email.objects.all(): for email in Email.objects.all():
raw = email.raw raw = email.raw
if raw is None: if raw is None or raw == '':
continue continue
parsed, body, attachments = parse_email_body(raw.encode('utf-8'), NullLogger()) parsed, body, attachments = parse_email_body(raw.encode('utf-8'), NullLogger())
email.attachments.clear() email.attachments.clear()

View file

@ -92,8 +92,8 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
session = mock.Mock() session = mock.Mock()
envelope = Envelope() envelope = Envelope()
envelope.mail_from = 'test1@test' envelope.mail_from = 'test1@test'
envelope.rcpt_tos = ['test2@test'] envelope.rcpt_tos = ['test2@localhost']
envelope.content = b'Subject: test\nFrom: test3@test\nTo: test4@test\nMessage-ID: <1@test>\n\ntest' envelope.content = b'Subject: test\nFrom: test3@test\nTo: test4@localhost\nMessage-ID: <1@test>\n\ntest'
result = async_to_sync(handler.handle_DATA)(server, session, envelope) result = async_to_sync(handler.handle_DATA)(server, session, envelope)
self.assertEqual(result, '250 Message accepted for delivery') self.assertEqual(result, '250 Message accepted for delivery')
self.assertEqual(len(Email.objects.all()), 2) self.assertEqual(len(Email.objects.all()), 2)
@ -101,14 +101,14 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
aiosmtplib.send.assert_called_once() aiosmtplib.send.assert_called_once()
self.assertEqual('test', Email.objects.all()[0].subject) self.assertEqual('test', Email.objects.all()[0].subject)
self.assertEqual('test1@test', Email.objects.all()[0].sender) self.assertEqual('test1@test', Email.objects.all()[0].sender)
self.assertEqual('test2@test', Email.objects.all()[0].recipient) self.assertEqual('test2@localhost', Email.objects.all()[0].recipient)
self.assertEqual('test', Email.objects.all()[0].body) self.assertEqual('test', Email.objects.all()[0].body)
self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread) self.assertEqual(IssueThread.objects.all()[0], Email.objects.all()[0].issue_thread)
self.assertEqual('<1@test>', Email.objects.all()[0].reference) self.assertEqual('<1@test>', Email.objects.all()[0].reference)
self.assertEqual(None, Email.objects.all()[0].in_reply_to) self.assertEqual(None, Email.objects.all()[0].in_reply_to)
self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()), self.assertEqual(expected_auto_reply_subject.format('test', IssueThread.objects.all()[0].short_uuid()),
Email.objects.all()[1].subject) Email.objects.all()[1].subject)
self.assertEqual('test2@test', Email.objects.all()[1].sender) self.assertEqual('test2@localhost', Email.objects.all()[1].sender)
self.assertEqual('test1@test', Email.objects.all()[1].recipient) self.assertEqual('test1@test', Email.objects.all()[1].recipient)
self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()), self.assertEqual(expected_auto_reply.format(IssueThread.objects.all()[0].short_uuid()),
Email.objects.all()[1].body) Email.objects.all()[1].body)
@ -284,19 +284,19 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
def test_mail_reply(self): def test_mail_reply(self):
issue_thread = IssueThread.objects.create( issue_thread = IssueThread.objects.create(
name="test", name="test subject",
) )
mail1 = Email.objects.create( mail1 = Email.objects.create(
subject='test subject', subject='test subject',
body='test', body='test',
sender='test1@test', sender='test1@test',
recipient='test2@' + MAIL_DOMAIN, recipient='test2@localhost',
issue_thread=issue_thread, issue_thread=issue_thread,
) )
mail1_reply = Email.objects.create( mail1_reply = Email.objects.create(
subject='Re: test subject', subject='Message received',
body='Thank you for your message.', body='Thank you for your message.',
sender='test2@' + MAIL_DOMAIN, sender='test2@localhost',
recipient='test1@test', recipient='test1@test',
in_reply_to=mail1.reference, in_reply_to=mail1.reference,
issue_thread=issue_thread, issue_thread=issue_thread,
@ -310,8 +310,22 @@ class LMTPHandlerTestCase(TestCase): # TODO replace with less hacky test
self.assertEqual(len(Email.objects.all()), 3) self.assertEqual(len(Email.objects.all()), 3)
self.assertEqual(len(IssueThread.objects.all()), 1) self.assertEqual(len(IssueThread.objects.all()), 1)
aiosmtplib.send.assert_called_once() aiosmtplib.send.assert_called_once()
self.assertEqual(Email.objects.all()[2].subject, 'Re: test subject') self.assertEqual(Email.objects.all()[0].subject, 'test subject')
self.assertEqual(Email.objects.all()[2].sender, 'test2@' + MAIL_DOMAIN) self.assertEqual(Email.objects.all()[0].sender, 'test1@test')
self.assertEqual(Email.objects.all()[0].recipient, 'test2@localhost')
self.assertEqual(Email.objects.all()[0].body, 'test')
self.assertEqual(Email.objects.all()[0].issue_thread, issue_thread)
self.assertEqual(Email.objects.all()[0].reference, mail1.reference)
self.assertEqual(Email.objects.all()[1].subject, 'Message received')
self.assertEqual(Email.objects.all()[1].sender, 'test2@localhost')
self.assertEqual(Email.objects.all()[1].recipient, 'test1@test')
self.assertEqual(Email.objects.all()[1].body, 'Thank you for your message.')
self.assertEqual(Email.objects.all()[1].issue_thread, issue_thread)
self.assertTrue(Email.objects.all()[1].reference.startswith("<"))
self.assertTrue(Email.objects.all()[1].reference.endswith("@localhost>"))
self.assertEqual(Email.objects.all()[1].in_reply_to, mail1.reference)
self.assertEqual(Email.objects.all()[2].subject, 'Re: test subject [#{0}]'.format(issue_thread.short_uuid()))
self.assertEqual(Email.objects.all()[2].sender, 'test2@localhost')
self.assertEqual(Email.objects.all()[2].recipient, 'test1@test') self.assertEqual(Email.objects.all()[2].recipient, 'test1@test')
self.assertEqual(Email.objects.all()[2].body, 'test') self.assertEqual(Email.objects.all()[2].body, 'test')
self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread) self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread)
@ -633,25 +647,26 @@ dGVzdGltYWdl
def test_mail_plus_issue_thread(self): def test_mail_plus_issue_thread(self):
issue_thread = IssueThread.objects.create( issue_thread = IssueThread.objects.create(
name="test", name="test subject",
) )
mail1 = Email.objects.create( mail1 = Email.objects.create(
subject='test subject', subject='test subject',
body='test', body='test',
sender='test1@test', sender='test1@test',
recipient='test2@test', recipient='test2@localhost',
issue_thread=issue_thread, issue_thread=issue_thread,
) )
mail1_reply = Email.objects.create( mail1_reply = Email.objects.create(
subject='Message received', subject='Message received',
body='Thank you for your message.', body='Thank you for your message.',
sender='test2@test', sender='test2@localhost',
recipient='test1@test', recipient='test1@test',
in_reply_to=mail1.reference, in_reply_to=mail1.reference,
issue_thread=issue_thread, issue_thread=issue_thread,
) )
from aiosmtpd.smtp import Envelope from aiosmtpd.smtp import Envelope
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from email.message import EmailMessage
import aiosmtplib import aiosmtplib
import logging import logging
logging.disable(logging.CRITICAL) logging.disable(logging.CRITICAL)
@ -677,4 +692,23 @@ dGVzdGltYWdl
self.assertEqual(Email.objects.all()[2].body, 'bar') self.assertEqual(Email.objects.all()[2].body, 'bar')
self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread) self.assertEqual(Email.objects.all()[2].issue_thread, issue_thread)
self.assertEqual(Email.objects.all()[2].reference, '<3@test>') self.assertEqual(Email.objects.all()[2].reference, '<3@test>')
self.assertEqual('test', IssueThread.objects.all()[0].name) self.assertEqual('test subject', IssueThread.objects.all()[0].name)
response = self.client.post(f'/api/2/tickets/{issue_thread.id}/reply/', {
'message': 'test'
})
aiosmtplib.send.assert_called_once();
self.assertEqual(response.status_code, 201)
self.assertEqual(4, len(Email.objects.all()))
self.assertEqual(4, len(Email.objects.filter(issue_thread=issue_thread)))
self.assertEqual(1, len(IssueThread.objects.all()))
self.assertEqual(Email.objects.all()[3].subject, 'Re: test subject [#{0}]'.format(issue_thread.short_uuid()))
self.assertEqual(Email.objects.all()[3].sender, 'test2@localhost')
self.assertEqual(Email.objects.all()[3].recipient, 'test1@test')
self.assertEqual(Email.objects.all()[3].body, 'test')
self.assertEqual(Email.objects.all()[3].issue_thread, issue_thread)
self.assertTrue(Email.objects.all()[3].reference.startswith("<"))
self.assertTrue(Email.objects.all()[3].reference.endswith("@localhost>"))
self.assertEqual(Email.objects.all()[3].in_reply_to, mail1.reference)
self.assertEqual('test subject', IssueThread.objects.all()[0].name)
self.assertEqual('pending_new', IssueThread.objects.all()[0].state)
self.assertEqual(None, IssueThread.objects.all()[0].assigned_to)

View file

@ -10,6 +10,7 @@ from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer from channels.layers import get_channel_layer
from core.settings import MAIL_DOMAIN from core.settings import MAIL_DOMAIN
from mail.api_v2 import AttachmentSerializer
from mail.models import Email from mail.models import Email
from mail.protocol import send_smtp, make_reply, collect_references from mail.protocol import send_smtp, make_reply, collect_references
from notify_sessions.models import SystemEvent from notify_sessions.models import SystemEvent
@ -75,6 +76,7 @@ class IssueSerializer(serializers.ModelSerializer):
'recipient': email.recipient, 'recipient': email.recipient,
'subject': email.subject, 'subject': email.subject,
'body': email.body, 'body': email.body,
'attachments': AttachmentSerializer(email.attachments.all(), many=True).data,
}) })
return sorted(timeline, key=lambda x: x['timestamp']) return sorted(timeline, key=lambda x: x['timestamp'])

View file

@ -3,7 +3,7 @@ from datetime import datetime, timedelta
from django.test import TestCase, Client from django.test import TestCase, Client
from authentication.models import ExtendedUser from authentication.models import ExtendedUser
from mail.models import Email from mail.models import Email, EmailAttachment
from tickets.models import IssueThread, StateChange, Comment from tickets.models import IssueThread, StateChange, Comment
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from knox.models import AuthToken from knox.models import AuthToken
@ -147,6 +147,88 @@ class IssueApiTest(TestCase):
self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
response.json()[2]['timeline'][1]['timestamp']) response.json()[2]['timeline'][1]['timestamp'])
def test_issues_with_files(self):
from django.core.files.base import ContentFile
from hashlib import sha256
now = datetime.now()
issue = IssueThread.objects.create(
name="test issue",
)
mail1 = Email.objects.create(
subject='test',
body='test',
sender='test',
recipient='test',
issue_thread=issue,
timestamp=now,
)
mail2 = Email.objects.create(
subject='test',
body='test',
sender='test',
recipient='test',
issue_thread=issue,
in_reply_to=mail1.reference,
timestamp=now + timedelta(seconds=2),
)
comment = Comment.objects.create(
issue_thread=issue,
comment="test",
timestamp=now + timedelta(seconds=3),
)
file1 = EmailAttachment.objects.create(
name='file1', mime_type='text/plain', file=ContentFile(b"foo1", "f1"),
hash=sha256(b"foo1").hexdigest(), email=mail1
)
file2 = EmailAttachment.objects.create(
name='file2', mime_type='text/plain', file=ContentFile(b"foo2", "f2"),
hash=sha256(b"foo2").hexdigest(), email=mail1
)
response = self.client.get('/api/2/tickets/')
self.assertEqual(200, response.status_code)
self.assertEqual(1, len(response.json()))
self.assertEqual(issue.id, response.json()[0]['id'])
self.assertEqual('pending_new', response.json()[0]['state'])
self.assertEqual('test issue', response.json()[0]['name'])
self.assertEqual(None, response.json()[0]['assigned_to'])
self.assertEqual(36, len(response.json()[0]['uuid']))
self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
response.json()[0]['last_activity'])
self.assertEqual(4, len(response.json()[0]['timeline']))
self.assertEqual('state', response.json()[0]['timeline'][0]['type'])
self.assertEqual('mail', response.json()[0]['timeline'][1]['type'])
self.assertEqual('mail', response.json()[0]['timeline'][2]['type'])
self.assertEqual('comment', response.json()[0]['timeline'][3]['type'])
self.assertEqual(mail1.id, response.json()[0]['timeline'][1]['id'])
self.assertEqual(mail2.id, response.json()[0]['timeline'][2]['id'])
self.assertEqual(comment.id, response.json()[0]['timeline'][3]['id'])
self.assertEqual('pending_new', response.json()[0]['timeline'][0]['state'])
self.assertEqual('test', response.json()[0]['timeline'][1]['sender'])
self.assertEqual('test', response.json()[0]['timeline'][1]['recipient'])
self.assertEqual('test', response.json()[0]['timeline'][1]['subject'])
self.assertEqual('test', response.json()[0]['timeline'][1]['body'])
self.assertEqual(mail1.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
response.json()[0]['timeline'][1]['timestamp'])
self.assertEqual('test', response.json()[0]['timeline'][2]['sender'])
self.assertEqual('test', response.json()[0]['timeline'][2]['recipient'])
self.assertEqual('test', response.json()[0]['timeline'][2]['subject'])
self.assertEqual('test', response.json()[0]['timeline'][2]['body'])
self.assertEqual(mail2.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
response.json()[0]['timeline'][2]['timestamp'])
self.assertEqual('test', response.json()[0]['timeline'][3]['comment'])
self.assertEqual(comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
response.json()[0]['timeline'][3]['timestamp'])
self.assertEqual(2, len(response.json()[0]['timeline'][1]['attachments']))
self.assertEqual(0, len(response.json()[0]['timeline'][2]['attachments']))
self.assertEqual('file1', response.json()[0]['timeline'][1]['attachments'][0]['name'])
self.assertEqual('file2', response.json()[0]['timeline'][1]['attachments'][1]['name'])
self.assertEqual('text/plain', response.json()[0]['timeline'][1]['attachments'][0]['mime_type'])
self.assertEqual('text/plain', response.json()[0]['timeline'][1]['attachments'][1]['mime_type'])
self.assertEqual(file1.hash, response.json()[0]['timeline'][1]['attachments'][0]['hash'])
self.assertEqual(file2.hash, response.json()[0]['timeline'][1]['attachments'][1]['hash'])
def test_manual_creation(self): def test_manual_creation(self):
response = self.client.post('/api/2/tickets/manual/', response = self.client.post('/api/2/tickets/manual/',
{'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'}, {'name': 'test issue', 'sender': 'test', 'recipient': 'test', 'body': 'test'},

View file

@ -29,6 +29,7 @@
"vue-router": "^3.1.3", "vue-router": "^3.1.3",
"vuex": "^3.1.2", "vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0", "vuex-router-sync": "^5.0.0",
"vuex-shared-mutations": "^1.0.2",
"yarn": "^1.22.21" "yarn": "^1.22.21"
}, },
"devDependencies": { "devDependencies": {

View file

@ -0,0 +1,48 @@
<template>
<a :href="image_data">{{ download }}</a>
</template>
<style scoped>
</style>
<script>
import {mapActions} from "vuex";
export default {
name: "AuthenticatedDataLink",
props: {
href: {
type: String,
required: true
},
download: {
type: String,
required: false,
default: "unnamed"
}
},
data() {
return {
image_data: "",
servers: []
}
},
methods: {
...mapActions(['fetchImage']),
loadImage() {
this.fetchImage(this.href).then((response) => {
const mime_type = response.headers.get("content-type");
response.arrayBuffer().then((buf) => {
const base64 = btoa(new Uint8Array(buf)
.reduce((data, byte) => data + String.fromCharCode(byte), ""));
this.image_data = "data:" + mime_type + ";base64," + base64;
});
})
}
},
mounted() {
this.loadImage();
}
}
</script>

View file

@ -18,6 +18,14 @@
<font-awesome-icon icon="user"/> <font-awesome-icon icon="user"/>
</button--> </button-->
</div> </div>
<div class="card-footer" v-if="item.attachments.length">
<ul>
<li v-for="attachment in item.attachments">
<AuthenticatedImage :src="`/media/2/256/${attachment.hash}/`" :alt="attachment.name" v-if="attachment.mime_type.startsWith('image/')"/>
<AuthenticatedDataLink :href="`/media/2/256/${attachment.hash}/`" :download="attachment.name" v-else/>
</li>
</ul>
</div>
</div> </div>
<!--button class="show-replies"> <!--button class="show-replies">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-forward" <svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-forward"
@ -44,8 +52,12 @@
<script> <script>
import AuthenticatedImage from "@/components/AuthenticatedImage.vue";
import AuthenticatedDataLink from "@/components/AuthenticatedDataLink.vue";
export default { export default {
name: 'TimelineMail', name: 'TimelineMail',
components: {AuthenticatedImage, AuthenticatedDataLink},
props: { props: {
'item': { 'item': {
type: Object, type: Object,

View file

@ -7,6 +7,7 @@ import router from '../router';
import * as base64 from 'base-64'; import * as base64 from 'base-64';
import * as utf8 from 'utf8'; import * as utf8 from 'utf8';
import {ticketStateColorLookup, ticketStateIconLookup} from "@/utils"; import {ticketStateColorLookup, ticketStateIconLookup} from "@/utils";
import createMutationsSharer from "vuex-shared-mutations";
Vue.use(Vuex); Vue.use(Vuex);
const axios = AxiosBootstrap.create({ const axios = AxiosBootstrap.create({
@ -103,7 +104,6 @@ const store = new Vuex.Store({
slug: slug, slug: slug,
text: 'Unknown' text: 'Unknown'
} }
} }
}, },
isLoggedIn(state) { isLoggedIn(state) {
@ -273,10 +273,11 @@ const store = new Vuex.Store({
//async verifyToken({commit, state}) { //async verifyToken({commit, state}) {
async afterLogin({dispatch}) { async afterLogin({dispatch}) {
const boxes = dispatch('loadBoxes'); const boxes = dispatch('loadBoxes');
const states = dispatch('fetchTicketStates');
const items = dispatch('loadEventItems'); const items = dispatch('loadEventItems');
const tickets = dispatch('loadTickets'); const tickets = dispatch('loadTickets');
const user = dispatch('loadUserInfo'); const user = dispatch('loadUserInfo');
await Promise.all([boxes, items, tickets, user]); await Promise.all([boxes, items, tickets, user, states]);
}, },
async fetchImage({state}, url) { async fetchImage({state}, url) {
return await fetch(url, {headers: {'Authorization': `Token ${state.token}`}}); return await fetch(url, {headers: {'Authorization': `Token ${state.token}`}});
@ -394,7 +395,31 @@ const store = new Vuex.Store({
const {data} = await axios.patch(`/2/tickets/${id}/`, ticket); const {data} = await axios.patch(`/2/tickets/${id}/`, ticket);
commit('updateTicket', data); commit('updateTicket', data);
} }
} },
plugins: [createMutationsSharer({
predicate: [
'replaceLoadedItems',
'setItemCache',
'setLayout',
'replaceBoxes',
'updateItem',
'removeItem',
'appendItem',
'replaceTickets',
'replaceUsers',
'replaceGroups',
'updateTicket',
'openAddBoxModal',
'closeAddBoxModal',
'createToast',
'removeToast',
'setRemember',
'setUser',
'setPermissions',
'setToken',
'logout',
]
})],
}); });
export default store; export default store;