Add row context menu.

It replaces the `Actions` column in the old table and provides
more actions.

The user can now edit not just the comments and labels but also
the timestamp / shotpoint as requested in #78 (closes #78).

Because events are grouped by timestamp / shotpoint (each row
represents a unique timestamp or shotpoint), the behaviour is
slightly different depending on whether the user clicks on a
row containing a single (editable) event, or on one of multiple
editable events in the same row. Also, rows containing only
read-only events are recognised and no edition actions are
provided for those.
This commit is contained in:
D. Berge
2022-02-27 19:45:52 +01:00
parent d47c8a9e10
commit 4e0737335f

View File

@@ -95,6 +95,98 @@
</v-card-title>
<v-card-text>
<!-- BEGIN Context menu for log entries -->
<v-menu v-if="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>
</v-list>
</v-menu>
<!-- END Context menu for log entries -->
<v-data-table
:headers="headers"
:items="rows"
@@ -109,6 +201,7 @@
fixed-header
:footer-props='{itemsPerPageOptions: [ 10, 25, 50, 100, 500, -1 ]}'
@click:row="setActiveItem"
@contextmenu:row="contextMenu"
>
<template v-slot:item.tstamp="{value}">
@@ -121,6 +214,7 @@
<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"
@@ -259,6 +353,12 @@ export default {
// Row highlighter
activeItem: null,
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuItem: null
}
},
@@ -376,6 +476,32 @@ export default {
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);
},
async getEventCount () {
//this.eventCount = await this.api([`/project/${this.$route.params.project}/event/?count`]);
this.eventCount = null;
@@ -440,6 +566,19 @@ export default {
};
},
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,
@@ -457,6 +596,23 @@ export default {
}
},
/** 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) => {
@@ -512,17 +668,46 @@ export default {
}, 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.getEvents({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.getEvents({cache: "reload"});
}
}
const url = `/project/${this.$route.params.project}/event/${id}`;
await this.api([url, {
method: "DELETE",
}, callback]);
}
},