Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • mdm-lab/wiki
  • hvaysset/wiki
  • jsousa/wiki
  • tclabby/wiki
4 results
Show changes
Commits on Source (64)
Showing
with 1665 additions and 345 deletions
data/ filter=lfs diff=lfs merge=lfs -text data/ filter=lfs diff=lfs merge=lfs -text
data/**/*.csv filter=lfs diff=lfs merge=lfs -text data/**/*.csv filter=lfs diff=lfs merge=lfs -text
data/**/*.tsv filter=lfs diff=lfs merge=lfs -text
...@@ -319,7 +319,7 @@ get-zotero: ...@@ -319,7 +319,7 @@ get-zotero:
variables: variables:
CONTEXT: "." CONTEXT: "."
DOCKERFILE: "Dockerfile" DOCKERFILE: "Dockerfile"
BASE_URL: /wiki/ BASE_URL: /
MEILI_HOST: "http://localhost:7700" MEILI_HOST: "http://localhost:7700"
before_script: before_script:
- *docker-login - *docker-login
...@@ -328,7 +328,7 @@ get-zotero: ...@@ -328,7 +328,7 @@ get-zotero:
docker buildx build --pull -t "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA" docker buildx build --pull -t "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA"
--build-arg "BASE_URL=$BASE_URL" --build-arg "BASE_URL=$BASE_URL"
--build-arg "MEILI_HOST=$MEILI_HOST" --build-arg "MEILI_HOST=$MEILI_HOST"
--build-arg "MEILI_API_KEY=$MEILI_API_KEY" --build-arg "MEILI_API_KEY=$MEILI_API_KEY"
-f $DOCKERFILE $CONTEXT -f $DOCKERFILE $CONTEXT
- docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA" - docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA"
...@@ -341,6 +341,7 @@ build:dev:wiki: ...@@ -341,6 +341,7 @@ build:dev:wiki:
# - get-pfam # - get-pfam
variables: variables:
BASE_URL: /wiki/ BASE_URL: /wiki/
HOST_URL: https://${HOST_DEV}
before_script: before_script:
- *docker-login - *docker-login
# - "sed -i 's/MEILISEARCH_API_KEY/${$MEILI_API_KEY}/g' nuxt.config.ts" # - "sed -i 's/MEILISEARCH_API_KEY/${$MEILI_API_KEY}/g' nuxt.config.ts"
...@@ -356,6 +357,8 @@ build:prod:wiki: ...@@ -356,6 +357,8 @@ build:prod:wiki:
# - get-pfam # - get-pfam
variables: variables:
BASE_URL: /wiki/ BASE_URL: /wiki/
HOST_URL: https://${HOST_PROD}
rules: rules:
- if: $CI_COMMIT_BRANCH == "main" - if: $CI_COMMIT_BRANCH == "main"
...@@ -486,7 +489,7 @@ build:prod:wiki: ...@@ -486,7 +489,7 @@ build:prod:wiki:
deploy:dev: deploy:dev:
extends: .deploy extends: .deploy
rules: rules:
- if: $CI_COMMIT_BRANCH == "dev" - if: $CI_COMMIT_BRANCH == "dev" || $CI_COMMIT_BRANCH == "foldseek-pages"
needs: needs:
- "build:dev:wiki" - "build:dev:wiki"
when: manual when: manual
......
...@@ -15,14 +15,14 @@ FROM node:21.1-bookworm-slim as dev ...@@ -15,14 +15,14 @@ FROM node:21.1-bookworm-slim as dev
ARG BASE_URL=/ ARG BASE_URL=/
ARG MEILI_HOST=http://localhost:7700 ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY=api_key ARG MEILI_API_KEY=api_key
ARG HOST_URL
ENV NUXT_APP_BASE_URL=${BASE_URL} ENV NUXT_APP_BASE_URL=${BASE_URL}
# nuxt module # nuxt module
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST} ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY} ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY}
ENV NUXT_PUBLIC_HOST_URL=${HOST_URL}
ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST} # ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY} # ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app WORKDIR /usr/src/app
...@@ -43,10 +43,21 @@ RUN npm run build ...@@ -43,10 +43,21 @@ RUN npm run build
### STAGE: serve ### ### STAGE: serve ###
FROM node:21.1-bookworm-slim as serve FROM node:21.1-bookworm-slim as serve
ARG BASE_URL=/
ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY
ARG HOST_URL
WORKDIR /usr/src/app ENV NUXT_APP_BASE_URL=${BASE_URL}
COPY --from=build /usr/src/app/.output ./
# nuxt module
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY}
ENV NUXT_PUBLIC_HOST_URL=${HOST_URL}
WORKDIR /usr/src/app
COPY --chown=node:node --from=build /usr/src/app/.output ./
USER node
CMD [ "node", "server/index.mjs"] CMD [ "node", "server/index.mjs"]
...@@ -55,6 +66,7 @@ FROM node:21.1-bookworm-slim as generate ...@@ -55,6 +66,7 @@ FROM node:21.1-bookworm-slim as generate
ARG BASE_URL=/ ARG BASE_URL=/
ARG MEILI_HOST=http://localhost:7700 ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY ARG MEILI_API_KEY
ARG HOST_URL
ENV NODE_OPTIONS=--max_old_space_size=12288 ENV NODE_OPTIONS=--max_old_space_size=12288
ENV NUXT_APP_BASE_URL=${BASE_URL} ENV NUXT_APP_BASE_URL=${BASE_URL}
...@@ -62,9 +74,11 @@ ENV NUXT_APP_BASE_URL=${BASE_URL} ...@@ -62,9 +74,11 @@ ENV NUXT_APP_BASE_URL=${BASE_URL}
# nuxt module # nuxt module
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST} ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_HOST_URL=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY} ENV NUXT_PUBLIC_MEILISEARCH_CLIENT_SEARCH_API_KEY=${MEILI_API_KEY}
ENV NUXT_PUBLIC_HOST_URL=${HOST_URL}
ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST} # ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY} # ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app WORKDIR /usr/src/app
......
This diff is collapsed.
assets/foldseek.png

142 KiB

<script setup lang="ts">
import { filter } from '@observablehq/plot'
export interface FilterItem {
type: 'facet' | 'innerOperator' | 'outerOperator' | 'value'
value: string
title: string
count?: number
deletable: boolean
props: Record<string, any>
}
export interface FacetItem {
title: string
value: string
type: "facet"
icon?: string
count?: number
}
export interface OperatorItem {
title: string,
type: 'operator'
}
export interface FacetCategory {
title: string
type: "subheader"
}
export interface FacetDivider {
type: "divider"
}
export type FacetInputItem = FacetItem | FacetCategory | FacetDivider
export interface Props {
db: string
modelValue: FilterItem[] | undefined
facets: MaybeRef<FacetInputItem[] | undefined>
facetDistribution: MaybeRef<Record<string, Record<string, number>> | undefined>
isValidFilters?: MaybeRef<boolean>
autocompleteProps?: Record<string, any>
}
const emit = defineEmits(['update:modelValue', "meiliFilters"])
const filterId = ref<number>(0)
const props = withDefaults(defineProps<Props>(), {
modelValue: undefined,
autocompleteProps: () => {
return {
chips: true,
clearable: true,
multiple: true,
"auto-select-first": true,
"return-object": true,
"prepend-inner-icon": "md:filter_alt",
"hide-details": "auto",
"item-value": "value", "item-title": "title",
label: "Filter results...",
"single-line": true,
}
},
isValidFilters: false
});
// const { result: msResult } = useMeiliSearch(props.db)
const isAutocompleteFocused = ref<boolean>(false)
// const facetDistribution: Ref<Record<string, Record<string, number>>> = useState('facetDistribution')
const autocompleteProps = computed(() => {
return {
...props.autocompleteProps,
items: toValue(autocompleteItems)
}
})
const filterStep = computed(() => {
const toValFilterItems = toValue(props.modelValue)
if (toValFilterItems !== undefined) {
return toValFilterItems.length % 4
}
})
const innerOperatorItems = ref<FilterItem[]>([
{
type: "innerOperator", value: '=', title: "is", deletable: false, props: {
type: "innerOperator", deletable: false
}
}, {
type: "innerOperator", value: '!=', title: "is not", deletable: false, props: {
type: "innerOperator",
deletable: false
}
},
])
const outerOperatorItems = ref<FilterItem[]>([
{
type: "outerOperator", value: 'AND', title: "AND", deletable: false, props: {
type: "outerOperator", deletable: false
}
}, {
type: "outerOperator", value: 'OR', title: "OR", deletable: false, props: {
type: "outerOperator",
deletable: false
}
},
])
const autocompleteItems = computed(() => {
const toValFilterItems = toValue(props.modelValue)
// const index = toValFilterItems?.length ?? 0
if (filterStep.value === undefined || filterStep.value === 0) {
filterId.value++
return toValue(props.facets)?.map(facetItem => {
switch (facetItem.type) {
case "facet":
return {
type: "facet",
value: `${facetItem.value}-${filterId.value}`,
title: facetItem.title,
deletable: false,
icon: facetItem?.icon,
count: facetItem?.count,
props: {
deletable: false,
type: "facet"
}
}
case "subheader":
return {
type: "subheader",
title: facetItem.title,
deletable: false,
props: {
type: "subheader"
}
}
case "divider":
return { type: "divider" }
default:
break;
}
})
}
if (filterStep.value === 1) {
filterId.value++
return innerOperatorItems.value.map(it => { return { ...it, value: `${it.value}-${filterId.value}`, } })
}
if (filterStep.value === 2) {
filterId.value++
// get the facet value
if (Array.isArray(toValFilterItems)) {
const { type, value } = toValFilterItems?.slice(-2, -1)[0]
const sanitizedValue = value.split("-")[0]
const facetDistri = toValue(props.facetDistribution)
return facetDistri?.[sanitizedValue] ? Object.entries(facetDistri[sanitizedValue]).map(([key, val]) => {
return {
type: "value", value: `${key}-${filterId.value}`, title: key, count: val, deletable: true, props: {
type: "value", count: val, deletable: true
}
}
}) : []
}
}
if (filterStep.value === 3) {
filterId.value++
return outerOperatorItems.value.map(it => { return { ...it, value: `${it.value}-${filterId.value}`, } })
}
})
const hasFacetDistribution = computed(() => {
const toValFacetDistribution = toValue(props.facetDistribution)
return toValFacetDistribution !== undefined && Object.keys(toValFacetDistribution).length > 0
})
function updateAutocompleteFocused(isFocused: boolean) {
isAutocompleteFocused.value = isFocused
}
function emitUpdateModelValue(filters: MaybeRef<FilterItem[] | undefined>) {
emit('update:modelValue', toValue(filters))
}
function clearFilters() {
emitUpdateModelValue(undefined)
}
function deleteOneFilter(index: number) {
const toValFilterItems = toValue(props.modelValue)
// check if the next item is an outeroperator
const nextFilterItem = toValFilterItems?.slice(index + 1, index + 2)
if (index + 1 === toValFilterItems?.length && toValFilterItems?.length >= 7) {
// need to remove the previous outer operator
toValFilterItems?.splice(index - 3, 4)
}
else if (nextFilterItem?.length === 1 && nextFilterItem[0].type === 'outerOperator') {
toValFilterItems?.splice(index - 2, 4)
}
else {
toValFilterItems?.splice(index - 2, 3)
}
emitUpdateModelValue(toValFilterItems)
}
function isItemFilter(type: string | undefined) {
return type === "facet" || type === "innerOperator" || type === "outerOperator" || type === "value"
}
const hint = ref<string>('All <span class="font-weight-bold">OR</span> in a row are grouped together. Example: <span class="font-weight-bold">brex OR avs AND Archea</span> &rArr; <span class="font-weight-bold">(brex OR avs) AND Archea</span>')
</script>
<template>
<v-autocomplete :model-value="props.modelValue" @click:clear="clearFilters" v-bind="autocompleteProps" :hint="hint"
persistent-hint @update:focused="updateAutocompleteFocused" @update:modelValue="emitUpdateModelValue"
:loading="!hasFacetDistribution" :disabled="!hasFacetDistribution">
<template #message="{ message }">
<span v-html="message"></span>
</template>
<template #item="{ props, item }">
<v-list-item v-if="isItemFilter(item?.raw?.type)" v-bind="{ ...props, active: false }" :title="item.title"
:prepend-icon="item?.raw?.icon ? item.raw.icon : undefined"
:subtitle="item.raw?.count ? item.raw.count : ''" :value="props.value">
</v-list-item>
<v-divider v-if="item.raw.type === 'divider'"></v-divider>
<v-list-subheader v-if="item.raw.type === 'subheader'" :title="item.raw.title"></v-list-subheader>
</template>
<template #chip="{ props, item, index }">
<v-chip v-bind="props" :text="item.raw.title" :closable="item.props.deletable"
@click:close="item.props.type === deleteOneFilter(index)"></v-chip>
</template>
<!-- <template #append>
<v-btn variant="text" icon="md:filter_alt" @click="emitUpdateModelValue(props.modelValue)"
:disabled="!isValidFilters"></v-btn>
</template> -->
</v-autocomplete>
</template>
\ No newline at end of file
<template> <template>
<v-row> <v-row>
<v-col> <v-col>
<v-btn prepend-icon="mdi-gitlab" variant="text" size="small" :href="path" target="_blank">Edit on gitlab</v-btn> <v-btn prepend-icon="i-vscode-icons:file-type-gitlab" variant="text" size="small" :href="path" target="_blank">Edit on gitlab</v-btn>
<v-divider> </v-divider> <v-divider> </v-divider>
</v-col> </v-col>
</v-row> </v-row>
......
<script setup lang="ts">
interface Props {
foldseekPath: string
title: string
}
const props = withDefaults(defineProps<Props>(), { title: "Result of Foldseek search" });
const { width, height } = useDisplay()
const dialog = ref(false)
const iframe = ref()
const layout = ref({
scrollbarWidth: 15,
paddingLeft: 24,
paddingRight: 24,
toolbarHeight: 48,
containerPaddingTop: 16,
containerPaddingBottom: 10
})
const xMargin = computed(() => {
const toValLayout = toValue(layout)
return toValLayout.scrollbarWidth + toValLayout.paddingLeft + toValLayout.paddingRight
})
const yMargin = computed(() => {
const toValLayout = toValue(layout)
return toValLayout.toolbarHeight + toValLayout.containerPaddingBottom + toValLayout.containerPaddingTop
})
const computedWidth = computed(() => {
return toValue(width) - toValue(xMargin)
})
const computedHeight = computed(() => {
return toValue(height) - toValue(yMargin)
})
function fullscreen() {
console.log(iframe.value)
iframe.value.requestFullscreen();
}
</script>
<template>
<v-dialog v-model="dialog" fullscreen transition="dialog-bottom-transition">
<template v-slot:activator="{ props }">
<v-avatar>
<v-img src="~/assets/foldseek.png" alt="Foldseek results" v-bind="props" class="cursor-pointer"></v-img>
<!-- <v-btn color="primary" dark v-bind="props">
<v-img src="~/assets/foldseek.png" alt="Foldseek results"></v-img>
</v-btn> -->
</v-avatar>
</template>
<v-card variant="flat">
<v-toolbar flat color="transparent" density="compact">
<v-btn variant="text" color="primary" prepend-icon="mdi-arrow-left" @click="dialog = false">
Return to structure list
</v-btn>
<v-divider vertical inset></v-divider>
<v-toolbar-title> {{ props.title }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<v-btn @click="fullscreen()" icon="md:fullscreen"></v-btn>
</v-toolbar-items>
</v-toolbar>
<v-card-text>
<iframe ref="iframe" :width="computedWidth" :height="computedHeight" allow="fullscreen" loading="eager"
:src="props.foldseekPath"></iframe>
</v-card-text>
</v-card>
</v-dialog>
</template>
<style scoped>
.cursor-pointer {
cursor: pointer
}
</style>
\ No newline at end of file
...@@ -9,6 +9,7 @@ export interface Props { ...@@ -9,6 +9,7 @@ export interface Props {
edit?: boolean edit?: boolean
navDrawer?: boolean navDrawer?: boolean
title?: string title?: string
density?: 'compact' | 'prominent'
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
...@@ -16,15 +17,31 @@ const props = withDefaults(defineProps<Props>(), { ...@@ -16,15 +17,31 @@ const props = withDefaults(defineProps<Props>(), {
toc: true, toc: true,
edit: true, edit: true,
navDrawer: true, navDrawer: true,
title: null title: "",
density: undefined
}); });
const drawer = ref(true); const drawer = ref(true);
const { page } = useContent(); const { page } = useContent();
const scrollThreshold = ref(200) const scrollThreshold = ref(200)
const density = ref<'compact' | 'prominent'>("prominent") const density = ref<'compact' | 'prominent'>("prominent")
onMounted(() => {
if (props?.density) {
density.value = props.density
} else {
density.value = "prominent"
}
})
function onScroll() { function onScroll() {
if (window.scrollY > scrollThreshold.value) {
if (props?.density) {
return props.density
}
else if (window.scrollY > scrollThreshold.value) {
density.value = "compact" density.value = "compact"
} }
else { density.value = "prominent" } else { density.value = "prominent" }
...@@ -36,9 +53,9 @@ function onScroll() { ...@@ -36,9 +53,9 @@ function onScroll() {
<v-main style="min-height: 300px"> <v-main style="min-height: 300px">
<v-container v-scroll="onScroll" :fluid="fluid"> <v-container v-scroll="onScroll" :fluid="fluid">
<v-row justify="center"> <v-row justify="center">
<v-col cols="auto"> <v-col cols="auto" class="pa-0">
<v-card flat color="transparent" :min-width="mobile ? undefined : 900" :max-width="fluid ? undefined : 1500"> <v-card flat color="transparent" :min-width="mobile ? undefined : 900" :max-width="fluid ? undefined : 1500">
<v-card-text> <v-card-text class="pa-0">
<slot /> <slot />
</v-card-text> </v-card-text>
<EditGitlab v-if="edit" /> <EditGitlab v-if="edit" />
...@@ -48,6 +65,10 @@ function onScroll() { ...@@ -48,6 +65,10 @@ function onScroll() {
</v-row> </v-row>
</v-container> </v-container>
<!-- <Footer></Footer> --> <!-- <Footer></Footer> -->
<!-- <div class="i-ph-anchor-simple-thin d-none" />
<div class="i-tabler:database d-none" />
<div class="i-mdi:book-education-outline d-none" />
<div class="i-tabler:help d-none" /> -->
</v-main> </v-main>
<NavNavbar v-model:drawer="drawer" :title="title !== null ? title : undefined" :density="density" <NavNavbar v-model:drawer="drawer" :title="title !== null ? title : undefined" :density="density"
:drawer-enabled="navDrawer" /> :drawer-enabled="navDrawer" />
......
...@@ -32,7 +32,7 @@ watchEffect(() => { ...@@ -32,7 +32,7 @@ watchEffect(() => {
const sections = ref([ const sections = ref([
{ {
id: "webservice", id: "webservice",
label: "webservice", label: "Webservice",
href: runtimeConfig.public.defenseFinderWebservice, href: runtimeConfig.public.defenseFinderWebservice,
}, },
{ id: "wiki", label: "Wiki", to: '/', }, { id: "wiki", label: "Wiki", to: '/', },
......
<script setup lang="ts"> <script setup lang="ts">
// import type { FacetDistribution } from "meilisearch"; // import type { FacetDistribution } from "meilisearch";
import { useCsvDownload } from "@/composables/useCsvDownload" import { useCsvDownload } from "@/composables/useCsvDownload"
import { useMeiliFilters } from "@/composables/useMeiliFilters"
import { useSlots } from 'vue' import { useSlots } from 'vue'
import { useDisplay } from "vuetify"; import { useDisplay } from "vuetify";
import { useThrottleFn } from '@vueuse/core' import { useThrottleFn } from '@vueuse/core'
import type { FacetInputItem, FilterItem } from '@/components/AutocompleteMeiliFacets.vue'
import { useMeiliSearch } from "#imports" import { useMeiliSearch } from "#imports"
import type { Filter } from "meilisearch";
// import { saveAs } from "file-saver"; // import { saveAs } from "file-saver";
import { useFileSystemAccess } from '@vueuse/core'
export interface SortItem { export interface SortItem {
key: string, key: string,
order: boolean | 'asc' | 'desc' order: boolean | 'asc' | 'desc'
...@@ -22,47 +23,57 @@ export interface NumericalFilter { ...@@ -22,47 +23,57 @@ export interface NumericalFilter {
export interface NumericalFilterModel extends NumericalFilter { export interface NumericalFilterModel extends NumericalFilter {
model: [number, number] model: [number, number]
} }
export interface AutocompleteMeiliFacetProps {
db: string
facets: FacetInputItem[] | undefined
facetDistribution: Record<string, Record<string, number>> | undefined
}
export interface Props { export interface Props {
title?: string title?: string
db?: string
sortBy?: SortItem[] sortBy?: SortItem[]
facets: MaybeRef<string[]> numericalFilters?: Ref<string[] | undefined>
numericalFilters?: MaybeRef<string | undefined>
dataTableServerProps: Record<string, any> dataTableServerProps: Record<string, any>
columnsToDownload?: MaybeRef<string[] | undefined> columnsToDownload?: MaybeRef<string[] | undefined>
autocompleteMeiliFacetsProps: AutocompleteMeiliFacetProps
} }
export interface FilterItem {
type: 'facet' | 'operator' | 'value' | 'text'
value: string
title: string
count?: number
deletable: boolean
props: Record<string, any>
}
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
title: '', title: '',
db: 'refseq',
columnsToDownload: undefined, columnsToDownload: undefined,
sortBy: () => [{ key: "type", order: "asc" }], sortBy: () => [{ key: "type", order: "asc" }],
numericalFilters: undefined numericalFilters: () => ref(undefined),
autocompleteMeiliFacetsProps: () => {
return {
db: 'refseq',
facetDistribution: undefined,
facets: undefined
}
}
}); });
// const facetDistribution: Ref<Record<string, Record<string, number>> | undefined> = useState(`refseqFacetDistribution`)
const slots = useSlots() const slots = useSlots()
const sortByRef = toRef(props.sortBy) const sortByRef = toRef(props.sortBy)
const facetsRef = toRef(props.facets)
const emit = defineEmits(["refresh:search"]) const emit = defineEmits(["refresh:search"])
const { search: msSearch, result: msResult } = useMeiliSearch(props.db) const { search: msSearch, result: msResult } = useMeiliSearch(props.autocompleteMeiliFacetsProps.db)
const search: Ref<string> = ref(""); const search: Ref<string> = ref("");
const filterOrSearch: Ref<FilterItem[] | null> = ref(null) const filterOrSearch: Ref<FilterItem[] | null> = ref(null)
const hitsPerPage: Ref<number> = ref(25) const hitsPerPage: Ref<number> = ref(25)
const itemsPerPage: Ref<number[]> = ref([25, 50, 100]) const itemsPerPage: Ref<number[]> = ref([25, 50, 100])
const filterError: Ref<string | null> = ref(null) const filterError: Ref<string | null> = ref(null)
const msFilter: Ref<string | undefined> = ref(undefined) // const msFilter: Ref<string | undefined> = ref(undefined)
const page = ref(1) const page = ref(1)
let loading = ref(false) let loading = ref(false)
const expanded = ref([]) const expanded = ref([])
...@@ -75,6 +86,9 @@ const computedTableHeight = computed(() => { ...@@ -75,6 +86,9 @@ const computedTableHeight = computed(() => {
const pendingDownloadData = ref(false) const pendingDownloadData = ref(false)
const toRefNumericalFilters = toRef(props.numericalFilters)
// const meiliFilters = ref<string | undefined>(undefined)
const filterInputValues = computed(() => { const filterInputValues = computed(() => {
if (filterOrSearch.value != null) { if (filterOrSearch.value != null) {
return filterOrSearch.value.filter(({ props }) => props.type !== 'text') return filterOrSearch.value.filter(({ props }) => props.type !== 'text')
...@@ -83,9 +97,6 @@ const filterInputValues = computed(() => { ...@@ -83,9 +97,6 @@ const filterInputValues = computed(() => {
} }
}) })
const isFilter = computed(() => {
return Array.isArray(filterOrSearch.value)
})
const msSortBy = computed(() => { const msSortBy = computed(() => {
if (sortByRef.value.length > 0) { if (sortByRef.value.length > 0) {
...@@ -114,26 +125,67 @@ const notPaginatedParams = computed(() => { ...@@ -114,26 +125,67 @@ const notPaginatedParams = computed(() => {
watch([paginationParams, msSortBy, page], ([newParams, newSort, newPage]) => { watch([paginationParams, msSortBy, page], ([newParams, newSort, newPage]) => {
searchOrFilter() if (toValue(isValidFilters)) {
searchOrFilter()
}
}) })
onMounted(async () => {
onBeforeMount(async () => {
searchOrFilter() searchOrFilter()
emitRefreshRes() emitRefreshRes()
}) })
const msFilterCompo = ref<FilterItem[] | undefined>(undefined)
const computedFilterStr = computed(() => {
const toValFilters = toValue(msFilterCompo)
let filtersStr: string | undefined = undefined
if (toValFilters !== undefined && toValFilters.length > 0) {
const tmpFilterItems = [...toValFilters]
if (tmpFilterItems.length % 4 === 0) {
tmpFilterItems.splice(-1)
}
filtersStr = "(" + tmpFilterItems.map((it, index) => {
const sanitizedValue = it.value.split("-").slice(0, -1).join("-")
if ((index + 1) % 4 === 3) {
return `"${sanitizedValue}"`
} else if ((index + 1) % 4 === 0) {
return ` ${sanitizedValue} `
}
else {
return `${sanitizedValue}`
}
}).join("") + ")"
}
return [filtersStr, props.numericalFilters].filter(f => f !== undefined && f !== null).join(" AND ")
const computedFilter = computed(() => {
return [toValue(msFilter), props.numericalFilters].filter(f => f !== undefined).join(" AND ")
}) })
const computedF = computed(() => toValue(props.numericalFilters))
const { arrayFilters: computedFilter } = useMeiliFilters(msFilterCompo, computedF)
// const computedFilter = computed(() => {
// const toValFilters = toValue(msFilterCompo)
// if (toValFilters !== undefined && toValFilters.length > 0) {
// meiliFilterAsArray
// }
// })
watch(computedFilter, () => { watch(computedFilter, () => {
searchOrFilter() console.log(toValue(computedFilter))
emitRefreshRes() if (toValue(isValidFilters) && (toValue(computedFilter) !== undefined || toValue(filterInputValues) === null)) {
searchOrFilter()
emitRefreshRes()
}
}) })
...@@ -147,154 +199,86 @@ const throttleSearch = useThrottleFn(async () => { ...@@ -147,154 +199,86 @@ const throttleSearch = useThrottleFn(async () => {
searchOrFilter() searchOrFilter()
emitRefreshRes() emitRefreshRes()
}, 300) }, 300)
async function searchOrFilter() {
// do something, it will be called at most 1 time per second
try { const lastFilterItem = computed(() => {
loading.value = true const toValFilterItems = toValue(msFilterCompo)
// const q = queryInputValue.value === null ? "" : queryInputValue.value if (toValFilterItems !== undefined && Array.isArray(toValFilterItems)) {
const q = search.value return toValFilterItems.slice(-1)[0]
await msSearch(q, { ...paginationParams.value, filter: toValue(computedFilter), sort: msSortBy.value }) }
} catch (error: any) { })
filterError.value = error
console.log(error) const isValidFilters = computed(() => {
const toValFilterItems = toValue(msFilterCompo)
if (toValFilterItems === undefined || Array.isArray(toValFilterItems) && toValFilterItems?.length === 0) {
return true
} }
finally { else {
loading.value = false const toValLastFilterItem = toValue(lastFilterItem)
if (toValLastFilterItem !== undefined) {
console.log(toValLastFilterItem.type)
console.log(toValLastFilterItem.type === 'value')
return toValLastFilterItem.type === 'value'
// && isAutocompleteFocused.value === false
// || (toValFilterStep === 0 && toValLastFilterItem.type === "outerOperator" && toValLastFilterItem.value.split("-")[0] === "AND")
}
}
return false
})
async function searchOrFilter() {
if (toValue(isValidFilters)) {
// do something, it will be called at most 1 time per second
try {
loading.value = true
// const q = queryInputValue.value === null ? "" : queryInputValue.value
const q = search.value
await msSearch(q, { ...paginationParams.value, filter: toValue(computedFilter), sort: msSortBy.value })
} catch (error: any) {
filterError.value = error
console.log(error)
}
finally {
loading.value = false
}
} }
} }
function emitRefreshRes() { function emitRefreshRes() {
console.log("emit refresh:search")
const q = search.value const q = search.value
emit("refresh:search", { emit("refresh:search", {
index: props.db, index: props.autocompleteMeiliFacetsProps.db,
query: q, query: q,
params: { ...notPaginatedParams.value, filter: toValue(computedFilter), sort: msSortBy.value } params: { ...notPaginatedParams.value, filter: toValue(computedFilter), sort: msSortBy.value }
}) })
} }
function clearFilterOrSearch() {
filterOrSearch.value = null
// searchOrFilter()
// emitRefreshRes()
}
// watch(msFilter, async (fos) => {
// searchOrFilter()
// emitRefreshRes()
// search.value = ''
// })
const totalHits = computed(() => { const totalHits = computed(() => {
return toValue(msResult)?.totalHits ?? toValue(msResult)?.estimatedTotalHits ?? 0 return toValue(msResult)?.totalHits ?? toValue(msResult)?.estimatedTotalHits ?? 0
}) })
watch(filterInputValues, (newSoF) => {
if (isFilter.value && filterInputValues.value !== null && filterInputValues.value?.length % 3 === 0) {
msFilter.value = filterInputValues.value.map((it, index) => {
const sanitizedValue = it.value.split("-").slice(0, -1).join("-")
if (index >= 1 && (index + 1) % 3 === 1) {
return ` AND ${sanitizedValue}`
} else if ((index + 1) % 3 === 0) {
return `"${sanitizedValue}"`
} else {
return `${sanitizedValue}`
}
}).join("")
}
})
watch(search, () => { watch(search, () => {
searchOrFilter() searchOrFilter()
emitRefreshRes() // emitRefreshRes()
}) })
const filterStep = computed(() => { // watch(msFilterCompo, () => {
return filterInputValues.value !== null && filterInputValues.value.length > 0 ? filterInputValues.value?.length % 3 : null // searchOrFilter()
})
const operatorItems = ref([
{
type: "operator", value: '=', title: "is", deletable: false, props: {
type: "operator", deletable: false
}
}, {
type: "operator", value: '!=', title: "is not", deletable: false, props: {
type: "operator",
deletable: false
}
},
])
const autocompleteItems = computed(() => {
const index = filterOrSearch.value?.length ?? 0
// console.log(index)
if (filterStep.value === null || filterStep.value === 0) {
return toValue(facetsRef).map(value => {
return {
type: "facet",
value: `${value}-${index}`,
title: value,
deletable: false,
props: {
deletable: false,
type: "facet"
}
}
})
}
if (filterStep.value === 1) {
return operatorItems.value.map(it => { return { ...it, value: `${it.value}-${index}`, } })
}
if (filterStep.value === 2) {
// get the facet value
if (Array.isArray(filterOrSearch.value)) {
const { type, value } = filterOrSearch.value?.slice(-2, -1)[0]
const sanitizedValue = value.split("-")[0]
// console.log("compute new facets")
const facetDistri = msResult.value?.facetDistribution
// console.log(facetDistri)
return facetDistri?.[sanitizedValue] ? Object.entries(facetDistri[sanitizedValue]).map(([key, val]) => {
return {
type: "value", value: `${key}-${index}`, title: key, count: val, deletable: true, props: {
type: "value", count: val, deletable: true
}
}
}) : []
}
}
})
function selectItem(item) {
filterOrSearch.value = Array.isArray(filterOrSearch.value) ? [...filterOrSearch.value, item] : [item]
}
function deleteOneFilter(index: number) {
if (isFilter.value) {
filterOrSearch.value?.splice(index - 2, 2)
}
}
function clearSearch() { // })
search.value = ""
}
async function downloadData() { async function downloadData() {
pendingDownloadData.value = true pendingDownloadData.value = true
try { try {
const { data } = await useAsyncMeiliSearch({ const { data } = await useAsyncMeiliSearch({
index: props.db, index: props.autocompleteMeiliFacetsProps.db,
params: { ...toValue(notPaginatedParams), filter: toValue(computedFilter), sort: toValue(msSortBy) }, params: { ...toValue(notPaginatedParams), filter: toValue(computedFilter), sort: toValue(msSortBy) },
query: toValue(search), query: toValue(search),
}) })
...@@ -304,84 +288,50 @@ async function downloadData() { ...@@ -304,84 +288,50 @@ async function downloadData() {
pendingDownloadData.value = false pendingDownloadData.value = false
} }
} }
function focusedOrBlur(isFocused: boolean) {
if (!isFocused) {
emitRefreshRes()
}
}
</script> </script>
<template> <template>
<v-card flat color="transparent"> <v-card flat color="transparent">
<v-card-text> <slot name="numerical-filters" :search="throttleSearch"></slot>
</v-card-text>
<v-card-text>
<slot name="numerical-filters" :search="throttleSearch"></slot>
</v-card-text>
<v-data-table-server v-if="!msError" v-model:page="page" color="primary" v-bind="dataTableServerProps" <v-data-table-server v-if="!msError" v-model:page="page" color="primary" v-bind="dataTableServerProps"
v-model:items-per-page="hitsPerPage" v-model:sortBy="sortByRef" v-model:expanded="expanded" fixed-header v-model:items-per-page="hitsPerPage" v-model:sortBy="sortByRef" v-model:expanded="expanded" fixed-header
:loading="loading" :items="msResult?.hits ?? []" :items-length="totalHits" density="compact" :loading="loading" :items="msResult?.hits ?? []" :items-length="totalHits" density="compact"
:items-per-page-options="itemsPerPage" :height="computedTableHeight" class="elevation-1 mt-2"> :items-per-page-options="itemsPerPage" :height="computedTableHeight" class="elevation-1 mt-2">
<template #top> <template #top>
<template v-if="mobile"> <v-card variant="flat" color="transparent">
<v-toolbar> <v-badge :content="totalHits" color="primary" class="mx-2"> <v-card-title>
<v-btn prepend-icon="md:download" :loading="pendingDownloadData" variant="text" color="primary"
@click="downloadData()">{{
props.title }}
</v-btn>
</v-badge></v-toolbar>
<v-toolbar><v-text-field v-model="search" label="Search..." hide-details="auto"
prepend-inner-icon="mdi-magnify" single-line clearable class="mx-2"></v-text-field></v-toolbar>
<v-toolbar><v-autocomplete ref="autocompleteInput" hide-details v-model:model-value="filterOrSearch"
auto-select-first chips clearable label="Filter results..." :items="autocompleteItems"
single-line item-value="value" item-title="title" multiple return-object
prepend-inner-icon="md:search" @click:appendInner="searchOrFilter" class="mx-2"
@click:clear="clearFilterOrSearch" @update:modelValue="() => clearSearch()">
<template #chip="{ props, item, index }">
<v-chip v-bind="props" :text="item.raw.title" :closable="item.props.deletable"
@click:close="item.props.type === deleteOneFilter(index)"></v-chip>
</template>
<template #item="{ props, item }">
<v-list-item v-bind="{ ...props, active: false, onClick: () => selectItem(item) }"
:title="item.title" :subtitle="item.raw?.count ? item.raw.count : ''"
:value="props.value">
</v-list-item>
</template>
</v-autocomplete></v-toolbar>
</template>
<template v-else>
<v-toolbar>
<v-badge :content="totalHits" color="primary" class="mr-3"> <v-badge :content="totalHits" color="primary" class="mr-3">
<v-btn prepend-icon="md:download" :loading="pendingDownloadData" variant="text" color="primary" <v-btn prepend-icon="md:download" :loading="pendingDownloadData" variant="text" color="primary"
@click="downloadData()">{{ @click="downloadData()">{{
props.title }} props.title }}
</v-btn> </v-btn>
</v-badge> </v-badge>
<v-spacer></v-spacer> </v-card-title>
<v-card variant="flat" color="transparent" :min-width="400" class="mx-2" :rounded="false"> <v-card-title>
<v-text-field v-model="search" label="Search..." hide-details="auto" <v-text-field v-model="search" label="Search..." hide-details="auto" :disabled="pendingDownloadData"
prepend-inner-icon="mdi-magnify" single-line clearable></v-text-field> prepend-inner-icon="mdi-magnify" single-line clearable
</v-card> @update:focused="focusedOrBlur"></v-text-field>
<v-card variant="flat" color="transparent" :min-width="500" class="mx-2" :rounded="false"> </v-card-title>
<v-autocomplete ref="autocompleteInput" hide-details v-model:model-value="filterOrSearch" <v-card-title>
auto-select-first chips clearable label="Filter results..." :items="autocompleteItems" <AutocompleteMeiliFacets v-model="msFilterCompo" v-bind="props.autocompleteMeiliFacetsProps"
single-line item-value="value" item-title="title" multiple return-object :is-valid-filters="isValidFilters">
prepend-inner-icon="md:search" @click:appendInner="searchOrFilter" </AutocompleteMeiliFacets>
@click:clear="clearFilterOrSearch" @update:modelValue="() => clearSearch()">
<template #chip="{ props, item, index }">
<v-chip v-bind="props" :text="item.raw.title" :closable="item.props.deletable" </v-card-title>
@click:close="item.props.type === deleteOneFilter(index)"></v-chip>
</template>
<template #item="{ props, item }">
<v-list-item v-bind="{ ...props, active: false, onClick: () => selectItem(item) }"
:title="item.title" :subtitle="item.raw?.count ? item.raw.count : ''"
:value="props.value">
</v-list-item>
</template>
</v-autocomplete>
</v-card>
</v-toolbar>
</template>
</v-card>
</template> </template>
<template v-for="(slot, index) of Object.keys(slots)" :key="index" v-slot:[slot]="data"> <template v-for="(slot, index) of Object.keys(slots)" :key="index" v-slot:[slot]="data">
......
...@@ -107,7 +107,7 @@ const pdbeMolstarComponent = ref(null) ...@@ -107,7 +107,7 @@ const pdbeMolstarComponent = ref(null)
const selectedPdb: Ref<string | null> = ref(null) const selectedPdb: Ref<string | null> = ref(null)
const structureToDownload: Ref<string | null> = ref(null) const structureToDownload: Ref<string | null> = ref(null)
const selectedPaePath = computed(() => { const selectedPaePath = computed(() => {
return selectedPdb.value ? `${selectedPdb.value.split(".").slice(0, -1).join('.')}.png` : null return selectedPdb.value ? `${selectedPdb.value.split(".").slice(0, -1).join('.')}.png` : undefined
}) })
watch(selectedPdb, (newSelectedPdb, prevSelectPdb) => { watch(selectedPdb, (newSelectedPdb, prevSelectPdb) => {
...@@ -190,10 +190,9 @@ const moleculeFormat: Ref<string> = ref("pdb") ...@@ -190,10 +190,9 @@ const moleculeFormat: Ref<string> = ref("pdb")
<v-col :cols="mobile ? 12 : 'auto'"> <v-col :cols="mobile ? 12 : 'auto'">
<v-sheet v-if="selectedPdb" <v-sheet v-if="selectedPdb"
class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4 my-3" class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4 my-3"
:height="computedHeight" :width="computedWidth" position="relative"> :height="computedHeight" :width="computedWidth" style="position:relative;">
<pdbe-molstar ref="pdbeMolstarComponent" landscape="true" hide-controls="true" <pdbe-molstar ref="pdbeMolstarComponent" :custom-data-url="selectedPdb" alphafold-view
:custom-data-url="selectedPdb" alphafold-view="true" sequence-panel="true" landscape="false" :custom-data-format="moleculeFormat"></pdbe-molstar>
:custom-data-format="moleculeFormat"></pdbe-molstar>
</v-sheet> </v-sheet>
</v-col> </v-col>
<v-col :cols="mobile ? 12 : undefined"> <v-col :cols="mobile ? 12 : undefined">
...@@ -272,4 +271,4 @@ div.msp-plugin-content.msp-layout-expanded { ...@@ -272,4 +271,4 @@ div.msp-plugin-content.msp-layout-expanded {
height: 16px; height: 16px;
width: 16px; width: 16px;
} }
</style> </style>
\ No newline at end of file
...@@ -2,36 +2,98 @@ ...@@ -2,36 +2,98 @@
import * as Plot from "@observablehq/plot"; import * as Plot from "@observablehq/plot";
import PlotFigure from "~/components/PlotFigure"; import PlotFigure from "~/components/PlotFigure";
import { useDisplay } from "vuetify"; import { useDisplay } from "vuetify";
import type { SortItem } from "@/components/ServerDbTable.vue"
import { ServerDbTable } from "#components" import { ServerDbTable } from "#components"
import { useSerialize } from "@/composables/useSerialize"; import { useSerialize } from "@/composables/useSerialize";
import { useRasterize } from "@/composables/useRasterize"; import { useRasterize } from "@/composables/useRasterize";
import { useDownloadBlob } from '@/composables/useDownloadBlob'; import { useDownloadBlob } from '@/composables/useDownloadBlob';
import type { SortItem, AutocompleteMeiliFacetProps } from "@/components/ServerDbTable.vue"
import type { ComponentPublicInstance } from 'vue' import type { ComponentPublicInstance } from 'vue'
const sortBy: Ref<SortItem[]> = ref([{ key: 'type', order: "asc" }]) const sortBy: Ref<SortItem[]> = ref([{ key: 'type', order: "asc" }])
const itemValue = ref("id"); const itemValue = ref("id");
const { width } = useDisplay(); const { width } = useDisplay();
const scaleTransform: Ref<string[]> = ref([]) const dbName = ref("refseq")
onBeforeMount(async () => {
const { data } = await useAsyncMeiliSearch({
index: toValue(dbName), query: "", params: {
facets: ["*"],
filter: [],
page: 1,
hitsPerPage: 25,
}
})
autocompleteMeiliFacetsProps.value.facetDistribution = toValue(data)?.facetDistribution
})
onMounted(async () => {
const { data } = await useAsyncMeiliSearch({
index: toValue(dbName), query: "", params: {
facets: ["*"],
filter: [],
page: 1,
hitsPerPage: 25,
}
})
autocompleteMeiliFacetsProps.value.facetDistribution = toValue(data)?.facetDistribution
})
const { serialize } = useSerialize() const { serialize } = useSerialize()
const { rasterize } = useRasterize() const { rasterize } = useRasterize()
const { download } = useDownloadBlob() const { download } = useDownloadBlob()
const facets = ref([ const autocompleteMeiliFacetsProps = ref<AutocompleteMeiliFacetProps>({
"replicon", db: toValue(dbName),
"type", facets: [
"subtype", { title: "Defense System", type: "subheader" },
"Superkingdom", { title: "System", value: "type", type: "facet", icon: "i-tabler:virus-off", },
"phylum", { title: "Subsystem", value: "subtype", type: "facet", icon: "i-tabler:virus-off" },
"order", { type: "divider" },
"family", { title: "Taxonomy", type: "subheader" },
"genus", { title: "Superkingdom", value: "Superkingdom", type: "facet", icon: "i-tabler:binary-tree" },
"species", { title: "Phylum", value: "phylum", type: "facet", icon: "i-tabler:binary-tree" },
]) { title: "Order", value: "order", type: "facet", icon: "i-tabler:binary-tree" },
{ title: "Family", value: "family", type: "facet", icon: "i-tabler:binary-tree" },
{ title: "Genus", value: "genus", type: "facet", icon: "i-tabler:binary-tree" },
{ title: "Species", value: "species", type: "facet", icon: "i-tabler:binary-tree" },
{ type: "divider" },
{ title: "Replicon", value: "replicon", type: "facet", icon: "mdi-dna", },
],
facetDistribution: undefined
})
const computedAutocompleteMeiliFacetsProps = computed(() => {
const toValFacetDistribution = toValue(autocompleteMeiliFacetsProps).facetDistribution
const toValFacets = toValue(autocompleteMeiliFacetsProps).facets
if (toValFacetDistribution !== undefined && toValFacets !== undefined) {
return {
...toValue(autocompleteMeiliFacetsProps), facets: toValFacets.map(facet => {
if (facet.type === "facet") {
const count = toValFacetDistribution?.[facet.value] ? Object.keys(toValFacetDistribution[facet.value]).length : undefined
return count ? { ...facet, count } : { ...facet }
}
else {
return { ...facet }
}
})
}
}
else {
return toValue(autocompleteMeiliFacetsProps)
}
})
const availableTaxo: Ref<string[]> = ref([ const availableTaxo: Ref<string[]> = ref([
"species", "species",
"genus", "genus",
...@@ -42,16 +104,16 @@ const availableTaxo: Ref<string[]> = ref([ ...@@ -42,16 +104,16 @@ const availableTaxo: Ref<string[]> = ref([
]); ]);
const scaleTypes = ref<string[]>(['linear', 'sqrt', 'log', 'symlog']) const scaleTypes = ref<string[]>(['linear', 'sqrt', 'log', 'symlog'])
const selectedTaxoRank = ref("phylum"); const selectedTaxoRank = ref("Superkingdom");
const headers = ref([ const headers = ref([
{ title: "Replicon", key: "replicon" }, { title: "Replicon", key: "replicon" },
{ {
title: "Type", title: "System",
key: "type", key: "type",
}, },
{ {
title: "Subtype", title: "Subsystem",
key: "subtype", key: "subtype",
}, },
{ {
...@@ -72,7 +134,7 @@ const computedHeaders = computed(() => { ...@@ -72,7 +134,7 @@ const computedHeaders = computed(() => {
} }
})] })]
}) })
const { result: msResult } = useMeiliSearch('refseq') const { result: msResult } = useMeiliSearch(toValue(dbName))
const computedWidth = computed(() => { const computedWidth = computed(() => {
const currentWidth = fullWidth.value ? width.value : width.value / 2 const currentWidth = fullWidth.value ? width.value : width.value / 2
...@@ -82,20 +144,11 @@ const computedWidth = computed(() => { ...@@ -82,20 +144,11 @@ const computedWidth = computed(() => {
const allHits: Ref<Record<string, any> | undefined> = ref(undefined) const allHits: Ref<Record<string, any> | undefined> = ref(undefined)
// onMounted(async () => {
// console.log("on mounted get all hits")
// const params = {
// facets: ["*"],
// filter: undefined,
// sort: ["type:asc"],
// limit: 500000
// }
// getAllHits({ index: "refseq", params, query: "" })
// })
const pendingAllHits = ref(false) const pendingAllHits = ref(false)
async function getAllHits(params: { index: string, params: Record<string, any>, query: string }) { async function getAllHits(params: { index: string, params: Record<string, any>, query: string }) {
console.log(params.index) console.log(params.index)
if (params.index === 'refseq') { if (params.index === toValue(dbName)) {
console.log("get all hits in function ") console.log("get all hits in function ")
console.log(params) console.log(params)
pendingAllHits.value = true pendingAllHits.value = true
...@@ -112,10 +165,6 @@ async function getAllHits(params: { index: string, params: Record<string, any>, ...@@ -112,10 +165,6 @@ async function getAllHits(params: { index: string, params: Record<string, any>,
} }
const plotHeight = computed(() => {
return computedWidth.value / 3;
// return 500
});
const defaultDataTableServerProps = ref({ const defaultDataTableServerProps = ref({
showExpand: false showExpand: false
...@@ -222,7 +271,7 @@ function namesToAccessionChips(names: string[]) { ...@@ -222,7 +271,7 @@ function namesToAccessionChips(names: string[]) {
return namesToCollapsibleChips(names).map(it => { return namesToCollapsibleChips(names).map(it => {
return { return {
...it, ...it,
// href: new URL(it.title, "http://toto.pasteur.cloud").toString() href: new URL(it.title, "https://www.ncbi.nlm.nih.gov/protein/").toString()
} }
}) })
} }
...@@ -235,7 +284,7 @@ const binPlotOptions = ref({ ...@@ -235,7 +284,7 @@ const binPlotOptions = ref({
grid: true, grid: true,
x: { tickRotate: 90, tip: true, label: "Systems" }, x: { tickRotate: 90, tip: true, label: "Systems" },
// y: { tickFormat: 's' }, // y: { tickFormat: 's' },
color: { scheme: "turbo", legend: true }, color: { scheme: "plasma", legend: true },
}) })
const binPlotDataOptions = computed(() => { const binPlotDataOptions = computed(() => {
...@@ -249,10 +298,7 @@ const binPlotDataOptions = computed(() => { ...@@ -249,10 +298,7 @@ const binPlotDataOptions = computed(() => {
type: scaleType.value, type: scaleType.value,
tickFormat: '~s', tickFormat: '~s',
ticks: scaleType.value === 'symlog' ? 3 : 5, ticks: scaleType.value === 'symlog' ? 3 : 5,
// width: 350
}, },
// fy: { domain: groupSortDomain.value },
marks: [ marks: [
Plot.cell(toValueAllHits?.hits ?? [], Plot.group({ fill: "count" }, { x: "type", y: selectedTaxoRank.value, tip: true, inset: 0.5, sort: { y: "fill" } })), Plot.cell(toValueAllHits?.hits ?? [], Plot.group({ fill: "count" }, { x: "type", y: selectedTaxoRank.value, tip: true, inset: 0.5, sort: { y: "fill" } })),
] ]
...@@ -419,13 +465,25 @@ async function downloadPng(component: ComponentPublicInstance | null, filename: ...@@ -419,13 +465,25 @@ async function downloadPng(component: ComponentPublicInstance | null, filename:
</v-expansion-panel> </v-expansion-panel>
</v-expansion-panels> </v-expansion-panels>
<ServerDbTable title="RefSeq" db="refseq" :sortBy="sortBy" :facets="facets" <ServerDbTable title="RefSeq" :sortBy="sortBy"
:autocomplete-meili-facets-props="computedAutocompleteMeiliFacetsProps"
:data-table-server-props="dataTableServerProps" @refresh:search="(params) => getAllHits(params)"> :data-table-server-props="dataTableServerProps" @refresh:search="(params) => getAllHits(params)">
<template #[`item.accession_in_sys`]="{ item }"> <template #[`item.accession_in_sys`]="{ item }">
<CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)"> <CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)">
</CollapsibleChips> </CollapsibleChips>
</template> </template>
<template #[`item.type`]="{ item }">
<v-chip color="info" link size="small" :to="`/defense-systems/${item.type.toLowerCase()}`" target="_blank"> {{
item.type }}
</v-chip>
</template>
<template #[`item.subtype`]="{ item }">
<v-chip color="info" link size="small" :to="`/defense-systems/${item.type.toLowerCase()}`" target="_blank"> {{
item.subtype }}
</v-chip>
</template>
</ServerDbTable> </ServerDbTable>
......
<script setup lang="ts"> <script setup lang="ts">
import * as Plot from "@observablehq/plot"; import type { SortItem, AutocompleteMeiliFacetProps } from "@/components/ServerDbTable.vue"
import PlotFigure from "~/components/PlotFigure";
import type { SortItem } from "@/components/ServerDbTable.vue"
import { useNumericalFilter } from "@/composables/useNumericalfilter" import { useNumericalFilter } from "@/composables/useNumericalfilter"
import { useRefinedUrl } from "@/composables/useRefinedUrl"
import { ServerDbTable } from "#components"
import { withQuery, joinURL, withTrailingSlash } from 'ufo'
import { ServerDbTable } from "#components"
interface Item {
Foldseek_name: string
System_name_ok: string
}
const sortBy: Ref<SortItem[]> = ref([{ key: 'System', order: "asc" }]) const sortBy: Ref<SortItem[]> = ref([{ key: 'System', order: "asc" }])
const itemValue = ref("id"); const itemValue = ref("id");
const facets: Ref<string[]> = ref(["System", "subtype", "gene_name", "completed", "prediction_type", "proteins_in_the_prediction", "system_genes", const dbName = ref("structure")
])
onBeforeMount(async () => {
console.log("dans le mounted refseq")
const { data } = await useAsyncMeiliSearch({
index: toValue(dbName), query: "", params: {
facets: ["*"],
filter: [],
page: 1,
hitsPerPage: 25,
}
})
autocompleteMeiliFacetsProps.value.facetDistribution = toValue(data)?.facetDistribution
})
const autocompleteMeiliFacetsProps = ref<AutocompleteMeiliFacetProps>({
db: toValue(dbName),
facets: [
{ title: "Defense System", type: "subheader" },
{ title: "System", value: "System", type: "facet", icon: "i-tabler:virus-off", },
{ title: "Subsystem", value: "subtype", type: "facet", icon: "i-tabler:virus-off" },
{ type: "divider" },
{ title: "Gene name", value: "gene_name", type: "facet", icon: "mdi-dna" },
{ title: "Completed", value: "completed", type: "facet", icon: "md:done" },
{ title: "Prediction type", value: "prediction_type", type: "facet", icon: "i-gravity-ui:molecule" },
],
facetDistribution: undefined
})
const computedAutocompleteMeiliFacetsProps = computed(() => {
const toValFacetDistribution = toValue(autocompleteMeiliFacetsProps).facetDistribution
const toValFacets = toValue(autocompleteMeiliFacetsProps).facets
if (toValFacetDistribution !== undefined && toValFacets !== undefined) {
return {
...toValue(autocompleteMeiliFacetsProps), facets: toValFacets.map(facet => {
if (facet.type === "facet") {
const count = toValFacetDistribution?.[facet.value] ? Object.keys(toValFacetDistribution[facet.value]).length : undefined
return count ? { ...facet, count } : { ...facet }
}
else {
return { ...facet }
}
})
}
}
else {
return toValue(autocompleteMeiliFacetsProps)
}
})
const headers: Ref<Object[]> = ref([ const headers: Ref<Object[]> = ref([
{ title: 'Structure', key: 'structure', sortable: false, removable: false }, { title: 'Structure', key: 'structure', sortable: false, removable: false },
{ title: 'Foldseek', key: 'Foldseek_name', sortable: false },
{ title: "System", key: "System", removable: false }, { title: "System", key: "System", removable: false },
{ title: "Gene name", key: "gene_name", removable: false }, { title: "Gene name", key: "gene_name", removable: false },
{ title: "Subtype", key: "subtype", removable: false }, { title: "Subtype", key: "subtype", removable: false },
...@@ -31,14 +93,19 @@ const headers: Ref<Object[]> = ref([ ...@@ -31,14 +93,19 @@ const headers: Ref<Object[]> = ref([
]) ])
const { search: msSearch, result: msResult } = useMeiliSearch('structure')
const { range: plddtsRange, stringifyFilter: plddtsFilter, reset: plddtsReset } = useNumericalFilter("plddts", 0, 100) const { range: plddtsRange, stringifyFilter: plddtsFilter, reset: plddtsReset } = useNumericalFilter("plddts", 0, 100)
const { range: iptmRange, stringifyFilter: iptmFilter, reset: iptmReset } = useNumericalFilter("iptm+ptm", 0, 1) const { range: iptmRange, stringifyFilter: iptmFilter, reset: iptmReset } = useNumericalFilter("iptm+ptm", 0, 1)
const { range: pdockqRange, stringifyFilter: pdockqFilter, reset: pdockqReset } = useNumericalFilter("pDockQ", 0, 1) const { range: pdockqRange, stringifyFilter: pdockqFilter, reset: pdockqReset } = useNumericalFilter("pDockQ", 0, 1)
function isString(item: Ref<string | undefined>): item is Ref<string> {
return toValue(item) !== undefined
}
const numericalFilters = computed(() => { const numericalFilters = computed(() => {
const listFilters = [plddtsFilter, iptmFilter, pdockqFilter].map(f => toValue(f)).filter(f => f !== undefined) const listFilters = [plddtsFilter, iptmFilter, pdockqFilter].filter(isString).map(f => toValue(f))
return listFilters.length > 0 ? listFilters.join(" AND ") : undefined return listFilters.length > 0 ? listFilters : undefined
}) })
const defaultDataTableServerProps = ref({ const defaultDataTableServerProps = ref({
...@@ -55,6 +122,12 @@ const dataTableServerProps = computed(() => { ...@@ -55,6 +122,12 @@ const dataTableServerProps = computed(() => {
function toFolseekUrl(item: Item) {
const url = joinURL("/" + item.System_name_ok, item.Foldseek_name)
const { refinedUrl } = useRefinedUrl(url)
console.log(toValue(refinedUrl))
return toValue(refinedUrl)
}
function namesToCollapsibleChips(names: string[], systemDir: string, file: string | null = null) { function namesToCollapsibleChips(names: string[], systemDir: string, file: string | null = null) {
...@@ -70,63 +143,64 @@ function pdbNameToCif(pdbPath: string) { ...@@ -70,63 +143,64 @@ function pdbNameToCif(pdbPath: string) {
return `${cifPath}.cif` return `${cifPath}.cif`
} }
function toSystemName(rawName: string) {
// Does it work if it's a list of system genes ?
// split on __ for systeme_vgenes
return rawName.split("__")[0].toLocaleLowerCase()
}
const plddtDistribution = computed(() => {
if (toValue(msResult)?.facetDistribution?.plddts) {
return Object.entries(toValue(msResult).facetDistribution.plddts).map(([key, value]) => { })
}
})
</script> </script>
<template> <template>
<ServerDbTable title="Predicted Structures" db="structure" :sortBy="sortBy" :facets="facets" <ServerDbTable title="Predicted Structures" :sortBy="sortBy" :data-table-server-props="dataTableServerProps"
:data-table-server-props="dataTableServerProps" :numerical-filters="numericalFilters"> :autocomplete-meili-facets-props="computedAutocompleteMeiliFacetsProps"
:numerical-filters="toRef(numericalFilters)">
<template #numerical-filters="{ search }"> <template #numerical-filters="{ search }">
<v-row> <v-list>
<v-col cols="12" md="12" lg="4"> <v-list-item>
<v-range-slider v-model="plddtsRange" strict density="compact" hide-details="auto" label="pLDDT" <v-list-item-title class="text-subtitle-1 text-medium-emphasis">
step="0.5" :min="0" :max="100" thumb-label="always" @update:modelValue="search()"> pLDDT
<template #append> </v-list-item-title>
<v-btn variant="text" icon="md:restart_alt" @click="plddtsReset()"></v-btn> <v-row>
</template> <v-col class="pt-8 pl-8" :lg="8">
</v-range-slider> <v-range-slider v-model="plddtsRange" strict density="compact" hide-details="auto" step="0.5"
</v-col> :min="0" :max="100" thumb-label="always" @update:modelValue="search()">
<v-col cols="12" md="12" lg="4"> <template #append>
<v-btn variant="text" icon="md:restart_alt" @click="plddtsReset()"></v-btn>
<v-range-slider v-model="iptmRange" strict density="compact" hide-details="auto" label="iptm+ptm" </template>
step="0.1" :min="0" :max="1" thumb-label="always" @update:modelValue="search()">
<template #append> </v-range-slider>
<v-btn variant="text" icon="md:restart_alt" @click="iptmReset()"></v-btn> </v-col></v-row>
</template> </v-list-item>
</v-range-slider> <v-list-item>
</v-col> <v-list-item-title class="text-subtitle-1 text-medium-emphasis">
<!-- pdockqReset --> iptm+ptm
<v-col cols="12" md="12" lg="4"> </v-list-item-title>
<v-range-slider v-model="pdockqRange" strict density="compact" hide-details="auto" label="pDockQ" <v-row>
step="0.1" :min="0" :max="1" thumb-label="always" @update:modelValue="search()"> <v-col class="pt-8 pl-8" :lg="8">
<template #append> <v-range-slider v-model="iptmRange" strict density="compact" hide-details="auto" step="0.1"
<v-btn variant="text" icon="md:restart_alt" @click="pdockqReset()"></v-btn> :min="0" :max="1" thumb-label="always" @update:modelValue="search()">
</template> <template #append>
</v-range-slider> <v-btn variant="text" icon="md:restart_alt" @click="iptmReset()"></v-btn>
</v-col> </template>
</v-row> </v-range-slider>
</v-col></v-row>
</v-list-item>
<v-list-item>
<v-list-item-title class="text-subtitle-1 text-medium-emphasis">pDockQ</v-list-item-title>
<v-row>
<v-col class="pt-8 pl-8" :lg="8">
<v-range-slider v-model="pdockqRange" density="compact" strict hide-details="auto" step="0.1"
:min="0" :max="1" thumb-label="always" @update:modelValue="search()">
<template #append>
<v-btn variant="text" icon="md:restart_alt" @click="pdockqReset()"></v-btn>
</template>
</v-range-slider>
</v-col></v-row>
</v-list-item>
</v-list>
</template>
<template #[`item.Foldseek_name`]="{ item }">
<FoldseekDialog v-if="item.Foldseek_name !== 'na'" :foldseek-path="toFolseekUrl(item)"></FoldseekDialog>
<!-- <NuxtLink v-if="item.Foldseek_name !== 'na'" :to="toFolseekUrl(item)" :external="false">
<v-avatar>
<v-img src="~/assets/foldseek.png" alt="Foldseek results"></v-img>
</v-avatar>
</NuxtLink> -->
</template> </template>
<template #[`item.proteins_in_the_prediction`]="{ item }"> <template #[`item.proteins_in_the_prediction`]="{ item }">
<CollapsibleChips <CollapsibleChips
:items="namesToCollapsibleChips(item.proteins_in_the_prediction, item.System_name_ok, item.fasta_file)"> :items="namesToCollapsibleChips(item.proteins_in_the_prediction, item.System_name_ok, item.fasta_file)">
......
<script setup lang="ts"> <script setup lang="ts">
import type { SortItem } from "@/components/ServerDbTable.vue" import type { SortItem, AutocompleteMeiliFacetProps } from "@/components/ServerDbTable.vue"
import { ServerDbTable } from "#components" import { ServerDbTable } from "#components"
const sortBy: Ref<SortItem[]> = ref([{ key: 'title', order: "asc" }]) const sortBy: Ref<SortItem[]> = ref([{ key: 'title', order: "asc" }])
const itemValue = ref("title"); const itemValue = ref("title");
const facets: Ref<string[]> = ref(["title", "Sensor", "Effector", "Activator", "PFAM.AC", "PFAM.DE"]) const dbName = ref("systems")
onBeforeMount(async () => {
const { data } = await useAsyncMeiliSearch({
index: toValue(dbName), query: "", params: {
facets: ["*"],
filter: [],
page: 1,
hitsPerPage: 25,
}
})
autocompleteMeiliFacetsProps.value.facetDistribution = toValue(data)?.facetDistribution
})
const autocompleteMeiliFacetsProps = ref<AutocompleteMeiliFacetProps>({
db: toValue(dbName),
facets: [
{ title: "Defense System", type: "subheader" },
{ title: 'System', value: "title", type: "facet", icon: "i-tabler:virus-off" },
{ type: "divider" },
{ title: "Mechanism", type: "subheader" },
{ title: 'Sensor', value: "Sensor", type: "facet", icon: "i-tabler:shield-cog" },
{ title: 'Effector', value: "Effector", type: "facet", icon: "i-tabler:shield-cog" },
{ title: 'Activator', value: "Activator", type: "facet", icon: "i-tabler:shield-cog" },
{ type: "divider" },
{ title: "PFAM", type: "subheader" },
{ title: 'Acession', value: "PFAM.AC", type: "facet", icon: "i-tabler:circle-key" },
{ title: 'Description', value: "PFAM.DE", type: "facet", icon: "i-tabler:file-description" },
{ type: "divider" },
{ title: 'Contributor', value: "contributors", type: "facet", icon: "i-tabler:user-heart" },
],
facetDistribution: undefined
})
const computedAutocompleteMeiliFacetsProps = computed(() => {
const toValFacetDistribution = toValue(autocompleteMeiliFacetsProps).facetDistribution
const toValFacets = toValue(autocompleteMeiliFacetsProps).facets
if (toValFacetDistribution !== undefined && toValFacets !== undefined) {
return {
...toValue(autocompleteMeiliFacetsProps), facets: toValFacets.map(facet => {
if (facet.type === "facet") {
const count = toValFacetDistribution?.[facet.value] ? Object.keys(toValFacetDistribution[facet.value]).length : undefined
return count ? { ...facet, count } : { ...facet }
}
else {
return { ...facet }
}
})
}
}
else {
return toValue(autocompleteMeiliFacetsProps)
}
})
const headers: Ref<Object[]> = ref([ const headers: Ref<Object[]> = ref([
{ title: "System", key: "title", removable: false }, { title: "System", key: "title", removable: false },
{ title: "Article", key: "doi", removable: false }, { title: "Article", key: "doi", removable: false },
...@@ -16,7 +80,6 @@ const headers: Ref<Object[]> = ref([ ...@@ -16,7 +80,6 @@ const headers: Ref<Object[]> = ref([
]) ])
const { search: msSearch, result: msResult } = useMeiliSearch('systems')
const defaultDataTableServerProps = ref({ const defaultDataTableServerProps = ref({
showExpand: false showExpand: false
...@@ -34,7 +97,8 @@ const columnsToDownload = ref(['title', 'doi', 'Sensor', 'Activator', 'Effector' ...@@ -34,7 +97,8 @@ const columnsToDownload = ref(['title', 'doi', 'Sensor', 'Activator', 'Effector'
</script> </script>
<template> <template>
<ServerDbTable title="List Systems" db="systems" :sortBy="sortBy" :facets="facets" <ServerDbTable title="List Systems" :db="dbName" :sortBy="sortBy"
:autocomplete-meili-facets-props="computedAutocompleteMeiliFacetsProps"
:data-table-server-props="dataTableServerProps" :columns-to-download="columnsToDownload"> :data-table-server-props="dataTableServerProps" :columns-to-download="columnsToDownload">
...@@ -48,8 +112,11 @@ const columnsToDownload = ref(['title', 'doi', 'Sensor', 'Activator', 'Effector' ...@@ -48,8 +112,11 @@ const columnsToDownload = ref(['title', 'doi', 'Sensor', 'Activator', 'Effector'
<ArticleDoi v-if="item?.doi" :doi="item.doi" :abstract="item?.abstract" :divider="false" :enumerate="false" /> <ArticleDoi v-if="item?.doi" :doi="item.doi" :abstract="item?.abstract" :divider="false" :enumerate="false" />
</template> </template>
<template #[`item.PFAM`]="{ item }"> <template #[`item.PFAM`]="{ item }">
<pfam-chips v-if="item?.PFAM" :pfams="item.PFAM"></pfam-chips> <pfam-chips v-if="item?.PFAM" :pfams="item.PFAM"></pfam-chips>
</template> </template>
<template #[`item.contributors`]="{ item }">
<CollapsibleChips v-if="item?.contributors" :items="item.contributors.map(it => ({ title: it }))">
</CollapsibleChips>
</template>
</ServerDbTable> </ServerDbTable>
</template> </template>
\ No newline at end of file
export interface FilterItem {
type: 'facet' | 'operator' | 'value' | 'text'
value: string
title: string
count?: number
deletable: boolean
props: Record<string, any>
}
export function useFacetFilters(inputFilters: MaybeRef<FilterItem[] | undefined>) {
}
\ No newline at end of file
...@@ -17,8 +17,8 @@ export async function useFetchMsDocument( ...@@ -17,8 +17,8 @@ export async function useFetchMsDocument(
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const client = new MeiliSearch({ const client = new MeiliSearch({
host: runtimeConfig.public.meiliHost, host: runtimeConfig.public.meilisearchClient.hostUrl,
apiKey: runtimeConfig.public.meiliApiKey apiKey: runtimeConfig.public.meilisearchClient.searchApiKey
}) })
const pending = ref(false) const pending = ref(false)
const filterError: Ref<string | null> = ref(null) const filterError: Ref<string | null> = ref(null)
......
import type { FilterItem } from '@/components/AutocompleteMeiliFacets.vue'
import { filter } from '@observablehq/plot'
import type { Filter } from "meilisearch"
export function useMeiliFilters(filters: MaybeRef<FilterItem[] | undefined>, numericalFilters: Ref<string[] | undefined>) {
const arrayFilters = ref<Filter | undefined>(undefined)
const nf = toRef(numericalFilters)
watch(nf, () => {
console.log("mon nf", nf)
})
watch(numericalFilters,() => {
console.log("je watch numerical filter", numericalFilters)
})
watchEffect(() => {
console.log("dans le computed de useMeilisFilter")
const toValFilters = toValue(filters)
const toValNumericalFilter = toValue(nf)
console.log(toValNumericalFilter)
let categoricalFilters: FilterItem[] | undefined = undefined
if (toValFilters !== undefined && toValFilters.length >= 3) {
const cachedFilters = [...toValFilters]
// Remove last element if it is an outerOperator
if (cachedFilters.length % 4 === 0 && cachedFilters.slice(-1)[0].type === 'outerOperator') {
cachedFilters.splice(-1)
}
let previousOperator: 'AND' | 'OR' | undefined = undefined
const arrayFilters = cachedFilters.reduce<FilterItem[]>((acc, curr, index) => {
const sanitizedValue = curr.value.split("-").slice(0, -1).join("-")
const position = index + 1
switch (position % 4) {
case 0:
// if this is the first time pass by an outeroperator
if (previousOperator === undefined) {
const newFilter = acc.splice(-3).join("")
console.log("newFilter: ", newFilter)
if (sanitizedValue === 'OR') {
acc = [[newFilter]]
}
else {
acc.push(newFilter)
}
}
previousOperator = sanitizedValue
break;
case 3:
acc.push(`'${sanitizedValue}'`)
const newFilter = acc.splice(-3).join("")
if (previousOperator === 'AND') {
acc.push(newFilter)
}
else {
const previousElem = acc.slice(-1)[0]
if (Array.isArray(previousElem)) {
acc.slice(-1)[0].push(newFilter)
}
else if (previousElem !== undefined) {
const previousElem = acc.splice(-1)[0]
console.log("previousElem: ", previousElem)
console.log("newFilter: ", newFilter)
acc.push([previousElem, newFilter])
}
else {
acc.push(newFilter)
}
}
break
default:
acc.push(sanitizedValue)
break;
}
return acc
}, [])
categoricalFilters = arrayFilters.length > 0 ? arrayFilters : undefined
}
function isFilterItem(item: FilterItem[] | string[] | undefined): item is FilterItem[] | string[] {
return item !== undefined
}
console.log(categoricalFilters)
const filterItemArray = [categoricalFilters, toValNumericalFilter].filter(isFilterItem).flat()
console.log(filterItemArray)
// const returnArr = filterItemArray.reduce<FilterItem | string | undefined>((acc, curr) => {
// const toValCurr = toValue(curr)
// if (toValCurr === undefined) {
// return acc
// }
// else {
// if (acc !== undefined) {
// return [...acc, ...toValCurr]
// }
// else { return [...toValCurr] }
// }
// }, undefined)
// console.log(returnArr)
// return returnArr
arrayFilters.value = filterItemArray
})
// const arrayFilters: ComputedRef<Filter | undefined> = computed(() => {
// })
return { arrayFilters }
}
...@@ -6,17 +6,11 @@ export function useNumericalFilter( ...@@ -6,17 +6,11 @@ export function useNumericalFilter(
const range: Ref<[number, number]> = ref([toValue(min), toValue(max)]) const range: Ref<[number, number]> = ref([toValue(min), toValue(max)])
const stringifyFilter: Ref<string | undefined> = ref(undefined)
const stringifyFilter: Ref<string | undefined> = ref(`${toValue(id)} ${toValue(min)} TO ${toValue(max)}`)
watchEffect(() => { watchEffect(() => {
console.log("watch reange")
console.log(range.value)
if (range.value[0] === toValue(min) && range.value[1] === toValue(max)) { if (range.value[0] === toValue(min) && range.value[1] === toValue(max)) {
stringifyFilter.value = undefined stringifyFilter.value = undefined
} else { } else {
......
--- ---
title: Contributing to the Wiki title: Contributing to the Wiki
layout: article layout: article
navigation:
--- ---
# Contributing to the Wiki # Contributing to the Wiki
......