Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Preserve tree expansion state #22347

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/app/cypress/e2e/specs_list_latest_runs.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,42 @@ describe('ACI - Latest runs and Average duration', { viewportWidth: 1200 }, () =
specShouldShow('z008.spec.js', ['gray-300', 'gray-300', 'jade-400'], 'RUNNING')
cy.get(averageDurationSelector('z008.spec.js')).contains('2:03')
})

describe('preserving tree expansion state', () => {
it('should preserve state when row data is updated without additions/deletions', () => {
// Collapse a directory
cy.get('button[data-cy="row-directory-depth-1"]').first()
.should('have.attr', 'aria-expanded', 'true')
.click()
.should('have.attr', 'aria-expanded', 'false')

// Trigger cloud specs list change by scrolling
cy.get('.spec-list-container')
.scrollTo('bottom', { duration: 500 })
.wait(100)
.scrollTo('top', { duration: 500 })

// Directory should still be collapsed
cy.get('button[data-cy="row-directory-depth-1"]').first()
.should('have.attr', 'aria-expanded', 'false')
})

it('should expand all directories when search is performed', () => {
// Collapse a directory
cy.get('button[data-cy="row-directory-depth-0"]').first()
.should('have.attr', 'aria-expanded', 'true')
.click()
.should('have.attr', 'aria-expanded', 'false')
.then((dir) => {
// Perform a search/filter operation
cy.findByLabelText('Search Specs').type(dir.text()[0])
})

// Previously-collapsed directory should automatically expand
cy.get('button[data-cy="row-directory-depth-0"]').first()
.should('have.attr', 'aria-expanded', 'true')
})
})
})

context('polling indicates new data', () => {
Expand Down
11 changes: 10 additions & 1 deletion packages/app/src/specs/SpecsList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,18 @@ const specs = computed(() => {
return fuzzySortSpecs(specs2, debouncedSearchString.value)
})

// Maintain a cache of what tree directories are expanded/collapsed so the tree state is visually preserved
// when specs list data is updated on scroll (e.g., latest-runs & average-duration data loading async)
const treeExpansionCache = ref(new Map<string, boolean>())

// When search value changes reset the tree expansion cache so that any collapsed directories re-expand
watch(() => search.value, () => treeExpansionCache.value.clear())
// When specs are added or removed reset the tree expansion cache so that any collapsed directories re-expand
watch(() => specs.value.length, () => treeExpansionCache.value.clear())

const collapsible = computed(() => {
return useCollapsibleTree(
buildSpecTree<FuzzyFoundSpec<SpecsListFragment>>(specs.value), { dropRoot: true },
buildSpecTree<FuzzyFoundSpec<SpecsListFragment>>(specs.value), { dropRoot: true, cache: treeExpansionCache.value },
)
})
const treeSpecList = computed(() => collapsible.value.tree.filter(((item) => !item.hidden.value)))
Expand Down
17 changes: 14 additions & 3 deletions packages/frontend-shared/src/composables/useCollapsibleTree.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { ComputedRef, Ref } from 'vue'
import { computed } from 'vue'
import { computed, watch } from 'vue'
import { useToggle } from '@vueuse/core'

export type RawNode <T> = {
id: string
name: string
children: RawNode<T>[]
}
Expand All @@ -23,6 +24,12 @@ export type UseCollapsibleTreeNode <T extends RawNode<T>> = {
export interface UseCollapsibleTreeOptions {
expandInitially?: boolean
dropRoot?: boolean
/**
* Provide a long-lived cache to preserve directory collapse state across tree re-builds.
* This can be useful when row data is updating but doesn't represent a change to the
* structure of the tree.
*/
cache?: Map<string, boolean>
}

function collectRoots<T extends RawNode<T>> (node: UseCollapsibleTreeNode<T> | null, acc: UseCollapsibleTreeNode<T>[] = []) {
Expand All @@ -38,9 +45,13 @@ function collectRoots<T extends RawNode<T>> (node: UseCollapsibleTreeNode<T> | n
}

export const useCollapsibleTreeNode = <T extends RawNode<T>>(rawNode: T, options: UseCollapsibleTreeOptions, depth: number, parent: UseCollapsibleTreeNode<T> | null): UseCollapsibleTreeNode<T> => {
const { cache, expandInitially } = options
const treeNode = rawNode as UseCollapsibleTreeNode<T>
const roots = parent ? collectRoots<T>(parent) : []
const [expanded, toggle] = useToggle(!!options.expandInitially)
const [expanded, toggle] = useToggle(cache?.get(rawNode.id) ?? !!expandInitially)

watch(() => expanded.value, (newValue) => cache?.set(rawNode.id, newValue))

const hidden = computed(() => {
return !!roots.find((r) => r.expanded.value === false)
})
Expand Down Expand Up @@ -92,7 +103,7 @@ function sortTree<T extends RawNode<T>> (tree: T) {
}

export function useCollapsibleTree <T extends RawNode<T>> (tree: T, options: UseCollapsibleTreeOptions = {}) {
options.expandInitially = options.expandInitially || true
options.expandInitially = options.expandInitially ?? true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

sortTree(tree)
const collapsibleTree = buildTree<T>(tree, options)

Expand Down