Compare commits

...

27 Commits

Author SHA1 Message Date
oguzhankoral 50e58a4091 fix: upgrade cta 2026-03-06 11:13:05 +03:00
Oğuzhan Koral 05bfc40ad6 Merge branch 'main' into bjorn/cnx-2294-upsell-message-display-only-when-user-is-trying-to-create-a 2026-03-06 10:53:00 +03:00
Oğuzhan Koral 009cc77bab fix: correct url for create workspace action (#93) 2026-03-06 10:52:46 +03:00
Björn Steinhagen a8b802b7e3 fix(dui): prevents empty publish selection state
fix(dui): prevents empty publish selection state
2026-03-03 08:29:40 +02:00
Björn Steinhagen 6fc3df4a0d refactor: centralized filter validation and generalized 'empty selection' checks 2026-03-02 15:34:36 +02:00
Björn Steinhagen f47f19c02d Merge branch 'main' into bjorn/cnx-3125-prevent-publishing-without-a-valid-selection 2026-02-25 14:42:35 +02:00
Oğuzhan Koral 85f806368a feat: handle model card state according to given ingestion id (#89)
* feat: handle model card state according to given ingestion id

* chore: linting
2026-02-25 14:00:59 +03:00
Björn Steinhagen 35ddce1f90 fix(dui): prevents invalidate selection filter across not just selection 2026-02-25 11:01:30 +02:00
Björn Steinhagen a37b3389d6 fix(dui): prevents empty publish selection state 2026-02-25 10:23:19 +02:00
Björn Steinhagen ed4aa92ce1 fix: disable deletion of model card while ops are happening (#87)
* chore: battling git

* fix: logic to card base for sender and receiver fix
2026-02-16 11:50:28 +03:00
Björn Steinhagen 69c87e8ed3 chore: upsell message 2026-02-12 21:28:37 +02:00
Björn Steinhagen 60f3bed254 feat: loading state on publish wizard
feat: loading state on publish button
2026-02-03 14:16:20 +02:00
Björn 2f412df64a feat: loading state on publish button 2026-02-03 14:11:37 +02:00
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
Oğuzhan Koral eef0a59719 feat: disable intercom for non speckle distributions + partner badge (#84)
* feat: disable intercom for non speckle distributions + partner badge

* no logging
2026-01-16 18:00:49 +03:00
Dogukan Karatas 19f306756c fix: handle network connectivity in DUI (#80)
* error handler

* top-level handling

* internet check

* pass other network errors

---------

Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
2026-01-12 17:51:47 +03:00
Oğuzhan Koral 305b100d34 feat: remove personal projects (#82) 2026-01-08 12:56:31 +03:00
Oğuzhan Koral f2cc0d55e3 fix: workspace avatars (#81)
* fix: workspace avatars

* get rid of from old logo prop
2026-01-06 16:21:53 +03:00
Oğuzhan Koral fdfef1d496 feat: issues (#77)
* WIP

* feat: readonly issues in connectors

* fix created at on replies

* filter out by resourceStringId

* show label name if just one

* generate gql

* linting

* linting
2025-12-10 18:01:13 +03:00
Oğuzhan Koral 5174af78cc fix: remove completed state for workspaces (#78)
* fix: remove completed state for workspaces

* remove experimental create automation dialog
2025-12-03 18:45:04 +03:00
Oğuzhan Koral ede6e99440 feat: new auth is default, desktop service is legacy and fallback (#76)
* feat: new auth is default, desktop service is legacy and fallback

* cleanup

* css

* rename login to signin

* better buttons

* default value instead placeholder
2025-11-25 23:04:40 +03:00
Oğuzhan Koral 9c708c64a0 fix: account by url should default to active one first (#75) 2025-11-11 18:31:21 +03:00
Oğuzhan Koral 41e635c8ef store url in cache (#74) 2025-10-27 16:53:35 +03:00
Oğuzhan Koral 095ccf114d feat: auth in dui (#71)
* feat: auth in dui

* feat: enable auth with registered app

* feat: handle exceptions
2025-10-27 15:31:56 +03:00
Dogukan Karatas a95fd9bdfe adds the server_domain (#62) 2025-10-16 16:23:01 +03:00
Dogukan Karatas bc665a008c userId is added to properties (#61) 2025-10-16 16:12:03 +03:00
Oğuzhan Koral 00a6a66ee0 fix: confusion on CTA and dry messaging (#70)
* fix: confusion on CTA and dry messaging

* make bjorn happy again
2025-10-15 10:39:04 +03:00
54 changed files with 5435 additions and 1124 deletions
+160
View File
@@ -0,0 +1,160 @@
<template>
<div class="flex flex-col space-y-2">
<div v-if="isDesktopServiceAvailable">
<div v-show="!isAddingAccount" class="text-foreground-2 space-y-2">
<FormButton
text
size="sm"
full-width
@click="showCustomServerInput = !showCustomServerInput"
>
{{ showCustomServerInput ? 'Use default server' : 'Set custom server url' }}
</FormButton>
<div v-if="showCustomServerInput">
<FormTextInput
v-model="customServerUrl"
name="name"
:show-label="false"
color="foundation"
autocomplete="off"
show-clear
@clear="showCustomServerInput = false"
/>
</div>
<div class="flex space-x-2">
<FormButton
color="outline"
class="px-1"
:icon-left="ArrowLeftIcon"
hide-text
@click="emit('backToSignIn')"
/>
<FormButton full-width @click="startAccountAddFlow()">
Sign in (Legacy)
</FormButton>
</div>
</div>
<div v-show="isAddingAccount" class="text-foreground-2 mt-2 mb-4 space-y-2">
<div class="text-sm text-center">
Please check your browser: waiting for authorization to complete.
</div>
<div class="py-2"><CommonLoadingBar :loading="isAddingAccount" /></div>
<div v-if="showHelp" class="bg-blue-500/10 p-2 rounded-md space-y-2">
<div class="text-sm text-center">Having trouble?</div>
<FormButton size="sm" full-width @click="restartFlow()">Retry</FormButton>
<FormButton
text
size="sm"
full-width
@click="$openUrl('https://speckle.community')"
>
Get in touch with us
</FormButton>
</div>
</div>
</div>
<div v-else class="space-y-3">
<div class="text-foreground-2 text-sm">
The Speckle Desktop Service is required to add accounts as legacy way. This
background service handles authentication securely.
</div>
<div class="flex space-x-2">
<FormButton
color="outline"
class="px-1"
:icon-left="ArrowLeftIcon"
hide-text
@click="emit('backToSignIn')"
/>
<FormButton full-width @click="$openUrl('https://releases.speckle.systems')">
Download Desktop Service
</FormButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useIntervalFn } from '@vueuse/core'
import { useHostAppStore } from '~/store/hostApp'
import { ToastNotificationType } from '@speckle/ui-components'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { useAccountStore } from '~~/store/accounts'
import { useDesktopService } from '~/lib/core/composables/desktopService'
import { ArrowLeftIcon } from '@heroicons/vue/24/solid'
const accountStore = useAccountStore()
const { pingDesktopService } = useDesktopService()
const hostApp = useHostAppStore()
const app = useNuxtApp()
const { trackEvent } = useMixpanel()
const emit = defineEmits<{
(e: 'backToSignIn'): void
}>()
const showCustomServerInput = ref(false)
const isAddingAccount = ref(false)
const isDesktopServiceAvailable = ref(false) // this should be false default because there is a delay if /ping is not successful.
const customServerUrl = ref<string | undefined>('https://app.speckle.systems')
const showHelp = ref(false)
const accountCheckerIntervalFn = useIntervalFn(
async () => {
const previousAccountCount = accountStore.accounts.length
await accountStore.refreshAccounts()
const currentAccountCount = accountStore.accounts.length
if (previousAccountCount !== currentAccountCount) {
isAddingAccount.value = false
showCustomServerInput.value = false
accountCheckerIntervalFn.pause()
trackEvent('DUI Account Added')
}
},
1000,
{ immediate: false }
)
const startAccountAddFlow = () => {
isAddingAccount.value = true
accountCheckerIntervalFn.resume()
setTimeout(() => {
showHelp.value = true
}, 10_000)
const url = customServerUrl.value
? `http://localhost:29364/auth/add-account?serverUrl=${
new URL(customServerUrl.value).origin
}`
: `http://localhost:29364/auth/add-account`
app.$openUrl(url)
// this is a annoying timeout that we cannot detect if user added same account or not.
setTimeout(() => {
if (isAddingAccount.value) {
isAddingAccount.value = false
showCustomServerInput.value = false
accountCheckerIntervalFn.pause()
// Note to Dim: not sure about toast
hostApp.setNotification({
title: 'Sign In',
type: ToastNotificationType.Info,
description:
'Sign in timed out. This may have happened because you tried adding an existing account.'
})
// TODO: we could log it to sentry/seq later to see how likely it happens?
}
}, 30_000)
}
const restartFlow = () => {
isAddingAccount.value = false
showHelp.value = false
}
onMounted(async () => {
isDesktopServiceAvailable.value = await pingDesktopService()
})
</script>
+22 -29
View File
@@ -39,33 +39,20 @@
title="Add a new account"
fullscreen="none"
>
<div>
<div v-if="isDesktopServiceAvailable">
<AccountsSignInFlow />
</div>
<div v-else class="space-y-3">
<div class="text-foreground-2 text-sm">
The Speckle Desktop Service is required to add accounts. This
background service handles authentication securely.
</div>
<FormButton
full-width
@click="$openUrl('https://releases.speckle.systems')"
>
Download Desktop Service
</FormButton>
<div class="text-center">
<div class="text-foreground-2 text-xs mb-2">Already installed?</div>
<FormButton
text
size="sm"
full-width
@click="accountStore.refreshAccounts()"
>
Refresh accounts
</FormButton>
</div>
</div>
<div class="flex flex-col space-y-2">
<AccountsSignInFlow v-if="!showLegacy" />
<AccountsLegacySignInFlow v-else @back-to-sign-in="showLegacy = false" />
<FormButton
v-if="!showLegacy"
text
full-width
size="sm"
class="text-xs"
@click="showLegacy = true"
>
Legacy Sign in
</FormButton>
</div>
</CommonDialog>
</div>
@@ -84,7 +71,6 @@ import { useDesktopService } from '~/lib/core/composables/desktopService'
const { trackEvent } = useMixpanel()
const app = useNuxtApp()
const { $openUrl } = useNuxtApp()
const { pingDesktopService } = useDesktopService()
const props = withDefaults(
@@ -102,7 +88,7 @@ defineEmits<{
}>()
const showAddNewAccount = ref(false)
// const showAccountsDialog = ref(false)
const showLegacy = ref(false)
const showAccountsDialog = defineModel<boolean>('open', {
required: false,
@@ -122,6 +108,13 @@ watch(showAccountsDialog, (newVal) => {
}
})
watch(showAddNewAccount, (newVal) => {
if (newVal) {
// reset the current/legacy state on every add account sub-dialog
showLegacy.value = false
}
})
const accountStore = useAccountStore()
const { accounts, activeAccount, userSelectedAccount, isLoading } =
storeToRefs(accountStore)
+35 -102
View File
@@ -1,118 +1,51 @@
<template>
<div>
<div v-show="!isAddingAccount" class="text-foreground-2 my-2 space-y-2">
<div v-if="showCustomServerInput">
<FormTextInput
v-model="customServerUrl"
name="name"
:show-label="false"
placeholder="https://app.speckle.systems"
color="foundation"
autocomplete="off"
show-clear
@clear="showCustomServerInput = false"
/>
</div>
<FormButton full-width @click="startAccountAddFlow()">Sign In</FormButton>
<FormButton
text
size="sm"
full-width
@click="showCustomServerInput = !showCustomServerInput"
>
{{ showCustomServerInput ? 'Use default server' : 'Set custom server url' }}
</FormButton>
<div class="flex flex-col space-y-2">
<FormButton
text
size="sm"
full-width
@click="showCustomServerInput = !showCustomServerInput"
>
{{ showCustomServerInput ? 'Use default server' : 'Set custom server url' }}
</FormButton>
<div v-if="showCustomServerInput">
<FormTextInput
v-model="customServerUrl"
name="name"
:show-label="false"
placeholder="https://app.speckle.systems"
color="foundation"
autocomplete="off"
show-clear
@clear="showCustomServerInput = false"
/>
</div>
<div v-show="isAddingAccount" class="text-foreground-2 mt-2 mb-4 space-y-2">
<div class="text-sm text-center">
Please check your browser: waiting for authorization to complete.
</div>
<div class="py-2"><CommonLoadingBar :loading="isAddingAccount" /></div>
<div v-if="showHelp" class="bg-blue-500/10 p-2 rounded-md space-y-2">
<div class="text-sm text-center">Having trouble?</div>
<FormButton size="sm" full-width @click="restartFlow()">Retry</FormButton>
<FormButton
text
size="sm"
full-width
@click="$openUrl('https://speckle.community')"
>
Get in touch with us
</FormButton>
</div>
</div>
<FormButton v-if="canAddAccount" full-width @click="logIn()">Sign in</FormButton>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useIntervalFn } from '@vueuse/core'
import { useAccountStore } from '~~/store/accounts'
import { useHostAppStore } from '~/store/hostApp'
import { ToastNotificationType } from '@speckle/ui-components'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { useAuthManager } from '~/lib/authn/useAuthManager'
import type { BaseBridge } from '~/lib/bridge/base'
const accountStore = useAccountStore()
const hostApp = useHostAppStore()
const app = useNuxtApp()
const { trackEvent } = useMixpanel()
const customServerUrl = ref<string | undefined>(undefined)
const isAddingAccount = ref(false)
const showHelp = ref(false)
const customServerUrl = ref<string | undefined>('https://app.speckle.systems')
const showCustomServerInput = ref(false)
const accountCheckerIntervalFn = useIntervalFn(
async () => {
const previousAccountCount = accountStore.accounts.length
await accountStore.refreshAccounts()
const currentAccountCount = accountStore.accounts.length
if (previousAccountCount !== currentAccountCount) {
isAddingAccount.value = false
showCustomServerInput.value = false
accountCheckerIntervalFn.pause()
trackEvent('DUI Account Added')
}
},
1000,
{ immediate: false }
const { $accountBinding } = useNuxtApp()
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
)
const startAccountAddFlow = () => {
isAddingAccount.value = true
accountCheckerIntervalFn.resume()
setTimeout(() => {
showHelp.value = true
}, 10_000)
const url = customServerUrl.value
? `http://localhost:29364/auth/add-account?serverUrl=${
new URL(customServerUrl.value).origin
}`
: `http://localhost:29364/auth/add-account`
const { generateChallenge } = useAuthManager()
app.$openUrl(url)
// this is a annoying timeout that we cannot detect if user added same account or not.
setTimeout(() => {
if (isAddingAccount.value) {
isAddingAccount.value = false
showCustomServerInput.value = false
accountCheckerIntervalFn.pause()
// Note to Dim: not sure about toast
hostApp.setNotification({
title: 'Sign In',
type: ToastNotificationType.Info,
description:
'Sign in timed out. This may have happened because you tried adding an existing account.'
})
// TODO: we could log it to sentry/seq later to see how likely it happens?
}
}, 30_000)
}
const restartFlow = () => {
isAddingAccount.value = false
showHelp.value = false
const logIn = () => {
const serverUrl = customServerUrl.value
? new URL(customServerUrl.value).origin
: 'https://app.speckle.systems'
const challenge = generateChallenge(serverUrl)
const authUrl = `${serverUrl}/authn/verify/sdui/${challenge}`
window.location.href = authUrl
}
</script>
-159
View File
@@ -1,159 +0,0 @@
<!-- NOT WILL BE USED SINCE WE ENABLE AUTOMATION CREATION FROM DUI3 -->
<template>
<div class="p-0">
<slot name="activator" :toggle="toggleDialog"></slot>
<CommonDialog
v-model:open="showAutomateDialog"
:title="`Settings`"
fullscreen="none"
>
<div v-if="hasFunctions">
<FormSelectBase
key="name"
v-model="selectedFunction"
clearable
label="Automate functions"
placeholder="Nothing selected"
name="Functions"
show-label
:items="functions"
mount-menu-on-body
>
<template #something-selected="{ value }">
<span>{{ isArray(value) ? value[0].name : value.name }}</span>
</template>
<template #option="{ item }">
<div class="flex items-center">
<span class="truncate">{{ item.name }}</span>
</div>
</template>
</FormSelectBase>
</div>
<div v-if="selectedFunction && finalParams && step === 0">
<FormJsonForm
ref="jsonForm"
:data="data"
:schema="finalParams"
class="space-y-4"
:validate-on-mount="false"
@change="handler"
/>
</div>
<div v-if="step === 1">
<FormTextInput
v-model="automationName"
name="automationName"
label="Automation name"
color="foundation"
show-label
help="Give your automation a name"
placeholder="Name"
show-required
validate-on-value-update
/>
</div>
<FormButton
v-if="selectedFunction && step === 0"
size="sm"
class="mt-4"
@click="step++"
>
Next
</FormButton>
<FormButton
v-if="selectedFunction && step === 1"
size="sm"
class="mt-4"
@click="createAutomationHandler"
>
Create
</FormButton>
</CommonDialog>
</div>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import type { AutomateFunctionItemFragment } from '~/lib/common/generated/gql/graphql'
import {
automateFunctionsQuery,
createAutomationMutation
} from '~/lib/graphql/mutationsAndQueries'
import { provideApolloClient, useMutation, useQuery } from '@vue/apollo-composable'
import { useAccountStore, type DUIAccount } from '~/store/accounts'
import type { ApolloError } from '@apollo/client/errors'
import { formatVersionParams } from '~/lib/common/helpers/jsonSchema'
import { useJsonFormsChangeHandler } from '~/lib/core/composables/jsonSchema'
import { isArray } from 'lodash-es'
const props = defineProps<{
projectId: string
modelId: string
}>()
const step = ref<number>(0)
const automationName = ref<string>('')
const accountStore = useAccountStore()
const { activeAccount } = storeToRefs(accountStore)
const accountId = computed(() => activeAccount.value?.accountInfo.id) // NOTE: none of the tokens here has read, write access to automate, only frontend tokens have. Keep in mind after first pass!
const selectedFunction = ref<AutomateFunctionItemFragment>()
const showAutomateDialog = ref(false)
const toggleDialog = () => {
showAutomateDialog.value = !showAutomateDialog.value
}
const { mutate } = provideApolloClient((activeAccount.value as DUIAccount).client)(() =>
useMutation(createAutomationMutation)
)
const createAutomationHandler = async () => {
const _res = await mutate({
projectId: props.projectId,
input: { name: automationName.value, enabled: false }
})
showAutomateDialog.value = false
}
const { result: functionsResult, onError } = useQuery(
automateFunctionsQuery,
() => ({}),
() => ({ clientId: accountId.value, debounce: 500, fetchPolicy: 'network-only' })
)
onError((err: ApolloError) => {
console.warn(err.message)
})
const functions = computed(() => functionsResult.value?.automateFunctions.items)
const hasFunctions = computed(() => functions.value?.length !== 0)
const release = computed(() =>
selectedFunction.value?.releases.items.length
? selectedFunction.value?.releases.items[0]
: undefined
)
const finalParams = computed(() => formatVersionParams(release.value?.inputSchema))
const { handler } = useJsonFormsChangeHandler({
schema: finalParams
})
console.log(finalParams)
type DataType = Record<string, unknown>
const data = computed(() => {
const kvp = {} as DataType
if (finalParams.value) {
Object.entries(finalParams.value).forEach((k, _) => {
kvp[k as unknown as string] = undefined
})
}
return kvp
})
</script>
+11 -10
View File
@@ -27,16 +27,17 @@
>
{{ notification.secondaryCta.name }}
</FormButton>
<FormButton
v-if="notification.cta"
v-tippy="notification.cta.tooltipText"
size="sm"
color="primary"
full-width
@click.stop="notification.cta?.action"
>
{{ notification.cta.name }}
</FormButton>
<div v-if="notification.cta" v-tippy="notification.cta.tooltipText">
<FormButton
:disabled="notification.cta.disabled"
size="sm"
color="primary"
full-width
@click.stop="notification.cta?.action"
>
{{ notification.cta.name }}
</FormButton>
</div>
</div>
</div>
<div
+17
View File
@@ -0,0 +1,17 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6.73336 1.45469C7.57004 1.29277 8.43001 1.29277 9.26669 1.45469M9.26669 14.5454C8.43001 14.7073 7.57004 14.7073 6.73336 14.5454M11.7394 2.48069C12.447 2.96017 13.0558 3.57127 13.5327 4.28069M1.45469 9.26669C1.29277 8.43001 1.29277 7.57004 1.45469 6.73336M13.5194 11.7394C13.0399 12.447 12.4288 13.0558 11.7194 13.5327M14.5454 6.73336C14.7073 7.57004 14.7073 8.43001 14.5454 9.26669M2.48069 4.26069C2.96017 3.55304 3.57127 2.94421 4.28069 2.46736M4.26069 13.5194C3.55304 13.0399 2.94421 12.4288 2.46736 11.7194"
stroke="#707070"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
+18
View File
@@ -0,0 +1,18 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 1.33301C11.6819 1.33301 14.667 4.3181 14.667 8C14.6669 11.6819 11.6819 14.667 8 14.667C4.3182 14.6669 1.33305 11.6818 1.33301 8C1.33301 4.31816 4.31818 1.3331 8 1.33301ZM10.5303 6.13672C10.2374 5.84383 9.76262 5.84383 9.46973 6.13672L7.33301 8.27246L6.53027 7.46973C6.23742 7.17705 5.76258 7.17705 5.46973 7.46973C5.17713 7.76259 5.17708 8.23745 5.46973 8.53027L6.80273 9.86426C6.94329 10.0047 7.13433 10.0839 7.33301 10.084C7.53165 10.084 7.72268 10.0046 7.86328 9.86426L10.5303 7.19727C10.8231 6.90445 10.8229 6.42963 10.5303 6.13672Z"
fill="#15803D"
/>
<path
d="M8 1.33301C11.6819 1.33301 14.667 4.3181 14.667 8C14.6669 11.6819 11.6819 14.667 8 14.667C4.3182 14.6669 1.33305 11.6818 1.33301 8C1.33301 4.31816 4.31818 1.3331 8 1.33301ZM10.5303 6.13672C10.2374 5.84383 9.76262 5.84383 9.46973 6.13672L7.33301 8.27246L6.53027 7.46973C6.23742 7.17705 5.76258 7.17705 5.46973 7.46973C5.17713 7.76259 5.17708 8.23745 5.46973 8.53027L6.80273 9.86426C6.94329 10.0047 7.13433 10.0839 7.33301 10.084C7.53165 10.084 7.72268 10.0046 7.86328 9.86426L10.5303 7.19727C10.8231 6.90445 10.8229 6.42963 10.5303 6.13672Z"
fill="#16A34A"
/>
</svg>
</template>
+35
View File
@@ -0,0 +1,35 @@
<template>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.00024 2.08337C11.2678 2.08355 13.9163 4.73279 13.9163 8.00037C13.9161 11.2678 11.2677 13.9162 8.00024 13.9164C4.73267 13.9164 2.08343 11.2679 2.08325 8.00037C2.08325 4.73268 4.73256 2.08337 8.00024 2.08337Z"
stroke="#EAB308"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8.75 4.83789C10.1832 5.17655 11.25 6.46328 11.25 8C11.25 9.53664 10.1831 10.8224 8.75 11.1611V4.83789Z"
fill="#EAB308"
/>
<path
d="M8.75 4.83789C10.1832 5.17655 11.25 6.46328 11.25 8C11.25 9.53664 10.1831 10.8224 8.75 11.1611V4.83789Z"
stroke="#7C7C7D"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8.75 4.83789C10.1832 5.17655 11.25 6.46328 11.25 8C11.25 9.53664 10.1831 10.8224 8.75 11.1611V4.83789Z"
stroke="#EAB308"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</template>
+26 -4
View File
@@ -49,9 +49,26 @@
>
<span class="">Update</span>
</FormButton> -->
<div class="text-[8px] text-foreground-disabled max-[150px]:hidden">
<div
class="text-[8px] text-foreground-disabled max-[150px]:hidden"
:class="{ 'mr-2': !hostAppStore.isDistributedBySpeckle }"
>
{{ hostAppStore.connectorVersion }}
</div>
<div
v-if="!hostAppStore.isDistributedBySpeckle && hostAppStore.hostAppName"
v-tippy="
`${hostAppStore.hostAppName
.charAt(0)
.toUpperCase()}${hostAppStore.hostAppName.slice(
1
)} connector is not distributed by Speckle.`
"
class="text-xs text-foreground-disabled max-[150px]:hidden mr-1"
>
<CommonBadge color="secondary">Partner</CommonBadge>
</div>
<HeaderButton
v-if="hostAppStore.isDistributedBySpeckle"
v-tippy="'Documentation and help'"
@@ -65,7 +82,11 @@
class="w-4 text-foreground-disabled group-hover:text-foreground-2"
/>
</HeaderButton>
<HeaderButton v-tippy="'Send us feedback'" @click="openFeedbackDialog()">
<HeaderButton
v-if="hostAppStore.isDistributedBySpeckle"
v-tippy="'Send us feedback'"
@click="openFeedbackDialog()"
>
<ChatBubbleLeftIcon
class="w-4 text-foreground-disabled group-hover:text-foreground-2"
/>
@@ -106,8 +127,9 @@ const { $intercom } = useNuxtApp()
const openFeedbackDialog = () => {
if (
hostAppStore.hostAppName?.toLowerCase() === 'revit' &&
hostAppStore.hostAppVersion?.includes('2022')
(hostAppStore.hostAppName?.toLowerCase() === 'revit' &&
hostAppStore.hostAppVersion?.includes('2022')) ||
!hostAppStore.isDistributedBySpeckle
) {
showFeedbackDialog.value = true
} else {
+21 -2
View File
@@ -5,8 +5,19 @@
>
Welcome to Speckle
</h1>
<div v-if="isDesktopServiceAvailable">
<AccountsSignInFlow />
<div v-if="isDesktopServiceAvailable || canAddAccount">
<AccountsSignInFlow v-if="!showLegacy" />
<AccountsLegacySignInFlow v-else @back-to-sign-in="showLegacy = false" />
<FormButton
v-if="!showLegacy"
text
full-width
size="sm"
class="text-xs"
@click="showLegacy = true"
>
Legacy Sign in
</FormButton>
</div>
<div v-else>
<div class="text-foreground-2 mt-2 mb-4">
@@ -37,10 +48,18 @@
<script setup lang="ts">
import { useAccountStore } from '~~/store/accounts'
import { useDesktopService } from '~/lib/core/composables/desktopService'
import type { BaseBridge } from '~/lib/bridge/base'
const accountStore = useAccountStore()
const { pingDesktopService } = useDesktopService()
const { $accountBinding } = useNuxtApp()
const canAddAccount = ['AddAccount', 'addAccount'].some((name) =>
($accountBinding as unknown as BaseBridge).availableMethodNames.includes(name)
)
const showLegacy = ref(false)
const isDesktopServiceAvailable = ref(false) // this should be false default because there is a delay if /ping is not successful.
onMounted(async () => {
+51
View File
@@ -0,0 +1,51 @@
<!-- CommonTiptapViewer.vue -->
<template>
<!-- read-only output -->
<div
v-if="html"
class="p-1 pl-3 group w-full whitespace-pre-wrap break-words"
v-html="html"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { JSONContent } from '@tiptap/core'
const props = defineProps<{
doc: JSONContent | null | undefined
}>()
const escapeHtml = (str: string): string =>
str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
function renderNode(node?: JSONContent): string {
if (!node) return ''
const children = (node.content ?? []).map(renderNode).join('')
switch (node.type) {
case 'doc':
return children
case 'paragraph':
// empty paragraph → visual empty line
return children ? `<p>${children}</p>` : '<p><br /></p>'
case 'text': {
const text = escapeHtml(node.text ?? '')
// if you need marks later (bold, italic, etc.), handle here
return text
}
case 'hardBreak':
return '<br />'
default:
// unknown node → just render its children
return children
}
}
const html = computed(() => (props.doc ? renderNode(props.doc) : ''))
</script>
+77
View File
@@ -0,0 +1,77 @@
<template>
<div class="p-0">
<slot name="activator" :toggle="toggleDialog"></slot>
<CommonDialog v-model:open="showIssuesDialog" :title="`Issues`" fullscreen="none">
<div class="flex flex-col space-y-2">
<div v-if="selectedIssue" class="flex flex-col space-y-1.5">
<div class="relative flex items-center h-8">
<div class="absolute left-0">
<FormButton
color="outline"
hide-text
:icon-left="ArrowLeft"
@click="selectedIssue = undefined"
/>
</div>
<div class="mx-auto text-foreground-2 font-medium font-mono text-body-xs">
{{ selectedIssue.identifier }}
</div>
<div class="absolute right-0">
<FormButton
v-tippy="'Open issue in browser'"
color="outline"
hide-text
:icon-left="ArrowTopRightOnSquareIcon"
@click="openIssueOnWeb(selectedIssue.id)"
/>
</div>
</div>
<hr />
<IssuesSelectedItem :issue="selectedIssue" />
</div>
<div v-if="!selectedIssue" class="flex flex-col space-y-2">
<IssuesItem
v-for="issue in issues"
:key="issue.id"
:issue="issue"
:model-card="modelCard"
@select="selectedIssue = issue"
@open-on-web="(issueId) => openIssueOnWeb(issueId)"
/>
</div>
</div>
</CommonDialog>
</div>
</template>
<script setup lang="ts">
import type { IssuesItemFragment } from '~/lib/common/generated/gql/graphql'
import type { IModelCard } from '~/lib/models/card'
import { ArrowLeft } from 'lucide-vue-next'
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid'
const props = defineProps<{
issues: IssuesItemFragment[]
modelCard: IModelCard
}>()
const app = useNuxtApp()
const showIssuesDialog = ref(false)
const selectedIssue = ref<IssuesItemFragment | undefined>(undefined)
const toggleDialog = () => {
showIssuesDialog.value = !showIssuesDialog.value
}
const openIssueOnWeb = (issueId: string) => {
app.$baseBinding.openUrl(
`${props.modelCard.serverUrl}/projects/${props.modelCard?.projectId}/models/${props.modelCard.modelId}#threadId=${issueId}`
)
}
watch(showIssuesDialog, (open) => {
if (!open) selectedIssue.value = undefined
})
</script>
+142
View File
@@ -0,0 +1,142 @@
<template>
<button
class="gap-1 border rounded-xl border-outline-3 p-1.5 pt-1 pl-3 group hover:shadow-md hover:cursor-pointer space-y-2"
@click="emit('select'), highlightModel()"
>
<!-- Item Header -->
<div class="flex justify-between items-center">
<div class="text-foreground-2 font-medium font-mono text-body-xs">
{{ issue.identifier }}
</div>
<div class="flex items-center">
<FormButton
v-if="store.hostAppName !== 'navisworks' && store.hostAppName !== 'etabs'"
v-tippy="'Highlight'"
color="subtle"
:icon-left="CursorArrowRaysIcon"
hide-text
size="sm"
@click.stop="highlightModel"
/>
<FormButton
v-tippy="'Open issue in browser'"
color="subtle"
:icon-left="ArrowTopRightOnSquareIcon"
hide-text
size="sm"
class="mr-1"
@click.stop="emit('open-on-web', issue.id)"
/>
<UserAvatar :user="issue.assignee?.user" size="xs" class="rounded-full" />
</div>
</div>
<!-- Item Title & status -->
<div class="flex items-center gap-1">
<IssuesStatusIcon :status="issue.status" />
<div class="line-clamp-2 font-medium text-body-2xs text-foreground">
{{ issue.title ? issue.title : 'No title' }}
</div>
</div>
<!-- Remaining secondary fields -->
<div class="flex items-center gap-4 ml-0.5">
<IssuesPriorityIcon :priority="issue.priority" />
<IssuesLabels :labels="issue.labels" />
<div v-if="formattedDate" class="flex items-center gap-1 h-6">
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
<span class="text-body-3xs text-foreground-2 font-medium">
{{ formattedDate }}
</span>
</div>
<div v-else class="flex items-center gap-1 h-6">
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
<span class="text-body-3xs text-foreground-2 font-medium">No due date</span>
</div>
</div>
</button>
</template>
<script lang="ts" setup>
import type { IssuesItemFragment } from '~/lib/common/generated/gql/graphql'
import { CursorArrowRaysIcon } from '@heroicons/vue/24/outline'
import { Calendar } from 'lucide-vue-next'
import dayjs from 'dayjs'
import { useHostAppStore } from '~~/store/hostApp'
import { ToastNotificationType } from '@speckle/ui-components'
import type { IModelCard } from '~/lib/models/card'
import type { SenderModelCard } from '~/lib/models/card/send'
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid'
const store = useHostAppStore()
const props = defineProps<{
modelCard: IModelCard
issue: IssuesItemFragment
}>()
const emit = defineEmits<{
(e: 'select'): void
(e: 'open-on-web', issueId: string): void
}>()
const app = useNuxtApp()
type IssueViewerState = {
ui: {
filters: {
selectedObjectApplicationIds?: Record<string, string>
}
}
}
const highlightModel = async () => {
if (!props.issue.viewerState) {
store.setNotification({
title: 'Objects not found to highlight',
type: ToastNotificationType.Info
})
return
}
if (props.modelCard.typeDiscriminator !== 'SenderModelCard') return
const sender = props.modelCard as SenderModelCard
type SelectedObjectMap = Record<string, string>
const selectedObjectApplicationIds = Object.values(
((props.issue.viewerState as IssueViewerState).ui.filters
.selectedObjectApplicationIds ?? {}) as SelectedObjectMap
)
const appIdsToHighlight = (sender.sendFilter?.selectedObjectIds ?? []).filter((id) =>
selectedObjectApplicationIds.includes(id)
)
if (appIdsToHighlight.length > 0) {
await app.$baseBinding.highlightObjects(appIdsToHighlight)
} else {
store.setNotification({
title: 'Objects not found to highlight on this model.',
type: ToastNotificationType.Info
})
}
}
const formattedDate = computed((): string | null => {
try {
const date = props.issue.dueDate ? dayjs(props.issue.dueDate).toDate() : null
if (!(date instanceof Date)) return null
const time = date.getTime()
if (isNaN(time)) return null
return new Intl.DateTimeFormat('en-GB', {
month: 'short',
day: 'numeric'
}).format(date)
} catch {
return null
}
})
</script>
+39
View File
@@ -0,0 +1,39 @@
<template>
<div class="flex items-center gap-1.5">
<div class="flex items-center -space-x-1">
<template
v-for="labelItem in maxVisible ? labels.slice(0, maxVisible) : labels"
:key="labelItem.id"
>
<div
v-if="labelItem.hexColor"
class="w-2 h-2 rounded-full shrink-0"
:style="{ backgroundColor: labelItem.hexColor }"
/>
</template>
</div>
<!-- Single label -->
<span
v-if="labels.length === 1"
class="text-body-3xs font-medium flex items-center gap-1"
:style="{ color: labels[0].hexColor || undefined }"
>
{{ labels[0].name }}
</span>
<!-- Multiple labels -->
<span v-else class="text-body-3xs text-foreground-2 font-medium">
{{ labels.length }} label{{ labels.length !== 1 ? 's' : '' }}
</span>
</div>
</template>
<script setup lang="ts">
import type { Label } from '~/lib/issues/types'
defineProps<{
labels: Label[]
maxVisible?: number
}>()
</script>
+34
View File
@@ -0,0 +1,34 @@
<template>
<Tippy interactive placement="bottom" :offset="[0, 6]">
<!-- Trigger -->
<template #default>
<IssuesLabelGroup :labels="labels" />
</template>
<!-- Tooltip content -->
<template v-if="labels.length > 0" #content>
<div class="rounded-md shadow-lg p-0.5 text-xs space-y-1">
<div
v-for="label in labels"
:key="label.id"
class="flex items-center space-x-2"
>
<span
class="w-2 h-2 rounded-full"
:style="{ backgroundColor: label.hexColor }"
/>
<span>{{ label.name }}</span>
</div>
</div>
</template>
</Tippy>
</template>
<script setup lang="ts">
import { Tippy } from 'vue-tippy'
import type { Label } from '~/lib/issues/types'
defineProps<{
labels: Label[]
}>()
</script>
+60
View File
@@ -0,0 +1,60 @@
<template>
<div class="flex items-center space-x-2">
<div
v-if="priority !== null && priority !== 'none'"
v-tippy="showLabel ? undefined : priorityText"
class="flex flex-col gap-0.5 items-start justify-center w-3 h-3"
>
<!-- Top line -->
<div
class="h-0.5 rounded-full bg-foreground-2 w-3"
:class="priority !== 'high' && 'opacity-25'"
/>
<!-- Middle line -->
<div
class="h-0.5 rounded-full bg-foreground-2 w-2"
:class="priority === 'low' && 'opacity-25'"
/>
<!-- Bottom line -->
<div class="h-0.5 rounded-full bg-foreground-2 w-1" />
</div>
<!-- No priority: Two dashes -->
<div v-else class="flex gap-0.5 items-center justify-center h-3 w-3">
<div class="h-px rounded-full bg-foreground-3 w-1" />
<div class="h-px rounded-full bg-foreground-3 w-1" />
</div>
<span v-if="showLabel" class="text-body-3xs text-foreground-2 font-medium">
{{ priorityText }}
</span>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
priority: 'none' | 'low' | 'medium' | 'high' | null
showLabel?: boolean
}>(),
{
showLabel: false
}
)
const priorityText = computed(() => {
switch (props.priority) {
case 'high':
return 'High'
case 'medium':
return 'Medium'
case 'low':
return 'Low'
case 'none':
return 'No priority'
case null:
return 'No priority'
default:
return ''
}
})
</script>
+112
View File
@@ -0,0 +1,112 @@
<template>
<div class="flex flex-col space-y-1.5">
<div class="flex flex-col items-start space-y-2 p-2">
<div class="line-clamp-2 font-medium text-body text-foreground">
{{ issue.title ? issue.title : 'No title' }}
</div>
<IssuesBasicTiptap
v-if="issue.description?.doc"
class="border rounded-xl border-outline-3"
:doc="issue.description?.doc"
></IssuesBasicTiptap>
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
<IssuesStatusIcon :status="issue.status" show-label />
<IssuesPriorityIcon :priority="issue.priority" show-label />
<div class="flex items-center justify-between space-x-1">
<UserAvatar :user="issue.assignee?.user" size="xs" />
<span class="text-body-3xs text-foreground-2 font-medium">
{{ issue.assignee ? issue.assignee?.user.name : 'No assignee' }}
</span>
</div>
<IssuesLabels :labels="issue.labels" />
<div v-if="formattedDate" class="flex items-center gap-1 h-6">
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
<span class="text-body-3xs text-foreground-2 font-medium">
{{ formattedDate }}
</span>
</div>
<div v-else class="flex items-center gap-1 h-6">
<Calendar class="text-foreground-2 shrink-0" :stroke-width="1.5" :size="12" />
<span class="text-body-3xs text-foreground-2 font-medium">No due date</span>
</div>
</div>
<div
v-if="issue.activities && issue.activities.totalCount > 0"
class="flex items-center gap-2 p-1 min-w-0"
>
<UserAvatar
:user="issue.activities?.items?.[0]?.actor?.user"
size="xs"
class="shrink-0"
/>
<div class="text-body-2xs text-foreground-2 leading-tight min-w-0">
<span class="font-medium">
{{ issue.activities?.items?.[0]?.actor?.user.name }}
</span>
<span>
&nbsp;created this issue &middot;
{{ dayjs(issue.activities?.items?.[0].createdAt).from(dayjs()) }}
</span>
</div>
</div>
<div
v-if="issue.replies && issue.replies.totalCount > 0"
class="flex flex-col justify-between space-y-2 w-full"
>
<div
v-for="reply in issue.replies.items"
:key="reply.id"
class="flex flex-col items-start border rounded-xl border-outline-3 p-1 w-full"
>
<div class="flex items-center gap-2 w-full">
<UserAvatar :user="reply.author?.user" size="xs" class="shrink-0" />
<div class="text-body-2xs text-foreground-2 leading-tight min-w-0">
<span class="font-medium">
{{ reply.author?.user.name }}
</span>
<span>
&nbsp;replied &middot;
{{ dayjs(reply.createdAt).from(dayjs()) }}
</span>
</div>
</div>
<IssuesBasicTiptap
v-if="reply.description?.doc"
class="ml-4"
:doc="reply.description?.doc"
/>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { IssuesItemFragment } from '~/lib/common/generated/gql/graphql'
import dayjs from 'dayjs'
import { Calendar } from 'lucide-vue-next'
const props = defineProps<{
issue: IssuesItemFragment
}>()
const formattedDate = computed((): string | null => {
try {
const date = props.issue.dueDate ? dayjs(props.issue.dueDate).toDate() : null
if (!(date instanceof Date)) return null
const time = date.getTime()
if (isNaN(time)) return null
return new Intl.DateTimeFormat('en-GB', {
month: 'short',
day: 'numeric'
}).format(date)
} catch {
return null
}
})
</script>
+49
View File
@@ -0,0 +1,49 @@
<template>
<div
v-tippy="showLabel ? undefined : statusText"
class="flex items-center gap-1 rounded-md hover:bg-foreground-1"
>
<GlobalIconStatusOpen v-if="status === 'open'" class="w-3 h-3 shrink-0" />
<GlobalIconStatusReview
v-else-if="status === 'readyForReview'"
class="w-3 h-3 shrink-0"
/>
<GlobalIconStatusResolved
v-else-if="status === 'resolved'"
class="w-3 h-3 shrink-0"
/>
<span v-if="showLabel" class="text-body-3xs text-foreground-2 font-medium">
{{ statusText }}
</span>
</div>
</template>
<script setup lang="ts">
import GlobalIconStatusOpen from '~/components/global/icon/StatusOpen.vue'
import GlobalIconStatusReview from '~/components/global/icon/StatusReview.vue'
import GlobalIconStatusResolved from '~/components/global/icon/StatusResolved.vue'
const props = withDefaults(
defineProps<{
status: 'open' | 'readyForReview' | 'resolved'
showLabel?: boolean
}>(),
{
showLabel: false
}
)
const statusText = computed(() => {
switch (props.status) {
case 'open':
return 'Open'
case 'readyForReview':
return 'Ready for review'
case 'resolved':
return 'Resolved'
default:
return ''
}
})
</script>
+33
View File
@@ -5,6 +5,7 @@
:icon-left="Bars3Icon"
hide-text
size="sm"
:disabled="!!props.modelCard.progress"
@click.stop="openModelCardActionsDialog = true"
/>
<CommonDialog
@@ -32,6 +33,18 @@
</button>
</template>
</ReportBase>
<IssuesDialog
v-if="issues && issues.length > 0"
:model-card="modelCard"
:issues="issues"
>
<template #activator="{ toggle }">
<button class="action action-normal" @click="toggle()">
<div class="truncate max-[275px]:text-xs">Issues</div>
<div><Cog6ToothIcon class="w-5 h-5" /></div>
</button>
</template>
</IssuesDialog>
<button
v-for="item in items"
:key="item.name"
@@ -57,6 +70,10 @@ import {
} from '@heroicons/vue/24/outline'
import type { IModelCard } from '~/lib/models/card'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { issuesListQuery } from '~/lib/issues/graphql/queries'
import { useAccountStore } from '~/store/accounts'
import { storeToRefs } from 'pinia'
import { useQuery } from '@vue/apollo-composable'
const { trackEvent } = useMixpanel()
@@ -113,6 +130,22 @@ const items = [
}
}
]
const accountStore = useAccountStore()
const { activeAccount } = storeToRefs(accountStore)
const accountId = computed(() => activeAccount.value.accountInfo.id)
const { result: issuesResult } = useQuery(
issuesListQuery,
() => ({ projectId: props.modelCard.projectId }),
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: 'network-only'
})
)
const issues = computed(() => issuesResult?.value?.project.issues.items)
</script>
<style scoped lang="postcss">
.action {
+63 -16
View File
@@ -5,10 +5,12 @@
>
<div v-if="modelData" class="relative px-1 py-1">
<div class="relative flex items-center space-x-2 min-w-0">
<div class="text-foreground-2 mt-[2px] flex items-center -space-x-2 relative">
<div
v-tippy="buttonTooltip"
class="text-foreground-2 mt-[2px] flex items-center -space-x-2 relative"
>
<!-- CTA button -->
<FormButton
v-tippy="buttonTooltip"
color="outline"
:icon-left="
modelCard.progress
@@ -19,7 +21,9 @@
"
hide-text
class=""
:disabled="!canEdit || isSettingsMissing"
:disabled="
(!canEdit || isSettingsMissing || ctaDisabled) && !modelCard.progress
"
@click.stop="$emit('manual-publish-or-load')"
></FormButton>
</div>
@@ -67,6 +71,22 @@
size="sm"
@click="deleteSettings"
/> -->
<IssuesDialog
v-if="issues && issues.length > 0"
:model-card="modelCard"
:issues="issues"
>
<template #activator="{ toggle }">
<FormButton
v-tippy="'Issues'"
color="subtle"
:icon-left="MessageCircleMore"
hide-text
size="sm"
@click="toggle()"
/>
</template>
</IssuesDialog>
<FormButton
v-if="store.hostAppName !== 'navisworks' && store.hostAppName !== 'etabs'"
v-tippy="'Highlight'"
@@ -179,7 +199,7 @@
>
<div
v-tippy="
`${latestCommentNotification.comment?.author.name} just left a
`${latestCommentNotification.comment?.author?.name} just left a
comment.`
"
class="flex items-center space-x-1"
@@ -189,8 +209,8 @@
:users="[latestCommentNotification.comment?.author as AvatarUserWithId]"
/>
<span class="line-clamp-1">
{{ latestCommentNotification.comment?.author.name }} just left a
comment.
{{ latestCommentNotification.comment?.author?.name }} just left a
comment on the issue.
</span>
</div>
<div>
@@ -236,17 +256,26 @@ import type { ProjectCommentsUpdatedMessage } from '~/lib/common/generated/gql/g
import { useFunctionRunsStatusSummary } from '~/lib/automate/runStatus'
import { CursorArrowRaysIcon, XCircleIcon, TrashIcon } from '@heroicons/vue/24/outline'
import type { AvatarUserWithId } from '@speckle/ui-components'
import { issuesListQuery } from '~/lib/issues/graphql/queries'
import { MessageCircleMore } from 'lucide-vue-next'
const app = useNuxtApp()
const store = useHostAppStore()
const accStore = useAccountStore()
const { trackEvent } = useMixpanel()
const props = defineProps<{
modelCard: IModelCard
project: ProjectModelGroup
canEdit: boolean
}>()
const props = withDefaults(
defineProps<{
modelCard: IModelCard
project: ProjectModelGroup
canEdit: boolean
ctaDisabled?: boolean
ctaDisabledMessage?: string
}>(),
{
ctaDisabled: false
}
)
defineEmits<{
(e: 'manual-publish-or-load'): void
@@ -257,11 +286,9 @@ const isSender = computed(() => {
})
const buttonTooltip = computed(() => {
return props.modelCard.progress
? 'Cancel'
: isSender.value
? 'Publish model'
: 'Load selected version'
if (props.modelCard.progress) return 'Cancel'
if (props.ctaDisabled) return props.ctaDisabledMessage
return isSender.value ? 'Publish model' : 'Load selected version'
})
const projectAccount = computed(() =>
@@ -327,6 +354,25 @@ const summary = computed(() => {
})
})
const { result: issuesResult, refetch: refetchIssues } = useQuery(
issuesListQuery,
() => ({ projectId: props.modelCard.projectId }),
() => ({
clientId,
debounce: 500,
fetchPolicy: 'network-only'
})
)
const issues = computed(() =>
issuesResult?.value?.project.issues.items.filter(
(issue) =>
issue.status !== 'resolved' &&
issue.resourceIdString &&
(issue.resourceIdString as string).includes(props.modelCard.modelId)
)
)
provide<IModelCard>('cardBase', props.modelCard)
const highlightModel = () => {
@@ -486,6 +532,7 @@ onCommentResult((res) => {
latestCommentNotification.value = res.data
?.projectCommentsUpdated as ProjectCommentsUpdatedMessage
startCommentClearTimeout()
refetchIssues()
})
const viewComment = () => {
+98 -25
View File
@@ -4,6 +4,8 @@
:model-card="modelCard"
:project="project"
:can-edit="canEdit"
:cta-disabled="ctaDisabled"
:cta-disabled-message="ctaDisabledMessage"
@manual-publish-or-load="sendOrCancel"
>
<div class="flex max-[275px]:w-full overflow-hidden my-2">
@@ -17,7 +19,6 @@
full-width
@click.stop="openFilterDialog = true"
>
<!-- Sending&nbsp; -->
<span class="font-bold">{{ modelCard.sendFilter?.name }}:&nbsp;</span>
<span class="truncate">{{ modelCard.sendFilter?.summary }}</span>
</FormButton>
@@ -31,13 +32,23 @@
<FilterListSelect :filter="modelCard.sendFilter" @update:filter="updateFilter" />
<div class="mt-4 flex justify-end items-center space-x-2">
<!-- TODO: Ux wise, users might want to just save the selection and publish it later. -->
<FormButton size="sm" color="outline" @click.stop="saveFilter()">
<FormButton
size="sm"
color="outline"
:disabled="isSaveDisabled"
@click.stop="saveFilter()"
>
Save
</FormButton>
<FormButton size="sm" @click.stop="saveFilterAndSend()">
Save & Publish
</FormButton>
<div v-tippy="!canCreateVersionPerm ? canCreateVersionMessage : ''">
<FormButton
size="sm"
:disabled="!canCreateVersionPerm || isSaveDisabled"
@click.stop="saveFilterAndSend()"
>
Save & Publish
</FormButton>
</div>
</div>
</CommonDialog>
@@ -108,7 +119,7 @@
</ModelCardBase>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onMounted, computed } from 'vue'
import ModelCardBase from '~/components/model/CardBase.vue'
import { Square3Stack3DIcon } from '@heroicons/vue/20/solid'
import type { ModelCardNotification } from '~/lib/models/card/notification'
@@ -117,13 +128,22 @@ import type { ProjectModelGroup } from '~/store/hostApp'
import { useHostAppStore } from '~/store/hostApp'
import { useMixpanel } from '~/lib/core/composables/mixpanel'
import { ToastNotificationType, ValidationHelpers } from '@speckle/ui-components'
import { provideApolloClient, useMutation } from '@vue/apollo-composable'
import {
provideApolloClient,
useMutation,
useSubscription
} from '@vue/apollo-composable'
import { useAccountStore, type DUIAccount } from '~/store/accounts'
import { setVersionMessageMutation } from '~/lib/graphql/mutationsAndQueries'
const hostAppStore = useHostAppStore()
import { workspacePlanUsageUpdatedSubscription } from '~/lib/workspaces/graphql/subscriptions'
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
const store = useHostAppStore()
const accountStore = useAccountStore()
const { trackEvent } = useMixpanel()
const app = useNuxtApp()
const { canCreateModelIngestion } = useCheckGraphql()
const cardBase = ref<InstanceType<typeof ModelCardBase>>()
const props = defineProps<{
@@ -132,37 +152,87 @@ const props = defineProps<{
canEdit: boolean
}>()
const store = useHostAppStore()
const account = accountStore.accounts.find(
(acc) => acc.accountInfo.id === props.project.accountId
) as DUIAccount
const clientId = account.accountInfo.id
const openFilterDialog = ref(false)
app.$baseBinding?.on('documentChanged', () => {
openFilterDialog.value = false
})
const canCreateVersionPerm = ref(true)
const canCreateVersionMessage = ref<string | null>(null)
const checkPermissions = async () => {
const res = await canCreateModelIngestion(
props.modelCard.projectId,
props.modelCard.modelId,
props.modelCard.accountId
)
if (res.queryAvailable) {
canCreateVersionPerm.value = res.authorized
canCreateVersionMessage.value = res.message || null
}
}
const ctaDisabled = computed(
() => !canCreateVersionPerm.value || !!props.modelCard.progress
)
const ctaDisabledMessage = computed(() => canCreateVersionMessage.value || undefined)
const { onResult: onWorkspacePlanUsageUpdated } = useSubscription(
workspacePlanUsageUpdatedSubscription,
() => ({
input: {
workspaceId: props.modelCard.workspaceId as string
}
}),
() => ({ clientId })
)
onWorkspacePlanUsageUpdated(() => {
void checkPermissions()
})
const sendOrCancel = () => {
if (!props.canEdit) {
// check for progress first to allow cancelling even if permissions changed
if (props.modelCard.progress) {
store.sendModelCancel(props.modelCard.modelCardId)
return
}
if (props.modelCard.progress) store.sendModelCancel(props.modelCard.modelCardId)
else store.sendModel(props.modelCard.modelCardId, 'ModelCardButton')
if (!props.canEdit || !canCreateVersionPerm.value) {
return
}
store.sendModel(props.modelCard.modelCardId, 'ModelCardButton')
hasSetVersionMessage.value = false
}
let newFilter: ISendFilter
const newFilter = ref<ISendFilter>()
const updateFilter = (filter: ISendFilter) => {
newFilter = filter
newFilter.value = filter
}
const isSaveDisabled = computed(() => {
const filterToCheck = newFilter.value || props.modelCard.sendFilter
return !store.validateSendFilter(filterToCheck).valid
})
const saveFilter = async () => {
if (!newFilter.value) return // Safety check
void trackEvent('DUI3 Action', {
name: 'Publish Card Filter Change',
filter: newFilter.typeDiscriminator
filter: newFilter.value.typeDiscriminator
})
// do not reset idmap while creating a new one because it is managed by host app
newFilter.idMap = props.modelCard.sendFilter?.idMap
newFilter.value.idMap = props.modelCard.sendFilter?.idMap
await store.patchModel(props.modelCard.modelCardId, {
sendFilter: newFilter,
sendFilter: newFilter.value,
expired: true
})
openFilterDialog.value = false
@@ -173,11 +243,6 @@ const isUpdatingVersionMessage = ref(false)
const hasSetVersionMessage = ref(false)
const versionMessage = ref<string>()
const accountStore = useAccountStore()
const account = accountStore.accounts.find(
(acc) => acc.accountInfo.id === props.project.accountId
) as DUIAccount
const setVersionMessage = async (message: string) => {
if (!props.modelCard.latestCreatedVersionId) {
return
@@ -203,14 +268,14 @@ const setVersionMessage = async (message: string) => {
if (res?.data?.versionMutations.update.id) {
// seemed to noisy, and autoclose does not work for some reason.
// nicer ux to just close the dialog
// hostAppStore.setNotification({
// store.setNotification({
// type: ToastNotificationType.Info,
// title: 'Version message saved',
// autoClose: true
// })
hasSetVersionMessage.value = true
} else {
hostAppStore.setNotification({
store.setNotification({
type: ToastNotificationType.Danger,
title: 'Request failed',
description: 'Failed to update version message.',
@@ -261,6 +326,10 @@ const expiredNotification = computed(() => {
const ctaType = props.modelCard.progress ? 'Restart' : 'Update'
notification.cta = {
name: ctaType,
disabled: !canCreateVersionPerm.value,
tooltipText: !canCreateVersionPerm.value
? canCreateVersionMessage.value || 'Publish limit reached'
: undefined,
action: async () => {
hasSetVersionMessage.value = false
if (props.modelCard.progress) {
@@ -337,4 +406,8 @@ const latestVersionNotification = computed(() => {
}
return notification
})
onMounted(() => {
void checkPermissions()
})
</script>
+99 -18
View File
@@ -21,7 +21,6 @@
@search-text-update="updateSearchText"
/>
</div>
<!-- Model selector wizard -->
<div v-if="step === 2 && selectedProject && selectedAccountId">
<WizardModelSelector
:project="selectedProject"
@@ -32,7 +31,6 @@
@next="selectModel"
/>
</div>
<!-- Version selector wizard -->
<div v-if="step === 3">
<SendFiltersAndSettings
v-model="filter"
@@ -44,8 +42,15 @@
}
"
/>
<div class="mt-2">
<FormButton full-width @click="addModel">Publish</FormButton>
<div v-tippy="publishTooltipMessage" class="mt-2">
<FormButton
full-width
:disabled="isPublishDisabled"
:loading="isLoadingPermissions"
@click="addModel"
>
Publish
</FormButton>
</div>
</div>
<div v-if="urlParseError" class="p-2 text-danger">
@@ -55,6 +60,7 @@
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useSubscription } from '@vue/apollo-composable'
import type {
ModelListModelItemFragment,
ProjectListProjectItemFragment
@@ -63,10 +69,13 @@ 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 { useSelectionStore } from '~/store/selection'
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()
@@ -87,6 +96,29 @@ const settings = ref<CardSetting[] | undefined>(undefined)
const settingsWereChanged = ref(false)
const { tryParseUrl, urlParsedData, urlParseError } = useAddByUrl()
const { canCreateModelIngestion, canCreateVersion } = useCheckGraphql()
const canPublish = ref(false)
const publishLimitMessage = ref<string | undefined>(undefined)
const isLoadingPermissions = ref(false)
const hostAppStore = useHostAppStore()
const selectionStore = useSelectionStore()
const publishValidation = computed(() => hostAppStore.validateSendFilter(filter.value))
const isPublishDisabled = computed(() => {
return (
!canPublish.value || isLoadingPermissions.value || !publishValidation.value.valid
)
})
const publishTooltipMessage = computed(() => {
if (!publishValidation.value.valid) return publishValidation.value.reason
if (!canPublish.value && !isLoadingPermissions.value)
return publishLimitMessage.value || ''
return ''
})
const updateSearchText = (text: string | undefined) => {
urlParseError.value = undefined
if (!text) return
@@ -102,9 +134,72 @@ watch(urlParsedData, (newVal) => {
watch(showSendDialog, (newVal) => {
if (newVal) {
urlParseError.value = undefined
void selectionStore.refreshSelectionFromHostApp()
}
})
const checkPermissions = async () => {
if (!selectedProject.value || !selectedModel.value) return
isLoadingPermissions.value = true
try {
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
}
} finally {
isLoadingPermissions.value = false
}
}
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
@@ -125,20 +220,6 @@ const selectModel = (model: ModelListModelItemFragment) => {
void trackEvent('DUI3 Action', { name: 'Publish Wizard', step: 'model selected' })
}
// Clears data if going backwards in the wizard
watch(step, (newVal, oldVal) => {
if (newVal > oldVal) {
return // exit fast on forward
}
if (newVal === 1) {
selectedProject.value = undefined
selectedModel.value = undefined
}
if (newVal === 2) selectedModel.value = undefined
})
const hostAppStore = useHostAppStore()
// accountId, serverUrl, projectId, modelId, sendFilter, settings
const addModel = async () => {
void trackEvent('DUI3 Action', {
-163
View File
@@ -1,163 +0,0 @@
<template>
<div
:class="[
'text-foreground-on-primary flex shrink-0 items-center justify-center overflow-hidden rounded-full font-semibold uppercase transition',
sizeClasses,
bgClasses,
borderClasses,
hoverClasses,
activeClasses
]"
>
<slot>
<div
v-if="user?.avatar"
class="h-full w-full bg-cover bg-center bg-no-repeat"
:style="{ backgroundImage: `url('${user.avatar}')` }"
/>
<div
v-else-if="initials"
class="flex h-full w-full select-none items-center justify-center"
>
{{ initials }}
</div>
<div v-else><UserCircleIcon :class="iconClasses" /></div>
</slot>
</div>
</template>
<script setup lang="ts">
import { UserCircleIcon } from '@heroicons/vue/20/solid'
type UserAvatar = {
name: string
avatar?: string | null | undefined
}
type UserAvatarSize = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | 'editable'
const props = withDefaults(
defineProps<{
user?: UserAvatar
size?: UserAvatarSize
hoverEffect?: boolean
active?: boolean
noBorder?: boolean
noBackground?: boolean
}>(),
{
user: undefined,
size: 'base',
hoverEffect: false
}
)
const initials = computed(() => {
if (!props.user?.name.length) return
const parts = props.user.name.split(' ')
const firstLetter = parts[0]?.[0] || ''
const secondLetter = parts[1]?.[0] || ''
if (props.size === 'sm' || props.size === 'xs') return firstLetter
return firstLetter + secondLetter
})
const borderClasses = computed(() => {
if (props.noBorder) return ''
return 'border-2 border-foundation'
})
const bgClasses = computed(() => {
if (props.noBackground) return ''
return 'bg-primary'
})
const hoverClasses = computed(() => {
if (props.hoverEffect)
return 'hover:border-primary focus:border-primary active:scale-95'
return ''
})
const activeClasses = computed(() => {
if (props.active) return 'border-primary'
return ''
})
const heightClasses = computed(() => {
const size = props.size
switch (size) {
case 'xs':
return 'h-5'
case 'sm':
return 'h-6'
case 'lg':
return 'h-10'
case 'xl':
return 'h-14'
case 'editable':
return 'h-60'
case 'base':
default:
return 'h-8'
}
})
const widthClasses = computed(() => {
const size = props.size
switch (size) {
case 'xs':
return 'w-5'
case 'sm':
return 'w-6'
case 'lg':
return 'w-10'
case 'xl':
return 'w-14'
case 'editable':
return 'w-60'
case 'base':
default:
return 'w-8'
}
})
const textClasses = computed(() => {
const size = props.size
switch (size) {
case 'xs':
return 'text-tiny'
case 'sm':
return 'text-xs'
case 'lg':
return 'text-md'
case 'xl':
return 'text-2xl'
case 'editable':
return 'h1'
case 'base':
default:
return 'text-sm'
}
})
const iconClasses = computed(() => {
const size = props.size
switch (size) {
case 'xs':
return 'w-3 h-3'
case 'sm':
return 'w-3 h-3'
case 'lg':
return 'w-5 h-5'
case 'xl':
return 'w-8 h-8'
case 'editable':
return 'w-20 h-20'
case 'base':
default:
return 'w-4 h-4'
}
})
const sizeClasses = computed(
() => `${widthClasses.value} ${heightClasses.value} ${textClasses.value}`
)
</script>
-27
View File
@@ -32,33 +32,6 @@
</FormButton>
</div>
</div>
<div
v-if="
canCreateModelResult &&
!canCreateModelResult.project.permissions.canCreateModel.authorized
"
>
<CommonAlert title="Cannot create new models" color="info" hide-icon>
<template #description>
{{ canCreateModelResult.project.permissions.canCreateModel.message }}
<FormButton
v-if="workspaceSlug"
full-width
color="primary"
size="sm"
class="mt-2"
@click="
$openUrl(
`${account.accountInfo.serverInfo.url}/settings/workspaces/${workspaceSlug}/billing`
)
"
>
Explore Plans
</FormButton>
</template>
</CommonAlert>
</div>
<div class="relative grid grid-cols-1 gap-2">
<CommonLoadingBar v-if="loading" loading />
+46 -67
View File
@@ -3,12 +3,18 @@
<div class="space-y-2 relative">
<div v-if="workspacesEnabled && workspaces" class="flex items-center space-x-2">
<div class="flex-grow min-w-0">
<!-- NO WORKSPACE YET -->
<div v-if="workspaces.length === 0">
<FormButton
full-width
class="flex items-center"
@click="$openUrl('https://app.speckle.systems/workspaces/actions/create')"
@click="
$openUrl(
`${activeAccount.accountInfo.serverInfo.url.replace(
/\/$/,
''
)}/workspaces/actions/create`
)
"
>
<div class="min-w-0 truncate flex-grow">
<span>{{ 'Create a workspace' }}</span>
@@ -31,7 +37,7 @@
<WorkspaceAvatar
:size="'xs'"
:name="selectedWorkspace.name || ''"
:logo="selectedWorkspace.logo"
:logo="selectedWorkspace.logoUrl"
/>
<div class="min-w-0 truncate flex-grow text-left">
<span>{{ selectedWorkspace.name }}</span>
@@ -72,13 +78,7 @@
color="foundation"
/>
<div class="flex justify-between items-center space-x-2">
<div
v-tippy="
canCreateProject
? 'Create new project'
: canCreateProjectPermissionCheck?.message
"
>
<div v-if="canCreateProject" v-tippy="'Create new project'">
<FormButton
color="outline"
:disabled="!canCreateProject"
@@ -88,6 +88,22 @@
<PlusIcon class="w-4 -mx-2" />
</FormButton>
</div>
<div
v-else
v-tippy="
canCreateProject
? 'Create new project'
: canCreateProjectPermissionCheck?.message
"
>
<FormButton
color="primary"
:class="`p-1.5 bg-foundation rounded text-foreground border`"
@click="upgradePlanButtonAction"
>
<ArrowUpCircleIcon class="w-4 -mx-2" />
</FormButton>
</div>
<CommonDialog
v-model:open="showProjectCreateDialog"
:title="`Create new project`"
@@ -132,28 +148,6 @@
</div>
</div>
</div>
<div
v-if="
canCreateProjectPermissionCheck &&
!canCreateProjectPermissionCheck.authorized
"
>
<CommonAlert color="info" hide-icon>
<template #description>
{{ canCreateProjectPermissionCheck.message }}
<FormButton
v-if="showUpgradeButton"
full-width
class="mt-2"
color="primary"
size="sm"
@click="upgradeButtonAction()"
>
Upgrade now
</FormButton>
</template>
</CommonAlert>
</div>
<WizardPersonalProjectsWarning v-if="isPersonalProjectsAsWorkspace" />
@@ -202,7 +196,7 @@
<script setup lang="ts">
import { ChevronDownIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline'
import { storeToRefs } from 'pinia'
import { PlusIcon } from '@heroicons/vue/20/solid'
import { PlusIcon, ArrowUpCircleIcon } from '@heroicons/vue/20/solid'
import type { DUIAccount } from '~/store/accounts'
import { useAccountStore } from '~/store/accounts'
import {
@@ -341,11 +335,7 @@ const activeWorkspace = computed(() => {
if (activeWorkspace) return activeWorkspace
}
// if activeWorkspace is null will mean that it is personal projects - this fallback wont be the case soon
return {
id: 'personalProject',
name: 'Personal Projects'
} as WorkspaceListWorkspaceItemFragment
return workspaces.value?.[0] // fallback to first workspace if none is active
})
const selectedWorkspace = ref<WorkspaceListWorkspaceItemFragment | undefined>(
@@ -497,35 +487,6 @@ const canCreateProjectPermissionCheck = computed(() => {
return null
})
const upgradeButtonAction = () => {
if (!canCreateProjectPermissionCheck.value) return
if (canCreateProjectPermissionCheck.value.code === 'WorkspaceNoEditorSeat') {
// open url to workspace/settings/users
$openUrl(
`${account.value.accountInfo.serverInfo.url}/settings/workspaces/${selectedWorkspace.value?.slug}/members`
)
return
}
if (canCreateProjectPermissionCheck.value.code === 'WorkspaceLimitsReached') {
// open url to workspace/billing
$openUrl(
`${account.value.accountInfo.serverInfo.url}/settings/workspaces/${selectedWorkspace.value?.slug}/billing`
)
return
}
}
const showUpgradeButton = computed(() => {
if (!canCreateProjectPermissionCheck.value) return false
if (
canCreateProjectPermissionCheck.value.code === 'WorkspaceNoEditorSeat' ||
canCreateProjectPermissionCheck.value.code === 'WorkspaceLimitsReached'
) {
return true
}
return false
})
const isCreatingProject = ref(false)
const showProjectCreateDialog = ref(false)
@@ -614,6 +575,24 @@ const createNewPersonalProject = async (name: string) => {
isCreatingProject.value = false
}
const upgradePlanButtonAction = () => {
if (!canCreateProjectPermissionCheck.value) return
if (canCreateProjectPermissionCheck.value.code === 'WorkspaceNoEditorSeat') {
// open url to workspace/settings/users
$openUrl(
`${account.value.accountInfo.serverInfo.url}/settings/workspaces/${selectedWorkspace.value?.slug}/members`
)
return
}
if (canCreateProjectPermissionCheck.value.code === 'WorkspaceLimitsReached') {
// open url to workspace/billing
$openUrl(
`${account.value.accountInfo.serverInfo.url}/settings/workspaces/${selectedWorkspace.value?.slug}/billing`
)
return
}
}
const loadMore = () => {
fetchMore({
variables: { cursor: projectsResult.value?.activeUser?.projects.cursor },
+1 -1
View File
@@ -14,7 +14,7 @@
<WorkspaceAvatar
:size="'sm'"
:name="workspace.name || ''"
:logo="workspace.logo"
:logo="workspace.logoUrl"
/>
<div class="min-w-0 grow">
<div class="truncate overflow-hidden min-w-0 flex items-center space-x-2">
+1 -7
View File
@@ -33,13 +33,7 @@ defineEmits<{
(e: 'workspace:selected', result: WorkspaceListWorkspaceItemFragment): void
}>()
const workspacesWithPersonalProjects = computed(() => [
...props.workspaces.filter((w) => w.creationState?.completed !== false),
{
id: 'personalProject',
name: 'Personal Projects'
} as WorkspaceListWorkspaceItemFragment
])
const workspacesWithPersonalProjects = computed(() => [...props.workspaces])
const toggleDialog = () => {
showWorkspaceSelectorDialog.value = !showWorkspaceSelectorDialog.value
View File
+29
View File
@@ -0,0 +1,29 @@
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const CHALLENGE_KEY = 'speckle_challenge'
const CHALLENGE_URL_KEY = 'speckle_url_challenge'
export function useAuthManager() {
const generateChallenge = (url: string): string => {
let result = ''
for (let i = 0; i < 12; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length))
}
localStorage.setItem(CHALLENGE_KEY, result) // <-- persist it
localStorage.setItem(CHALLENGE_URL_KEY, url)
return result
}
const getChallenge = (): string | null => {
return localStorage.getItem(CHALLENGE_KEY)
}
const getChallengeUrl = (): string | null => {
return localStorage.getItem(CHALLENGE_URL_KEY)
}
return {
getChallenge,
getChallengeUrl,
generateChallenge
}
}
@@ -7,6 +7,7 @@ export const IAccountBindingKey = 'accountsBinding'
export interface IAccountBinding extends IBinding<IAccountBindingEvents> {
getAccounts: () => Promise<Account[]>
addAccount: (accountId: string, account: Account) => Promise<void>
removeAccount: (accountId: string) => Promise<void>
}
@@ -15,6 +16,7 @@ export type Account = {
id: string
isDefault: boolean
token: string
refreshToken: string
serverInfo: {
name: string
url: string
@@ -54,6 +56,10 @@ export class MockedAccountBinding implements IAccountBinding {
]) as Account[]
}
public async addAccount(accountId: string, account: Account) {
return await console.log('no way dude', accountId, account)
}
public async removeAccount(accountId: string) {
return await console.log('no way dude', accountId)
}
+1
View File
@@ -26,6 +26,7 @@ export interface ISendBindingEvents
modelCardId: string
versionId: string
sendConversionResults: ConversionResult[]
ingestionId?: string
}) => void
setIdMap: (args: {
modelCardId: string
+69 -15
View File
@@ -16,6 +16,9 @@ import type { Emitter } from 'nanoevents'
import { useDesktopService } from '~/lib/core/composables/desktopService'
import type { ToastNotification } from '@speckle/ui-components'
import { ToastNotificationType } from '@speckle/ui-components'
import { useModelIngestion } from '../ingestion/composables/useModelIngestion'
import type { ISenderModelCard } from '../models/card/send'
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
export type SendBatchViaBrowserArgs = {
modelCardId: string
@@ -466,24 +469,75 @@ export class ArchicadBridge {
}
private async createVersion(args: CreateVersionArgs) {
const accountStore = useAccountStore()
const { accounts } = storeToRefs(accountStore)
const account = accounts.value.find((acc) => acc.accountInfo.id === args.accountId)
const hostAppStore = useHostAppStore()
const { completeIngestionWithVersion } = useModelIngestion()
const { canCreateModelIngestion } = useCheckGraphql()
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
useMutation(createVersionMutation)
const modelCard = hostAppStore.models.find(
(model) => model.modelCardId === args.modelCardId
) as ISenderModelCard
const canCreateIngestion = await canCreateModelIngestion(
modelCard.projectId,
modelCard.modelId,
modelCard.accountId
)
const hostAppStore = useHostAppStore()
const result = await createVersion.mutate({
input: {
modelId: args.modelId,
objectId: args.referencedObjectId,
sourceApplication: hostAppStore.hostAppName,
projectId: args.projectId
if (canCreateIngestion.queryAvailable) {
const ingestionId = hostAppStore.activeIngestions[args.modelCardId]
if (!ingestionId) {
hostAppStore.setNotification({
type: ToastNotificationType.Danger,
title: 'Ingestion Failed',
description: 'Ingestion ID not found to create version.'
})
throw new Error(`Ingestion failed: Ingestion ID not found to create version.`)
}
})
return result?.data?.versionMutations?.create?.id
const res = await completeIngestionWithVersion(
modelCard,
ingestionId,
args.referencedObjectId
)
if (res?.statusData.__typename === 'ModelIngestionSuccessStatus') {
return res?.statusData.versionId
}
if (res?.statusData.__typename === 'ModelIngestionFailedStatus') {
const errorReason = res?.statusData.errorReason || 'Unknown error'
hostAppStore.setNotification({
type: ToastNotificationType.Danger,
title: 'Ingestion Failed',
description: errorReason
})
throw new Error(`Ingestion failed: ${errorReason}.`)
}
hostAppStore.setNotification({
type: ToastNotificationType.Danger,
title: 'Ingestion Error',
description: 'Ingestion status does not match expected types.'
})
throw new Error(
`Ingestion status does not match with the expected types as success or failure.`
)
} else {
const accountStore = useAccountStore()
const account = accountStore.getAccountClient(args.accountId)
const { mutate } = provideApolloClient(account)(() =>
useMutation(createVersionMutation)
)
const result = await mutate({
input: {
modelId: args.modelId,
objectId: args.referencedObjectId,
sourceApplication: args.sourceApplication || 'Archicad',
projectId: args.projectId
}
})
return result?.data?.versionMutations?.create?.id
}
}
}
+74 -22
View File
@@ -19,6 +19,9 @@ import type {
ReceiveViaBrowserArgs,
CreateVersionArgs
} from '~/lib/bridge/server'
import { useModelIngestion } from '../ingestion/composables/useModelIngestion'
import type { ISenderModelCard } from '../models/card/send'
import { useCheckGraphql } from '~/lib/core/composables/useCheckGraphql'
declare let sketchup: {
exec: (data: Record<string, unknown>) => void
@@ -297,40 +300,89 @@ export class SketchupBridge extends BaseBridge {
sourceApplication: 'sketchup',
message: message || 'send from sketchup'
}
const versionId = await this.createVersion(args)
const hostAppStore = useHostAppStore()
// TODO: Alignment needed
hostAppStore.setModelSendResult({
modelCardId: args.modelCardId,
versionId: versionId as string,
sendConversionResults
})
try {
const versionId = await this.createVersion(args)
hostAppStore.setModelSendResult({
modelCardId: args.modelCardId,
versionId: versionId as string,
sendConversionResults
})
} catch (err) {
hostAppStore.setHostAppError({
message: (err as Error).message || 'Unknown error occurred',
error: (err as Error).toString(),
stackTrace: (err as Error).stack || ''
})
}
}
public async createVersion(args: CreateVersionArgs) {
const accountStore = useAccountStore()
const hostAppStore = useHostAppStore()
const accountStore = useAccountStore()
const { accounts } = storeToRefs(accountStore)
const account = accounts.value.find((acc) => acc.accountInfo.id === args.accountId)
const { completeIngestionWithVersion } = useModelIngestion()
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
useMutation(createVersionMutation)
const modelCard = hostAppStore.models.find(
(model) => model.modelCardId === args.modelCardId
)
// sketchup versions are provided as 2 digit. i.e. 22, 23, 24
// we are safe with this string concatanation for 77 years
const hostAppName = `SketchUp 20${hostAppStore.hostAppVersion}`
if (!modelCard) {
throw new Error('Model card not found') // ctor
}
const result = await createVersion.mutate({
input: {
modelId: args.modelId,
objectId: args.referencedObjectId,
sourceApplication: hostAppName,
projectId: args.projectId
const { canCreateModelIngestion } = useCheckGraphql()
const canCreateIngestion = await canCreateModelIngestion(
modelCard.projectId,
modelCard.modelId,
modelCard.accountId
)
if (canCreateIngestion.queryAvailable) {
const ingestionId = hostAppStore.activeIngestions[args.modelCardId]
if (!ingestionId) {
throw new Error(`Ingestion failed: Ingestion ID not found to create version.`)
}
})
return result?.data?.versionMutations?.create?.id
const res = await completeIngestionWithVersion(
modelCard as ISenderModelCard,
ingestionId,
args.referencedObjectId
)
if (res?.statusData.__typename === 'ModelIngestionSuccessStatus') {
return res?.statusData.versionId
}
if (res?.statusData.__typename === 'ModelIngestionFailedStatus') {
throw new Error(
`Ingestion failed: ${res?.statusData.errorReason || 'Unknown error'}.`
)
}
throw new Error(
`Ingestion status does not match with the expected types as success or failure.`
)
} else {
// for the self hosters that does not have available graphql for ingestions
const createVersion = provideApolloClient((account as DUIAccount).client)(() =>
useMutation(createVersionMutation)
)
// sketchup versions are provided as 2 digit. i.e. 22, 23, 24
// we are safe with this string concatanation for 77 years
const hostAppName = `SketchUp 20${hostAppStore.hostAppVersion}`
const result = await createVersion.mutate({
input: {
modelId: args.modelId,
objectId: args.referencedObjectId,
sourceApplication: hostAppName,
projectId: args.projectId
}
})
return result?.data?.versionMutations?.create?.id
}
}
public async create(): Promise<boolean> {
+75 -9
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 logo\n role\n readOnly\n creationState {\n completed\n }\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 }\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,
@@ -33,6 +33,7 @@ type Documents = {
"\n query CanCreatePersonalProject {\n activeUser {\n permissions {\n canCreatePersonalProject {\n authorized\n code\n message\n payload\n }\n }\n }\n }\n": typeof types.CanCreatePersonalProjectDocument,
"\n query CanCreateProjectInWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n permissions {\n canCreateProject {\n authorized\n code\n message\n payload\n }\n }\n }\n }\n": typeof types.CanCreateProjectInWorkspaceDocument,
"\n query CanCreateModelInProject($projectId: String!) {\n project(id: $projectId) {\n permissions {\n canCreateModel {\n authorized\n code\n message\n }\n }\n }\n }\n": typeof types.CanCreateModelInProjectDocument,
"\n query CanCreateVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateVersion {\n authorized\n code\n message\n errorMessage\n }\n }\n }\n }\n }\n": typeof types.CanCreateVersionDocument,
"\n query ActiveWorkspace {\n activeUser {\n activeWorkspace {\n id\n name\n }\n }\n }\n": typeof types.ActiveWorkspaceDocument,
"\n fragment ProjectListProjectItem on Project {\n id\n name\n role\n updatedAt\n workspaceId\n workspace {\n id\n name\n slug\n role\n }\n models {\n totalCount\n }\n permissions {\n canLoad {\n authorized\n code\n message\n }\n canPublish {\n authorized\n code\n message\n }\n }\n }\n": typeof types.ProjectListProjectItemFragmentDoc,
"\n query ProjectListQuery($limit: Int!, $filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(limit: $limit, filter: $filter, cursor: $cursor) {\n totalCount\n cursor\n items {\n ...ProjectListProjectItem\n }\n }\n }\n }\n": typeof types.ProjectListQueryDocument,
@@ -44,7 +45,7 @@ type Documents = {
"\n query ProjectAddByUrlQueryWithVersion(\n $projectId: String!\n $modelId: String!\n $versionId: String!\n ) {\n project(id: $projectId) {\n ...ProjectListProjectItem\n model(id: $modelId) {\n ...ModelListModelItem\n version(id: $versionId) {\n ...VersionListItem\n }\n }\n }\n }\n": typeof types.ProjectAddByUrlQueryWithVersionDocument,
"\n query ProjectAddByUrlQueryWithoutVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n ...ProjectListProjectItem\n model(id: $modelId) {\n ...ModelListModelItem\n }\n }\n }\n": typeof types.ProjectAddByUrlQueryWithoutVersionDocument,
"\n query ProjectDetails($projectId: String!) {\n project(id: $projectId) {\n id\n role\n name\n workspace {\n name\n slug\n readOnly\n role\n }\n team {\n user {\n avatar\n id\n name\n }\n }\n visibility\n permissions {\n canLoad {\n authorized\n code\n message\n }\n canPublish {\n authorized\n code\n message\n }\n }\n }\n }\n": typeof types.ProjectDetailsDocument,
"\n query AutomateFunctions {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n": typeof types.AutomateFunctionsDocument,
"\n query AutomateFunctions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n }\n": typeof types.AutomateFunctionsDocument,
"\n query ModelDetails($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n id\n name\n model(id: $modelId) {\n id\n displayName\n name\n versions {\n totalCount\n items {\n id\n }\n }\n author {\n id\n name\n avatar\n }\n }\n }\n }\n": typeof types.ModelDetailsDocument,
"\n query VersionDetails($projectId: String!, $versionId: String!, $modelId: String!) {\n project(id: $projectId) {\n id\n name\n model(id: $modelId) {\n id\n name\n versions(limit: 1) {\n items {\n id\n createdAt\n sourceApplication\n authorUser {\n id\n }\n }\n }\n version(id: $versionId) {\n id\n referencedObject\n message\n sourceApplication\n createdAt\n previewUrl\n }\n }\n }\n }\n": typeof types.VersionDetailsDocument,
"\n query ServerInfo {\n serverInfo {\n workspaces {\n workspacesEnabled\n }\n }\n }\n": typeof types.ServerInfoDocument,
@@ -52,8 +53,18 @@ type Documents = {
"\n subscription ProjectTriggeredAutomationsStatusUpdated($projectId: String!) {\n projectTriggeredAutomationsStatusUpdated(projectId: $projectId) {\n type\n version {\n id\n }\n model {\n id\n }\n project {\n id\n }\n run {\n ...AutomationRunItem\n }\n }\n }\n": typeof types.ProjectTriggeredAutomationsStatusUpdatedDocument,
"\n subscription OnUserProjectsUpdated {\n userProjectsUpdated {\n id\n project {\n id\n visibility\n team {\n id\n role\n }\n }\n }\n }\n": typeof types.OnUserProjectsUpdatedDocument,
"\n subscription ProjectUpdated($projectId: String!) {\n projectUpdated(id: $projectId) {\n id\n project {\n visibility\n }\n }\n }\n": typeof types.ProjectUpdatedDocument,
"\n subscription Subscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n": typeof types.SubscriptionDocument,
"\n subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n": typeof types.ModelViewingSubscriptionDocument,
"\n subscription ProjectCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n comment {\n author {\n avatar\n id\n name\n }\n id\n hasParent\n parent {\n id\n }\n }\n type\n }\n }\n": typeof types.ProjectCommentsUpdatedDocument,
"\n mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {\n projectMutations {\n modelIngestionMutations {\n create(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.CreateModelIngestionDocument,
"\n mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {\n projectMutations {\n modelIngestionMutations {\n updateProgress(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.UpdateModelIngestionProgressDocument,
"\n mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {\n projectMutations {\n modelIngestionMutations {\n completeWithVersion(input: $input) {\n id\n statusData {\n __typename\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionFailedStatus {\n errorStacktrace\n errorReason\n status\n }\n ... on ModelIngestionCancelledStatus {\n cancellationMessage\n status\n }\n ... on ModelIngestionQueuedStatus {\n progressMessage\n status\n }\n }\n }\n }\n }\n }\n": typeof types.CompleteModelIngestionWithVersionDocument,
"\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.FailModelIngestionWithErrorDocument,
"\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n": typeof types.FailModelIngestionWithCancelDocument,
"\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n": typeof types.CanCreateIngestionDocument,
"\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n": typeof types.ProjectModelIngestionUpdatedDocument,
"\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 subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n": typeof types.WorkspacePlanUsageUpdatedDocument,
};
const documents: Documents = {
"\n mutation SetActiveWorkspaceMutation($slug: String) {\n activeUserMutations {\n setActiveWorkspace(slug: $slug) {\n id\n }\n }\n }\n": types.SetActiveWorkspaceMutationDocument,
@@ -64,7 +75,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 logo\n role\n readOnly\n creationState {\n completed\n }\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 }\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,
@@ -75,6 +86,7 @@ const documents: Documents = {
"\n query CanCreatePersonalProject {\n activeUser {\n permissions {\n canCreatePersonalProject {\n authorized\n code\n message\n payload\n }\n }\n }\n }\n": types.CanCreatePersonalProjectDocument,
"\n query CanCreateProjectInWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n permissions {\n canCreateProject {\n authorized\n code\n message\n payload\n }\n }\n }\n }\n": types.CanCreateProjectInWorkspaceDocument,
"\n query CanCreateModelInProject($projectId: String!) {\n project(id: $projectId) {\n permissions {\n canCreateModel {\n authorized\n code\n message\n }\n }\n }\n }\n": types.CanCreateModelInProjectDocument,
"\n query CanCreateVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateVersion {\n authorized\n code\n message\n errorMessage\n }\n }\n }\n }\n }\n": types.CanCreateVersionDocument,
"\n query ActiveWorkspace {\n activeUser {\n activeWorkspace {\n id\n name\n }\n }\n }\n": types.ActiveWorkspaceDocument,
"\n fragment ProjectListProjectItem on Project {\n id\n name\n role\n updatedAt\n workspaceId\n workspace {\n id\n name\n slug\n role\n }\n models {\n totalCount\n }\n permissions {\n canLoad {\n authorized\n code\n message\n }\n canPublish {\n authorized\n code\n message\n }\n }\n }\n": types.ProjectListProjectItemFragmentDoc,
"\n query ProjectListQuery($limit: Int!, $filter: UserProjectsFilter, $cursor: String) {\n activeUser {\n id\n projects(limit: $limit, filter: $filter, cursor: $cursor) {\n totalCount\n cursor\n items {\n ...ProjectListProjectItem\n }\n }\n }\n }\n": types.ProjectListQueryDocument,
@@ -86,7 +98,7 @@ const documents: Documents = {
"\n query ProjectAddByUrlQueryWithVersion(\n $projectId: String!\n $modelId: String!\n $versionId: String!\n ) {\n project(id: $projectId) {\n ...ProjectListProjectItem\n model(id: $modelId) {\n ...ModelListModelItem\n version(id: $versionId) {\n ...VersionListItem\n }\n }\n }\n }\n": types.ProjectAddByUrlQueryWithVersionDocument,
"\n query ProjectAddByUrlQueryWithoutVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n ...ProjectListProjectItem\n model(id: $modelId) {\n ...ModelListModelItem\n }\n }\n }\n": types.ProjectAddByUrlQueryWithoutVersionDocument,
"\n query ProjectDetails($projectId: String!) {\n project(id: $projectId) {\n id\n role\n name\n workspace {\n name\n slug\n readOnly\n role\n }\n team {\n user {\n avatar\n id\n name\n }\n }\n visibility\n permissions {\n canLoad {\n authorized\n code\n message\n }\n canPublish {\n authorized\n code\n message\n }\n }\n }\n }\n": types.ProjectDetailsDocument,
"\n query AutomateFunctions {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n": types.AutomateFunctionsDocument,
"\n query AutomateFunctions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n }\n": types.AutomateFunctionsDocument,
"\n query ModelDetails($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n id\n name\n model(id: $modelId) {\n id\n displayName\n name\n versions {\n totalCount\n items {\n id\n }\n }\n author {\n id\n name\n avatar\n }\n }\n }\n }\n": types.ModelDetailsDocument,
"\n query VersionDetails($projectId: String!, $versionId: String!, $modelId: String!) {\n project(id: $projectId) {\n id\n name\n model(id: $modelId) {\n id\n name\n versions(limit: 1) {\n items {\n id\n createdAt\n sourceApplication\n authorUser {\n id\n }\n }\n }\n version(id: $versionId) {\n id\n referencedObject\n message\n sourceApplication\n createdAt\n previewUrl\n }\n }\n }\n }\n": types.VersionDetailsDocument,
"\n query ServerInfo {\n serverInfo {\n workspaces {\n workspacesEnabled\n }\n }\n }\n": types.ServerInfoDocument,
@@ -94,8 +106,18 @@ const documents: Documents = {
"\n subscription ProjectTriggeredAutomationsStatusUpdated($projectId: String!) {\n projectTriggeredAutomationsStatusUpdated(projectId: $projectId) {\n type\n version {\n id\n }\n model {\n id\n }\n project {\n id\n }\n run {\n ...AutomationRunItem\n }\n }\n }\n": types.ProjectTriggeredAutomationsStatusUpdatedDocument,
"\n subscription OnUserProjectsUpdated {\n userProjectsUpdated {\n id\n project {\n id\n visibility\n team {\n id\n role\n }\n }\n }\n }\n": types.OnUserProjectsUpdatedDocument,
"\n subscription ProjectUpdated($projectId: String!) {\n projectUpdated(id: $projectId) {\n id\n project {\n visibility\n }\n }\n }\n": types.ProjectUpdatedDocument,
"\n subscription Subscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n": types.SubscriptionDocument,
"\n subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n": types.ModelViewingSubscriptionDocument,
"\n subscription ProjectCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n comment {\n author {\n avatar\n id\n name\n }\n id\n hasParent\n parent {\n id\n }\n }\n type\n }\n }\n": types.ProjectCommentsUpdatedDocument,
"\n mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {\n projectMutations {\n modelIngestionMutations {\n create(input: $input) {\n id\n }\n }\n }\n }\n": types.CreateModelIngestionDocument,
"\n mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {\n projectMutations {\n modelIngestionMutations {\n updateProgress(input: $input) {\n id\n }\n }\n }\n }\n": types.UpdateModelIngestionProgressDocument,
"\n mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {\n projectMutations {\n modelIngestionMutations {\n completeWithVersion(input: $input) {\n id\n statusData {\n __typename\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionFailedStatus {\n errorStacktrace\n errorReason\n status\n }\n ... on ModelIngestionCancelledStatus {\n cancellationMessage\n status\n }\n ... on ModelIngestionQueuedStatus {\n progressMessage\n status\n }\n }\n }\n }\n }\n }\n": types.CompleteModelIngestionWithVersionDocument,
"\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n": types.FailModelIngestionWithErrorDocument,
"\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n": types.FailModelIngestionWithCancelDocument,
"\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n": types.CanCreateIngestionDocument,
"\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n": types.ProjectModelIngestionUpdatedDocument,
"\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 subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n": types.WorkspacePlanUsageUpdatedDocument,
};
/**
@@ -147,7 +169,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 logo\n role\n readOnly\n creationState {\n completed\n }\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 logo\n role\n readOnly\n creationState {\n completed\n }\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 }\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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -188,6 +210,10 @@ export function graphql(source: "\n query CanCreateProjectInWorkspace($workspac
* 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 CanCreateModelInProject($projectId: String!) {\n project(id: $projectId) {\n permissions {\n canCreateModel {\n authorized\n code\n message\n }\n }\n }\n }\n"): (typeof documents)["\n query CanCreateModelInProject($projectId: String!) {\n project(id: $projectId) {\n permissions {\n canCreateModel {\n authorized\n code\n message\n }\n }\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 CanCreateVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateVersion {\n authorized\n code\n message\n errorMessage\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query CanCreateVersion($projectId: String!, $modelId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateVersion {\n authorized\n code\n message\n errorMessage\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -235,7 +261,7 @@ export function graphql(source: "\n query ProjectDetails($projectId: String!) {
/**
* 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 AutomateFunctions {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n"): (typeof documents)["\n query AutomateFunctions {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n"];
export function graphql(source: "\n query AutomateFunctions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n }\n"): (typeof documents)["\n query AutomateFunctions($workspaceId: String!) {\n workspace(id: $workspaceId) {\n automateFunctions {\n items {\n ...AutomateFunctionItem\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -267,11 +293,51 @@ export function graphql(source: "\n subscription ProjectUpdated($projectId: Str
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n subscription Subscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n"): (typeof documents)["\n subscription Subscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n"];
export function graphql(source: "\n subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\n }\n }\n"): (typeof documents)["\n subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {\n viewerUserActivityBroadcasted(target: $target) {\n userName\n userId\n sessionId\n user {\n name\n id\n avatar\n }\n status\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 subscription ProjectCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n comment {\n author {\n avatar\n id\n name\n }\n id\n hasParent\n parent {\n id\n }\n }\n type\n }\n }\n"): (typeof documents)["\n subscription ProjectCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n comment {\n author {\n avatar\n id\n name\n }\n id\n hasParent\n parent {\n id\n }\n }\n type\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 mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {\n projectMutations {\n modelIngestionMutations {\n create(input: $input) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {\n projectMutations {\n modelIngestionMutations {\n create(input: $input) {\n id\n }\n }\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 mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {\n projectMutations {\n modelIngestionMutations {\n updateProgress(input: $input) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {\n projectMutations {\n modelIngestionMutations {\n updateProgress(input: $input) {\n id\n }\n }\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 mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {\n projectMutations {\n modelIngestionMutations {\n completeWithVersion(input: $input) {\n id\n statusData {\n __typename\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionFailedStatus {\n errorStacktrace\n errorReason\n status\n }\n ... on ModelIngestionCancelledStatus {\n cancellationMessage\n status\n }\n ... on ModelIngestionQueuedStatus {\n progressMessage\n status\n }\n }\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {\n projectMutations {\n modelIngestionMutations {\n completeWithVersion(input: $input) {\n id\n statusData {\n __typename\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionFailedStatus {\n errorStacktrace\n errorReason\n status\n }\n ... on ModelIngestionCancelledStatus {\n cancellationMessage\n status\n }\n ... on ModelIngestionQueuedStatus {\n progressMessage\n status\n }\n }\n }\n }\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 mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithError(input: $input) {\n id\n }\n }\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 mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {\n projectMutations {\n modelIngestionMutations {\n failWithCancel(input: $input) {\n id\n }\n }\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 CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query CanCreateIngestion($modelId: String!, $projectId: String!) {\n project(id: $projectId) {\n model(id: $modelId) {\n permissions {\n canCreateIngestion {\n authorized\n code\n message\n }\n }\n }\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 subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\n }\n }\n"): (typeof documents)["\n subscription ProjectModelIngestionUpdated(\n $input: ProjectModelIngestionSubscriptionInput!\n ) {\n projectModelIngestionUpdated(input: $input) {\n type\n modelIngestion {\n id\n statusData {\n __typename\n ... on ModelIngestionSuccessStatus {\n status\n versionId\n }\n ... on ModelIngestionProcessingStatus {\n status\n progressMessage\n progress\n }\n ... on ModelIngestionFailedStatus {\n status\n errorReason\n }\n ... on ModelIngestionCancelledStatus {\n status\n cancellationMessage\n }\n ... on ModelIngestionQueuedStatus {\n status\n progressMessage\n }\n }\n }\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 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 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"];
/**
* 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 IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\n }\n }\n"): (typeof documents)["\n query IssuesList($projectId: String!) {\n project(id: $projectId) {\n id\n issues {\n totalCount\n items {\n ...IssuesItem\n }\n }\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 subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n"): (typeof documents)["\n subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {\n workspacePlanUsageUpdated(input: $input)\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
File diff suppressed because one or more lines are too long
+12 -6
View File
@@ -6,16 +6,17 @@ interface CustomProperties {
[key: string]: object | string | boolean | number | undefined | null
}
// Cached email and server
// Cached email, server, and userId
const lastEmail: Ref<string | undefined> = ref(undefined)
const lastServer: Ref<string | undefined> = ref(undefined)
const lastUserId: Ref<string | undefined> = ref(undefined)
/**
* Get Mixpanel functions
* In DUI3, quite likely to change distinct id of the track operation since we can trigger repetitive calls that belongs to different account.
* Also we have some operations that explicitly not belong to any account, i.e. first "Send" or "Load" click,
* with this case we use default account on manager to get "email" and "server" and cache them for later anonymous track.
* In each call we update "lastEmail" and "lastServer" for the following potential anonymous tracks.
* with this case we use default account on manager to get "email", "server", and "userId" and cache them for later anonymous track.
* In each call we update "lastEmail", "lastServer", and "lastUserId" for the following potential anonymous tracks.
*/
export function useMixpanel() {
const hostApp = useHostAppStore()
@@ -42,11 +43,13 @@ export function useMixpanel() {
const account = accounts.find((a) => a.accountInfo.id === accountId)
lastEmail.value = account?.accountInfo.userInfo.email
lastServer.value = account?.accountInfo.serverInfo.url
lastUserId.value = account?.accountInfo.userInfo.id
} else {
// do not set if they cached already
if (lastEmail.value === undefined || lastServer.value === undefined) {
lastEmail.value = activeAccount.accountInfo.userInfo.email
lastServer.value = activeAccount.accountInfo.serverInfo.url
lastUserId.value = activeAccount.accountInfo.userInfo.id
}
}
@@ -62,9 +65,9 @@ export function useMixpanel() {
}
const hashedEmail =
'@' + md5(lastEmail.value.toLowerCase() as string).toUpperCase()
const hashedServer = md5(
new URL(lastServer.value).hostname.toLowerCase() as string
).toUpperCase()
const serverUrl = new URL(lastServer.value)
const serverHostname = serverUrl.hostname.toLowerCase()
const hashedServer = md5(serverHostname).toUpperCase()
// Get os info from userAgent text
// taken from original mixpanel implementation
@@ -84,6 +87,8 @@ export function useMixpanel() {
distinct_id: hashedEmail,
// eslint-disable-next-line camelcase
server_id: hashedServer,
// eslint-disable-next-line camelcase
server_domain: serverHostname,
token: mixpanelTokenId as string,
type: isAction ? 'action' : undefined,
hostApp: hostApp.hostAppName,
@@ -92,6 +97,7 @@ export function useMixpanel() {
// eslint-disable-next-line camelcase
core_version: hostApp.connectorVersion,
email: lastEmail.value,
userId: lastUserId.value,
...customProperties
}
+71
View File
@@ -0,0 +1,71 @@
import { canCreateVersionQuery } from '~/lib/graphql/mutationsAndQueries'
import { canCreateModelIngestionQuery } from '~/lib/ingestion/graphql/queries'
import { useAccountStore } from '~/store/accounts'
// use this composable whenever we need to check against available graphqls over servers
export function useCheckGraphql() {
/**
* Checks the ingestions available for the server,
* if available, returns with respond by appending `queryAvailable = true`
* otherwise, returns fake result object with `queryAvailable = false`
*/
const canCreateModelIngestion = async (
projectId: string,
modelId: string,
accountId: string
) => {
const accountsStore = useAccountStore()
const client = accountsStore.getAccountClient(accountId)
try {
const result = await client.query({
query: canCreateModelIngestionQuery,
variables: {
projectId,
modelId
},
fetchPolicy: 'network-only'
})
return {
...result.data.project.model.permissions.canCreateIngestion,
queryAvailable: true
}
} catch {
return { queryAvailable: false, authorized: false, message: undefined }
}
}
/**
* Checks if user can create a version for the given model.
* Used to validate before starting a publish operation.
*/
const canCreateVersion = async (
projectId: string,
modelId: string,
accountId: string
) => {
const accountsStore = useAccountStore()
const client = accountsStore.getAccountClient(accountId)
try {
const result = await client.query({
query: canCreateVersionQuery,
variables: {
projectId,
modelId
},
fetchPolicy: 'network-only'
})
return result.data.project.model.permissions.canCreateVersion
} catch (error) {
// If we can't check, allow the attempt - server will reject if not allowed
console.error('Failed to check canCreateVersion:', error)
return { authorized: true, message: null }
}
}
return {
canCreateVersion,
canCreateModelIngestion
}
}
+25 -9
View File
@@ -86,12 +86,9 @@ export const workspaceListFragment = graphql(`
description
createdAt
updatedAt
logo
logoUrl
role
readOnly
creationState {
completed
}
permissions {
canCreateProject {
authorized
@@ -249,6 +246,23 @@ export const canCreateModelInProjectQuery = graphql(`
}
`)
export const canCreateVersionQuery = graphql(`
query CanCreateVersion($projectId: String!, $modelId: String!) {
project(id: $projectId) {
model(id: $modelId) {
permissions {
canCreateVersion {
authorized
code
message
errorMessage
}
}
}
}
}
`)
export const activeWorkspaceQuery = graphql(`
query ActiveWorkspace {
activeUser {
@@ -459,10 +473,12 @@ export const projectDetailsQuery = graphql(`
`)
export const automateFunctionsQuery = graphql(`
query AutomateFunctions {
automateFunctions {
items {
...AutomateFunctionItem
query AutomateFunctions($workspaceId: String!) {
workspace(id: $workspaceId) {
automateFunctions {
items {
...AutomateFunctionItem
}
}
}
}
@@ -607,7 +623,7 @@ export const projectUpdatedSubscription = graphql(`
`)
export const modelViewingSubscription = graphql(`
subscription Subscription($target: ViewerUpdateTrackingTarget!) {
subscription ModelViewingSubscription($target: ViewerUpdateTrackingTarget!) {
viewerUserActivityBroadcasted(target: $target) {
userName
userId
@@ -0,0 +1,319 @@
import {
provideApolloClient,
useMutation,
useSubscription
} from '@vue/apollo-composable'
import { useAccountStore } from '~/store/accounts'
import { useHostAppStore } from '~/store/hostApp'
import {
completeModelIngestionWithVersion,
createModelIngestion,
updateModelIngestionProgress,
failModelIngestionWithError,
failModelIngestionWithCancel
} from '../graphql/mutations'
import { projectModelIngestionUpdatedSubscription } from '../graphql/subscriptions'
import type {
SourceDataInput,
ProjectModelIngestionUpdatedSubscription
} from '~~/lib/common/generated/gql/graphql'
import type { ISenderModelCard } from '~/lib/models/card/send'
import { storeToRefs } from 'pinia'
import { ToastNotificationType } from '@speckle/ui-components'
/**
* New way of creating versions.
* It is essential for server to track limits on versions.
* The flow is as follows:
* 0. Check if the user has enough limits to create a new version (this is handled outside of this composable)
* 1. Start a new ingestion
* 2. Update the ingestion with the new data when connector throws progress via 'setModelProgress' event
* 3. Complete the version with the root object id that passed by connector or server/sketchup bridges in JS
*/
export const useModelIngestion = () => {
const store = useHostAppStore()
const accountStore = useAccountStore()
const startIngestion = async (
senderModelCard: ISenderModelCard,
progressMessage: string,
sourceData: SourceDataInput
) => {
const { activeIngestions } = storeToRefs(store)
const client = accountStore.getAccountClient(senderModelCard.accountId)
const { mutate } = provideApolloClient(client)(() =>
useMutation(createModelIngestion)
)
const res = await mutate({
input: {
projectId: senderModelCard.projectId,
modelId: senderModelCard.modelId,
progressMessage,
sourceData,
maxIdleTimeoutSeconds: 7200 // 2h
}
})
if (res?.errors?.length) {
const msg = res.errors[0].message
store.setNotification({
type: ToastNotificationType.Danger,
title: 'Ingestion Error',
description: msg
})
throw new Error(msg)
}
const ingestionId = res?.data?.projectMutations.modelIngestionMutations.create.id
if (ingestionId) {
activeIngestions.value[senderModelCard.modelCardId] = ingestionId
}
return res?.data?.projectMutations.modelIngestionMutations.create
}
const updateIngestion = async (
senderModelCard: ISenderModelCard,
ingestionId: string,
progressMessage: string,
progress?: number
) => {
const client = accountStore.getAccountClient(senderModelCard.accountId)
const { mutate } = provideApolloClient(client)(() =>
useMutation(updateModelIngestionProgress)
)
const res = await mutate({
input: {
projectId: senderModelCard.projectId,
ingestionId,
progressMessage,
progress
}
})
if (res?.errors?.length) {
const msg = res.errors[0].message
store.setNotification({
type: ToastNotificationType.Danger,
title: 'Ingestion Error',
description: msg
})
throw new Error(msg)
}
return res?.data?.projectMutations.modelIngestionMutations.updateProgress
}
const failIngestion = async (
senderModelCard: ISenderModelCard,
ingestionId: string,
errorReason: string,
errorStacktrace?: string
) => {
const client = accountStore.getAccountClient(senderModelCard.accountId)
const { mutate } = provideApolloClient(client)(() =>
useMutation(failModelIngestionWithError)
)
const res = await mutate({
input: {
projectId: senderModelCard.projectId,
ingestionId,
errorReason,
errorStacktrace
}
})
if (res?.errors?.length) {
const msg = res.errors[0].message
store.setNotification({
type: ToastNotificationType.Danger,
title: 'Ingestion Error',
description: msg
})
throw new Error(msg)
}
const { activeIngestions } = storeToRefs(store)
// clean the failed ingestion
activeIngestions.value = Object.fromEntries(
Object.entries(activeIngestions.value).filter(
([key]) => key !== senderModelCard.modelCardId
)
)
}
const cancelIngestion = async (
senderModelCard: ISenderModelCard,
ingestionId: string,
cancellationMessage: string = 'Cancelled by user'
) => {
const client = accountStore.getAccountClient(senderModelCard.accountId)
const { mutate } = provideApolloClient(client)(() =>
useMutation(failModelIngestionWithCancel)
)
const res = await mutate({
input: {
projectId: senderModelCard.projectId,
ingestionId,
cancellationMessage
}
})
if (res?.errors?.length) {
const msg = res.errors[0].message
store.setNotification({
type: ToastNotificationType.Danger,
title: 'Ingestion Error',
description: msg
})
throw new Error(msg)
}
const { activeIngestions } = storeToRefs(store)
// clean the cancelled ingestion
activeIngestions.value = Object.fromEntries(
Object.entries(activeIngestions.value).filter(
([key]) => key !== senderModelCard.modelCardId
)
)
}
const completeIngestionWithVersion = async (
senderModelCard: ISenderModelCard,
ingestionId: string,
rootObjectId: string
) => {
const client = accountStore.getAccountClient(senderModelCard.accountId)
const { mutate } = provideApolloClient(client)(() =>
useMutation(completeModelIngestionWithVersion)
)
const res = await mutate({
input: {
projectId: senderModelCard.projectId,
ingestionId,
rootObjectId
}
})
if (res?.errors?.length) {
const msg = res.errors[0].message
store.setNotification({
type: ToastNotificationType.Danger,
title: 'Ingestion Error',
description: msg
})
throw new Error(msg)
}
const { activeIngestions } = storeToRefs(store)
// clean the completed ingestion
activeIngestions.value = Object.fromEntries(
Object.entries(activeIngestions.value).filter(
([key]) => key !== senderModelCard.modelCardId
)
)
return res?.data?.projectMutations.modelIngestionMutations.completeWithVersion
}
// Tracks active ingestion subscriptions so they can be stopped on cancel or terminal state
const activeSubscriptions: Record<string, () => void> = {}
/**
* Subscribes to ingestion status updates for a given ingestionId.
* Used when the connector (.NET SDK) handles the ingestion and passes the ingestionId
* back to the DUI via setModelSendResult. The DUI then subscribes to track
* the server-side processing state until a terminal status is reached.
*
* Manages model card state directly: updates progress, sets versionId on success,
* sets error on failure, and clears progress on terminal states.
*/
const subscribeToIngestion = (
senderModelCard: ISenderModelCard,
ingestionId: string
) => {
const client = accountStore.getAccountClient(senderModelCard.accountId)
senderModelCard.progress = { status: 'Remote processing...' }
const { onResult, onError, stop } = provideApolloClient(client)(() =>
useSubscription(projectModelIngestionUpdatedSubscription, () => ({
input: {
projectId: senderModelCard.projectId,
ingestionReference: { ingestionId }
}
}))
)
activeSubscriptions[senderModelCard.modelCardId] = stop
onResult((result) => {
const data = result.data as ProjectModelIngestionUpdatedSubscription | undefined
const statusData = data?.projectModelIngestionUpdated?.modelIngestion?.statusData
if (!statusData) return
switch (statusData.__typename) {
case 'ModelIngestionSuccessStatus':
senderModelCard.latestCreatedVersionId = statusData.versionId
senderModelCard.progress = undefined
unsubscribeFromIngestion(senderModelCard.modelCardId)
break
case 'ModelIngestionProcessingStatus':
senderModelCard.progress = {
status: statusData.progressMessage,
progress: statusData.progress ?? undefined
}
break
case 'ModelIngestionFailedStatus':
senderModelCard.error = {
errorMessage: statusData.errorReason,
dismissible: true
}
senderModelCard.progress = undefined
unsubscribeFromIngestion(senderModelCard.modelCardId)
break
case 'ModelIngestionCancelledStatus':
senderModelCard.progress = undefined
unsubscribeFromIngestion(senderModelCard.modelCardId)
break
case 'ModelIngestionQueuedStatus':
senderModelCard.progress = {
status: statusData.progressMessage
}
break
}
})
onError((err) => {
console.error('Ingestion subscription error:', err)
unsubscribeFromIngestion(senderModelCard.modelCardId)
})
}
const unsubscribeFromIngestion = (modelCardId: string) => {
const stop = activeSubscriptions[modelCardId]
if (stop) {
stop()
delete activeSubscriptions[modelCardId]
}
}
return {
startIngestion,
updateIngestion,
failIngestion,
cancelIngestion,
completeIngestionWithVersion,
subscribeToIngestion,
unsubscribeFromIngestion
}
}
+86
View File
@@ -0,0 +1,86 @@
import { graphql } from '~~/lib/common/generated/gql'
export const createModelIngestion = graphql(`
mutation CreateModelIngestion($input: ModelIngestionCreateInput!) {
projectMutations {
modelIngestionMutations {
create(input: $input) {
id
}
}
}
}
`)
export const updateModelIngestionProgress = graphql(`
mutation UpdateModelIngestionProgress($input: ModelIngestionUpdateInput!) {
projectMutations {
modelIngestionMutations {
updateProgress(input: $input) {
id
}
}
}
}
`)
export const completeModelIngestionWithVersion = graphql(`
mutation CompleteModelIngestionWithVersion($input: ModelIngestionSuccessInput!) {
projectMutations {
modelIngestionMutations {
completeWithVersion(input: $input) {
id
statusData {
__typename
... on ModelIngestionProcessingStatus {
status
progressMessage
progress
}
... on ModelIngestionSuccessStatus {
status
versionId
}
... on ModelIngestionFailedStatus {
errorStacktrace
errorReason
status
}
... on ModelIngestionCancelledStatus {
cancellationMessage
status
}
... on ModelIngestionQueuedStatus {
progressMessage
status
}
}
}
}
}
}
`)
export const failModelIngestionWithError = graphql(`
mutation FailModelIngestionWithError($input: ModelIngestionFailedInput!) {
projectMutations {
modelIngestionMutations {
failWithError(input: $input) {
id
}
}
}
}
`)
export const failModelIngestionWithCancel = graphql(`
mutation FailModelIngestionWithCancel($input: ModelIngestionCancelledInput!) {
projectMutations {
modelIngestionMutations {
failWithCancel(input: $input) {
id
}
}
}
}
`)
+17
View File
@@ -0,0 +1,17 @@
import { graphql } from '~~/lib/common/generated/gql'
export const canCreateModelIngestionQuery = graphql(`
query CanCreateIngestion($modelId: String!, $projectId: String!) {
project(id: $projectId) {
model(id: $modelId) {
permissions {
canCreateIngestion {
authorized
code
message
}
}
}
}
}
`)
+38
View File
@@ -0,0 +1,38 @@
import { graphql } from '~~/lib/common/generated/gql'
export const projectModelIngestionUpdatedSubscription = graphql(`
subscription ProjectModelIngestionUpdated(
$input: ProjectModelIngestionSubscriptionInput!
) {
projectModelIngestionUpdated(input: $input) {
type
modelIngestion {
id
statusData {
__typename
... on ModelIngestionSuccessStatus {
status
versionId
}
... on ModelIngestionProcessingStatus {
status
progressMessage
progress
}
... on ModelIngestionFailedStatus {
status
errorReason
}
... on ModelIngestionCancelledStatus {
status
cancellationMessage
}
... on ModelIngestionQueuedStatus {
status
progressMessage
}
}
}
}
}
`)
+71
View File
@@ -0,0 +1,71 @@
import { graphql } from '~~/lib/common/generated/gql'
export const issueFragment = graphql(`
fragment IssuesItem on Issue {
id
status
title
priority
viewerState
identifier
resourceIdString
activities(input: { limit: 1, sortDirection: asc }) {
totalCount
items {
actor {
id
user {
name
id
avatar
}
}
eventType
createdAt
}
}
replies {
totalCount
items {
id
author {
id
user {
name
id
avatar
}
}
createdAt
description {
doc
}
}
}
description {
doc
}
labels {
hexColor
id
name
}
author {
id
user {
id
name
avatar
}
}
dueDate
assignee {
id
user {
id
avatar
name
}
}
}
`)
+15
View File
@@ -0,0 +1,15 @@
import { graphql } from '~~/lib/common/generated/gql'
export const issuesListQuery = graphql(`
query IssuesList($projectId: String!) {
project(id: $projectId) {
id
issues {
totalCount
items {
...IssuesItem
}
}
}
}
`)
+7
View File
@@ -0,0 +1,7 @@
export type Label = {
id: string
name: string
hexColor?: string
}
export type LabelsValue = Label[]
+1
View File
@@ -15,6 +15,7 @@ export type ModelCardNotification = {
name: string
tooltipText?: string
action: () => void
disabled?: boolean
}
/**
* If set, will display a view report button next to cta
+57
View File
@@ -1,6 +1,24 @@
import { computed } from 'vue'
import type {
ISendFilter,
SendFilterSelect,
RevitCategoriesSendFilter,
RevitViewsSendFilter
} from '~/lib/models/card/send'
import { ValidationHelpers } from '@speckle/ui-components'
import type { GenericValidateFunction } from 'vee-validate'
export const isSelectFilter = (f: ISendFilter): f is SendFilterSelect =>
f.type === 'Select' || 'selectedItems' in f
export const isRevitCategoriesFilter = (
f: ISendFilter
): f is RevitCategoriesSendFilter =>
f.id === 'revitCategories' || f.id === 'archicadLayers'
export const isRevitViewsFilter = (f: ISendFilter): f is RevitViewsSendFilter =>
f.id === 'revitViews'
export const isEmail = ValidationHelpers.isEmail
export const isOneOrMultipleEmails = ValidationHelpers.isOneOrMultipleEmails
@@ -42,3 +60,42 @@ export function useModelNameValidationRules() {
isValidModelName
])
}
export type FilterValidationResult = { valid: boolean; reason?: string }
export function validateFilter(
filter: ISendFilter | undefined,
context: { selectionCount: number }
): FilterValidationResult {
if (!filter) return { valid: false, reason: 'No filter selected' }
// Selection Filter check
if (filter.name === 'Selection' || filter.id === 'selection') {
return context.selectionCount > 0
? { valid: true }
: { valid: false, reason: 'No objects selected to publish' }
}
// List-based filters (Rhino Layers, etc.)
if (isSelectFilter(filter)) {
return (filter.selectedItems?.length ?? 0) > 0
? { valid: true }
: { valid: false, reason: 'No items selected to publish' }
}
// Category-based filters
if (isRevitCategoriesFilter(filter)) {
return (filter.selectedCategories?.length ?? 0) > 0
? { valid: true }
: { valid: false, reason: 'No categories selected to publish' }
}
// View-based filters
if (isRevitViewsFilter(filter)) {
return filter.selectedView?.trim()
? { valid: true }
: { valid: false, reason: 'No view selected to publish' }
}
return { valid: true }
}
+7
View File
@@ -0,0 +1,7 @@
import { graphql } from '~~/lib/common/generated/gql'
export const workspacePlanUsageUpdatedSubscription = graphql(`
subscription WorkspacePlanUsageUpdated($input: WorkspacePlanUsageSubscriptionInput!) {
workspacePlanUsageUpdated(input: $input)
}
`)
+17
View File
@@ -38,6 +38,22 @@
"@speckle/tailwind-theme": "2.25.0",
"@speckle/ui-components": "^2.25.0",
"@speckle/ui-components-nuxt": "^2.25.0",
"@tiptap/core": "2.10.3",
"@tiptap/extension-bold": "2.10.3",
"@tiptap/extension-document": "2.10.3",
"@tiptap/extension-hard-break": "2.10.3",
"@tiptap/extension-history": "2.10.3",
"@tiptap/extension-italic": "2.10.3",
"@tiptap/extension-link": "2.10.3",
"@tiptap/extension-mention": "2.10.3",
"@tiptap/extension-paragraph": "2.10.3",
"@tiptap/extension-placeholder": "2.10.3",
"@tiptap/extension-strike": "2.10.3",
"@tiptap/extension-text": "2.10.3",
"@tiptap/extension-underline": "2.10.3",
"@tiptap/pm": "2.10.3",
"@tiptap/suggestion": "2.10.3",
"@tiptap/vue-3": "2.10.3",
"@vue/apollo-composable": "^4.0.0-beta.5",
"@vueuse/core": "^9.13.0",
"apollo-upload-client": "^17.0.0",
@@ -47,6 +63,7 @@
"graphql": "^16.6.0",
"graphql-tag": "^2.12.6",
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.537.0",
"nanoevents": "^8.0.0",
"pinia": "^2.1.4",
"portal-vue": "^3.0.0",
+101
View File
@@ -0,0 +1,101 @@
<template>
<div class="flex items-center justify-center"><InfiniteLoading /></div>
</template>
<script setup lang="ts">
import { md5 } from '@speckle/shared'
import { ToastNotificationType } from '@speckle/ui-components'
import { useRoute, useRouter } from 'vue-router'
import { useAuthManager } from '~/lib/authn/useAuthManager'
import type { Account } from '~/lib/bindings/definitions/IAccountBinding'
import { useHostAppStore } from '~/store/hostApp'
const route = useRoute()
const router = useRouter()
const { getChallenge, getChallengeUrl } = useAuthManager()
const { $accountBinding } = useNuxtApp()
const hostApp = useHostAppStore()
onMounted(async () => {
try {
const origin = getChallengeUrl()
const accessCode = route.query.access_code as string | undefined
if (accessCode && origin) {
const challenge = getChallenge()
const body = {
appId: 'sdui',
appSecret: 'sdui',
accessCode,
challenge
}
// Exchange the access code for a real token (optional)
const response = await fetch(new URL('/auth/token', origin), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
if (!response.ok) {
const errorText = await response.text()
hostApp.setNotification({
title: 'Log In',
type: ToastNotificationType.Danger,
description: `Token exchange failed with status ${response.status}: ${errorText}`
})
// Stop processing and redirect immediately on failure
return router.replace('/')
}
const { token, refreshToken } = (await response.json()) as {
token: string
refreshToken: string
}
const graphqlQuery = {
query:
'query { activeUser { id name email company avatar } serverInfo { name company adminContact description version } }'
}
const userAndServerInfoResponse = await fetch(new URL('/graphql', origin), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` // Add the token as a Bearer token
},
body: JSON.stringify(graphqlQuery)
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const userAndServerInfo = await userAndServerInfoResponse.json()
const accountId = md5(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
userAndServerInfo.data.activeUser.email + origin
).toUpperCase()
const account: Account = {
id: accountId,
token,
refreshToken,
isDefault: true,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
serverInfo: { url: origin, ...userAndServerInfo.data.serverInfo },
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
userInfo: userAndServerInfo.data.activeUser
}
await $accountBinding.addAccount(accountId, account)
} else {
throw new Error('No access code is found.')
}
} catch (error) {
hostApp.setNotification({
type: ToastNotificationType.Danger,
title: 'Failed to add your Speckle account.',
description: error as string
})
} finally {
router.replace('/')
}
})
</script>
+10
View File
@@ -7,6 +7,7 @@ import Intercom, {
trackEvent
} from '@intercom/messenger-js-sdk'
import { useAccountStore } from '~/store/accounts'
import { useHostAppStore } from '~/store/hostApp'
import { storeToRefs } from 'pinia'
const disabledRoutes: string[] = []
@@ -15,7 +16,9 @@ export const useIntercom = () => {
const route = useRoute()
const accountStore = useAccountStore()
const hostAppStore = useHostAppStore()
const { activeAccount } = storeToRefs(accountStore)
const { isDistributedBySpeckle } = storeToRefs(hostAppStore)
const isInitialized = ref(false)
@@ -80,6 +83,13 @@ export const useIntercom = () => {
}
})
// 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()
}
})
watch(activeAccount, (newValue) => {
if (newValue) {
if (!isInitialized.value) {
+19 -8
View File
@@ -165,14 +165,13 @@ export const useAccountStore = defineStore('accountStore', () => {
// hostAppStore.setNotification(notification)
}
// if (res.networkError) {
// const notification: ToastNotification = {
// type: ToastNotificationType.Danger,
// title: 'Network Error',
// description: res.networkError.message
// }
// hostAppStore.setNotification(notification)
// }
if (res.networkError && !navigator.onLine) {
hostAppStore.setNotification({
type: ToastNotificationType.Danger,
title: 'No Internet Connection',
description: 'Please check your network connection and try again.'
})
}
})
const link = splitLink(
@@ -286,12 +285,23 @@ export const useAccountStore = defineStore('accountStore', () => {
}
const accountByServerUrl = (serverUrl: string) => {
if (activeAccount.value.accountInfo.serverInfo.url === serverUrl) {
return activeAccount.value
}
const accountMatchWithServerUrl = accounts.value.find(
(acc) => acc.accountInfo.serverInfo.url === serverUrl
)
if (accountMatchWithServerUrl) return accountMatchWithServerUrl
}
const getAccountClient = (accountId: string) => {
return (
accounts.value.find(
(account) => account.accountInfo.id === accountId
) as DUIAccount
).client
}
const provideClients = () => {
provideApolloClients(apolloClients)
}
@@ -318,6 +328,7 @@ export const useAccountStore = defineStore('accountStore', () => {
return {
isLoading,
accounts,
getAccountClient,
defaultAccount,
activeAccount,
userSelectedAccount,
+186 -25
View File
@@ -13,7 +13,10 @@ import type {
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'
@@ -28,6 +31,8 @@ import {
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
@@ -43,7 +48,15 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
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)
@@ -65,6 +78,9 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
// 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
}
@@ -95,6 +111,11 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
isDistributedBySpeckle.value = val
}
const shouldHandleIngestion = computed(() => {
const hostAppsThatUsesDUIForGraphql = ['sketchup', 'archicad', 'Vectorworks']
return hostAppsThatUsesDUIForGraphql.includes(hostAppName.value as string)
})
/**
* Model Card Operations
*/
@@ -281,20 +302,59 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
const account = accountStore.accounts.find(
(acc) => acc.accountInfo.id === args.accountId
)
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
// 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}`)
}
})
} catch (err) {
console.error(`triggerCreateVersion is 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}`)
}
}
})
@@ -310,6 +370,14 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
*/
app.$sendBinding?.on('refreshSendFilters', () => void refreshSendFilters())
const validateSendFilter = (filter?: ISendFilter) => {
const selectionStore = useSelectionStore()
return validateFilter(filter, {
selectionCount: selectionStore.selectionInfo.selectedObjectIds?.length ?? 0
})
}
/**
* Send functionality
*/
@@ -318,10 +386,57 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
* Tells the host app to start sending a specific model card. This will reach inside the host application.
* @param modelId
*/
const sendModel = (modelCardId: string, actionSource: string) => {
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(
@@ -346,11 +461,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
model.accountId
)
}
model.latestCreatedVersionId = undefined
model.error = undefined
model.progress = { status: 'Starting to send...' }
model.expired = false
model.report = undefined
// 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.
@@ -377,6 +488,17 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
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) => {
@@ -393,13 +515,22 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
modelCardId: string
versionId: string
sendConversionResults: ConversionResult[]
ingestionId?: string
}) => {
const model = documentModelStore.value.models.find(
(m) => m.modelCardId === args.modelCardId
) as ISenderModelCard
model.latestCreatedVersionId = args.versionId
// Conversion results are always valid regardless of ingestion state
model.report = args.sendConversionResults
model.progress = undefined
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)
@@ -479,7 +610,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
app.$receiveBinding?.on('setModelReceiveResult', setModelReceiveResult)
// GENERIC STUFF
const handleModelProgressEvents = (args: {
const handleModelProgressEvents = async (args: {
modelCardId: string
progress?: ModelCardProgress
}) => {
@@ -487,9 +618,24 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
(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 = (args: {
const setModelError = async (args: {
modelCardId: string
error: string | { errorMessage: string; dismissible?: boolean }
}) => {
@@ -505,6 +651,19 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
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.
@@ -750,6 +909,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
hostAppName,
hostAppVersion,
connectorVersion,
activeIngestions,
isConnectorUpToDate,
latestAvailableVersion,
documentInfo,
@@ -788,6 +948,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
getSendSettings,
setModelSendResult,
setModelReceiveResult,
handleModelProgressEvents
handleModelProgressEvents,
validateSendFilter
}
})
+530
View File
@@ -3552,6 +3552,13 @@ __metadata:
languageName: node
linkType: hard
"@remirror/core-constants@npm:3.0.0":
version: 3.0.0
resolution: "@remirror/core-constants@npm:3.0.0"
checksum: 10c0/15909dd00a2d90cf1f65583bb03ff97c27bb3ec3e22467cdaec3e9cfdae50c687d044df342b985a951d28306cc94cf9188bf7742c7a811ebbb62fd9c5a16ed44
languageName: node
linkType: hard
"@repeaterjs/repeater@npm:^3.0.4, @repeaterjs/repeater@npm:^3.0.6":
version: 3.0.6
resolution: "@repeaterjs/repeater@npm:3.0.6"
@@ -4088,6 +4095,204 @@ __metadata:
languageName: node
linkType: hard
"@tiptap/core@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/core@npm:2.10.3"
peerDependencies:
"@tiptap/pm": ^2.7.0
checksum: 10c0/b72a6956720f766bc909954a919870907379875ba41b122db28eadc4e8f67b5b2ebe01cfc9df9dae715ef5d585853b97714c00607514d040ae4fd4d1c2ce70e2
languageName: node
linkType: hard
"@tiptap/extension-bold@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-bold@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
checksum: 10c0/5c5c168da9a994f77b2e6a0a1924c1d648d3af34c3c36b1fd33ca6a0824fff9192bdc92b5e2433350d641e7efc4b6c40a1f1d5c8b47cb99b05c2d4c6070ed88c
languageName: node
linkType: hard
"@tiptap/extension-bubble-menu@npm:^2.10.3":
version: 2.27.1
resolution: "@tiptap/extension-bubble-menu@npm:2.27.1"
dependencies:
tippy.js: "npm:^6.3.7"
peerDependencies:
"@tiptap/core": ^2.7.0
"@tiptap/pm": ^2.7.0
checksum: 10c0/b7cd0c75be3b93b44f5ed35e752c5db3e98281feaa456013b4016b75fc5a362511e3cd3758b79a32a5bda94cfa0058246ecaf066be617b535d164122121b13dd
languageName: node
linkType: hard
"@tiptap/extension-document@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-document@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
checksum: 10c0/7c30c8418f2b78298eb84ea92da3a99889f357937bc9207db3f698f1d83c1cb7f9b54d8c9a87205a998180e4b8229b5170eff3fe72a5f989fabf0a01effb3679
languageName: node
linkType: hard
"@tiptap/extension-floating-menu@npm:^2.10.3":
version: 2.27.1
resolution: "@tiptap/extension-floating-menu@npm:2.27.1"
dependencies:
tippy.js: "npm:^6.3.7"
peerDependencies:
"@tiptap/core": ^2.7.0
"@tiptap/pm": ^2.7.0
checksum: 10c0/85c1b99fafa2ec16842806c1d05801a747a75b0bb67a216b309ee83e026319c1d9d56bace3d81d5e699757913d4257fa87ffe3c34987f31e040ece22d1dfc36a
languageName: node
linkType: hard
"@tiptap/extension-hard-break@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-hard-break@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
checksum: 10c0/0cc805819690ff9f5efa15e5db1d4bb7e604ac859ce46ef93306454a4668e1fa784c0f6fc7e19b5a5ea5d54cffc2a6161a1c5a58836b90db79da2c3f1a9dd729
languageName: node
linkType: hard
"@tiptap/extension-history@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-history@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
"@tiptap/pm": ^2.7.0
checksum: 10c0/cb6fd2f8e931d2da14c551679cf1d78522b2fda63d92c93d80a114531be36f34b4b096bfed695b71cb9a348f2e5928a93b2d1c2cd550f086b0afaffb49bc057f
languageName: node
linkType: hard
"@tiptap/extension-italic@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-italic@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
checksum: 10c0/8cc067d934dc43813524509482e93f949bdb0428695a0629d3e0f966d0f0382de5f35db0c4e02ec5dec978af403683e15cb6f5ee89eccb417f95a8de09412f26
languageName: node
linkType: hard
"@tiptap/extension-link@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-link@npm:2.10.3"
dependencies:
linkifyjs: "npm:^4.1.0"
peerDependencies:
"@tiptap/core": ^2.7.0
"@tiptap/pm": ^2.7.0
checksum: 10c0/c127fdac8408b9d5a120003c36817b443cae8f0aa1568280007b5d44198dac60af55df44b202f4204be8aefc762a3933c71a864b0af06679b31d326ee589cf15
languageName: node
linkType: hard
"@tiptap/extension-mention@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-mention@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
"@tiptap/pm": ^2.7.0
"@tiptap/suggestion": ^2.7.0
checksum: 10c0/833e495f611def2619ec4ebd7e47e8714ef756b1ce642aa778336c4be63e56a1a2d1b06c09688e7d6fc3655ddd24d7fb0179bee10e6ceb05ebc5cc3498cc9e87
languageName: node
linkType: hard
"@tiptap/extension-paragraph@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-paragraph@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
checksum: 10c0/4c55fef6600d5ae37b149c3f3bcb1a49ea62e37728e7f6032e2093ad50da34fda1bcc3007f8693749da9dd5ca2929066a29565e61627d987d74a6472a0ca699d
languageName: node
linkType: hard
"@tiptap/extension-placeholder@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-placeholder@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
"@tiptap/pm": ^2.7.0
checksum: 10c0/9a081ba6b4e3f295633ed2f6cf950891850ea282b24e131f7f2ad942a9f881e337d9757c1df0b7187e39b56c93259b7ade8fc8b46364d8b84067e0491d3b6110
languageName: node
linkType: hard
"@tiptap/extension-strike@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-strike@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
checksum: 10c0/c22eda35bfb9122ed8fa081de03d2e90b7b57174b107401ca3fde67f97968d4d6aa45c0b8218e3c16e3d5df9b0b1c8802bba46a12ca28c45649f98ed480fe7fa
languageName: node
linkType: hard
"@tiptap/extension-text@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-text@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
checksum: 10c0/4f4514bbf119285f49fef18a67100f845162780aefd0df8fee385c56c3dd0f1be6d4da2a0d9207f21902d8bcb795b87cfff18376738e8de0df540b4b19ecd971
languageName: node
linkType: hard
"@tiptap/extension-underline@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/extension-underline@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
checksum: 10c0/ceb517232ade674da57d360d0ce589911e349ba87c79c9d7cb48cfbe67a28e2c410c0103b379564c3fbc64ed1ec9d3d5b7ef3c364f4bc22d1f890775beb58090
languageName: node
linkType: hard
"@tiptap/pm@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/pm@npm:2.10.3"
dependencies:
prosemirror-changeset: "npm:^2.2.1"
prosemirror-collab: "npm:^1.3.1"
prosemirror-commands: "npm:^1.6.2"
prosemirror-dropcursor: "npm:^1.8.1"
prosemirror-gapcursor: "npm:^1.3.2"
prosemirror-history: "npm:^1.4.1"
prosemirror-inputrules: "npm:^1.4.0"
prosemirror-keymap: "npm:^1.2.2"
prosemirror-markdown: "npm:^1.13.1"
prosemirror-menu: "npm:^1.2.4"
prosemirror-model: "npm:^1.23.0"
prosemirror-schema-basic: "npm:^1.2.3"
prosemirror-schema-list: "npm:^1.4.1"
prosemirror-state: "npm:^1.4.3"
prosemirror-tables: "npm:^1.6.1"
prosemirror-trailing-node: "npm:^3.0.0"
prosemirror-transform: "npm:^1.10.2"
prosemirror-view: "npm:^1.37.0"
checksum: 10c0/8da579aa1e052f056cee7765dafb3a00bed2a9e110e0db01225d0e6d3a3e701ff4bc58e5bd13cc48eb946370edabaa6f19d6fcf33aa790fad3b4845f99fdfd56
languageName: node
linkType: hard
"@tiptap/suggestion@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/suggestion@npm:2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
"@tiptap/pm": ^2.7.0
checksum: 10c0/552e0842dce3feb088e28bbcfe936ce4c4b041dcd593eba09a51a83bc1824a2605e27e6b5251b8d0edc3db798975637ea09776427510d76a836dd68744d22a38
languageName: node
linkType: hard
"@tiptap/vue-3@npm:2.10.3":
version: 2.10.3
resolution: "@tiptap/vue-3@npm:2.10.3"
dependencies:
"@tiptap/extension-bubble-menu": "npm:^2.10.3"
"@tiptap/extension-floating-menu": "npm:^2.10.3"
peerDependencies:
"@tiptap/core": ^2.7.0
"@tiptap/pm": ^2.7.0
vue: ^3.0.0
checksum: 10c0/10eff4df3450bf672ef8ee9ce9f5679df6a920f21418ad55c4fdbc2b83827ab3a0aca6538f35ae7bfb56351f51740a645d9f87a5d61db0f6d61c347b24c787fc
languageName: node
linkType: hard
"@trysound/sax@npm:0.2.0":
version: 0.2.0
resolution: "@trysound/sax@npm:0.2.0"
@@ -4178,6 +4383,13 @@ __metadata:
languageName: node
linkType: hard
"@types/linkify-it@npm:^5":
version: 5.0.0
resolution: "@types/linkify-it@npm:5.0.0"
checksum: 10c0/7bbbf45b9dde17bf3f184fee585aef0e7342f6954f0377a24e4ff42ab5a85d5b806aaa5c8d16e2faf2a6b87b2d94467a196b7d2b85c9c7de2f0eaac5487aaab8
languageName: node
linkType: hard
"@types/lodash-es@npm:^4.17.6":
version: 4.17.12
resolution: "@types/lodash-es@npm:4.17.12"
@@ -4194,6 +4406,23 @@ __metadata:
languageName: node
linkType: hard
"@types/markdown-it@npm:^14.0.0":
version: 14.1.2
resolution: "@types/markdown-it@npm:14.1.2"
dependencies:
"@types/linkify-it": "npm:^5"
"@types/mdurl": "npm:^2"
checksum: 10c0/34f709f0476bd4e7b2ba7c3341072a6d532f1f4cb6f70aef371e403af8a08a7c372ba6907ac426bc618d356dab660c5b872791ff6c1ead80c483e0d639c6f127
languageName: node
linkType: hard
"@types/mdurl@npm:^2":
version: 2.0.0
resolution: "@types/mdurl@npm:2.0.0"
checksum: 10c0/cde7bb571630ed1ceb3b92a28f7b59890bb38b8f34cd35326e2df43eebfc74985e6aa6fd4184e307393bad8a9e0783a519a3f9d13c8e03788c0f98e5ec869c5e
languageName: node
linkType: hard
"@types/node@npm:*, @types/node@npm:>=10.0.0":
version: 22.15.17
resolution: "@types/node@npm:22.15.17"
@@ -6863,6 +7092,13 @@ __metadata:
languageName: node
linkType: hard
"crelt@npm:^1.0.0":
version: 1.0.6
resolution: "crelt@npm:1.0.6"
checksum: 10c0/e0fb76dff50c5eb47f2ea9b786c17f9425c66276025adee80876bdbf4a84ab72e899e56d3928431ab0cb057a105ef704df80fe5726ef0f7b1658f815521bdf09
languageName: node
linkType: hard
"cron-parser@npm:^4.9.0":
version: 4.9.0
resolution: "cron-parser@npm:4.9.0"
@@ -10827,6 +11063,22 @@ __metadata:
languageName: node
linkType: hard
"linkify-it@npm:^5.0.0":
version: 5.0.0
resolution: "linkify-it@npm:5.0.0"
dependencies:
uc.micro: "npm:^2.0.0"
checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d
languageName: node
linkType: hard
"linkifyjs@npm:^4.1.0":
version: 4.3.2
resolution: "linkifyjs@npm:4.3.2"
checksum: 10c0/1a85e6b368304a4417567fe5e38651681e3e82465590836942d1b4f3c834cc35532898eb1e2479f6337d9144b297d418eb708b6be8ed0b3dc3954a3588e07971
languageName: node
linkType: hard
"listhen@npm:^1.5.6, listhen@npm:^1.9.0":
version: 1.9.0
resolution: "listhen@npm:1.9.0"
@@ -11113,6 +11365,15 @@ __metadata:
languageName: node
linkType: hard
"lucide-vue-next@npm:^0.537.0":
version: 0.537.0
resolution: "lucide-vue-next@npm:0.537.0"
peerDependencies:
vue: ">=3.0.1"
checksum: 10c0/5004c551894c19394ec15340ebb588e64ab7e237a12de713780b56b58f82cd473cfd2007e33a5a6d678f513aa559a4f84a0375321466f4e465e9755d585df877
languageName: node
linkType: hard
"luxon@npm:^3.2.1":
version: 3.6.1
resolution: "luxon@npm:3.6.1"
@@ -11193,6 +11454,22 @@ __metadata:
languageName: node
linkType: hard
"markdown-it@npm:^14.0.0":
version: 14.1.0
resolution: "markdown-it@npm:14.1.0"
dependencies:
argparse: "npm:^2.0.1"
entities: "npm:^4.4.0"
linkify-it: "npm:^5.0.0"
mdurl: "npm:^2.0.0"
punycode.js: "npm:^2.3.1"
uc.micro: "npm:^2.1.0"
bin:
markdown-it: bin/markdown-it.mjs
checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4
languageName: node
linkType: hard
"matcher@npm:^3.0.0":
version: 3.0.0
resolution: "matcher@npm:3.0.0"
@@ -11237,6 +11514,13 @@ __metadata:
languageName: node
linkType: hard
"mdurl@npm:^2.0.0":
version: 2.0.0
resolution: "mdurl@npm:2.0.0"
checksum: 10c0/633db522272f75ce4788440669137c77540d74a83e9015666a9557a152c02e245b192edc20bc90ae953bbab727503994a53b236b4d9c99bdaee594d0e7dd2ce0
languageName: node
linkType: hard
"media-typer@npm:0.3.0":
version: 0.3.0
resolution: "media-typer@npm:0.3.0"
@@ -12404,6 +12688,13 @@ __metadata:
languageName: node
linkType: hard
"orderedmap@npm:^2.0.0":
version: 2.1.1
resolution: "orderedmap@npm:2.1.1"
checksum: 10c0/8d7d266659d1828937046e8b2a7b5f75914e0391db985da0ca75cd2246cccbf6d6f3a0886aa2034da15ee4923e8c45f95f8b588f575f535f0adecdefccc54634
languageName: node
linkType: hard
"os-tmpdir@npm:~1.0.2":
version: 1.0.2
resolution: "os-tmpdir@npm:1.0.2"
@@ -13592,6 +13883,200 @@ __metadata:
languageName: node
linkType: hard
"prosemirror-changeset@npm:^2.2.1":
version: 2.3.1
resolution: "prosemirror-changeset@npm:2.3.1"
dependencies:
prosemirror-transform: "npm:^1.0.0"
checksum: 10c0/efd6578ee4535d72d11c032b49921f14b3f7ccae680eb14c8d9f6cc1fbec00299c598475af0ab432864976bdbb7f94f011193278b2d19eadda83b754fe6d8a35
languageName: node
linkType: hard
"prosemirror-collab@npm:^1.3.1":
version: 1.3.1
resolution: "prosemirror-collab@npm:1.3.1"
dependencies:
prosemirror-state: "npm:^1.0.0"
checksum: 10c0/5d7553c136929cfd847b8781be599561d0f21e78fae80d930eb5f1d4d644307bc779cdfaeae86dd31a8be8f562c28dee19f1a06a2900e9b591b02957151fe90c
languageName: node
linkType: hard
"prosemirror-commands@npm:^1.0.0, prosemirror-commands@npm:^1.6.2":
version: 1.7.1
resolution: "prosemirror-commands@npm:1.7.1"
dependencies:
prosemirror-model: "npm:^1.0.0"
prosemirror-state: "npm:^1.0.0"
prosemirror-transform: "npm:^1.10.2"
checksum: 10c0/4884ea7a66b79b51e72bb2ef358284d70e9a071deb4cbfab3dd8ee3449e9a0e34cb391d92f487c013d3716b823fc5568ad5e409a9444b3630ae0b87617c2fca1
languageName: node
linkType: hard
"prosemirror-dropcursor@npm:^1.8.1":
version: 1.8.2
resolution: "prosemirror-dropcursor@npm:1.8.2"
dependencies:
prosemirror-state: "npm:^1.0.0"
prosemirror-transform: "npm:^1.1.0"
prosemirror-view: "npm:^1.1.0"
checksum: 10c0/c3d9e456a64fecc77a6e6a0350116598550dee8cb55f74e8b66fdb26150c48340ddd1f43184134b24d0f2e710b6879ff6ec72c215dc618a6a673320a91c90478
languageName: node
linkType: hard
"prosemirror-gapcursor@npm:^1.3.2":
version: 1.4.0
resolution: "prosemirror-gapcursor@npm:1.4.0"
dependencies:
prosemirror-keymap: "npm:^1.0.0"
prosemirror-model: "npm:^1.0.0"
prosemirror-state: "npm:^1.0.0"
prosemirror-view: "npm:^1.0.0"
checksum: 10c0/c9f8274198642ca37209338d4222099d7d50c4dd743c8aab25703e2845c6ced691ca9368ad6ac2aaa1899b51709e7411c5fbada306c58301df82f168fbc4517e
languageName: node
linkType: hard
"prosemirror-history@npm:^1.0.0, prosemirror-history@npm:^1.4.1":
version: 1.5.0
resolution: "prosemirror-history@npm:1.5.0"
dependencies:
prosemirror-state: "npm:^1.2.2"
prosemirror-transform: "npm:^1.0.0"
prosemirror-view: "npm:^1.31.0"
rope-sequence: "npm:^1.3.0"
checksum: 10c0/9f24c99316c30a52ff40ddd59fc83b5180f1326ee6f466bfa82b847a503b0cdff234c35d5e3decb39ae1382a189ceec7462333ed7d6c34e2c95921892bb98b78
languageName: node
linkType: hard
"prosemirror-inputrules@npm:^1.4.0":
version: 1.5.1
resolution: "prosemirror-inputrules@npm:1.5.1"
dependencies:
prosemirror-state: "npm:^1.0.0"
prosemirror-transform: "npm:^1.0.0"
checksum: 10c0/cff1ff9f7e726bf324e6cddd2c48984e6b2970cc98cd5dc59e6dbe2e9df0e01e4a2d100c77c721067804170b9607769c0fee7c98f2cfb4f3cff9af919fce31b9
languageName: node
linkType: hard
"prosemirror-keymap@npm:^1.0.0, prosemirror-keymap@npm:^1.2.2, prosemirror-keymap@npm:^1.2.3":
version: 1.2.3
resolution: "prosemirror-keymap@npm:1.2.3"
dependencies:
prosemirror-state: "npm:^1.0.0"
w3c-keyname: "npm:^2.2.0"
checksum: 10c0/0ec2f8bd9b608d0e6a0cdab1d66f9a6b41edcff0239b32ccca1018a0733e52448e4758218a2d472fb8c33c1609426dc6bad4944b28c1c3d509a83201a23035e9
languageName: node
linkType: hard
"prosemirror-markdown@npm:^1.13.1":
version: 1.13.2
resolution: "prosemirror-markdown@npm:1.13.2"
dependencies:
"@types/markdown-it": "npm:^14.0.0"
markdown-it: "npm:^14.0.0"
prosemirror-model: "npm:^1.25.0"
checksum: 10c0/53c48ef0d0d18ca0a7c39bdb18485508bfe10582f9b1d04d7114e6f6e9678a4481b318f310b19d4e95f65d947fbe6348affddcb909ad9b8c9f865cc07ceff22b
languageName: node
linkType: hard
"prosemirror-menu@npm:^1.2.4":
version: 1.2.5
resolution: "prosemirror-menu@npm:1.2.5"
dependencies:
crelt: "npm:^1.0.0"
prosemirror-commands: "npm:^1.0.0"
prosemirror-history: "npm:^1.0.0"
prosemirror-state: "npm:^1.0.0"
checksum: 10c0/a4da649aa3c7bfb74128da203984009b44fd48638ff76ec7b209635fafd23b05d7d5bed9520282cdcf886f73eafcfbda4e77f55d81a92db333f8807d84ded2f9
languageName: node
linkType: hard
"prosemirror-model@npm:^1.0.0, prosemirror-model@npm:^1.20.0, prosemirror-model@npm:^1.21.0, prosemirror-model@npm:^1.23.0, prosemirror-model@npm:^1.25.0, prosemirror-model@npm:^1.25.4":
version: 1.25.4
resolution: "prosemirror-model@npm:1.25.4"
dependencies:
orderedmap: "npm:^2.0.0"
checksum: 10c0/5ba99a235497df3452c0e2dfb71ee05d898fb9cb539c2a92583524fc4f337d8abab5c6b49b3af242c86327ea27cce41c7169a1e0c7bb4f7ef1502b311bd00cfc
languageName: node
linkType: hard
"prosemirror-schema-basic@npm:^1.2.3":
version: 1.2.4
resolution: "prosemirror-schema-basic@npm:1.2.4"
dependencies:
prosemirror-model: "npm:^1.25.0"
checksum: 10c0/cd86f88a5eb51ab5459aa91e6824e73ec15b0f1546fee89be7826663663ef11eefaacacda5a14c43b4c8d8477fd653642418b9c7d485bb92e323f9b8e7607a78
languageName: node
linkType: hard
"prosemirror-schema-list@npm:^1.4.1":
version: 1.5.1
resolution: "prosemirror-schema-list@npm:1.5.1"
dependencies:
prosemirror-model: "npm:^1.0.0"
prosemirror-state: "npm:^1.0.0"
prosemirror-transform: "npm:^1.7.3"
checksum: 10c0/e6fd27446bc90556a9797f6ca0cb54e7db53cc7c20fbf633b7d0f4709c45accfa2f3a0f6575fe47aa83cb75781a9b773198d236a44db9d8eef2802a1501e4301
languageName: node
linkType: hard
"prosemirror-state@npm:^1.0.0, prosemirror-state@npm:^1.2.2, prosemirror-state@npm:^1.4.3, prosemirror-state@npm:^1.4.4":
version: 1.4.4
resolution: "prosemirror-state@npm:1.4.4"
dependencies:
prosemirror-model: "npm:^1.0.0"
prosemirror-transform: "npm:^1.0.0"
prosemirror-view: "npm:^1.27.0"
checksum: 10c0/1428636a37c127afe0d11a4f4eb44d75a7f16717940405082bd16fa4c28cfeef9375d49f48e411e5f0fa26f3e5798af7f270edc9bc9b0e647df8bb79983bcd59
languageName: node
linkType: hard
"prosemirror-tables@npm:^1.6.1":
version: 1.8.3
resolution: "prosemirror-tables@npm:1.8.3"
dependencies:
prosemirror-keymap: "npm:^1.2.3"
prosemirror-model: "npm:^1.25.4"
prosemirror-state: "npm:^1.4.4"
prosemirror-transform: "npm:^1.10.5"
prosemirror-view: "npm:^1.41.4"
checksum: 10c0/3c1a7ca0684b9182159af679173dde6cbf3c5af4dd3f2278712b0f33d5d065d014a913d0897881e44c82204d57b89c52cb8876d66f2737a36519f092c9613fed
languageName: node
linkType: hard
"prosemirror-trailing-node@npm:^3.0.0":
version: 3.0.0
resolution: "prosemirror-trailing-node@npm:3.0.0"
dependencies:
"@remirror/core-constants": "npm:3.0.0"
escape-string-regexp: "npm:^4.0.0"
peerDependencies:
prosemirror-model: ^1.22.1
prosemirror-state: ^1.4.2
prosemirror-view: ^1.33.8
checksum: 10c0/d512054543a872c667bcd661f207c54a38287a8e62a2ff4aa87d65aefbad0bf3a6315cc7531d9c63cc7a7ef93504966b6c9496af90287a710914688feba72454
languageName: node
linkType: hard
"prosemirror-transform@npm:^1.0.0, prosemirror-transform@npm:^1.1.0, prosemirror-transform@npm:^1.10.2, prosemirror-transform@npm:^1.10.5, prosemirror-transform@npm:^1.7.3":
version: 1.10.5
resolution: "prosemirror-transform@npm:1.10.5"
dependencies:
prosemirror-model: "npm:^1.21.0"
checksum: 10c0/64e5aeaa30f15a2873214913b3fda6fe9973802e6291d5913a549c71bf6de3e167ab0d967ee880041e3fecdcb3d950388346d805f21b3a1b6ec77285c17f40bd
languageName: node
linkType: hard
"prosemirror-view@npm:^1.0.0, prosemirror-view@npm:^1.1.0, prosemirror-view@npm:^1.27.0, prosemirror-view@npm:^1.31.0, prosemirror-view@npm:^1.37.0, prosemirror-view@npm:^1.41.4":
version: 1.41.4
resolution: "prosemirror-view@npm:1.41.4"
dependencies:
prosemirror-model: "npm:^1.20.0"
prosemirror-state: "npm:^1.0.0"
prosemirror-transform: "npm:^1.1.0"
checksum: 10c0/613e36cb27757c115ab301d3f7674e979450c928064b95fe26666765a2fc3efa80402ca8f6e4b8484e5d2f6945a14d842e1ff2900c86e4deea73613be0f42d5a
languageName: node
linkType: hard
"proto-list@npm:~1.2.1":
version: 1.2.4
resolution: "proto-list@npm:1.2.4"
@@ -13626,6 +14111,13 @@ __metadata:
languageName: node
linkType: hard
"punycode.js@npm:^2.3.1":
version: 2.3.1
resolution: "punycode.js@npm:2.3.1"
checksum: 10c0/1d12c1c0e06127fa5db56bd7fdf698daf9a78104456a6b67326877afc21feaa821257b171539caedd2f0524027fa38e67b13dd094159c8d70b6d26d2bea4dfdb
languageName: node
linkType: hard
"punycode@npm:^2.1.0":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
@@ -14296,6 +14788,13 @@ __metadata:
languageName: node
linkType: hard
"rope-sequence@npm:^1.3.0":
version: 1.3.4
resolution: "rope-sequence@npm:1.3.4"
checksum: 10c0/caa90be3d7a7cad155fb354a4679a1280dc9819c81bd319542a0d893a64e152284abb9cc1631d4351b328016a8d6c35a48c912234edfaf5173daef44b2e3609b
languageName: node
linkType: hard
"router@npm:^2.2.0":
version: 2.2.0
resolution: "router@npm:2.2.0"
@@ -14966,6 +15465,22 @@ __metadata:
"@speckle/tailwind-theme": "npm:2.25.0"
"@speckle/ui-components": "npm:^2.25.0"
"@speckle/ui-components-nuxt": "npm:^2.25.0"
"@tiptap/core": "npm:2.10.3"
"@tiptap/extension-bold": "npm:2.10.3"
"@tiptap/extension-document": "npm:2.10.3"
"@tiptap/extension-hard-break": "npm:2.10.3"
"@tiptap/extension-history": "npm:2.10.3"
"@tiptap/extension-italic": "npm:2.10.3"
"@tiptap/extension-link": "npm:2.10.3"
"@tiptap/extension-mention": "npm:2.10.3"
"@tiptap/extension-paragraph": "npm:2.10.3"
"@tiptap/extension-placeholder": "npm:2.10.3"
"@tiptap/extension-strike": "npm:2.10.3"
"@tiptap/extension-text": "npm:2.10.3"
"@tiptap/extension-underline": "npm:2.10.3"
"@tiptap/pm": "npm:2.10.3"
"@tiptap/suggestion": "npm:2.10.3"
"@tiptap/vue-3": "npm:2.10.3"
"@types/apollo-upload-client": "npm:^17.0.1"
"@types/eslint": "npm:^9.6.1"
"@types/lodash-es": "npm:^4.17.6"
@@ -14988,6 +15503,7 @@ __metadata:
graphql: "npm:^16.6.0"
graphql-tag: "npm:^2.12.6"
lodash-es: "npm:^4.17.21"
lucide-vue-next: "npm:^0.537.0"
nanoevents: "npm:^8.0.0"
nuxt: "npm:^3.17.3"
pinia: "npm:^2.1.4"
@@ -16088,6 +16604,13 @@ __metadata:
languageName: node
linkType: hard
"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0":
version: 2.1.0
resolution: "uc.micro@npm:2.1.0"
checksum: 10c0/8862eddb412dda76f15db8ad1c640ccc2f47cdf8252a4a30be908d535602c8d33f9855dfcccb8b8837855c1ce1eaa563f7fa7ebe3c98fd0794351aab9b9c55fa
languageName: node
linkType: hard
"ufo@npm:^1.1.2, ufo@npm:^1.3.2, ufo@npm:^1.5.4, ufo@npm:^1.6.1":
version: 1.6.1
resolution: "ufo@npm:1.6.1"
@@ -17051,6 +17574,13 @@ __metadata:
languageName: node
linkType: hard
"w3c-keyname@npm:^2.2.0":
version: 2.2.8
resolution: "w3c-keyname@npm:2.2.8"
checksum: 10c0/37cf335c90efff31672ebb345577d681e2177f7ff9006a9ad47c68c5a9d265ba4a7b39d6c2599ceea639ca9315584ce4bd9c9fbf7a7217bfb7a599e71943c4c4
languageName: node
linkType: hard
"wcwidth@npm:^1.0.1":
version: 1.0.1
resolution: "wcwidth@npm:1.0.1"