From bfc47d664991f4ee8e71c2b668cfb11522f067ae Mon Sep 17 00:00:00 2001 From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr> Date: Thu, 6 Feb 2025 14:24:18 +0100 Subject: [PATCH 1/5] refactor: :art: Prettier formatting --- src/components/SequenceBoard.vue | 135 ++++++++++--------------------- src/typings/typeUtils.ts | 4 +- 2 files changed, 45 insertions(+), 94 deletions(-) diff --git a/src/components/SequenceBoard.vue b/src/components/SequenceBoard.vue index df6849b..694b03a 100644 --- a/src/components/SequenceBoard.vue +++ b/src/components/SequenceBoard.vue @@ -172,10 +172,7 @@ const splitSequence = computed(() => */ const isInObject = (position: number, objectId: string): boolean => { const object = props.objects?.[objectId] - return ( - !!object && - _inRange(position, object.start, object.end + 1) - ) + return !!object && _inRange(position, object.start, object.end + 1) } /** @@ -196,9 +193,7 @@ const objectIdsContaining = (position: number): string[] => * @returns A list of tuples, `[object ID, object]` for each of the objects * containing the nucleotide. */ -const objectsContaining = ( - position: number -): [string, objectModel][] => +const objectsContaining = (position: number): [string, objectModel][] => props.objects ? Object.entries(props.objects).filter(([objectId]) => isInObject(position, objectId) @@ -222,9 +217,7 @@ const objectsBoundaryPositions = computed( props.objects && Object.values(props.objects).reduce( (objectsBoundaryPositions, object) => - objectsBoundaryPositions - .add(object.start) - .add(object.end), + objectsBoundaryPositions.add(object.start).add(object.end), new Set<number>() ) ) @@ -350,38 +343,34 @@ const objectsLineCount = ref<{ * dividing by the line height. */ const updateObjectsLineCount = () => - (objectsLineCount.value = _mapValues( - props.objects, - (object) => { - // Parent DOM elements of the first and last nucleotide of the object - const objectStartNucleotideElementParent = objectBoundaryElements.value.find( - (element) => - element.dataset.position === object.start.toString() - )?.offsetParent - const objectEndNucleotideElementParent = objectBoundaryElements.value.find( - (element) => - element.dataset.position === object.end.toString() + (objectsLineCount.value = _mapValues(props.objects, (object) => { + // Parent DOM elements of the first and last nucleotide of the object + const objectStartNucleotideElementParent = + objectBoundaryElements.value.find( + (element) => element.dataset.position === object.start.toString() )?.offsetParent + const objectEndNucleotideElementParent = objectBoundaryElements.value.find( + (element) => element.dataset.position === object.end.toString() + )?.offsetParent - if ( - !( - objectStartNucleotideElementParent instanceof HTMLElement && - objectEndNucleotideElementParent instanceof HTMLElement - ) - ) { - return undefined - } - - return ( - lineHeight.value && - Math.round( - (objectEndNucleotideElementParent.offsetTop - - objectStartNucleotideElementParent.offsetTop) / - lineHeight.value - ) + 1 + if ( + !( + objectStartNucleotideElementParent instanceof HTMLElement && + objectEndNucleotideElementParent instanceof HTMLElement ) + ) { + return undefined } - )) + + return ( + lineHeight.value && + Math.round( + (objectEndNucleotideElementParent.offsetTop - + objectStartNucleotideElementParent.offsetTop) / + lineHeight.value + ) + 1 + ) + })) /** * Width of the sequence container. @@ -525,8 +514,7 @@ const objectBoxStyles = computed(() => _mapValues(props.objects, (object, objectId) => { // DOM element of the first and last nucleotides of the object const objectStartNucleotideElement = objectBoundaryElements.value.find( - (element) => - element.dataset.position === object.start.toString() + (element) => element.dataset.position === object.start.toString() ) const objectEndNucleotideElement = objectBoundaryElements.value.find( (element) => element.dataset.position === object.end.toString() @@ -557,8 +545,7 @@ const objectBoxStyles = computed(() => (objectEndNucleotideElement.offsetWidth || 0) // Number of fragment in which the current object box is divided - const objectBoxFragmentsCount = - objectsLineCount.value?.[objectId] || 0 + const objectBoxFragmentsCount = objectsLineCount.value?.[objectId] || 0 // Array of length equals # of fragments, containing style for each of them return Array.from( @@ -739,19 +726,13 @@ const lockTooltip = () => { v-if=" objectsContaining( nucleotideIndex + groupIndex * nucleotideGroupsSize + 1 - ).find( - ([, object]) => - object.shouldTooltip - ) + ).find(([, object]) => object.shouldTooltip) " :class="[ 'absolute -bottom-0.5 -left-[0.1875rem] -right-[0.1875rem] -top-0.5', objectsContaining( nucleotideIndex + groupIndex * nucleotideGroupsSize + 1 - ).find( - ([, object]) => - object.link - ) + ).find(([, object]) => object.link) ? 'cursor-pointer' : 'cursor-help' ]" @@ -761,10 +742,7 @@ const lockTooltip = () => { ).forEach( ([objectId, object]) => object.shouldTooltip && - hoveredObjects.add([ - objectId, - object - ]) + hoveredObjects.add([objectId, object]) ) " @mouseleave="hoveredObjects.clear()" @@ -789,19 +767,13 @@ const lockTooltip = () => { v-if=" objectsContaining( nucleotideIndex + groupIndex * nucleotideGroupsSize + 1 - ).find( - ([, object]) => - object.shouldTooltip - ) + ).find(([, object]) => object.shouldTooltip) " :class="[ 'absolute -bottom-0.5 -left-[0.1875rem] -right-[0.1875rem] -top-0.5', objectsContaining( nucleotideIndex + groupIndex * nucleotideGroupsSize + 1 - ).find( - ([, object]) => - object.link - ) + ).find(([, object]) => object.link) ? 'cursor-pointer' : 'cursor-help' ]" @@ -811,10 +783,7 @@ const lockTooltip = () => { ).forEach( ([objectId, object]) => object.shouldTooltip && - hoveredObjects.add([ - objectId, - object - ]) + hoveredObjects.add([objectId, object]) ) " @mouseleave="hoveredObjects.clear()" @@ -834,30 +803,22 @@ const lockTooltip = () => { </span> <span - v-for="[objectId, object] in Object.entries( - objects || {} - )" + v-for="[objectId, object] in Object.entries(objects || {})" :key="objectId" class="object" > <span - v-for="fragmentIndex in range( - objectsLineCount?.[objectId] || 0 - )" + v-for="fragmentIndex in range(objectsLineCount?.[objectId] || 0)" :key="fragmentIndex" :style="objectBoxStyles?.[objectId]?.[fragmentIndex]" :class="[ 'object-fragment absolute h-8 border-y-2 bg-opacity-50 px-0.5 text-2xl mix-blend-multiply', - [ - `!border-${object.color}-600`, - `bg-${object.color}-100` - ], + [`!border-${object.color}-600`, `bg-${object.color}-100`], { 'rounded-l-xl border-l-2': fragmentIndex === 0, 'rounded-r-xl border-r-2': objectsLineCount && - fragmentIndex + 1 === - objectsLineCount[objectId] + fragmentIndex + 1 === objectsLineCount[objectId] }, object.shouldTooltip && (object.link ? 'cursor-pointer' : 'cursor-help') @@ -872,17 +833,11 @@ const lockTooltip = () => { @unlock="lockedTooltipObjects = undefined" > <ul class="flex flex-col gap-1"> - <li - v-for="(object, index) in tooltipObjects" - :key="index" - > + <li v-for="(object, index) in tooltipObjects" :key="index"> <RouterLink v-if="object[1].link" :to="object[1].link" - :class="[ - `text-${object[1].color}-600`, - 'whitespace-nowrap' - ]" + :class="[`text-${object[1].color}-600`, 'whitespace-nowrap']" > <slot name="tooltip-item" @@ -890,9 +845,7 @@ const lockTooltip = () => { :object-id="object[0]" > {{ object[1].name || object[1].link }} - {{ - object[1].type && ` - ${object[1].type}` - }} + {{ object[1].type && ` - ${object[1].type}` }} </slot> </RouterLink> <span v-else> @@ -902,9 +855,7 @@ const lockTooltip = () => { :object-id="object[0]" > {{ object[1].name || object[1].link }} - {{ - object[1].type && ` - ${object[1].type}` - }} + {{ object[1].type && ` - ${object[1].type}` }} </slot> </span> </li> diff --git a/src/typings/typeUtils.ts b/src/typings/typeUtils.ts index d23a9f3..96c259a 100644 --- a/src/typings/typeUtils.ts +++ b/src/typings/typeUtils.ts @@ -115,6 +115,6 @@ export type SubType<BaseType, ConditionType> = Pick< * Checks if an array contains at least one element, narrowing its type. * @param array The array for which to check the length. */ -export function isNotEmpty<T>(array: T[]): array is [T,...T[]] { +export function isNotEmpty<T>(array: T[]): array is [T, ...T[]] { return array.length >= 1 -} \ No newline at end of file +} -- GitLab From b370db4b95f14a5e8cc45496ee99330bd8727bbf Mon Sep 17 00:00:00 2001 From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr> Date: Thu, 6 Feb 2025 14:27:41 +0100 Subject: [PATCH 2/5] fix(advanced-selection): :bug: wrong gql query variable name --- src/components/SelectionFormGuide.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionFormGuide.vue b/src/components/SelectionFormGuide.vue index 91b30ae..e11414f 100644 --- a/src/components/SelectionFormGuide.vue +++ b/src/components/SelectionFormGuide.vue @@ -74,7 +74,7 @@ const selection = ref<GuideSelectionModel>({ const gqlQuery = useQuery({ query: guideSelectionQuery, variables: toRef(() => ({ - guideSubclass: selection.value.guideSubclasses?.length + guideSubclasses: selection.value.guideSubclasses?.length ? selection.value.guideSubclasses : undefined, targetNames: selection.value.targetNames?.length -- GitLab From cbe656e03023d1e9bd8a79518bf3ccc2232c05c7 Mon Sep 17 00:00:00 2001 From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr> Date: Thu, 6 Feb 2025 14:29:47 +0100 Subject: [PATCH 3/5] feat(advanced-selection): :sparkles: add a text to describe advanced selection --- src/views/SelectionView.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/views/SelectionView.vue b/src/views/SelectionView.vue index cf4fd8e..9868e25 100644 --- a/src/views/SelectionView.vue +++ b/src/views/SelectionView.vue @@ -180,6 +180,11 @@ const onlyTargetIdActiveMode = computed(() => Advanced selection </h1> + <h3 class="my-8 text-center italic text-slate-500"> + Filter the {{ activeMode }}s of the snoBoard's database to list them in + the table. + </h3> + <SelectionForm v-model:current-mode-model="activeMode" v-model:selection-model="selection" -- GitLab From fb241fd3c7f962f31fff7465fcbcb9b1879b235e Mon Sep 17 00:00:00 2001 From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr> Date: Thu, 6 Feb 2025 14:33:14 +0100 Subject: [PATCH 4/5] feat(linear-sequence): :fire: instruction text always visible (no tooltip) --- src/components/SequenceBoard.vue | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/components/SequenceBoard.vue b/src/components/SequenceBoard.vue index 694b03a..eb31849 100644 --- a/src/components/SequenceBoard.vue +++ b/src/components/SequenceBoard.vue @@ -14,7 +14,6 @@ import Toolbar from 'primevue/toolbar' import IconFa6SolidDna from '~icons/fa6-solid/dna' import IconFa6SolidCircleCheck from '~icons/fa6-solid/circle-check' import IconFa6RegularCopy from '~icons/fa6-regular/copy' -import IconFa6SolidCircleInfo from '~icons/fa6-solid/circle-info' /** * Composables imports */ @@ -634,18 +633,9 @@ const lockTooltip = () => { <slot name="legend-item-description" :item="item" /> </template> </BaseLegendButtonOverlay> - <span class="text-sm italic text-slate-600 md:hidden"> + <span class="text-sm italic text-slate-600"> Click on an object to lock its tooltip in place. </span> - <span - v-tooltip.right="{ - value: 'Click on an object to lock its tooltip in place.', - pt: { text: 'text-xs' } - }" - class="absolute -right-2 -top-2 hidden text-base text-slate-600 md:inline" - > - <icon-fa6-solid-circle-info /> - </span> </div> </template> -- GitLab From 7b9c6cb4d0941f5d0414c01d8405ebf92d5d57cd Mon Sep 17 00:00:00 2001 From: Julien Touchais <5978-julien.touchais@users.noreply.forgemia.inra.fr> Date: Thu, 6 Feb 2025 14:37:00 +0100 Subject: [PATCH 5/5] feat(advanced-selection): :sparkles: add information about the role of each field --- src/components/SelectionFormGuide.vue | 64 ++++++++++++++++++-- src/components/SelectionFormModification.vue | 49 +++++++++++++-- src/components/SelectionFormTarget.vue | 49 +++++++++++++-- 3 files changed, 149 insertions(+), 13 deletions(-) diff --git a/src/components/SelectionFormGuide.vue b/src/components/SelectionFormGuide.vue index e11414f..f6e2be2 100644 --- a/src/components/SelectionFormGuide.vue +++ b/src/components/SelectionFormGuide.vue @@ -10,7 +10,7 @@ import BaseRenderedMarkdown from './BaseRenderedMarkdown.vue' import Chip from 'primevue/chip' import SelectButton from 'primevue/selectbutton' import MultiSelect from 'primevue/multiselect' -import { GuideClass, ModifType } from '@/gql/codegen/graphql' +import IconFa6SolidCircleQuestion from '~icons/fa6-solid/circle-question' /** * Composables imports */ @@ -19,6 +19,10 @@ import { useQuery } from '@urql/vue' * Other 3rd-party imports */ import { uniqWith as _uniqWith, isEqual as _isEqual } from 'lodash-es' +/** + * Types imports + */ +import { GuideClass, ModifType } from '@/gql/codegen/graphql' /** * Utils imports */ @@ -293,7 +297,19 @@ const organismIdOptionsWithDisabling = computed(() => <div class="grid max-w-max grid-cols-[max-content_1fr] grid-rows-4 items-center gap-x-16 gap-y-8" > - <h3 class="text-lg font-bold">Guide subclasses</h3> + <h3 class="text-lg font-bold"> + Guide subclasses + <span + v-tooltip.right="{ + value: 'Only guides of the selected subclasses will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <SelectButton v-model="selection.guideSubclasses" :options="guideSubclassOptionsWithDisabling" @@ -325,7 +341,20 @@ const organismIdOptionsWithDisabling = computed(() => </template> </SelectButton> - <h3 class="text-lg font-bold">Targets</h3> + <h3 class="text-lg font-bold"> + Targets + <span + v-tooltip.right="{ + value: + 'Only guides which interact with the selected targets will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <MultiSelect v-model="selection.targetNames" :options="targetNameOptionsWithDisablingGroups" @@ -370,7 +399,20 @@ const organismIdOptionsWithDisabling = computed(() => </template> </MultiSelect> - <h3 class="text-lg font-bold">Guided modification types</h3> + <h3 class="text-lg font-bold"> + Guided modification types + <span + v-tooltip.right="{ + value: + 'Only guides which guide at least a modification of the selected types will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <SelectButton v-model="selection.modificationTypes" :options="modificationTypeOptionsWithDisabling" @@ -389,7 +431,19 @@ const organismIdOptionsWithDisabling = computed(() => </template> </SelectButton> - <h3 class="text-lg font-bold">Organisms</h3> + <h3 class="text-lg font-bold"> + Organisms + <span + v-tooltip.right="{ + value: 'Only guides of the selected organisms will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <MultiSelect v-model="selection.organismIds" :options="organismIdOptionsWithDisabling" diff --git a/src/components/SelectionFormModification.vue b/src/components/SelectionFormModification.vue index c0a2344..6c102b9 100644 --- a/src/components/SelectionFormModification.vue +++ b/src/components/SelectionFormModification.vue @@ -10,7 +10,7 @@ import BaseRenderedMarkdown from './BaseRenderedMarkdown.vue' import Chip from 'primevue/chip' import SelectButton from 'primevue/selectbutton' import MultiSelect from 'primevue/multiselect' -import { ModifType } from '@/gql/codegen/graphql' +import IconFa6SolidCircleQuestion from '~icons/fa6-solid/circle-question' /** * Composables imports */ @@ -19,6 +19,10 @@ import { useQuery } from '@urql/vue' * Other 3rd-party imports */ import { uniqWith as _uniqWith, isEqual as _isEqual } from 'lodash-es' +/** + * Types imports + */ +import { ModifType } from '@/gql/codegen/graphql' /** * Utils imports */ @@ -258,7 +262,19 @@ watchEffect(() => { <div class="grid max-w-max grid-cols-[max-content_1fr] grid-rows-3 items-center gap-x-16 gap-y-8" > - <h3 class="text-lg font-bold">Modification types</h3> + <h3 class="text-lg font-bold"> + Modification types + <span + v-tooltip.right="{ + value: 'Only modifications of the selected types will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <SelectButton v-model="selection.modificationTypes" :options="modificationTypeOptionsWithDisabling" @@ -277,7 +293,19 @@ watchEffect(() => { </template> </SelectButton> - <h3 class="text-lg font-bold">Targets</h3> + <h3 class="text-lg font-bold"> + Targets + <span + v-tooltip.right="{ + value: 'Only modifications of the selected targets will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <MultiSelect v-model="selection.targetNames" :options="targetNameOptionsWithDisablingGroups" @@ -317,7 +345,20 @@ watchEffect(() => { </template> </MultiSelect> - <h3 class="text-lg font-bold">Organisms</h3> + <h3 class="text-lg font-bold"> + Organisms + <span + v-tooltip.right="{ + value: + 'Only modifications of the targets of the selected organisms will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <MultiSelect v-model="selection.organismIds" :options="organismIdOptionsWithDisabling" diff --git a/src/components/SelectionFormTarget.vue b/src/components/SelectionFormTarget.vue index d22e087..044ee38 100644 --- a/src/components/SelectionFormTarget.vue +++ b/src/components/SelectionFormTarget.vue @@ -10,7 +10,7 @@ import BaseRenderedMarkdown from './BaseRenderedMarkdown.vue' import Chip from 'primevue/chip' import SelectButton from 'primevue/selectbutton' import MultiSelect from 'primevue/multiselect' -import { ModifType } from '@/gql/codegen/graphql' +import IconFa6SolidCircleQuestion from '~icons/fa6-solid/circle-question' /** * Composables imports */ @@ -23,6 +23,10 @@ import { isEqual as _isEqual, remove as _remove } from 'lodash-es' +/** + * Types imports + */ +import { ModifType } from '@/gql/codegen/graphql' /** * Utils imports */ @@ -262,7 +266,19 @@ watchEffect(() => { <div class="grid max-w-max grid-cols-[max-content_1fr] grid-rows-3 items-center gap-x-16 gap-y-8" > - <h3 class="text-lg font-bold">Targets</h3> + <h3 class="text-lg font-bold"> + Target families + <span + v-tooltip.right="{ + value: 'Only targets of the selected units will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <MultiSelect v-model="selection.targetNames" :options="targetNameOptionsWithDisablingGroups" @@ -302,7 +318,20 @@ watchEffect(() => { </template> </MultiSelect> - <h3 class="text-lg font-bold">Modification types</h3> + <h3 class="text-lg font-bold"> + Modification types + <span + v-tooltip.right="{ + value: + 'Only targets on which exist modifications of the selected types will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <SelectButton v-model="selection.modificationTypes" :options="modificationTypeOptionsWithDisabling" @@ -321,7 +350,19 @@ watchEffect(() => { </template> </SelectButton> - <h3 class="text-lg font-bold">Organisms</h3> + <h3 class="text-lg font-bold"> + Organisms + <span + v-tooltip.right="{ + value: 'Only targets of the selected organisms will be retained.', + pt: { text: 'text-xs' } + }" + class="-mt-1 inline-block align-top text-sm text-slate-600" + > + <icon-fa6-solid-circle-question class="inline" /> + </span> + </h3> + <MultiSelect v-model="selection.organismIds" :options="organismIdOptionsWithDisabling" -- GitLab