Merge branch 'gallery_filter' into 'v3'
Adds filtering to the gallery See merge request openflexure/openflexure-microscope-server!621
This commit is contained in:
commit
3e38722d45
10 changed files with 387 additions and 63 deletions
|
|
@ -249,6 +249,48 @@
|
|||
}
|
||||
}
|
||||
|
||||
// A button styled like text. For use in menus
|
||||
.ofm-text-button {
|
||||
display: block;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
color: @global-color;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
.hook-inverse() {
|
||||
.ofm-text-button{
|
||||
color: @inverse-global-color;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Opaque - button-like colours
|
||||
*
|
||||
* The buttons are transparent. If an element overlays others it should
|
||||
* Look the same as the buttons, but have no transparency.
|
||||
*/
|
||||
.ofm-opaque-element{
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.hook-inverse() {
|
||||
.ofm-opaque-element{
|
||||
background: #282626;
|
||||
}
|
||||
}
|
||||
|
||||
.ofm-opaque-highlight:hover {
|
||||
background: #fff;
|
||||
}
|
||||
.hook-inverse() {
|
||||
.ofm-opaque-highlight:hover {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Accordion
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -267,6 +267,7 @@ export default {
|
|||
|
||||
#container-left {
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable;
|
||||
background-color: rgba(180, 180, 180, 0.025);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
|||
150
webapp/src/components/genericComponents/multiSelectDropdown.vue
Normal file
150
webapp/src/components/genericComponents/multiSelectDropdown.vue
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<template>
|
||||
<div ref="multiselect" class="multi-select">
|
||||
<div
|
||||
class="dropdown-top ofm-opaque-element ofm-opaque-highlight"
|
||||
:class="{ open: isOpen }"
|
||||
@click="isOpen = !isOpen"
|
||||
>
|
||||
<span class="dropdown-top-text">{{ title }}</span>
|
||||
<span>{{ arrow }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isOpen" class="dropdown-menu ofm-opaque-element">
|
||||
<button class="ofm-text-button ofm-opaque-highlight" @click="selectAll">Select All</button>
|
||||
<button class="ofm-text-button ofm-opaque-highlight" @click="selectNone">Select None</button>
|
||||
<hr class="dropdown-separator" />
|
||||
<label v-for="option in options" :key="option" class="dropdown-item ofm-opaque-highlight">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="uk-checkbox"
|
||||
:value="option"
|
||||
:checked="modelValue.includes(option)"
|
||||
@change="toggleOption(option)"
|
||||
/>
|
||||
{{ option }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MultiSelectDropdown",
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "Filter",
|
||||
},
|
||||
},
|
||||
|
||||
emits: ["update:modelValue"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
arrow() {
|
||||
return this.isOpen ? "▲" : "▼";
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener("click", this.handleClickOutside);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
window.removeEventListener("click", this.handleClickOutside);
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleClickOutside(event) {
|
||||
if (!this.$refs.multiselect.contains(event.target)) {
|
||||
this.isOpen = false;
|
||||
}
|
||||
},
|
||||
toggleOption(value) {
|
||||
const selected = [...this.modelValue];
|
||||
|
||||
const index = selected.indexOf(value);
|
||||
|
||||
if (index > -1) {
|
||||
selected.splice(index, 1);
|
||||
} else {
|
||||
selected.push(value);
|
||||
}
|
||||
|
||||
this.$emit("update:modelValue", selected);
|
||||
},
|
||||
|
||||
selectAll() {
|
||||
this.$emit("update:modelValue", this.options);
|
||||
},
|
||||
|
||||
selectNone() {
|
||||
this.$emit("update:modelValue", []);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import url("../../assets/less/variables.less");
|
||||
|
||||
.multi-select {
|
||||
position: relative;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.dropdown-top {
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-top-text {
|
||||
color: @global-primary-background;
|
||||
}
|
||||
|
||||
.dropdown-top.open {
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border-left: 1px solid #ccc;
|
||||
border-right: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
border-radius: 0 0 4px 4px;
|
||||
max-height: 250px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-separator {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,10 +1,17 @@
|
|||
<template>
|
||||
<div ref="galleryDisplay" class="galleryDisplay uk-padding uk-padding-remove-top">
|
||||
<div ref="galleryDisplay" class="gallery-display uk-padding uk-padding-remove-top">
|
||||
<!-- Gallery nav bar -->
|
||||
<nav class="gallery-navbar uk-navbar-container uk-navbar-transparent" uk-navbar="mode: click">
|
||||
<!-- Right side buttons -->
|
||||
<div class="uk-navbar-right">
|
||||
<div class="uk-grid">
|
||||
<div class="gallery-button">
|
||||
<multi-select-dropdown
|
||||
v-model="selectedCardTypes"
|
||||
:options="allCardTypes"
|
||||
title="Filter Gallery"
|
||||
/>
|
||||
</div>
|
||||
<div class="gallery-button">
|
||||
<action-button
|
||||
class="uk-width-1-1"
|
||||
|
|
@ -25,11 +32,13 @@
|
|||
thing="gallery"
|
||||
action="delete_all_data"
|
||||
submit-label="Delete All"
|
||||
:submit-data="{ card_types: selectedCardTypes }"
|
||||
:is-disabled="totalPages == 0"
|
||||
:can-terminate="true"
|
||||
:button-primary="false"
|
||||
:modal-progress="true"
|
||||
:requires-confirmation="true"
|
||||
:confirmation-message="'<p>Are you sure you want to delete all gallery data from the microscope?</p><p>This is <b>irreversible</b>!</p>'"
|
||||
:confirmation-message="deleteAllConfirmationMessage"
|
||||
@error="modalError"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -54,7 +63,7 @@
|
|||
<div class="gallery-grid uk-grid-match" uk-grid>
|
||||
<div v-if="noItems">
|
||||
<h2>Nothing to show</h2>
|
||||
<p>There is no captured data to show..</p>
|
||||
<p>There is no captured data to show.</p>
|
||||
</div>
|
||||
<div v-for="itemData in paginatedItems" :key="itemData.id">
|
||||
<gallery-card
|
||||
|
|
@ -75,14 +84,14 @@
|
|||
|
||||
<script>
|
||||
import PaginateLinks from "@/components/genericComponents/paginateLinks.vue";
|
||||
import MultiSelectDropdown from "@/components/genericComponents/multiSelectDropdown.vue";
|
||||
import actionButton from "../labThingsComponents/actionButton.vue";
|
||||
import galleryCard from "./galleryComponents/galleryCard.vue";
|
||||
import galleryModal from "./galleryComponents/galleryViewer.vue";
|
||||
import { eventBus } from "../../eventBus.js";
|
||||
import { useIntersectionObserver } from "@vueuse/core";
|
||||
import { useSettingsStore } from "@/stores/settings.js";
|
||||
import { mapState, storeToRefs } from "pinia";
|
||||
import { watch } from "vue";
|
||||
import { mapState } from "pinia";
|
||||
|
||||
// Export main app
|
||||
export default {
|
||||
|
|
@ -92,6 +101,7 @@ export default {
|
|||
galleryCard,
|
||||
galleryModal,
|
||||
PaginateLinks,
|
||||
MultiSelectDropdown,
|
||||
},
|
||||
|
||||
emits: ["scrollTop"],
|
||||
|
|
@ -103,33 +113,49 @@ export default {
|
|||
osdViewer: null,
|
||||
currentPage: 1,
|
||||
itemsPerPage: 18,
|
||||
selectedCardTypes: [],
|
||||
allCardTypes: [],
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapState(useSettingsStore, ["baseUri", "ready"]),
|
||||
filtered_items() {
|
||||
return this.all_items.filter((item) => this.selectedCardTypes.includes(item.card_type));
|
||||
},
|
||||
noItems() {
|
||||
return !this.all_items || this.all_items?.length === 0;
|
||||
return !this.filtered_items || this.filtered_items?.length === 0;
|
||||
},
|
||||
totalPages() {
|
||||
return Math.ceil((this.all_items?.length || 0) / this.itemsPerPage);
|
||||
return Math.ceil((this.filtered_items?.length || 0) / this.itemsPerPage);
|
||||
},
|
||||
paginatedItems() {
|
||||
const start = (this.currentPage - 1) * this.itemsPerPage;
|
||||
return (this.all_items || []).slice(start, start + this.itemsPerPage);
|
||||
return (this.filtered_items || []).slice(start, start + this.itemsPerPage);
|
||||
},
|
||||
deleteAllConfirmationMessage() {
|
||||
return `
|
||||
<p>Are you sure you want to delete all gallery data with the following types</p>
|
||||
<ul>
|
||||
${this.selectedCardTypes.map((type) => `<li>${type}</li>`).join("\n")}
|
||||
</ul>
|
||||
<p>from the microscope?</p>
|
||||
<p>This is <b>irreversible</b>!</p>
|
||||
`;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
totalPages(newPageCount) {
|
||||
if (this.currentPage > newPageCount) {
|
||||
this.currentPage = Math.max(1, newPageCount);
|
||||
} else if (this.currentPage < 1) {
|
||||
this.currentPage == 1;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
const store = useSettingsStore();
|
||||
const { ready } = storeToRefs(store);
|
||||
this.unwatchStoreFunction = watch(ready, (isReady) => {
|
||||
if (isReady) {
|
||||
this.refreshGallery();
|
||||
} else {
|
||||
this.all_items = [];
|
||||
}
|
||||
});
|
||||
useIntersectionObserver(
|
||||
this.$refs.galleryDisplay,
|
||||
([{ isIntersecting }]) => {
|
||||
|
|
@ -139,6 +165,8 @@ export default {
|
|||
threshold: 0.0, // Adjust as needed
|
||||
},
|
||||
);
|
||||
this.allCardTypes = await this.readThingProperty("gallery", "card_types");
|
||||
this.selectedCardTypes = this.allCardTypes;
|
||||
// Update on mount (does nothing if not connected)
|
||||
await this.refreshGallery();
|
||||
// A global signal listener to perform a gallery refresh
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue