mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:27:09 +00:00
Compare commits
15 Commits
v2024.19.1
...
226-allow-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9a228ca32 | ||
|
|
3e33f10ea5 | ||
|
|
54fe18035f | ||
|
|
1ade981902 | ||
|
|
013a52ff55 | ||
|
|
a74fd56f15 | ||
|
|
a369f2dd7b | ||
|
|
83c3f9b401 | ||
|
|
798178af55 | ||
|
|
320f67fd06 | ||
|
|
9f2e25278b | ||
|
|
d2f8444042 | ||
|
|
28beef81de | ||
|
|
9a2fdeab0e | ||
|
|
9a3cf7997e |
@@ -35,7 +35,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
import DougalNavigation from './components/navigation';
|
import DougalNavigation from './components/navigation';
|
||||||
import DougalFooter from './components/footer';
|
import DougalFooter from './components/footer';
|
||||||
|
|
||||||
@@ -53,7 +53,8 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
snackText () { return this.$store.state.snack.snackText },
|
snackText () { return this.$store.state.snack.snackText },
|
||||||
snackColour () { return this.$store.state.snack.snackColour }
|
snackColour () { return this.$store.state.snack.snackColour },
|
||||||
|
...mapGetters(["serverEvent"])
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
@@ -75,17 +76,25 @@ export default {
|
|||||||
if (!newVal) {
|
if (!newVal) {
|
||||||
this.$store.commit('setSnackText', "");
|
this.$store.commit('setSnackText', "");
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async serverEvent (event) {
|
||||||
|
if (event.channel == "project" && event.payload?.schema == "public") {
|
||||||
|
// Projects changed in some way or another
|
||||||
|
await this.refreshProjects();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(["setCredentials"])
|
...mapActions(["setCredentials", "refreshProjects"])
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted () {
|
async mounted () {
|
||||||
// Local Storage values are always strings
|
// Local Storage values are always strings
|
||||||
this.$vuetify.theme.dark = localStorage.getItem("darkTheme") == "true";
|
this.$vuetify.theme.dark = localStorage.getItem("darkTheme") == "true";
|
||||||
this.setCredentials()
|
await this.setCredentials();
|
||||||
|
this.refreshProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<v-tabs :value="tab" show-arrows>
|
||||||
|
<v-tab v-for="tab, index in tabs" :key="index" link :to="tabLink(tab.href)" v-text="tab.text"></v-tab>
|
||||||
|
</v-tabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DougalAppBarExtensionProject',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tabs: [
|
||||||
|
{ href: "summary", text: "Summary" },
|
||||||
|
{ href: "lines", text: "Lines" },
|
||||||
|
{ href: "plan", text: "Plan" },
|
||||||
|
{ href: "sequences", text: "Sequences" },
|
||||||
|
{ href: "calendar", text: "Calendar" },
|
||||||
|
{ href: "log", text: "Log" },
|
||||||
|
{ href: "qc", text: "QC" },
|
||||||
|
{ href: "graphs", text: "Graphs" },
|
||||||
|
{ href: "map", text: "Map" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
page () {
|
||||||
|
return this.$route.path.split(/\/+/)[3];
|
||||||
|
},
|
||||||
|
|
||||||
|
tab () {
|
||||||
|
return this.tabs.findIndex(t => t.href == this.page);
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
tabLink (href) {
|
||||||
|
return `/projects/${this.$route.params.project}/${href}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -123,29 +123,11 @@
|
|||||||
|
|
||||||
<v-row dense>
|
<v-row dense>
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-combobox
|
<dougal-event-select
|
||||||
ref="remarks"
|
v-bind.sync="entryRemarks"
|
||||||
v-model="entryRemarks"
|
:preset-remarks="presetRemarks"
|
||||||
:disabled="loading"
|
@update:labels="(v) => this.entryLabels = v"
|
||||||
:search-input.sync="entryRemarksInput"
|
></dougal-event-select>
|
||||||
:items="remarksAvailable"
|
|
||||||
:filter="searchRemarks"
|
|
||||||
item-text="text"
|
|
||||||
return-object
|
|
||||||
label="Remarks"
|
|
||||||
hint="Placeholders: @DMS@, @DEG@, @EN@, @WD@, @BSP@, @CMG@, …"
|
|
||||||
prepend-icon="mdi-text-box-outline"
|
|
||||||
append-outer-icon="mdi-magnify"
|
|
||||||
@click:append-outer="(e) => remarksMenu = e"
|
|
||||||
></v-combobox>
|
|
||||||
|
|
||||||
<dougal-context-menu
|
|
||||||
:value="remarksMenu"
|
|
||||||
@input="handleRemarksMenu"
|
|
||||||
:items="presetRemarks"
|
|
||||||
absolute
|
|
||||||
></dougal-context-menu>
|
|
||||||
|
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
@@ -290,6 +272,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import DougalContextMenu from '@/components/context-menu';
|
import DougalContextMenu from '@/components/context-menu';
|
||||||
|
import DougalEventSelect from '@/components/event-select';
|
||||||
|
|
||||||
function stringSort (a, b) {
|
function stringSort (a, b) {
|
||||||
return a == b
|
return a == b
|
||||||
@@ -308,6 +291,7 @@ function flattenRemarks(items, keywords=[], labels=[]) {
|
|||||||
if (!item.items) {
|
if (!item.items) {
|
||||||
result.push({
|
result.push({
|
||||||
text: item.text,
|
text: item.text,
|
||||||
|
properties: item.properties,
|
||||||
labels: labels.concat(item.labels??[]),
|
labels: labels.concat(item.labels??[]),
|
||||||
keywords
|
keywords
|
||||||
})
|
})
|
||||||
@@ -342,7 +326,8 @@ export default {
|
|||||||
name: 'DougalEventEdit',
|
name: 'DougalEventEdit',
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
DougalContextMenu
|
DougalContextMenu,
|
||||||
|
DougalEventSelect
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -354,6 +339,7 @@ export default {
|
|||||||
sequence: { type: Number },
|
sequence: { type: Number },
|
||||||
point: { type: Number },
|
point: { type: Number },
|
||||||
remarks: { type: String },
|
remarks: { type: String },
|
||||||
|
meta: { type: Object },
|
||||||
labels: { type: Array, default: () => [] },
|
labels: { type: Array, default: () => [] },
|
||||||
latitude: { type: Number },
|
latitude: { type: Number },
|
||||||
longitude: { type: Number },
|
longitude: { type: Number },
|
||||||
@@ -371,18 +357,11 @@ export default {
|
|||||||
entrySequence: null,
|
entrySequence: null,
|
||||||
entryPoint: null,
|
entryPoint: null,
|
||||||
entryRemarks: null,
|
entryRemarks: null,
|
||||||
entryRemarksInput: null,
|
|
||||||
entryLatitude: null,
|
entryLatitude: null,
|
||||||
entryLongitude: null
|
entryLongitude: null
|
||||||
}),
|
}),
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
remarksAvailable () {
|
|
||||||
return this.entryRemarksInput == this.entryRemarks?.text ||
|
|
||||||
this.entryRemarksInput == this.entryRemarks
|
|
||||||
? []
|
|
||||||
: flattenRemarks(this.presetRemarks);
|
|
||||||
},
|
|
||||||
|
|
||||||
allSelected () {
|
allSelected () {
|
||||||
return this.entryLabels.length === this.items.length
|
return this.entryLabels.length === this.items.length
|
||||||
@@ -394,11 +373,6 @@ export default {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// The user is editing the remarks
|
|
||||||
if (this.entryRemarksText != this.entryRemarksInput) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected label set distinct from input labels
|
// Selected label set distinct from input labels
|
||||||
if (distinctSets(this.selectedLabels, this.entryLabels, (i) => i.text)) {
|
if (distinctSets(this.selectedLabels, this.entryLabels, (i) => i.text)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -502,11 +476,8 @@ export default {
|
|||||||
|
|
||||||
this.entrySequence = this.sequence;
|
this.entrySequence = this.sequence;
|
||||||
this.entryPoint = this.point;
|
this.entryPoint = this.point;
|
||||||
this.entryRemarks = this.remarks;
|
|
||||||
this.entryLabels = [...(this.labels??[])];
|
this.entryLabels = [...(this.labels??[])];
|
||||||
|
this.makeEntryRemarks();
|
||||||
// Focus remarks field
|
|
||||||
this.$nextTick(() => this.$refs.remarks.focus());
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -577,22 +548,13 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
searchRemarks (item, queryText, itemText) {
|
makeEntryRemarks () {
|
||||||
const needle = queryText.toLowerCase();
|
this.entryRemarks = {
|
||||||
const text = item.text.toLowerCase();
|
template: null,
|
||||||
const keywords = item.keywords.map(i => i.toLowerCase());
|
schema: {},
|
||||||
const labels = item.labels.map(i => i.toLowerCase());
|
values: [],
|
||||||
return text.includes(needle) ||
|
...this.meta?.structured_values,
|
||||||
keywords.some(i => i.includes(needle)) ||
|
text: this.remarks
|
||||||
labels.some(i => i.includes(needle));
|
|
||||||
},
|
|
||||||
|
|
||||||
handleRemarksMenu (event) {
|
|
||||||
if (typeof event == 'boolean') {
|
|
||||||
this.remarksMenu = event;
|
|
||||||
} else {
|
|
||||||
this.entryRemarks = event;
|
|
||||||
this.remarksMenu = false;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -657,14 +619,24 @@ export default {
|
|||||||
save () {
|
save () {
|
||||||
// In case the focus goes directly from the remarks field
|
// In case the focus goes directly from the remarks field
|
||||||
// to the Save button.
|
// to the Save button.
|
||||||
if (this.entryRemarksInput != this.entryRemarksText) {
|
|
||||||
this.entryRemarks = this.entryRemarksInput;
|
let meta;
|
||||||
|
|
||||||
|
if (this.entryRemarks.values?.length) {
|
||||||
|
meta = {
|
||||||
|
structured_values: {
|
||||||
|
template: this.entryRemarks.template,
|
||||||
|
schema: this.entryRemarks.schema,
|
||||||
|
values: this.entryRemarks.values
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
remarks: this.entryRemarksText,
|
remarks: this.entryRemarksText,
|
||||||
labels: this.entryLabels
|
labels: this.entryLabels,
|
||||||
|
meta
|
||||||
};
|
};
|
||||||
|
|
||||||
/* NOTE This is the purist way.
|
/* NOTE This is the purist way.
|
||||||
|
|||||||
142
lib/www/client/source/src/components/event-properties.vue
Normal file
142
lib/www/client/source/src/components/event-properties.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<v-card flat>
|
||||||
|
<v-card-subtitle v-text="text">
|
||||||
|
</v-card-subtitle>
|
||||||
|
<v-card-text style="max-height:350px;overflow:scroll;">
|
||||||
|
<v-form>
|
||||||
|
<template v-for="key in fieldKeys">
|
||||||
|
<template v-if="schema[key].enum">
|
||||||
|
<v-select v-if="schema[key].type == 'number'" :key="key"
|
||||||
|
v-model.number="fieldValues[key]"
|
||||||
|
:items="schema[key].enum"
|
||||||
|
:label="schema[key].title"
|
||||||
|
:hint="schema[key].description"
|
||||||
|
@input="updateFieldValue(key, Number($event))"
|
||||||
|
></v-select>
|
||||||
|
<v-select v-else :key="key"
|
||||||
|
v-model="fieldValues[key]"
|
||||||
|
:items="schema[key].enum"
|
||||||
|
:label="schema[key].title"
|
||||||
|
:hint="schema[key].description"
|
||||||
|
@input="updateFieldValue(key, $event)"
|
||||||
|
></v-select>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<v-text-field v-if="schema[key].type == 'number'" :key="key"
|
||||||
|
v-model.number="fieldValues[key]"
|
||||||
|
type="number"
|
||||||
|
:min="schema[key].minimum"
|
||||||
|
:max="schema[key].maximum"
|
||||||
|
:step="schema[key].multiplier"
|
||||||
|
:label="schema[key].title"
|
||||||
|
:hint="schema[key].description"
|
||||||
|
@input="updateFieldValue(key, Number($event))"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field v-else-if="schema[key].type == 'string'" :key="key"
|
||||||
|
v-model="fieldValues[key]"
|
||||||
|
:label="schema[key].title"
|
||||||
|
:hint="schema[key].description"
|
||||||
|
@input="updateFieldValue(key, $event)"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-checkbox v-else-if="schema[key].type == 'boolean'" :key="key"
|
||||||
|
v-model="fieldValues[key]"
|
||||||
|
:label="schema[key].title"
|
||||||
|
:hint="schema[key].description"
|
||||||
|
@change="updateFieldValue(key, $event)"
|
||||||
|
>
|
||||||
|
</v-checkbox>
|
||||||
|
<v-text-field v-else :key="key"
|
||||||
|
v-model="fieldValues[key]"
|
||||||
|
:label="schema[key].title"
|
||||||
|
:hint="schema[key].description"
|
||||||
|
@input="updateFieldValue(key, $event)"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DougalEventPropertiesEdit",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
value: String,
|
||||||
|
template: String,
|
||||||
|
schema: Object,
|
||||||
|
values: Array
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
fieldKeys () {
|
||||||
|
return Object.entries(this.schema).sort((a, b) => a[1].title > b[1].title ? 1 : -1).map(i => i[0]);
|
||||||
|
},
|
||||||
|
|
||||||
|
fieldValues () {
|
||||||
|
const keys = Object.keys(this.schema ?? this.values);
|
||||||
|
return Object.fromEntries(
|
||||||
|
keys.map( (k, idx) =>
|
||||||
|
[ k, this.values?.[idx] ?? this.schema[k].default ]));
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
fields () {
|
||||||
|
// TODO Remove this and rename fields → schema
|
||||||
|
return this.schema;
|
||||||
|
},
|
||||||
|
*/
|
||||||
|
|
||||||
|
text () {
|
||||||
|
if (this.template) {
|
||||||
|
const rx = /{{([a-z_][a-z0-9_]*)}}/ig;
|
||||||
|
return this.template.replace(rx, (match, p1) => this.fieldValues[p1] ?? "(n/a)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
|
||||||
|
values () {
|
||||||
|
this.$emit("input", this.text);
|
||||||
|
},
|
||||||
|
|
||||||
|
template () {
|
||||||
|
this.$emit("input", this.text);
|
||||||
|
},
|
||||||
|
|
||||||
|
schema () {
|
||||||
|
this.$emit("input", this.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateFieldValue(key, ev) {
|
||||||
|
const values = {...this.fieldValues};
|
||||||
|
values[key] = ev;
|
||||||
|
this.$emit("update:values", Object.values(values));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mount () {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
163
lib/www/client/source/src/components/event-select.vue
Normal file
163
lib/www/client/source/src/components/event-select.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-combobox
|
||||||
|
ref="remarks"
|
||||||
|
:value="text"
|
||||||
|
@input="handleComboBox"
|
||||||
|
:search-input.sync="entryRemarksInput"
|
||||||
|
:items="remarksAvailable"
|
||||||
|
:filter="searchRemarks"
|
||||||
|
item-text="text"
|
||||||
|
return-object
|
||||||
|
label="Remarks"
|
||||||
|
hint="Placeholders: @DMS@, @DEG@, @EN@, @WD@, @BSP@, @CMG@, …"
|
||||||
|
prepend-icon="mdi-text-box-outline"
|
||||||
|
append-outer-icon="mdi-magnify"
|
||||||
|
@click:append-outer="(e) => remarksMenu = e"
|
||||||
|
></v-combobox>
|
||||||
|
|
||||||
|
<dougal-context-menu
|
||||||
|
:value="remarksMenu"
|
||||||
|
@input="handleRemarksMenu"
|
||||||
|
:items="presetRemarks"
|
||||||
|
absolute
|
||||||
|
></dougal-context-menu>
|
||||||
|
|
||||||
|
<v-expansion-panels v-if="haveProperties"
|
||||||
|
class="px-8"
|
||||||
|
:value="0"
|
||||||
|
>
|
||||||
|
<v-expansion-panel>
|
||||||
|
<v-expansion-panel-header>Properties</v-expansion-panel-header>
|
||||||
|
<v-expansion-panel-content>
|
||||||
|
<dougal-event-properties-edit
|
||||||
|
:value="text"
|
||||||
|
@input="$emit('update:text', $event)"
|
||||||
|
:template="template"
|
||||||
|
:schema="schema"
|
||||||
|
:values="values"
|
||||||
|
@update:values="$emit('update:values', $event)"
|
||||||
|
>
|
||||||
|
</dougal-event-properties-edit>
|
||||||
|
</v-expansion-panel-content>
|
||||||
|
</v-expansion-panel>
|
||||||
|
</v-expansion-panels>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapGetters } from 'vuex';
|
||||||
|
import DougalContextMenu from '@/components/context-menu';
|
||||||
|
import DougalEventPropertiesEdit from '@/components/event-properties';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DougalEventSelect",
|
||||||
|
|
||||||
|
components: {
|
||||||
|
DougalContextMenu,
|
||||||
|
DougalEventPropertiesEdit
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
text: String,
|
||||||
|
template: String,
|
||||||
|
schema: Object,
|
||||||
|
values: Array,
|
||||||
|
presetRemarks: Array
|
||||||
|
},
|
||||||
|
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
entryRemarksInput: null,
|
||||||
|
remarksMenu: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
remarksAvailable () {
|
||||||
|
return this.entryRemarksInput == this.text
|
||||||
|
? []
|
||||||
|
: this.flattenRemarks(this.presetRemarks);
|
||||||
|
},
|
||||||
|
|
||||||
|
haveProperties () {
|
||||||
|
for (const key in this.schema) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
|
||||||
|
flattenRemarks (items, keywords=[], labels=[]) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
if (items) {
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.items) {
|
||||||
|
result.push({
|
||||||
|
text: item.text,
|
||||||
|
properties: item.properties,
|
||||||
|
labels: labels.concat(item.labels??[]),
|
||||||
|
keywords
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const k = [...keywords, item.text];
|
||||||
|
const l = [...labels, ...(item.labels??[])];
|
||||||
|
result.push(...this.flattenRemarks(item.items, k, l))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
searchRemarks (item, queryText, itemText) {
|
||||||
|
const needle = queryText.toLowerCase();
|
||||||
|
const text = item.text.toLowerCase();
|
||||||
|
const keywords = item.keywords.map(i => i.toLowerCase());
|
||||||
|
const labels = item.labels.map(i => i.toLowerCase());
|
||||||
|
return text.includes(needle) ||
|
||||||
|
keywords.some(i => i.includes(needle)) ||
|
||||||
|
labels.some(i => i.includes(needle));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleComboBox (event) {
|
||||||
|
if (typeof event == "object") {
|
||||||
|
this.$emit("update:text", event.text);
|
||||||
|
this.$emit("update:template", event.template ?? event.text);
|
||||||
|
this.$emit("update:schema", event.properties);
|
||||||
|
this.$emit("update:labels", event.labels);
|
||||||
|
} else {
|
||||||
|
this.$emit("update:text", event);
|
||||||
|
this.$emit("update:template", null);
|
||||||
|
this.$emit("update:properties", null);
|
||||||
|
this.$emit("update:labels", []);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleRemarksMenu (event) {
|
||||||
|
if (typeof event == 'boolean') {
|
||||||
|
this.remarksMenu = event;
|
||||||
|
} else {
|
||||||
|
this.$emit("update:text", event.text);
|
||||||
|
this.$emit("update:template", event.template ?? event.text);
|
||||||
|
this.$emit("update:schema", event.properties);
|
||||||
|
this.$emit("update:labels", event.labels);
|
||||||
|
this.remarksMenu = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mount () {
|
||||||
|
// Focus remarks field
|
||||||
|
this.$nextTick(() => this.$refs.remarks.focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
@@ -71,17 +71,10 @@
|
|||||||
|
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
|
||||||
<!--
|
|
||||||
<v-btn small text class="ml-2" title="Log out" link to="/?logout=1">
|
|
||||||
<v-icon small>mdi-logout</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
-->
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:extension v-if="$route.matched.find(i => i.name == 'Project')">
|
<template v-slot:extension v-if="appBarExtension">
|
||||||
<v-tabs :value="tab" show-arrows align-with-title>
|
<div :is="appBarExtension"></div>
|
||||||
<v-tab v-for="tab, index in tabs" :key="index" link :to="tabLink(tab.href)" v-text="tab.text"></v-tab>
|
|
||||||
</v-tabs>
|
|
||||||
</template>
|
</template>
|
||||||
</v-app-bar>
|
</v-app-bar>
|
||||||
|
|
||||||
@@ -95,24 +88,17 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
drawer: false,
|
drawer: false,
|
||||||
tabs: [
|
|
||||||
{ href: "summary", text: "Summary" },
|
|
||||||
{ href: "lines", text: "Lines" },
|
|
||||||
{ href: "plan", text: "Plan" },
|
|
||||||
{ href: "sequences", text: "Sequences" },
|
|
||||||
{ href: "calendar", text: "Calendar" },
|
|
||||||
{ href: "log", text: "Log" },
|
|
||||||
{ href: "qc", text: "QC" },
|
|
||||||
{ href: "graphs", text: "Graphs" },
|
|
||||||
{ href: "map", text: "Map" }
|
|
||||||
],
|
|
||||||
path: []
|
path: []
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
tab () {
|
|
||||||
return this.tabs.findIndex(t => t.href == this.$route.path.split(/\/+/)[3]);
|
appBarExtension () {
|
||||||
|
return this.$route.matched
|
||||||
|
.filter(i => i.meta?.appBarExtension)
|
||||||
|
.map(i => i.meta.appBarExtension)
|
||||||
|
.pop()?.component;
|
||||||
},
|
},
|
||||||
|
|
||||||
...mapGetters(['user', 'loading'])
|
...mapGetters(['user', 'loading'])
|
||||||
@@ -131,9 +117,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
tabLink (href) {
|
|
||||||
return `/projects/${this.$route.params.project}/${href}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
breadcrumbs () {
|
breadcrumbs () {
|
||||||
this.path = this.$route.matched
|
this.path = this.$route.matched
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import Log from '../views/Log.vue'
|
|||||||
import QC from '../views/QC.vue'
|
import QC from '../views/QC.vue'
|
||||||
import Graphs from '../views/Graphs.vue'
|
import Graphs from '../views/Graphs.vue'
|
||||||
import Map from '../views/Map.vue'
|
import Map from '../views/Map.vue'
|
||||||
|
import DougalAppBarExtensionProject from '../components/app-bar-extension-project'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
@@ -100,7 +100,10 @@ Vue.use(VueRouter)
|
|||||||
text: (ctx) => ctx.$store.state.project.projectName || "…",
|
text: (ctx) => ctx.$store.state.project.projectName || "…",
|
||||||
href: (ctx) => `/projects/${ctx.$store.state.project.projectId || ctx.$route.params.project || ""}/`
|
href: (ctx) => `/projects/${ctx.$store.state.project.projectId || ctx.$route.params.project || ""}/`
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
appBarExtension: {
|
||||||
|
component: DougalAppBarExtensionProject
|
||||||
|
}
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,7 +37,65 @@
|
|||||||
</v-btn>
|
</v-btn>
|
||||||
<v-toolbar-title v-if="$refs.calendar">
|
<v-toolbar-title v-if="$refs.calendar">
|
||||||
{{ $refs.calendar.title }}
|
{{ $refs.calendar.title }}
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="secondary--text"
|
||||||
|
style="font-size:small;"
|
||||||
|
>
|
||||||
|
({{downloadableItemCount}} log entries)
|
||||||
|
</span>
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-menu class="ml-5" v-if="calendarDates">
|
||||||
|
<template v-slot:activator="{on, attrs}">
|
||||||
|
<v-btn
|
||||||
|
class="ml-5"
|
||||||
|
small
|
||||||
|
:title="`Download events for the period ${calendarDates.start} ‒ ${calendarDates.end}`"
|
||||||
|
v-on="on"
|
||||||
|
v-bind="attrs"
|
||||||
|
>
|
||||||
|
<span class="d-none d-lg-inline">Download as…</span>
|
||||||
|
<v-icon right small>mdi-cloud-download</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
:href="downloadUrl({mime: 'application/vnd.seis+json', filename: `event-log-${calendarDates.start}-${calendarDates.end}-multiseis.json`})"
|
||||||
|
title="Download as a Multiseis-compatible Seis+JSON file."
|
||||||
|
>Seis+JSON</v-list-item>
|
||||||
|
<!-- Not yet supported
|
||||||
|
<v-list-item
|
||||||
|
:href="downloadUrl({mime: 'application/geo+json'})"
|
||||||
|
title="Download as a QGIS-compatible GeoJSON file"
|
||||||
|
>GeoJSON</v-list-item>
|
||||||
|
-->
|
||||||
|
<v-list-item
|
||||||
|
:href="downloadUrl({mime: 'application/json', filename: `event-log-${calendarDates.start}-${calendarDates.end}.json`})"
|
||||||
|
title="Download as a generic JSON file"
|
||||||
|
>JSON</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
:href="downloadUrl({mime: 'application/yaml', filename: `event-log-${calendarDates.start}-${calendarDates.end}.yaml`})"
|
||||||
|
title="Download as a YAML file"
|
||||||
|
>YAML</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
:href="downloadUrl({mime: 'text/csv', sortBy: 'tstamp', sortDesc: false, filename: `event-log-${calendarDates.start}-${calendarDates.end}.csv`})"
|
||||||
|
title="Download as Comma Separated Values file"
|
||||||
|
>CSV</v-list-item>
|
||||||
|
<!-- Not yet supportd
|
||||||
|
<v-list-item
|
||||||
|
:href="downloadUrl({mime: 'text/html'})"
|
||||||
|
title="Download as an HTML formatted file"
|
||||||
|
>HTML</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
:href="downloadUrl({mime: 'application/pdf'})"
|
||||||
|
title="Download as a Portable Document File"
|
||||||
|
>PDF</v-list-item>
|
||||||
|
-->
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn v-if="categoriesAvailable"
|
<v-btn v-if="categoriesAvailable"
|
||||||
small
|
small
|
||||||
@@ -169,6 +227,23 @@ export default {
|
|||||||
return [...new Set(this.visibleItems.map(i => i.category ?? "General"))];
|
return [...new Set(this.visibleItems.map(i => i.category ?? "General"))];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
calendarDates () {
|
||||||
|
// The this.items.length reference is only needed to force recalculation
|
||||||
|
// of this computed property, as this.$refs is not reactive.
|
||||||
|
// https://github.com/vuejs/vue/issues/3842
|
||||||
|
if (this.items.length && this.$refs.calendar) {
|
||||||
|
return {
|
||||||
|
start: this.$refs.calendar.renderProps.start.date,
|
||||||
|
end: this.$refs.calendar.renderProps.end.date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
downloadableItemCount () {
|
||||||
|
return this.events.filter(i => i.tstamp.substr(0, 10) >= this.calendarDates?.start &&
|
||||||
|
i.tstamp.substr(0, 10) <= this.calendarDates?.end).length;
|
||||||
|
},
|
||||||
|
|
||||||
...mapGetters(['sequencesLoading', 'sequences', 'events'])
|
...mapGetters(['sequencesLoading', 'sequences', 'events'])
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -298,6 +373,18 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
downloadUrl (qry) {
|
||||||
|
if (this.calendarDates) {
|
||||||
|
const url = new URL(`/api/project/${this.$route.params.project}/event`, document.location.href);
|
||||||
|
for (const key in qry) {
|
||||||
|
url.searchParams.set(key, qry[key]);
|
||||||
|
}
|
||||||
|
url.searchParams.set("date0", this.calendarDates.start);
|
||||||
|
url.searchParams.set("date1", this.calendarDates.end);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
...mapActions(["api"])
|
...mapActions(["api"])
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,49 @@
|
|||||||
>PDF</v-list-item>
|
>PDF</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
<v-menu v-else>
|
||||||
|
<template v-slot:activator="{on, attrs}">
|
||||||
|
<v-btn class="ml-5" small v-on="on" v-bind="attrs">
|
||||||
|
<span class="d-none d-lg-inline">Download as…</span>
|
||||||
|
<v-icon right small>mdi-cloud-download</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<v-list>
|
||||||
|
<v-list-item
|
||||||
|
:href="`/api/project/${$route.params.project}/event?mime=application%2Fvnd.seis%2Bjson&filename=event-log-multiseis.json`"
|
||||||
|
title="Download as a Multiseis-compatible Seis+JSON file."
|
||||||
|
>Seis+JSON</v-list-item>
|
||||||
|
<!-- Not yet implemented
|
||||||
|
<v-list-item
|
||||||
|
:href="`/api/project/${$route.params.project}/event?mime=application%2Fgeo%2Bjson&filename=event-log.geojson`"
|
||||||
|
title="Download as a QGIS-compatible GeoJSON file"
|
||||||
|
>GeoJSON</v-list-item>
|
||||||
|
-->
|
||||||
|
<v-list-item
|
||||||
|
:href="`/api/project/${$route.params.project}/event?mime=application%2Fjson&filename=event-log.json`"
|
||||||
|
title="Download as a generic JSON file"
|
||||||
|
>JSON</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
:href="`/api/project/${$route.params.project}/event?mime=application%2Fyaml&filename=event-log.yaml`"
|
||||||
|
title="Download as a YAML file"
|
||||||
|
>YAML</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
:href="`/api/project/${$route.params.project}/event?mime=text%2Fcsv&sortBy=tstamp&sortDesc=false&filename=event-log.csv`"
|
||||||
|
title="Download as Comma Separated Values file"
|
||||||
|
>CSV</v-list-item>
|
||||||
|
<!-- Not yet implemented
|
||||||
|
<v-list-item
|
||||||
|
:href="`/api/project/${$route.params.project}/event?mime=text%2Fhtml&filename=event-log.html`"
|
||||||
|
title="Download as an HTML formatted file"
|
||||||
|
>HTML</v-list-item>
|
||||||
|
<v-list-item
|
||||||
|
:href="`/api/project/${$route.params.project}/event?mime=application%2Fpdf&filename=event-log.pdf`"
|
||||||
|
title="Download as a Portable Document File"
|
||||||
|
>PDF</v-list-item>
|
||||||
|
-->
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const yaml = require('./yaml');
|
|||||||
const geojson = require('./geojson');
|
const geojson = require('./geojson');
|
||||||
const seis = require('./seis');
|
const seis = require('./seis');
|
||||||
const csv = require('./csv');
|
const csv = require('./csv');
|
||||||
|
const setContentDisposition = require('../../../../lib/utils/setContentDisposition');
|
||||||
|
|
||||||
|
|
||||||
module.exports = async function (req, res, next) {
|
module.exports = async function (req, res, next) {
|
||||||
@@ -19,6 +20,7 @@ module.exports = async function (req, res, next) {
|
|||||||
|
|
||||||
if (mimetype) {
|
if (mimetype) {
|
||||||
res.set("Content-Type", mimetype);
|
res.set("Content-Type", mimetype);
|
||||||
|
setContentDisposition(req, res);
|
||||||
await handlers[mimetype](req, res, next);
|
await handlers[mimetype](req, res, next);
|
||||||
} else {
|
} else {
|
||||||
res.status(406).send();
|
res.status(406).send();
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ async function list (projectId, opts = {}) {
|
|||||||
WHERE
|
WHERE
|
||||||
($1::numeric IS NULL OR sequence = $1) AND
|
($1::numeric IS NULL OR sequence = $1) AND
|
||||||
($2::numeric[] IS NULL OR sequence = ANY( $2 )) AND
|
($2::numeric[] IS NULL OR sequence = ANY( $2 )) AND
|
||||||
($3::timestamptz IS NULL OR date(tstamp) = $3) AND
|
|
||||||
($3::timestamptz IS NULL OR
|
($3::timestamptz IS NULL OR
|
||||||
(($4::timestamptz IS NULL AND date(tstamp) = $3) OR
|
(($4::timestamptz IS NULL AND date(tstamp) = $3) OR
|
||||||
date(tstamp) BETWEEN SYMMETRIC $3 AND $4)) AND
|
date(tstamp) BETWEEN SYMMETRIC $3 AND $4)) AND
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ module.exports = {
|
|||||||
removeNulls: require('./removeNulls'),
|
removeNulls: require('./removeNulls'),
|
||||||
logicalPath: require('./logicalPath'),
|
logicalPath: require('./logicalPath'),
|
||||||
ranges: require('./ranges'),
|
ranges: require('./ranges'),
|
||||||
unique: require('./unique')
|
unique: require('./unique'),
|
||||||
|
setContentDisposition: require('./setContentDisposition')
|
||||||
};
|
};
|
||||||
|
|||||||
8
lib/www/server/lib/utils/setContentDisposition.js
Normal file
8
lib/www/server/lib/utils/setContentDisposition.js
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
function setContentDisposition (req, res) {
|
||||||
|
if (req.query.filename) {
|
||||||
|
res.set("Content-Disposition", `attachment; filename="${req.query.filename}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = setContentDisposition;
|
||||||
@@ -216,6 +216,26 @@ components:
|
|||||||
pattern: "(([^\\s,;:]+)(\\s*[,;:\\s]\\s*)?)+"
|
pattern: "(([^\\s,;:]+)(\\s*[,;:\\s]\\s*)?)+"
|
||||||
example: "line,point,tstamp"
|
example: "line,point,tstamp"
|
||||||
|
|
||||||
|
QueryMime:
|
||||||
|
name: mime
|
||||||
|
description: |
|
||||||
|
Ask for the representation of the requested resource to be provided in the indicated MIME type. Overrides the [`Accept`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept) request header values (if any).
|
||||||
|
|
||||||
|
Note that the server is not obliged to honour the requested type and a default (usually `application/json`) might be provided instead. Always check the response's [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) header.
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
QueryFilename:
|
||||||
|
name: filename
|
||||||
|
description: |
|
||||||
|
Cause the [`Content-Disposition`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition) response header to be set and suggest a file name to download as.
|
||||||
|
|
||||||
|
Note that the [`Content-Type`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) response header is not affected by this parameter.
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
|
||||||
CSVDelimiter:
|
CSVDelimiter:
|
||||||
description: Delimiter value to use in CSV requests or responses.
|
description: Delimiter value to use in CSV requests or responses.
|
||||||
name: delimiter
|
name: delimiter
|
||||||
@@ -1394,6 +1414,8 @@ paths:
|
|||||||
- $ref: "#/components/parameters/CSVDelimiter"
|
- $ref: "#/components/parameters/CSVDelimiter"
|
||||||
- $ref: "#/components/parameters/CSVFields"
|
- $ref: "#/components/parameters/CSVFields"
|
||||||
- $ref: "#/components/parameters/CSVHeader"
|
- $ref: "#/components/parameters/CSVHeader"
|
||||||
|
- $ref: "#/components/parameters/QueryMime"
|
||||||
|
- $ref: "#/components/parameters/QueryFilename"
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: List of project events
|
description: List of project events
|
||||||
|
|||||||
Reference in New Issue
Block a user