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/**/*.csv filter=lfs diff=lfs merge=lfs -text
data/**/*.tsv filter=lfs diff=lfs merge=lfs -text
......@@ -319,7 +319,7 @@ get-zotero:
variables:
CONTEXT: "."
DOCKERFILE: "Dockerfile"
BASE_URL: /wiki/
BASE_URL: /
MEILI_HOST: "http://localhost:7700"
before_script:
- *docker-login
......@@ -328,7 +328,7 @@ get-zotero:
docker buildx build --pull -t "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA"
--build-arg "BASE_URL=$BASE_URL"
--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
- docker push "$CI_REGISTRY_IMAGE/$IMAGE_NAME:$CI_COMMIT_SHORT_SHA"
......@@ -341,6 +341,7 @@ build:dev:wiki:
# - get-pfam
variables:
BASE_URL: /wiki/
HOST_URL: https://${HOST_DEV}
before_script:
- *docker-login
# - "sed -i 's/MEILISEARCH_API_KEY/${$MEILI_API_KEY}/g' nuxt.config.ts"
......@@ -356,6 +357,8 @@ build:prod:wiki:
# - get-pfam
variables:
BASE_URL: /wiki/
HOST_URL: https://${HOST_PROD}
rules:
- if: $CI_COMMIT_BRANCH == "main"
......@@ -486,7 +489,7 @@ build:prod:wiki:
deploy:dev:
extends: .deploy
rules:
- if: $CI_COMMIT_BRANCH == "dev"
- if: $CI_COMMIT_BRANCH == "dev" || $CI_COMMIT_BRANCH == "foldseek-pages"
needs:
- "build:dev:wiki"
when: manual
......
......@@ -15,14 +15,14 @@ FROM node:21.1-bookworm-slim as dev
ARG BASE_URL=/
ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY=api_key
ARG HOST_URL
ENV NUXT_APP_BASE_URL=${BASE_URL}
# 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_MEILI_HOST=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
ENV NUXT_PUBLIC_HOST_URL=${HOST_URL}
# ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
# ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
WORKDIR /usr/src/app
......@@ -43,10 +43,21 @@ RUN npm run build
### STAGE: 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
COPY --from=build /usr/src/app/.output ./
ENV NUXT_APP_BASE_URL=${BASE_URL}
# 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"]
......@@ -55,6 +66,7 @@ FROM node:21.1-bookworm-slim as generate
ARG BASE_URL=/
ARG MEILI_HOST=http://localhost:7700
ARG MEILI_API_KEY
ARG HOST_URL
ENV NODE_OPTIONS=--max_old_space_size=12288
ENV NUXT_APP_BASE_URL=${BASE_URL}
......@@ -62,9 +74,11 @@ ENV NUXT_APP_BASE_URL=${BASE_URL}
# 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}
ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
# ENV NUXT_PUBLIC_MEILI_HOST=${MEILI_HOST}
# ENV NUXT_PUBLIC_MEILI_API_KEY=${MEILI_API_KEY}
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>
<v-row>
<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-col>
</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 {
edit?: boolean
navDrawer?: boolean
title?: string
density?: 'compact' | 'prominent'
}
const props = withDefaults(defineProps<Props>(), {
......@@ -16,15 +17,31 @@ const props = withDefaults(defineProps<Props>(), {
toc: true,
edit: true,
navDrawer: true,
title: null
title: "",
density: undefined
});
const drawer = ref(true);
const { page } = useContent();
const scrollThreshold = ref(200)
const density = ref<'compact' | 'prominent'>("prominent")
onMounted(() => {
if (props?.density) {
density.value = props.density
} else {
density.value = "prominent"
}
})
function onScroll() {
if (window.scrollY > scrollThreshold.value) {
if (props?.density) {
return props.density
}
else if (window.scrollY > scrollThreshold.value) {
density.value = "compact"
}
else { density.value = "prominent" }
......@@ -36,9 +53,9 @@ function onScroll() {
<v-main style="min-height: 300px">
<v-container v-scroll="onScroll" :fluid="fluid">
<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-text>
<v-card-text class="pa-0">
<slot />
</v-card-text>
<EditGitlab v-if="edit" />
......@@ -48,6 +65,10 @@ function onScroll() {
</v-row>
</v-container>
<!-- <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>
<NavNavbar v-model:drawer="drawer" :title="title !== null ? title : undefined" :density="density"
:drawer-enabled="navDrawer" />
......
......@@ -32,7 +32,7 @@ watchEffect(() => {
const sections = ref([
{
id: "webservice",
label: "webservice",
label: "Webservice",
href: runtimeConfig.public.defenseFinderWebservice,
},
{ id: "wiki", label: "Wiki", to: '/', },
......
<script setup lang="ts">
// import type { FacetDistribution } from "meilisearch";
import { useCsvDownload } from "@/composables/useCsvDownload"
import { useMeiliFilters } from "@/composables/useMeiliFilters"
import { useSlots } from 'vue'
import { useDisplay } from "vuetify";
import { useThrottleFn } from '@vueuse/core'
import type { FacetInputItem, FilterItem } from '@/components/AutocompleteMeiliFacets.vue'
import { useMeiliSearch } from "#imports"
import type { Filter } from "meilisearch";
// import { saveAs } from "file-saver";
import { useFileSystemAccess } from '@vueuse/core'
export interface SortItem {
key: string,
order: boolean | 'asc' | 'desc'
......@@ -22,47 +23,57 @@ export interface NumericalFilter {
export interface NumericalFilterModel extends NumericalFilter {
model: [number, number]
}
export interface AutocompleteMeiliFacetProps {
db: string
facets: FacetInputItem[] | undefined
facetDistribution: Record<string, Record<string, number>> | undefined
}
export interface Props {
title?: string
db?: string
sortBy?: SortItem[]
facets: MaybeRef<string[]>
numericalFilters?: MaybeRef<string | undefined>
numericalFilters?: Ref<string[] | undefined>
dataTableServerProps: Record<string, any>
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>(), {
title: '',
db: 'refseq',
columnsToDownload: undefined,
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 sortByRef = toRef(props.sortBy)
const facetsRef = toRef(props.facets)
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 filterOrSearch: Ref<FilterItem[] | null> = ref(null)
const hitsPerPage: Ref<number> = ref(25)
const itemsPerPage: Ref<number[]> = ref([25, 50, 100])
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)
let loading = ref(false)
const expanded = ref([])
......@@ -75,6 +86,9 @@ const computedTableHeight = computed(() => {
const pendingDownloadData = ref(false)
const toRefNumericalFilters = toRef(props.numericalFilters)
// const meiliFilters = ref<string | undefined>(undefined)
const filterInputValues = computed(() => {
if (filterOrSearch.value != null) {
return filterOrSearch.value.filter(({ props }) => props.type !== 'text')
......@@ -83,9 +97,6 @@ const filterInputValues = computed(() => {
}
})
const isFilter = computed(() => {
return Array.isArray(filterOrSearch.value)
})
const msSortBy = computed(() => {
if (sortByRef.value.length > 0) {
......@@ -114,26 +125,67 @@ const notPaginatedParams = computed(() => {
watch([paginationParams, msSortBy, page], ([newParams, newSort, newPage]) => {
searchOrFilter()
if (toValue(isValidFilters)) {
searchOrFilter()
}
})
onMounted(async () => {
onBeforeMount(async () => {
searchOrFilter()
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, () => {
searchOrFilter()
emitRefreshRes()
console.log(toValue(computedFilter))
if (toValue(isValidFilters) && (toValue(computedFilter) !== undefined || toValue(filterInputValues) === null)) {
searchOrFilter()
emitRefreshRes()
}
})
......@@ -147,154 +199,86 @@ const throttleSearch = useThrottleFn(async () => {
searchOrFilter()
emitRefreshRes()
}, 300)
async function searchOrFilter() {
// 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)
const lastFilterItem = computed(() => {
const toValFilterItems = toValue(msFilterCompo)
if (toValFilterItems !== undefined && Array.isArray(toValFilterItems)) {
return toValFilterItems.slice(-1)[0]
}
})
const isValidFilters = computed(() => {
const toValFilterItems = toValue(msFilterCompo)
if (toValFilterItems === undefined || Array.isArray(toValFilterItems) && toValFilterItems?.length === 0) {
return true
}
finally {
loading.value = false
else {
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() {
console.log("emit refresh:search")
const q = search.value
emit("refresh:search", {
index: props.db,
index: props.autocompleteMeiliFacetsProps.db,
query: q,
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(() => {
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, () => {
searchOrFilter()
emitRefreshRes()
// emitRefreshRes()
})
const filterStep = computed(() => {
return filterInputValues.value !== null && filterInputValues.value.length > 0 ? filterInputValues.value?.length % 3 : null
})
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)
}
}
// watch(msFilterCompo, () => {
// searchOrFilter()
function clearSearch() {
search.value = ""
}
// })
async function downloadData() {
pendingDownloadData.value = true
try {
const { data } = await useAsyncMeiliSearch({
index: props.db,
index: props.autocompleteMeiliFacetsProps.db,
params: { ...toValue(notPaginatedParams), filter: toValue(computedFilter), sort: toValue(msSortBy) },
query: toValue(search),
})
......@@ -304,84 +288,50 @@ async function downloadData() {
pendingDownloadData.value = false
}
}
function focusedOrBlur(isFocused: boolean) {
if (!isFocused) {
emitRefreshRes()
}
}
</script>
<template>
<v-card flat color="transparent">
<v-card-text>
</v-card-text>
<v-card-text>
<slot name="numerical-filters" :search="throttleSearch"></slot>
</v-card-text>
<slot name="numerical-filters" :search="throttleSearch"></slot>
<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
:loading="loading" :items="msResult?.hits ?? []" :items-length="totalHits" density="compact"
:items-per-page-options="itemsPerPage" :height="computedTableHeight" class="elevation-1 mt-2">
<template #top>
<template v-if="mobile">
<v-toolbar> <v-badge :content="totalHits" color="primary" class="mx-2">
<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-card variant="flat" color="transparent">
<v-card-title>
<v-badge :content="totalHits" color="primary" class="mr-3">
<v-btn prepend-icon="md:download" :loading="pendingDownloadData" variant="text" color="primary"
@click="downloadData()">{{
props.title }}
</v-btn>
</v-badge>
<v-spacer></v-spacer>
<v-card variant="flat" color="transparent" :min-width="400" class="mx-2" :rounded="false">
<v-text-field v-model="search" label="Search..." hide-details="auto"
prepend-inner-icon="mdi-magnify" single-line clearable></v-text-field>
</v-card>
<v-card variant="flat" color="transparent" :min-width="500" class="mx-2" :rounded="false">
<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"
@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-card>
</v-toolbar>
</template>
</v-card-title>
<v-card-title>
<v-text-field v-model="search" label="Search..." hide-details="auto" :disabled="pendingDownloadData"
prepend-inner-icon="mdi-magnify" single-line clearable
@update:focused="focusedOrBlur"></v-text-field>
</v-card-title>
<v-card-title>
<AutocompleteMeiliFacets v-model="msFilterCompo" v-bind="props.autocompleteMeiliFacetsProps"
:is-valid-filters="isValidFilters">
</AutocompleteMeiliFacets>
</v-card-title>
</v-card>
</template>
<template v-for="(slot, index) of Object.keys(slots)" :key="index" v-slot:[slot]="data">
......
......@@ -107,7 +107,7 @@ const pdbeMolstarComponent = ref(null)
const selectedPdb: Ref<string | null> = ref(null)
const structureToDownload: Ref<string | null> = ref(null)
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) => {
......@@ -190,10 +190,9 @@ const moleculeFormat: Ref<string> = ref("pdb")
<v-col :cols="mobile ? 12 : 'auto'">
<v-sheet v-if="selectedPdb"
class="d-flex align-center justify-center flex-wrap text-center mx-auto px-4 my-3"
:height="computedHeight" :width="computedWidth" position="relative">
<pdbe-molstar ref="pdbeMolstarComponent" landscape="true" hide-controls="true"
:custom-data-url="selectedPdb" alphafold-view="true"
:custom-data-format="moleculeFormat"></pdbe-molstar>
:height="computedHeight" :width="computedWidth" style="position:relative;">
<pdbe-molstar ref="pdbeMolstarComponent" :custom-data-url="selectedPdb" alphafold-view
sequence-panel="true" landscape="false" :custom-data-format="moleculeFormat"></pdbe-molstar>
</v-sheet>
</v-col>
<v-col :cols="mobile ? 12 : undefined">
......@@ -272,4 +271,4 @@ div.msp-plugin-content.msp-layout-expanded {
height: 16px;
width: 16px;
}
</style>
\ No newline at end of file
</style>
......@@ -2,36 +2,98 @@
import * as Plot from "@observablehq/plot";
import PlotFigure from "~/components/PlotFigure";
import { useDisplay } from "vuetify";
import type { SortItem } from "@/components/ServerDbTable.vue"
import { ServerDbTable } from "#components"
import { useSerialize } from "@/composables/useSerialize";
import { useRasterize } from "@/composables/useRasterize";
import { useDownloadBlob } from '@/composables/useDownloadBlob';
import type { SortItem, AutocompleteMeiliFacetProps } from "@/components/ServerDbTable.vue"
import type { ComponentPublicInstance } from 'vue'
const sortBy: Ref<SortItem[]> = ref([{ key: 'type', order: "asc" }])
const itemValue = ref("id");
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 { rasterize } = useRasterize()
const { download } = useDownloadBlob()
const facets = ref([
"replicon",
"type",
"subtype",
"Superkingdom",
"phylum",
"order",
"family",
"genus",
"species",
])
const autocompleteMeiliFacetsProps = ref<AutocompleteMeiliFacetProps>({
db: toValue(dbName),
facets: [
{ title: "Defense System", type: "subheader" },
{ title: "System", value: "type", type: "facet", icon: "i-tabler:virus-off", },
{ title: "Subsystem", value: "subtype", type: "facet", icon: "i-tabler:virus-off" },
{ type: "divider" },
{ title: "Taxonomy", type: "subheader" },
{ title: "Superkingdom", value: "Superkingdom", type: "facet", icon: "i-tabler:binary-tree" },
{ 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([
"species",
"genus",
......@@ -42,16 +104,16 @@ const availableTaxo: Ref<string[]> = ref([
]);
const scaleTypes = ref<string[]>(['linear', 'sqrt', 'log', 'symlog'])
const selectedTaxoRank = ref("phylum");
const selectedTaxoRank = ref("Superkingdom");
const headers = ref([
{ title: "Replicon", key: "replicon" },
{
title: "Type",
title: "System",
key: "type",
},
{
title: "Subtype",
title: "Subsystem",
key: "subtype",
},
{
......@@ -72,7 +134,7 @@ const computedHeaders = computed(() => {
}
})]
})
const { result: msResult } = useMeiliSearch('refseq')
const { result: msResult } = useMeiliSearch(toValue(dbName))
const computedWidth = computed(() => {
const currentWidth = fullWidth.value ? width.value : width.value / 2
......@@ -82,20 +144,11 @@ const computedWidth = computed(() => {
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)
async function getAllHits(params: { index: string, params: Record<string, any>, query: string }) {
console.log(params.index)
if (params.index === 'refseq') {
if (params.index === toValue(dbName)) {
console.log("get all hits in function ")
console.log(params)
pendingAllHits.value = true
......@@ -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({
showExpand: false
......@@ -222,7 +271,7 @@ function namesToAccessionChips(names: string[]) {
return namesToCollapsibleChips(names).map(it => {
return {
...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({
grid: true,
x: { tickRotate: 90, tip: true, label: "Systems" },
// y: { tickFormat: 's' },
color: { scheme: "turbo", legend: true },
color: { scheme: "plasma", legend: true },
})
const binPlotDataOptions = computed(() => {
......@@ -249,10 +298,7 @@ const binPlotDataOptions = computed(() => {
type: scaleType.value,
tickFormat: '~s',
ticks: scaleType.value === 'symlog' ? 3 : 5,
// width: 350
},
// fy: { domain: groupSortDomain.value },
marks: [
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:
</v-expansion-panel>
</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)">
<template #[`item.accession_in_sys`]="{ item }">
<CollapsibleChips :items="namesToAccessionChips(item.accession_in_sys)">
</CollapsibleChips>
</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>
......
<script setup lang="ts">
import * as Plot from "@observablehq/plot";
import PlotFigure from "~/components/PlotFigure";
import type { SortItem } from "@/components/ServerDbTable.vue"
import type { SortItem, AutocompleteMeiliFacetProps } from "@/components/ServerDbTable.vue"
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 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([
{ title: 'Structure', key: 'structure', sortable: false, removable: false },
{ title: 'Foldseek', key: 'Foldseek_name', sortable: false },
{ title: "System", key: "System", removable: false },
{ title: "Gene name", key: "gene_name", removable: false },
{ title: "Subtype", key: "subtype", removable: false },
......@@ -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: iptmRange, stringifyFilter: iptmFilter, reset: iptmReset } = useNumericalFilter("iptm+ptm", 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 listFilters = [plddtsFilter, iptmFilter, pdockqFilter].map(f => toValue(f)).filter(f => f !== undefined)
return listFilters.length > 0 ? listFilters.join(" AND ") : undefined
const listFilters = [plddtsFilter, iptmFilter, pdockqFilter].filter(isString).map(f => toValue(f))
return listFilters.length > 0 ? listFilters : undefined
})
const defaultDataTableServerProps = ref({
......@@ -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) {
......@@ -70,63 +143,64 @@ function pdbNameToCif(pdbPath: string) {
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>
<template>
<ServerDbTable title="Predicted Structures" db="structure" :sortBy="sortBy" :facets="facets"
:data-table-server-props="dataTableServerProps" :numerical-filters="numericalFilters">
<ServerDbTable title="Predicted Structures" :sortBy="sortBy" :data-table-server-props="dataTableServerProps"
:autocomplete-meili-facets-props="computedAutocompleteMeiliFacetsProps"
:numerical-filters="toRef(numericalFilters)">
<template #numerical-filters="{ search }">
<v-row>
<v-col cols="12" md="12" lg="4">
<v-range-slider v-model="plddtsRange" strict density="compact" hide-details="auto" label="pLDDT"
step="0.5" :min="0" :max="100" thumb-label="always" @update:modelValue="search()">
<template #append>
<v-btn variant="text" icon="md:restart_alt" @click="plddtsReset()"></v-btn>
</template>
</v-range-slider>
</v-col>
<v-col cols="12" md="12" lg="4">
<v-range-slider v-model="iptmRange" strict density="compact" hide-details="auto" label="iptm+ptm"
step="0.1" :min="0" :max="1" thumb-label="always" @update:modelValue="search()">
<template #append>
<v-btn variant="text" icon="md:restart_alt" @click="iptmReset()"></v-btn>
</template>
</v-range-slider>
</v-col>
<!-- pdockqReset -->
<v-col cols="12" md="12" lg="4">
<v-range-slider v-model="pdockqRange" strict density="compact" hide-details="auto" label="pDockQ"
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>
<v-list-item>
<v-list-item-title class="text-subtitle-1 text-medium-emphasis">
pLDDT
</v-list-item-title>
<v-row>
<v-col class="pt-8 pl-8" :lg="8">
<v-range-slider v-model="plddtsRange" strict density="compact" hide-details="auto" step="0.5"
:min="0" :max="100" thumb-label="always" @update:modelValue="search()">
<template #append>
<v-btn variant="text" icon="md:restart_alt" @click="plddtsReset()"></v-btn>
</template>
</v-range-slider>
</v-col></v-row>
</v-list-item>
<v-list-item>
<v-list-item-title class="text-subtitle-1 text-medium-emphasis">
iptm+ptm
</v-list-item-title>
<v-row>
<v-col class="pt-8 pl-8" :lg="8">
<v-range-slider v-model="iptmRange" strict density="compact" 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="iptmReset()"></v-btn>
</template>
</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 #[`item.proteins_in_the_prediction`]="{ item }">
<CollapsibleChips
:items="namesToCollapsibleChips(item.proteins_in_the_prediction, item.System_name_ok, item.fasta_file)">
......
<script setup lang="ts">
import type { SortItem } from "@/components/ServerDbTable.vue"
import type { SortItem, AutocompleteMeiliFacetProps } from "@/components/ServerDbTable.vue"
import { ServerDbTable } from "#components"
const sortBy: Ref<SortItem[]> = ref([{ key: 'title', order: "asc" }])
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([
{ title: "System", key: "title", removable: false },
{ title: "Article", key: "doi", removable: false },
......@@ -16,7 +80,6 @@ const headers: Ref<Object[]> = ref([
])
const { search: msSearch, result: msResult } = useMeiliSearch('systems')
const defaultDataTableServerProps = ref({
showExpand: false
......@@ -34,7 +97,8 @@ const columnsToDownload = ref(['title', 'doi', 'Sensor', 'Activator', 'Effector'
</script>
<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">
......@@ -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" />
</template>
<template #[`item.PFAM`]="{ item }">
<pfam-chips v-if="item?.PFAM" :pfams="item.PFAM"></pfam-chips>
</template>
<template #[`item.contributors`]="{ item }">
<CollapsibleChips v-if="item?.contributors" :items="item.contributors.map(it => ({ title: it }))">
</CollapsibleChips>
</template>
</ServerDbTable>
</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(
const runtimeConfig = useRuntimeConfig();
const client = new MeiliSearch({
host: runtimeConfig.public.meiliHost,
apiKey: runtimeConfig.public.meiliApiKey
host: runtimeConfig.public.meilisearchClient.hostUrl,
apiKey: runtimeConfig.public.meilisearchClient.searchApiKey
})
const pending = ref(false)
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(
const range: Ref<[number, number]> = ref([toValue(min), toValue(max)])
const stringifyFilter: Ref<string | undefined> = ref(`${toValue(id)} ${toValue(min)} TO ${toValue(max)}`)
const stringifyFilter: Ref<string | undefined> = ref(undefined)
watchEffect(() => {
console.log("watch reange")
console.log(range.value)
if (range.value[0] === toValue(min) && range.value[1] === toValue(max)) {
stringifyFilter.value = undefined
} else {
......
---
title: Contributing to the Wiki
layout: article
navigation:
---
# Contributing to the Wiki
......