This commit is contained in:
jedi 2024-01-31 22:51:01 +01:00
parent 4645a2f48c
commit be96901129
10 changed files with 272 additions and 111 deletions

View file

@ -52,11 +52,6 @@ export default {
},
created: function () {
document.title = document.location.hostname;
this.loadEvents().then(() => {
/*if (this.isLoggedIn) {
this.afterLogin();
}*/
});
}
};
</script>

View file

@ -7,7 +7,7 @@
<script>
import {mapActions} from "vuex";
import {mapActions, mapGetters, mapMutations} from "vuex";
export default {
name: "AuthenticatedImage",
@ -16,6 +16,7 @@ export default {
type: String,
required: true
},
cached: Boolean
},
data() {
return {
@ -23,8 +24,12 @@ export default {
servers: []
}
},
computed: {
...mapGetters(['getThumbnail']),
},
methods: {
...mapActions(['fetchImage']),
...mapMutations(['setThumbnail']),
async loadImage() {
const response = await this.fetchImage(this.src);
const mime_type = response.headers.get("content-type");
@ -32,10 +37,22 @@ export default {
const base64 = btoa(new Uint8Array(buf)
.reduce((data, byte) => data + String.fromCharCode(byte), ""));
this.image_data = "data:" + mime_type + ";base64," + base64;
if (this.cached)
this.setThumbnail({
url: this.src,
data: this.image_data
});
}
},
mounted() {
setTimeout(() => {
if (this.cached) {
const c = this.getThumbnail(this.src);
if (c) {
this.image_data = c;
return;
}
}
this.loadImage();
}, 0);
}

View file

@ -25,8 +25,8 @@
<ul>
<li v-for="attachment in item.attachments" @click="openLightboxModalWith(attachment)">
<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-if="attachment.mime_type.startsWith('image/')" cached/>
<AuthenticatedDataLink :href="`/media/2/${attachment.hash}/`" :download="attachment.name"
v-else/>
</li>
</ul>

View file

@ -25,7 +25,15 @@ export default {
computed: {
...mapState(['state_options']),
lookupState: function () {
return this.state_options.find(state => state.value === this.item.state);
try {
if (this.item.state)
return this.state_options.find(state => state.value === this.item.state);
} catch (e) {
}
return {
text: 'Unknown',
value: 'unknown'
};
},
colorLookup: function () {
if (this.item.state.startsWith('closed_')) {

View file

@ -4,10 +4,71 @@ export default (config) => {
if (!('isLoadedKey' in config)) {
throw new Error("isLoadedKey not defined in config");
}
if (('asyncFetch' in config) && !('lastfetched' in config)) {
throw new Error("asyncFetch defined but lastfetched not defined in config");
}
if (config.debug) console.log('plugin created');
/** may only be called from worker */
const clone = (obj) => {
if (isProxy(obj)) {
obj = toRaw(obj);
}
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj.__proto__ === ({}).__proto__) {
return Object.assign({}, obj);
}
if (obj.__proto__ === [].__proto__) {
return obj.slice();
}
return obj;
}
const deepEqual = (a, b) => {
if (a === b) {
return true;
}
if (a === null || b === null) {
return false;
}
if (a.__proto__ === ({}).__proto__ && b.__proto__ === ({}).__proto__) {
if (Object.keys(a).length !== Object.keys(b).length) {
return false;
}
for (let key in b) {
if (!(key in a)) {
return false;
}
}
for (let key in a) {
if (!(key in b)) {
return false;
}
if (!deepEqual(a[key], b[key])) {
return false;
}
}
return true;
}
if (a.__proto__ === [].__proto__ && b.__proto__ === [].__proto__) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; i++) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
return true;
}
return false;
}
const worker_fun = function (self, ctx) {
/* globals WebSocket, SharedWorker, onconnect, onmessage, postMessage, close, location */
@ -18,32 +79,32 @@ export default (config) => {
const tryConnect = () => {
if (self.WebSocket === undefined) {
if(ctx.debug) console.log("no websocket support");
if (ctx.debug) console.log("no websocket support");
return;
}
if (!notify_socket || notify_socket.readyState !== WebSocket.OPEN) {
// global location is not useful in worker loaded from data url
const scheme = ctx.location.protocol === "https:" ? "wss" : "ws";
if(ctx.debug) console.log("connecting to", scheme + '://' + ctx.location.host + '/ws/2/notify/');
if (ctx.debug) console.log("connecting to", scheme + '://' + ctx.location.host + '/ws/2/notify/');
notify_socket = new WebSocket(scheme + '://' + ctx.location.host + '/ws/2/notify/');
notify_socket.onopen = (e) => {
if(ctx.debug) console.log("open", JSON.stringify(e));
if (ctx.debug) console.log("open", JSON.stringify(e));
};
notify_socket.onclose = (e) => {
if(ctx.debug) console.log("close", JSON.stringify(e));
if (ctx.debug) console.log("close", JSON.stringify(e));
setTimeout(() => {
tryConnect();
}, 1000);
};
notify_socket.onerror = (e) => {
if(ctx.debug) console.log("error", JSON.stringify(e));
if (ctx.debug) console.log("error", JSON.stringify(e));
setTimeout(() => {
tryConnect();
}, 1000);
};
notify_socket.onmessage = (e) => {
let data = JSON.parse(e.data);
if(ctx.debug) console.log("message", data);
if (ctx.debug) console.log("message", data);
//this.loadEventItems()
//this.loadTickets()
}
@ -105,15 +166,14 @@ export default (config) => {
case 'state_diff':
if (message_data.key in state) {
if (!deepEqual(state[message_data.key], message_data.old_value)) {
if (ctx.debug) console.log("state diff old value mismatch | state:",
state[message_data.key], " old:", message_data.old_value);
if (ctx.debug) console.log("state diff old value mismatch | state:", state[message_data.key], " old:", message_data.old_value);
}
if (!deepEqual(state[message_data.key], message_data.new_value)) {
if (ctx.debug) console.log("state diff changed | state:", state[message_data.key], " new:", message_data.new_value);
state[message_data.key] = message_data.new_value;
others(message_data);
} else {
if (ctx.debug) console.log("state diff no change | state:",
state[message_data.key], " new:", message_data.new_value);
if (ctx.debug) console.log("state diff no change | state:", state[message_data.key], " new:", message_data.new_value);
}
} else {
if (ctx.debug) console.log("state diff key not found", message_data.key);
@ -150,12 +210,13 @@ export default (config) => {
tryConnect();
}
if(ctx.debug) console.log("worker loaded");
if (ctx.debug) console.log("worker loaded");
}
const worker_context = {
location: {...location},
debug: config.debug
location: {
protocol: location.protocol, host: location.host
}, bug: config.debug
}
const worker_code = '(' + worker_fun.toString() + ')(self,' + JSON.stringify(worker_context) + ')';
const worker_url = 'data:application/javascript;base64,' + btoa(worker_code);
@ -165,7 +226,7 @@ export default (config) => {
if (config.debug) console.log('worker started');
const updateWorkerState = (key, new_value, old_value = null) => {
if(new_value === old_value) {
if (new_value === old_value) {
if (config.debug) console.log('updateWorkerState: no change', key, new_value);
return;
}
@ -203,19 +264,26 @@ export default (config) => {
} else {
for (let key in e.data.state) {
if (key in store.state) {
if (config.debug) console.log('worker state init received', key, e.data.state[key]);
store.state[key] = e.data.state[key];
if (config.debug) console.log('worker state init received', key, clone(e.data.state[key]));
if (!deepEqual(store.state[key], e.data.state[key])) {
store.state[key] = e.data.state[key];
}
} else {
if (config.debug) console.log("state init key not found", key);
}
}
}
store[config.isLoadedKey] = true;
store.state[config.isLoadedKey] = true;
if ('afterInit' in config) {
setTimeout(() => {
store.dispatch(config.afterInit);
}, 0);
}
break;
case 'state_diff':
if (config.debug) console.log('state_diff', e.data);
if (e.data.key in store.state) {
if (config.debug) console.log('worker state update', e.data.key, e.data.new_value);
if (config.debug) console.log('worker state update', e.data.key, clone(e.data.new_value));
//TODO this triggers the watcher again, but we don't want that
store.state[e.data.key] = e.data.new_value;
} else {
@ -248,7 +316,7 @@ export default (config) => {
if ('state' in config) {
config.watch.forEach((member) => {
store.watch((state, getters) => state[member], (newValue, oldValue) => {
if (config.debug) console.log('watch', member, newValue, oldValue);
if (config.debug) console.log('watch', member, clone(newValue), clone(oldValue));
updateWorkerState(member, newValue, oldValue);
});
});

View file

@ -31,9 +31,19 @@ const store = createStore({
expiry: null,
},
fetchedData: {},
shared_loaded: false,
thumbnailCache: {},
fetchedData: {
events: 0,
items: 0,
boxes: 0,
tickets: 0,
users: 0,
groups: 0,
states: 0,
},
persistent_loaded: false,
shared_loaded: false,
afterInitHandlers: [],
showAddBoxModal: false,
},
@ -75,6 +85,12 @@ const store = createStore({
isLoggedIn(state) {
return state.user && state.user.username !== null && state.user.token !== null;
},
getThumbnail: (state) => (url) => {
if (!url) return null;
if (!(url in state.thumbnailCache))
return null;
return state.thumbnailCache[url];
},
},
mutations: {
updateLastEvent(state, slug) {
@ -82,21 +98,25 @@ const store = createStore({
},
replaceEvents(state, events) {
state.events = events;
state.fetchedData = {...state.fetchedData, events: Date.now()};
},
replaceTicketStates(state, states) {
state.state_options = states;
state.fetchedData = {...state.fetchedData, states: Date.now()};
},
changeView(state, {view, slug}) {
router.push({path: `/${slug}/${view}`});
},
replaceLoadedItems(state, newItems) {
state.loadedItems = newItems;
state.fetchedData = {...state.fetchedData, items: Date.now()}; // TODO: manage caching items for different events and search results correctly
},
setItemCache(state, {slug, items}) {
state.itemCache[slug] = items;
},
replaceBoxes(state, loadedBoxes) {
state.loadedBoxes = loadedBoxes;
state.fetchedData = {...state.fetchedData, boxes: Date.now()};
},
updateItem(state, updatedItem) {
const item = state.loadedItems.filter(({uid}) => uid === updatedItem.uid)[0];
@ -110,12 +130,15 @@ const store = createStore({
},
replaceTickets(state, tickets) {
state.tickets = tickets;
state.fetchedData = {...state.fetchedData, tickets: Date.now()};
},
replaceUsers(state, users) {
state.users = users;
state.fetchedData = {...state.fetchedData, users: Date.now()};
},
replaceGroups(state, groups) {
state.groups = groups;
state.fetchedData = {...state.fetchedData, groups: Date.now()};
},
updateTicket(state, updatedTicket) {
const ticket = state.tickets.filter(({id}) => id === updatedTicket.id)[0];
@ -169,6 +192,9 @@ const store = createStore({
setTest(state, test) {
state.test = test;
},
setThumbnail(state, {url, data}) {
state.thumbnailCache[url] = data;
},
},
actions: {
async login({commit}, {username, password, remember}) {
@ -213,32 +239,50 @@ const store = createStore({
store.commit('logout');
},
//async verifyToken({commit, state}) {
/*async afterLogin({dispatch, state}) {
//const boxes = dispatch('loadBoxes');
//const states = dispatch('fetchTicketStates');
//const items = dispatch('loadEventItems');
//const tickets = dispatch('loadTickets');
//const user = dispatch('loadUserInfo');
//await Promise.all([boxes, items, tickets, user, states]);
async afterLogin({dispatch, state}) {
let promises = [];
promises.push(dispatch('loadBoxes'));
promises.push(dispatch('fetchTicketStates'));
promises.push(dispatch('loadEventItems'));
promises.push(dispatch('loadTickets'));
if (!state.user.permissions) {
const user = dispatch('loadUserInfo');
await Promise.all([user]);
promises.push(dispatch('loadUserInfo'));
}
},*/
await Promise.all(promises);
},
async afterSharedInit({dispatch, state}) {
const handlers = state.afterInitHandlers;
state.afterInitHandlers = [];
await Promise.all(handlers.map(h => h()).flat());
},
scheduleAfterInit({dispatch, state}, handler) {
if (state.shared_loaded) {
Promise.all(handler()).then(() => {
});
} else {
state.afterInitHandlers.push(handler);
}
},
async fetchImage({state}, url) {
return await fetch(url, {headers: {'Authorization': `Token ${state.user.token}`}});
},
async loadUserInfo({commit, state}) {
const {data} = await http.get('/2/self/', state.user.token);
const {data, success} = await http.get('/2/self/', state.user.token);
commit('setPermissions', data.permissions);
},
async loadEvents({commit, state}) {
const {data} = await http.get('/2/events/', state.user.token);
commit('replaceEvents', data);
if (!state.user.token) return;
if (state.fetchedData.events > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/events/', state.user.token);
if (data && success)
commit('replaceEvents', data);
},
async fetchTicketStates({commit, state}) {
const {data} = await http.get('/2/tickets/states/', state.user.token);
commit('replaceTicketStates', data);
if (!state.user.token) return;
if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/tickets/states/', state.user.token);
if (data && success)
commit('replaceTicketStates', data);
},
changeEvent({dispatch, getters, commit}, eventName) {
router.push({path: `/${eventName.slug}/${getters.getActiveView}/`});
@ -251,15 +295,19 @@ const store = createStore({
router.push({path: `/${getters.getEventSlug}/items/`, query: {box}});
},
async loadEventItems({commit, getters, state}) {
if (!state.user.token) return;
if (state.fetchedData.items > Date.now() - 1000 * 60 * 60 * 24) return;
try {
commit('replaceLoadedItems', []);
const slug = getters.getEventSlug;
if (slug in state.itemCache) {
commit('replaceLoadedItems', state.itemCache[slug]);
}
const {data} = await http.get(`/2/${slug}/items/`, state.user.token);
commit('replaceLoadedItems', data);
commit('setItemCache', {slug, items: data});
const {data, success} = await http.get(`/2/${slug}/items/`, state.user.token);
if (data && success) {
commit('replaceLoadedItems', data);
commit('setItemCache', {slug, items: data});
}
} catch (e) {
console.error("Error loading items");
}
@ -268,15 +316,19 @@ const store = createStore({
const foo = utf8.encode(query);
const bar = base64.encode(foo);
const {data} = await http.get(`/2/${getters.getEventSlug}/items/${bar}/`, state.user.token);
commit('replaceLoadedItems', data);
const {data, success} = await http.get(`/2/${getters.getEventSlug}/items/${bar}/`, state.user.token);
if (data && success)
commit('replaceLoadedItems', data);
},
async loadBoxes({commit, state}) {
const {data} = await http.get('/2/boxes/', state.user.token);
commit('replaceBoxes', data);
if (!state.user.token) return;
if (state.fetchedData.boxes > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/boxes/', state.user.token);
if (data && success)
commit('replaceBoxes', data);
},
async createBox({commit, dispatch, state}, box) {
const {data} = await http.post('/2/boxes/', box, state.user.token);
const {data, success} = await http.post('/2/boxes/', box, state.user.token);
commit('replaceBoxes', data);
dispatch('loadBoxes').then(() => {
commit('closeAddBoxModal');
@ -287,7 +339,10 @@ const store = createStore({
dispatch('loadBoxes');
},
async updateItem({commit, getters, state}, item) {
const {data} = await http.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item, state.user.token);
const {
data,
success
} = await http.put(`/2/${getters.getEventSlug}/item/${item.uid}/`, item, state.user.token);
commit('updateItem', data);
},
async markItemReturned({commit, getters, state}, item) {
@ -300,19 +355,22 @@ const store = createStore({
},
async postItem({commit, getters, state}, item) {
commit('updateLastUsed', {box: item.box, cid: item.cid});
const {data} = await http.post(`/2/${getters.getEventSlug}/item/`, item, state.user.token);
const {data, success} = await http.post(`/2/${getters.getEventSlug}/item/`, item, state.user.token);
commit('appendItem', data);
},
async loadTickets({commit, state}) {
const {data} = await http.get('/2/tickets/', state.user.token);
commit('replaceTickets', data);
if (!state.user.token) return;
if (state.fetchedData.tickets > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/tickets/', state.user.token);
if (data && success)
commit('replaceTickets', data);
},
async sendMail({commit, dispatch, state}, {id, message}) {
const {data} = await http.post(`/2/tickets/${id}/reply/`, {message}, state.user.token);
const {data, success} = await http.post(`/2/tickets/${id}/reply/`, {message}, state.user.token);
await dispatch('loadTickets');
},
async postManualTicket({commit, dispatch, state}, {sender, message, title,}) {
const {data} = await http.post(`/2/tickets/manual/`, {
const {data, success} = await http.post(`/2/tickets/manual/`, {
name: title,
sender,
body: message,
@ -320,53 +378,68 @@ const store = createStore({
}, state.user.token);
await dispatch('loadTickets');
},
async postComment({commit, dispatch, staee}, {id, message}) {
const {data} = await http.post(`/2/tickets/${id}/comment/`, {comment: message}, state.user.token);
async postComment({commit, dispatch, state}, {id, message}) {
const {data, success} = await http.post(`/2/tickets/${id}/comment/`, {comment: message}, state.user.token);
await dispatch('loadTickets');
},
async loadUsers({commit, state}) {
const {data} = await http.get('/2/users/', state.user.token);
commit('replaceUsers', data);
if (!state.user.token) return;
if (state.fetchedData.users > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/users/', state.user.token);
if (data && success)
commit('replaceUsers', data);
},
async loadGroups({commit, state}) {
const {data} = await http.get('/2/groups/', state.user.token);
commit('replaceGroups', data);
if (!state.user.token) return;
if (state.fetchedData.groups > Date.now() - 1000 * 60 * 60 * 24) return;
const {data, success} = await http.get('/2/groups/', state.user.token);
if (data && success)
commit('replaceGroups', data);
},
async updateTicket({commit, state}, ticket) {
const {data} = await http.put(`/2/tickets/${ticket.id}/`, ticket, state.user.token);
const {data, success} = await http.put(`/2/tickets/${ticket.id}/`, ticket, state.user.token);
commit('updateTicket', data);
},
async updateTicketPartial({commit, state}, {id, ...ticket}) {
const {data} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token);
const {data, success} = await http.patch(`/2/tickets/${id}/`, ticket, state.user.token);
commit('updateTicket', data);
}
},
plugins: [
persistentStatePlugin({ // TODO change remember to some kind of enable field
prefix: "lf_",
debug: true,
debug: false,
isLoadedKey: "persistent_loaded",
state: [
"remember",
"user",
"events",
]
}),
sharedStatePlugin({
debug: true,
debug: false,
isLoadedKey: "shared_loaded",
clearingMutation: "logout",
afterInit: "afterSharedInit",
state: [
"test",
"state_options",
"events",
"fetchData",
"fetchedData",
"tickets",
"users",
"groups",
"loadedBoxes",
"loadedItems",
],
watch: [
"test",
"state_options",
"events",
"fetchData",
"fetchedData",
"tickets",
"users",
"groups",
"loadedBoxes",
"loadedItems",
],
mutations: [
//"replaceTickets",
@ -375,19 +448,6 @@ const store = createStore({
],
});
// watch: {
// user: function (user) {
// console.log('user changed', user);
// if (user.token) {
// }
// //if(token && this.state.route.name === 'login') {
// //if (state.route.name !== 'login')
// // router.push('/login');
// //if (state.route.name === 'login' && state.route.query.redirect)
// // router.push(state.route.query.redirect);
// },
// },
store.watch((state) => state.user, (user) => {
console.log('user changed', user);
if (store.getters.isLoggedIn) {

View file

@ -25,6 +25,20 @@ function ticketStateIconLookup(ticket) {
}
const http = {
get: async (url, token) => {
if (!token) {
return null;
}
const response = await fetch('/api' + url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": `Token ${token}`,
},
});
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
},
post: async (url, data, token) => {
if (!token) {
return null;
@ -37,19 +51,8 @@ const http = {
},
body: JSON.stringify(data),
});
return {data: await response.json()};
}, get: async (url, token) => {
if (!token) {
return null;
}
const response = await fetch('/api' + url, {
method: 'GET',
headers: {
"Content-Type": "application/json",
"Authorization": `Token ${token}`,
},
});
return {data: await response.json()};
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
},
put: async (url, data, token) => {
if (!token) {
@ -63,7 +66,8 @@ const http = {
},
body: JSON.stringify(data),
});
return {data: await response.json()};
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
},
patch: async (url, data, token) => {
if (!token) {
@ -77,7 +81,8 @@ const http = {
},
body: JSON.stringify(data),
});
return {data: await response.json()};
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
},
delete: async (url, token) => {
if (!token) {
@ -90,7 +95,8 @@ const http = {
"Authorization": `Token ${token}`,
},
});
return {data: await response.json()};
const success = response.status === 200 || response.status === 201;
return {data: await response.json() || {}, success};
}
}

View file

@ -23,13 +23,15 @@
>
<template #actions="{ item }">
<div class="btn-group">
<button class="btn btn-success" @click.stop="confirm('return Item?') && markItemReturned(item)" title="returned">
<button class="btn btn-success"
@click.stop="confirm('return Item?') && markItemReturned(item)" title="returned">
<font-awesome-icon icon="check"/>
</button>
<button class="btn btn-secondary" @click.stop="openEditingModalWith(item)" title="edit">
<font-awesome-icon icon="edit"/>
</button>
<button class="btn btn-danger" @click.stop="confirm('delete Item?') && deleteItem(item)" title="delete">
<button class="btn btn-danger" @click.stop="confirm('delete Item?') && deleteItem(item)"
title="delete">
<font-awesome-icon icon="trash"/>
</button>
</div>
@ -45,7 +47,7 @@
v-slot="{ item }"
@itemActivated="openLightboxModalWith($event)"
>
<AuthenticatedImage v-if="item.file"
<AuthenticatedImage v-if="item.file" cached
:src="`/media/2/256/${item.file}/`"
class="card-img-top img-fluid"
/>
@ -93,7 +95,7 @@ export default {
...mapGetters(['layout']),
},
methods: {
...mapActions(['deleteItem', 'markItemReturned']),
...mapActions(['deleteItem', 'markItemReturned', 'loadEventItems', 'scheduleAfterInit']),
openLightboxModalWith(item) {
this.lightboxHash = item.file;
},
@ -113,6 +115,9 @@ export default {
confirm(message) {
return window.confirm(message);
}
},
mounted() {
this.scheduleAfterInit(() => [this.loadEventItems()]);
}
};
</script>

View file

@ -62,6 +62,7 @@ export default {
},
methods: {
...mapActions(['deleteItem', 'markItemReturned', 'sendMail', 'updateTicketPartial', 'postComment']),
...mapActions(['loadTickets', 'fetchTicketStates', 'loadUsers', 'scheduleAfterInit']),
handleMail(mail) {
this.sendMail({
id: this.ticket.id,
@ -87,7 +88,9 @@ export default {
})
}
},
created() {}
mounted() {
this.scheduleAfterInit(() => [this.fetchTicketStates(), this.loadTickets(), this.loadUsers()]);
}
};
</script>

View file

@ -65,7 +65,7 @@ export default {
...mapGetters(['stateInfo', 'getEventSlug', 'layout']),
},
methods: {
...mapActions(['loadTickets', 'fetchTicketStates']),
...mapActions(['loadTickets', 'fetchTicketStates', 'scheduleAfterInit']),
gotoDetail(ticket) {
this.$router.push({name: 'ticket', params: {id: ticket.id}});
},
@ -80,9 +80,8 @@ export default {
};
}
},
created() {
this.fetchTicketStates();
this.loadTickets();
mounted() {
this.scheduleAfterInit(() => [this.fetchTicketStates(), this.loadTickets()]);
}
};
</script>