Files
speckle-server/packages/frontend-2/lib/presentations/composables/setup.ts
T
Kristaps Fabians Geikins b4a8518aaf feat(fe2): presentation route viewer support (#5473)
* injectable projectId/resourceIdString

* WIP presentation viewer wrapper

* WIP new viewer core

* loading bar styling fix

* resize fix

* presentation state

* minor cleanup

* working view load

* some optimization

* minor adjustment

* resourceIdString fix

* viewer debug flag
2025-09-17 15:00:44 +03:00

149 lines
4.6 KiB
TypeScript

import type { Optional } from '@speckle/shared'
import { resourceBuilder } from '@speckle/shared/viewer/route'
import type { AsyncWritableComputedRef } from '@speckle/ui-components'
import { useQuery } from '@vue/apollo-composable'
import type { Get } from 'type-fest'
import {
SavedViewVisibility,
type ProjectPresentationPageQuery
} from '~/lib/common/generated/gql/graphql'
import { projectPresentationPageQuery } from '~/lib/presentations/graphql/queries'
type ResponseProject = Optional<Get<ProjectPresentationPageQuery, 'project'>>
type ResponseWorkspace = Get<ProjectPresentationPageQuery, 'project.workspace'>
type ResponseGroup = Get<ResponseProject, 'savedViewGroup'>
type ResponseView = NonNullable<Get<ResponseGroup, 'views.items.0'>>
export type InjectablePresentationState = Readonly<{
projectId: AsyncWritableComputedRef<string>
presentationId: AsyncWritableComputedRef<string>
response: {
project: ComputedRef<ResponseProject>
workspace: ComputedRef<ResponseWorkspace>
presentation: ComputedRef<ResponseGroup>
slides: ComputedRef<ResponseView[]>
/**
* We only show public slides
*/
visibleSlides: ComputedRef<ResponseView[]>
}
ui: {
/**
* Current slide to show (0 based indexing). Indexes are based on visibleSlides, not slides.
*/
slideIdx: Ref<number>
slide: ComputedRef<ResponseView | undefined>
}
viewer: {
/**
* The actual resource id string to load in the viewer - built from presentation metadata,
* active slide etc.
*/
resourceIdString: ComputedRef<string>
}
}>
type InitState = Pick<InjectablePresentationState, 'projectId' | 'presentationId'>
type ResponseState = Pick<InjectablePresentationState, 'response'>
type UiState = Pick<InjectablePresentationState, 'ui'>
type ViewerState = Pick<InjectablePresentationState, 'viewer'>
export const InjectablePresentationStateKey: InjectionKey<InjectablePresentationState> =
Symbol('INJECTABLE_PRESENTATION_STATE')
export type UseSetupPresentationParams = {
projectId: AsyncWritableComputedRef<string>
presentationId: AsyncWritableComputedRef<string>
}
const setupStateResponse = (initState: InitState): ResponseState => {
const { result } = useQuery(projectPresentationPageQuery, () => ({
projectId: initState.projectId.value,
savedViewGroupId: initState.presentationId.value,
input: {
limit: 100
}
}))
const project = computed(() => result.value?.project)
const presentation = computed(() => project.value?.savedViewGroup)
const workspace = computed(() => project.value?.workspace)
const slides = computed(() => presentation.value?.views.items || [])
const visibleSlides = computed(() =>
slides.value.filter((view) => view.visibility === SavedViewVisibility.Public)
)
return {
response: {
project,
workspace,
presentation,
slides,
visibleSlides
}
}
}
const setupStateViewer = (initState: ResponseState & UiState): ViewerState => {
const {
response: { presentation },
ui: { slideIdx }
} = initState
const resourceIdString = computed(() => {
const slides = presentation.value?.views.items || []
return resourceBuilder()
.addResources(slides.at(slideIdx.value)?.resourceIdString || '')
.toString()
})
return { viewer: { resourceIdString } }
}
const setupStateUi = (initState: ResponseState): UiState => {
const slideIdx = ref(0)
const slide = computed(() => {
const slides = initState.response.visibleSlides.value
return slides.at(slideIdx.value)
})
return {
ui: {
slideIdx,
slide
}
}
}
export const useSetupPresentationState = (params: UseSetupPresentationParams) => {
const initState: InitState = params
const responseState = setupStateResponse(initState)
const uiState = setupStateUi(responseState)
const viewerState = setupStateViewer({ ...responseState, ...uiState })
const state: InjectablePresentationState = {
...initState,
...responseState,
...uiState,
...viewerState
}
// We don't want the state to ever be proxified (e.g. when passed through props),
// cause that will break composables (refs will be automatically unwrapped as if
// they're accessed in a template)
const rawState = markRaw(state)
provide(InjectablePresentationStateKey, rawState)
return rawState
}
export const useInjectedPresentationState = (): InjectablePresentationState => {
// we're forcing TS to ignore the scenario where this data can't be found and returns undefined
// to avoid unnecessary null checks everywhere
const state = inject(InjectablePresentationStateKey) as InjectablePresentationState
return state
}