955 lines
32 KiB
TypeScript
955 lines
32 KiB
TypeScript
import type {
|
|
DocumentInfo,
|
|
DocumentModelStore
|
|
} from '~/lib/bindings/definitions/IBasicConnectorBinding'
|
|
import type { IModelCard, ModelCardProgress } from '~/lib/models/card'
|
|
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
|
import type { IReceiverModelCard } from '~/lib/models/card/receiver'
|
|
import type {
|
|
IDirectSelectionSendFilter,
|
|
ISendFilter,
|
|
ISendFilterSelectItem,
|
|
ISenderModelCard,
|
|
RevitViewsSendFilter,
|
|
SendFilterSelect
|
|
} from '~/lib/models/card/send'
|
|
import { useSelectionStore } from '~/store/selection'
|
|
import { validateFilter } from '~/lib/validation'
|
|
import type { ToastNotification } from '@speckle/ui-components'
|
|
import { ToastNotificationType } from '@speckle/ui-components'
|
|
import type { Nullable } from '@speckle/shared'
|
|
import type { HostAppError } from '~/lib/bridge/errorHandler'
|
|
import type { ConversionResult } from '~/lib/conversions/conversionResult'
|
|
import { defineStore } from 'pinia'
|
|
import type { CardSetting } from '~/lib/models/card/setting'
|
|
import type { DUIAccount } from '~/store/accounts'
|
|
import { useAccountStore } from '~/store/accounts'
|
|
import {
|
|
useUpdateConnector,
|
|
type Version
|
|
} from '~/lib/core/composables/updateConnector'
|
|
import { provideApolloClient, useMutation } from '@vue/apollo-composable'
|
|
import { createVersionMutation } from '~/lib/graphql/mutationsAndQueries'
|
|
import type { BaseBridge } from '~/lib/bridge/base'
|
|
import { useModelIngestion } from '~/lib/ingestion/composables/useModelIngestion'
|
|
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
|
|
|
|
export type ProjectModelGroup = {
|
|
projectId: string
|
|
accountId: string
|
|
serverUrl: string
|
|
senders: ISenderModelCard[]
|
|
receivers: IReceiverModelCard[]
|
|
}
|
|
|
|
export const useHostAppStore = defineStore('hostAppStore', () => {
|
|
const app = useNuxtApp()
|
|
const { trackEvent } = useMixpanel()
|
|
const { $openUrl } = useNuxtApp()
|
|
const accountsStore = useAccountStore()
|
|
const { checkUpdate } = useUpdateConnector()
|
|
const {
|
|
startIngestion,
|
|
updateIngestion,
|
|
failIngestion,
|
|
cancelIngestion,
|
|
completeIngestionWithVersion,
|
|
subscribeToIngestion,
|
|
unsubscribeFromIngestion
|
|
} = useModelIngestion()
|
|
const isDistributedBySpeckle = ref<boolean>(true)
|
|
const latestAvailableVersion = ref<Version | null>(null)
|
|
|
|
const currentNotification = ref<Nullable<ToastNotification>>(null)
|
|
const showErrorDialog = ref<boolean>(false)
|
|
const hostAppError = ref<Nullable<HostAppError>>(null)
|
|
|
|
const hostAppName = ref<string>()
|
|
const hostAppVersion = ref<string>()
|
|
const connectorVersion = ref<string>()
|
|
const documentInfo = ref<DocumentInfo>()
|
|
const documentModelStore = ref<DocumentModelStore>({ models: [] })
|
|
|
|
const availableViews = ref<string[]>() // TODO: later we can align views with -> const revitAvailableViews = ref<ISendFilterSelectItem[]>()
|
|
const navisworksAvailableSavedSets = ref<ISendFilterSelectItem[]>()
|
|
|
|
const isUpdateNotificationDisabled = ref(false)
|
|
|
|
// Different host apps can have different kind of ISendFilterSelect send filters, and we collect them here to generalize the component we use in `ListSelect`
|
|
const availableSelectSendFilters = ref<Record<string, SendFilterSelect>>({})
|
|
|
|
// kvp for modelCardId - ingestionId
|
|
const activeIngestions = ref<Record<string, string>>({})
|
|
|
|
const dismissNotification = () => {
|
|
currentNotification.value = null
|
|
}
|
|
|
|
const setNotification = (notification: Nullable<ToastNotification>) => {
|
|
currentNotification.value = notification
|
|
}
|
|
|
|
const setLatestAvailableVersion = (version: Nullable<Version>) => {
|
|
latestAvailableVersion.value = version
|
|
}
|
|
|
|
function downloadLatestVersion() {
|
|
$openUrl(latestAvailableVersion.value?.Url as string)
|
|
}
|
|
|
|
const isConnectorUpToDate = computed(() => {
|
|
if (!isDistributedBySpeckle.value) return true
|
|
if (!latestAvailableVersion.value?.Number || !connectorVersion.value) return true
|
|
return connectorVersion.value === latestAvailableVersion.value.Number
|
|
})
|
|
|
|
const setHostAppError = (error: Nullable<HostAppError>) => {
|
|
hostAppError.value = error
|
|
}
|
|
|
|
const setIsDistributedBySpeckle = (val: boolean) => {
|
|
isDistributedBySpeckle.value = val
|
|
}
|
|
|
|
const shouldHandleIngestion = computed(() => {
|
|
const hostAppsThatUsesDUIForGraphql = ['sketchup', 'archicad', 'Vectorworks']
|
|
return hostAppsThatUsesDUIForGraphql.includes(hostAppName.value as string)
|
|
})
|
|
|
|
/**
|
|
* Model Card Operations
|
|
*/
|
|
|
|
/**
|
|
* A list of all models currently in the file, grouped by the project they are part of.
|
|
*/
|
|
const projectModelGroups = computed(() => {
|
|
const projectModelGroups: ProjectModelGroup[] = []
|
|
for (const model of documentModelStore.value.models) {
|
|
let project = projectModelGroups.find((p) => p.projectId === model.projectId)
|
|
if (!project) {
|
|
project = {
|
|
projectId: model.projectId,
|
|
accountId: model.accountId,
|
|
serverUrl: model.serverUrl,
|
|
senders: [],
|
|
receivers: []
|
|
}
|
|
projectModelGroups.push(project)
|
|
}
|
|
if (model.typeDiscriminator.toLowerCase().includes('sender'))
|
|
project.senders.push(model as ISenderModelCard)
|
|
if (model.typeDiscriminator.toLowerCase().includes('receiver'))
|
|
project.receivers.push(model as IReceiverModelCard)
|
|
}
|
|
return projectModelGroups
|
|
})
|
|
|
|
const models = computed(() => {
|
|
return documentModelStore.value.models
|
|
})
|
|
|
|
/**
|
|
* Adds a new model and persists it to the host app file.
|
|
* @param model
|
|
*/
|
|
const addModel = async (model: IModelCard) => {
|
|
await app.$baseBinding.addModel(model)
|
|
documentModelStore.value.models.push(model)
|
|
}
|
|
|
|
/**
|
|
* Updates a model's provided properties and persists the changes in the host application.
|
|
* @param modelCardId The model card id (NOT the model id).
|
|
* @param properties Properties to update.
|
|
* @param updateInHostApp Whether to sync the state to the host app. Defaults to true.
|
|
*/
|
|
const patchModel = async (
|
|
modelCardId: string,
|
|
properties: Record<string, unknown>,
|
|
updateInHostApp: boolean = true
|
|
) => {
|
|
const modelIndex = documentModelStore.value.models.findIndex(
|
|
(m) => m.modelCardId === modelCardId
|
|
)
|
|
|
|
documentModelStore.value.models[modelIndex] = {
|
|
...documentModelStore.value.models[modelIndex],
|
|
...properties
|
|
}
|
|
|
|
if (!updateInHostApp) return
|
|
await app.$baseBinding.updateModel(documentModelStore.value.models[modelIndex])
|
|
}
|
|
|
|
/**
|
|
* Removes a model from the store and the host app file.
|
|
* @param model
|
|
*/
|
|
const removeModel = async (model: IModelCard) => {
|
|
await app.$baseBinding.removeModel(model)
|
|
documentModelStore.value.models = documentModelStore.value.models.filter(
|
|
(item) => item.modelCardId !== model.modelCardId
|
|
)
|
|
|
|
void trackEvent(
|
|
'DUI3 Action',
|
|
{ name: 'Remove Model Card', type: model.typeDiscriminator },
|
|
model.accountId
|
|
)
|
|
}
|
|
|
|
const removeAccountModels = async (accountId: string) => {
|
|
const modelsToRemove = documentModelStore.value.models.filter(
|
|
(item) => item.accountId === accountId
|
|
)
|
|
documentModelStore.value.models = documentModelStore.value.models.filter(
|
|
(item) => item.accountId !== accountId
|
|
)
|
|
|
|
if (modelsToRemove.length !== 0) {
|
|
await app.$baseBinding.removeModels(modelsToRemove)
|
|
}
|
|
}
|
|
|
|
const removeProjectModels = async (projectId: string) => {
|
|
const modelsToRemove = documentModelStore.value.models.filter(
|
|
(item) => item.projectId === projectId
|
|
)
|
|
if (modelsToRemove.length !== 0) {
|
|
await app.$baseBinding.removeModels(modelsToRemove)
|
|
}
|
|
}
|
|
|
|
const sendSettings = ref<CardSetting[]>()
|
|
const receiveSettings = ref<CardSetting[]>()
|
|
|
|
/**
|
|
* Send filters
|
|
*/
|
|
|
|
/**
|
|
* The host app's available send filters.
|
|
*/
|
|
const sendFilters = ref<ISendFilter[]>()
|
|
|
|
/**
|
|
* Selection filter shortcut - use it as a default if possible.
|
|
*/
|
|
const selectionFilter = computed(
|
|
() => sendFilters.value?.find((f) => f.name === 'Selection') as ISendFilter
|
|
)
|
|
|
|
// Used on Archicad so far since send operation blocks the UI thread.
|
|
app.$sendBinding?.on('triggerCancel', (modelCardId: string) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === modelCardId
|
|
) as ISenderModelCard
|
|
model.progress = undefined
|
|
model.error = undefined
|
|
void trackEvent('DUI3 Action', { name: 'Send Cancel' }, model.accountId)
|
|
model.latestCreatedVersionId = undefined
|
|
})
|
|
|
|
app.$sendBinding?.on(
|
|
'setIdMap',
|
|
async ({ modelCardId, idMap, newSelectedObjectIds }) => {
|
|
const modelCard = models.value.find(
|
|
(card) => card.modelCardId === modelCardId
|
|
) as ISenderModelCard
|
|
if (!modelCard) return
|
|
|
|
const newFilter = {
|
|
...modelCard.sendFilter,
|
|
selectedObjectIds: newSelectedObjectIds,
|
|
idMap
|
|
}
|
|
|
|
// https://linear.app/speckle/issue/CNX-1213/revit-when-moving-between-documents-cards-can-get-lost-between-them
|
|
await patchModel(modelCardId, { sendFilter: newFilter }) // we do not necessarily need to updateModel in revit bc we already do it on .NET - otherwise it is leading cleanup on document store bc of deferred action when we switched to the another doc
|
|
}
|
|
)
|
|
|
|
app.$selectionBinding?.on('setSelection', (selInfo) => {
|
|
const modelCards = models.value.filter(
|
|
(m) =>
|
|
m.typeDiscriminator.toLowerCase().includes('sender') &&
|
|
(m as ISenderModelCard).sendFilter?.name === 'Selection'
|
|
) as ISenderModelCard[]
|
|
|
|
for (const model of modelCards) {
|
|
const filter = model.sendFilter as IDirectSelectionSendFilter
|
|
if (selInfo.selectedObjectIds.length === 0) {
|
|
filter.expired = false
|
|
continue
|
|
}
|
|
const a1 = filter.selectedObjectIds.sort().join()
|
|
const a2 = selInfo.selectedObjectIds.sort().join()
|
|
|
|
filter.expired = a1 !== a2
|
|
// filter.expired =
|
|
// filter.selectedObjectIds.filter((id) => !selInfo.selectedObjectIds.includes(id))
|
|
// .length !== 0
|
|
}
|
|
})
|
|
|
|
/**
|
|
* It is needed for connectors that send data with REST API on host app side but do not have graphql logic to create version.
|
|
* Currently it is the case with Vectorworks only which asked by a extarnal collaborator.
|
|
*/
|
|
app.$sendBinding?.on('triggerCreateVersion', async (args) => {
|
|
const accountStore = useAccountStore()
|
|
const account = accountStore.accounts.find(
|
|
(acc) => acc.accountInfo.id === args.accountId
|
|
)
|
|
|
|
// Check if we have an ingestion ID for this model.
|
|
// If so, we are in the "New Business Model" flow and should use completeIngestionWithVersion.
|
|
const modelCard = documentModelStore.value.models.find(
|
|
(m) => m.modelId === args.modelId && m.projectId === args.projectId
|
|
) as ISenderModelCard
|
|
|
|
const { canCreateModelIngestion } = useCheckGraphql()
|
|
const canCreateIngestion = await canCreateModelIngestion(
|
|
args.projectId,
|
|
args.modelId,
|
|
args.accountId
|
|
)
|
|
|
|
if (canCreateIngestion.queryAvailable) {
|
|
const ingestionId = modelCard
|
|
? activeIngestions.value[modelCard.modelCardId]
|
|
: undefined
|
|
|
|
if (ingestionId && modelCard) {
|
|
try {
|
|
await completeIngestionWithVersion(
|
|
modelCard,
|
|
ingestionId,
|
|
args.referencedObjectId
|
|
)
|
|
} catch (err) {
|
|
console.error(`completeIngestionWithVersion failed: ${err}`)
|
|
}
|
|
} else {
|
|
setNotification({
|
|
type: ToastNotificationType.Danger,
|
|
title: 'Publish Error',
|
|
description: 'Could not complete publish: Ingestion ID missing.'
|
|
})
|
|
}
|
|
} else {
|
|
// Fallback to legacy flow (Old Server)
|
|
try {
|
|
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
|
|
useMutation(createVersionMutation)
|
|
)
|
|
await createVersion.mutate({
|
|
input: {
|
|
modelId: args.modelId,
|
|
objectId: args.referencedObjectId,
|
|
sourceApplication: args.sourceApplication,
|
|
projectId: args.projectId
|
|
}
|
|
})
|
|
} catch (err) {
|
|
console.error(`triggerCreateVersion is failed: ${err}`)
|
|
}
|
|
}
|
|
})
|
|
|
|
/**
|
|
* Everything filter shortcut - do not use it as a default.
|
|
*/
|
|
const everythingFilter = computed(
|
|
() => sendFilters.value?.find((f) => f.name === 'Everything') as ISendFilter
|
|
)
|
|
|
|
/**
|
|
* Subscribe to notifications about send filters.
|
|
*/
|
|
app.$sendBinding?.on('refreshSendFilters', () => void refreshSendFilters())
|
|
|
|
const validateSendFilter = (filter?: ISendFilter) => {
|
|
const selectionStore = useSelectionStore()
|
|
|
|
return validateFilter(filter, {
|
|
selectionCount: selectionStore.selectionInfo.selectedObjectIds?.length ?? 0
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Send functionality
|
|
*/
|
|
|
|
/**
|
|
* Tells the host app to start sending a specific model card. This will reach inside the host application.
|
|
* @param modelId
|
|
*/
|
|
const sendModel = async (modelCardId: string, actionSource: string) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === modelCardId
|
|
) as ISenderModelCard
|
|
const { canCreateModelIngestion, canCreateVersion } = useCheckGraphql()
|
|
const canCreateIngestion = await canCreateModelIngestion(
|
|
model.projectId,
|
|
model.modelId,
|
|
model.accountId
|
|
)
|
|
|
|
// for the connectors that don't have SDK to handle graqhql
|
|
if (shouldHandleIngestion.value && canCreateIngestion.queryAvailable) {
|
|
const sourceData = {
|
|
sourceApplicationSlug: hostAppName.value || 'unknown',
|
|
sourceApplicationVersion: hostAppVersion.value?.toString() || 'unknown'
|
|
}
|
|
if (canCreateIngestion.authorized) {
|
|
await startIngestion(model, 'Starting to publish', sourceData)
|
|
model.progress = { status: 'Converting the objects...' }
|
|
} else {
|
|
setNotification({
|
|
type: ToastNotificationType.Warning,
|
|
title: 'Cannot publish',
|
|
description: canCreateIngestion.message
|
|
})
|
|
return
|
|
}
|
|
} else {
|
|
// for the self hosters that does not have available graphql for ingestions
|
|
const canCreate = await canCreateVersion(
|
|
model.projectId,
|
|
model.modelId,
|
|
model.accountId
|
|
)
|
|
if (!canCreate.authorized) {
|
|
setNotification({
|
|
type: ToastNotificationType.Warning,
|
|
title: 'Cannot publish',
|
|
description: canCreate.message || 'Workspace limits have been reached'
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
model.latestCreatedVersionId = undefined
|
|
model.error = undefined
|
|
model.progress = { status: 'Starting to send...' }
|
|
model.expired = false
|
|
model.report = undefined
|
|
|
|
if (model.expired) {
|
|
// user sends via "Update" button
|
|
void trackEvent(
|
|
'Send',
|
|
{
|
|
expired: true,
|
|
actionSource: actionSource.toLowerCase(),
|
|
// eslint-disable-next-line camelcase
|
|
workspace_id: model.workspaceId
|
|
},
|
|
model.accountId
|
|
)
|
|
} else {
|
|
void trackEvent(
|
|
'Send',
|
|
{
|
|
expired: false,
|
|
actionSource: actionSource.toLowerCase(),
|
|
// eslint-disable-next-line camelcase
|
|
workspace_id: model.workspaceId
|
|
},
|
|
model.accountId
|
|
)
|
|
}
|
|
|
|
// You should stop asking why if you saw anything related autocad..
|
|
// It solves the press "escape" issue.
|
|
// Because probably we don't give enough time to acad complete it's previos task and it stucks.
|
|
const shittyHostApps = ['autocad']
|
|
if (shittyHostApps.includes(hostAppName.value as string)) {
|
|
setTimeout(() => {
|
|
void app.$sendBinding.send(modelCardId)
|
|
}, 500) // I prefer to sacrifice 500ms
|
|
} else {
|
|
void app.$sendBinding.send(modelCardId)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cancels a model card's ongoing send operation. This will reach inside the host application.
|
|
* @param modelId
|
|
*/
|
|
const sendModelCancel = async (modelCardId: string) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === modelCardId
|
|
) as ISenderModelCard
|
|
await app.$sendBinding.cancelSend(modelCardId)
|
|
model.progress = undefined
|
|
model.error = undefined
|
|
void trackEvent('DUI3 Action', { name: 'Send Cancel' }, model.accountId)
|
|
model.latestCreatedVersionId = undefined
|
|
|
|
// Clean up any active ingestion subscription from SDK-based connectors
|
|
unsubscribeFromIngestion(modelCardId)
|
|
|
|
// Cancel the ingestion if applicable
|
|
if (shouldHandleIngestion.value) {
|
|
const ingestionId = activeIngestions.value[modelCardId]
|
|
if (ingestionId) {
|
|
await cancelIngestion(model, ingestionId, 'Cancelled by user')
|
|
}
|
|
}
|
|
}
|
|
|
|
app.$sendBinding?.on('setModelsExpired', (modelCardIds) => {
|
|
documentModelStore.value.models
|
|
.filter((m) => modelCardIds.includes(m.modelCardId))
|
|
.forEach((model: ISenderModelCard) => {
|
|
model.latestCreatedVersionId = undefined
|
|
model.error = undefined
|
|
model.expired = true
|
|
})
|
|
})
|
|
|
|
const setModelSendResult = (args: {
|
|
modelCardId: string
|
|
versionId: string
|
|
sendConversionResults: ConversionResult[]
|
|
ingestionId?: string
|
|
}) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === args.modelCardId
|
|
) as ISenderModelCard
|
|
// Conversion results are always valid regardless of ingestion state
|
|
model.report = args.sendConversionResults
|
|
|
|
if (args.ingestionId) {
|
|
// Connector handled ingestion via SDK — composable subscribes and manages model card state to 'Version created' bla bla
|
|
subscribeToIngestion(model, args.ingestionId)
|
|
} else {
|
|
// Legacy path or no ingestion — behave as before
|
|
model.latestCreatedVersionId = args.versionId
|
|
model.progress = undefined
|
|
}
|
|
}
|
|
|
|
app.$sendBinding?.on('setModelSendResult', setModelSendResult)
|
|
|
|
/// RECEIVE STUFF
|
|
const receiveModel = async (modelCardId: string, actionSource: string) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === modelCardId
|
|
) as IReceiverModelCard
|
|
|
|
const account = accountsStore.accounts.find(
|
|
(a) => a.accountInfo.id === model.accountId
|
|
)
|
|
|
|
void trackEvent(
|
|
'Receive',
|
|
{
|
|
expired: model.expired,
|
|
sourceHostApp: model.selectedVersionSourceApp,
|
|
isMultiplayer: model.selectedVersionUserId !== account?.accountInfo.userInfo.id,
|
|
actionSource: actionSource.toLowerCase(),
|
|
// eslint-disable-next-line camelcase
|
|
workspace_id: model.workspaceId
|
|
},
|
|
model.accountId
|
|
)
|
|
|
|
model.report = undefined
|
|
model.error = undefined
|
|
model.displayReceiveComplete = false
|
|
model.hasDismissedUpdateWarning = true
|
|
model.progress = { status: 'Starting to receive...' }
|
|
|
|
// You should stop asking why if you saw anything related autocad..
|
|
// It solves the press "escape" issue.
|
|
// Because probably we don't give enough time to acad complete it's previos task and it stucks.
|
|
const shittyHostApps = ['autocad']
|
|
if (shittyHostApps.includes(hostAppName.value as string)) {
|
|
setTimeout(async () => {
|
|
await app.$receiveBinding.receive(modelCardId)
|
|
}, 500) // I prefer to sacrifice 500ms
|
|
} else {
|
|
await app.$receiveBinding.receive(modelCardId)
|
|
}
|
|
}
|
|
|
|
const receiveModelCancel = async (modelCardId: string) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === modelCardId
|
|
) as IReceiverModelCard
|
|
await app.$receiveBinding.cancelReceive(modelCardId)
|
|
void trackEvent('DUI3 Action', { name: 'Receive Cancel' }, model.accountId)
|
|
model.progress = undefined
|
|
}
|
|
|
|
const setModelReceiveResult = async (args: {
|
|
modelCardId: string
|
|
bakedObjectIds: string[]
|
|
conversionResults: ConversionResult[]
|
|
}) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === args.modelCardId
|
|
) as IReceiverModelCard
|
|
|
|
model.progress = undefined
|
|
model.displayReceiveComplete = true
|
|
model.bakedObjectIds = args.bakedObjectIds
|
|
model.report = args.conversionResults
|
|
|
|
// NOTE: going through this method to ensure state sync between FE and BE. It's because of a very weird rhino bug on first receives, ask dim and he will cry
|
|
// TODO: check if it's still needed - we can store the bakedobject ids straigth into the receive ops in .net. Is the above reproducible?
|
|
await patchModel(model.modelCardId, {
|
|
bakedObjectIds: args.bakedObjectIds
|
|
})
|
|
}
|
|
|
|
app.$receiveBinding?.on('setModelReceiveResult', setModelReceiveResult)
|
|
|
|
// GENERIC STUFF
|
|
const handleModelProgressEvents = async (args: {
|
|
modelCardId: string
|
|
progress?: ModelCardProgress
|
|
}) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === args.modelCardId
|
|
) as IModelCard
|
|
model.progress = args.progress
|
|
|
|
if (
|
|
model.typeDiscriminator.includes('SenderModelCard') &&
|
|
shouldHandleIngestion.value // for the connectors that don't have SDK to handle graqhql
|
|
) {
|
|
const ingestionId = activeIngestions.value[args.modelCardId]
|
|
if (ingestionId) {
|
|
await updateIngestion(
|
|
model,
|
|
ingestionId,
|
|
args.progress?.status || 'Progressing',
|
|
args.progress?.progress || 0
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
const setModelError = async (args: {
|
|
modelCardId: string
|
|
error: string | { errorMessage: string; dismissible?: boolean }
|
|
}) => {
|
|
const model = documentModelStore.value.models.find(
|
|
(m) => m.modelCardId === args.modelCardId
|
|
) as IModelCard
|
|
model.progress = undefined
|
|
if (typeof args.error === 'string') {
|
|
model.error = { errorMessage: args.error as string, dismissible: true }
|
|
} else {
|
|
model.error = args.error as {
|
|
errorMessage: string
|
|
dismissible: boolean
|
|
}
|
|
}
|
|
|
|
// Fail the ingestion if applicable
|
|
if (
|
|
model.typeDiscriminator.includes('SenderModelCard') &&
|
|
shouldHandleIngestion.value
|
|
) {
|
|
const ingestionId = activeIngestions.value[args.modelCardId]
|
|
if (ingestionId) {
|
|
const errorMessage =
|
|
typeof args.error === 'string' ? args.error : args.error.errorMessage
|
|
await failIngestion(model as ISenderModelCard, ingestionId, errorMessage)
|
|
}
|
|
}
|
|
}
|
|
|
|
// NOTE: all bindings that need to send these model events should register.
|
|
// EG, new binding "mapper binding" wants to send errors to the model card should
|
|
// be registed here. Why? Each binding gets its own "bridge" parent in .NET, which
|
|
// is hoisted as a separate global js object.
|
|
app.$sendBinding?.on('setModelProgress', handleModelProgressEvents)
|
|
app.$receiveBinding?.on('setModelProgress', handleModelProgressEvents)
|
|
|
|
app.$baseBinding?.on('setGlobalNotification', setNotification)
|
|
app.$sendBinding?.on('setGlobalNotification', setNotification)
|
|
app.$receiveBinding?.on('setGlobalNotification', setNotification)
|
|
app.$configBinding?.on('setGlobalNotification', setNotification)
|
|
app.$accountBinding?.on('setGlobalNotification', setNotification)
|
|
app.$selectionBinding?.on('setGlobalNotification', setNotification)
|
|
app.$testBindings?.on('setGlobalNotification', setNotification)
|
|
|
|
// Dummy binding to communicate with proper bridge for top level exceptions
|
|
app.$topLevelExceptionHandlerBinding?.on('setGlobalNotification', setNotification)
|
|
|
|
app.$sendBinding?.on('setModelError', setModelError)
|
|
app.$receiveBinding?.on('setModelError', setModelError)
|
|
app.$baseBinding?.on('setModelError', setModelError)
|
|
|
|
/**
|
|
* Used internally in this store store only for initialisation.
|
|
*/
|
|
const getHostAppName = async () =>
|
|
(hostAppName.value = await app.$baseBinding.getSourceApplicationName())
|
|
|
|
const getHostAppVersion = async () =>
|
|
(hostAppVersion.value = await app.$baseBinding.getSourceApplicationVersion())
|
|
|
|
const getConnectorVersion = async () => {
|
|
connectorVersion.value = await app.$baseBinding.getConnectorVersion()
|
|
|
|
const canGetGlobalConfig = ['getGlobalConfig', 'GetGlobalConfig'].some((name) =>
|
|
(app.$configBinding as unknown as BaseBridge).availableMethodNames.includes(name)
|
|
)
|
|
|
|
if (canGetGlobalConfig) {
|
|
const globalConfig = await app.$configBinding.getGlobalConfig()
|
|
if (globalConfig) {
|
|
isUpdateNotificationDisabled.value = globalConfig.isUpdateNotificationDisabled
|
|
}
|
|
}
|
|
|
|
// Checks whether new version available for the connector or not and throws a toast notification if any.
|
|
if (app.$isRunningOnConnector && !isUpdateNotificationDisabled.value) {
|
|
await checkUpdate()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used internally in this store store only for initialisation. Refreshed the document info from the host app. Should be called on document changed events.
|
|
*/
|
|
const refreshDocumentInfo = async () =>
|
|
(documentInfo.value = await app.$baseBinding.getDocumentInfo())
|
|
|
|
/**
|
|
* Used internally in this store store only for initialisation. Refreshes available model cards from the host app. Should be called on document changed events.
|
|
*/
|
|
const refreshDocumentModelStore = async () =>
|
|
(documentModelStore.value = await app.$baseBinding.getDocumentState())
|
|
|
|
/**
|
|
* Sources the available send filters from the app. This is useful in case of host app layer changes, etc.
|
|
*/
|
|
const refreshSendFilters = async () => {
|
|
sendFilters.value = await app.$sendBinding?.getSendFilters()
|
|
const revitViews = sendFilters.value.find(
|
|
(f) => f.id === 'revitViews'
|
|
) as RevitViewsSendFilter
|
|
if (revitViews) {
|
|
availableViews.value = revitViews.availableViews
|
|
}
|
|
|
|
const selectSendFilters = sendFilters.value.filter(
|
|
(f) => f.type === 'Select'
|
|
) as SendFilterSelect[]
|
|
if (selectSendFilters.length !== 0) {
|
|
selectSendFilters.forEach((selectSendFilter) => {
|
|
const id = selectSendFilter.id
|
|
availableSelectSendFilters.value[id] = selectSendFilter
|
|
})
|
|
}
|
|
|
|
const navisworksSavedSetsFromSendFilters = sendFilters.value.find(
|
|
(f) => f.id === 'navisworksSavedSets'
|
|
) as SendFilterSelect
|
|
if (navisworksSavedSetsFromSendFilters) {
|
|
navisworksAvailableSavedSets.value = navisworksSavedSetsFromSendFilters.items
|
|
}
|
|
|
|
tryToUpgradeSelectSendFilters() // in rhino we trigger refresh send filters whenever layer name has changed, this should be done for navis too!
|
|
}
|
|
|
|
const tryToUpgradeSelectSendFilters = async () => {
|
|
for (const model of documentModelStore.value.models) {
|
|
const isSender = model.typeDiscriminator.toLowerCase().includes('sender')
|
|
if (!isSender) continue // we do not care about receivers
|
|
|
|
if ((model as ISenderModelCard).sendFilter?.type !== 'Select') continue // we do not care about filters other than Select type
|
|
|
|
const existingSelectFilter = (model as ISenderModelCard)
|
|
.sendFilter as SendFilterSelect
|
|
const newSelectFilter = availableSelectSendFilters.value[existingSelectFilter.id]
|
|
if (!newSelectFilter) {
|
|
continue
|
|
}
|
|
// here we do the upgrade by checking available select filter items and existing select filter items against ids
|
|
const updatedSelectedItems = existingSelectFilter.selectedItems.map(
|
|
(selected) => {
|
|
const upgraded = newSelectFilter.items.find((item) => item.id === selected.id)
|
|
return upgraded ?? selected
|
|
}
|
|
)
|
|
|
|
existingSelectFilter.items = newSelectFilter.items
|
|
existingSelectFilter.selectedItems = updatedSelectedItems
|
|
existingSelectFilter.summary = existingSelectFilter.isMultiSelectable
|
|
? existingSelectFilter.selectedItems.map((v) => v.name).join(', ')
|
|
: existingSelectFilter.selectedItems[0].name
|
|
|
|
// update the state in host app
|
|
await patchModel(model.modelCardId, {
|
|
sendFilters: existingSelectFilter
|
|
})
|
|
}
|
|
}
|
|
|
|
const getSendSettings = async () => {
|
|
sendSettings.value = await app.$sendBinding.getSendSettings()
|
|
}
|
|
|
|
const getReceiveSettings = async () => {
|
|
receiveSettings.value = await app.$receiveBinding.getReceiveSettings()
|
|
}
|
|
|
|
const tryToUpgradeModelCardSettings = (
|
|
settings: CardSetting[],
|
|
typeDiscriminator: string
|
|
) => {
|
|
if (documentModelStore.value.models.length === 0) return
|
|
const modelCards = documentModelStore.value.models.filter((m) =>
|
|
m.typeDiscriminator.includes(typeDiscriminator)
|
|
)
|
|
if (modelCards.length === 0) return
|
|
|
|
const settingIds = settings?.map((s) => s.id) || []
|
|
modelCards.forEach(async (modelCard) => {
|
|
const idsToUpgrade = [] as string[]
|
|
const idsToDrop = [] as string[]
|
|
|
|
settingIds?.forEach((id) => {
|
|
const existingSetting = modelCard.settings?.find((s) => s.id === id)
|
|
|
|
if (!existingSetting) {
|
|
// If the setting does not exist, it's a new one to upgrade
|
|
idsToUpgrade.push(id)
|
|
} else if (existingSetting.type === 'string' && existingSetting.enum) {
|
|
// Check if existing setting's enum needs upgrading
|
|
const currentEnum = sendSettings.value?.find((s) => s.id === id)?.enum
|
|
if (currentEnum && existingSetting.enum.length !== currentEnum.length) {
|
|
idsToUpgrade.push(id)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Identify settings to drop (if they no longer exist in sendSettingIds)
|
|
modelCard.settings?.forEach((setting) => {
|
|
if (!settingIds.includes(setting.id)) {
|
|
idsToDrop.push(setting.id)
|
|
}
|
|
})
|
|
|
|
if (idsToUpgrade.length !== 0 || idsToDrop.length !== 0) {
|
|
// Prepare new settings by filtering the old ones and adding upgraded ones
|
|
const newSettings = modelCard.settings?.filter(
|
|
(setting) => !idsToDrop.includes(setting.id)
|
|
)
|
|
|
|
idsToUpgrade.forEach((id) => {
|
|
const upgradedSetting = sendSettings.value?.find((s) => s.id === id)
|
|
if (upgradedSetting) {
|
|
newSettings?.push(upgradedSetting)
|
|
}
|
|
})
|
|
|
|
// Patch the model with the new settings
|
|
await patchModel(modelCard.modelCardId, {
|
|
settings: newSettings
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
app.$baseBinding?.on(
|
|
'documentChanged',
|
|
() =>
|
|
setTimeout(async () => {
|
|
// void trackEvent('DUI3 Action', { name: 'Document changed' }) // noisy
|
|
void refreshDocumentInfo()
|
|
await refreshDocumentModelStore() // need to awaited since upgrading the card settings need documentModelStore in place
|
|
void refreshSendFilters()
|
|
void tryToUpgradeModelCardSettings(sendSettings.value || [], 'SenderModelCard')
|
|
}, 500) // timeout exists because of rhino
|
|
)
|
|
|
|
const initializeApp = async () => {
|
|
await getHostAppName()
|
|
await getHostAppVersion()
|
|
await getConnectorVersion()
|
|
await refreshDocumentInfo()
|
|
await refreshDocumentModelStore()
|
|
await refreshSendFilters()
|
|
await getSendSettings()
|
|
await getReceiveSettings()
|
|
tryToUpgradeModelCardSettings(sendSettings.value || [], 'SenderModelCard')
|
|
|
|
// Intercom shenanningans below
|
|
// Do not poke intercom in ancient revit version
|
|
if (
|
|
(hostAppName.value?.toLowerCase() === 'revit' &&
|
|
hostAppVersion.value?.includes('2022')) ||
|
|
!isDistributedBySpeckle.value
|
|
)
|
|
return
|
|
|
|
// guards against intercom being sometimes slower to init
|
|
setTimeout(() => {
|
|
app.$intercom.updateConnectorDetails(
|
|
hostAppName.value as string,
|
|
hostAppVersion.value as string,
|
|
connectorVersion.value as string
|
|
)
|
|
}, 1000)
|
|
}
|
|
|
|
initializeApp()
|
|
|
|
return {
|
|
hostAppName,
|
|
hostAppVersion,
|
|
connectorVersion,
|
|
activeIngestions,
|
|
isConnectorUpToDate,
|
|
latestAvailableVersion,
|
|
documentInfo,
|
|
projectModelGroups,
|
|
models,
|
|
sendFilters,
|
|
sendSettings,
|
|
receiveSettings,
|
|
selectionFilter,
|
|
everythingFilter,
|
|
currentNotification,
|
|
showErrorDialog,
|
|
hostAppError,
|
|
availableViews,
|
|
navisworksAvailableSavedSets,
|
|
availableSelectSendFilters,
|
|
isDistributedBySpeckle,
|
|
isUpdateNotificationDisabled,
|
|
setIsDistributedBySpeckle,
|
|
setNotification,
|
|
setModelError,
|
|
setLatestAvailableVersion,
|
|
downloadLatestVersion,
|
|
dismissNotification,
|
|
setHostAppError,
|
|
addModel,
|
|
patchModel,
|
|
removeModel,
|
|
removeAccountModels,
|
|
removeProjectModels,
|
|
sendModel,
|
|
receiveModel,
|
|
sendModelCancel,
|
|
receiveModelCancel,
|
|
refreshSendFilters,
|
|
getSendSettings,
|
|
setModelSendResult,
|
|
setModelReceiveResult,
|
|
handleModelProgressEvents,
|
|
validateSendFilter
|
|
}
|
|
})
|