fix: various presentations mode fixes related to resetting (#5635)
* better workspace feature flag ops * user activity is correctly tracked * more fixes
This commit is contained in:
committed by
GitHub
parent
42d0237952
commit
d394e1cd9b
@@ -74,7 +74,6 @@ import { SpeckleViewer } from '@speckle/shared'
|
|||||||
import { keyboardClick } from '@speckle/ui-components'
|
import { keyboardClick } from '@speckle/ui-components'
|
||||||
import { graphql } from '~/lib/common/generated/gql/gql'
|
import { graphql } from '~/lib/common/generated/gql/gql'
|
||||||
import type { HeaderNavShare_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
import type { HeaderNavShare_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||||
import { useCopyModelLink } from '~~/lib/projects/composables/modelManagement'
|
|
||||||
|
|
||||||
graphql(`
|
graphql(`
|
||||||
fragment HeaderNavShare_Project on Project {
|
fragment HeaderNavShare_Project on Project {
|
||||||
@@ -90,7 +89,6 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { copy } = useClipboard()
|
const { copy } = useClipboard()
|
||||||
const copyModelLink = useCopyModelLink()
|
|
||||||
const menuButtonId = useId()
|
const menuButtonId = useId()
|
||||||
|
|
||||||
const embedDialogOpen = ref(false)
|
const embedDialogOpen = ref(false)
|
||||||
@@ -99,37 +97,16 @@ const parsedResourceIds = computed(() =>
|
|||||||
SpeckleViewer.ViewerRoute.parseUrlParameters(props.resourceIdString)
|
SpeckleViewer.ViewerRoute.parseUrlParameters(props.resourceIdString)
|
||||||
)
|
)
|
||||||
|
|
||||||
const firstResource = computed(() => parsedResourceIds.value[0] || {})
|
|
||||||
|
|
||||||
const versionId = computed(() => {
|
|
||||||
if (SpeckleViewer.ViewerRoute.isModelResource(firstResource.value)) {
|
|
||||||
return firstResource.value.versionId
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const modelId = computed(() => {
|
|
||||||
if (SpeckleViewer.ViewerRoute.isModelResource(firstResource.value)) {
|
|
||||||
return firstResource.value.modelId // Assuming your firstResource object has a modelId property
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const isFederated = computed(() => parsedResourceIds.value.length > 1)
|
const isFederated = computed(() => parsedResourceIds.value.length > 1)
|
||||||
|
|
||||||
const handleCopyId = () => {
|
const handleCopyId = async () => {
|
||||||
copy(props.resourceIdString, { successMessage: 'ID copied to clipboard' })
|
await copy(props.resourceIdString, { successMessage: 'ID copied to clipboard' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
const handleCopyLink = async () => {
|
||||||
const modelIdValue = modelId.value
|
if (import.meta.server) return
|
||||||
const versionIdValue = versionId.value ? versionId.value : undefined
|
await copy(window.location.href, {
|
||||||
void copyModelLink({
|
successMessage: 'Copied link to clipboard'
|
||||||
model: {
|
|
||||||
projectId: props.project.id,
|
|
||||||
id: modelIdValue
|
|
||||||
},
|
|
||||||
versionId: versionIdValue
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { LucideChevronLeft, LucideChevronRight, LucideRotateCcw } from 'lucide-v
|
|||||||
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
||||||
import { clamp } from 'lodash-es'
|
import { clamp } from 'lodash-es'
|
||||||
import { useEventListener } from '@vueuse/core'
|
import { useEventListener } from '@vueuse/core'
|
||||||
|
import { useResetViewUtils } from '~/lib/presentations/composables/utils'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
hideUi?: boolean
|
hideUi?: boolean
|
||||||
@@ -36,8 +37,9 @@ defineProps<{
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
ui: { slideIdx: currentVisibleIndex, slideCount },
|
ui: { slideIdx: currentVisibleIndex, slideCount },
|
||||||
viewer: { resetView, hasViewChanged }
|
viewer: { hasViewChanged }
|
||||||
} = useInjectedPresentationState()
|
} = useInjectedPresentationState()
|
||||||
|
const { resetView } = useResetViewUtils()
|
||||||
|
|
||||||
const disablePrevious = computed(() => currentVisibleIndex.value === 0)
|
const disablePrevious = computed(() => currentVisibleIndex.value === 0)
|
||||||
const disableNext = computed(() =>
|
const disableNext = computed(() =>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { graphql } from '~~/lib/common/generated/gql'
|
|||||||
import type { PresentationSlideListSlide_SavedViewFragment } from '~~/lib/common/generated/gql/graphql'
|
import type { PresentationSlideListSlide_SavedViewFragment } from '~~/lib/common/generated/gql/graphql'
|
||||||
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
||||||
import { useAuthManager } from '~~/lib/auth/composables/auth'
|
import { useAuthManager } from '~~/lib/auth/composables/auth'
|
||||||
|
import { useResetViewUtils } from '~/lib/presentations/composables/utils'
|
||||||
|
|
||||||
graphql(`
|
graphql(`
|
||||||
fragment PresentationSlideListSlide_SavedView on SavedView {
|
fragment PresentationSlideListSlide_SavedView on SavedView {
|
||||||
@@ -42,10 +43,10 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ui: { slideIdx: currentSlideIdx, slide: currentSlide },
|
ui: { slideIdx: currentSlideIdx, slide: currentSlide }
|
||||||
viewer: { resetView }
|
|
||||||
} = useInjectedPresentationState()
|
} = useInjectedPresentationState()
|
||||||
const { presentationToken } = useAuthManager()
|
const { presentationToken } = useAuthManager()
|
||||||
|
const { resetView } = useResetViewUtils()
|
||||||
|
|
||||||
const isCurrentSlide = computed(() => currentSlide.value?.id === props.slide.id)
|
const isCurrentSlide = computed(() => currentSlide.value?.id === props.slide.id)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<template>
|
||||||
|
<div class="presentation-viewer-post-setup h-full"><slot /></div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePresentationViewerPostSetup } from '~/lib/presentations/composables/viewerPostSetup'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The only point of this component is to get around the stupid limitation where a component that injects() also can't provide() the same stuff back...
|
||||||
|
*/
|
||||||
|
|
||||||
|
usePresentationViewerPostSetup()
|
||||||
|
</script>
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<ViewerStateSetup :init-params="initParams">
|
<ViewerStateSetup :init-params="initParams">
|
||||||
<PresentationViewerSetup
|
<PresentationViewerPostSetup>
|
||||||
@loading-change="onLoadingChange"
|
<PresentationViewerSetup
|
||||||
@progress-change="onProgressChange"
|
@loading-change="onLoadingChange"
|
||||||
/>
|
@progress-change="onProgressChange"
|
||||||
|
/>
|
||||||
|
</PresentationViewerPostSetup>
|
||||||
</ViewerStateSetup>
|
</ViewerStateSetup>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ const parentEl = ref(null as Nullable<HTMLElement>)
|
|||||||
const { isLoggedIn } = useActiveUser()
|
const { isLoggedIn } = useActiveUser()
|
||||||
const viewerState = useInjectedViewerState()
|
const viewerState = useInjectedViewerState()
|
||||||
const { sessionId } = viewerState
|
const { sessionId } = viewerState
|
||||||
const { users } = useViewerUserActivityTracking({ parentEl })
|
const { users } = useViewerUserActivityTracking({ anchoredPointsParentEl: parentEl })
|
||||||
const { isOpenThread, open, closeAllThreads } = useThreadUtilities()
|
const { isOpenThread, open, closeAllThreads } = useThreadUtilities()
|
||||||
const {
|
const {
|
||||||
filters: { hasAnyFiltersApplied },
|
filters: { hasAnyFiltersApplied },
|
||||||
|
|||||||
@@ -135,9 +135,11 @@ export type AddDomainToWorkspaceInput = {
|
|||||||
workspaceId: Scalars['ID']['input'];
|
workspaceId: Scalars['ID']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Either the ID or slug must be set */
|
||||||
export type AdminAccessToWorkspaceFeatureInput = {
|
export type AdminAccessToWorkspaceFeatureInput = {
|
||||||
featureFlagName: WorkspaceFeatureFlagName;
|
featureFlagName: WorkspaceFeatureFlagName;
|
||||||
workspaceId: Scalars['ID']['input'];
|
workspaceId?: InputMaybe<Scalars['ID']['input']>;
|
||||||
|
workspaceSlug?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminInviteList = {
|
export type AdminInviteList = {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function wrapRefWithTracking<R extends Ref<unknown>>(
|
|||||||
},
|
},
|
||||||
set: (newVal) => {
|
set: (newVal) => {
|
||||||
if (!readsOnly) {
|
if (!readsOnly) {
|
||||||
logger().debug(`debugging: '${name}' written to`, newVal, getTrace())
|
logger().debug(`debugging: '${name}' written to`, { newVal }, getTrace())
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.value = newVal
|
ref.value = newVal
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import {
|
|||||||
SavedViewVisibility
|
SavedViewVisibility
|
||||||
} from '~/lib/common/generated/gql/graphql'
|
} from '~/lib/common/generated/gql/graphql'
|
||||||
import { projectPresentationPageQuery } from '~/lib/presentations/graphql/queries'
|
import { projectPresentationPageQuery } from '~/lib/presentations/graphql/queries'
|
||||||
import { useEventBus } from '~/lib/core/composables/eventBus'
|
|
||||||
import { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus'
|
|
||||||
import { useProjectSavedViewsUpdateTracking } from '~/lib/viewer/composables/savedViews/subscriptions'
|
import { useProjectSavedViewsUpdateTracking } from '~/lib/viewer/composables/savedViews/subscriptions'
|
||||||
|
|
||||||
type ResponseProject = Optional<Get<ProjectPresentationPageQuery, 'project'>>
|
type ResponseProject = Optional<Get<ProjectPresentationPageQuery, 'project'>>
|
||||||
@@ -44,10 +42,6 @@ export type InjectablePresentationState = Readonly<{
|
|||||||
* active slide etc.
|
* active slide etc.
|
||||||
*/
|
*/
|
||||||
resourceIdString: ComputedRef<string>
|
resourceIdString: ComputedRef<string>
|
||||||
/**
|
|
||||||
* Reset the current view to the saved view state of the current slide
|
|
||||||
*/
|
|
||||||
resetView: () => void
|
|
||||||
/**
|
/**
|
||||||
* Whether the current view has been changed from the saved view state
|
* Whether the current view has been changed from the saved view state
|
||||||
*/
|
*/
|
||||||
@@ -101,8 +95,6 @@ const setupStateViewer = (initState: ResponseState & UiState): ViewerState => {
|
|||||||
ui: { slideIdx }
|
ui: { slideIdx }
|
||||||
} = initState
|
} = initState
|
||||||
|
|
||||||
const { emit, on } = useEventBus()
|
|
||||||
|
|
||||||
const hasViewChanged = ref(false)
|
const hasViewChanged = ref(false)
|
||||||
|
|
||||||
const resourceIdString = computed(() => {
|
const resourceIdString = computed(() => {
|
||||||
@@ -113,32 +105,9 @@ const setupStateViewer = (initState: ResponseState & UiState): ViewerState => {
|
|||||||
.toString()
|
.toString()
|
||||||
})
|
})
|
||||||
|
|
||||||
const resetView = () => {
|
|
||||||
const slides = presentation.value?.views.items || []
|
|
||||||
const currentSlide = slides.at(slideIdx.value)
|
|
||||||
|
|
||||||
if (!currentSlide?.id) return
|
|
||||||
|
|
||||||
emit(ViewerEventBusKeys.ApplySavedView, {
|
|
||||||
id: currentSlide.id,
|
|
||||||
loadOriginal: false
|
|
||||||
})
|
|
||||||
|
|
||||||
hasViewChanged.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
on(ViewerEventBusKeys.UserChangedOpenedView, () => {
|
|
||||||
hasViewChanged.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(slideIdx, () => {
|
|
||||||
hasViewChanged.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
viewer: {
|
viewer: {
|
||||||
resourceIdString,
|
resourceIdString,
|
||||||
resetView,
|
|
||||||
hasViewChanged
|
hasViewChanged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
||||||
|
|
||||||
|
export const useResetViewUtils = () => {
|
||||||
|
const {
|
||||||
|
response: { presentation },
|
||||||
|
ui: { slideIdx },
|
||||||
|
viewer: { hasViewChanged }
|
||||||
|
} = useInjectedPresentationState()
|
||||||
|
|
||||||
|
const { emit } = useEventBus()
|
||||||
|
|
||||||
|
const resetView = () => {
|
||||||
|
const slides = presentation.value?.views.items || []
|
||||||
|
const currentSlide = slides.at(slideIdx.value)
|
||||||
|
|
||||||
|
if (!currentSlide?.id) return
|
||||||
|
|
||||||
|
emit(ViewerEventBusKeys.ApplySavedView, {
|
||||||
|
id: currentSlide.id,
|
||||||
|
loadOriginal: false
|
||||||
|
})
|
||||||
|
|
||||||
|
hasViewChanged.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return { resetView }
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useInjectedPresentationState } from '~/lib/presentations/composables/setup'
|
||||||
|
import { useViewerUserActivityTracking } from '~/lib/viewer/composables/activity'
|
||||||
|
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||||
|
|
||||||
|
const useActivityTrackingIntegration = () => {
|
||||||
|
useViewerUserActivityTracking({
|
||||||
|
trackInternallyOnly: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const useResetTrackingIntegration = () => {
|
||||||
|
const { on } = useEventBus()
|
||||||
|
const {
|
||||||
|
ui: { slideIdx },
|
||||||
|
viewer: { hasViewChanged }
|
||||||
|
} = useInjectedPresentationState()
|
||||||
|
const {
|
||||||
|
ui: {
|
||||||
|
savedViews: { savedViewStateId }
|
||||||
|
}
|
||||||
|
} = useInjectedViewerState()
|
||||||
|
|
||||||
|
on(ViewerEventBusKeys.UserChangedOpenedView, () => {
|
||||||
|
hasViewChanged.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
slideIdx,
|
||||||
|
() => {
|
||||||
|
savedViewStateId.value = undefined
|
||||||
|
hasViewChanged.value = false
|
||||||
|
},
|
||||||
|
{ flush: 'sync' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Post setup work to run after the viewer (and presentation) states have been set up
|
||||||
|
*/
|
||||||
|
export const usePresentationViewerPostSetup = () => {
|
||||||
|
if (import.meta.server) return
|
||||||
|
useActivityTrackingIntegration()
|
||||||
|
useResetTrackingIntegration()
|
||||||
|
}
|
||||||
@@ -261,10 +261,19 @@ export type UserActivityModel = Merge<
|
|||||||
/**
|
/**
|
||||||
* Track other user activity and emit viewing/disconnected updates
|
* Track other user activity and emit viewing/disconnected updates
|
||||||
*/
|
*/
|
||||||
export function useViewerUserActivityTracking(params: {
|
export function useViewerUserActivityTracking(
|
||||||
parentEl: Ref<Nullable<HTMLElement>>
|
params?: Partial<{
|
||||||
}) {
|
/**
|
||||||
const { parentEl } = params
|
* Set if you need users to be positioned correctly in viewer world space in an overlaid anchored points element
|
||||||
|
*/
|
||||||
|
anchoredPointsParentEl: Ref<Nullable<HTMLElement>>
|
||||||
|
/**
|
||||||
|
* Whether to only track viewer state changes, without broadcasting it to other users or getting updates from them
|
||||||
|
*/
|
||||||
|
trackInternallyOnly: boolean
|
||||||
|
}>
|
||||||
|
) {
|
||||||
|
const { anchoredPointsParentEl: parentEl, trackInternallyOnly } = params || {}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
projectId,
|
projectId,
|
||||||
@@ -275,10 +284,27 @@ export function useViewerUserActivityTracking(params: {
|
|||||||
} = useInjectedViewerState()
|
} = useInjectedViewerState()
|
||||||
const { isLoggedIn } = useActiveUser()
|
const { isLoggedIn } = useActiveUser()
|
||||||
const { triggerNotification } = useGlobalToast()
|
const { triggerNotification } = useGlobalToast()
|
||||||
|
const { update } = useViewerRealtimeActivityTracker()
|
||||||
const sendUpdate = useViewerUserActivityBroadcasting()
|
const sendUpdate = useViewerUserActivityBroadcasting()
|
||||||
const { isEnabled: isEmbedEnabled } = useEmbed()
|
const { isEnabled: isEmbedEnabled } = useEmbed()
|
||||||
const { activeUser } = useActiveUser()
|
const { activeUser } = useActiveUser()
|
||||||
|
|
||||||
|
const processViewerViewing = async () => {
|
||||||
|
if (trackInternallyOnly) {
|
||||||
|
update({ status: ViewerUserActivityStatus.Viewing })
|
||||||
|
} else {
|
||||||
|
await sendUpdate.emitViewing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processViewerDisconnected = async () => {
|
||||||
|
if (trackInternallyOnly) {
|
||||||
|
update({ status: ViewerUserActivityStatus.Disconnected })
|
||||||
|
} else {
|
||||||
|
await sendUpdate.emitDisconnected()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: For some reason subscription is set up twice? Vue Apollo bug?
|
// TODO: For some reason subscription is set up twice? Vue Apollo bug?
|
||||||
const { onResult: onUserActivity } = useSubscription(
|
const { onResult: onUserActivity } = useSubscription(
|
||||||
onViewerUserActivityBroadcastedSubscription,
|
onViewerUserActivityBroadcastedSubscription,
|
||||||
@@ -291,7 +317,7 @@ export function useViewerUserActivityTracking(params: {
|
|||||||
sessionId: sessionId.value
|
sessionId: sessionId.value
|
||||||
}),
|
}),
|
||||||
() => ({
|
() => ({
|
||||||
enabled: isLoggedIn.value
|
enabled: isLoggedIn.value && !trackInternallyOnly
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -378,53 +404,61 @@ export function useViewerUserActivityTracking(params: {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
useViewerAnchoredPoints<UserActivityModel, Partial<{ smoothTranslation: boolean }>>({
|
if (parentEl) {
|
||||||
parentEl,
|
useViewerAnchoredPoints<UserActivityModel, Partial<{ smoothTranslation: boolean }>>(
|
||||||
points: computed(() => Object.values(users.value)),
|
{
|
||||||
pointLocationGetter: (user) => {
|
parentEl,
|
||||||
const selection = user.state.ui.selection
|
points: computed(() => Object.values(users.value)),
|
||||||
const selectionVector = selection
|
pointLocationGetter: (user) => {
|
||||||
? new Vector3(selection[0], selection[1], selection[2])
|
const selection = user.state.ui.selection
|
||||||
: null
|
const selectionVector = selection
|
||||||
|
? new Vector3(selection[0], selection[1], selection[2])
|
||||||
|
: null
|
||||||
|
|
||||||
function getPointInBetweenByPerc(
|
function getPointInBetweenByPerc(
|
||||||
pointA: Vector3,
|
pointA: Vector3,
|
||||||
pointB: Vector3,
|
pointB: Vector3,
|
||||||
percentage: number
|
percentage: number
|
||||||
) {
|
) {
|
||||||
let dir = pointB.clone().sub(pointA)
|
let dir = pointB.clone().sub(pointA)
|
||||||
const len = dir.length()
|
const len = dir.length()
|
||||||
dir = dir.normalize().multiplyScalar(len * percentage)
|
dir = dir.normalize().multiplyScalar(len * percentage)
|
||||||
return pointA.clone().add(dir)
|
return pointA.clone().add(dir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is no selection location, return to a blended location based on the camera's target and location.
|
// If there is no selection location, return to a blended location based on the camera's target and location.
|
||||||
// This ensures that rotation and zoom will have an effect on the users' cursors and create a lively environment.
|
// This ensures that rotation and zoom will have an effect on the users' cursors and create a lively environment.
|
||||||
if (!selectionVector) {
|
if (!selectionVector) {
|
||||||
const camPos = user.state.ui.camera.position
|
const camPos = user.state.ui.camera.position
|
||||||
const camPosVector = new Vector3(camPos[0], camPos[1], camPos[2])
|
const camPosVector = new Vector3(camPos[0], camPos[1], camPos[2])
|
||||||
|
|
||||||
const camTarget = user.state.ui.camera.target
|
const camTarget = user.state.ui.camera.target
|
||||||
const camTargetVector = new Vector3(camTarget[0], camTarget[1], camTarget[2])
|
const camTargetVector = new Vector3(
|
||||||
|
camTarget[0],
|
||||||
|
camTarget[1],
|
||||||
|
camTarget[2]
|
||||||
|
)
|
||||||
|
|
||||||
return getPointInBetweenByPerc(camTargetVector, camPosVector, 0.2)
|
return getPointInBetweenByPerc(camTargetVector, camPosVector, 0.2)
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectionVector.clone()
|
return selectionVector.clone()
|
||||||
},
|
},
|
||||||
updatePositionCallback: (user, result, options) => {
|
updatePositionCallback: (user, result, options) => {
|
||||||
user.isOccluded = result.isOccluded
|
user.isOccluded = result.isOccluded
|
||||||
user.style = {
|
user.style = {
|
||||||
...user.style,
|
...user.style,
|
||||||
target: {
|
target: {
|
||||||
...user.style.target,
|
...user.style.target,
|
||||||
...result.style,
|
...result.style,
|
||||||
transition: options?.smoothTranslation === false ? '' : 'all 0.1s ease'
|
transition: options?.smoothTranslation === false ? '' : 'all 0.1s ease'
|
||||||
// opacity: user.isOccluded ? '0.5' : user.isStale ? '0.2' : '1.0' // note: handled in component via css
|
// opacity: user.isOccluded ? '0.5' : user.isStale ? '0.2' : '1.0' // note: handled in component via css
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
})
|
}
|
||||||
|
|
||||||
const hideStaleUsers = () => {
|
const hideStaleUsers = () => {
|
||||||
if (!Object.values(users.value).length) return
|
if (!Object.values(users.value).length) return
|
||||||
@@ -451,7 +485,7 @@ export function useViewerUserActivityTracking(params: {
|
|||||||
// Debounced disconnect function - 30 second delay
|
// Debounced disconnect function - 30 second delay
|
||||||
const debouncedDisconnect = debounce(
|
const debouncedDisconnect = debounce(
|
||||||
async () => {
|
async () => {
|
||||||
await sendUpdate.emitDisconnected()
|
await processViewerDisconnected()
|
||||||
},
|
},
|
||||||
30 * 1000 // 30 seconds
|
30 * 1000 // 30 seconds
|
||||||
)
|
)
|
||||||
@@ -463,13 +497,13 @@ export function useViewerUserActivityTracking(params: {
|
|||||||
} else {
|
} else {
|
||||||
// Window regained focus - cancel any pending disconnect and emit viewing
|
// Window regained focus - cancel any pending disconnect and emit viewing
|
||||||
debouncedDisconnect.cancel()
|
debouncedDisconnect.cancel()
|
||||||
await sendUpdate.emitViewing()
|
await processViewerViewing()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const sendUpdateAndHideStaleUsers = () => {
|
const sendUpdateAndHideStaleUsers = () => {
|
||||||
if (!focused.value) return
|
if (!focused.value) return
|
||||||
hideStaleUsers()
|
hideStaleUsers()
|
||||||
sendUpdate.emitViewing()
|
processViewerViewing()
|
||||||
}
|
}
|
||||||
|
|
||||||
useIntervalFn(sendUpdateAndHideStaleUsers, OWN_ACTIVITY_UPDATE_INTERVAL)
|
useIntervalFn(sendUpdateAndHideStaleUsers, OWN_ACTIVITY_UPDATE_INTERVAL)
|
||||||
@@ -484,22 +518,22 @@ export function useViewerUserActivityTracking(params: {
|
|||||||
doubleClickCallback: selectionCallback
|
doubleClickCallback: selectionCallback
|
||||||
})
|
})
|
||||||
|
|
||||||
useViewerCameraControlEndTracker(() => sendUpdate.emitViewing())
|
useViewerCameraControlEndTracker(() => processViewerViewing())
|
||||||
|
|
||||||
useOnBeforeWindowUnload(async () => {
|
useOnBeforeWindowUnload(async () => {
|
||||||
// Cancel any pending debounced disconnect since we're actually leaving
|
// Cancel any pending debounced disconnect since we're actually leaving
|
||||||
debouncedDisconnect.cancel()
|
debouncedDisconnect.cancel()
|
||||||
await sendUpdate.emitDisconnected()
|
await processViewerDisconnected()
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
sendUpdate.emitViewing()
|
processViewerViewing()
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
// Cancel any pending debounced disconnect
|
// Cancel any pending debounced disconnect
|
||||||
debouncedDisconnect.cancel()
|
debouncedDisconnect.cancel()
|
||||||
sendUpdate.emitDisconnected()
|
void processViewerDisconnected()
|
||||||
})
|
})
|
||||||
|
|
||||||
const state = useInjectedViewerState()
|
const state = useInjectedViewerState()
|
||||||
@@ -519,7 +553,7 @@ export function useViewerUserActivityTracking(params: {
|
|||||||
|
|
||||||
watch(resourceIdString, (newVal, oldVal) => {
|
watch(resourceIdString, (newVal, oldVal) => {
|
||||||
if (newVal !== oldVal) {
|
if (newVal !== oldVal) {
|
||||||
sendUpdate.emitViewing()
|
void processViewerViewing()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -24,16 +24,15 @@ export const useViewerSavedViewIntegration = () => {
|
|||||||
},
|
},
|
||||||
response: { savedView }
|
response: { savedView }
|
||||||
},
|
},
|
||||||
urlHashState: { savedView: urlHashStateSavedViewSettings }
|
urlHashState: { savedView: urlHashStateSavedViewSettings },
|
||||||
|
ui: {
|
||||||
|
savedViews: { savedViewStateId }
|
||||||
|
}
|
||||||
} = useInjectedViewerState()
|
} = useInjectedViewerState()
|
||||||
const applyState = useApplySerializedState()
|
const applyState = useApplySerializedState()
|
||||||
const { serializedStateId } = useViewerRealtimeActivityTracker()
|
const { serializedStateId } = useViewerRealtimeActivityTracker()
|
||||||
const { on, emit } = useEventBus()
|
const { on, emit } = useEventBus()
|
||||||
|
|
||||||
// Saved View ID will be unset, once the user does anything to the viewer that
|
|
||||||
// changes it from the saved view
|
|
||||||
const savedViewStateId = ref<string>()
|
|
||||||
|
|
||||||
const validState = (state: unknown) => (isSerializedViewerState(state) ? state : null)
|
const validState = (state: unknown) => (isSerializedViewerState(state) ? state : null)
|
||||||
|
|
||||||
const apply = async () => {
|
const apply = async () => {
|
||||||
@@ -131,6 +130,7 @@ export type SavedViewsUIState = ReturnType<typeof useBuildSavedViewsUIState>
|
|||||||
|
|
||||||
export const useBuildSavedViewsUIState = () => {
|
export const useBuildSavedViewsUIState = () => {
|
||||||
const openedGroupState = ref<Map<string, true>>(new Map())
|
const openedGroupState = ref<Map<string, true>>(new Map())
|
||||||
|
const savedViewStateId = ref<string>()
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
openedGroupState.value = new Map()
|
openedGroupState.value = new Map()
|
||||||
@@ -140,7 +140,12 @@ export const useBuildSavedViewsUIState = () => {
|
|||||||
/**
|
/**
|
||||||
* Groups that should currently be expanded/open
|
* Groups that should currently be expanded/open
|
||||||
*/
|
*/
|
||||||
openedGroupState
|
openedGroupState,
|
||||||
|
/**
|
||||||
|
* A kind of a "viewer snapshot" ID associated w/ the saved view being loaded. Helps track
|
||||||
|
* if user has changed the view since loading the saved view
|
||||||
|
*/
|
||||||
|
savedViewStateId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -342,7 +342,6 @@ export default defineNuxtPlugin(async (nuxtApp) => {
|
|||||||
{
|
{
|
||||||
err: error,
|
err: error,
|
||||||
info,
|
info,
|
||||||
isAppError: true,
|
|
||||||
vm: _vm?.$options.name,
|
vm: _vm?.$options.name,
|
||||||
errString: errorToString(error),
|
errString: errorToString(error),
|
||||||
vueErrorHandler: true,
|
vueErrorHandler: true,
|
||||||
|
|||||||
@@ -698,8 +698,12 @@ enum WorkspaceFeatureFlagName {
|
|||||||
presentations
|
presentations
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Either the ID or slug must be set
|
||||||
|
"""
|
||||||
input AdminAccessToWorkspaceFeatureInput {
|
input AdminAccessToWorkspaceFeatureInput {
|
||||||
workspaceId: ID!
|
workspaceId: ID
|
||||||
|
workspaceSlug: String
|
||||||
featureFlagName: WorkspaceFeatureFlagName!
|
featureFlagName: WorkspaceFeatureFlagName!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,9 +157,11 @@ export type AddDomainToWorkspaceInput = {
|
|||||||
workspaceId: Scalars['ID']['input'];
|
workspaceId: Scalars['ID']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Either the ID or slug must be set */
|
||||||
export type AdminAccessToWorkspaceFeatureInput = {
|
export type AdminAccessToWorkspaceFeatureInput = {
|
||||||
featureFlagName: WorkspaceFeatureFlagName;
|
featureFlagName: WorkspaceFeatureFlagName;
|
||||||
workspaceId: Scalars['ID']['input'];
|
workspaceId?: InputMaybe<Scalars['ID']['input']>;
|
||||||
|
workspaceSlug?: InputMaybe<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminInviteList = {
|
export type AdminInviteList = {
|
||||||
|
|||||||
@@ -26,9 +26,13 @@ export type {
|
|||||||
GetWorkspaceRolesAndSeats
|
GetWorkspaceRolesAndSeats
|
||||||
} from '@/modules/workspacesCore/domain/operations'
|
} from '@/modules/workspacesCore/domain/operations'
|
||||||
|
|
||||||
export type GetWorkspacePlan = (args: {
|
export type GetWorkspacePlan = (
|
||||||
workspaceId: string
|
args:
|
||||||
}) => Promise<WorkspacePlan | null>
|
| {
|
||||||
|
workspaceId: string
|
||||||
|
}
|
||||||
|
| { workspaceSlug: string }
|
||||||
|
) => Promise<WorkspacePlan | null>
|
||||||
|
|
||||||
export type GetWorkspacePlansByWorkspaceId = (args: {
|
export type GetWorkspacePlansByWorkspaceId = (args: {
|
||||||
workspaceIds: string[]
|
workspaceIds: string[]
|
||||||
|
|||||||
@@ -83,13 +83,24 @@ export const getWorkspaceWithPlanFactory =
|
|||||||
|
|
||||||
export const getWorkspacePlanFactory =
|
export const getWorkspacePlanFactory =
|
||||||
({ db }: { db: Knex }): GetWorkspacePlan =>
|
({ db }: { db: Knex }): GetWorkspacePlan =>
|
||||||
async ({ workspaceId }) => {
|
async (params) => {
|
||||||
const workspacePlan = await tables
|
const q = tables
|
||||||
.workspacePlans(db)
|
.workspacePlans(db)
|
||||||
.select()
|
.select<WorkspacePlan[]>(WorkspacePlans.cols)
|
||||||
.where({ workspaceId })
|
|
||||||
.first()
|
.first()
|
||||||
return workspacePlan ?? null
|
|
||||||
|
if ('workspaceId' in params) {
|
||||||
|
q.where({ workspaceId: params.workspaceId })
|
||||||
|
} else {
|
||||||
|
q.innerJoin(
|
||||||
|
Workspaces.name,
|
||||||
|
Workspaces.col.id,
|
||||||
|
WorkspacePlans.col.workspaceId
|
||||||
|
).where({ [Workspaces.col.slug]: params.workspaceSlug })
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = await q
|
||||||
|
return ret ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getWorkspacePlansByWorkspaceIdFactory =
|
export const getWorkspacePlansByWorkspaceIdFactory =
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ import {
|
|||||||
} from '@/modules/core/services/projects'
|
} from '@/modules/core/services/projects'
|
||||||
import { WorkspacePlanNotFoundError } from '@/modules/gatekeeper/errors/billing'
|
import { WorkspacePlanNotFoundError } from '@/modules/gatekeeper/errors/billing'
|
||||||
import { deleteProjectCommitsFactory } from '@/modules/core/repositories/commits'
|
import { deleteProjectCommitsFactory } from '@/modules/core/repositories/commits'
|
||||||
|
import { UserInputError } from '@/modules/core/errors/userinput'
|
||||||
|
|
||||||
const getServerInfo = getServerInfoFactory({ db })
|
const getServerInfo = getServerInfoFactory({ db })
|
||||||
const getUser = getUserFactory({ db })
|
const getUser = getUserFactory({ db })
|
||||||
@@ -623,13 +624,20 @@ export default FF_WORKSPACES_MODULE_ENABLED
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
giveAccessToWorkspaceFeature: async (_parent, { input }, ctx) => {
|
giveAccessToWorkspaceFeature: async (_parent, { input }, ctx) => {
|
||||||
const { workspaceId, featureFlagName } = input
|
const { workspaceId, featureFlagName, workspaceSlug } = input
|
||||||
const userId = ctx.userId
|
const userId = ctx.userId
|
||||||
if (!userId) throw new UnauthorizedError()
|
if (!userId) throw new UnauthorizedError()
|
||||||
|
if (!workspaceId && !workspaceSlug) {
|
||||||
|
throw new UserInputError(
|
||||||
|
'Either workspaceId or workspaceSlug must be provided'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const featureFlag = WorkspaceFeatureFlags[featureFlagName]
|
const featureFlag = WorkspaceFeatureFlags[featureFlagName]
|
||||||
|
const getWorkspacePlan = getWorkspacePlanFactory({ db })
|
||||||
const workspacePlan = await getWorkspacePlanFactory({ db })({ workspaceId })
|
const workspacePlan = await (workspaceId
|
||||||
|
? getWorkspacePlan({ workspaceId })
|
||||||
|
: getWorkspacePlan({ workspaceSlug: workspaceSlug! }))
|
||||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||||
|
|
||||||
workspacePlan.featureFlags |= featureFlag
|
workspacePlan.featureFlags |= featureFlag
|
||||||
@@ -641,13 +649,21 @@ export default FF_WORKSPACES_MODULE_ENABLED
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
removeAccessToWorkspaceFeature: async (_parent, { input }, ctx) => {
|
removeAccessToWorkspaceFeature: async (_parent, { input }, ctx) => {
|
||||||
const { workspaceId, featureFlagName } = input
|
const { workspaceId, featureFlagName, workspaceSlug } = input
|
||||||
const userId = ctx.userId
|
const userId = ctx.userId
|
||||||
if (!userId) throw new UnauthorizedError()
|
if (!userId) throw new UnauthorizedError()
|
||||||
|
if (!workspaceId && !workspaceSlug) {
|
||||||
|
throw new UserInputError(
|
||||||
|
'Either workspaceId or workspaceSlug must be provided'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const featureFlag = WorkspaceFeatureFlags[featureFlagName]
|
const featureFlag = WorkspaceFeatureFlags[featureFlagName]
|
||||||
|
|
||||||
const workspacePlan = await getWorkspacePlanFactory({ db })({ workspaceId })
|
const getWorkspacePlan = getWorkspacePlanFactory({ db })
|
||||||
|
const workspacePlan = await (workspaceId
|
||||||
|
? getWorkspacePlan({ workspaceId })
|
||||||
|
: getWorkspacePlan({ workspaceSlug: workspaceSlug! }))
|
||||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||||
|
|
||||||
workspacePlan.featureFlags ^= featureFlag
|
workspacePlan.featureFlags ^= featureFlag
|
||||||
|
|||||||
@@ -139,7 +139,8 @@
|
|||||||
|
|
||||||
"vue.complete.casing.props": "kebab",
|
"vue.complete.casing.props": "kebab",
|
||||||
"vue.inlayHints.missingProps": true,
|
"vue.inlayHints.missingProps": true,
|
||||||
"circleci.persistedProjectSelection": ["gh/specklesystems/speckle-server"]
|
"circleci.persistedProjectSelection": ["gh/specklesystems/speckle-server"],
|
||||||
|
"vue.suggest.componentNameCasing": "alwaysPascalCase"
|
||||||
// "vitest.nodeEnv": {
|
// "vitest.nodeEnv": {
|
||||||
// "DISABLE_ALL_FFS": "1"
|
// "DISABLE_ALL_FFS": "1"
|
||||||
// }
|
// }
|
||||||
|
|||||||
Reference in New Issue
Block a user