mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:31:48 +00:00
This commit adds a reusable and accessible single select tree view, and is used in the repo page to render the file tree sidebar. Overview: `TreeView.svelte` and `TreeNode.svelte` provide the generic tree implementation. They depend on `TreeState`, which needs to be provided as context by the caller, and `TreeProvider` which needs to be passed as prop. `FileTree.svelte` uses those components to provide the sidebar file tree. The code layout data loader fetches the file data for the current path (or the "highest" path visited for the current directory, similar to how the current web app works), which the code layout component uses to create a `FileTreeProvider`. Noteworthy: - Tree state (focused, selected, expanded nodes) is stored globally (per repo) so that we can restore the state when opening/closing the sidebar or navigating back to the repo. - I set the maximum number of fetched and shown nodes to 1000. The more nodes we show the longer it takes for the (sub-)tree to be visible (we have that problem in the current web app too) and I see no benefit from showing 2500 instead of 1000 nodes (but we can always tweak this). - Update performance (selecting nodes, change focused node) is really good (irrespective of the number of rendered nodes (see video, compare with current web app). - I tried to make it fully keyboard accessible. I haven't implemented some of the more "exotic" `aria-*` props like `aria-posinset`. - Renders info node when not all nodes have been fetched/rendered (like in the current web app) What it doesn't do (yet): - It doesn't handle single directory children. Supporting this would require changes to data loading and representation. - It doesn't render submodules. - Multiselection - Unit tests https://github.com/sourcegraph/sourcegraph/assets/179026/158dd428-9463-4098-bf68-fff4897e899a ## Test plan Open repository, select a file and use arrow keys to navigate the file tree.
91 lines
2.8 KiB
TypeScript
91 lines
2.8 KiB
TypeScript
import type { Observable } from 'rxjs'
|
|
import { shareReplay } from 'rxjs/operators'
|
|
import { type Readable, type Writable, writable, get, type Unsubscriber } from 'svelte/store'
|
|
|
|
export type LoadingData<D, E = Error> =
|
|
| { loading: true }
|
|
| { loading: false; data: D; error: null }
|
|
| { loading: false; data: null; error: E }
|
|
|
|
/**
|
|
* Converts a promise to a readable store which emits data, loading and error states.
|
|
* Sometimes load functions return deferred promises and the data needs to be
|
|
* "post processed" in code (i.e. not using {#await}).
|
|
* Usually when working with async data one has to be careful with outdated data.
|
|
* If the load function has been called again we don't want to process the
|
|
* previous data anymore.
|
|
* Using a (reactive) store makes that simpler since Svelte will automatically unsubscribe
|
|
* when the store changes.
|
|
*/
|
|
export function asStore<T, E = Error>(
|
|
promise: Promise<T>
|
|
): Readable<LoadingData<T, E>> & { set(promise: Promise<T>): void } {
|
|
const { subscribe, set } = writable<LoadingData<T, E>>({ loading: true })
|
|
|
|
function process(currentPromise: Promise<T>) {
|
|
promise = currentPromise
|
|
currentPromise.then(
|
|
result => {
|
|
if (currentPromise === promise) {
|
|
set({ loading: false, data: result, error: null })
|
|
}
|
|
},
|
|
error => {
|
|
if (currentPromise === promise) {
|
|
set({ loading: false, data: null, error })
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
process(promise)
|
|
|
|
return {
|
|
subscribe,
|
|
set: process,
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to convert an Observable to a Svelte Readable. Useful when a
|
|
* real Readable is needed to satisfy an interface.
|
|
*/
|
|
export function readableObservable<T>(observable: Observable<T>): Readable<T> {
|
|
const sharedObservable = observable.pipe(shareReplay(1))
|
|
return {
|
|
subscribe(subscriber) {
|
|
const subscription = sharedObservable.subscribe(subscriber)
|
|
return () => subscription.unsubscribe()
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a helper store that syncs with the currently set store.
|
|
*/
|
|
export function createForwardStore<T>(store: Writable<T>): Writable<T> & { updateStore(store: Writable<T>): void } {
|
|
const { subscribe, set } = writable<T>(get(store), () => link(store))
|
|
|
|
let unsubscribe: Unsubscriber | null = null
|
|
function link(store: Writable<T>): Unsubscriber {
|
|
unsubscribe?.()
|
|
return (unsubscribe = store.subscribe(set))
|
|
}
|
|
|
|
return {
|
|
subscribe,
|
|
set(value) {
|
|
store.set(value)
|
|
},
|
|
update(value) {
|
|
store.update(value)
|
|
},
|
|
updateStore(newStore) {
|
|
if (newStore !== store) {
|
|
store = newStore
|
|
link(store)
|
|
}
|
|
},
|
|
}
|
|
}
|