Compare commits

...

6 Commits

Author SHA1 Message Date
Björn Steinhagen 8d4bbeea8b chore: comment 2026-04-15 08:30:05 +02:00
Björn Steinhagen 237501ad30 chore: community forum fallback 2026-04-15 08:26:21 +02:00
Björn Steinhagen 7160b64f55 fix: openFeedbackDialog to unwrap bool 2026-04-14 23:31:01 +02:00
Björn Steinhagen bdc904201a feat: safeguard through feedback dialog 2026-04-14 23:03:20 +02:00
Björn Steinhagen 6d6b5e1fd5 feat: use canAccesscanAccessHelpCenter to show or hide intercom 2026-04-14 22:58:17 +02:00
Björn Steinhagen b4ce93d26e feat: updates workspace fragment 2026-04-14 22:31:49 +02:00
6 changed files with 145 additions and 50 deletions
+4 -1
View File
@@ -132,8 +132,11 @@ const openFeedbackDialog = () => {
!hostAppStore.isDistributedBySpeckle
) {
showFeedbackDialog.value = true
} else {
} else if ($intercom.shouldEnableIntercom.value) {
$intercom.show()
} else {
// community forum fallback
app.$openUrl('https://speckle.community')
}
}
</script>
+9 -3
View File
@@ -22,7 +22,7 @@ type Documents = {
"\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectListProjectItem\n }\n }\n }\n": typeof types.CreateProjectDocument,
"\n mutation CreateProjectInWorkspace($input: WorkspaceProjectCreateInput!) {\n workspaceMutations {\n projects {\n create(input: $input) {\n ...ProjectListProjectItem\n }\n }\n }\n }\n": typeof types.CreateProjectInWorkspaceDocument,
"\n mutation StreamAccessRequestCreate($input: String!) {\n streamAccessRequestCreate(streamId: $input) {\n id\n }\n }\n": typeof types.StreamAccessRequestCreateDocument,
"\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n": typeof types.WorkspaceListWorkspaceItemFragmentDoc,
"\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n canAccessHelpCenter {\n authorized\n }\n }\n }\n": typeof types.WorkspaceListWorkspaceItemFragmentDoc,
"\n fragment AutomateFunctionItem on AutomateFunction {\n name\n isFeatured\n id\n creator {\n name\n }\n releases {\n items {\n inputSchema\n }\n }\n }\n": typeof types.AutomateFunctionItemFragmentDoc,
"\n mutation CreateAutomation($projectId: ID!, $input: ProjectAutomationCreateInput!) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n create(input: $input) {\n id\n name\n }\n }\n }\n }\n": typeof types.CreateAutomationDocument,
"\n fragment AutomateFunctionRunItem on AutomateFunctionRun {\n id\n status\n statusMessage\n results\n contextView\n function {\n id\n name\n logo\n }\n }\n": typeof types.AutomateFunctionRunItemFragmentDoc,
@@ -65,6 +65,7 @@ type Documents = {
"\n fragment IssuesItem on Issue {\n id\n status\n title\n priority\n viewerState\n identifier\n resourceIdString\n activities(input: { limit: 1, sortDirection: asc }) {\n totalCount\n items {\n actor {\n id\n user {\n name\n id\n avatar\n }\n }\n eventType\n createdAt\n }\n }\n replies {\n totalCount\n items {\n id\n author {\n id\n user {\n name\n id\n avatar\n }\n }\n createdAt\n description {\n doc\n }\n }\n }\n description {\n doc\n }\n labels {\n hexColor\n id\n name\n }\n author {\n id\n user {\n id\n name\n avatar\n }\n }\n dueDate\n assignee {\n id\n user {\n id\n avatar\n name\n }\n }\n }\n": typeof types.IssuesItemFragmentDoc,
"\n query IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n": typeof types.IssuesListDocument,
"\n query IssueResourceMetaSearch(\n $workspaceId: String!\n $resourceType: ResourceMetaType!\n $resourceId: String!\n $projectId: String\n $metaType: String\n ) {\n resourceMetaSearch(\n workspaceId: $workspaceId\n resourceType: $resourceType\n resourceId: $resourceId\n projectId: $projectId\n metaType: $metaType\n ) {\n data\n }\n }\n": typeof types.IssueResourceMetaSearchDocument,
"\n query WorkspaceIntercomPermission($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n permissions {\n canAccessHelpCenter {\n authorized\n }\n }\n }\n }\n": typeof types.WorkspaceIntercomPermissionDocument,
"\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n": typeof types.WorkspacePlanUsageUpdatedDocument,
};
const documents: Documents = {
@@ -76,7 +77,7 @@ const documents: Documents = {
"\n mutation CreateProject($input: ProjectCreateInput) {\n projectMutations {\n create(input: $input) {\n ...ProjectListProjectItem\n }\n }\n }\n": types.CreateProjectDocument,
"\n mutation CreateProjectInWorkspace($input: WorkspaceProjectCreateInput!) {\n workspaceMutations {\n projects {\n create(input: $input) {\n ...ProjectListProjectItem\n }\n }\n }\n }\n": types.CreateProjectInWorkspaceDocument,
"\n mutation StreamAccessRequestCreate($input: String!) {\n streamAccessRequestCreate(streamId: $input) {\n id\n }\n }\n": types.StreamAccessRequestCreateDocument,
"\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n": types.WorkspaceListWorkspaceItemFragmentDoc,
"\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n canAccessHelpCenter {\n authorized\n }\n }\n }\n": types.WorkspaceListWorkspaceItemFragmentDoc,
"\n fragment AutomateFunctionItem on AutomateFunction {\n name\n isFeatured\n id\n creator {\n name\n }\n releases {\n items {\n inputSchema\n }\n }\n }\n": types.AutomateFunctionItemFragmentDoc,
"\n mutation CreateAutomation($projectId: ID!, $input: ProjectAutomationCreateInput!) {\n projectMutations {\n automationMutations(projectId: $projectId) {\n create(input: $input) {\n id\n name\n }\n }\n }\n }\n": types.CreateAutomationDocument,
"\n fragment AutomateFunctionRunItem on AutomateFunctionRun {\n id\n status\n statusMessage\n results\n contextView\n function {\n id\n name\n logo\n }\n }\n": types.AutomateFunctionRunItemFragmentDoc,
@@ -119,6 +120,7 @@ const documents: Documents = {
"\n fragment IssuesItem on Issue {\n id\n status\n title\n priority\n viewerState\n identifier\n resourceIdString\n activities(input: { limit: 1, sortDirection: asc }) {\n totalCount\n items {\n actor {\n id\n user {\n name\n id\n avatar\n }\n }\n eventType\n createdAt\n }\n }\n replies {\n totalCount\n items {\n id\n author {\n id\n user {\n name\n id\n avatar\n }\n }\n createdAt\n description {\n doc\n }\n }\n }\n description {\n doc\n }\n labels {\n hexColor\n id\n name\n }\n author {\n id\n user {\n id\n name\n avatar\n }\n }\n dueDate\n assignee {\n id\n user {\n id\n avatar\n name\n }\n }\n }\n": types.IssuesItemFragmentDoc,
"\n query IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n": types.IssuesListDocument,
"\n query IssueResourceMetaSearch(\n $workspaceId: String!\n $resourceType: ResourceMetaType!\n $resourceId: String!\n $projectId: String\n $metaType: String\n ) {\n resourceMetaSearch(\n workspaceId: $workspaceId\n resourceType: $resourceType\n resourceId: $resourceId\n projectId: $projectId\n metaType: $metaType\n ) {\n data\n }\n }\n": types.IssueResourceMetaSearchDocument,
"\n query WorkspaceIntercomPermission($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n permissions {\n canAccessHelpCenter {\n authorized\n }\n }\n }\n }\n": types.WorkspaceIntercomPermissionDocument,
"\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n": types.WorkspacePlanUsageUpdatedDocument,
};
@@ -171,7 +173,7 @@ export function graphql(source: "\n mutation StreamAccessRequestCreate($input:
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n }\n }\n"];
export function graphql(source: "\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n canAccessHelpCenter {\n authorized\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceListWorkspaceItem on Workspace {\n id\n slug\n name\n description\n createdAt\n updatedAt\n logoUrl\n role\n readOnly\n permissions {\n canCreateProject {\n authorized\n code\n message\n }\n canAccessHelpCenter {\n authorized\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -340,6 +342,10 @@ export function graphql(source: "\n query IssuesList($projectId: String!) {\n
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query IssueResourceMetaSearch(\n $workspaceId: String!\n $resourceType: ResourceMetaType!\n $resourceId: String!\n $projectId: String\n $metaType: String\n ) {\n resourceMetaSearch(\n workspaceId: $workspaceId\n resourceType: $resourceType\n resourceId: $resourceId\n projectId: $projectId\n metaType: $metaType\n ) {\n data\n }\n }\n"): (typeof documents)["\n query IssueResourceMetaSearch(\n $workspaceId: String!\n $resourceType: ResourceMetaType!\n $resourceId: String!\n $projectId: String\n $metaType: String\n ) {\n resourceMetaSearch(\n workspaceId: $workspaceId\n resourceType: $resourceType\n resourceId: $resourceId\n projectId: $projectId\n metaType: $metaType\n ) {\n data\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query WorkspaceIntercomPermission($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n permissions {\n canAccessHelpCenter {\n authorized\n }\n }\n }\n }\n"): (typeof documents)["\n query WorkspaceIntercomPermission($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n permissions {\n canAccessHelpCenter {\n authorized\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
+3
View File
@@ -95,6 +95,9 @@ export const workspaceListFragment = graphql(`
code
message
}
canAccessHelpCenter {
authorized
}
}
}
`)
+14
View File
@@ -0,0 +1,14 @@
import { graphql } from '~/lib/common/generated/gql'
export const workspaceIntercomPermissionQuery = graphql(`
query WorkspaceIntercomPermission($workspaceId: String!) {
workspace(id: $workspaceId) {
id
permissions {
canAccessHelpCenter {
authorized
}
}
}
}
`)
+69 -36
View File
@@ -8,7 +8,9 @@ import Intercom, {
} from '@intercom/messenger-js-sdk'
import { useAccountStore } from '~/store/accounts'
import { useHostAppStore } from '~/store/hostApp'
import { useConfigStore } from '~/store/config'
import { storeToRefs } from 'pinia'
import { workspaceIntercomPermissionQuery } from '~/lib/workspaces/graphql/queries'
const disabledRoutes: string[] = []
@@ -17,16 +19,22 @@ export const useIntercom = () => {
const accountStore = useAccountStore()
const hostAppStore = useHostAppStore()
const configStore = useConfigStore()
const { activeAccount } = storeToRefs(accountStore)
const { isDistributedBySpeckle } = storeToRefs(hostAppStore)
const { userSelectedWorkspaceId } = storeToRefs(configStore)
const isInitialized = ref(false)
const hasIntercomAccess = ref(false)
const isRouteBlacklisted = computed(() => {
return disabledRoutes.some((disabledRoute) => route.path.includes(disabledRoute))
})
const shouldEnableIntercom = computed(() => !isRouteBlacklisted.value)
const shouldEnableIntercom = computed(() => {
return !isRouteBlacklisted.value && hasIntercomAccess.value
})
const bootIntercom = () => {
if (!shouldEnableIntercom.value || isInitialized.value || !activeAccount.value)
@@ -44,13 +52,10 @@ export const useIntercom = () => {
}
const showIntercom = () => {
if (!isInitialized.value) return
show()
if (isInitialized.value) show()
}
const hideIntercom = () => {
if (!isInitialized.value) return
hide()
if (isInitialized.value) hide()
}
const shutdownIntercom = () => {
@@ -60,8 +65,7 @@ export const useIntercom = () => {
}
const trackIntercom = (event: string, metadata?: Record<string, unknown>) => {
if (!isInitialized.value) return
trackEvent(event, metadata)
if (isInitialized.value) trackEvent(event, metadata)
}
const updateConnectorDetails = (
@@ -69,51 +73,80 @@ export const useIntercom = () => {
hostAppVersion: string,
connectorVersion: string
) => {
update({
page_title: `CNX: (hostApp: ${hostAppName}:v${hostAppVersion}),(version: ${connectorVersion})`
})
if (isInitialized.value) {
update({
page_title: `CNX: (hostApp: ${hostAppName}:v${hostAppVersion}),(version: ${connectorVersion})`
})
}
}
// On route change, check if we need to shutodwn or boot Intercom
watch(route, () => {
if (isRouteBlacklisted.value) {
shutdownIntercom()
} else {
bootIntercom()
const checkPermissions = async () => {
if (!activeAccount.value || !userSelectedWorkspaceId.value) {
// userSelectedWorkspaceId is only null before any publish/load action,
// at which point the NavBar (and feedback button) isn't visible anyway
hasIntercomAccess.value = false
return
}
try {
const { data } = await activeAccount.value.client.query({
query: workspaceIntercomPermissionQuery,
variables: { workspaceId: userSelectedWorkspaceId.value },
fetchPolicy: 'cache-first'
})
hasIntercomAccess.value =
data?.workspace?.permissions?.canAccessHelpCenter?.authorized === true
} catch (e) {
console.warn('Failed to fetch Intercom permissions for workspace', e)
hasIntercomAccess.value = false
}
}
watch(route, () => {
if (isRouteBlacklisted.value || !shouldEnableIntercom.value) shutdownIntercom()
else bootIntercom()
})
// we listen to changes in the host app distribution status that fetched on updateConnector composable after the intercom is initialized, we cant simply rely on activeAccount watcher
watch(isDistributedBySpeckle, (newValue) => {
if (!newValue) {
shutdownIntercom()
}
if (!newValue) shutdownIntercom()
})
watch(activeAccount, (newValue) => {
if (newValue) {
if (!isInitialized.value) {
bootIntercom() // if active account changed and itercom is not initialised, do it
return // we do not need to update, as that's done by default in the init
}
update({
user_id: activeAccount.value.accountInfo.userInfo.id || '',
name: activeAccount.value.accountInfo.userInfo.name,
email: activeAccount.value.accountInfo.userInfo.email
})
} else {
if (isInitialized.value) {
watch(
[activeAccount, userSelectedWorkspaceId],
async ([newAccount], [oldAccount]) => {
await checkPermissions()
if (newAccount) {
if (!shouldEnableIntercom.value) {
shutdownIntercom()
return
}
if (!isInitialized.value) {
bootIntercom()
} else if (newAccount.accountInfo.id !== oldAccount?.accountInfo?.id) {
update({
user_id: newAccount.accountInfo.userInfo.id || '',
name: newAccount.accountInfo.userInfo.name,
email: newAccount.accountInfo.userInfo.email
})
}
} else {
shutdownIntercom()
}
}
})
},
{ immediate: true }
)
return {
show: showIntercom,
hide: hideIntercom,
shutdown: shutdownIntercom,
track: trackIntercom,
updateConnectorDetails
updateConnectorDetails,
shouldEnableIntercom
}
}