feat: shared loading state + cleanup loading state management (#5835)
* feat: implement shared loading bar component and polished loading states across the app * feat: align loading states + ensureQueryData changes * fix: lint + bugs * fix: skeleton for manage servers page * fix: merge conflict fix
This commit is contained in:
@@ -13,6 +13,9 @@ export * from './server-console'
|
||||
export * from './server-manage-core-runtime'
|
||||
export * from './sticky-observer'
|
||||
export * from './terminal'
|
||||
export * from './use-loading-bar-token'
|
||||
export * from './use-loading-state-core'
|
||||
export * from './use-ready-state'
|
||||
export * from './use-server-image'
|
||||
export * from './use-server-project'
|
||||
export * from './virtual-scroll'
|
||||
|
||||
43
packages/ui/src/composables/use-loading-bar-token.ts
Normal file
43
packages/ui/src/composables/use-loading-bar-token.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { onBeforeUnmount, watch } from 'vue'
|
||||
|
||||
import { injectLoadingState } from '#ui/providers/loading-state'
|
||||
|
||||
/**
|
||||
* Register a `LoadingBar` token for as long as `pending` is truthy.
|
||||
*
|
||||
* Use this when the component that owns the load is not the natural place
|
||||
* to mount a `<ReadyTransition>` (e.g. a page root with a complex v-if
|
||||
* cascade where wrapping the template is awkward). `<ReadyTransition>`
|
||||
* remains the preferred API when it fits.
|
||||
*
|
||||
* Safe to call without a provider mounted; becomes a no-op.
|
||||
*/
|
||||
export function useLoadingBarToken(pending: Ref<boolean>): void {
|
||||
const loadingState = injectLoadingState(null)
|
||||
if (!loadingState) return
|
||||
|
||||
let token: symbol | null = null
|
||||
|
||||
function release() {
|
||||
if (token) {
|
||||
loadingState.end(token)
|
||||
token = null
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
pending,
|
||||
(now) => {
|
||||
if (typeof window === 'undefined') return
|
||||
if (now && !token) {
|
||||
token = loadingState.begin()
|
||||
} else if (!now) {
|
||||
release()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onBeforeUnmount(release)
|
||||
}
|
||||
61
packages/ui/src/composables/use-loading-state-core.ts
Normal file
61
packages/ui/src/composables/use-loading-state-core.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
|
||||
import type { LoadingStateProvider } from '#ui/providers/loading-state'
|
||||
|
||||
export interface LoadingStateCoreOptions {
|
||||
/** Initial value of the host kill-switch. Default: true. */
|
||||
barEnabled?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a token-based `LoadingStateProvider` implementation.
|
||||
*
|
||||
* Multiple `ReadyTransition` instances (or any caller) can hold tokens at the
|
||||
* same time; the bar stays visible while at least one is live. `end(token)`
|
||||
* is idempotent so a stale token release after unmount is harmless.
|
||||
*
|
||||
* SSR safe: timers and DOM access are deferred to component code; this core
|
||||
* is pure reactive state.
|
||||
*/
|
||||
export function createLoadingStateCore(opts: LoadingStateCoreOptions = {}): LoadingStateProvider {
|
||||
const tokens = shallowRef<Set<symbol>>(new Set())
|
||||
const barEnabled = ref(opts.barEnabled ?? true)
|
||||
const pending = computed(() => tokens.value.size > 0)
|
||||
|
||||
function begin(): symbol {
|
||||
const token = Symbol('loading-state-token')
|
||||
const next = new Set(tokens.value)
|
||||
next.add(token)
|
||||
tokens.value = next
|
||||
return token
|
||||
}
|
||||
|
||||
function end(token: symbol): void {
|
||||
if (!tokens.value.has(token)) return
|
||||
const next = new Set(tokens.value)
|
||||
next.delete(token)
|
||||
tokens.value = next
|
||||
}
|
||||
|
||||
function beginManual(durationMs = 500): void {
|
||||
const token = begin()
|
||||
if (typeof window === 'undefined') {
|
||||
end(token)
|
||||
return
|
||||
}
|
||||
window.setTimeout(() => end(token), durationMs)
|
||||
}
|
||||
|
||||
function setEnabled(enabled: boolean): void {
|
||||
barEnabled.value = enabled
|
||||
}
|
||||
|
||||
return {
|
||||
pending,
|
||||
barEnabled,
|
||||
begin,
|
||||
end,
|
||||
beginManual,
|
||||
setEnabled,
|
||||
}
|
||||
}
|
||||
24
packages/ui/src/composables/use-ready-state.ts
Normal file
24
packages/ui/src/composables/use-ready-state.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { DefaultError, UseQueryReturnType } from '@tanstack/vue-query'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
/** Subset of {@link UseQueryReturnType} passed to {@link useReadyState}. */
|
||||
export type ReadyStateQuery<TData, TError = DefaultError> = Pick<
|
||||
UseQueryReturnType<TData, TError>,
|
||||
'isLoading' | 'data'
|
||||
>
|
||||
|
||||
/**
|
||||
* Returns true while a query is loading for the FIRST time (no cached data yet).
|
||||
*
|
||||
* Excludes background refetches and refetch-on-window-focus by design — those
|
||||
* have `isLoading === false` once data exists in the cache, so `ReadyTransition`
|
||||
* stays open and the loading bar stays silent.
|
||||
*
|
||||
* Pair with `<ReadyTransition :pending="var which is useReadyState(query)" />`.
|
||||
*/
|
||||
export function useReadyState<TData, TError = DefaultError>(
|
||||
query: ReadyStateQuery<TData, TError>,
|
||||
): Readonly<Ref<boolean>> {
|
||||
return computed(() => query.isLoading.value && query.data.value === undefined)
|
||||
}
|
||||
Reference in New Issue
Block a user