Files
dougal-software/lib/www/client/source/src/views/LineList.vue

518 lines
15 KiB
Vue
Raw Normal View History

2020-08-08 23:59:13 +02:00
<template>
<v-container fluid>
2020-09-30 20:04:35 +02:00
<v-card>
<v-card-title>
<v-toolbar flat>
2020-10-02 00:47:39 +02:00
<v-toolbar-title>Preplots</v-toolbar-title>
2020-09-30 20:04:35 +02:00
<v-spacer></v-spacer>
<v-text-field
v-model="filter"
append-icon="mdi-magnify"
label="Filter"
single-line
clearable
></v-text-field>
</v-toolbar>
</v-card-title>
<v-card-text>
<v-menu v-if="writeaccess"
v-model="contextMenuShow"
:position-x="contextMenuX"
:position-y="contextMenuY"
absolute
offset-y
>
<v-list dense v-if="contextMenuItem">
<template v-if="!selectOn">
<v-list-item @click="setNTBA" v-if="contextMenuItem.ntba || (contextMenuItem.num_points == contextMenuItem.na)">
<v-list-item-title v-if="contextMenuItem.ntba"
title="Mark the line as part of the acquisition plan"
>Unset NTBA</v-list-item-title>
<v-list-item-title v-else
title="Mark the line as not to be acquired"
>Set NTBA</v-list-item-title>
</v-list-item>
<v-list-item @click="setComplete" v-if="contextMenuItem.na && (contextMenuItem.num_points != contextMenuItem.na || contextMenuItem.tba != contextMenuItem.na)">
<v-list-item-title v-if="contextMenuItem.tba != contextMenuItem.na"
title="Mark any remaining points as pending acquisition"
>Unset line complete</v-list-item-title>
<v-list-item-title v-else
title="Mark any remaining points as not to be acquired"
>Set line complete</v-list-item-title>
</v-list-item>
<v-list-item @click="addToPlan" v-if="!contextMenuItem.ntba && !isPlanned(contextMenuItem)">
<v-list-item-title>Add to plan</v-list-item-title>
</v-list-item>
<v-list-item @click="removeFromPlan" v-if="isPlanned(contextMenuItem)">
<v-list-item-title>Remove from plan</v-list-item-title>
</v-list-item>
<v-list-item @click="showLineColourDialog">
<v-list-item-title>Set colour</v-list-item-title>
</v-list-item>
</template>
<template v-else>
<v-list-item @click="showLineColourDialog">
<v-list-item-title>Set colour</v-list-item-title>
</v-list-item>
</template>
<v-divider></v-divider>
<v-list-item>
<v-list-item-action-text>Multi-select</v-list-item-action-text>
<v-list-item-action><v-checkbox v-model="selectOn"></v-checkbox></v-list-item-action>
</v-list-item>
</v-list>
</v-menu>
<v-dialog
v-model="colourPickerShow"
max-width="300"
>
<v-card>
<v-card-title>Choose line colour</v-card-title>
<v-card-text>
<v-color-picker
v-model="selectedColour"
show-swatches
hide-canvas
hide-inputs
hide-mode-switch
@update:color="selectedColour.alpha = 0.5"
>
</v-color-picker>
</v-card-text>
<v-card-actions>
<v-btn small
@click="selectedColour=null; setLineColour()"
>
Clear
</v-btn>
<v-spacer></v-spacer>
<v-btn small
@click="setLineColour"
>
Set
</v-btn>
<v-spacer></v-spacer>
<v-btn small
@click="colourPickerShow=false"
>
Cancel
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
2020-09-30 20:04:35 +02:00
<v-data-table
:headers="headers"
:items="items"
item-key="line"
:items-per-page.sync="itemsPerPage"
2020-09-30 20:04:35 +02:00
:search="filter"
:loading="loading"
:fixed-header="true"
:footer-props='{itemsPerPageOptions: [ 10, 25, 50, 100, 500, -1 ]}'
:item-class="itemClass"
:show-select="selectOn"
v-model="selectedRows"
@click:row="setActiveItem"
@contextmenu:row="contextMenu"
>
2020-09-30 20:04:35 +02:00
<template v-slot:item.status="{item}">
<dougal-line-status
:preplot="item"
:sequences="sequences.filter(s => s.line == item.line)"
:sequence-href="(s) => `/projects/${$route.params.project}/log/sequence/${s.sequence}`"
>
2020-10-01 15:31:24 +02:00
<template v-slot:empty>
<div v-if="!item.ntba" class="sequence" title="Virgin"></div>
<div v-else class="sequence ntba" title="Not to be acquired"><span>NTBA</span></div>
2020-10-01 15:31:24 +02:00
</template>
2020-09-30 20:04:35 +02:00
</dougal-line-status>
</template>
<template v-slot:item.tba="{item, value}">
<span :class="!value && (item.na ? 'warning--text' : 'success--text')">{{ value }}</span>
</template>
2020-09-30 20:04:35 +02:00
<template v-slot:item.length="props">
<span>{{ Math.round(props.value) }} m</span>
</template>
<template v-slot:item.azimuth="props">
<span>{{ props.value.toFixed(1) }} °</span>
</template>
<template v-slot:item.remarks="{item}">
<v-text-field v-if="edit && edit.line == item.line && edit.key == 'remarks'"
type="text"
v-model="edit.value"
prepend-icon="mdi-restore"
append-outer-icon="mdi-content-save-edit-outline"
clearable
@click:prepend="edit.value = item.remarks; edit = null"
@click:append-outer="edit = null"
>
</v-text-field>
<div v-else>
<span v-html="$options.filters.markdownInline(item.remarks)"></span>
<v-btn v-if="writeaccess && edit === null"
icon
small
title="Edit"
:disabled="loading"
@click="editItem(item, 'remarks')"
>
<v-icon small>mdi-square-edit-outline</v-icon>
</v-btn>
</div>
</template>
2020-09-30 20:04:35 +02:00
</v-data-table>
</v-card-text>
</v-card>
2020-08-08 23:59:13 +02:00
</v-container>
</template>
<style lang="stylus" scoped>
.ntba
background-image repeating-linear-gradient(-45deg, #ff00004d, #ff000080 10px, #d3d3d31a 10px, #d3d3d314 20px)
cursor not-allowed
display flex
align-items center
justify-content center
</style>
2020-08-08 23:59:13 +02:00
<script>
import { mapActions, mapGetters } from 'vuex';
import DougalLineStatus from '@/components/line-status';
2020-08-08 23:59:13 +02:00
export default {
name: "LineList",
components: {
DougalLineStatus
},
2020-08-08 23:59:13 +02:00
data () {
return {
headers: [
{
value: "line",
text: "Line"
},
{
value: "status",
text: "Status"
},
{
value: "fsp",
text: "FSP",
align: "end"
},
{
value: "lsp",
text: "LSP",
align: "end"
},
{
value: "num_points",
text: "Points",
align: "end"
},
{
value: "na",
text: "Virgin",
align: "end"
},
{
value: "tba",
text: "Remaining",
2020-08-08 23:59:13 +02:00
align: "end"
},
{
value: "length",
text: "Length",
align: "end"
},
{
value: "azimuth",
text: "Azimuth",
align: "end"
},
{
value: "remarks",
text: "Remarks"
}
],
items: [],
selectOn: false,
selectedRows: [],
filter: null,
num_lines: null,
sequences: [],
activeItem: null,
2020-10-02 00:41:16 +02:00
edit: null, // {line, key, value}
queuedReload: false,
itemsPerPage: 25,
// Context menu stuff
contextMenuShow: false,
contextMenuX: 0,
contextMenuY: 0,
contextMenuItem: null,
// Colour picker stuff
colourPickerShow: false,
selectedColour: null,
styles: null
2020-08-08 23:59:13 +02:00
}
},
computed: {
...mapGetters(['user', 'writeaccess', 'loading', 'serverEvent'])
},
watch: {
async edit (newVal, oldVal) {
if (newVal === null && oldVal !== null) {
const item = this.items.find(i => i.line == oldVal.line);
// Get around this Vuetify feature
// https://github.com/vuetifyjs/vuetify/issues/4144
if (oldVal.value === null) oldVal.value = "";
if (item && item[oldVal.key] != oldVal.value) {
if (await this.saveItem(oldVal)) {
item[oldVal.key] = oldVal.value;
} else {
this.edit = oldVal;
}
}
}
2020-10-02 00:41:16 +02:00
},
async serverEvent (event) {
if (event.payload.pid == this.$route.params.project) {
if (event.channel == "preplot_lines" || event.channel == "preplot_points") {
if (!this.loading && !this.queuedReload) {
// Do not force a non-cached response if refreshing as a result
// of an event notification. We will assume that the server has
// already had time to update the cache by the time our request
// gets back to it.
this.getLines();
} else {
this.queuedReload = true;
}
} else if ([ "planned_lines", "raw_lines", "final_lines" ].includes(event.channel)) {
if (!this.loading && !this.queuedReload) {
this.getSequences();
} else {
this.queuedReload = true;
}
2020-10-02 00:41:16 +02:00
}
}
},
queuedReload (newVal, oldVal) {
if (newVal && !oldVal && !this.loading) {
this.getLines();
this.getSequences();
}
},
loading (newVal, oldVal) {
if (!newVal && oldVal && this.queuedReload) {
this.getLines();
this.getSequences();
}
},
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;
}
},
2020-08-08 23:59:13 +02:00
methods: {
itemClass (item) {
const colourClass = item.meta.colour ? "bg-clr-"+item.meta.colour.slice(1) : null;
if (colourClass && ![...this.styles.cssRules].some(i => i.selectorText == "."+colourClass)) {
const rule = `.${colourClass} { background-color: ${item.meta.colour}; }`;
this.styles.insertRule(rule);
}
return [
item.meta.colour ? colourClass : "",
(this.activeItem == item && !this.edit) ? 'blue accent-1 elevation-3' : ''
];
},
isPlanned(item) {
return this.sequences.find(i => i.line == item.line && i.status == 'planned');
},
contextMenu (e, {item}) {
e.preventDefault();
this.contextMenuShow = false;
this.contextMenuX = e.clientX;
this.contextMenuY = e.clientY;
this.contextMenuItem = item;
this.$nextTick( () => this.contextMenuShow = true );
},
setNTBA () {
this.removeFromPlan();
this.saveItem({
line: this.contextMenuItem.line,
key: 'ntba',
value: !this.contextMenuItem.ntba
})
},
setComplete () {
this.saveItem({
line: this.contextMenuItem.line,
key: 'complete',
value: this.contextMenuItem.na && this.contextMenuItem.tba == this.contextMenuItem.na
})
},
2020-10-08 16:39:04 +02:00
async addToPlan () {
const payload = {
sequence: null,
line: this.contextMenuItem.line,
fsp: this.contextMenuItem.fsp,
lsp: this.contextMenuItem.lsp
}
const url = `/project/${this.$route.params.project}/plan`;
const init = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: payload
}
await this.api([url, init]);
},
async removeFromPlan () {
const plannedLine = this.sequences.find(i => i.status == "planned" && i.line == this.contextMenuItem.line);
if (plannedLine && plannedLine.sequence) {
const url = `/project/${this.$route.params.project}/plan/${plannedLine.sequence}`;
const init = {
method: "DELETE"
}
await this.api([url, init]);
}
},
showLineColourDialog () {
this.selectedColour = this.contextMenuItem.meta.colour
? {hexa: this.contextMenuItem.meta.colour}
: null;
this.colourPickerShow = true;
},
setLineColour () {
const items = this.selectOn ? this.selectedRows : [ this.contextMenuItem ];
const colour = this.selectedColour ? this.selectedColour.hex+"80" : null;
this.selectedRows = [];
this.selectOn = false;
for (const item of items) {
if (colour) {
item.meta.colour = colour;
} else {
delete item.meta.colour;
}
this.saveItem({line: item.line, key: "meta", value: item.meta});
this.colourPickerShow = false;
}
},
editItem (item, key) {
this.edit = {
line: item.line,
key,
value: item[key]
}
},
async saveItem (edit) {
if (!edit) return;
try {
const url = `/project/${this.$route.params.project}/line/${edit.line}`;
const init = {
method: "PATCH",
body: {
[edit.key]: edit.value
}
};
let res;
await this.api([url, init, (e, r) => res = r]);
return res && res.ok;
} catch (err) {
return false;
}
},
2020-08-08 23:59:13 +02:00
async getNumLines () {
const projectInfo = await this.api([`/project/${this.$route.params.project}`]);
this.num_lines = projectInfo.lines;
},
async getLines () {
2020-09-30 20:04:35 +02:00
const url = `/project/${this.$route.params.project}/line`;
2020-08-08 23:59:13 +02:00
2020-10-02 00:41:16 +02:00
this.queuedReload = false;
2020-08-08 23:59:13 +02:00
this.items = await this.api([url]) || [];
},
async getSequences () {
const urlS = `/project/${this.$route.params.project}/sequence`;
this.sequences = await this.api([urlS]) || [];
const urlP = `/project/${this.$route.params.project}/plan`;
const planned = await this.api([urlP]) || [];
planned.forEach(i => i.status = "planned");
this.sequences.push(...planned);
},
2020-08-08 23:59:13 +02:00
setActiveItem (item) {
this.activeItem = this.activeItem == item
? null
: item;
},
2020-08-08 23:59:13 +02:00
...mapActions(["api"])
},
mounted () {
this.getLines();
this.getNumLines();
this.getSequences();
// Initialise stylesheet
const el = document.createElement("style");
document.head.appendChild(el);
this.styles = document.styleSheets[document.styleSheets.length-1];
2020-08-08 23:59:13 +02:00
}
}
</script>