Repository: molgenis/molgenis-emx2
Pull request #5056: fix(directory): deselecting child after selecting parent results in invalid state
Author: login: chinook25
Created: 2025-05-14T09:13:28Z | Merged: 2025-05-26T10:00:55Z

### What are the main changes you did
- follow up on #5047 
- Fixes the issue that when you select a parent node and then deselect a child the functionality breaks down visibly.

### Known issues (that already existed)
- Intermediate states don't update correctly after refresh due to lazy loading of child components
- (de-)Selecting diseases too fast causes issues
- Selecting too many filters might cause timeout.

### How to test
- Go to a Directory
- Open a chapter in ICD-10 and select an entire block (chapter is correctly marked as partially selected)
- Open the block that you selected and deselect an individual code


### Checklist
- [ ] updated docs in case of new feature
- [ ] added/updated tests
- [ ] added/updated testplan to include a test for this fix, including ref to bug using # notation


Diff:
diff --git a/apps/directory/playwright.config.ts b/apps/directory/playwright.config.ts
index 16bdd3997e1..24a4ccd214c 100644
--- a/apps/directory/playwright.config.ts
+++ b/apps/directory/playwright.config.ts
@@ -1,34 +1,34 @@
-import { defineConfig, devices, PlaywrightTestConfig } from '@playwright/test';
+import { defineConfig, devices, PlaywrightTestConfig } from "@playwright/test";
 
 export default defineConfig<PlaywrightTestConfig>({
-  testDir: './tests',
+  testDir: "./tests",
   fullyParallel: true,
   forbidOnly: !!process.env.CI,
   retries: process.env.CI ? 2 : 0,
   workers: process.env.CI ? 1 : undefined,
-  reporter: process.env.CI ? [['list'],
-  ['junit', { outputFile: 'results.xml' }]
-  ] : 'html',
+  reporter: process.env.CI
+    ? [["list"], ["junit", { outputFile: "results.xml" }]]
+    : "html",
   use: {
     baseURL: process.env.E2E_BASE_URL || "http://localhost:5173/", // change to specific http://localhost:*/, preview, etc.
-    trace: 'on-first-retry',
+    trace: "on-first-retry",
   },
 
-  snapshotPathTemplate: '__screenshots__/{testFilePath}/{arg}{ext}',
+  snapshotPathTemplate: "__screenshots__/{testFilePath}/{arg}{ext}",
 
   projects: [
     {
-      name: 'chromium',
-      use: { ...devices['Desktop Chrome'] },
-      testIgnore: '*/admin!*.spec.ts'
+      name: "chromium",
+      use: { ...devices["Desktop Chrome"] },
+      testIgnore: "*/admin!*.spec.ts",
     },
     {
-      name: 'chromium',
+      name: "chromium",
       use: {
-        ...devices['Desktop Chrome'],
-        storageState: 'e2e/.auth/user.json'
+        ...devices["Desktop Chrome"],
+        storageState: "e2e/.auth/user.json",
       },
-      testMatch: '*/admin!*.spec.ts'
+      testMatch: "*/admin!*.spec.ts",
     },
   ],
-});
\ No newline at end of file
+});
diff --git a/apps/directory/src/App.vue b/apps/directory/src/App.vue
index 691b60c6f59..cec06dea328 100644
--- a/apps/directory/src/App.vue
+++ b/apps/directory/src/App.vue
@@ -12,16 +12,16 @@
 </template>
 
 <script setup lang="ts">
+import { useFavicon, usePreferredDark } from "@vueuse/core";
 //@ts-expect-error
 import { Molgenis } from "molgenis-components";
-import { onMounted, computed, ref, watch } from "vue";
+import { computed, onMounted, ref, watch } from "vue";
 import { LocationQuery, useRoute } from "vue-router";
 import Error from "./components/Error.vue";
 import { applyBookmark, createBookmark } from "./functions/bookmarkMapper";
 import { useCheckoutStore } from "./stores/checkoutStore";
 import { useFiltersStore } from "./stores/filtersStore";
 import { useSettingsStore } from "./stores/settingsStore";
-import { useFavicon, usePreferredDark } from "@vueuse/core";
 
 const route = useRoute();
 const query = computed(() => route.query);
@@ -61,7 +61,6 @@ watch(
         checkoutStore.selectedCollections,
         checkoutStore.selectedServices
       );
-      applyBookmark(newQuery);
     }
 
     if (filtersStore.filtersReady && !checkoutStore.cartUpdated) {
diff --git a/apps/directory/src/components/filters/base/TreeBranchComponent.vue b/apps/directory/src/components/filters/base/TreeBranchComponent.vue
index 51c3cb6d0bc..a5421d1d3f0 100644
--- a/apps/directory/src/components/filters/base/TreeBranchComponent.vue
+++ b/apps/directory/src/components/filters/base/TreeBranchComponent.vue
@@ -10,8 +10,8 @@
         type="checkbox"
         :ref="`${option.name}-checkbox`"
         class="mr-1"
-        :checked="selected"
-        :indeterminate.prop="isIndeterminate && !selected"
+        :checked="isSelected"
+        :indeterminate.prop="isIndeterminate"
       />
       <label>
         <span class="code">{{ option.code }}</span> {{ option.label }}
@@ -25,8 +25,6 @@
         v-if="open"
         :options="option.children"
         :facetIdentifier="facetIdentifier"
-        :parentSelected="selected"
-        @indeterminate-update="handleChildIndeterminateUpdate"
         :filter="filter"
       />
     </li>
@@ -35,85 +33,83 @@
 
 <script setup lang="ts">
 import { computed, defineAsyncComponent, ref, toRefs, watch } from "vue";
+import { IOntologyItem } from "../../../interfaces/interfaces";
 import { useFiltersStore } from "../../../stores/filtersStore";
 
 /** need to lazy load because it gets too large quickly. Over 9000! */
 const TreeComponent = defineAsyncComponent(() => import("./TreeComponent.vue"));
 
 const filtersStore = useFiltersStore();
-const emit = defineEmits(["indeterminate-update"]);
 
 const props = withDefaults(
   defineProps<{
     facetIdentifier: string;
-    option: Record<string, any>;
+    option: IOntologyItem;
     parentSelected?: boolean;
     filter?: string;
   }>(),
-  { parentSelected: false, filter: "" }
+  { filter: "" }
 );
 
-const { facetIdentifier, option, parentSelected, filter } = toRefs(props);
+const { facetIdentifier, option, filter } = toRefs(props);
 
 const open = ref<boolean>(!!filter.value);
-const childIsIndeterminate = ref<boolean>(false);
 
-const selectedFilters = computed<any[]>(() => {
-  return filtersStore.getFilterValue(facetIdentifier.value) || [];
-});
+const indeterminateDiseases = computed(
+  () => filtersStore.indeterminateDiseases
+);
+const selectedDiseases = computed(() => filtersStore.selectedDiseases);
+const numberOfSelectedChildren = computed(getNumberOfSelectedChildren);
 
-const selected = computed(() => {
-  if (parentSelected.value) {
-    return true;
-  } else if (selectedChildren.value.length === option.value.children?.length) {
+const isSelected = computed<boolean>(
+  () => selectedDiseases.value[option.value.name]
+);
+
+const isIndeterminate = computed<boolean>(() => {
+  if (lessThenAllChildrenSelected() || hasIndeterminateChild()) {
     return true;
-  } else if (!selectedFilters.value?.length) {
-    return false;
   } else {
-    return selectedFilters.value.some(
-      (selectedValue: Record<string, any>) =>
-        selectedValue.name === option.value.name
-    );
+    return false;
   }
 });
 
-const selectedChildren = computed(() => {
-  const childNames =
-    option.value.children?.map(
-      (childOption: Record<string, any>) => childOption.name
-    ) || [];
-  return selectedFilters.value.filter((selectedFilter: Record<string, any>) =>
-    childNames.includes(selectedFilter.name)
-  );
+watch(filter, (newFilter: string) => (open.value = !!newFilter));
+watch(
+  isIndeterminate,
+  (newValue: boolean) => {
+    filtersStore.setDiseaseIndeterminate(option.value.name, newValue);
+  },
+  { immediate: true }
+);
+watch(numberOfSelectedChildren, (newValue) => {
+  if (!isSelected.value && newValue === option.value.children?.length) {
+    selectOption(true, option.value);
+  }
 });
 
-const isIndeterminate = computed<boolean>(() => {
-  if (parentSelected.value) return false;
-  if (childIsIndeterminate.value) return true;
-  if (!option.value.children) return false;
+function hasIndeterminateChild(): boolean {
+  return !!option.value.children?.some((child) => {
+    return indeterminateDiseases.value[child.name];
+  });
+}
 
+function lessThenAllChildrenSelected(): boolean {
   return (
-    selectedChildren.value.length > 0 &&
-    selectedChildren.value.length <= option.value.children.length
+    numberOfSelectedChildren.value > 0 &&
+    numberOfSelectedChildren.value < (option.value.children?.length ?? Infinity)
   );
-});
-
-watch(isIndeterminate, signalParentOurIndeterminateStatus);
-
-watch(filter, (newFilter: string) => {
-  open.value = !!newFilter;
-});
-
-function selectOption(checked: boolean, option: Record<string, any>) {
-  filtersStore.updateOntologyFilter(facetIdentifier.value, option, checked);
 }
 
-function handleChildIndeterminateUpdate(newStatus: boolean) {
-  childIsIndeterminate.value = newStatus;
+function selectOption(checked: boolean, option: IOntologyItem) {
+  filtersStore.updateOntologyFilter(facetIdentifier.value, option, checked);
 }
 
-function signalParentOurIndeterminateStatus() {
-  emit("indeterminate-update", isIndeterminate.value);
+function getNumberOfSelectedChildren() {
+  return (
+    option.value.children?.filter(
+      (child: IOntologyItem) => selectedDiseases.value[child.name]
+    ).length || 0
+  );
 }
 </script>
 
diff --git a/apps/directory/src/components/filters/base/TreeComponent.vue b/apps/directory/src/components/filters/base/TreeComponent.vue
index 89d0159001b..9b443a60015 100644
--- a/apps/directory/src/components/filters/base/TreeComponent.vue
+++ b/apps/directory/src/components/filters/base/TreeComponent.vue
@@ -4,8 +4,6 @@
       <TreeBranchComponent
         :option="option"
         :facetIdentifier="facetIdentifier"
-        @indeterminate-update="signalParentOurIndeterminateStatus"
-        :parentSelected="parentSelected"
         :filter="filter"
       />
     </div>
@@ -31,16 +29,13 @@ const props = withDefaults(
   defineProps<{
     facetIdentifier: string;
     options: any[];
-    parentSelected?: boolean;
     filter?: string;
   }>(),
   { filter: "" }
 );
 
 const { facetIdentifier } = props;
-const { filter, options, parentSelected } = toRefs(props);
-
-const emit = defineEmits(["indeterminate-update"]);
+const { filter, options } = toRefs(props);
 
 const filteredOptions = computed(() => {
   const sortedOptions = sortOptions(options.value);
@@ -80,10 +75,6 @@ function getTopPaginatedOptions() {
   }
 }
 
-function signalParentOurIndeterminateStatus(status: boolean) {
-  emit("indeterminate-update", status);
-}
-
 function sortOptions(options: any[] | undefined) {
   if (options?.length) {
     let copy = JSON.parse(JSON.stringify(options));
diff --git a/apps/directory/src/components/partial-views/ServiceDetails.vue b/apps/directory/src/components/partial-views/ServiceDetails.vue
index a6af457cc15..84177c787b0 100644
--- a/apps/directory/src/components/partial-views/ServiceDetails.vue
+++ b/apps/directory/src/components/partial-views/ServiceDetails.vue
@@ -99,7 +99,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, defineProps } from "vue";
+import { computed } from "vue";
 import { IBiobanks, IServices } from "../../interfaces/directory";
 import quality from "../../components/generators/view-components/quality.vue";
 import string from "../../components/generators/view-components/string.vue";
diff --git a/apps/directory/src/functions/bookmarkMapper.ts b/apps/directory/src/functions/bookmarkMapper.ts
index d8644541b36..8b3cb8a9f70 100644
--- a/apps/directory/src/functions/bookmarkMapper.ts
+++ b/apps/directory/src/functions/bookmarkMapper.ts
@@ -1,5 +1,6 @@
 import { LocationQuery } from "vue-router";
 import useErrorHandler from "../composables/errorHandler";
+import { IOntologyItem } from "../interfaces/interfaces";
 import router from "../router";
 import { labelValuePair, useCheckoutStore } from "../stores/checkoutStore";
 import { useCollectionStore } from "../stores/collectionStore";
@@ -10,7 +11,6 @@ const { setError } = useErrorHandler();
 
 export async function applyBookmark(watchedQuery: LocationQuery) {
   if (bookmarkApplied) {
-    bookmarkApplied = false;
     return;
   }
 
@@ -71,12 +71,14 @@ export async function applyBookmark(watchedQuery: LocationQuery) {
         const diagnosisFacetDetails = filtersStore.facetDetails[filterName];
         /** the diagnosis available has been encoded, to discourage messing with the tree and breaking stuff. */
         const queryValues = atob(filtersToAdd).split(",");
-        const options = await filtersStore.getOntologyOptionsForCodes(
-          diagnosisFacetDetails,
-          queryValues
-        );
-        filtersStore.updateOntologyFilter(filterName, options, true, true);
-        /** retrieve these from the server, this is easier than tree traversal */
+        const options: IOntologyItem[] =
+          await filtersStore.getOntologyOptionsForCodes(
+            diagnosisFacetDetails,
+            queryValues
+          );
+        options.forEach((option) => {
+          filtersStore.updateOntologyFilter(filterName, option, true, true);
+        });
       } else {
         const filterOptions = filtersStore.filterOptionsCache[filterName];
         if (filterOptions) {
@@ -90,6 +92,7 @@ export async function applyBookmark(watchedQuery: LocationQuery) {
     }
   }
   filtersStore.bookmarkWaitingForApplication = false;
+  bookmarkApplied = true;
 }
 
 export function createBookmark(
diff --git a/apps/directory/src/functions/flattenOntologyBranch.test.ts b/apps/directory/src/functions/flattenOntologyBranch.test.ts
new file mode 100644
index 00000000000..f759ceb8d13
--- /dev/null
+++ b/apps/directory/src/functions/flattenOntologyBranch.test.ts
@@ -0,0 +1,52 @@
+import { describe, expect, it } from "vitest";
+import { IOntologyItem } from "../interfaces/interfaces";
+import flattenOntologyBranch from "./flattenOntologyBranch";
+
+describe("flattenOntologyBranch", () => {
+  it("should return an just the root for a node without children", () => {
+    const branch: IOntologyItem = {
+      name: "Empty",
+      label: "Empty Label",
+      code: "empty_code",
+    };
+
+    const result = flattenOntologyBranch(branch);
+
+    expect(result).toEqual([branch]);
+  });
+
+  it("should flatten an ontology branch", () => {
+    const grandChild1: IOntologyItem = {
+      name: "Grandchild 1",
+      label: "Grandchild 1 Label",
+      code: "grandchild_1_code",
+    };
+    const grandChild2: IOntologyItem = {
+      name: "Grandchild 2",
+      label: "Grandchild 2 Label",
+      code: "grandchild_2_code",
+    };
+    const child1: IOntologyItem = {
+      name: "Child 1",
+      label: "Child 1 Label",
+      code: "child_1_code",
+      children: [grandChild1, grandChild2],
+    };
+    const child2: IOntologyItem = {
+      name: "Child 2",
+      label: "Child 2 Label",
+      code: "child_2_code",
+    };
+    const branch: IOntologyItem = {
+      name: "Root",
+      label: "Root Label",
+      code: "root_code",
+      children: [child1, child2],
+    };
+
+    const result = flattenOntologyBranch(branch);
+
+    const expectedResult = [branch, child2, child1, grandChild2, grandChild1];
+    expect(result).toEqual(expectedResult);
+  });
+});
diff --git a/apps/directory/src/functions/flattenOntologyBranch.ts b/apps/directory/src/functions/flattenOntologyBranch.ts
new file mode 100644
index 00000000000..522782e9de4
--- /dev/null
+++ b/apps/directory/src/functions/flattenOntologyBranch.ts
@@ -0,0 +1,17 @@
+import { IOntologyItem } from "../interfaces/interfaces";
+
+/** Uses basic non-recursive depth-first search algorithm*/
+export default function flattenOntologyBranch(branch: IOntologyItem) {
+  let stack = [branch];
+  let result: IOntologyItem[] = [];
+  while (stack.length) {
+    const current = stack.pop();
+    if (current?.children) {
+      stack.push(...current.children);
+    }
+    if (current) {
+      result.push(current);
+    }
+  }
+  return result;
+}
diff --git a/apps/directory/src/stores/filtersStore.ts b/apps/directory/src/stores/filtersStore.ts
index 63b36c1a509..8fd12da7545 100644
--- a/apps/directory/src/stores/filtersStore.ts
+++ b/apps/directory/src/stores/filtersStore.ts
@@ -1,3 +1,4 @@
+import * as _ from "lodash";
 import { defineStore } from "pinia";
 import { computed, ref, watch } from "vue";
 import { createFilters } from "../filter-config/facetConfigurator";
@@ -6,14 +7,15 @@ import { applyBookmark, createBookmark } from "../functions/bookmarkMapper";
 import { useBiobanksStore } from "./biobanksStore";
 import { useCheckoutStore } from "./checkoutStore";
 import { useSettingsStore } from "./settingsStore";
-import * as _ from "lodash";
 //@ts-ignore
 import { QueryEMX2 } from "molgenis-components";
 import useErrorHandler from "../composables/errorHandler";
+import flattenOntologyBranch from "../functions/flattenOntologyBranch";
 import { IFilterOption, IOntologyItem } from "../interfaces/interfaces";
 import router from "../router";
 
 const { setError } = useErrorHandler();
+const DIAGNOSIS_AVAILABLE = "Diagnosisavailable";
 
 export const useFiltersStore = defineStore("filtersStore", () => {
   const biobankStore = useBiobanksStore();
@@ -42,6 +44,9 @@ export const useFiltersStore = defineStore("filtersStore", () => {
 
   let filtersReadyToRender = ref(false);
 
+  const indeterminateDiseases = ref<Record<string, boolean>>({});
+  const selectedDiseases = ref<Record<string, boolean>>({});
+
   watch(
     () => settingsStore.configurationFetched,
     () => {
@@ -147,110 +152,59 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     return codeFound || nameFound || labelFound;
   }
 
-  function flattenOntologyBranch(
-    branch: IOntologyItem,
-    flattenedBranches: IOntologyItem[] = []
-  ) {
-    if (!branch.children?.length) {
-      return [...flattenedBranches, branch];
-    } else {
-      for (const child of branch.children) {
-        flattenedBranches = flattenOntologyBranch(child, flattenedBranches);
-        delete child.children;
-        flattenedBranches?.push(child);
-      }
-    }
-    return flattenedBranches;
-  }
-
   function updateOntologyFilter(
     filterName: string,
-    value: any,
+    value: IOntologyItem,
     add: boolean,
     fromBookmark: any = false
   ) {
     bookmarkTriggeredFilter.value = fromBookmark;
 
-    /** value can be a child (single value), or a parent with its children > make it into an array of values */
-    let processedValue = value;
-
-    if (value.children?.length) {
-      let copyBranch = JSON.parse(JSON.stringify(value));
-      let allChildrenValues = flattenOntologyBranch(copyBranch);
-      delete copyBranch.children;
-      allChildrenValues.push(copyBranch);
-
-      let deduplicatedValues: IOntologyItem[] = [];
-      let codesProcessed: string[] = [];
-
-      for (const childValue of allChildrenValues) {
-        if (!codesProcessed.includes(childValue.code)) {
-          deduplicatedValues.push(childValue);
-          codesProcessed.push(childValue.code);
-        }
-      }
-
-      processedValue = deduplicatedValues;
-    }
-
-    const multipleOptions = Array.isArray(processedValue);
+    const childValues = flattenOntologyBranch(value);
+    const processedValues = _.uniqBy(childValues, "code");
 
     if (add) {
-      multipleOptions
-        ? addOntologyOptions(filterName, processedValue)
-        : addOntologyOption(filterName, processedValue);
+      addOntologyOptions(filterName, processedValues);
     } else {
-      multipleOptions
-        ? removeOntologyOptions(filterName, processedValue)
-        : removeOntologyOption(filterName, processedValue);
+      removeOntologyOptions(filterName, processedValues);
     }
   }
 
-  function addOntologyOption(filterName: string, value: IOntologyItem) {
-    if (filters.value[filterName]) {
-      /** sanity check, if it is there already then the job is done */
-      if (
-        filters.value[filterName].some(
-          (option: IOntologyItem) => option.name === value.name
-        )
-      )
-        return;
-
-      filters.value[filterName].push(value);
-    } else {
-      filters.value[filterName] = [value];
-    }
-  }
+  function addOntologyOptions(filterName: string, options: IOntologyItem[]) {
+    let ontologySet = options;
 
-  function addOntologyOptions(filterName: string, value: IOntologyItem[]) {
-    const diagnosisavailableCount = filters.value.Diagnosisavailable?.length
-      ? filters.value.Diagnosisavailable.length
-      : 0;
-    const limit = 50;
-    let ontologySet = value;
-    const slotsRemaining = limit - diagnosisavailableCount;
-    if (
-      filterName === "Diagnosisavailable" &&
-      getFilterType("Diagnosisavailable") === "all"
-    ) {
+    if (getFilterType(DIAGNOSIS_AVAILABLE) === "all") {
+      const diagnosisAvailableCount =
+        filters.value.Diagnosisavailable?.length || 0;
+      const limit = 50;
+      const slotsRemaining = limit - diagnosisAvailableCount;
       ontologySet = ontologySet.slice(0, slotsRemaining);
     }
 
+    const newSelectedDiseases = ontologySet.reduce(
+      (accum: Record<string, boolean>, option: IOntologyItem) => {
+        accum[option.name] = true;
+        return accum;
+      },
+      {}
+    );
+
+    selectedDiseases.value = {
+      ...selectedDiseases.value,
+      ...newSelectedDiseases,
+    };
+
     if (filters.value[filterName]) {
-      const existingValues = filters.value[filterName].map(
-        (option: IOntologyItem) => option.name
-      );
-      const filterOptionsToAdd = ontologySet.filter(
-        (newValue: IOntologyItem) => !existingValues.includes(newValue.name)
+      filters.value[filterName] = _.uniqBy(
+        filters.value[filterName].concat(ontologySet),
+        "name"
       );
-      filters.value[filterName] =
-        filters.value[filterName].concat(filterOptionsToAdd);
     } else {
       filters.value[filterName] = ontologySet;
     }
   }
 
-  /** did not move this to be used in filteroptions because the store is async. */
+  /** did not move this to be used in filterOptions because the store is async. */
   function getOntologyAttributes(filterFacet: Record<string, any>) {
     const { filterLabelAttribute, filterValueAttribute } = filterFacet;
     return [
@@ -268,7 +222,7 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     const { sourceTable, sortColumn, sortDirection } = filterFacet;
     const attributes = getOntologyAttributes(filterFacet);
 
-    let codesToQuery = convertArrayToChunks(codes, 600);
+    const codesToQuery = _.chunk(codes, 600);
     const ontologyResults = [];
 
     for (const codeBlock of codesToQuery) {
@@ -293,36 +247,44 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     return ontologyResults;
   }
 
-  function removeOntologyOption(filterName: string, value: IOntologyItem) {
-    /** can't remove an option which is not present. Jobs done. */
-    if (!filters.value[filterName]) return;
-
-    filters.value[filterName] = filters.value[filterName].filter(
-      (option: IOntologyItem) => option.name !== value.name
-    );
-
-    /** everything is deselected, remove the filter entirely */
-    if (filters.value[filterName].length === 0)
-      delete filters.value[filterName];
-  }
+  function removeOntologyOptions(filterName: string, values: IOntologyItem[]) {
+    if (!filters.value[filterName]) {
+      selectedDiseases.value = {};
+      indeterminateDiseases.value = {};
+      return;
+    }
 
-  function removeOntologyOptions(filterName: string, value: IOntologyItem[]) {
-    /** can't remove an option which is not present. Jobs done. */
-    if (!filters.value[filterName]) return;
+    const valuesToRemove = getValuesToRemove(values);
 
-    const valuesToRemove = value.map((value) => value.name);
+    valuesToRemove.forEach((name: string) => {
+      delete selectedDiseases.value[name];
+    });
 
     filters.value[filterName] = filters.value[filterName].filter(
       (option: IOntologyItem) => !valuesToRemove.includes(option.name)
     );
 
-    /** everything is deselected, remove the filter entirely */
-    if (filters.value[filterName].length === 0)
+    if (filters.value[filterName].length === 0) {
       delete filters.value[filterName];
+    }
+  }
+
+  function getValuesToRemove(values: IOntologyItem[]) {
+    return values.reduce((accum: string[], value) => {
+      accum.push(value.name);
+      if (value.parent) {
+        value.parent.forEach((parent) => {
+          accum.push(parent.name);
+        });
+      }
+      return accum;
+    }, []);
   }
 
   function clearAllFilters() {
     filters.value = {};
+    selectedDiseases.value = {};
+    indeterminateDiseases.value = {};
     createBookmark(
       filters.value,
       checkoutStore.selectedCollections,
@@ -363,16 +325,18 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     return filters.value[filterName];
   }
 
-  function updateFilterType(filterName: string, value: any, fromBookmark: any) {
+  function updateFilterType(
+    filterName: string,
+    value: any,
+    fromBookmark: boolean
+  ) {
     if (
-      filterName === "Diagnosisavailable" &&
+      filterName === DIAGNOSIS_AVAILABLE &&
       (filterType.value[filterName] === "any" ||
         filterType.value[filterName] === undefined) &&
-      filters.value["Diagnosisavailable"]?.length > 50
+      filters.value[filterName]?.length > 50
     ) {
-      filters.value["Diagnosisavailable"] = filters.value[
-        "Diagnosisavailable"
-      ].slice(0, 50);
+      filters.value[filterName] = filters.value[filterName].slice(0, 50);
     }
 
     bookmarkTriggeredFilter.value = fromBookmark;
@@ -409,53 +373,33 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     bookmarkTriggeredFilter.value = false;
   }
 
-  /**
-   * returns for given input: ([11], 5): [[5],[5],[1]]
-   */
-  function convertArrayToChunks(fullArray: any[], chunkSize = 100) {
-    let chunkArray = [];
-    const arrayLength = fullArray.length;
-
-    let chunk = [];
-    if (fullArray.length > 20) {
-      for (let index = 0; index < arrayLength; index++) {
-        chunk.push(fullArray[index]);
-        if (chunk.length === chunkSize) {
-          chunkArray.push(chunk);
-          chunk = [];
-        }
-      }
-      if (chunk.length) {
-        chunkArray.push(chunk);
-        chunk = [];
-      }
-    } else {
-      chunkArray = [fullArray];
-    }
-
-    return chunkArray;
+  function setDiseaseIndeterminate(diseaseName: string, value: boolean): void {
+    indeterminateDiseases.value[diseaseName] = value;
   }
 
   return {
-    facetDetails,
-    updateFilter,
+    checkOntologyDescendantsIfMatches,
     clearAllFilters,
-    updateOntologyFilter,
-    getFilterValue,
-    updateFilterType,
     getFilterType,
+    getFilterValue,
+    getOntologyOptionsForCodes,
     getValuePropertyForFacet,
-    checkOntologyDescendantsIfMatches,
     ontologyItemMatchesQuery,
-    getOntologyOptionsForCodes,
-    filterOptionsCache,
-    hasActiveFilters,
-    hasActiveBiobankOnlyFilters,
-    filters,
+    setDiseaseIndeterminate,
+    updateFilter,
+    updateFilterType,
+    updateOntologyFilter,
+    bookmarkWaitingForApplication,
+    facetDetails,
     filterFacets,
+    filterOptionsCache,
     filtersReady,
-    bookmarkWaitingForApplication,
-    filterTriggeredBookmark,
     filtersReadyToRender,
+    filters,
+    filterTriggeredBookmark,
+    hasActiveFilters,
+    hasActiveBiobankOnlyFilters,
+    indeterminateDiseases,
+    selectedDiseases,
   };
 });
diff --git a/apps/directory/tests/add-remove-from-service-detail.spec.ts b/apps/directory/tests/add-remove-from-service-detail.spec.ts
index b90e70e06fc..3a6113b91ed 100644
--- a/apps/directory/tests/add-remove-from-service-detail.spec.ts
+++ b/apps/directory/tests/add-remove-from-service-detail.spec.ts
@@ -1,7 +1,7 @@
 import { test, expect } from "@playwright/test";
 import { getAppRoute } from "./getAppRoute";
 
-test("add/remove service from service detaill page", async ({ page }) => {
+test("add/remove service from service detail page", async ({ page }) => {
   await page.goto(getAppRoute());
   await page.getByRole("button", { name: "Services" }).first().click();
   await page.getByRole("link", { name: "Biobank Service" }).click();
diff --git a/apps/directory/tests/diseases-filter-tree.spec.ts b/apps/directory/tests/diseases-filter-tree.spec.ts
new file mode 100644
index 00000000000..13b7dd86101
--- /dev/null
+++ b/apps/directory/tests/diseases-filter-tree.spec.ts
@@ -0,0 +1,106 @@
+import { test, expect } from "@playwright/test";
+import { getAppRoute } from "./getAppRoute";
+
+test("The tree state of the Diagnosis Available filter", async ({ page }) => {
+  await page.goto(getAppRoute());
+  await page.locator("#Diagnosisavailable svg").click();
+
+  // Open the first item of the list
+  await page
+    .getByRole("listitem")
+    .filter({ hasText: "▲I Certain infectious and" })
+    .locator("span")
+    .first()
+    .click();
+  await page
+    .getByRole("listitem")
+    .filter({ hasText: /^▲A00-A09 Intestinal infectious diseases$/ })
+    .locator("span")
+    .first()
+    .click();
+  await page
+    .getByRole("listitem")
+    .filter({ hasText: /^▲A00 Cholera$/ })
+    .locator("span")
+    .first()
+    .click();
+
+  // Select A00 Cholera and its children
+  await page
+    .getByRole("listitem")
+    .filter({ hasText: /^▼A00 Cholera$/ })
+    .getByRole("checkbox")
+    .check();
+  await expect(
+    page
+      .getByRole("listitem")
+      .filter({ hasText: /^▼A00 Cholera$/ })
+      .getByRole("checkbox")
+  ).toBeChecked();
+  await expect(
+    page
+      .getByRole("listitem")
+      .filter({ hasText: /^▼A00-A09 Intestinal infectious diseases$/ })
+      .getByRole("checkbox")
+  ).toBeChecked({ indeterminate: true });
+
+  // Deselect its children and check indeterminate state
+  await page
+    .getByRole("listitem")
+    .filter({
+      hasText: /^A00\.0 Cholera due to Vibrio cholerae 01, biovar cholerae$/,
+    })
+    .getByRole("checkbox")
+    .uncheck();
+  await expect(
+    page
+      .getByRole("listitem")
+      .filter({ hasText: /^▼A00 Cholera$/ })
+      .getByRole("checkbox")
+  ).toBeChecked({ indeterminate: true });
+  await expect(
+    page
+      .getByRole("listitem")
+      .filter({ hasText: /^▼A00-A09 Intestinal infectious diseases$/ })
+      .getByRole("checkbox")
+  ).toBeChecked({ indeterminate: true });
+  await expect(
+    page
+      .getByRole("listitem")
+      .filter({ hasText: "▼I Certain infectious and" })
+      .getByRole("checkbox")
+  ).toBeChecked({ indeterminate: true });
+
+  await page
+    .getByRole("listitem")
+    .filter({
+      hasText: /^A00\.1 Cholera due to Vibrio cholerae 01, biovar eltor$/,
+    })
+    .getByRole("checkbox")
+    .uncheck();
+  await page
+    .getByRole("listitem")
+    .filter({ hasText: /^A00\.9 Cholera, unspecified$/ })
+    .getByRole("checkbox")
+    .uncheck();
+
+  // Nothing should be checked
+  await expect(
+    page
+      .getByRole("listitem")
+      .filter({ hasText: /^▼A00 Cholera$/ })
+      .getByRole("checkbox")
+  ).not.toBeChecked({ indeterminate: false });
+  await expect(
+    page
+      .getByRole("listitem")
+      .filter({ hasText: /^▼A00-A09 Intestinal infectious diseases$/ })
+      .getByRole("checkbox")
+  ).not.toBeChecked({ indeterminate: false });
+  await expect(
+    page
+      .getByRole("listitem")
+      .filter({ hasText: "▼I Certain infectious and" })
+      .getByRole("checkbox")
+  ).not.toBeChecked({ indeterminate: false });
+});
diff --git a/apps/yarn.lock b/apps/yarn.lock
index c3eceb95180..0d343c6dfa8 100644
--- a/apps/yarn.lock
+++ b/apps/yarn.lock
@@ -13704,7 +13704,16 @@ string-env-interpolation@1.0.1:
   resolved "https://registry.yarnpkg.com/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz#ad4397ae4ac53fe6c91d1402ad6f6a52862c7152"
   integrity sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==
 
-"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
+
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -13744,8 +13753,7 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
-  name strip-ansi-cjs
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -13766,6 +13774,13 @@ strip-ansi@^4.0.0:
   dependencies:
     ansi-regex "^3.0.0"
 
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
 strip-ansi@^7.0.1, strip-ansi@^7.1.0:
   version "7.1.0"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -15760,8 +15775,7 @@ wordwrap@^1.0.0, wordwrap@~1.0.0:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
   integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
-  name wrap-ansi-cjs
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -15778,6 +15792,15 @@ wrap-ansi@^3.0.1:
     string-width "^2.1.1"
     strip-ansi "^4.0.0"
 
+wrap-ansi@^7.0.0:
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 wrap-ansi@^8.1.0:
   version "8.1.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"


Reviews:
user: login: MaxPostema
body: Only checked code, still needs functional check. 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T09:17:04Z
user: login: dtroelofsprins
body: Tested several cases and I think the marks are now properly shown
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:35:01Z
user: login: connoratrug
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:40:36Z
user: login: connoratrug
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:42:19Z
user: login: connoratrug
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:44:43Z
user: login: connoratrug
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:45:17Z
user: login: connoratrug
body: code looks good ( few details, see comments)
bug is fixed on pr preview

given the about of code and combination of details a regression is needed ( requested change)
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T07:52:03Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR fixes an issue where deselecting a child after selecting its parent causes an invalid state, and it improves the ontology filter handling and bookmark application logic.  
- Refactored the ontology branch flattening and filter update logic.  
- Introduced selectedDiseases and indeterminateDiseases state management.  
- Updated bookmark application and minor component changes.

### Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                                              | Description                                                          |
|-------------------------------------------------------------------|----------------------------------------------------------------------|
| apps/directory/src/stores/filtersStore.ts                         | Refactored flattenOntologyBranch, updateOntologyFilter logic, and state management for selected/indeterminate diseases. |
| apps/directory/src/functions/bookmarkMapper.ts                    | Updated applyBookmark to iterate over options and set bookmarkApplied flag. |
| apps/directory/src/components/partial-views/ServiceDetails.vue      | Removed superfluous imports.                                          |
| apps/directory/src/components/filters/base/TreeComponent.vue        | Removed unused props and event bindings.                              |
| apps/directory/src/components/filters/base/TreeBranchComponent.vue    | Adjusted computed properties and watchers to use new disease selection states. |
| apps/directory/src/App.vue                                          | Reordered imports and removed a redundant applyBookmark call.         |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**apps/directory/src/stores/filtersStore.ts:17**
* [nitpick] The constant 'Diagnosisavailable' might be clearer if written in a consistent case (e.g. 'DiagnosisAvailable') to match naming conventions elsewhere in the project.
```
const DIAGNOSIS_AVAILABLE = "Diagnosisavailable";
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T08:24:53Z
user: login: chinook25
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T08:25:00Z
user: login: chinook25
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T08:26:27Z
user: login: chinook25
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T08:31:11Z
user: login: connoratrug
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T11:22:41Z
user: login: connoratrug
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T07:49:42Z
user: login: esthervanenckevort
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-26T09:20:02Z

Review comments:
diff_hunk: @@ -12,16 +12,16 @@
 </template>
 
 <script setup lang="ts">
+import { useFavicon, usePreferredDark } from "@vueuse/core";
 //@ts-expect-error
 import { Molgenis } from "molgenis-components";
-import { onMounted, computed, ref, watch } from "vue";
+import { computed, onMounted, ref, watch } from "vue";
 import { LocationQuery, useRoute } from "vue-router";
 import Error from "./components/Error.vue";
 import { applyBookmark, createBookmark } from "./functions/bookmarkMapper";
 import { useCheckoutStore } from "./stores/checkoutStore";
 import { useFiltersStore } from "./stores/filtersStore";
 import { useSettingsStore } from "./stores/settingsStore";
-import { useFavicon, usePreferredDark } from "@vueuse/core";
path: apps/directory/src/App.vue
user: login: connoratrug
body: why are these added ?
created_at: 2025-05-23T07:40:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -10,8 +10,8 @@
         type="checkbox"
         :ref="`${option.name}-checkbox`"
         class="mr-1"
-        :checked="selected"
-        :indeterminate.prop="isIndeterminate && !selected"
+        :checked="isSelected"
+        :indeterminate.prop="isIndeterminate"
path: apps/directory/src/components/filters/base/TreeBranchComponent.vue
user: login: connoratrug
body: i don't understand the syntax here, can you explain what's going on here ?
created_at: 2025-05-23T07:42:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,110 +151,74 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     return codeFound || nameFound || labelFound;
   }
 
-  function flattenOntologyBranch(
-    branch: IOntologyItem,
-    flattenedBranches: IOntologyItem[] = []
-  ) {
-    if (!branch.children?.length) {
-      return [...flattenedBranches, branch];
-    } else {
-      for (const child of branch.children) {
-        flattenedBranches = flattenOntologyBranch(child, flattenedBranches);
-        delete child.children;
-        flattenedBranches?.push(child);
+  function flattenOntologyBranch(branch: IOntologyItem) {
+    let stack = [branch];
+    let result: IOntologyItem[] = [];
+    while (stack.length) {
+      const current = stack.pop();
+      if (current?.children) {
+        stack = stack.concat(current.children);
+      }
+      if (current) {
+        result.push(current);
       }
     }
-    return flattenedBranches;
+    return result;
path: apps/directory/src/stores/filtersStore.ts
user: login: connoratrug
body: add test
created_at: 2025-05-23T07:44:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,110 +151,74 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     return codeFound || nameFound || labelFound;
   }
 
-  function flattenOntologyBranch(
-    branch: IOntologyItem,
-    flattenedBranches: IOntologyItem[] = []
-  ) {
-    if (!branch.children?.length) {
-      return [...flattenedBranches, branch];
-    } else {
-      for (const child of branch.children) {
-        flattenedBranches = flattenOntologyBranch(child, flattenedBranches);
-        delete child.children;
-        flattenedBranches?.push(child);
+  function flattenOntologyBranch(branch: IOntologyItem) {
+    let stack = [branch];
+    let result: IOntologyItem[] = [];
+    while (stack.length) {
+      const current = stack.pop();
+      if (current?.children) {
+        stack = stack.concat(current.children);
+      }
+      if (current) {
+        result.push(current);
       }
     }
-    return flattenedBranches;
+    return result;
   }
 
   function updateOntologyFilter(
     filterName: string,
-    value: any,
+    value: IOntologyItem,
     add: boolean,
     fromBookmark: any = false
   ) {
     bookmarkTriggeredFilter.value = fromBookmark;
 
-    /** value can be a child (single value), or a parent with its children > make it into an array of values */
-    let processedValue = value;
-
-    if (value.children?.length) {
-      let copyBranch = JSON.parse(JSON.stringify(value));
-      let allChildrenValues = flattenOntologyBranch(copyBranch);
-      delete copyBranch.children;
-      allChildrenValues.push(copyBranch);
-
-      let deduplicatedValues: IOntologyItem[] = [];
-      let codesProcessed: string[] = [];
-
-      for (const childValue of allChildrenValues) {
-        if (!codesProcessed.includes(childValue.code)) {
-          deduplicatedValues.push(childValue);
-          codesProcessed.push(childValue.code);
-        }
-      }
-
-      processedValue = deduplicatedValues;
-    }
-
-    const multipleOptions = Array.isArray(processedValue);
+    const childValues = flattenOntologyBranch(value);
+    const processedValues = _.uniqBy(childValues, "code");
path: apps/directory/src/stores/filtersStore.ts
user: login: connoratrug
body: is lodash still required ?
created_at: 2025-05-23T07:45:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,110 +151,74 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     return codeFound || nameFound || labelFound;
   }
 
-  function flattenOntologyBranch(
-    branch: IOntologyItem,
-    flattenedBranches: IOntologyItem[] = []
-  ) {
-    if (!branch.children?.length) {
-      return [...flattenedBranches, branch];
-    } else {
-      for (const child of branch.children) {
-        flattenedBranches = flattenOntologyBranch(child, flattenedBranches);
-        delete child.children;
-        flattenedBranches?.push(child);
+  function flattenOntologyBranch(branch: IOntologyItem) {
+    let stack = [branch];
+    let result: IOntologyItem[] = [];
+    while (stack.length) {
+      const current = stack.pop();
+      if (current?.children) {
+        stack = stack.concat(current.children);
path: apps/directory/src/stores/filtersStore.ts
user: login: Copilot
body: Consider using stack.push(...current.children) instead of stack.concat to improve performance and clarity when processing the ontology branch.
```suggestion
        stack.push(...current.children);
```
created_at: 2025-05-23T08:24:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,16 +12,16 @@
 </template>
 
 <script setup lang="ts">
+import { useFavicon, usePreferredDark } from "@vueuse/core";
 //@ts-expect-error
 import { Molgenis } from "molgenis-components";
-import { onMounted, computed, ref, watch } from "vue";
+import { computed, onMounted, ref, watch } from "vue";
 import { LocationQuery, useRoute } from "vue-router";
 import Error from "./components/Error.vue";
 import { applyBookmark, createBookmark } from "./functions/bookmarkMapper";
 import { useCheckoutStore } from "./stores/checkoutStore";
 import { useFiltersStore } from "./stores/filtersStore";
 import { useSettingsStore } from "./stores/settingsStore";
-import { useFavicon, usePreferredDark } from "@vueuse/core";
path: apps/directory/src/App.vue
user: login: chinook25
body: just moved due to auto import update, not a real change in this PR. Where added in an earlier PR that sets the BBMRI favicon on the app.
created_at: 2025-05-23T08:25:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,110 +151,74 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     return codeFound || nameFound || labelFound;
   }
 
-  function flattenOntologyBranch(
-    branch: IOntologyItem,
-    flattenedBranches: IOntologyItem[] = []
-  ) {
-    if (!branch.children?.length) {
-      return [...flattenedBranches, branch];
-    } else {
-      for (const child of branch.children) {
-        flattenedBranches = flattenOntologyBranch(child, flattenedBranches);
-        delete child.children;
-        flattenedBranches?.push(child);
+  function flattenOntologyBranch(branch: IOntologyItem) {
+    let stack = [branch];
+    let result: IOntologyItem[] = [];
+    while (stack.length) {
+      const current = stack.pop();
+      if (current?.children) {
+        stack = stack.concat(current.children);
+      }
+      if (current) {
+        result.push(current);
       }
     }
-    return flattenedBranches;
+    return result;
   }
 
   function updateOntologyFilter(
     filterName: string,
-    value: any,
+    value: IOntologyItem,
     add: boolean,
     fromBookmark: any = false
   ) {
     bookmarkTriggeredFilter.value = fromBookmark;
 
-    /** value can be a child (single value), or a parent with its children > make it into an array of values */
-    let processedValue = value;
-
-    if (value.children?.length) {
-      let copyBranch = JSON.parse(JSON.stringify(value));
-      let allChildrenValues = flattenOntologyBranch(copyBranch);
-      delete copyBranch.children;
-      allChildrenValues.push(copyBranch);
-
-      let deduplicatedValues: IOntologyItem[] = [];
-      let codesProcessed: string[] = [];
-
-      for (const childValue of allChildrenValues) {
-        if (!codesProcessed.includes(childValue.code)) {
-          deduplicatedValues.push(childValue);
-          codesProcessed.push(childValue.code);
-        }
-      }
-
-      processedValue = deduplicatedValues;
-    }
-
-    const multipleOptions = Array.isArray(processedValue);
+    const childValues = flattenOntologyBranch(value);
+    const processedValues = _.uniqBy(childValues, "code");
path: apps/directory/src/stores/filtersStore.ts
user: login: chinook25
body: It already was being used in the directory app, so when I don't have to implement these kind of functions myself I prefer using it. 
created_at: 2025-05-23T08:26:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -147,110 +151,74 @@ export const useFiltersStore = defineStore("filtersStore", () => {
     return codeFound || nameFound || labelFound;
   }
 
-  function flattenOntologyBranch(
-    branch: IOntologyItem,
-    flattenedBranches: IOntologyItem[] = []
-  ) {
-    if (!branch.children?.length) {
-      return [...flattenedBranches, branch];
-    } else {
-      for (const child of branch.children) {
-        flattenedBranches = flattenOntologyBranch(child, flattenedBranches);
-        delete child.children;
-        flattenedBranches?.push(child);
+  function flattenOntologyBranch(branch: IOntologyItem) {
+    let stack = [branch];
+    let result: IOntologyItem[] = [];
+    while (stack.length) {
+      const current = stack.pop();
+      if (current?.children) {
+        stack = stack.concat(current.children);
+      }
+      if (current) {
+        result.push(current);
       }
     }
-    return flattenedBranches;
+    return result;
path: apps/directory/src/stores/filtersStore.ts
user: login: chinook25
body: good idea

created_at: 2025-05-23T08:31:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,105 @@
+import { test, expect } from "@playwright/test";
+
+test("The tree state of the Diagnosis Available filter", async ({ page }) => {
+  await page.goto("http://localhost:5173/#/catalogue");
path: apps/directory/tests/diseases-filter-tree.spec.ts
user: login: connoratrug
body: i think this needs to be replaced with 'await page.goto(getAppRoute());" see other test as example , so is works both locally and on the CI 
created_at: 2025-05-23T11:22:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-23T10:59:35Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=molgenis_molgenis-emx2&pullRequest=5056) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=molgenis_molgenis-emx2&pullRequest=5056&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=molgenis_molgenis-emx2&pullRequest=5056&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=molgenis_molgenis-emx2&pullRequest=5056&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=molgenis_molgenis-emx2&pullRequest=5056&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=molgenis_molgenis-emx2&pullRequest=5056&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=molgenis_molgenis-emx2&pullRequest=5056)


author_association: NONE