mirror of
https://github.com/simon987/sist2.git
synced 2025-04-16 08:56:45 +00:00
Option to update media type tab in real time, add media type table in details
This commit is contained in:
parent
64b8aab8bf
commit
625f3d0d6e
4
sist2-vue/dist/js/chunk-vendors.js
vendored
4
sist2-vue/dist/js/chunk-vendors.js
vendored
File diff suppressed because one or more lines are too long
2
sist2-vue/dist/js/index.js
vendored
2
sist2-vue/dist/js/index.js
vendored
File diff suppressed because one or more lines are too long
@ -256,20 +256,31 @@ class Sist2Api {
|
||||
});
|
||||
}
|
||||
|
||||
getMimeTypes() {
|
||||
return this.esQuery({
|
||||
aggs: {
|
||||
mimeTypes: {
|
||||
terms: {
|
||||
field: "mime",
|
||||
size: 10000
|
||||
}
|
||||
getMimeTypes(query = undefined) {
|
||||
const AGGS = {
|
||||
mimeTypes: {
|
||||
terms: {
|
||||
field: "mime",
|
||||
size: 10000
|
||||
}
|
||||
},
|
||||
size: 0,
|
||||
}).then(resp => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!query) {
|
||||
query = {
|
||||
aggs: AGGS,
|
||||
size: 0,
|
||||
};
|
||||
} else {
|
||||
query.size = 0;
|
||||
query.aggs = AGGS;
|
||||
}
|
||||
|
||||
return this.esQuery(query).then(resp => {
|
||||
const mimeMap: any[] = [];
|
||||
resp["aggregations"]["mimeTypes"]["buckets"].sort((a: any, b: any) => a.key > b.key).forEach((bucket: any) => {
|
||||
const buckets = resp["aggregations"]["mimeTypes"]["buckets"];
|
||||
|
||||
buckets.sort((a: any, b: any) => a.key > b.key).forEach((bucket: any) => {
|
||||
const tmp = bucket["key"].split("/");
|
||||
const category = tmp[0];
|
||||
const mime = tmp[1];
|
||||
@ -289,11 +300,18 @@ class Sist2Api {
|
||||
});
|
||||
|
||||
if (!category_exists) {
|
||||
mimeMap.push({"text": category, children: [child]});
|
||||
mimeMap.push({text: category, children: [child], id: category});
|
||||
}
|
||||
})
|
||||
|
||||
return mimeMap;
|
||||
mimeMap.forEach(node => {
|
||||
if (node.children) {
|
||||
node.children.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
})
|
||||
mimeMap.sort((a, b) => a.id.localeCompare(b.id))
|
||||
|
||||
return {buckets, mimeMap};
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -7,40 +7,24 @@ import InspireTree from "inspire-tree";
|
||||
import InspireTreeDOM from "inspire-tree-dom";
|
||||
|
||||
import "inspire-tree-dom/dist/inspire-tree-light.min.css";
|
||||
import {getSelectedTreeNodes} from "@/util";
|
||||
import {getSelectedTreeNodes, getTreeNodeAttributes} from "@/util";
|
||||
import Sist2Api from "@/Sist2Api";
|
||||
import Sist2Query from "@/Sist2Query";
|
||||
|
||||
export default {
|
||||
name: "MimePicker",
|
||||
data() {
|
||||
return {
|
||||
mimeTree: null,
|
||||
stashedMimeTreeAttributes: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (mutation.type === "setUiMimeMap") {
|
||||
const mimeMap = mutation.payload.slice();
|
||||
|
||||
const elem = document.getElementById("mimeTree");
|
||||
console.log(elem);
|
||||
|
||||
this.mimeTree = new InspireTree({
|
||||
selection: {
|
||||
mode: 'checkbox'
|
||||
},
|
||||
data: mimeMap
|
||||
});
|
||||
new InspireTreeDOM(this.mimeTree, {
|
||||
target: '#mimeTree'
|
||||
});
|
||||
this.mimeTree.on("node.state.changed", this.handleTreeClick);
|
||||
this.mimeTree.deselect();
|
||||
|
||||
if (this.$store.state._onLoadSelectedMimeTypes.length > 0) {
|
||||
this.$store.state._onLoadSelectedMimeTypes.forEach(mime => {
|
||||
this.mimeTree.node(mime).select();
|
||||
});
|
||||
}
|
||||
if (mutation.type === "setUiMimeMap" && this.mimeTree === null) {
|
||||
this.initializeTree();
|
||||
} else if (mutation.type === "busSearch") {
|
||||
this.updateTree();
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -52,6 +36,73 @@ export default {
|
||||
|
||||
this.$store.commit("setSelectedMimeTypes", getSelectedTreeNodes(this.mimeTree));
|
||||
},
|
||||
updateTree() {
|
||||
|
||||
if (this.$store.getters.optUpdateMimeMap === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stashedMimeTreeAttributes === null) {
|
||||
this.stashedMimeTreeAttributes = getTreeNodeAttributes(this.mimeTree);
|
||||
}
|
||||
|
||||
const query = Sist2Query.searchQuery();
|
||||
|
||||
Sist2Api.getMimeTypes(query).then(({buckets, mimeMap}) => {
|
||||
this.$store.commit("setUiMimeMap", mimeMap);
|
||||
this.$store.commit("setUiDetailsMimeAgg", buckets);
|
||||
|
||||
this.mimeTree.removeAll();
|
||||
this.mimeTree.addNodes(mimeMap);
|
||||
|
||||
// Restore selected mimes
|
||||
if (this.stashedMimeTreeAttributes === null) {
|
||||
// NOTE: This happens when successive fast searches are triggered
|
||||
this.stashedMimeTreeAttributes = {};
|
||||
// Always add the selected mime types
|
||||
this.$store.state.selectedMimeTypes.forEach(mime => {
|
||||
this.stashedMimeTreeAttributes[mime] = {
|
||||
checked: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(this.stashedMimeTreeAttributes).forEach(([mime, attributes]) => {
|
||||
if (this.mimeTree.node(mime)) {
|
||||
if (attributes.checked) {
|
||||
this.mimeTree.node(mime).select();
|
||||
}
|
||||
if (attributes.collapsed === false) {
|
||||
this.mimeTree.node(mime).expand();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.stashedMimeTreeAttributes = null;
|
||||
});
|
||||
},
|
||||
|
||||
initializeTree() {
|
||||
const mimeMap = this.$store.state.uiMimeMap;
|
||||
|
||||
this.mimeTree = new InspireTree({
|
||||
selection: {
|
||||
mode: "checkbox"
|
||||
},
|
||||
data: mimeMap
|
||||
});
|
||||
|
||||
new InspireTreeDOM(this.mimeTree, {
|
||||
target: "#mimeTree"
|
||||
});
|
||||
this.mimeTree.on("node.state.changed", this.handleTreeClick);
|
||||
this.mimeTree.deselect();
|
||||
|
||||
if (this.$store.state._onLoadSelectedMimeTypes.length > 0) {
|
||||
this.$store.state._onLoadSelectedMimeTypes.forEach(mime => {
|
||||
this.mimeTree.node(mime).select();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -3,7 +3,10 @@
|
||||
<span>{{ hitCount }} {{ hitCount === 1 ? $t("hit") : $t("hits") }}</span>
|
||||
|
||||
<div style="float: right">
|
||||
<b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile">{{ $t("details") }}</b-button>
|
||||
<b-button v-b-toggle.collapse-1 variant="primary" class="not-mobile" @click="onToggle()">{{
|
||||
$t("details")
|
||||
}}
|
||||
</b-button>
|
||||
|
||||
<template v-if="hitCount !== 0">
|
||||
<SortSelect class="ml-2"></SortSelect>
|
||||
@ -14,22 +17,42 @@
|
||||
|
||||
<b-collapse id="collapse-1" class="pt-2" style="clear:both;">
|
||||
<b-card>
|
||||
<b-table :items="tableItems" small borderless thead-class="hidden" class="mb-0"></b-table>
|
||||
<b-table :items="tableItems" small borderless bordered thead-class="hidden" class="mb-0"></b-table>
|
||||
|
||||
<br/>
|
||||
<h4>
|
||||
{{$t("mimeTypes")}}
|
||||
<b-button size="sm" variant="primary" class="float-right" @click="onCopyClick"><ClipboardIcon/></b-button>
|
||||
</h4>
|
||||
<Preloader v-if="$store.state.uiDetailsMimeAgg == null"></Preloader>
|
||||
<b-table
|
||||
v-else
|
||||
sort-by="doc_count"
|
||||
:sort-desc="true"
|
||||
thead-class="hidden"
|
||||
:items="$store.state.uiDetailsMimeAgg" small bordered class="mb-0"
|
||||
></b-table>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {EsResult} from "@/Sist2Api";
|
||||
import Sist2Api, {EsResult} from "@/Sist2Api";
|
||||
import Vue from "vue";
|
||||
import {humanFileSize} from "@/util";
|
||||
import DisplayModeToggle from "@/components/DisplayModeToggle.vue";
|
||||
import SortSelect from "@/components/SortSelect.vue";
|
||||
import Preloader from "@/components/Preloader.vue";
|
||||
import Sist2Query from "@/Sist2Query";
|
||||
import ClipboardIcon from "@/components/icons/ClipboardIcon.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: "ResultsCard",
|
||||
components: {SortSelect, DisplayModeToggle},
|
||||
components: {ClipboardIcon, Preloader, SortSelect, DisplayModeToggle},
|
||||
created() {
|
||||
|
||||
},
|
||||
computed: {
|
||||
lastResultsLoaded() {
|
||||
return this.$store.state.lastQueryResults != null;
|
||||
@ -54,6 +77,39 @@ export default Vue.extend({
|
||||
totalSize() {
|
||||
return humanFileSize((this.$store.state.lastQueryResults as EsResult).aggregations.total_size.value);
|
||||
},
|
||||
onToggle() {
|
||||
const show = !document.getElementById("collapse-1").classList.contains("show");
|
||||
this.$store.commit("setUiShowDetails", show);
|
||||
|
||||
if (show && this.$store.state.uiDetailsMimeAgg == null && !this.$store.state.optUpdateMimeMap) {
|
||||
// Mime aggs are not updated automatically, update now
|
||||
this.forceUpdateMimeAgg();
|
||||
}
|
||||
},
|
||||
onCopyClick() {
|
||||
let tsvString = "";
|
||||
this.$store.state.uiDetailsMimeAgg.slice().sort((a,b) => b["doc_count"] - a["doc_count"]).forEach(row => {
|
||||
tsvString += `${row["key"]}\t${row["doc_count"]}\n`;
|
||||
});
|
||||
|
||||
navigator.clipboard.writeText(tsvString);
|
||||
|
||||
this.$bvToast.toast(
|
||||
this.$t("toast.copiedToClipboard"),
|
||||
{
|
||||
title: null,
|
||||
noAutoHide: false,
|
||||
toaster: "b-toaster-bottom-right",
|
||||
headerClass: "hidden",
|
||||
bodyClass: "toast-body-info",
|
||||
});
|
||||
},
|
||||
forceUpdateMimeAgg() {
|
||||
const query = Sist2Query.searchQuery();
|
||||
Sist2Api.getMimeTypes(query).then(({buckets}) => {
|
||||
this.$store.commit("setUiDetailsMimeAgg", buckets);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -120,7 +120,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.$store.subscribe((mutation) => {
|
||||
if (mutation.type === "setUiMimeMap") {
|
||||
if (mutation.type === "setUiMimeMap" && this.tagTree === null) {
|
||||
this.initializeTree();
|
||||
this.updateTree();
|
||||
} else if (mutation.type === "busUpdateTags") {
|
||||
@ -147,6 +147,7 @@ export default {
|
||||
this.tagTree.on("node.state.changed", this.handleTreeClick);
|
||||
},
|
||||
updateTree() {
|
||||
// TODO: remember which tags are selected and restore?
|
||||
const tagMap = [];
|
||||
Sist2Api.getTags().then(tags => {
|
||||
tags.forEach(tag => addTag(tagMap, tag.id, tag.id, tag.count));
|
||||
|
21
sist2-vue/src/components/icons/ClipboardIcon.vue
Normal file
21
sist2-vue/src/components/icons/ClipboardIcon.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<svg style="width:24px;height:24px" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17,9H7V7H17M17,13H7V11H17M14,17H7V15H14M12,3A1,1 0 0,1 13,4A1,1 0 0,1 12,5A1,1 0 0,1 11,4A1,1 0 0,1 12,3M19,3H14.82C14.4,1.84 13.3,1 12,1C10.7,1 9.6,1.84 9.18,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ClipboardIcon"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
</style>
|
@ -66,7 +66,8 @@ export default {
|
||||
resultSize: "Number of results per page",
|
||||
tagOrOperator: "Use OR operator when specifying multiple tags.",
|
||||
hideDuplicates: "Hide duplicate results based on checksum",
|
||||
hideLegacy: "Hide the 'legacyES' Elasticsearch notice"
|
||||
hideLegacy: "Hide the 'legacyES' Elasticsearch notice",
|
||||
updateMimeMap: "Update the Media Types tree in real time"
|
||||
},
|
||||
queryMode: {
|
||||
simple: "Simple",
|
||||
@ -129,7 +130,8 @@ export default {
|
||||
esQueryErr: "Could not parse or execute query, please check the Advanced search documentation. " +
|
||||
"See server logs for more information.",
|
||||
dupeTagTitle: "Duplicate tag",
|
||||
dupeTag: "This tag already exists for this document."
|
||||
dupeTag: "This tag already exists for this document.",
|
||||
copiedToClipboard: "Copied to clipboard"
|
||||
},
|
||||
saveTagModalTitle: "Add tag",
|
||||
saveTagPlaceholder: "Tag name",
|
||||
@ -226,7 +228,8 @@ export default {
|
||||
resultSize: "Nombre de résultats par page",
|
||||
tagOrOperator: "Utiliser l'opérateur OU lors de la spécification de plusieurs tags",
|
||||
hideDuplicates: "Masquer les résultats en double",
|
||||
hideLegacy: "Masquer la notice 'legacyES' Elasticsearch"
|
||||
hideLegacy: "Masquer la notice 'legacyES' Elasticsearch",
|
||||
updateMimeMap: "Mettre à jour l'arbre de Types de médias en temps réel"
|
||||
},
|
||||
queryMode: {
|
||||
simple: "Simple",
|
||||
@ -290,7 +293,8 @@ export default {
|
||||
esQueryErr: "Impossible d'analyser ou d'exécuter la requête, veuillez consulter la documentation sur la " +
|
||||
"recherche avancée. Voir les journaux du serveur pour plus d'informations.",
|
||||
dupeTagTitle: "Tag en double",
|
||||
dupeTag: "Ce tag existe déjà pour ce document."
|
||||
dupeTag: "Ce tag existe déjà pour ce document.",
|
||||
copiedToClipboard: "Copié dans le presse-papier"
|
||||
},
|
||||
saveTagModalTitle: "Ajouter un tag",
|
||||
saveTagPlaceholder: "Nom du tag",
|
||||
@ -386,7 +390,8 @@ export default {
|
||||
resultSize: "每页结果数",
|
||||
tagOrOperator: "使用或操作(OR)匹配多个标签。",
|
||||
hideDuplicates: "使用校验码隐藏重复结果",
|
||||
hideLegacy: "隐藏'legacyES' Elasticsearch 通知"
|
||||
hideLegacy: "隐藏'legacyES' Elasticsearch 通知",
|
||||
updateMimeMap: "媒体类型树的实时更新"
|
||||
},
|
||||
queryMode: {
|
||||
simple: "简单",
|
||||
@ -449,7 +454,8 @@ export default {
|
||||
esQueryErr: "无法识别或执行查询,请查阅高级搜索文档。" +
|
||||
"查看服务日志以获取更多信息。",
|
||||
dupeTagTitle: "重复标签",
|
||||
dupeTag: "该标签已存在于此文档。"
|
||||
dupeTag: "该标签已存在于此文档。",
|
||||
copiedToClipboard: "复制到剪贴板"
|
||||
},
|
||||
saveTagModalTitle: "增加标签",
|
||||
saveTagPlaceholder: "标签名",
|
||||
|
@ -48,6 +48,7 @@ export default new Vuex.Store({
|
||||
optLightboxLoadOnlyCurrent: false,
|
||||
optLightboxSlideDuration: 15,
|
||||
optHideLegacy: false,
|
||||
optUpdateMimeMap: true,
|
||||
|
||||
_onLoadSelectedIndices: [] as string[],
|
||||
_onLoadSelectedMimeTypes: [] as string[],
|
||||
@ -72,9 +73,14 @@ export default new Vuex.Store({
|
||||
uiLightboxSlide: 0,
|
||||
uiReachedScrollEnd: false,
|
||||
|
||||
uiDetailsMimeAgg: null,
|
||||
uiShowDetails: false,
|
||||
|
||||
uiMimeMap: [] as any[]
|
||||
},
|
||||
mutations: {
|
||||
setUiShowDetails: (state, val) => state.uiShowDetails = val,
|
||||
setUiDetailsMimeAgg: (state, val) => state.uiDetailsMimeAgg = val,
|
||||
setUiReachedScrollEnd: (state, val) => state.uiReachedScrollEnd = val,
|
||||
setTags: (state, val) => state.tags = val,
|
||||
setPathText: (state, val) => state.pathText = val,
|
||||
@ -150,6 +156,7 @@ export default new Vuex.Store({
|
||||
setOptTreemapSize: (state, val) => state.optTreemapSize = val,
|
||||
setOptTreemapColor: (state, val) => state.optTreemapColor = val,
|
||||
setOptHideLegacy: (state, val) => state.optHideLegacy = val,
|
||||
setOptUpdateMimeMap: (state, val) => state.optUpdateMimeMap = val,
|
||||
|
||||
setOptLightboxLoadOnlyCurrent: (state, val) => state.optLightboxLoadOnlyCurrent = val,
|
||||
setOptLightboxSlideDuration: (state, val) => state.optLightboxSlideDuration = val,
|
||||
@ -162,6 +169,9 @@ export default new Vuex.Store({
|
||||
busUpdateTags: () => {
|
||||
// noop
|
||||
},
|
||||
busSearch: () => {
|
||||
// noop
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setSist2Info: (store, val) => {
|
||||
@ -290,6 +300,7 @@ export default new Vuex.Store({
|
||||
commit("setUiLightboxTypes", []);
|
||||
commit("setUiLightboxCaptions", []);
|
||||
commit("setUiLightboxKey", 0);
|
||||
commit("setUiDetailsMimeAgg", null);
|
||||
}
|
||||
},
|
||||
modules: {},
|
||||
@ -354,5 +365,6 @@ export default new Vuex.Store({
|
||||
optLightboxSlideDuration: state => state.optLightboxSlideDuration,
|
||||
optResultSize: state => state.size,
|
||||
optHideLegacy: state => state.optHideLegacy,
|
||||
optUpdateMimeMap: state => state.optUpdateMimeMap,
|
||||
}
|
||||
})
|
@ -97,6 +97,30 @@ export function getSelectedTreeNodes(tree: any) {
|
||||
return Array.from(selectedNodes);
|
||||
}
|
||||
|
||||
export function getTreeNodeAttributes(tree: any) {
|
||||
const nodes = tree.selectable();
|
||||
const attributes = {};
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
|
||||
let id = null;
|
||||
|
||||
if (nodes[i].text.indexOf("(") !== -1 && nodes[i].values) {
|
||||
id = nodes[i].values.slice(-1)[0];
|
||||
} else {
|
||||
id = nodes[i].id
|
||||
}
|
||||
|
||||
attributes[id] = {
|
||||
checked: nodes[i].itree.state.checked,
|
||||
collapsed: nodes[i].itree.state.collapsed,
|
||||
}
|
||||
}
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
|
||||
export function serializeMimes(mimes: string[]): string | undefined {
|
||||
if (mimes.length == 0) {
|
||||
return undefined;
|
||||
|
@ -37,6 +37,10 @@
|
||||
<b-form-checkbox :checked="optHideLegacy" @input="setOptHideLegacy">
|
||||
{{ $t("opt.hideLegacy") }}
|
||||
</b-form-checkbox>
|
||||
|
||||
<b-form-checkbox :checked="optUpdateMimeMap" @input="setOptUpdateMimeMap">
|
||||
{{ $t("opt.updateMimeMap") }}
|
||||
</b-form-checkbox>
|
||||
</b-card>
|
||||
|
||||
<br/>
|
||||
@ -224,6 +228,7 @@ export default {
|
||||
"optLang",
|
||||
"optHideDuplicates",
|
||||
"optHideLegacy",
|
||||
"optUpdateMimeMap",
|
||||
]),
|
||||
clientWidth() {
|
||||
return window.innerWidth;
|
||||
@ -266,7 +271,8 @@ export default {
|
||||
"setOptTagOrOperator",
|
||||
"setOptLang",
|
||||
"setOptHideDuplicates",
|
||||
"setOptHideLegacy"
|
||||
"setOptHideLegacy",
|
||||
"setOptUpdateMimeMap"
|
||||
]),
|
||||
onResetClick() {
|
||||
localStorage.removeItem("sist2_configuration");
|
||||
|
@ -139,7 +139,7 @@ export default Vue.extend({
|
||||
this.setSist2Info(data);
|
||||
this.setIndices(data.indices);
|
||||
|
||||
Sist2Api.getMimeTypes().then(mimeMap => {
|
||||
Sist2Api.getMimeTypes(Sist2Query.searchQuery()).then(({mimeMap}) => {
|
||||
this.$store.commit("setUiMimeMap", mimeMap);
|
||||
this.uiLoading = false;
|
||||
this.search(true);
|
||||
@ -185,6 +185,7 @@ export default Vue.extend({
|
||||
async searchNow(q: any) {
|
||||
this.searchBusy = true;
|
||||
await this.$store.dispatch("incrementQuerySequence");
|
||||
this.$store.commit("busSearch");
|
||||
|
||||
Sist2Api.esQuery(q).then(async (resp: EsResult) => {
|
||||
await this.handleSearch(resp);
|
||||
@ -286,6 +287,11 @@ export default Vue.extend({
|
||||
border: none;
|
||||
}
|
||||
|
||||
.toast-header-info, .toast-body-info {
|
||||
background: #2196f3;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.toast-header-error, .toast-body-error {
|
||||
background: #a94442;
|
||||
color: #f2dede !important;
|
||||
|
6
src/web/static_generated.c
vendored
6
src/web/static_generated.c
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user