mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 12:17:08 +00:00
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:
@@ -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]);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user