Files
dougal-software/lib/www/client/source/src/views/Log.vue
D. Berge ef8466992c Add automatic event icon to log.
So that the user can visually see which events were created by
Dougal (not including QC events).
2025-08-18 11:22:58 +02:00

896 lines
28 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<v-container fluid>
<v-card>
<v-card-title>
<v-toolbar flat>
<v-toolbar-title>
<span class="d-none d-lg-inline">
{{
$route.params.sequence
? ($route.params.sequence.includes && $route.params.sequence.includes(";"))
? `Sequences ${$route.params.sequence.split(";").sort().join(", ")}`
: `Sequence ${$route.params.sequence}`
: $route.params.date0
? $route.params.date1
? `Between ${$route.params.date0} and ${$route.params.date1}`
: `On ${$route.params.date0}`
: "All events"
}}
</span>
<span class="d-lg-none">
{{
$route.params.sequence
? ($route.params.sequence.includes && $route.params.sequence.includes(";"))
? `${$route.params.sequence.split(";").sort().join(", ")}`
: `${$route.params.sequence}`
: $route.params.date0
? $route.params.date1
? `${$route.params.date0} ${$route.params.date1}`
: `${$route.params.date0}`
: ""
}}
</span>
</v-toolbar-title>
<a v-if="$route.params.sequence"
class="mr-3"
:href="`/projects/${$route.params.project}/sequences/${$route.params.sequence}`"
title="View the shotlog for this sequence"
>
<v-icon
right
color="teal"
>mdi-format-list-numbered</v-icon>
</a>
<dougal-event-edit v-if="$parent.writeaccess()"
v-model="eventDialog"
v-bind="editedEvent"
:available-labels="userLabels"
:preset-remarks="presetRemarks"
@new="newEvent"
@changed="saveEvent"
>
</dougal-event-edit>
<dougal-event-edit-labels v-if="$parent.writeaccess()"
v-model="eventLabelsDialog"
:labels="userLabels"
:selected="contextMenuItem ? contextMenuItem.labels||[] : []"
@selectionChanged="(data) => patchEvent(contextMenuItem.id, data)"
>
</dougal-event-edit-labels>
<v-menu v-if="$route.params.sequence">
<template v-slot:activator="{on, attrs}">
<v-btn class="ml-3" small v-on="on" v-bind="attrs">
<span class="d-none d-lg-inline">Download as
<v-icon right small>mdi-cloud-download</v-icon>
</span>
<span class="d-lg-none">
<v-icon small>mdi-cloud-download</v-icon>
</span>
</v-btn>
</template>
<v-list>
<v-list-item
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=application%2Fvnd.seis%2Bjson`"
title="Download as a Multiseis-compatible Seis+JSON file."
>Seis+JSON</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=application%2Fgeo%2Bjson`"
title="Download as a QGIS-compatible GeoJSON file"
>GeoJSON</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=application%2Fjson`"
title="Download as a generic JSON file"
>JSON</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=text%2Fcsv`"
title="Download as Comma Separated Values file"
>CSV</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=text%2Fhtml`"
title="Download as an HTML formatted file"
>HTML</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/event/-/${$route.params.sequence}?mime=application%2Fpdf`"
title="Download as a Portable Document File"
>PDF</v-list-item>
</v-list>
</v-menu>
<v-menu v-else>
<template v-slot:activator="{on, attrs}">
<v-btn class="ml-5" small v-on="on" v-bind="attrs">
<span class="d-none d-lg-inline">Download as</span>
<v-icon right small>mdi-cloud-download</v-icon>
</v-btn>
</template>
<v-list>
<v-list-item
:href="`/api/project/${$route.params.project}/event?mime=application%2Fvnd.seis%2Bjson&filename=event-log-multiseis.json`"
title="Download as a Multiseis-compatible Seis+JSON file."
>Seis+JSON</v-list-item>
<!-- Not yet implemented
<v-list-item
:href="`/api/project/${$route.params.project}/event?mime=application%2Fgeo%2Bjson&filename=event-log.geojson`"
title="Download as a QGIS-compatible GeoJSON file"
>GeoJSON</v-list-item>
-->
<v-list-item
:href="`/api/project/${$route.params.project}/event?mime=application%2Fjson&filename=event-log.json`"
title="Download as a generic JSON file"
>JSON</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/event?mime=application%2Fyaml&filename=event-log.yaml`"
title="Download as a YAML file"
>YAML</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/event?mime=text%2Fcsv&sortBy=tstamp&sortDesc=false&filename=event-log.csv`"
title="Download as Comma Separated Values file"
>CSV</v-list-item>
<!-- Not yet implemented
<v-list-item
:href="`/api/project/${$route.params.project}/event?mime=text%2Fhtml&filename=event-log.html`"
title="Download as an HTML formatted file"
>HTML</v-list-item>
<v-list-item
:href="`/api/project/${$route.params.project}/event?mime=application%2Fpdf&filename=event-log.pdf`"
title="Download as a Portable Document File"
>PDF</v-list-item>
-->
</v-list>
</v-menu>
<v-spacer></v-spacer>
<v-text-field
v-model="filter"
append-icon="mdi-magnify"
label="Filter"
single-line
clearable
hide-details>
<template v-slot:prepend-inner>
<v-chip v-if="labelSearch"
class="mr-1"
small
close
@click:close="labelSearch=null"
:color="labels[labelSearch] && labels[labelSearch].view.colour"
:title="labels[labelSearch] && labels[labelSearch].view.description"
:dark="labels[labelSearch] && labels[labelSearch].view.dark"
:light="labels[labelSearch] && labels[labelSearch].view.light"
>{{labelSearch}}</v-chip>
</template>
</v-text-field>
</v-toolbar>
</v-card-title>
<v-card-text>
<!-- BEGIN Context menu for log entries -->
<v-menu v-if="$parent.writeaccess()"
v-model="contextMenuShow"
:position-x="contextMenuX"
:position-y="contextMenuY"
absolute
offset-y
>
<!-- Add comment: goes on every entry -->
<v-list dense v-if="contextMenuItem">
<v-list-item
@click="() => addEvent(contextMenuItem.items ? contextMenuItem.items[0] : contextMenuItem)"
>
<v-list-item-icon><v-icon>mdi-pencil-plus</v-icon></v-list-item-icon>
<v-list-item-title>Add comment</v-list-item-title>
</v-list-item>
<!-- BEGIN These options don't apply to read-only items -->
<template v-if="!isRowReadonly(contextMenuItem)">
<!-- BEGIN These are whole row entries NOTE: Why? TODO Reconsider this -->
<template v-if="contextMenuItem.key">
<!-- If the item has a shotpoint, that's what we edit -->
<!-- NOTE We may use this in the future to bulk edit comments
related to the same shot / moment, but not now.
<v-list-item @click="true" v-if="contextMenuItem.sequence">
<v-list-item-icon><v-icon>mdi-pencil</v-icon></v-list-item-icon>
<v-list-item-title>Edit shotpoint</v-list-item-title>
</v-list-item>
-->
<!-- Otherwise, it's the timestamp -->
<!-- NOTE We may use this in the future to bulk edit comments
related to the same shot / moment, but not now.
<v-list-item @click="true" v-else>
<v-list-item-icon><v-icon>mdi-pencil</v-icon></v-list-item-icon>
<v-list-item-title>Edit timestamp</v-list-item-title>
</v-list-item>
-->
</template>
<!-- END These are whole row entries -->
<!-- BEGIN These apply to individual comments -->
<!-- Edit comment -->
<v-list-item @click="editEvent(contextMenuItem)" v-if="contextMenuItem.remarks">
<v-list-item-icon><v-icon>mdi-pencil</v-icon></v-list-item-icon>
<v-list-item-title>Edit comment</v-list-item-title>
</v-list-item>
<!-- Edit labels -->
<!-- NOTE The edit comment dialogue takes care of this just fine, but let's
leave it for now and see if people use it. -->
<v-list-item @click="() => eventLabelsDialog=true" v-if="contextMenuItem.labels">
<v-list-item-icon><v-icon>mdi-tag-multiple</v-icon></v-list-item-icon>
<v-list-item-title>Edit labels</v-list-item-title>
</v-list-item>
<!-- END These apply to individual comments -->
</template>
<!-- END These options don't apply to read-only items -->
<!-- View item on map if it has a geometry -->
<v-list-item v-if="viewOnMap(contextMenuItem)" :href="viewOnMap(contextMenuItem)">
<v-list-item-icon><v-icon>mdi-map</v-icon></v-list-item-icon>
<v-list-item-title>View on map</v-list-item-title>
</v-list-item>
<v-divider></v-divider>
<!-- Delete command: single comment -->
<v-list-item @click="() => removeEvent(contextMenuItem)" v-if="deletableEntries(contextMenuItem) == 1">
<v-list-item-icon><v-icon>mdi-delete</v-icon></v-list-item-icon>
<v-list-item-title class="warning--text">Delete comment</v-list-item-title>
</v-list-item>
<!-- Delete command: read-only item (no action) -->
<v-list-item v-else-if="deletableEntries(contextMenuItem) == 0" disabled>
<v-list-item-icon><v-icon>mdi-delete-off</v-icon></v-list-item-icon>
<v-list-item-title>This entry is read-only</v-list-item-title>
</v-list-item>
<!-- Delete command: multiple comments -->
<v-list-item @click="() => removeEvent(contextMenuItem)" v-else>
<v-list-item-icon><v-icon>mdi-delete</v-icon></v-list-item-icon>
<v-list-item-title class="error--text">Delete all comments</v-list-item-title>
</v-list-item>
<!-- BEGIN This section only applies to QC events -->
<template v-if="contextMenuItem.meta.qc_id">
<v-divider></v-divider>
<!-- Mark QC accepted -->
<v-list-item @click="() => acceptQc(contextMenuItem)" v-if="!isAcceptedQc(contextMenuItem)">
<v-list-item-icon><v-icon>mdi-check</v-icon></v-list-item-icon>
<v-list-item-title>Mark QC accepted</v-list-item-title>
</v-list-item>
<!-- Unmark QC accepted -->
<v-list-item @click="() => acceptQc(contextMenuItem, false)" v-else>
<v-list-item-icon><v-icon>mdi-restore</v-icon></v-list-item-icon>
<v-list-item-title>Unmark QC accepted</v-list-item-title>
</v-list-item>
</template>
<!-- END This section only applies to QC events -->
</v-list>
</v-menu>
<!-- END Context menu for log entries -->
<v-data-table
dense
:headers="headers"
:items="rows"
:items-per-page.sync="itemsPerPage"
:server-items-length="eventCount"
item-key="key"
:item-class="itemClass"
sort-by="tstamp"
:sort-desc="true"
:search="filter"
:loading="eventsLoading"
:options.sync="options"
fixed-header
:footer-props='{itemsPerPageOptions: [ 10, 25, 50, 100, 500, -1 ], showFirstLastPage: true}'
@click:row="setActiveItem"
@contextmenu:row="contextMenu"
>
<template v-slot:item.tstamp="{value}">
<span style="white-space:nowrap;" v-if="value">
{{ value.replace(/(.{10})T(.{8}).{4}Z$/, "$1 $2") }}
</span>
</template>
<template v-slot:item.remarks="{item}">
<template>
<div>
<div v-for="entry in item.items"
@contextmenu.stop="(e) => contextMenu(e, {entry})"
>
<span v-if="entry.labels.length">
<v-chip v-for="label in entry.labels"
class="mr-1 px-2 underline-on-hover"
x-small
:color="labels[label] && labels[label].view.colour"
:title="labels[label] && labels[label].view.description"
:dark="labels[label] && labels[label].view.dark"
:light="labels[label] && labels[label].view.light"
:key="label"
@click="labelSearch=label"
>{{label}}</v-chip>
</span>
<v-icon v-if="entry.meta.auto || entry.meta.author"
x-small
left
color="primary"
:title="entry.meta.author?`Automatic event by ${entry.meta.author}`:'Automatic event'"
>mdi-robot</v-icon>
<dougal-event-edit-history v-if="entry.has_edits && $parent.writeaccess()"
:id="entry.id"
:disabled="eventsLoading"
:labels="labels"
></dougal-event-edit-history>
<span v-if="entry.meta.readonly"
class="entry text--secondary"
v-html="$options.filters.markdownInline(entry.remarks)">
</span>
<span v-else
class="entry underline-on-hover"
v-html="$options.filters.markdownInline(entry.remarks)">
</span>
</div>
</div>
</template>
</template>
<template v-slot:footer.prepend>
<v-checkbox v-for="label in filterableLabels"
:key="label"
class="mr-3"
v-model="shownLabels"
:value="label"
:title="`Show ${label} events`"
dense
hide-details
>
<template v-slot:label>
<v-chip
x-small
:color="labels[label] && labels[label].view.colour"
:title="labels[label] && labels[label].view.description"
:dark="labels[label] && labels[label].view.dark"
:light="labels[label] && labels[label].view.light"
>{{label}}
</v-chip>
</template>
</v-checkbox>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
</template>
<style>
tr.align-top td:not(.align-default) {
vertical-align: top;
}
</style>
<style scoped>
.hover {
opacity: 0.4;
}
.hover:hover {
opacity: 1;
}
.hover.off:hover {
opacity: 0.4;
}
.underline-on-hover:hover {
text-decoration: underline;
}
.entry {
cursor: default;
}
.entry.underline-on-hover:hover {
text-decoration: underline;
}
</style>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import DougalContextMenu from '@/components/context-menu';
import DougalEventEdit from '@/components/event-edit'
import DougalEventEditLabels from '@/components/event-edit-labels'
import DougalEventEditHistory from '@/components/event-edit-history'
function ArraysEqual (a, b) {
return a.every(i => b.includes(i)) && b.every(i => a.includes(i));
}
function EventKey (e) {
return e.type+e.id;
}
function clone (obj) {
return JSON.parse(JSON.stringify(obj));
}
export default {
name: "Log",
components: {
DougalEventEdit,
DougalEventEditLabels,
DougalEventEditHistory,
DougalContextMenu
},
data () {
return {
headers: [
{
value: "tstamp",
text: "Timestamp",
width: "20ex"
},
{
value: "sequence",
text: "Sequence",
align: "end",
width: "10ex"
},
{
value: "point",
text: "Shotpoint",
align: "end",
width: "10ex"
},
{
value: "remarks",
text: "Text",
width: "100%",
cellClass: "align-default"
}
],
items: [],
options: {},
filter: "",
filterableLabels: [ "QC", "QCAccepted" ],
shownLabels: [ "QC", "QCAccepted" ],
eventCount: null,
eventDialog: false,
eventLabelsDialog: false,
defaultEventTimestamp: null,
remarksMenu: null,
remarksMenuItem: null,
editedEvent: {},
labelSearch: null,
queuedReload: false,
itemsPerPage: 25,
// Row highlighter
activeItem: null,
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuItem: null
}
},
computed: {
rows () {
const rows = {};
this.items
.filter(i => {
return !this.$route.params.sequence || (this.$route.params.sequence == i.sequence)
})
.filter(i => {
for (const label of this.filterableLabels) {
if (!this.shownLabels.includes(label) && i.labels.includes(label)) {
return false;
}
}
return true;
})
.forEach(i => {
const key = (i.sequence && i.point) ? (i.sequence+"@"+i.point) : i.tstamp;
if (!rows[key]) {
rows[key] = {
key,
tstamp: i.tstamp,
sequence: i.sequence,
point: i.point,
items: []
}
}
const row = rows[key];
row.items.push(i);
});
return Object.values(rows);
},
popularLabels () {
const tuples = this.items.flatMap( i => i.labels )
.filter( l => (this.labels[l]??{})?.model?.user )
.reduce( (acc, cur) => {
return cur in acc ? ++acc[cur][1] : acc[cur]=[cur,1], acc
}, {});
return Object.values(tuples)
.sort( (a, b) => b[1]-a[1] );
},
presetRemarks () {
return this.projectConfiguration?.events?.presetRemarks ?? [];
},
defaultSequence () {
if (this.$route.params.sequence) {
return Number(this.$route.params.sequence.split(";").pop());
} else {
return this.sequence;
}
},
...mapGetters(['user', 'eventsLoading', 'online', 'sequence', 'line', 'point', 'position', 'timestamp', 'lineName', 'events', 'labels', 'userLabels', 'projectConfiguration']),
...mapState({projectSchema: state => state.project.projectSchema})
},
watch: {
options: {
async handler () {
await this.fetchEvents();
},
deep: true
},
async events () {
console.log("Events changed");
await this.fetchEvents();
},
eventDialog (val) {
if (val) {
// If not online
this.defaultEventTimestamp = Date.now();
}
},
filter (newVal, oldVal) {
if (newVal?.toLowerCase() != oldVal?.toLowerCase()) {
this.fetchEvents();
}
},
labelSearch () {
this.fetchEvents();
},
itemsPerPage (newVal, oldVal) {
localStorage.setItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`, newVal);
},
user (newVal, oldVal) {
this.itemsPerPage = Number(localStorage.getItem(`dougal/prefs/${this.user?.name}/${this.$route.params.project}/${this.$options.name}/items-per-page`)) || 25;
}
},
methods: {
debug (value) {
console.log("DEBUG", value);
},
contextMenu (e, {item, entry}) {
e.preventDefault();
this.contextMenuShow = false;
this.contextMenuX = e.clientX;
this.contextMenuY = e.clientY;
this.contextMenuItem = entry ?? item;
this.$nextTick( () => this.contextMenuShow = true );
},
/** Check if this item (or entry) has multiple
* deletable items, just one, or none (i.e., it is read-only)
*
* @return {Number} Number of deletable items.
*/
deletableEntries (item) {
return item.items
? item.items.filter(e => !e.meta.readonly).length
: item.meta.readonly
? 0
: 1;
},
isRowReadonly (item) {
return !this.deletableEntries(item);
},
itemClass (item) {
if (this.activeItem == item) {
return 'align-top blue accent-1 elevation-3';
} else if (item.sequence && item.point && item.tstamp) {
return this.$vuetify.theme.isDark
? 'align-top blue-grey darken-4'
: 'align-top blue-grey lighten-5';
} else {
return 'align-top';
}
},
async fetchEvents (opts = {}) {
const options = {
text: this.filter,
label: this.labelSearch,
...this.options
};
const res = await this.getEvents([this.$route.params.project, options]);
this.items = res.events;
this.eventCount = res.count;
},
newItem (from = {}) {
const type = (from.sequence && from.point) ? "sequence" : "timed";
const tstamp = from.tstamp || (new Date).toISOString();
const sequence = from.sequence || null; // FIXME TODO Use vuex
const point = from.point || null; // FIXME TODO Use vuex
const geometry = from.geometry || null; // FIXME TODO Use vuex
return {
type,
id: null,
tstamp,
sequence,
point,
remarks: "",
labels: [],
geometry
};
},
cloneEvent (template = {}) {
this.editedEvent = clone(template);
// NOTE Can we actually use "id", it being a reserved HTML attribute?
this.editedEvent.id = template.id;
if (template.meta?.geometry?.type == "Point") {
this.editedEvent.longitude = template.meta.geometry.coordinates[0];
this.editedEvent.latitude = template.meta.geometry.coordinates[1];
}
},
/** Add a brand new event.
*
* Used when adding a new event to the database,
* assumed to have occurred in the immediate past,
* so we populate it with the last received real-time
* information (timestamp, shotpoint, position, etc.)
*/
newEvent () {
this.editedEvent = {
tstamp: this.timestamp,
sequence: this.sequence,
point: this.point,
longitude: (this.position??[])[0],
latitude: (this.position??[])[1]
}
},
/** Add an event based on another.
*
* Used when adding an event to a timestamp or
* point for which we already have an event.
*/
addEvent (template) {
this.cloneEvent(template);
this.editedEvent.id = null;
this.editedEvent.remarks = null;
this.editedEvent.labels = null;
this.eventDialog = true;
},
editEvent (template) {
this.cloneEvent(template);
this.eventDialog = true;
},
async patchEvent (id, data) {
const callback = (err, res) => {
if (!err && res.ok) {
this.showSnack(["Event saved", "success"]);
this.queuedReload = true;
this.fetchEvents({cache: "reload"});
}
}
const url = `/project/${this.$route.params.project}/event/${id}`;
await this.api([url, {
method: "PATCH",
body: data
}, callback]);
},
// TODO POST or PATCH depending on whether this is a new event
// (no id) or an edit (id present)
async saveEvent (event) {
const callback = (err, res) => {
if (!err && res.ok) {
this.showSnack(["Event saved", "success"]);
this.queuedReload = true;
this.fetchEvents({cache: "reload"});
}
}
if (event) {
if (event.id) {
const id = event.id;
delete event.id;
// If this is an edit, ensure that it is *either*
// a timestamp event or a sequence + point one.
if (event.sequence && event.point && event.tstamp) {
delete event.tstamp;
}
this.putEvent(id, event, callback); // No await
} else {
this.postEvent(event, callback); // No await
}
}
},
async putEvent (id, event, callback) {
const url = `/project/${this.$route.params.project}/event/${id}`;
await this.api([url, {
method: "PATCH",
body: event
}, callback]);
},
async postEvent (event, callback) {
const url = `/project/${this.$route.params.project}/event`;
await this.api([url, {
method: "POST",
body: event
}, callback]);
},
async removeEvent (target) {
if (Array.isArray(target?.items)) {
return await this.removeEvent(target.items);
}
if (Array.isArray(target)) {
if (target.length == 1) {
return await this.removeEvent(target[0]);
}
const ids = target.map(i => i?.id ?? i);
const callback = (err, res) => {
if (!err && res.ok) {
this.showSnack([`${ids.length} events deleted`, "red"]);
this.queuedReload = true;
this.fetchEvents({cache: "reload"});
}
}
Promise.all(ids.forEach( id => {
const url = `/project/${this.$route.params.project}/event/${id}`;
return this.api([url, {method: "DELETE"}]);
})).then(callback);
} else {
const id = target?.id ?? target;
const callback = (err, res) => {
if (!err && res.ok) {
this.showSnack(["Event deleted", "red"]);
this.queuedReload = true;
this.fetchEvents({cache: "reload"});
}
}
const url = `/project/${this.$route.params.project}/event/${id}`;
await this.api([url, {
method: "DELETE",
}, callback]);
}
},
addPresetRemark ({text}) {
if (this.remarksMenuItem) {
this.remarksMenuItem.remarks = text;
}
this.remarksMenuItem = null;
},
availableLabels (usedLabels) {
return Object.keys(this.labels)
.filter(k => !usedLabels.includes(k) && this.labels[k].model.user);
},
handleKeyboardEvent (e) {
if (e.ctrlKey && !e.altKey && !e.shiftKey && (e.keyCode == 13 || e.key == "F2")) {
// Add timed event if offline or shot event if online
this.eventDialog = true;
} else if (e.ctrlKey && !e.altKey && e.shiftKey && (e.keyCode == 13 || e.key == "F2")) {
// Add timed event (even if online)
this.eventDialog = true;
}
},
viewOnMap(item) {
if (item?.meta && item.meta?.geometry?.type == "Point") {
const [ lon, lat ] = item.meta.geometry.coordinates;
return `map#z15x${lon.toFixed(6)}y${lat.toFixed(6)}::${lon.toFixed(6)},${lat.toFixed(6)}`;
} else if (item?.items) {
return this.viewOnMap(item.items[0]);
}
},
isAcceptedQc (item) {
return item.labels.includes('QCAccepted');
},
async acceptQc (item, accept = true) {
const url = accept
? `/project/${this.$route.params.project}/qc/results/accept`
: `/project/${this.$route.params.project}/qc/results/unaccept`;
await this.api([url, {
method: "POST",
body: [ item.id ]
}]);
},
setActiveItem (item) {
// Disable setting the active item for now,
// it's kind of annoying.
return;
/*
this.activeItem = this.activeItem == item
? null
: item;
*/
},
...mapActions(["api", "showSnack", "refreshEvents", "getEvents"])
},
async mounted () {
this.fetchEvents();
window.addEventListener('keyup', this.handleKeyboardEvent);
},
beforeDestroy () {
window.removeEventListener('keyup', this.handleKeyboardEvent);
}
}
</script>