diff --git a/packages/frontend-2/components/viewer/PreSetupWrapper.vue b/packages/frontend-2/components/viewer/PreSetupWrapper.vue
index eaf6df817..3909cc249 100644
--- a/packages/frontend-2/components/viewer/PreSetupWrapper.vue
+++ b/packages/frontend-2/components/viewer/PreSetupWrapper.vue
@@ -97,7 +97,11 @@
:url="route.path"
/>
-
+
@@ -113,17 +117,28 @@ import { useFilterUtilities } from '~/lib/viewer/composables/ui'
import { projectsRoute } from '~~/lib/common/helpers/route'
import { workspaceRoute } from '~/lib/common/helpers/route'
import { useMixpanel } from '~/lib/core/composables/mp'
+import { writableAsyncComputed } from '~/lib/common/composables/async'
const emit = defineEmits<{
setup: [InjectableViewerState]
}>()
+const router = useRouter()
const route = useRoute()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
-const modelId = computed(() => route.params.modelId as string)
-
-const projectId = computed(() => route.params.id as string)
+const resourceIdString = computed(() => route.params.modelId as string)
+const projectId = writableAsyncComputed({
+ get: () => route.params.id as string,
+ set: async (value: string) => {
+ // Just rewrite route id param
+ await router.push({
+ params: { id: value }
+ })
+ },
+ initialState: route.params.id as string,
+ asyncRead: false
+})
const state = useSetupViewer({
projectId
diff --git a/packages/frontend-2/lib/billing/composables/actions.ts b/packages/frontend-2/lib/billing/composables/actions.ts
index 1f63d02bf..82cea6fde 100644
--- a/packages/frontend-2/lib/billing/composables/actions.ts
+++ b/packages/frontend-2/lib/billing/composables/actions.ts
@@ -48,9 +48,13 @@ export const useBillingActions = () => {
const { mutate: cancelCheckoutSessionMutation } = useMutation(
settingsBillingCancelCheckoutSessionMutation
)
+ const logger = useLogger()
const billingPortalRedirect = async (workspaceId: MaybeNullOrUndefined) => {
- if (!workspaceId) return
+ if (!workspaceId) {
+ logger.error('[Billing Portal] No workspaceId provided, returning early')
+ return
+ }
mixpanel.track('Workspace Billing Portal Button Clicked', {
// eslint-disable-next-line camelcase
@@ -66,6 +70,11 @@ export const useBillingActions = () => {
if (result.data?.workspace.customerPortalUrl) {
window.open(result.data.workspace.customerPortalUrl, '_blank')
+ } else {
+ logger.warn(
+ '[Billing Portal] No portal URL returned, full response:',
+ result.data
+ )
}
}
diff --git a/packages/frontend-2/lib/viewer/composables/activity.ts b/packages/frontend-2/lib/viewer/composables/activity.ts
index 0f9ea71ff..2258f570f 100644
--- a/packages/frontend-2/lib/viewer/composables/activity.ts
+++ b/packages/frontend-2/lib/viewer/composables/activity.ts
@@ -82,13 +82,13 @@ export function useViewerUserActivityBroadcasting(
const apollo = useApolloClient().client
const { isEnabled: isEmbedEnabled } = useEmbed()
- const isSameState = (
- a: Optional,
- b: Optional
+ const isSameMessage = (
+ previousSerializedMessage: Optional,
+ newMessage: ViewerUserActivityMessageInput
) => {
- if (xor(a, b)) return false
- if (!a || !b) return false
- return JSON.stringify(a) === JSON.stringify(b)
+ if (xor(previousSerializedMessage, newMessage)) return false
+ if (!previousSerializedMessage && !newMessage) return false
+ return previousSerializedMessage === JSON.stringify(newMessage)
}
const invokeMutation = async (message: ViewerUserActivityMessageInput) => {
@@ -106,14 +106,14 @@ export function useViewerUserActivityBroadcasting(
return result.data?.broadcastViewerUserActivity || false
}
- let previousMessage: Optional = undefined
+ let serializedPreviousMessage: Optional = undefined
const invokeObservabilityEvent = async (message: ViewerUserActivityMessageInput) => {
const dd = window.DD_RUM
if (!dd || !('addAction' in dd)) return
- if (isSameState(previousMessage, message)) return
+ if (isSameMessage(serializedPreviousMessage, message)) return
- previousMessage = message
+ serializedPreviousMessage = JSON.stringify(message)
dd.addAction('Viewer User Activity', { message })
}
diff --git a/packages/frontend-2/lib/viewer/composables/serialization.ts b/packages/frontend-2/lib/viewer/composables/serialization.ts
index 713c5d32e..3b57b22f8 100644
--- a/packages/frontend-2/lib/viewer/composables/serialization.ts
+++ b/packages/frontend-2/lib/viewer/composables/serialization.ts
@@ -138,6 +138,7 @@ export enum StateApplyMode {
export function useApplySerializedState() {
const {
+ projectId,
ui: {
camera: { position, target, isOrthoProjection },
sectionBox,
@@ -173,6 +174,16 @@ export function useApplySerializedState() {
return
}
+ if (state.projectId && state.projectId !== projectId.value) {
+ await projectId.update(state.projectId)
+ }
+
+ if (
+ [StateApplyMode.Spotlight, StateApplyMode.ThreadFullContextOpen].includes(mode)
+ ) {
+ await resourceIdString.update(state.resources?.request?.resourceIdString || '')
+ }
+
position.value = new Vector3(
state.ui?.camera?.position?.[0],
state.ui?.camera?.position?.[1],
diff --git a/packages/frontend-2/lib/viewer/composables/setup.ts b/packages/frontend-2/lib/viewer/composables/setup.ts
index 1c1f06d51..aa7d0167f 100644
--- a/packages/frontend-2/lib/viewer/composables/setup.ts
+++ b/packages/frontend-2/lib/viewer/composables/setup.ts
@@ -18,7 +18,6 @@ import {
type VisualDiffMode,
ViewMode
} from '@speckle/viewer'
-import type { MaybeRef } from '@vueuse/shared'
import { inject, ref, provide } from 'vue'
import type { ComputedRef, WritableComputedRef, Raw, Ref, ShallowRef } from 'vue'
import { useScopedState } from '~~/lib/common/composables/scopedState'
@@ -82,7 +81,7 @@ export type InjectableViewerState = Readonly<{
/**
* The project which we're opening in the viewer (all loaded models should belong to it)
*/
- projectId: ComputedRef
+ projectId: AsyncWritableComputedRef
/**
* User viewer session ID. The same user will have different IDs in different tabs if multiple are open.
* This is used to ignore user activity messages from the same tab.
@@ -400,8 +399,6 @@ function setupInitialState(params: UseSetupViewerParams): InitialSetupState {
public: { viewerDebug }
} = useRuntimeConfig()
- const projectId = computed(() => unref(params.projectId))
-
const sessionId = computed(() => nanoid())
const isInitialized = ref(false)
const { instance, initPromise, container } = useScopedState(
@@ -412,7 +409,7 @@ function setupInitialState(params: UseSetupViewerParams): InitialSetupState {
const hasDoneInitialLoad = ref(false)
return {
- projectId,
+ projectId: params.projectId,
sessionId,
viewer: import.meta.server
? ({
@@ -1030,7 +1027,7 @@ function setupInterfaceState(
}
}
-type UseSetupViewerParams = { projectId: MaybeRef }
+type UseSetupViewerParams = { projectId: AsyncWritableComputedRef }
export function useSetupViewer(params: UseSetupViewerParams): InjectableViewerState {
// Initialize full state object - each subsequent state initialization depends on
diff --git a/packages/frontend-2/lib/viewer/composables/setup/dev.ts b/packages/frontend-2/lib/viewer/composables/setup/dev.ts
index 7d54c3292..5afd21437 100644
--- a/packages/frontend-2/lib/viewer/composables/setup/dev.ts
+++ b/packages/frontend-2/lib/viewer/composables/setup/dev.ts
@@ -1,12 +1,13 @@
import { ViewerEvent } from '@speckle/viewer'
import {
StateApplyMode,
- useApplySerializedState
+ useApplySerializedState,
+ useStateSerialization
} from '~/lib/viewer/composables/serialization'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import { useViewerEventListener } from '~~/lib/viewer/composables/viewer'
import type { SpeckleViewer } from '@speckle/shared'
-import { get } from 'lodash-es'
+import { get, isString } from 'lodash-es'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function useDebugViewerEvents() {
@@ -18,38 +19,62 @@ function useDebugViewerEvents() {
}
function useDebugViewer() {
- const state = useInjectedViewerState()
+ const fullViewerState = useInjectedViewerState()
const apply = useApplySerializedState()
+ const { serialize } = useStateSerialization()
const {
viewer: { instance }
- } = state
+ } = fullViewerState
+
+ const ensureObj = (obj: O | string): O => {
+ return isString(obj) ? JSON.parse(obj) : obj
+ }
+
+ const applyState = (
+ state: SpeckleViewer.ViewerState.SerializedViewerState | string
+ ) => {
+ return apply(ensureObj(state), StateApplyMode.TheadFullContextOpen)
+ }
// Get current viewer instance
window.VIEWER = instance
// Get current viewer state
- window.VIEWER_STATE = () => state
+ window.VIEWER_STATE = () => fullViewerState
+
+ // Get serialized version of current state
+ window.VIEWER_SERIALIZED_STATE = (...args: Parameters) => {
+ const serialized = serialize(...args)
+ return JSON.stringify(serialized)
+ }
// Apply viewer state
window.APPLY_VIEWER_STATE = (
state: SpeckleViewer.ViewerState.SerializedViewerState
- ) => apply(state, StateApplyMode.TheadFullContextOpen)
+ ) => applyState(state)
// Apply DD user activity event
- window.APPLY_VIEWER_DD_EVENT = (event: {
- content: {
- attributes: {
- context: { message: { state: SpeckleViewer.ViewerState.SerializedViewerState } }
- }
- }
- }) => {
+ window.APPLY_VIEWER_DD_EVENT = (
+ event:
+ | {
+ content: {
+ attributes: {
+ context: {
+ message: { state: SpeckleViewer.ViewerState.SerializedViewerState }
+ }
+ }
+ }
+ }
+ | string
+ ) => {
+ event = ensureObj(event)
const path = 'content.attributes.context.message.state'
const state = get(event, path)
if (!state) {
throw new Error('Cant find serialized state at path: ' + path)
}
- return apply(state, StateApplyMode.TheadFullContextOpen)
+ return applyState(state)
}
}
diff --git a/packages/frontend-2/type-augmentations/window.d.ts b/packages/frontend-2/type-augmentations/window.d.ts
index 7065b2647..ffd1a5933 100644
--- a/packages/frontend-2/type-augmentations/window.d.ts
+++ b/packages/frontend-2/type-augmentations/window.d.ts
@@ -11,6 +11,7 @@ declare global {
// Debug keys, don't need to type properly cause we only use them manually from dev tools
VIEWER?: any
VIEWER_STATE?: any
+ VIEWER_SERIALIZED_STATE?: any
APPLY_VIEWER_STATE?: any
APPLY_VIEWER_DD_EVENT?: any
}
diff --git a/packages/server/modules/auth/tests/helpers/registration.ts b/packages/server/modules/auth/tests/helpers/registration.ts
index 3e8048d25..ebd3628c2 100644
--- a/packages/server/modules/auth/tests/helpers/registration.ts
+++ b/packages/server/modules/auth/tests/helpers/registration.ts
@@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'
import { RelativeURL } from '@speckle/shared'
import { expect } from 'chai'
import type { Express } from 'express'
-import { has, isString, random } from 'lodash'
+import { has, isString } from 'lodash'
import request from 'supertest'
export const appId = 'spklwebapp' // same values as on FE
@@ -233,7 +233,7 @@ export type LocalAuthRestApiHelpers = ReturnType
export const generateRegistrationParams = (): RegisterParams => ({
challenge: faker.string.uuid(),
user: {
- email: `${random(0, 1000)}@example.org`.toLowerCase(),
+ email: faker.internet.email().toLowerCase(),
password: faker.internet.password(),
name: faker.person.fullName()
}