mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 13:07:08 +00:00
Adapt events view to new schema
This commit is contained in:
@@ -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 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
|
||||
:color="labels[label].view.colour"
|
||||
:title="labels[label].view.description"
|
||||
:key="label"
|
||||
@click:close="removeLabel(label, item)"
|
||||
>{{label}}</v-chip>
|
||||
</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"
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="entry in item.items"
|
||||
>
|
||||
mdi-map
|
||||
</v-icon>
|
||||
</a>
|
||||
</div>
|
||||
<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"
|
||||
: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>
|
||||
|
||||
</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
|
||||
@@ -807,13 +526,13 @@ export default {
|
||||
item.items.some( i => i.labels.some( l => l.toLowerCase().includes(s) ));
|
||||
}
|
||||
},
|
||||
|
||||
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;
|
||||
return `map#15/${lon.toFixed(6)}/${lat.toFixed(6)}`;
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user