mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 11:17:08 +00:00
Add QC view to web interface.
This commit implements most of what is required by #23, with the exception of labels processing as stated in https://gitlab.com/wgp/dougal/software/-/issues/23/designs/20200908_101516.jpg#note_408968681
This commit is contained in:
@@ -47,6 +47,7 @@ export default {
|
||||
{ href: "sequences", text: "Sequences" },
|
||||
{ href: "calendar", text: "Calendar" },
|
||||
{ href: "log", text: "Log" },
|
||||
{ href: "qc", text: "QC" },
|
||||
{ href: "map", text: "Map" }
|
||||
],
|
||||
path: []
|
||||
|
||||
@@ -10,6 +10,7 @@ import SequenceList from '../views/SequenceList.vue'
|
||||
import SequenceSummary from '../views/SequenceSummary.vue'
|
||||
import Calendar from '../views/Calendar.vue'
|
||||
import Log from '../views/Log.vue'
|
||||
import QC from '../views/QC.vue'
|
||||
import Map from '../views/Map.vue'
|
||||
|
||||
|
||||
@@ -103,6 +104,10 @@ Vue.use(VueRouter)
|
||||
{ path: "date/:date0/:date1", name: "logByDates" }
|
||||
]
|
||||
},
|
||||
{
|
||||
path: "qc",
|
||||
component: QC
|
||||
},
|
||||
{
|
||||
path: "map",
|
||||
component: Map
|
||||
|
||||
309
lib/www/client/source/src/views/QC.vue
Normal file
309
lib/www/client/source/src/views/QC.vue
Normal file
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>Project quality control</v-toolbar-title>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<span title="Check this to select multiple sequences">
|
||||
<v-checkbox
|
||||
v-model="multiple"
|
||||
label="Multiple"
|
||||
></v-checkbox>
|
||||
</span>
|
||||
<v-select
|
||||
v-model="selectedSequences"
|
||||
:items="multiple ? sequences : [ {text: 'All', value: null}, ...sequences ]"
|
||||
style="max-width:250px;"
|
||||
class="mx-2"
|
||||
placeholder="Sequence"
|
||||
:multiple="multiple"
|
||||
>
|
||||
<template v-slot:selection="{item, index}">
|
||||
<v-chip small v-if="index < 3">
|
||||
{{item.text || item}}
|
||||
</v-chip>
|
||||
<span v-if="index == 3" class="grey--text caption">(+ {{selectedSequences.length-3}} {{selectedSequences.length == 4 ? 'other' : 'others'}})</span>
|
||||
</template>
|
||||
</v-select>
|
||||
<v-text-field
|
||||
class="mx-2"
|
||||
v-model="search"
|
||||
append-icon="mdi-magnify"
|
||||
label="Filter"
|
||||
single-line
|
||||
clearable
|
||||
></v-text-field>
|
||||
<v-checkbox
|
||||
v-model="autoexpand"
|
||||
label="Auto expand"
|
||||
title="Automatically expand all items when searching"
|
||||
></v-checkbox>
|
||||
</v-toolbar>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
|
||||
<v-treeview
|
||||
:items="filteredItems"
|
||||
:open.sync="open"
|
||||
item-key="serial"
|
||||
:open-on-click="true"
|
||||
>
|
||||
|
||||
<template v-slot:label="{item}">
|
||||
<div @dblclick.stop.prevent="toggleChildren(item)">
|
||||
{{item.name}}
|
||||
<v-chip v-if="item.children && itemCount(item)"
|
||||
small
|
||||
color="warning"
|
||||
v-text="itemCount(item)"
|
||||
>
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</v-treeview>
|
||||
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: "QC",
|
||||
|
||||
data () {
|
||||
return {
|
||||
items: [],
|
||||
open: [],
|
||||
options: {},
|
||||
search: null,
|
||||
selectedSequences: null,
|
||||
multiple: false,
|
||||
autoexpand: false,
|
||||
itemIndex: 0
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
filteredItems () {
|
||||
return this.items.map( item =>
|
||||
this.filterByText(
|
||||
this.filterBySequence(item, this.multiple? this.selectedSequences : this.selectedSequences && [this.selectedSequences]),
|
||||
this.search
|
||||
)
|
||||
).filter(i => !!i);
|
||||
//return this.items.map( item => this.filter(item, this.search, this.selectedSequences) ).filter(i => !!i);
|
||||
},
|
||||
|
||||
sequences () {
|
||||
function getSeq (item) {
|
||||
return "_id" in item
|
||||
? Array.isArray(item._id)
|
||||
? Number(item._id[0])
|
||||
: Number(item._id)
|
||||
: "children" in item
|
||||
? item.children.map(i => getSeq(i)).flat()
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const s = new Set();
|
||||
this.items.map(i => getSeq(i)).flat().filter(i => !!i).forEach(i => s.add(i));
|
||||
return [...s].sort((a,b) => b-a); // Descending order
|
||||
},
|
||||
|
||||
...mapGetters(['loading'])
|
||||
},
|
||||
|
||||
watch: {
|
||||
filteredItems (v) {
|
||||
if (this.autoexpand) {
|
||||
v.forEach( item => this.toggleChildren(item, true) );
|
||||
}
|
||||
},
|
||||
|
||||
multiple (value) {
|
||||
if (value && !Array.isArray(this.selectedSequences)) {
|
||||
this.selectedSequences = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
itemCount (item, count = 0) {
|
||||
let sum = count;
|
||||
if (item.children) {
|
||||
sum += item.children.map(child => this.itemCount(child)).reduce( (a, b) => a+b, 0 )
|
||||
} else {
|
||||
sum++;
|
||||
}
|
||||
return sum;
|
||||
},
|
||||
|
||||
filterByText(item, queryText) {
|
||||
if (!queryText || !item) return item;
|
||||
|
||||
if (item.children) {
|
||||
const newItem = Object.assign({}, item);
|
||||
newItem.children = item.children.map( child => this.filterByText(child, queryText) ).filter(i => !!i)
|
||||
if (newItem.children.length > 0) {
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.name && item.name.toLowerCase().indexOf(queryText.toLowerCase()) > -1) {
|
||||
return item;
|
||||
}
|
||||
},
|
||||
|
||||
filterBySequence(item, sequences) {
|
||||
if (!sequences || !sequences.length) return item;
|
||||
|
||||
if (item._id) {
|
||||
if ( (item._id.length > 1 && sequences.includes(item._id[0])) || sequences.includes(item) ) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
const newItem = Object.assign({}, item);
|
||||
newItem.children = item.children.map( child => this.filterBySequence(child, sequences) ).filter(i => !!i);
|
||||
if (newItem.children.length > 0) {
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
filterXX (item, queryText, sequences) {
|
||||
if (!queryText && !sequences) return item;
|
||||
|
||||
const number = /\d+/.test(queryText)
|
||||
? Number(queryText)
|
||||
: null;
|
||||
|
||||
const newItem = Object.assign({}, item);
|
||||
if (item.children) {
|
||||
newItem.children = item.children.map( child => this.filter(child, queryText, sequences) ).filter(i => !!i)
|
||||
if (newItem.children.length) {
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
|
||||
if (sequences) {
|
||||
if (!Array.isArray(sequences)) {
|
||||
sequences = [sequences];
|
||||
}
|
||||
|
||||
if ("_id" in item) {
|
||||
if ( (item._id.length > 1 && sequences.includes(item._id[0])) || (sequences.includes(item._id)) ) {
|
||||
if (queryText) {
|
||||
return (item.name && item.name.toLowerCase().indexOf(queryText.toLowerCase()) > -1)
|
||||
? newItem
|
||||
: null;
|
||||
}
|
||||
return newItem;
|
||||
}
|
||||
}
|
||||
} else if (item.name && item.name.toLowerCase().indexOf(queryText.toLowerCase()) > -1) {
|
||||
return newItem;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
toggleChildren (item, state) {
|
||||
const open = typeof state == 'undefined'
|
||||
? !this.open.includes(item.serial)
|
||||
: state;
|
||||
|
||||
if (item.children) {
|
||||
item.children.forEach(child => this.toggleChildren(child, open));
|
||||
}
|
||||
|
||||
if (open) {
|
||||
if (!this.open.includes(item.serial)) {
|
||||
this.open.push(item.serial);
|
||||
}
|
||||
} else {
|
||||
const index = this.open.indexOf(item.serial);
|
||||
if (index > -1) {
|
||||
this.open.splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
transform (item) {
|
||||
item.serial = ++this.itemIndex;
|
||||
if (item.check) {
|
||||
switch (item.iterate) {
|
||||
case "sequences":
|
||||
item.check = item.check.map(check => ({
|
||||
_id: check._id,
|
||||
name: `Sequence ${check._id}: ${check.results}`
|
||||
}));
|
||||
break;
|
||||
case "shots":
|
||||
default:
|
||||
const bySequence = {};
|
||||
for (const check of item.check) {
|
||||
if (!bySequence[check._id[0]]) {
|
||||
bySequence[check._id[0]] = [];
|
||||
}
|
||||
bySequence[check._id[0]].push({
|
||||
_id: check._id,
|
||||
name: `Point ${check._id[1]}: ${check.results}`
|
||||
});
|
||||
}
|
||||
item.check = Object.keys(bySequence).map(seq => ({
|
||||
_id: seq,
|
||||
name: `Sequence: ${seq}`,
|
||||
children: bySequence[seq]
|
||||
}));
|
||||
}
|
||||
if (!("children" in item)) {
|
||||
item.children = item.check;
|
||||
delete item.check;
|
||||
}
|
||||
}
|
||||
if (item.children) {
|
||||
for (const child of item.children) {
|
||||
this.transform(child);
|
||||
}
|
||||
if (item.check) {
|
||||
item.children = item.check.concatenate(item.children);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
},
|
||||
|
||||
async getQCData () {
|
||||
|
||||
const url = `/project/${this.$route.params.project}/info/qc`;
|
||||
|
||||
const res = await this.api([url]);
|
||||
|
||||
if (res) {
|
||||
this.items = res.results.map(i => this.transform(i)) || [];
|
||||
this.updatedOn = res.updatedOn;
|
||||
} else {
|
||||
this.items = [];
|
||||
this.updatedOn = null;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
...mapActions(["api"])
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.getQCData();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user