Files
speckle-connectors-dui/components/send/Wizard.vue
T
Oğuzhan Koral c7e0929eca feat: new business model changes (#85)
* feat: initial can create version implementation on model card

* feat: disable model card CTAs for send

* feat: initial model ingestion tests

* fix: apply ingestion send to all CTAs

* feat: sketchup bridge

* feat: centeralize the start ingestion logic in host app store

* fix: sketchup is handling via model ingestion

* chore: cosmetics

* feat(ingestion): add failWithError and failWithCancel GraphQL mutations

* feat(ingestion): add failIngestion and cancelIngestion methods to useModelIngestion composable

* feat(ingestion): handle ingestion failure and cancellation in hostAppStore

* fix: reviewers comments

* fix: don't know where the f that came from

* refactor(ingestion): remove unused statusData and fix lint errors

* feat(wizard): add canCreateVersion permission check to publish wizard

* TODOs

* feat(permissions): add 1s polling for canCreateVersion to reflect workspace limit changes

* fix(tooltip): undefined doesnt refresh v-tippy

* fix(wizard): too much ctrl z lol

* refactor(permissions): check canCreateVersion on action instead of polling

* feat(hostApp): adds fallback for model ingestion on older servers

* fix: ingestion available check and rock'n roll

* feat: workspace plan updated subscription boilerplate

* fix: bump the timeout to 2h

* feat: handle version limits in publish flows via subscription

* feat: align Archicad and Vectorworks with new ingestion flow

* chore: onMounted at end of file

* fix: logic and ui adjustments

* fix: refactoring and permissions

* refactor: ingestionStatus renamed to activeIngestions

* fix: error handling and notifications

* fix: global error handling

* chore: general alignment and clean up

* fix(vectorworks): now uses capital V

* chore: revert codegen

---------

Co-authored-by: Björn Steinhagen <88777268+bjoernsteinhagen@users.noreply.github.com>
Co-authored-by: Björn Steinhagen <steinhagen.bjoern@gmail.com>
2026-02-03 14:43:16 +03:00

247 lines
7.5 KiB
Vue

<template>
<CommonDialog
v-model:open="showSendDialog"
fullscreen="none"
:title="title"
:show-back-button="step !== 1"
@back="step--"
@fully-closed="
() => {
step = 1
settingsWereChanged = false
}
"
>
<div v-if="step === 1">
<WizardProjectSelector
is-sender
disable-no-write-access-projects
:url-parse-error="urlParseError"
@next="selectProject"
@search-text-update="updateSearchText"
/>
</div>
<div v-if="step === 2 && selectedProject && selectedAccountId">
<WizardModelSelector
:project="selectedProject"
:workspace-id="selectedProject.workspace?.id"
:workspace-slug="selectedProject.workspace?.slug"
:account-id="selectedAccountId"
is-sender
@next="selectModel"
/>
</div>
<div v-if="step === 3">
<SendFiltersAndSettings
v-model="filter"
@update:filter="(f) => (filter = f)"
@update:settings="
(s) => {
settings = s
settingsWereChanged = true
}
"
/>
<div v-tippy="!canPublish ? publishLimitMessage : ''" class="mt-2">
<FormButton full-width :disabled="!canPublish" @click="addModel">
Publish
</FormButton>
</div>
</div>
<div v-if="urlParseError" class="p-2 text-danger">
{{ urlParseError }}
</div>
</CommonDialog>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useSubscription } from '@vue/apollo-composable'
import type {
ModelListModelItemFragment,
ProjectListProjectItemFragment
} from '~/lib/common/generated/gql/graphql'
import type { ISendFilter } from '~/lib/models/card/send'
import { SenderModelCard } from '~/lib/models/card/send'
import { useHostAppStore } from '~/store/hostApp'
import { useAccountStore } from '~/store/accounts'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { useSettingsTracking } from '~/lib/core/composables/trackSettings'
import type { CardSetting } from '~/lib/models/card/setting'
import { useAddByUrl } from '~/lib/core/composables/addByUrl'
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
import { workspacePlanUsageUpdatedSubscription } from '~/lib/workspaces/graphql/subscriptions'
const { trackEvent } = useMixpanel()
const { trackSettingsChange } = useSettingsTracking()
const showSendDialog = defineModel<boolean>('open', { default: false })
const emit = defineEmits(['close'])
const step = ref(1)
const accountStore = useAccountStore()
const { activeAccount } = storeToRefs(accountStore)
const selectedAccountId = ref<string>(activeAccount.value?.accountInfo.id as string)
const selectedProject = ref<ProjectListProjectItemFragment>()
const selectedModel = ref<ModelListModelItemFragment>()
const filter = ref<ISendFilter | undefined>(undefined)
const settings = ref<CardSetting[] | undefined>(undefined)
const settingsWereChanged = ref(false)
const { tryParseUrl, urlParsedData, urlParseError } = useAddByUrl()
const { canCreateModelIngestion, canCreateVersion } = useCheckGraphql()
const canPublish = ref(true)
const publishLimitMessage = ref<string | undefined>(undefined)
const updateSearchText = (text: string | undefined) => {
urlParseError.value = undefined
if (!text) return
tryParseUrl(text, 'sender')
}
watch(urlParsedData, (newVal) => {
if (!newVal) return
selectProject(newVal.account?.accountInfo.id, newVal.project)
selectModel(newVal.model)
})
watch(showSendDialog, (newVal) => {
if (newVal) {
urlParseError.value = undefined
}
})
const checkPermissions = async () => {
if (!selectedProject.value || !selectedModel.value) return
const res = await canCreateModelIngestion(
selectedProject.value.id,
selectedModel.value.id,
selectedAccountId.value
)
if (res.queryAvailable) {
canPublish.value = res.authorized
publishLimitMessage.value = res.message || undefined
} else {
// check legacy canCreateVersion in else block
const legacyRes = await canCreateVersion(
selectedProject.value.id,
selectedModel.value.id,
selectedAccountId.value
)
canPublish.value = legacyRes.authorized
publishLimitMessage.value = legacyRes.message || undefined
}
}
watch(step, async (newVal, oldVal) => {
if (newVal > oldVal) {
if (newVal === 3) {
await checkPermissions()
}
return // exit fast on forward
}
if (newVal === 1) {
selectedProject.value = undefined
selectedModel.value = undefined
}
if (newVal === 2) selectedModel.value = undefined
})
const workspaceId = computed(() => selectedProject.value?.workspace?.id)
const { onResult: onUsageUpdate } = useSubscription(
workspacePlanUsageUpdatedSubscription,
() => ({
input: {
workspaceId: workspaceId.value || ''
}
}),
() => ({
enabled: !!workspaceId.value && step.value === 3,
clientId: selectedAccountId.value
})
)
onUsageUpdate(() => {
void checkPermissions()
})
const selectProject = (accountId: string, project: ProjectListProjectItemFragment) => {
step.value++
selectedAccountId.value = accountId
selectedProject.value = project
void trackEvent('DUI3 Action', { name: 'Publish Wizard', step: 'project selected' })
}
const title = computed(() => {
if (step.value === 1) return 'Select project'
if (step.value === 2) return 'Select model'
if (step.value === 3) return 'Select objects'
return ''
})
const selectModel = (model: ModelListModelItemFragment) => {
step.value++
selectedModel.value = model
void trackEvent('DUI3 Action', { name: 'Publish Wizard', step: 'model selected' })
}
const hostAppStore = useHostAppStore()
// accountId, serverUrl, projectId, modelId, sendFilter, settings
const addModel = async () => {
void trackEvent('DUI3 Action', {
name: 'Publish Wizard',
step: 'objects selected',
filter: filter.value?.typeDiscriminator
})
const existingModel = hostAppStore.models.find(
(m) =>
m.modelId === selectedModel.value?.id &&
m.typeDiscriminator.includes('SenderModelCard')
) as SenderModelCard
// track settings only if user changed them
// compare against existing model card settings
if (settingsWereChanged.value && settings.value) {
trackSettingsChange(
'Publish Settings Changed',
settings.value,
existingModel?.settings || hostAppStore.sendSettings || [],
selectedAccountId.value,
true
)
}
if (existingModel) {
emit('close')
// Patch the existing model card with new send filter and non-expired state!
await hostAppStore.patchModel(existingModel.modelCardId, {
sendFilter: filter.value as ISendFilter,
expired: false
})
void hostAppStore.sendModel(existingModel.modelCardId, 'Wizard')
return
}
const model = new SenderModelCard()
model.accountId = selectedAccountId.value
model.serverUrl = activeAccount.value?.accountInfo.serverInfo.url as string
model.projectId = selectedProject.value?.id as string
model.modelId = selectedModel.value?.id as string
model.workspaceId = selectedProject.value?.workspace?.id as string
model.workspaceSlug = selectedProject?.value?.workspace?.slug as string
model.sendFilter = filter.value as ISendFilter
model.sendFilter.idMap = {} // do not let it null from the beginning otherwise we will end up with null state on Revit...
model.settings = settings.value
model.expired = false
emit('close')
await hostAppStore.addModel(model)
void hostAppStore.sendModel(model.modelCardId, 'Wizard')
}
</script>