Adapt events view to new schema

This commit is contained in:
D. Berge
2022-02-27 19:28:15 +01:00
parent 180343754a
commit 278c46f975

View File

@@ -91,7 +91,7 @@
:headers="headers"
:items="rows"
:items-per-page.sync="itemsPerPage"
item-key="tstamp"
item-key="key"
sort-by="tstamp"
:sort-desc="true"
:search="filter"
@@ -108,175 +108,32 @@
</template>
<template v-slot:item.remarks="{item}">
<template v-if="writeaccess">
<v-edit-dialog v-if="item.items"
large
@save="rowEditorSave"
@cancel="rowEditorCancel"
@open="rowEditorOpen(item)"
@close="rowEditorClose"
> <div v-html="$options.filters.markdownInline(item.items.map(i => i.remarks).join('<br/>'))"></div>
<template v-slot:input>
<h3>{{
editedRow.sequence
? `${editedRow.sequence} @ ${editedRow.point}`
: editedRow.tstamp
? editedRow.tstamp.replace(/(.{10})T(.{8}).{4}Z$/, "$1 $2")
: editedRow.key
}}</h3><hr/>
<dougal-context-menu
:value="remarksMenu"
@input="addPresetRemark"
:items="presetRemarks"
absolute
></dougal-context-menu>
<template v-for="editedItem in editedRow.items">
<v-text-field
v-model="editedItem.remarks"
label="Edit"
single-line
hide-details="auto"
<template>
<div>
<div v-for="entry in item.items"
>
<template v-slot:prepend>
<v-icon v-show="!editedItem.remarks && presetRemarks"
title="Select predefined comments"
color="primary"
@click="(e) => {remarksMenuItem = editedItem; remarksMenu = e}"
>
mdi-dots-vertical
</v-icon>
</template>
<template v-slot:append v-if="editedItem.remarks || editedItem.labels.filter(l => labels[l].model.user).length">
<v-hover v-slot:default="{hover}">
<v-icon
title="Remove comment"
:color="hover ? 'error' : 'error lighten-4'"
@click="removeEvent(editedItem, editedRow)"
>mdi-minus-circle</v-icon>
</v-hover>
</template>
</v-text-field>
<v-container>
<v-row no-gutters>
<v-col class="flex-grow-0">
<!-- Add a new label control -->
<v-edit-dialog
large
@save="addLabel(editedItem)"
@cancel="selectedLabels=[]"
>
<v-icon
small
title="Add label"
>mdi-tag-plus</v-icon>
<template v-slot:input>
<v-autocomplete
:items="availableLabels(editedItem.labels)"
v-model="selectedLabels"
label="Add label"
chips
deletable-chips
multiple
autofocus
@keydown.stop="(e) => {if (e.key == 'Enter') debug(e)}"
@input="labelSearch = null;"
:search-input.sync="labelSearch"
>
<template v-slot:selection="data">
<v-chip
v-bind="data.attrs"
:input-value="data.selected"
small
@click="data.select"
:color="labels[data.item].view.colour"
:title="labels[data.item].view.description"
>{{data.item}}</v-chip>
</template>
</v-autocomplete>
</template>
</v-edit-dialog>
</v-col>
<v-col class="flex-grow-0">
<v-chip-group>
<v-chip v-for="label in editedItem.labels" :key="label"
small
:close="labels[label].model.user"
:color="labels[label].view.colour"
:title="labels[label].view.description"
@click:close="removeLabel(label, editedItem)"
>{{label}}</v-chip>
</v-chip-group>
</v-col>
</v-row>
</v-container>
</template>
<v-icon v-if="editedRow.items.length == 0 || editedRow.items[editedRow.items.length-1].remarks"
color="primary"
title="Add comment"
class="mb-2"
@click="addEvent"
>mdi-plus-circle</v-icon>
</template>
</v-edit-dialog>
<v-edit-dialog v-else
@save="rowEditorSave"
@cancel="rowEditorCancel"
@open="rowEditorOpen"
@close="rowEditorClose"
>
<template v-slot:input>
<v-text-field
v-model="props.item.remarks[0]"
label="Edit"
single-line
></v-text-field>
</template>
</v-edit-dialog>
</template>
<template v-else>
<div v-html="$options.filters.markdownInline(item.items.map(i => i.remarks).join('<br/>'))"></div>
</template>
</template>
<!-- Labels column -->
<template v-slot:item.labels="{item}">
<!-- Existing labels for row -->
<v-chip v-for="label in item.items.map(i => i.labels).flat()"
class="ma-1"
small
<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].view.colour"
:title="labels[label].view.description"
:key="label"
@click:close="removeLabel(label, item)"
:href="$route.path+'?label='+encodeURIComponent(label)"
>{{label}}</v-chip>
</span>
<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>
<!-- Actions column (FIXME currently not used) -->
<template v-slot:item.actions="{ item }">
<div style="white-space:nowrap;">
<a :href="viewOnMap(item)" v-if="viewOnMap(item)">
<v-icon v-if="$root.user || true"
small
class="mr-2"
title="View on map"
>
mdi-map
</v-icon>
</a>
</div>
</template>
</v-data-table>
@@ -285,6 +142,34 @@
</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';
@@ -333,16 +218,8 @@ export default {
{
value: "remarks",
text: "Text",
width: "100%"
},
{
value: "labels",
text: "Labels"
},
{
value: "actions",
text: "Actions",
sortable: false
width: "100%",
cellClass: "align-default"
}
],
items: [],
@@ -355,14 +232,6 @@ export default {
presetRemarks: null,
remarksMenu: null,
remarksMenuItem: null,
editedRow: {
key: null,
tstamp: null,
sequence: null,
point: null,
items: []
},
selectedLabels: [],
editedEvent: {},
labelSearch: null,
queuedReload: false,
@@ -374,7 +243,7 @@ export default {
rows () {
const rows = {};
this.items.forEach(i => {
const key = i.tstamp || (i.sequence+"@"+i.point);
const key = (i.sequence && i.point) ? (i.sequence+"@"+i.point) : i.tstamp;
if (!rows[key]) {
rows[key] = {
key,
@@ -608,144 +477,17 @@ export default {
}, callback]);
},
rowEditorOpen (row) {
this.editedRow = JSON.parse(JSON.stringify(row));
for (const item of this.editedRow.items) {
if (item.type != "timed" && item.type != "sequence") {
if (item.sequence && item.point) {
item.type = "sequence";
} else {
item.type = "timed";
}
}
}
},
rowEditorClose () {
},
async rowEditorSave () {
// Helper returns a callback that checks if two events have the same key
function within (item) {
return function (i) {
return EventKey(i) == EventKey(item);
}
}
const originalRow = this.rows.find(r => r.key == this.editedRow.key);
if (!originalRow) {
this.showSnack(["Cannot find the original item that was to be edited!", "error"]);
return;
}
const promises = [];
for (const editedItem of this.editedRow.items) {
// Process special text in remarks
if (editedItem.remarks) {
editedItem.remarks = this.$options.filters.position(editedItem.remarks, editedItem);
}
// Discard non user writable labels
editedItem.labels = editedItem.labels.filter(l => this.labels[l].model.user);
// Try to find this event in originalRow
const originalItem = originalRow.items.find(within(editedItem));
if (originalItem) {
// If found, check to see if its remarks or labels have changed
if (originalItem.remarks != editedItem.remarks || !ArraysEqual(originalItem.labels, editedItem.labels)) {
// This item has been modified
const url = `/project/${this.$route.params.project}/event/${editedItem.type}/${editedItem.id}/`;
const request = this.api([url, {
method: "PUT",
body: editedItem
}]);
promises.push(request);
} // Else, the item was not modified
} else {
// If not found, this is a new event
if (!editedItem.remarks.trim() && !editedItem.labels.length) {
// There is nothing to post, discard
continue;
}
const url = `/project/${this.$route.params.project}/event/`;
const request = this.api([url, {
method: "POST",
body: editedItem
}]);
promises.push(request);
}
}
for (const originalItem of originalRow.items) {
if (originalItem.virtual) {
continue;
}
// Try to find this event in editedRow
const editedItem = this.editedRow.items.find(within(originalItem));
if (!editedItem) {
// If not found, it has been deleted
const url = `/project/${this.$route.params.project}/event/${originalItem.type}/${originalItem.id}/`;
const request = this.api([url, {method: "DELETE"}]);
promises.push(request);
}
}
this.rowEditorCancel();
if (promises.length) {
this.showSnack(["Saving data…", "info"]);
await Promise.all(promises);
this.showSnack(["The changes have been saved", "success"]);
this.getEvents({cache: "reload"});
}
},
rowEditorCancel () {
this.$nextTick( () => {
this.editedRow = {
key: null,
tstamp: null,
sequence: null,
point: null,
items: []
}
});
},
addEvent () {
const count = this.editedRow.items.length;
const newItem = this.newItem(this.editedRow.items[count-1]);
this.editedRow.items.push(newItem);
},
removeEvent (event, row) {
const index = row.items.indexOf(event);
if (index >= 0) {
if (!event.virtual) {
// A virtual event will not be deletable so we can't
// remove it
row.items.splice(index, 1);
} else {
// What we do instead is clear any user-entered info
event.remarks = "";
event.labels = event.labels.filter(l => !this.labels[l].model.user);
}
}
},
@@ -761,29 +503,6 @@ export default {
.filter(k => !usedLabels.includes(k) && this.labels[k].model.user);
},
addLabel (editedItem) {
this.selectedLabels.forEach(label => {
if (!editedItem.labels.includes(label)) {
editedItem.labels.push(label);
}
});
this.selectedLabels = [];
},
removeLabel (label, item) {
item.labels = item.labels.filter(l => l != label);
/*
const url = `/project/${this.$route.params.project}/event/${item.type}/${item.id}/label/${label}`;
this.api([url, {method: "DELETE"}])
.then( () => this.showSnack([`Label ${item.label} removed`, "info"]) );
*/
},
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
@@ -808,12 +527,12 @@ export default {
}
},
viewOnMap(row) {
if (row && row.items && row.items.length) {
if (row.items[0].geometry && row.items[0].geometry.type == "Point") {
const [ lon, lat ] = row.items[0].geometry.coordinates;
viewOnMap(item) {
if (item?.meta && item.meta?.geometry?.type == "Point") {
const [ lon, lat ] = item.meta.geometry.coordinates;
return `map#15/${lon.toFixed(6)}/${lat.toFixed(6)}`;
}
} else if (item?.items) {
return this.viewOnMap(item.items[0]);
}
},