Merge branch 'main' into andrew/improve-ux-federated-comments
This commit is contained in:
@@ -97,7 +97,11 @@
|
||||
:url="route.path"
|
||||
/>
|
||||
<Portal to="primary-actions">
|
||||
<HeaderNavShare v-if="project" :resource-id-string="modelId" :project="project" />
|
||||
<HeaderNavShare
|
||||
v-if="project"
|
||||
:resource-id-string="resourceIdString"
|
||||
:project="project"
|
||||
/>
|
||||
</Portal>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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
|
||||
|
||||
@@ -48,9 +48,13 @@ export const useBillingActions = () => {
|
||||
const { mutate: cancelCheckoutSessionMutation } = useMutation(
|
||||
settingsBillingCancelCheckoutSessionMutation
|
||||
)
|
||||
const logger = useLogger()
|
||||
|
||||
const billingPortalRedirect = async (workspaceId: MaybeNullOrUndefined<string>) => {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,13 +82,13 @@ export function useViewerUserActivityBroadcasting(
|
||||
const apollo = useApolloClient().client
|
||||
const { isEnabled: isEmbedEnabled } = useEmbed()
|
||||
|
||||
const isSameState = (
|
||||
a: Optional<ViewerUserActivityMessageInput>,
|
||||
b: Optional<ViewerUserActivityMessageInput>
|
||||
const isSameMessage = (
|
||||
previousSerializedMessage: Optional<string>,
|
||||
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<ViewerUserActivityMessageInput> = undefined
|
||||
let serializedPreviousMessage: Optional<string> = 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 })
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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<string>
|
||||
projectId: AsyncWritableComputedRef<string>
|
||||
/**
|
||||
* 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<string> }
|
||||
type UseSetupViewerParams = { projectId: AsyncWritableComputedRef<string> }
|
||||
|
||||
export function useSetupViewer(params: UseSetupViewerParams): InjectableViewerState {
|
||||
// Initialize full state object - each subsequent state initialization depends on
|
||||
|
||||
@@ -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 = <O>(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<typeof serialize>) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<typeof localAuthRestApi>
|
||||
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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user