feat(acc): revamp (#5501)
* chore(acc): put permission gql in correct place * feat(acc): swap to new rvt import * fix(acc): add oda secrets * feat(acc): auth cookies * feat(acc): introduce integrations as workspace setting * feat(acc): create sync item from models * fix(acc): bump * fix(acc): naming lost in merge * feat(acc): no acc tab - table under settings * chore(acc): new sync but will disapper * feat(acc): see statuses over model list * chore(acc): fix return type * chore(acc): type saga * chore(acc): status badge * chore(acc): refactor acc gql (#5556) * checkpoint * fix(acc): refactor gql items * feat(acc): double button * chore(acc): gqlgen * fix(acc): model ids are not project ids * chore(acc): bump function version * chore(acc): split up clients * feat(acc): more-optimised gql folder fetching schema * feat(acc): acc folder contents gql impl * feat(acc): apollo cache optimisations * chore(acc): gqlgen * fix(acc): return something for * fix(acc): handle null values correctly * chore(acc): specify prod functions --------- Co-authored-by: Chuck Driesler <chuck@speckle.systems>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center justify-between border border-foreground-1 bg-foundation rounded-lg p-2"
|
||||
>
|
||||
<div class="flex items-center space-x-3">
|
||||
<img
|
||||
:src="integration.logo"
|
||||
alt=""
|
||||
class="w-10 h-10 p-1 object-cover border border-foreground-1 rounded-lg"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-medium">{{ integration.name }}</span>
|
||||
<span class="text-sm text-foreground-2">
|
||||
{{ integration.description }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<CommonLoadingIcon v-if="loading" :loading="true" class="opacity-50 mr-2" />
|
||||
<div v-else-if="integration.enabled">
|
||||
<div class="flex items-center text-sm text-foreground-2 space-x-2">
|
||||
<span
|
||||
v-if="
|
||||
integration.status === 'connected' || integration.status === 'expired'
|
||||
"
|
||||
class="w-2 h-2 rounded-full"
|
||||
:class="{
|
||||
'bg-success': integration.status === 'connected',
|
||||
'bg-warning': integration.status === 'expired'
|
||||
}"
|
||||
></span>
|
||||
<div>{{ statusText() }}</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<FormButton size="sm" color="outline" @click="handleCTA()">
|
||||
<span v-if="integration.status === 'notConnected'">Log in</span>
|
||||
<span v-else-if="integration.status === 'expired'">Reconnect</span>
|
||||
<span v-else>Log out</span>
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<FormButton size="sm" @click="$emit('upgrade')">Upgrade</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAccAuthManager } from '~/lib/acc/composables/useAccAuthManager'
|
||||
import { useAccIntegration } from '~/lib/integrations/composables/useAccIntegration'
|
||||
|
||||
const props = defineProps<{ workspaceId: string; workspaceSlug: string }>()
|
||||
|
||||
defineEmits<{
|
||||
(e: 'handleCTA'): void
|
||||
(e: 'upgrade'): void
|
||||
}>()
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const { integration, checkConnection } = useAccIntegration()
|
||||
const { tokens, authAcc, logOut, tryGetTokensFromCookies, fetchTokens } =
|
||||
useAccAuthManager() // later can be generalized
|
||||
|
||||
// await checkConnection(props.workspaceSlug, props.workspaceId || '')
|
||||
|
||||
const statusText = () => {
|
||||
switch (integration.value.status) {
|
||||
case 'connected':
|
||||
return 'Connected'
|
||||
case 'expired':
|
||||
return 'Expired'
|
||||
case 'notConnected':
|
||||
return ''
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const handleCTA = async () => {
|
||||
if (
|
||||
integration.value.status === 'notConnected' ||
|
||||
integration.value.status === 'expired'
|
||||
) {
|
||||
authAcc(`/settings/workspaces/${props.workspaceSlug}/integrations`)
|
||||
} else {
|
||||
logOut()
|
||||
await checkConnection(props.workspaceSlug, props.workspaceId || '')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
await tryGetTokensFromCookies()
|
||||
if (!tokens.value) {
|
||||
await fetchTokens()
|
||||
}
|
||||
await checkConnection(props.workspaceSlug, props.workspaceId || '')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="flex flex-row h-full overflow-hidden border rounded-lg bg-foundation">
|
||||
<!-- Left Pane for tree -->
|
||||
<div class="w-1/4 p-2 overflow-y-auto border-r">
|
||||
<h3 class="font-semibold text-lg text-center">Folders</h3>
|
||||
<hr class="mb-1" />
|
||||
<div v-if="!rootFolder"></div>
|
||||
<ul
|
||||
v-else-if="rootFolder && rootFolder.children?.items.length"
|
||||
class="space-y-1 pt-1"
|
||||
>
|
||||
<IntegrationsAccFolderNode
|
||||
v-for="folder in rootFolderChildren"
|
||||
:key="folder.id"
|
||||
:project-id="projectId"
|
||||
:folder-id="folder.id"
|
||||
:tokens="tokens"
|
||||
:selected-folder-id="selectedFolderId"
|
||||
@select="onFolderClick"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Right Pane for content -->
|
||||
<div class="w-3/4 p-2 overflow-y-auto">
|
||||
<h3 class="font-semibold text-lg text-center">Files</h3>
|
||||
<hr class="mb-1" />
|
||||
<IntegrationsAccFolderContents
|
||||
v-if="!!selectedFolderId"
|
||||
:key="`contents-${selectedFolderId}`"
|
||||
:project-id="projectId"
|
||||
:folder-id="selectedFolderId"
|
||||
:tokens="tokens"
|
||||
:selected-file-id="selectedFileId"
|
||||
@select="onFileSelected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useAcc, type AccItemVersion } from '~/lib/acc/composables/useAccFiles'
|
||||
import { useAccFolder } from '~/lib/acc/composables/useAccFolderData'
|
||||
|
||||
const props = defineProps<{
|
||||
hubId: string
|
||||
projectId: string
|
||||
tokens: AccTokens | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'file-selected': [fileId: string, fileVersion: AccItemVersion]
|
||||
}>()
|
||||
|
||||
const { init, rootProjectFolderId } = useAcc()
|
||||
|
||||
const rootFolder = useAccFolder(props.projectId, rootProjectFolderId, props.tokens)
|
||||
const rootFolderChildren = computed(
|
||||
() =>
|
||||
rootFolder.value?.children?.items?.filter(
|
||||
(child) => child.name === 'Project Files'
|
||||
) ?? []
|
||||
)
|
||||
|
||||
const selectedFolderId = ref<string | undefined>()
|
||||
const selectedFileId = ref<string | undefined>()
|
||||
|
||||
const onFolderClick = async (folderId: string) => {
|
||||
selectedFolderId.value = folderId
|
||||
selectedFileId.value = undefined
|
||||
}
|
||||
|
||||
const onFileSelected = (fileId: string, fileVersion: AccItemVersion) => {
|
||||
selectedFileId.value = fileId
|
||||
emit('file-selected', fileId, fileVersion)
|
||||
}
|
||||
|
||||
// Watch for changes in projectId to re-initialize the folder tree
|
||||
watch(
|
||||
() => props.projectId,
|
||||
async (newProjectId) => {
|
||||
selectedFolderId.value = undefined
|
||||
selectedFileId.value = undefined
|
||||
if (newProjectId && props.tokens) {
|
||||
await init(props.hubId, newProjectId, props.tokens.access_token)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(rootFolderChildren, (newValue) => {
|
||||
selectedFolderId.value = newValue.at(0)?.id
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<ul v-if="items?.length" class="space-y-1">
|
||||
<template v-for="item in items" :key="item.id">
|
||||
<li
|
||||
class="flex items-center space-x-1 px-2 rounded-md transition-colors w-full"
|
||||
:class="{
|
||||
'bg-foundation-focus font-semibold': selectedFileId === item.id,
|
||||
'hover:bg-primary-muted cursor-pointer': selectedFileId !== item.id
|
||||
}"
|
||||
>
|
||||
<button
|
||||
class="flex items-center space-x-1 p-1 rounded-md transition-colors w-full"
|
||||
@click="
|
||||
emit('select', item.id, removeNullOrUndefinedKeys(item.latestVersion))
|
||||
"
|
||||
>
|
||||
<span>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<hr />
|
||||
</template>
|
||||
</ul>
|
||||
<div v-else class="text-center text-foreground-2 py-2">
|
||||
<span>No files found.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { removeNullOrUndefinedKeys } from '@speckle/shared'
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import type { AccItemVersion } from '~/lib/acc/composables/useAccFiles'
|
||||
import { useAccFolder } from '~/lib/acc/composables/useAccFolderData'
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string
|
||||
folderId: string
|
||||
tokens: AccTokens | undefined
|
||||
selectedFileId: string | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [fileId: string, fileVersion: AccItemVersion]
|
||||
}>()
|
||||
|
||||
const folder = useAccFolder(props.projectId, props.folderId, props.tokens)
|
||||
|
||||
const items = computed(() => {
|
||||
return folder.value.contents?.items.filter(
|
||||
(item) => item.latestVersion.fileType?.toLowerCase() === 'rvt'
|
||||
)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center space-x-1 p-1 rounded-md transition-colors w-full"
|
||||
:class="{
|
||||
'bg-foundation-focus font-semibold': selectedFolderId === folder.id,
|
||||
'hover:bg-primary-muted cursor-pointer': selectedFolderId !== folder.id
|
||||
}"
|
||||
@click="emit('select', folderId)"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
:class="`h-4 w-5 transition ${!isExpanded ? '-rotate-90' : 'rotate-0'}`"
|
||||
@click.stop="isExpanded = !isExpanded"
|
||||
/>
|
||||
<span>{{ folder.name }}</span>
|
||||
</button>
|
||||
<ul
|
||||
v-if="isExpanded && folder.children && folder.children.items.length > 0"
|
||||
class="ml-4 mt-1 space-y-1"
|
||||
>
|
||||
<IntegrationsAccFolderNode
|
||||
v-for="child in folder.children.items"
|
||||
:key="child.id"
|
||||
:folder-id="child.id"
|
||||
:project-id="projectId"
|
||||
:tokens="tokens"
|
||||
:selected-folder-id="selectedFolderId"
|
||||
@select="(id) => emit('select', id)"
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import { useAccFolder } from '~/lib/acc/composables/useAccFolderData'
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
|
||||
graphql(`
|
||||
fragment AccIntegrationFolderNode_AccFolder on AccFolder {
|
||||
id
|
||||
name
|
||||
contents {
|
||||
items {
|
||||
id
|
||||
name
|
||||
latestVersion {
|
||||
id
|
||||
name
|
||||
versionNumber
|
||||
fileType
|
||||
}
|
||||
}
|
||||
}
|
||||
children {
|
||||
items {
|
||||
id
|
||||
name
|
||||
children {
|
||||
items {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
contents {
|
||||
items {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
// TODO ACC Maybe inject from shared local state within file navigation
|
||||
projectId: string
|
||||
folderId: string
|
||||
tokens?: AccTokens
|
||||
// TODO ACC Maybe inject from shared local state within file navigation
|
||||
selectedFolderId: string | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [folderId: string]
|
||||
}>()
|
||||
|
||||
const folder = useAccFolder(props.projectId, props.folderId, props.tokens)
|
||||
|
||||
// watch(
|
||||
// folder,
|
||||
// (f) => {
|
||||
// console.log({ resultFolder: f })
|
||||
// },
|
||||
// {
|
||||
// immediate: true
|
||||
// }
|
||||
// )
|
||||
|
||||
const isExpanded = ref(false)
|
||||
</script>
|
||||
+2
-2
@@ -11,12 +11,12 @@
|
||||
/>
|
||||
|
||||
<CommonLoadingBar v-if="loading" loading />
|
||||
<ProjectPageAccModelItem
|
||||
<IntegrationsAccModelItem
|
||||
v-for="model in models"
|
||||
:key="model.id"
|
||||
:model="model"
|
||||
:selected="model.id === selectedModel?.id"
|
||||
:disabled="!!props.accSyncItems?.find((i) => i.modelId === model.id)"
|
||||
:disabled="!!props.accSyncItems?.find((i) => i.model?.id === model.id)"
|
||||
@click="onModelItemClicked(model)"
|
||||
/>
|
||||
<button
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div v-tippy="statusLabel">
|
||||
<CommonBadge
|
||||
:color-classes="
|
||||
[runStatusClasses(status), 'shrink-0 grow-0 text-foreground'].join(' ')
|
||||
"
|
||||
>
|
||||
ACC
|
||||
</CommonBadge>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
AccSyncItemStatus,
|
||||
SyncStatusModelItem_AccSyncItemFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {
|
||||
id
|
||||
status
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
item: SyncStatusModelItem_AccSyncItemFragment
|
||||
}>()
|
||||
|
||||
const status = computed(() => props.item.status)
|
||||
const statusLabel = computed(
|
||||
() => status.value.charAt(0).toUpperCase() + status.value.slice(1)
|
||||
)
|
||||
|
||||
const runStatusClasses = (run: AccSyncItemStatus) => {
|
||||
const classParts = ['w-24 justify-center']
|
||||
|
||||
switch (run) {
|
||||
case 'syncing':
|
||||
classParts.push('bg-info-lighter')
|
||||
break
|
||||
case 'pending':
|
||||
classParts.push('bg-warning-lighter')
|
||||
break
|
||||
case 'paused':
|
||||
classParts.push('bg-warning-lighter')
|
||||
break
|
||||
case 'failed':
|
||||
classParts.push('bg-danger-lighter')
|
||||
break
|
||||
case 'succeeded':
|
||||
classParts.push('bg-success-lighter')
|
||||
break
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex text-body-xs text-foreground font-medium">Sync models</div>
|
||||
<LayoutTable
|
||||
class="bg-foundation"
|
||||
:columns="[
|
||||
{ id: 'status', header: 'Status', classes: 'col-span-2' },
|
||||
{ id: 'accFileName', header: 'File name', classes: 'col-span-2' },
|
||||
{ id: 'accFileViewName', header: 'View name', classes: 'col-span-2' },
|
||||
{ id: 'modelId', header: 'Model id', classes: 'col-span-2' },
|
||||
{ id: 'createdBy', header: 'Created by', classes: 'col-span-2' },
|
||||
{ id: 'actions', header: 'Actions', classes: 'col-span-2' }
|
||||
]"
|
||||
:items="accSyncItems"
|
||||
>
|
||||
<template #status="{ item }">
|
||||
<IntegrationsAccSyncStatus :status="item.status" />
|
||||
</template>
|
||||
<template #accFileName="{ item }">
|
||||
{{ item.accFileName }}
|
||||
</template>
|
||||
<template #accFileViewName="{ item }">
|
||||
{{ item.accFileViewName || '-' }}
|
||||
</template>
|
||||
<template #modelId="{ item }">
|
||||
<NuxtLink
|
||||
class="text-foreground-1 hover:text-blue-500 underline"
|
||||
:to="`/projects/${projectId}/models/${item.model?.id}`"
|
||||
>
|
||||
{{ item.model?.id }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #createdBy="{ item }">
|
||||
{{ item.author?.name }}
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<div class="space-x-2">
|
||||
<FormButton
|
||||
hide-text
|
||||
color="outline"
|
||||
:icon-left="item.status === 'paused' ? PlayIcon : PauseIcon"
|
||||
@click="handleStatusSyncItem(item.id, item.status === 'paused')"
|
||||
/>
|
||||
<FormButton
|
||||
hide-text
|
||||
color="outline"
|
||||
:icon-left="TrashIcon"
|
||||
@click="handleDeleteSyncItem(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable'
|
||||
import {
|
||||
accSyncItemDeleteMutation,
|
||||
accSyncItemUpdateMutation
|
||||
} from '~/lib/acc/graphql/mutations'
|
||||
import { projectAccSyncItemsQuery } from '~/lib/acc/graphql/queries'
|
||||
import { onProjectAccSyncItemUpdatedSubscription } from '~/lib/acc/graphql/subscriptions'
|
||||
import { PauseIcon } from '@heroicons/vue/24/solid'
|
||||
import { TrashIcon, PlayIcon } from '@heroicons/vue/24/outline'
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string
|
||||
tokens: AccTokens | undefined
|
||||
isLoggedIn: boolean
|
||||
}>()
|
||||
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const { result: accSyncItemsResult, refetch: refetchAccSyncItems } = useQuery(
|
||||
projectAccSyncItemsQuery,
|
||||
() => ({
|
||||
id: props.projectId
|
||||
})
|
||||
)
|
||||
|
||||
const accSyncItems = computed(
|
||||
() => accSyncItemsResult.value?.project.accSyncItems.items || []
|
||||
)
|
||||
|
||||
const { onResult: onProjectAccSyncItemsUpdated } = useSubscription(
|
||||
onProjectAccSyncItemUpdatedSubscription,
|
||||
() => ({
|
||||
id: props.projectId
|
||||
})
|
||||
)
|
||||
|
||||
onProjectAccSyncItemsUpdated((res) => {
|
||||
// TODO ACC: Mutate local cache instead of refetch
|
||||
refetchAccSyncItems()
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Info,
|
||||
title: `ACC sync model ${res.data?.projectAccSyncItemsUpdated.type.toLowerCase()}`,
|
||||
description: res.data?.projectAccSyncItemsUpdated.accSyncItem?.accFileName
|
||||
})
|
||||
})
|
||||
|
||||
const { mutate: deleteAccSyncItem } = useMutation(accSyncItemDeleteMutation)
|
||||
|
||||
const handleDeleteSyncItem = async (id: string) => {
|
||||
try {
|
||||
await deleteAccSyncItem({
|
||||
input: {
|
||||
projectId: props.projectId,
|
||||
id
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Delete sync item failed',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { mutate: updateAccSyncItem } = useMutation(accSyncItemUpdateMutation)
|
||||
|
||||
const handleStatusSyncItem = async (id: string, isPaused: boolean) => {
|
||||
try {
|
||||
await updateAccSyncItem({
|
||||
input: {
|
||||
projectId: props.projectId,
|
||||
id,
|
||||
status: isPaused ? 'pending' : 'paused'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Update sync item failed',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
:title="dialogTitle"
|
||||
:buttons="dialogButtons"
|
||||
max-width="lg"
|
||||
fullscreen="none"
|
||||
>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<IntegrationsAccHubs
|
||||
:hubs="hubs"
|
||||
:loading="loadingHubs"
|
||||
@hub-selected="onHubClick"
|
||||
/>
|
||||
|
||||
<IntegrationsAccProjects
|
||||
v-if="selectedHubId"
|
||||
:hub-id="selectedHubId"
|
||||
:projects="projects"
|
||||
:loading="loadingProjects"
|
||||
@project-selected="onProjectClick"
|
||||
/>
|
||||
|
||||
<IntegrationsAccFileSelector
|
||||
v-if="selectedProjectId && selectedHubId && tokens"
|
||||
:key="selectedProjectId"
|
||||
:hub-id="selectedHubId"
|
||||
:project-id="selectedProjectId"
|
||||
:tokens="tokens"
|
||||
@file-selected="onFileSelected"
|
||||
/>
|
||||
|
||||
<FormTextInput
|
||||
v-model="revitViewName"
|
||||
name="revitFileViewName"
|
||||
color="foundation"
|
||||
label="Revit view name (Optional)"
|
||||
show-label
|
||||
:disabled="!selectedFileVersion"
|
||||
></FormTextInput>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccHub } from '@speckle/shared/acc'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useMutation } from '@vue/apollo-composable'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useAccAuthManager } from '~/lib/acc/composables/useAccAuthManager'
|
||||
import {
|
||||
useAcc,
|
||||
type AccFolder,
|
||||
type AccItemVersion
|
||||
} from '~/lib/acc/composables/useAccFiles'
|
||||
import { accSyncItemCreateMutation } from '~/lib/acc/graphql/mutations'
|
||||
import { useCreateNewModel } from '~/lib/projects/composables/modelManagement'
|
||||
|
||||
type FormValues = { feedback: string }
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
intro?: string
|
||||
hideSuppport?: boolean
|
||||
projectId?: string
|
||||
metadata?: Record<string, unknown>
|
||||
}>()
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const { handleSubmit } = useForm<FormValues>()
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Create',
|
||||
props: { color: 'primary' },
|
||||
onClick: () => {
|
||||
onSubmit()
|
||||
},
|
||||
id: 'createAccSync'
|
||||
}
|
||||
])
|
||||
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const createModel = useCreateNewModel()
|
||||
|
||||
const dialogTitle = computed(() => props.title || 'Create sync from ACC')
|
||||
|
||||
const onSubmit = handleSubmit(async () => {
|
||||
await addSync()
|
||||
isOpen.value = false
|
||||
})
|
||||
|
||||
const selectedHub = ref<AccHub | null>(null)
|
||||
const selectedHubId = ref<string | null>(null)
|
||||
const selectedProjectId = ref<string | null>(null)
|
||||
|
||||
const revitViewName = ref<string>()
|
||||
|
||||
const selectedFolder = ref<AccFolder | undefined>()
|
||||
const selectedFileId = ref<string | undefined>()
|
||||
const selectedFileVersion = ref<AccItemVersion | undefined>()
|
||||
|
||||
const {
|
||||
hubs,
|
||||
fetchHubs,
|
||||
loadingHubs,
|
||||
projects,
|
||||
fetchProjects,
|
||||
loadingProjects,
|
||||
rootProjectFolderId,
|
||||
init
|
||||
} = useAcc()
|
||||
|
||||
const { tokens, tryGetTokensFromCookies } = useAccAuthManager()
|
||||
|
||||
const onHubClick = async (hub: AccHub) => {
|
||||
selectedHub.value = hub
|
||||
selectedHubId.value = hub.id
|
||||
await fetchProjects(hub.id, tokens.value!.access_token)
|
||||
}
|
||||
|
||||
const onProjectClick = async (hubId: string, projectId: string) => {
|
||||
selectedFolder.value = undefined
|
||||
selectedFileVersion.value = undefined
|
||||
selectedProjectId.value = projectId
|
||||
await init(hubId, projectId, tokens.value!.access_token)
|
||||
}
|
||||
|
||||
const onFileSelected = (fileId: string, fileVersion: AccItemVersion) => {
|
||||
selectedFileId.value = fileId
|
||||
selectedFileVersion.value = fileVersion
|
||||
}
|
||||
|
||||
const { mutate: createAccSyncItem } = useMutation(accSyncItemCreateMutation)
|
||||
|
||||
const addSync = async () => {
|
||||
try {
|
||||
if (!selectedFileVersion.value || !selectedFileVersion.value.fileType) {
|
||||
return
|
||||
}
|
||||
|
||||
const fileVersion = selectedFileVersion.value.versionNumber
|
||||
|
||||
const accFileViewName = revitViewName.value === '' ? undefined : revitViewName.value
|
||||
|
||||
const res = await createModel({
|
||||
name: selectedFileVersion.value.name,
|
||||
description: '',
|
||||
projectId: props.projectId as string
|
||||
})
|
||||
|
||||
await createAccSyncItem({
|
||||
input: {
|
||||
projectId: props.projectId as string,
|
||||
modelId: res?.id as string,
|
||||
accRegion: selectedHub.value?.attributes?.region as string,
|
||||
accFileExtension: selectedFileVersion.value.fileType,
|
||||
accHubId: selectedHubId.value!,
|
||||
accProjectId: selectedProjectId.value as string,
|
||||
accRootProjectFolderUrn: rootProjectFolderId.value!,
|
||||
accFileLineageUrn: selectedFileId.value as string,
|
||||
accFileName: selectedFileVersion.value.name,
|
||||
accFileVersionIndex: fileVersion,
|
||||
accFileVersionUrn: selectedFileVersion.value.id,
|
||||
accFileViewName
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Add sync item failed',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
} finally {
|
||||
revitViewName.value = undefined
|
||||
}
|
||||
}
|
||||
|
||||
watch(tokens, (newTokens) => {
|
||||
if (newTokens?.access_token) {
|
||||
fetchHubs(newTokens?.access_token)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await tryGetTokensFromCookies()
|
||||
if (tokens.value) {
|
||||
fetchHubs(tokens.value.access_token)
|
||||
}
|
||||
})
|
||||
|
||||
watch(isOpen, (newVal) => {
|
||||
if (newVal) {
|
||||
selectedFileVersion.value = undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-row h-full overflow-hidden border rounded-lg bg-foundation">
|
||||
<!-- Left Pane for tree -->
|
||||
<div class="w-1/4 p-2 overflow-y-auto border-r">
|
||||
<h3 class="font-semibold text-lg text-center">Folders</h3>
|
||||
<hr class="mb-1" />
|
||||
<div v-if="loadingTree" class="text-center text-foreground-2 py-2">
|
||||
Loading folders...
|
||||
<InfiniteLoading />
|
||||
</div>
|
||||
<ul v-else-if="folderTree && folderTree.children" class="space-y-1 pt-1">
|
||||
<ProjectPageAccFolderNode
|
||||
v-for="folder in folderTree.children"
|
||||
:key="folder.id"
|
||||
:folder="folder"
|
||||
:on-select-folder="onFolderClick"
|
||||
:selected-folder-id="selectedFolder?.id"
|
||||
/>
|
||||
</ul>
|
||||
<div v-else class="text-center text-foreground-2 py-4">No folders found.</div>
|
||||
</div>
|
||||
<!-- Right Pane for content -->
|
||||
<div class="w-3/4 p-2 overflow-y-auto">
|
||||
<h3 class="font-semibold text-lg text-center">Files</h3>
|
||||
<hr class="mb-1" />
|
||||
<div
|
||||
v-if="loadingItems || loadingTree"
|
||||
class="text-center text-foreground-2 py-2"
|
||||
>
|
||||
<div v-if="selectedFolder">
|
||||
<span>Loading files in</span>
|
||||
<span class="font-bold">{{ ` ${selectedFolder.attributes.name} ` }}</span>
|
||||
<span>folder.</span>
|
||||
<InfiniteLoading />
|
||||
</div>
|
||||
<div v-else>Waiting for folder selection...</div>
|
||||
</div>
|
||||
<ul v-else-if="folderItems.length > 0" class="space-y-1">
|
||||
<template v-for="item in folderItems" :key="item.id">
|
||||
<li
|
||||
class="flex items-center space-x-1 px-2 rounded-md transition-colors w-full"
|
||||
:class="{
|
||||
'bg-foundation-focus font-semibold': selectedFile?.id === item.id,
|
||||
'hover:bg-primary-muted cursor-pointer': selectedFile?.id !== item.id
|
||||
}"
|
||||
>
|
||||
<button
|
||||
class="flex items-center space-x-1 p-1 rounded-md transition-colors w-full"
|
||||
@click="onFileSelected(item)"
|
||||
>
|
||||
<span>
|
||||
{{ item.attributes.name || item.attributes.displayName }}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<hr />
|
||||
</template>
|
||||
</ul>
|
||||
<div v-else class="text-center text-foreground-2 py-2">
|
||||
<div v-if="selectedFolder">
|
||||
<span>No files found in</span>
|
||||
<span class="font-bold">{{ ` ${selectedFolder.attributes.name} ` }}</span>
|
||||
<span>folder.</span>
|
||||
</div>
|
||||
<span v-else>Select a folder to view files..</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import { ref, watch } from 'vue'
|
||||
import { useAcc } from '~/lib/acc/composables/useAcc'
|
||||
import type { AccFolder, AccItem } from '~/lib/acc/composables/useAcc'
|
||||
|
||||
const props = defineProps<{
|
||||
hubId: string
|
||||
projectId: string
|
||||
tokens: AccTokens | undefined
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['file-selected'])
|
||||
|
||||
const {
|
||||
loadingTree,
|
||||
loadingItems,
|
||||
folderTree,
|
||||
folderItems,
|
||||
fetchItemsForFolder,
|
||||
init
|
||||
} = useAcc()
|
||||
|
||||
const selectedFolder = ref<AccFolder | undefined>()
|
||||
const selectedFile = ref<AccItem | undefined>()
|
||||
|
||||
const onFolderClick = async (folder: AccFolder) => {
|
||||
selectedFolder.value = folder
|
||||
selectedFile.value = undefined
|
||||
await fetchItemsForFolder(folder.id, props.projectId, props.tokens!.access_token)
|
||||
}
|
||||
|
||||
const onFileSelected = (item: AccItem) => {
|
||||
selectedFile.value = item
|
||||
emit('file-selected', item)
|
||||
}
|
||||
|
||||
// Watch for changes in projectId to re-initialize the folder tree
|
||||
watch(
|
||||
() => props.projectId,
|
||||
async (newProjectId) => {
|
||||
selectedFolder.value = undefined
|
||||
selectedFile.value = undefined
|
||||
if (newProjectId && props.tokens) {
|
||||
await init(props.hubId, newProjectId, props.tokens.access_token)
|
||||
// Automatically select the first folder and fetch its files
|
||||
if (
|
||||
folderTree.value &&
|
||||
folderTree.value.children &&
|
||||
folderTree.value.children.length > 0
|
||||
) {
|
||||
await onFolderClick(folderTree.value.children[0])
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -1,49 +0,0 @@
|
||||
<template>
|
||||
<li>
|
||||
<button
|
||||
class="flex items-center space-x-1 p-1 rounded-md transition-colors w-full"
|
||||
:class="{
|
||||
'bg-foundation-focus font-semibold': selectedFolderId === folder.id,
|
||||
'hover:bg-primary-muted cursor-pointer': selectedFolderId !== folder.id
|
||||
}"
|
||||
@click="select(folder)"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
:class="`h-4 w-5 transition ${!isExpanded ? '-rotate-90' : 'rotate-0'}`"
|
||||
@click.stop="isExpanded = !isExpanded"
|
||||
/>
|
||||
<span>{{ folder.attributes.name }}</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
v-if="isExpanded && folder.children && folder.children.length > 0"
|
||||
class="ml-4 mt-1 space-y-1"
|
||||
>
|
||||
<ProjectPageAccFolderNode
|
||||
v-for="child in folder.children"
|
||||
:key="child.id"
|
||||
:folder="child"
|
||||
:on-select-folder="onSelectFolder"
|
||||
:selected-folder-id="selectedFolderId"
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
|
||||
import type { AccFolder } from '~/lib/acc/composables/useAcc'
|
||||
|
||||
const props = defineProps<{
|
||||
folder: AccFolder
|
||||
onSelectFolder: (folder: AccFolder) => void
|
||||
selectedFolderId: string | undefined
|
||||
}>()
|
||||
|
||||
const isExpanded = ref(false)
|
||||
|
||||
const select = (folder: AccFolder) => {
|
||||
props.onSelectFolder(folder)
|
||||
}
|
||||
</script>
|
||||
@@ -1,339 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="flex text-body-xs text-foreground font-medium">Sync models</div>
|
||||
<LayoutTable
|
||||
class="bg-foundation"
|
||||
:columns="[
|
||||
{ id: 'status', header: 'Status', classes: 'col-span-2' },
|
||||
{ id: 'accFileName', header: 'File name', classes: 'col-span-2' },
|
||||
{ id: 'accFileViewName', header: 'View name', classes: 'col-span-2' },
|
||||
{ id: 'modelId', header: 'Model id', classes: 'col-span-2' },
|
||||
{ id: 'createdBy', header: 'Created by', classes: 'col-span-2' },
|
||||
{ id: 'actions', header: 'Actions', classes: 'col-span-2' }
|
||||
]"
|
||||
:items="accSyncItems"
|
||||
>
|
||||
<template #status="{ item }">
|
||||
<ProjectPageAccSyncStatus :status="item.status" />
|
||||
</template>
|
||||
<template #accFileName="{ item }">
|
||||
{{ item.accFileName }}
|
||||
</template>
|
||||
<template #accFileViewName="{ item }">
|
||||
{{ item.accFileViewName || '-' }}
|
||||
</template>
|
||||
<template #modelId="{ item }">
|
||||
<NuxtLink
|
||||
class="text-foreground-1 hover:text-blue-500 underline"
|
||||
:to="`/projects/${projectId}/models/${item.modelId}`"
|
||||
>
|
||||
{{ item.modelId }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template #createdBy="{ item }">
|
||||
{{ item.author?.name }}
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<div class="space-x-2">
|
||||
<FormButton
|
||||
hide-text
|
||||
color="outline"
|
||||
:icon-left="item.status === 'paused' ? PlayIcon : PauseIcon"
|
||||
@click="handleStatusSyncItem(item.id, item.status === 'paused')"
|
||||
/>
|
||||
<FormButton
|
||||
hide-text
|
||||
color="outline"
|
||||
:icon-left="TrashIcon"
|
||||
@click="handleDeleteSyncItem(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
<FormButton
|
||||
color="outline"
|
||||
size="sm"
|
||||
:disabled="!isLoggedIn"
|
||||
:disabled-tooltip="'Log in required'"
|
||||
@click="showNewSyncDialog = true"
|
||||
>
|
||||
<template #default>
|
||||
<div v-tippy="isLoggedIn ? undefined : 'Log in required'">New sync</div>
|
||||
</template>
|
||||
</FormButton>
|
||||
<LayoutDialog
|
||||
v-model:open="showNewSyncDialog"
|
||||
title="Create new sync"
|
||||
@fully-closed="step = 0"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<div v-if="step === 0" class="space-y-2">
|
||||
<ProjectPageAccHubs
|
||||
:hubs="hubs"
|
||||
:loading="loadingHubs"
|
||||
@hub-selected="onHubClick"
|
||||
/>
|
||||
|
||||
<ProjectPageAccProjects
|
||||
v-if="selectedHubId"
|
||||
:hub-id="selectedHubId"
|
||||
:projects="projects"
|
||||
:loading="loadingProjects"
|
||||
@project-selected="onProjectClick"
|
||||
/>
|
||||
|
||||
<ProjectPageAccFileSelector
|
||||
v-if="selectedProjectId && selectedHubId && tokens"
|
||||
:hub-id="selectedHubId"
|
||||
:project-id="selectedProjectId"
|
||||
:tokens="tokens"
|
||||
@file-selected="onFileSelected"
|
||||
/>
|
||||
|
||||
<FormTextInput
|
||||
v-model="revitViewName"
|
||||
name="revitFileViewName"
|
||||
color="foundation"
|
||||
label="Revit view name (Optional)"
|
||||
show-label
|
||||
:disabled="!selectedFile"
|
||||
></FormTextInput>
|
||||
|
||||
<div class="flex flex-row justify-center mt-4 space-x-2">
|
||||
<FormButton size="sm" color="outline" @click="showNewSyncDialog = false">
|
||||
Cancel
|
||||
</FormButton>
|
||||
<FormButton size="sm" :disabled="!selectedFile" @click="step++">
|
||||
Next
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="step === 1" class="flex flex-col space-y-2">
|
||||
<CommonAlert color="info" hide-icon>
|
||||
<template #title>
|
||||
Selected ACC file:
|
||||
{{
|
||||
selectedFile?.attributes.name || selectedFile?.attributes.displayName
|
||||
}}
|
||||
</template>
|
||||
</CommonAlert>
|
||||
<hr />
|
||||
<ProjectPageAccModelSelector
|
||||
:project-id="projectId"
|
||||
:acc-sync-items="accSyncItems"
|
||||
@model-selected="(model) => (selectedModel = model)"
|
||||
/>
|
||||
<hr />
|
||||
<div class="flex flex-row justify-center space-x-2">
|
||||
<FormButton size="sm" color="outline" @click="showNewSyncDialog = false">
|
||||
Cancel
|
||||
</FormButton>
|
||||
<FormButton size="sm" :disabled="!selectedModel" @click="addSync">
|
||||
Add
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccTokens, AccHub, AccItem } from '@speckle/shared/acc'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { ProjectPageLatestItemsModelItemFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { useMutation, useQuery, useSubscription } from '@vue/apollo-composable'
|
||||
import {
|
||||
accSyncItemCreateMutation,
|
||||
accSyncItemDeleteMutation,
|
||||
accSyncItemUpdateMutation
|
||||
} from '~/lib/acc/graphql/mutations'
|
||||
import { projectAccSyncItemsQuery } from '~/lib/acc/graphql/queries'
|
||||
import { onProjectAccSyncItemUpdatedSubscription } from '~/lib/acc/graphql/subscriptions'
|
||||
import { PauseIcon } from '@heroicons/vue/24/solid'
|
||||
import { TrashIcon, PlayIcon } from '@heroicons/vue/24/outline'
|
||||
|
||||
import type { AccFolder } from '~/lib/acc/composables/useAcc'
|
||||
import { useAcc } from '~/lib/acc/composables/useAcc'
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string
|
||||
tokens: AccTokens | undefined
|
||||
isLoggedIn: boolean
|
||||
}>()
|
||||
|
||||
const step = ref(0)
|
||||
const showNewSyncDialog = ref(false)
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const tokens = computed(() => props.tokens)
|
||||
|
||||
const selectedHub = ref<AccHub | null>(null)
|
||||
const selectedHubId = ref<string | null>(null)
|
||||
const selectedProjectId = ref<string | null>(null)
|
||||
|
||||
const revitViewName = ref<string>()
|
||||
const selectedModel = ref<ProjectPageLatestItemsModelItemFragment>()
|
||||
|
||||
const selectedFolder = ref<AccFolder | undefined>()
|
||||
const selectedFile = ref<AccItem | undefined>()
|
||||
|
||||
// Use the composable to get the state and functions
|
||||
const {
|
||||
hubs,
|
||||
fetchHubs,
|
||||
loadingHubs,
|
||||
projects,
|
||||
fetchProjects,
|
||||
loadingProjects,
|
||||
folderTree,
|
||||
fetchItemsForFolder,
|
||||
rootProjectFolderId,
|
||||
init
|
||||
} = useAcc()
|
||||
|
||||
const onFileSelected = (item: AccItem) => {
|
||||
selectedFile.value = item
|
||||
}
|
||||
|
||||
const { result: accSyncItemsResult, refetch: refetchAccSyncItems } = useQuery(
|
||||
projectAccSyncItemsQuery,
|
||||
() => ({
|
||||
id: props.projectId
|
||||
})
|
||||
)
|
||||
|
||||
const accSyncItems = computed(
|
||||
() => accSyncItemsResult.value?.project.accSyncItems.items || []
|
||||
)
|
||||
|
||||
const { onResult: onProjectAccSyncItemsUpdated } = useSubscription(
|
||||
onProjectAccSyncItemUpdatedSubscription,
|
||||
() => ({
|
||||
id: props.projectId
|
||||
})
|
||||
)
|
||||
|
||||
onProjectAccSyncItemsUpdated((res) => {
|
||||
// TODO ACC: Mutate local cache instead of refetch
|
||||
|
||||
refetchAccSyncItems()
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Info,
|
||||
title: `ACC sync model ${res.data?.projectAccSyncItemsUpdated.type.toLowerCase()}`,
|
||||
description: res.data?.projectAccSyncItemsUpdated.accSyncItem?.accFileName
|
||||
})
|
||||
})
|
||||
|
||||
const onHubClick = async (hub: AccHub) => {
|
||||
selectedHub.value = hub
|
||||
selectedHubId.value = hub.id
|
||||
await fetchProjects(hub.id, tokens.value!.access_token)
|
||||
}
|
||||
|
||||
// Refactored onProjectClick to use the composable's init function
|
||||
const onProjectClick = async (hubId: string, projectId: string) => {
|
||||
selectedFolder.value = undefined
|
||||
selectedFile.value = undefined
|
||||
selectedProjectId.value = projectId
|
||||
await init(hubId, projectId, tokens.value!.access_token)
|
||||
|
||||
// defaulting to first
|
||||
if (folderTree.value && folderTree.value.children) {
|
||||
selectedFolder.value = folderTree.value?.children[0]
|
||||
await fetchItemsForFolder(
|
||||
selectedFolder.value.id,
|
||||
selectedProjectId.value,
|
||||
tokens.value!.access_token
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const { mutate: createAccSyncItem } = useMutation(accSyncItemCreateMutation)
|
||||
|
||||
const addSync = async () => {
|
||||
try {
|
||||
// annoying but looks like ACC does not give the exact version number directly
|
||||
const fileVersion = Number(
|
||||
new URLSearchParams(selectedFile.value?.latestVersionId?.split('?')[1]).get(
|
||||
'version'
|
||||
)
|
||||
)
|
||||
|
||||
const accFileViewName = revitViewName.value === '' ? undefined : revitViewName.value
|
||||
|
||||
await createAccSyncItem({
|
||||
input: {
|
||||
projectId: props.projectId,
|
||||
modelId: selectedModel.value?.id as string,
|
||||
accRegion: selectedHub.value?.attributes?.region as string,
|
||||
accFileExtension: selectedFile.value?.fileExtension as string,
|
||||
accHubId: selectedHubId.value!,
|
||||
accProjectId: selectedProjectId.value as string,
|
||||
accRootProjectFolderUrn: rootProjectFolderId.value!,
|
||||
accFileLineageUrn: selectedFile.value?.id as string,
|
||||
accFileName: (selectedFile.value?.attributes.displayName ||
|
||||
selectedFile.value?.attributes.name) as string,
|
||||
accFileVersionIndex: fileVersion,
|
||||
accFileVersionUrn: selectedFile.value?.latestVersionId as string,
|
||||
accFileViewName
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Add sync item failed',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
} finally {
|
||||
revitViewName.value = undefined
|
||||
showNewSyncDialog.value = false
|
||||
step.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const { mutate: deleteAccSyncItem } = useMutation(accSyncItemDeleteMutation)
|
||||
|
||||
const handleDeleteSyncItem = async (id: string) => {
|
||||
try {
|
||||
await deleteAccSyncItem({
|
||||
input: {
|
||||
projectId: props.projectId,
|
||||
id
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Delete sync item failed',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const { mutate: updateAccSyncItem } = useMutation(accSyncItemUpdateMutation)
|
||||
|
||||
const handleStatusSyncItem = async (id: string, isPaused: boolean) => {
|
||||
try {
|
||||
await updateAccSyncItem({
|
||||
input: {
|
||||
projectId: props.projectId,
|
||||
id,
|
||||
status: isPaused ? 'pending' : 'paused'
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Update sync item failed',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
watch(tokens, (newTokens) => {
|
||||
if (newTokens?.access_token) {
|
||||
fetchHubs(newTokens?.access_token)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,152 +0,0 @@
|
||||
<template>
|
||||
<div class="flex flex-col text-xs space-y-2">
|
||||
<ProjectPageAccSyncs
|
||||
:project-id="projectId"
|
||||
:is-logged-in="hasTokens"
|
||||
:tokens="tokens"
|
||||
/>
|
||||
|
||||
<ClientOnly>
|
||||
<div v-if="!hasTokens">
|
||||
<CommonLoadingBar v-if="loadingTokens" :loading="true" class="my-2" />
|
||||
<div v-else>
|
||||
<hr class="mb-2" />
|
||||
<FormButton size="sm" @click="authAcc()">Connect to ACC</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
|
||||
<!-- USER INFO -->
|
||||
<div v-if="userInfo" class="flex flex-col space-y-2">
|
||||
<hr class="my-2" />
|
||||
<div class="flex flex-col text ml-1 space-y-2 mb-2">
|
||||
<span>
|
||||
<strong>Name:</strong>
|
||||
{{ userInfo.firstName }} {{ userInfo.lastName }}
|
||||
</span>
|
||||
<span>
|
||||
<strong>Email:</strong>
|
||||
{{ userInfo.emailId }}
|
||||
</span>
|
||||
<span>
|
||||
<strong>User ID:</strong>
|
||||
{{ userInfo.userId }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- <div v-if="tokens?.access_token" class="flex flex-row items-center space-x-2">
|
||||
<FormButton
|
||||
class="mr-2"
|
||||
hide-text
|
||||
:icon-left="DocumentDuplicateIcon"
|
||||
color="outline"
|
||||
@click="copy(tokens?.access_token)"
|
||||
>
|
||||
Copy to clipboard
|
||||
</FormButton>
|
||||
{{ tokens?.access_token.slice(0, 32) }}...
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { AccTokens, AccUserInfo } from '@speckle/shared/acc'
|
||||
// import { DocumentDuplicateIcon } from '@heroicons/vue/24/outline'
|
||||
|
||||
const props = defineProps<{ projectId: string }>()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
// const { copy } = useClipboard()
|
||||
|
||||
const apiOrigin = useApiOrigin()
|
||||
const tokens = ref<AccTokens>()
|
||||
const hasTokens = computed(() => !!tokens.value?.access_token)
|
||||
const loadingTokens = ref(true)
|
||||
const userInfo = ref<AccUserInfo>()
|
||||
const loadingUser = ref(false)
|
||||
|
||||
// AUTH + TOKEN FLOW
|
||||
const fetchTokens = async () => {
|
||||
try {
|
||||
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/status`, {
|
||||
credentials: 'include'
|
||||
})
|
||||
if (!res.ok) return
|
||||
tokens.value = await res.json()
|
||||
} finally {
|
||||
loadingTokens.value = false
|
||||
}
|
||||
}
|
||||
fetchTokens()
|
||||
|
||||
const authAcc = async () => {
|
||||
try {
|
||||
const response = await fetch(`${apiOrigin}/api/v1/acc/auth/login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ projectId: props.projectId })
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to initiate ACC login.')
|
||||
const { authorizeUrl } = await response.json()
|
||||
if (!authorizeUrl) throw new Error('No authorize URL returned by server.')
|
||||
window.location.href = authorizeUrl
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Error starting ACC login',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleRefresh = (expiresInSeconds: number) => {
|
||||
const refreshTime = (expiresInSeconds - 60) * 1000
|
||||
setTimeout(async () => {
|
||||
loadingTokens.value = true
|
||||
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include'
|
||||
})
|
||||
if (res.ok) {
|
||||
const refreshed = await res.json()
|
||||
await fetchTokens()
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
title: 'ACC tokens refreshed',
|
||||
description: refreshed
|
||||
})
|
||||
scheduleRefresh(refreshed.expires_in)
|
||||
}
|
||||
loadingTokens.value = false
|
||||
}, refreshTime)
|
||||
}
|
||||
|
||||
watch(tokens, (newTokens) => {
|
||||
if (newTokens?.expires_in) scheduleRefresh(newTokens.expires_in)
|
||||
if (newTokens?.access_token) {
|
||||
fetchUserInfo()
|
||||
}
|
||||
})
|
||||
|
||||
// USER INFO
|
||||
const fetchUserInfo = async () => {
|
||||
loadingUser.value = true
|
||||
try {
|
||||
const res = await fetch(
|
||||
'https://developer.api.autodesk.com/userprofile/v1/users/@me',
|
||||
{ headers: { Authorization: `Bearer ${tokens.value!.access_token}` } }
|
||||
)
|
||||
if (!res.ok) throw new Error('Failed to get user info directly from ACC')
|
||||
userInfo.value = await res.json()
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Error fetching user info directly',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
} finally {
|
||||
loadingUser.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -26,6 +26,18 @@
|
||||
New model
|
||||
</FormButton>
|
||||
</div>
|
||||
<!-- I believe for now sync limits corralate with model limit since new sync creates new model, once we have limits for syncs, this should change -->
|
||||
<div
|
||||
v-tippy="canCreateModel.cantClickCreateReason.value"
|
||||
class="grow inline-flex sm:grow-0 lg:hidden"
|
||||
>
|
||||
<FormButton
|
||||
:disabled="!canCreateModel.canClickCreate.value"
|
||||
@click="showNewAccSync = true"
|
||||
>
|
||||
New sync
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -78,7 +90,23 @@
|
||||
>
|
||||
View all in 3D
|
||||
</FormButton>
|
||||
<div v-tippy="canCreateModel.cantClickCreateReason.value" class="test123">
|
||||
<LayoutMenu
|
||||
v-if="showAccIntegration"
|
||||
v-model:open="showMenu"
|
||||
:items="menuItems"
|
||||
:menu-position="HorizontalDirection.Left"
|
||||
:menu-id="menuId"
|
||||
@click.stop.prevent
|
||||
@chosen="onActionChosen"
|
||||
>
|
||||
<FormButton color="primary" @click="showMenu = !showMenu">
|
||||
<div class="flex items-center gap-1">
|
||||
Add model
|
||||
<ChevronDownIcon class="h-3 w-3" />
|
||||
</div>
|
||||
</FormButton>
|
||||
</LayoutMenu>
|
||||
<div v-else v-tippy="canCreateModel.cantClickCreateReason.value">
|
||||
<FormButton
|
||||
:disabled="!canCreateModel.canClickCreate.value"
|
||||
class="hidden lg:inline-flex shrink-0"
|
||||
@@ -91,9 +119,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<ProjectModelsAdd v-model:open="showNewDialog" :project="project" />
|
||||
<IntegrationsAccDialogCreateSync :open="showNewAccSync" :project-id="project?.id" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
|
||||
import { SourceApps, SpeckleViewer } from '@speckle/shared'
|
||||
import type { SourceAppDefinition } from '@speckle/shared'
|
||||
import { debounce } from 'lodash-es'
|
||||
@@ -103,9 +133,13 @@ import type {
|
||||
ProjectModelsPageHeader_ProjectFragment
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { modelRoute } from '~~/lib/common/helpers/route'
|
||||
import type { GridListToggleValue } from '~~/lib/layout/helpers/components'
|
||||
import type {
|
||||
GridListToggleValue,
|
||||
LayoutMenuItem
|
||||
} from '~~/lib/layout/helpers/components'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useCanCreateModel } from '~/lib/projects/composables/permissions'
|
||||
import { HorizontalDirection } from '@speckle/ui-components'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selected-members', val: FormUsersSelectItemFragment[]): void
|
||||
@@ -147,6 +181,9 @@ graphql(`
|
||||
canCreateModel {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canReadAccIntegrationSettings {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
...ProjectModelsAdd_Project
|
||||
}
|
||||
@@ -168,6 +205,8 @@ const sourceAppsBtnId = useId()
|
||||
const router = useRouter()
|
||||
const mp = useMixpanel()
|
||||
|
||||
const menuId = useId()
|
||||
|
||||
const onViewAllClick = () => {
|
||||
router.push(allModelsRoute.value)
|
||||
|
||||
@@ -180,6 +219,11 @@ const onViewAllClick = () => {
|
||||
}
|
||||
|
||||
const showNewDialog = ref(false)
|
||||
const showNewAccSync = ref(false)
|
||||
|
||||
const showAccIntegration = computed(
|
||||
() => props.project?.permissions.canReadAccIntegrationSettings.authorized
|
||||
)
|
||||
|
||||
const canCreateModel = useCanCreateModel({
|
||||
project: computed(() => props.project)
|
||||
@@ -217,6 +261,46 @@ const allModelsRoute = computed(() => {
|
||||
return modelRoute(props.projectId, resourceIdString)
|
||||
})
|
||||
|
||||
const showMenu = ref(false)
|
||||
|
||||
enum AddNewModelActionTypes {
|
||||
NewModel = 'new-model',
|
||||
NewAccSyncItem = 'new-acc-sync-item'
|
||||
}
|
||||
|
||||
const menuItems = computed<LayoutMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
title: 'Create new model...',
|
||||
id: AddNewModelActionTypes.NewModel,
|
||||
disabled: !canCreateModel.canClickCreate.value,
|
||||
disabledTooltip: canCreateModel.cantClickCreateReason.value
|
||||
},
|
||||
// TODO ACC: Upload a file
|
||||
{
|
||||
// TODO: Do we show this disabled in all non-enterprise cases?
|
||||
title: 'Sync from ACC...',
|
||||
id: AddNewModelActionTypes.NewAccSyncItem,
|
||||
// I believe for now sync limits corralate with model limit since new sync creates new model, once we have limits for syncs, this should change
|
||||
disabled: !canCreateModel.canClickCreate.value,
|
||||
disabledTooltip: canCreateModel.cantClickCreateReason.value
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) => {
|
||||
const { item } = params
|
||||
|
||||
switch (item.id) {
|
||||
case AddNewModelActionTypes.NewModel:
|
||||
handleCreateModelClick()
|
||||
break
|
||||
case AddNewModelActionTypes.NewAccSyncItem:
|
||||
showNewAccSync.value = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const team = computed(() => props.project?.team.map((t) => t.user) || [])
|
||||
|
||||
const updateDebouncedSearch = debounce(() => {
|
||||
|
||||
@@ -55,8 +55,12 @@
|
||||
submodel
|
||||
</FormButton>
|
||||
</div>
|
||||
<div v-if="accSyncItem" class="flex items-center ml-2">
|
||||
<IntegrationsAccSyncStatusModelItem :item="accSyncItem" />
|
||||
</div>
|
||||
<!-- Spacer -->
|
||||
<div class="flex-grow"></div>
|
||||
|
||||
<template v-if="!isPendingFileUpload(item)">
|
||||
<div
|
||||
v-show="
|
||||
@@ -308,6 +312,9 @@ graphql(`
|
||||
...ProjectPageLatestItemsModelItem
|
||||
...ProjectCardImportFileArea_Model
|
||||
...ProjectPageModelsCard_Model
|
||||
accSyncItem {
|
||||
...SyncStatusModelItem_AccSyncItem
|
||||
}
|
||||
}
|
||||
hasChildren
|
||||
updatedAt
|
||||
@@ -332,6 +339,10 @@ const props = defineProps<{
|
||||
const router = useRouter()
|
||||
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
||||
|
||||
const accSyncItem = computed(() =>
|
||||
props.item.__typename === 'ModelsTreeItem' ? props.item.model?.accSyncItem : undefined
|
||||
)
|
||||
|
||||
const importArea = ref(
|
||||
null as Nullable<{
|
||||
triggerPicker: () => void
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:disabled="loading"
|
||||
class="z-[1] relative"
|
||||
/>
|
||||
|
||||
<ProjectPageModelsResults
|
||||
v-model:grid-or-list="gridOrList"
|
||||
v-model:search="search"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div class="flex flex-col text-xs space-y-2">
|
||||
<IntegrationsAccSyncs
|
||||
:project-id="projectId"
|
||||
:is-logged-in="hasTokens"
|
||||
:tokens="tokens"
|
||||
/>
|
||||
<ClientOnly>
|
||||
<div v-if="!hasTokens">
|
||||
<CommonLoadingBar v-if="loadingTokens" :loading="true" class="my-2" />
|
||||
<div v-else>
|
||||
<hr class="mb-2" />
|
||||
<FormButton size="sm" @click="authAcc(`/projects/${projectId}/acc`)">
|
||||
Connect to ACC
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- USER INFO -->
|
||||
<div v-if="userInfo" class="flex flex-col space-y-2">
|
||||
<hr class="my-2" />
|
||||
<div class="flex flex-col text ml-1 space-y-2 mb-2">
|
||||
<span>
|
||||
<strong>Name:</strong>
|
||||
{{ userInfo.firstName }} {{ userInfo.lastName }}
|
||||
</span>
|
||||
<span>
|
||||
<strong>Email:</strong>
|
||||
{{ userInfo.emailId }}
|
||||
</span>
|
||||
<span>
|
||||
<strong>User ID:</strong>
|
||||
{{ userInfo.userId }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAccAuthManager } from '~/lib/acc/composables/useAccAuthManager'
|
||||
import { useAccUser } from '~/lib/acc/composables/useAccUser'
|
||||
|
||||
defineProps<{ projectId: string }>()
|
||||
|
||||
const hasTokens = computed(() => !!tokens.value?.access_token)
|
||||
|
||||
const { tokens, loadingTokens, authAcc, tryGetTokensFromCookies } = useAccAuthManager()
|
||||
const { userInfo, fetchUserInfo } = useAccUser()
|
||||
|
||||
watch(tokens, async (newTokens) => {
|
||||
if (newTokens?.access_token) {
|
||||
await fetchUserInfo(newTokens?.access_token)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await tryGetTokensFromCookies()
|
||||
if (tokens.value) {
|
||||
await fetchUserInfo(tokens.value?.access_token)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,272 +0,0 @@
|
||||
import type { AccHub, AccProject } from '@speckle/shared/acc'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// Placeholder types for demonstration. You should use your actual types.
|
||||
export interface AccItem {
|
||||
id: string
|
||||
type: string
|
||||
attributes: {
|
||||
name: string
|
||||
displayName: string
|
||||
fileType?: string
|
||||
objectCount?: number
|
||||
}
|
||||
latestVersionId?: string
|
||||
fileExtension?: string
|
||||
storageUrn?: string | null
|
||||
}
|
||||
|
||||
export interface AccFolder extends AccItem {
|
||||
children?: AccFolder[]
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable function to handle ACC data fetching and state management.
|
||||
* The project details are passed to the `init` function and exptects to refresh all state when user selected new project.
|
||||
*/
|
||||
export function useAcc() {
|
||||
const loadingTree = ref<boolean>(false)
|
||||
const loadingItems = ref<boolean>(false)
|
||||
const loadingHubs = ref<boolean>(false)
|
||||
const loadingProjects = ref<boolean>(false)
|
||||
|
||||
const folderTree = ref<AccFolder | undefined>()
|
||||
const folderItems = ref<AccItem[]>([])
|
||||
const hubs = ref<AccHub[]>([])
|
||||
const projects = ref<AccProject[]>([])
|
||||
|
||||
const rootProjectFolderId = ref<string | undefined>()
|
||||
const supportedFileExtensions = ['rvt']
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
// ACC API Functions
|
||||
|
||||
/**
|
||||
* Fetches all hubs for the authenticated user.
|
||||
*/
|
||||
const fetchHubs = async (token: string) => {
|
||||
loadingHubs.value = true
|
||||
try {
|
||||
const res = await fetch('https://developer.api.autodesk.com/project/v1/hubs', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
if (!res.ok) throw new Error('Failed to fetch hubs')
|
||||
hubs.value = (await res.json()).data
|
||||
} catch (error) {
|
||||
logger.error(error, 'Error fetching ACC hubs')
|
||||
hubs.value = []
|
||||
} finally {
|
||||
loadingHubs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all projects for a given hub.
|
||||
*/
|
||||
const fetchProjects = async (hubId: string, token: string) => {
|
||||
loadingProjects.value = true
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
if (!res.ok) throw new Error('Failed to fetch projects')
|
||||
projects.value = (await res.json()).data
|
||||
} catch (error) {
|
||||
logger.error(error, 'Error fetching ACC projects')
|
||||
projects.value = []
|
||||
} finally {
|
||||
loadingProjects.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the root folder ID for the project.
|
||||
*/
|
||||
const getProjectRootFolderId = async (
|
||||
hubId: string,
|
||||
projectId: string,
|
||||
token: string
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects/${projectId}`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
if (!res.ok) throw new Error('Failed to get project details')
|
||||
|
||||
const r = await res.json()
|
||||
rootProjectFolderId.value = r.data.relationships?.rootFolder?.data?.id || null
|
||||
return rootProjectFolderId.value
|
||||
} catch (error) {
|
||||
logger.error(error, `Error getting root folder ID for project: ${projectId}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the immediate contents (folders and items) of a single folder.
|
||||
* This is a non-recursive, single-level fetch.
|
||||
*/
|
||||
const fetchFolderContents = async (
|
||||
projectId: string,
|
||||
folderId: string,
|
||||
token: string
|
||||
): Promise<AccItem[]> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/folders/${folderId}/contents`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch contents of folder ${folderId}`)
|
||||
}
|
||||
const data = (await res.json()).data
|
||||
return data
|
||||
} catch (error) {
|
||||
logger.error(error, `Error fetching folder contents for ${folderId}:`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest version details for a specific item (file).
|
||||
* This function is separated for on-demand use.
|
||||
*/
|
||||
const fetchItemLatestVersion = async (
|
||||
projectId: string,
|
||||
itemId: string,
|
||||
token: string
|
||||
) => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/items/${itemId}/tip`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch latest version for item ${itemId}`)
|
||||
}
|
||||
const data = (await res.json()).data
|
||||
return data
|
||||
} catch (error) {
|
||||
logger.error(error, `Error fetching latest version for item ${itemId}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Application Logic
|
||||
|
||||
/**
|
||||
* Builds the nested folder tree structure on initial project load.
|
||||
* This is a recursive function that only fetches folders, not files.
|
||||
* It now uses `attributes.objectCount` to avoid unnecessary API calls for empty folders.
|
||||
*/
|
||||
const buildFolderTree = async (
|
||||
projectId: string,
|
||||
folderId: string,
|
||||
token: string
|
||||
): Promise<AccFolder> => {
|
||||
const contents = await fetchFolderContents(projectId, folderId, token)
|
||||
const folders = contents.filter((item) => item.type === 'folders') as AccFolder[]
|
||||
|
||||
const populatedFolders: AccFolder[] = []
|
||||
for (const folder of folders) {
|
||||
// We only want to add a folder to the tree if it contains something.
|
||||
// The `objectCount` attribute tells us if it's empty.
|
||||
if (folder.attributes.objectCount && folder.attributes.objectCount > 0) {
|
||||
// Recursively build the full subtree for this folder
|
||||
const subTree = await buildFolderTree(projectId, folder.id, token)
|
||||
populatedFolders.push({
|
||||
id: folder.id,
|
||||
type: folder.type,
|
||||
attributes: {
|
||||
name: folder.attributes.name || folder.attributes.displayName,
|
||||
displayName: folder.attributes.displayName
|
||||
},
|
||||
children: subTree.children
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const folderTree = {
|
||||
id: folderId,
|
||||
type: 'folders',
|
||||
attributes: { name: 'Root Folder', displayName: 'Root Folder' },
|
||||
relationships: {},
|
||||
children: populatedFolders
|
||||
} as AccFolder
|
||||
|
||||
return folderTree
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all items (files) for a specific folder when a user clicks on it.
|
||||
*/
|
||||
const fetchItemsForFolder = async (
|
||||
folderId: string,
|
||||
projectId: string,
|
||||
token: string
|
||||
) => {
|
||||
loadingItems.value = true
|
||||
folderItems.value = [] // Clear previous items
|
||||
|
||||
const contents = await fetchFolderContents(projectId, folderId, token)
|
||||
const items = contents.filter((item) => item.type === 'items') as AccItem[] // items === files
|
||||
|
||||
const itemPromises = items.map(async (item) => {
|
||||
const version = await fetchItemLatestVersion(projectId, item.id, token)
|
||||
if (version) {
|
||||
const storageUrn = version.relationships?.storage?.data?.id || null
|
||||
return {
|
||||
...item,
|
||||
latestVersionId: version.id,
|
||||
fileExtension: version.attributes.fileType,
|
||||
storageUrn
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
|
||||
folderItems.value = (await Promise.all(itemPromises)).filter((item) =>
|
||||
supportedFileExtensions.includes(item.fileExtension)
|
||||
)
|
||||
loadingItems.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point to initialize the folder tree for the selected project.
|
||||
*/
|
||||
const init = async (hubId: string, projectId: string, token: string) => {
|
||||
loadingTree.value = true
|
||||
folderItems.value = []
|
||||
folderTree.value = undefined
|
||||
rootProjectFolderId.value = undefined
|
||||
try {
|
||||
const rootFolderId = await getProjectRootFolderId(hubId, projectId, token)
|
||||
if (rootFolderId) {
|
||||
folderTree.value = await buildFolderTree(projectId, rootFolderId, token)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error, 'Failed to initialize Autodesk ACC composable')
|
||||
} finally {
|
||||
loadingTree.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadingTree,
|
||||
loadingItems,
|
||||
loadingHubs,
|
||||
loadingProjects,
|
||||
folderTree,
|
||||
folderItems,
|
||||
hubs,
|
||||
projects,
|
||||
rootProjectFolderId,
|
||||
fetchHubs,
|
||||
fetchProjects,
|
||||
fetchItemsForFolder,
|
||||
init
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
/**
|
||||
* Manages authentication logic of ACC.
|
||||
* We store tokens and its timestamp in under `acc_tokens` cookie.
|
||||
* Detection of "Refresh needed" happens with timestamp check.
|
||||
* ACC auth logic returns only expires in seconds and we need to correlate it with timestamp to substract later to understand refresh needed or not.
|
||||
* Token lifespans:
|
||||
* - Bearer token: 60 minutes
|
||||
* - Refresh token: 15 days
|
||||
*/
|
||||
export function useAccAuthManager() {
|
||||
const ACC_COOKIE_KEY = 'acc_tokens'
|
||||
const logger = useLogger()
|
||||
const apiOrigin = useApiOrigin()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const loadingTokens = ref(false)
|
||||
const tokens = ref<AccTokens>()
|
||||
const isExpired = ref(false)
|
||||
const REFRESH_TOKEN_LIFESPAN = 15 * 24 * 60 * 60 // in seconds
|
||||
|
||||
/**
|
||||
* Main logic to understand existing token in cookies is expired or not.
|
||||
* If refresh needed, we refresh and schedule
|
||||
* Otherwise, we calculate the time diff and schedule refresh accordingly
|
||||
*/
|
||||
const tryGetTokensFromCookies = async () => {
|
||||
const accTokens = Cookies.get(ACC_COOKIE_KEY)
|
||||
if (accTokens) {
|
||||
logger.info('Acc tokens are found in cookies')
|
||||
const tokensInCookies = JSON.parse(accTokens) as AccTokens
|
||||
const timeDiff = (Date.now() - tokensInCookies.timestamp) / 1000 // in seconds
|
||||
|
||||
if (timeDiff > REFRESH_TOKEN_LIFESPAN) {
|
||||
logger.info('Acc refresh token in cookies is expired')
|
||||
isExpired.value = true
|
||||
logOut()
|
||||
} else if (timeDiff + 300 > tokensInCookies.expires_in) {
|
||||
logger.info('Acc access token in cookies need refreshing')
|
||||
// 300s (6min) is arbitrary guard
|
||||
const refreshedTokens = await refreshTokens(tokensInCookies)
|
||||
tokens.value = refreshedTokens
|
||||
await saveTokensToCookies()
|
||||
if (tokens.value) scheduleRefresh(tokens.value)
|
||||
} else {
|
||||
logger.info('Acc tokens in cookies still valid')
|
||||
tokens.value = tokensInCookies
|
||||
const remainingTime = tokensInCookies.expires_in - timeDiff
|
||||
scheduleRefresh(tokens.value, remainingTime)
|
||||
}
|
||||
}
|
||||
loadingTokens.value = false
|
||||
}
|
||||
|
||||
const logOut = () => {
|
||||
tokens.value = undefined
|
||||
Cookies.remove(ACC_COOKIE_KEY)
|
||||
}
|
||||
|
||||
const refreshTokens = async (tokensToRefresh: AccTokens) => {
|
||||
try {
|
||||
loadingTokens.value = true
|
||||
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(tokensToRefresh)
|
||||
})
|
||||
if (res.ok) {
|
||||
const refreshedTokens = await res.json()
|
||||
tokens.value = { ...refreshedTokens, timestamp: Date.now() }
|
||||
return refreshedTokens
|
||||
}
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Error on refreshing ACC credientials',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
} finally {
|
||||
loadingTokens.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const saveTokensToCookies = async () => {
|
||||
const tokensWithTimestamp = { ...tokens.value, timestamp: Date.now() }
|
||||
Cookies.set('acc_tokens', JSON.stringify(tokensWithTimestamp), {
|
||||
expires: 30, // since acc refresh token lifespan 15 days, it is a safe expiration
|
||||
secure: true,
|
||||
sameSite: 'Strict'
|
||||
})
|
||||
isExpired.value = false
|
||||
}
|
||||
|
||||
const fetchTokens = async () => {
|
||||
try {
|
||||
loadingTokens.value = true
|
||||
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/status`, {
|
||||
credentials: 'include'
|
||||
})
|
||||
if (!res.ok) return
|
||||
tokens.value = await res.json()
|
||||
if (tokens.value?.expires_in) {
|
||||
scheduleRefresh(tokens.value)
|
||||
}
|
||||
await saveTokensToCookies()
|
||||
} finally {
|
||||
loadingTokens.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const authAcc = async (callbackEndpoint: string) => {
|
||||
try {
|
||||
const response = await fetch(`${apiOrigin}/api/v1/acc/auth/login`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ callbackEndpoint })
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to initiate ACC login.')
|
||||
const { authorizeUrl } = await response.json()
|
||||
if (!authorizeUrl) throw new Error('No authorize URL returned by server.')
|
||||
window.location.href = authorizeUrl
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Error starting ACC login',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleRefresh = (
|
||||
tokensToScheduleRefresh: AccTokens,
|
||||
resfreshInSeconds?: number
|
||||
) => {
|
||||
const refreshTimeInMs =
|
||||
(resfreshInSeconds ?? tokensToScheduleRefresh.expires_in) * 1000
|
||||
setTimeout(async () => {
|
||||
loadingTokens.value = true
|
||||
const res = await fetch(`${apiOrigin}/api/v1/acc/auth/refresh`, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(tokensToScheduleRefresh)
|
||||
})
|
||||
if (res.ok) {
|
||||
const refreshed = await res.json()
|
||||
tokens.value = refreshed
|
||||
// triggerNotification({
|
||||
// type: ToastNotificationType.Success,
|
||||
// title: 'ACC tokens refreshed'
|
||||
// })
|
||||
await saveTokensToCookies()
|
||||
scheduleRefresh(refreshed, refreshed.expires_in)
|
||||
}
|
||||
loadingTokens.value = false
|
||||
}, refreshTimeInMs)
|
||||
}
|
||||
|
||||
return {
|
||||
isExpired,
|
||||
tokens,
|
||||
loadingTokens,
|
||||
authAcc,
|
||||
logOut,
|
||||
fetchTokens,
|
||||
refreshTokens,
|
||||
tryGetTokensFromCookies,
|
||||
saveTokensToCookies
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
import type { AccHub, AccProject } from '@speckle/shared/acc'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// Placeholder types for demonstration. You should use your actual types.
|
||||
export interface AccItem {
|
||||
id: string
|
||||
type: string
|
||||
attributes: {
|
||||
name: string
|
||||
displayName: string
|
||||
fileType?: string
|
||||
objectCount?: number
|
||||
}
|
||||
latestVersionId?: string
|
||||
fileExtension?: string
|
||||
storageUrn?: string | null
|
||||
}
|
||||
|
||||
export interface AccFolder extends AccItem {
|
||||
children?: AccFolder[]
|
||||
}
|
||||
|
||||
export type AccItemVersion = {
|
||||
id: string
|
||||
name: string
|
||||
fileType?: string
|
||||
versionNumber: number
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable function to handle ACC data fetching and state management.
|
||||
* The project details are passed to the `init` function and exptects to refresh all state when user selected new project.
|
||||
*/
|
||||
export function useAcc() {
|
||||
const loadingTree = ref<boolean>(false)
|
||||
const loadingItems = ref<boolean>(false)
|
||||
const loadingHubs = ref<boolean>(false)
|
||||
const loadingProjects = ref<boolean>(false)
|
||||
|
||||
const folderTree = ref<AccFolder | undefined>()
|
||||
const folderItems = ref<AccItem[]>([])
|
||||
const hubs = ref<AccHub[]>([])
|
||||
const projects = ref<AccProject[]>([])
|
||||
|
||||
const rootProjectFolderId = ref<string | undefined>()
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
// ACC API Functions
|
||||
|
||||
/**
|
||||
* Fetches all hubs for the authenticated user.
|
||||
*/
|
||||
const fetchHubs = async (token: string) => {
|
||||
loadingHubs.value = true
|
||||
try {
|
||||
const res = await fetch('https://developer.api.autodesk.com/project/v1/hubs', {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
if (!res.ok) throw new Error('Failed to fetch hubs')
|
||||
hubs.value = (await res.json()).data
|
||||
} catch (error) {
|
||||
logger.error(error, 'Error fetching ACC hubs')
|
||||
hubs.value = []
|
||||
} finally {
|
||||
loadingHubs.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// type HubsResponse = {
|
||||
// data: {
|
||||
// id: string
|
||||
// type: 'hubs'
|
||||
// attributes: {
|
||||
// name: string
|
||||
// region: string
|
||||
// }
|
||||
// }[]
|
||||
// }
|
||||
|
||||
/**
|
||||
* Fetches all projects for a given hub.
|
||||
*/
|
||||
const fetchProjects = async (hubId: string, token: string) => {
|
||||
loadingProjects.value = true
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
if (!res.ok) throw new Error('Failed to fetch projects')
|
||||
projects.value = (await res.json()).data
|
||||
} catch (error) {
|
||||
logger.error(error, 'Error fetching ACC projects')
|
||||
projects.value = []
|
||||
} finally {
|
||||
loadingProjects.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// type ProjectsResponse = {
|
||||
// data: {
|
||||
// id: string
|
||||
// type: 'projects'
|
||||
// attributes: {
|
||||
// name: string
|
||||
// }
|
||||
// relationships: {
|
||||
// hub: {
|
||||
// data: {
|
||||
// id: string
|
||||
// type: string
|
||||
// }
|
||||
// }
|
||||
// rootFolder: {
|
||||
// data: {
|
||||
// id: string
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }[]
|
||||
// }
|
||||
|
||||
/**
|
||||
* Fetches the root folder ID for the project.
|
||||
*/
|
||||
const getProjectRootFolderId = async (
|
||||
hubId: string,
|
||||
projectId: string,
|
||||
token: string
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://developer.api.autodesk.com/project/v1/hubs/${hubId}/projects/${projectId}`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
if (!res.ok) throw new Error('Failed to get project details')
|
||||
|
||||
const r = await res.json()
|
||||
rootProjectFolderId.value = r.data.relationships?.rootFolder?.data?.id || null
|
||||
return rootProjectFolderId.value
|
||||
} catch (error) {
|
||||
logger.error(error, `Error getting root folder ID for project: ${projectId}`)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point to initialize the folder tree for the selected project.
|
||||
*/
|
||||
const init = async (hubId: string, projectId: string, token: string) => {
|
||||
loadingTree.value = true
|
||||
folderItems.value = []
|
||||
folderTree.value = undefined
|
||||
rootProjectFolderId.value = undefined
|
||||
try {
|
||||
await getProjectRootFolderId(hubId, projectId, token)
|
||||
} catch (error) {
|
||||
logger.error(error, 'Failed to initialize Autodesk ACC composable')
|
||||
} finally {
|
||||
loadingTree.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadingTree,
|
||||
loadingItems,
|
||||
loadingHubs,
|
||||
loadingProjects,
|
||||
folderTree,
|
||||
folderItems,
|
||||
hubs,
|
||||
projects,
|
||||
rootProjectFolderId,
|
||||
fetchHubs,
|
||||
fetchProjects,
|
||||
init
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { gql } from '@apollo/client/core'
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import { useApolloClient, useQuery } from '@vue/apollo-composable'
|
||||
import { accFolderDataQuery } from '~/lib/acc/graphql/queries'
|
||||
import type { AccIntegrationFolderNode_AccFolderFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { useActiveWorkspaceSlug } from '~/lib/user/composables/activeWorkspace'
|
||||
|
||||
export const useAccFolder = (
|
||||
accProjectId: string,
|
||||
accFolderId: MaybeRef<string | undefined>,
|
||||
accTokens?: AccTokens
|
||||
) => {
|
||||
const workspaceSlug = useActiveWorkspaceSlug()
|
||||
|
||||
const apollo = useApolloClient()
|
||||
|
||||
const cachedFolder = computed(() => {
|
||||
return apollo.client.cache.readFragment<AccIntegrationFolderNode_AccFolderFragment>(
|
||||
{
|
||||
id: `AccFolder:${unref(accFolderId)}`,
|
||||
fragment: gql`
|
||||
fragment AccFolderContents on AccFolder {
|
||||
id
|
||||
name
|
||||
contents {
|
||||
items {
|
||||
id
|
||||
name
|
||||
latestVersion {
|
||||
id
|
||||
name
|
||||
versionNumber
|
||||
fileType
|
||||
}
|
||||
}
|
||||
}
|
||||
children {
|
||||
items {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// watch(cachedFolder, (v) => {
|
||||
// console.log({ cachedFolder: v })
|
||||
// }, {
|
||||
// immediate: true
|
||||
// })
|
||||
|
||||
const { result: folder } = useQuery(
|
||||
accFolderDataQuery,
|
||||
() => ({
|
||||
workspaceSlug: workspaceSlug.value!,
|
||||
accToken: accTokens!.access_token,
|
||||
accProjectId,
|
||||
accFolderId: unref(accFolderId)!
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!unref(accFolderId) && !!accTokens && !!workspaceSlug.value
|
||||
})
|
||||
)
|
||||
|
||||
// watch(folder, (v) => {
|
||||
// console.log({ queryFolder: v })
|
||||
// }, {
|
||||
// immediate: true
|
||||
// })
|
||||
|
||||
const folderData = computed(() => ({
|
||||
id: accFolderId,
|
||||
...cachedFolder.value,
|
||||
...folder.value?.workspaceBySlug.integrations?.acc?.folder
|
||||
}))
|
||||
|
||||
return folderData
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { AccUserInfo } from '@speckle/shared/acc'
|
||||
|
||||
export function useAccUser() {
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const loadingUser = ref(false)
|
||||
const userInfo = ref<AccUserInfo>()
|
||||
|
||||
const fetchUserInfo = async (token: string) => {
|
||||
loadingUser.value = true
|
||||
try {
|
||||
const res = await fetch(
|
||||
'https://developer.api.autodesk.com/userprofile/v1/users/@me',
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
if (!res.ok) throw new Error('Failed to get user info directly from ACC')
|
||||
userInfo.value = await res.json()
|
||||
} catch (error) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Error fetching user info directly',
|
||||
description: error instanceof Error ? error.message : 'Unexpected error'
|
||||
})
|
||||
} finally {
|
||||
loadingUser.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loadingUser,
|
||||
userInfo,
|
||||
fetchUserInfo
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,12 @@ import { graphql } from '~~/lib/common/generated/gql'
|
||||
export const projectAccSyncItemFragment = graphql(`
|
||||
fragment ProjectAccSyncItem on AccSyncItem {
|
||||
id
|
||||
projectId
|
||||
modelId
|
||||
project {
|
||||
id
|
||||
}
|
||||
model {
|
||||
id
|
||||
}
|
||||
accRegion
|
||||
accHubId
|
||||
accProjectId
|
||||
|
||||
@@ -11,3 +11,24 @@ export const projectAccSyncItemsQuery = graphql(`
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const accFolderDataQuery = graphql(`
|
||||
query AccFolderData(
|
||||
$workspaceSlug: String!
|
||||
$accToken: String!
|
||||
$accProjectId: String!
|
||||
$accFolderId: String!
|
||||
) {
|
||||
workspaceBySlug(slug: $workspaceSlug) {
|
||||
id
|
||||
integrations {
|
||||
acc(token: $accToken) {
|
||||
folder(projectId: $accProjectId, folderId: $accFolderId) {
|
||||
id
|
||||
...AccIntegrationFolderNode_AccFolder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
import type { AccHub, AccItem } from '@speckle/shared/acc'
|
||||
import type { Integration } from '~/lib/integrations/types'
|
||||
import accLogo from '~/assets/images/integrations/acc.png'
|
||||
|
||||
export const AccIntegration: Integration = {
|
||||
cookieKey: 'acc_tokens',
|
||||
name: 'Autodesk Construction Cloud',
|
||||
description: 'Sync your files in ACC into Speckle.',
|
||||
logo: accLogo,
|
||||
connected: false,
|
||||
enabled: false,
|
||||
status: 'notConnected'
|
||||
}
|
||||
|
||||
// TODO ACC: Replace with type information inferred from gql queries, if possible
|
||||
export type AccSyncItem = {
|
||||
|
||||
@@ -63,6 +63,8 @@ type Documents = {
|
||||
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc,
|
||||
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n workspaceSlug\n user {\n id\n }\n }\n": typeof types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
|
||||
"\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspace {\n id\n name\n }\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": typeof types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment AccIntegrationFolderNode_AccFolder on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n children {\n items {\n id\n name\n }\n }\n contents {\n items {\n id\n name\n }\n }\n }\n }\n }\n": typeof types.AccIntegrationFolderNode_AccFolderFragmentDoc,
|
||||
"\n fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {\n id\n status\n }\n": typeof types.SyncStatusModelItem_AccSyncItemFragmentDoc,
|
||||
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n slug\n domainBasedMembershipProtectionEnabled\n defaultSeatType\n domains {\n domain\n id\n }\n seats {\n editors {\n available\n }\n }\n ...InviteDialogSharedSelectUsers_Workspace\n ...WorkspacesPlan_Workspace\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n workspaceId\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n ...WorkspacesPlan_Workspace\n }\n ...InviteDialogProjectRow_Project\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc,
|
||||
"\n fragment InviteDialogProjectRow_Project on Project {\n id\n workspaceId\n workspace {\n id\n role\n }\n }\n": typeof types.InviteDialogProjectRow_ProjectFragmentDoc,
|
||||
@@ -115,10 +117,10 @@ type Documents = {
|
||||
"\n fragment ProjectPageModelsActions_Project on Project {\n id\n workspace {\n id\n slug\n }\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.ProjectPageModelsActions_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsCardProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsCard_Model on Model {\n id\n homeView {\n id\n resourceIds\n }\n lastUpload: uploads(input: { limit: 1, cursor: null }) {\n items {\n id\n updatedAt\n convertedStatus\n }\n }\n lastVersion: versions(limit: 1, cursor: null) {\n items {\n id\n createdAt\n }\n }\n }\n": typeof types.ProjectPageModelsCard_ModelFragmentDoc,
|
||||
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n": typeof types.ProjectModelsPageHeader_ProjectFragmentDoc,
|
||||
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n": typeof types.ProjectModelsPageHeader_ProjectFragmentDoc,
|
||||
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": typeof types.ProjectModelsPageResults_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n ...UseCanCreateModel_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
|
||||
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n }\n hasChildren\n updatedAt\n }\n": typeof types.SingleLevelModelTreeItemFragmentDoc,
|
||||
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n accSyncItem {\n ...SyncStatusModelItem_AccSyncItem\n }\n }\n hasChildren\n updatedAt\n }\n": typeof types.SingleLevelModelTreeItemFragmentDoc,
|
||||
"\n fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {\n id\n convertedStatus\n convertedMessage\n fileName\n fileSize\n convertedLastUpdate\n convertedVersionId\n uploadDate\n uploadComplete\n branchName\n ...UseFailedFileImportJobUtils_FileUpload\n }\n": typeof types.ProjectPageModelsUploadsDialog_FileUploadFragmentDoc,
|
||||
"\n query GetModelUploads(\n $projectId: String!\n $modelId: String!\n $input: GetModelUploadsInput!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n uploads(input: $input) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageModelsUploadsDialog_FileUpload\n }\n }\n }\n }\n }\n": typeof types.GetModelUploadsDocument,
|
||||
"\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": typeof types.ProjectPageModelsCardDeleteDialogFragmentDoc,
|
||||
@@ -224,11 +226,13 @@ type Documents = {
|
||||
"\n fragment WorkspaceSidebar_Workspace on Workspace {\n ...WorkspaceSidebarMembers_Workspace\n ...WorkspaceSidebarAbout_Workspace\n ...WorkspaceSidebarSecurity_Workspace\n id\n role\n slug\n domains {\n id\n }\n plan {\n name\n }\n }\n": typeof types.WorkspaceSidebar_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceWizard_Workspace on Workspace {\n creationState {\n completed\n state\n }\n name\n slug\n }\n": typeof types.WorkspaceWizard_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc,
|
||||
"\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n projectId\n modelId\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n": typeof types.ProjectAccSyncItemFragmentDoc,
|
||||
"\n fragment AccFolderContents on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n }\n }\n }\n ": typeof types.AccFolderContentsFragmentDoc,
|
||||
"\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n project {\n id\n }\n model {\n id\n }\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n": typeof types.ProjectAccSyncItemFragmentDoc,
|
||||
"\n mutation CreateAccSyncItem($input: CreateAccSyncItemInput!) {\n accSyncItemMutations {\n create(input: $input) {\n id\n accFileLineageUrn\n status\n }\n }\n }\n": typeof types.CreateAccSyncItemDocument,
|
||||
"\n mutation DeleteAccSyncItem($input: DeleteAccSyncItemInput!) {\n accSyncItemMutations {\n delete(input: $input)\n }\n }\n": typeof types.DeleteAccSyncItemDocument,
|
||||
"\n mutation UpdateAccSyncItem($input: UpdateAccSyncItemInput!) {\n accSyncItemMutations {\n update(input: $input) {\n id\n status\n }\n }\n }\n": typeof types.UpdateAccSyncItemDocument,
|
||||
"\n query ProjectAccSyncItems($id: String!) {\n project(id: $id) {\n accSyncItems {\n items {\n ...ProjectAccSyncItem\n }\n }\n }\n }\n": typeof types.ProjectAccSyncItemsDocument,
|
||||
"\n query AccFolderData(\n $workspaceSlug: String!\n $accToken: String!\n $accProjectId: String!\n $accFolderId: String!\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n integrations {\n acc(token: $accToken) {\n folder(projectId: $accProjectId, folderId: $accFolderId) {\n id\n ...AccIntegrationFolderNode_AccFolder\n }\n }\n }\n }\n }\n": typeof types.AccFolderDataDocument,
|
||||
"\n subscription OnProjectAccSyncItemUpdated($id: String!, $itemIds: [String!]) {\n projectAccSyncItemsUpdated(id: $id, itemIds: $itemIds) {\n type\n accSyncItem {\n ...ProjectAccSyncItem\n }\n }\n }\n": typeof types.OnProjectAccSyncItemUpdatedDocument,
|
||||
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n primary\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n ...ProjectsAdd_User\n }\n }\n": typeof types.ActiveUserMainMetadataDocument,
|
||||
"\n query ActiveUserProjectsToMove($filter: UserProjectsFilter) {\n activeUser {\n id\n projects(filter: $filter) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserProjectsToMoveDocument,
|
||||
@@ -508,6 +512,7 @@ type Documents = {
|
||||
"\n mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityAutoJoinEnabled\n }\n }\n }\n": typeof types.WorkspaceUpdateAutoJoinMutationDocument,
|
||||
"\n mutation WorkspaceUpdateDefaultSeatTypeMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n defaultSeatType\n }\n }\n }\n": typeof types.WorkspaceUpdateDefaultSeatTypeMutationDocument,
|
||||
"\n mutation WorkspaceUpdateExclusiveMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n isExclusive\n }\n }\n }\n": typeof types.WorkspaceUpdateExclusiveMutationDocument,
|
||||
"\n query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {\n workspace(id: $workspaceId) {\n hasAccessToFeature(featureName: $featureName)\n }\n }\n": typeof types.WorkspaceDocument,
|
||||
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n slug\n }\n activeUser {\n id\n activeWorkspace {\n id\n slug\n }\n }\n }\n": typeof types.WorkspaceAccessCheckDocument,
|
||||
"\n query WorkspacePageQuery(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspacePage_Workspace\n }\n }\n": typeof types.WorkspacePageQueryDocument,
|
||||
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceDashboardProjectList_ProjectCollection\n }\n }\n }\n": typeof types.WorkspaceProjectsQueryDocument,
|
||||
@@ -540,10 +545,10 @@ type Documents = {
|
||||
"\n fragment AutomateFunctionPage_AutomateFunction on AutomateFunction {\n id\n name\n description\n logo\n supportedSourceApps\n tags\n ...AutomateFunctionPageHeader_Function\n ...AutomateFunctionPageInfo_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n creator {\n id\n }\n }\n": typeof types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
|
||||
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n ...AutomateFunctionEditDialog_Workspace\n }\n }\n }\n }\n": typeof types.AutomateFunctionPageDocument,
|
||||
"\n query AutomateFunctionPageWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...AutomateFunctionPageHeader_Workspace\n }\n }\n": typeof types.AutomateFunctionPageWorkspaceDocument,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": typeof types.ProjectPageAutomationPage_AutomationFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": typeof types.ProjectPageAutomationPage_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": typeof types.SettingsServerProjects_ProjectCollectionFragmentDoc,
|
||||
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": typeof types.SettingsServerRegionsDocument,
|
||||
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n plan {\n status\n name\n }\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
|
||||
@@ -606,6 +611,8 @@ const documents: Documents = {
|
||||
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc,
|
||||
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n workspaceSlug\n user {\n id\n }\n }\n": types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
|
||||
"\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspace {\n id\n name\n }\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment AccIntegrationFolderNode_AccFolder on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n children {\n items {\n id\n name\n }\n }\n contents {\n items {\n id\n name\n }\n }\n }\n }\n }\n": types.AccIntegrationFolderNode_AccFolderFragmentDoc,
|
||||
"\n fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {\n id\n status\n }\n": types.SyncStatusModelItem_AccSyncItemFragmentDoc,
|
||||
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n slug\n domainBasedMembershipProtectionEnabled\n defaultSeatType\n domains {\n domain\n id\n }\n seats {\n editors {\n available\n }\n }\n ...InviteDialogSharedSelectUsers_Workspace\n ...WorkspacesPlan_Workspace\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
|
||||
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n workspaceId\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n ...WorkspacesPlan_Workspace\n }\n ...InviteDialogProjectRow_Project\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
|
||||
"\n fragment InviteDialogProjectRow_Project on Project {\n id\n workspaceId\n workspace {\n id\n role\n }\n }\n": types.InviteDialogProjectRow_ProjectFragmentDoc,
|
||||
@@ -658,10 +665,10 @@ const documents: Documents = {
|
||||
"\n fragment ProjectPageModelsActions_Project on Project {\n id\n workspace {\n id\n slug\n }\n ...ProjectsModelPageEmbed_Project\n }\n": types.ProjectPageModelsActions_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsCardProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsCard_Model on Model {\n id\n homeView {\n id\n resourceIds\n }\n lastUpload: uploads(input: { limit: 1, cursor: null }) {\n items {\n id\n updatedAt\n convertedStatus\n }\n }\n lastVersion: versions(limit: 1, cursor: null) {\n items {\n id\n createdAt\n }\n }\n }\n": types.ProjectPageModelsCard_ModelFragmentDoc,
|
||||
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n": types.ProjectModelsPageHeader_ProjectFragmentDoc,
|
||||
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n": types.ProjectModelsPageHeader_ProjectFragmentDoc,
|
||||
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": types.ProjectModelsPageResults_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n ...UseCanCreateModel_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
|
||||
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
|
||||
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n accSyncItem {\n ...SyncStatusModelItem_AccSyncItem\n }\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
|
||||
"\n fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {\n id\n convertedStatus\n convertedMessage\n fileName\n fileSize\n convertedLastUpdate\n convertedVersionId\n uploadDate\n uploadComplete\n branchName\n ...UseFailedFileImportJobUtils_FileUpload\n }\n": types.ProjectPageModelsUploadsDialog_FileUploadFragmentDoc,
|
||||
"\n query GetModelUploads(\n $projectId: String!\n $modelId: String!\n $input: GetModelUploadsInput!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n uploads(input: $input) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageModelsUploadsDialog_FileUpload\n }\n }\n }\n }\n }\n": types.GetModelUploadsDocument,
|
||||
"\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": types.ProjectPageModelsCardDeleteDialogFragmentDoc,
|
||||
@@ -767,11 +774,13 @@ const documents: Documents = {
|
||||
"\n fragment WorkspaceSidebar_Workspace on Workspace {\n ...WorkspaceSidebarMembers_Workspace\n ...WorkspaceSidebarAbout_Workspace\n ...WorkspaceSidebarSecurity_Workspace\n id\n role\n slug\n domains {\n id\n }\n plan {\n name\n }\n }\n": types.WorkspaceSidebar_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceWizard_Workspace on Workspace {\n creationState {\n completed\n state\n }\n name\n slug\n }\n": types.WorkspaceWizard_WorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc,
|
||||
"\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n projectId\n modelId\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n": types.ProjectAccSyncItemFragmentDoc,
|
||||
"\n fragment AccFolderContents on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n }\n }\n }\n ": types.AccFolderContentsFragmentDoc,
|
||||
"\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n project {\n id\n }\n model {\n id\n }\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n": types.ProjectAccSyncItemFragmentDoc,
|
||||
"\n mutation CreateAccSyncItem($input: CreateAccSyncItemInput!) {\n accSyncItemMutations {\n create(input: $input) {\n id\n accFileLineageUrn\n status\n }\n }\n }\n": types.CreateAccSyncItemDocument,
|
||||
"\n mutation DeleteAccSyncItem($input: DeleteAccSyncItemInput!) {\n accSyncItemMutations {\n delete(input: $input)\n }\n }\n": types.DeleteAccSyncItemDocument,
|
||||
"\n mutation UpdateAccSyncItem($input: UpdateAccSyncItemInput!) {\n accSyncItemMutations {\n update(input: $input) {\n id\n status\n }\n }\n }\n": types.UpdateAccSyncItemDocument,
|
||||
"\n query ProjectAccSyncItems($id: String!) {\n project(id: $id) {\n accSyncItems {\n items {\n ...ProjectAccSyncItem\n }\n }\n }\n }\n": types.ProjectAccSyncItemsDocument,
|
||||
"\n query AccFolderData(\n $workspaceSlug: String!\n $accToken: String!\n $accProjectId: String!\n $accFolderId: String!\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n integrations {\n acc(token: $accToken) {\n folder(projectId: $accProjectId, folderId: $accFolderId) {\n id\n ...AccIntegrationFolderNode_AccFolder\n }\n }\n }\n }\n }\n": types.AccFolderDataDocument,
|
||||
"\n subscription OnProjectAccSyncItemUpdated($id: String!, $itemIds: [String!]) {\n projectAccSyncItemsUpdated(id: $id, itemIds: $itemIds) {\n type\n accSyncItem {\n ...ProjectAccSyncItem\n }\n }\n }\n": types.OnProjectAccSyncItemUpdatedDocument,
|
||||
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n primary\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n ...ProjectsAdd_User\n }\n }\n": types.ActiveUserMainMetadataDocument,
|
||||
"\n query ActiveUserProjectsToMove($filter: UserProjectsFilter) {\n activeUser {\n id\n projects(filter: $filter) {\n totalCount\n }\n }\n }\n": types.ActiveUserProjectsToMoveDocument,
|
||||
@@ -1051,6 +1060,7 @@ const documents: Documents = {
|
||||
"\n mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityAutoJoinEnabled\n }\n }\n }\n": types.WorkspaceUpdateAutoJoinMutationDocument,
|
||||
"\n mutation WorkspaceUpdateDefaultSeatTypeMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n defaultSeatType\n }\n }\n }\n": types.WorkspaceUpdateDefaultSeatTypeMutationDocument,
|
||||
"\n mutation WorkspaceUpdateExclusiveMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n isExclusive\n }\n }\n }\n": types.WorkspaceUpdateExclusiveMutationDocument,
|
||||
"\n query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {\n workspace(id: $workspaceId) {\n hasAccessToFeature(featureName: $featureName)\n }\n }\n": types.WorkspaceDocument,
|
||||
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n slug\n }\n activeUser {\n id\n activeWorkspace {\n id\n slug\n }\n }\n }\n": types.WorkspaceAccessCheckDocument,
|
||||
"\n query WorkspacePageQuery(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspacePage_Workspace\n }\n }\n": types.WorkspacePageQueryDocument,
|
||||
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceDashboardProjectList_ProjectCollection\n }\n }\n }\n": types.WorkspaceProjectsQueryDocument,
|
||||
@@ -1083,10 +1093,10 @@ const documents: Documents = {
|
||||
"\n fragment AutomateFunctionPage_AutomateFunction on AutomateFunction {\n id\n name\n description\n logo\n supportedSourceApps\n tags\n ...AutomateFunctionPageHeader_Function\n ...AutomateFunctionPageInfo_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n creator {\n id\n }\n }\n": types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
|
||||
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n activeUser {\n workspaces {\n items {\n ...AutomateFunctionCreateDialog_Workspace\n ...AutomateFunctionEditDialog_Workspace\n }\n }\n }\n }\n": types.AutomateFunctionPageDocument,
|
||||
"\n query AutomateFunctionPageWorkspace($workspaceId: String!) {\n workspace(id: $workspaceId) {\n id\n ...AutomateFunctionPageHeader_Workspace\n }\n }\n": types.AutomateFunctionPageWorkspaceDocument,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": types.ProjectPageAutomationPage_AutomationFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": types.ProjectPageAutomationPage_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
|
||||
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
|
||||
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": types.SettingsServerRegionsDocument,
|
||||
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n plan {\n status\n name\n }\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
|
||||
@@ -1310,6 +1320,14 @@ export function graphql(source: "\n fragment HeaderNavNotificationsProjectInvit
|
||||
* 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 HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspace {\n id\n name\n }\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n"): (typeof documents)["\n fragment HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspace {\n id\n name\n }\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\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 AccIntegrationFolderNode_AccFolder on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n children {\n items {\n id\n name\n }\n }\n contents {\n items {\n id\n name\n }\n }\n }\n }\n }\n"): (typeof documents)["\n fragment AccIntegrationFolderNode_AccFolder on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n children {\n items {\n id\n name\n }\n }\n contents {\n items {\n id\n name\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 SyncStatusModelItem_AccSyncItem on AccSyncItem {\n id\n status\n }\n"): (typeof documents)["\n fragment SyncStatusModelItem_AccSyncItem on AccSyncItem {\n id\n status\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1521,7 +1539,7 @@ export function graphql(source: "\n fragment ProjectPageModelsCard_Model on Mod
|
||||
/**
|
||||
* 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 ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n"): (typeof documents)["\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n"): (typeof documents)["\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectModelsAdd_Project\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1533,7 +1551,7 @@ export function graphql(source: "\n fragment ProjectPageModelsStructureItem_Pro
|
||||
/**
|
||||
* 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 SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n }\n hasChildren\n updatedAt\n }\n"): (typeof documents)["\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n }\n hasChildren\n updatedAt\n }\n"];
|
||||
export function graphql(source: "\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n accSyncItem {\n ...SyncStatusModelItem_AccSyncItem\n }\n }\n hasChildren\n updatedAt\n }\n"): (typeof documents)["\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n ...ProjectPageModelsCard_Model\n accSyncItem {\n ...SyncStatusModelItem_AccSyncItem\n }\n }\n hasChildren\n updatedAt\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1957,7 +1975,11 @@ export function graphql(source: "\n fragment WorkspaceWizardStepRegion_ServerIn
|
||||
/**
|
||||
* 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 ProjectAccSyncItem on AccSyncItem {\n id\n projectId\n modelId\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n"): (typeof documents)["\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n projectId\n modelId\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment AccFolderContents on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\n name\n }\n }\n }\n "): (typeof documents)["\n fragment AccFolderContents on AccFolder {\n id\n name\n contents {\n items {\n id\n name\n latestVersion {\n id\n name\n versionNumber\n fileType\n }\n }\n }\n children {\n items {\n id\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 fragment ProjectAccSyncItem on AccSyncItem {\n id\n project {\n id\n }\n model {\n id\n }\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n"): (typeof documents)["\n fragment ProjectAccSyncItem on AccSyncItem {\n id\n project {\n id\n }\n model {\n id\n }\n accRegion\n accHubId\n accProjectId\n accRootProjectFolderUrn\n accFileLineageUrn\n accFileName\n accFileExtension\n accFileVersionIndex\n accFileViewName\n updatedAt\n status\n author {\n name\n avatar\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1974,6 +1996,10 @@ export function graphql(source: "\n mutation UpdateAccSyncItem($input: UpdateAc
|
||||
* 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 ProjectAccSyncItems($id: String!) {\n project(id: $id) {\n accSyncItems {\n items {\n ...ProjectAccSyncItem\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectAccSyncItems($id: String!) {\n project(id: $id) {\n accSyncItems {\n items {\n ...ProjectAccSyncItem\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 AccFolderData(\n $workspaceSlug: String!\n $accToken: String!\n $accProjectId: String!\n $accFolderId: String!\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n integrations {\n acc(token: $accToken) {\n folder(projectId: $accProjectId, folderId: $accFolderId) {\n id\n ...AccIntegrationFolderNode_AccFolder\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query AccFolderData(\n $workspaceSlug: String!\n $accToken: String!\n $accProjectId: String!\n $accFolderId: String!\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n integrations {\n acc(token: $accToken) {\n folder(projectId: $accProjectId, folderId: $accFolderId) {\n id\n ...AccIntegrationFolderNode_AccFolder\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.
|
||||
*/
|
||||
@@ -3090,6 +3116,10 @@ export function graphql(source: "\n mutation WorkspaceUpdateDefaultSeatTypeMuta
|
||||
* 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 WorkspaceUpdateExclusiveMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n isExclusive\n }\n }\n }\n"): (typeof documents)["\n mutation WorkspaceUpdateExclusiveMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n isExclusive\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 Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {\n workspace(id: $workspaceId) {\n hasAccessToFeature(featureName: $featureName)\n }\n }\n"): (typeof documents)["\n query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {\n workspace(id: $workspaceId) {\n hasAccessToFeature(featureName: $featureName)\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -3221,7 +3251,7 @@ export function graphql(source: "\n query AutomateFunctionPageWorkspace($worksp
|
||||
/**
|
||||
* 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 ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -3233,7 +3263,7 @@ export function graphql(source: "\n fragment ProjectPageAutomationPage_Project
|
||||
/**
|
||||
* 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 ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -70,6 +70,11 @@ export const settingsWorkspaceRoutes = {
|
||||
route: (slug: MaybeNullOrUndefined<string>) =>
|
||||
slug ? `/settings/workspaces/${slug}/projects` : '/'
|
||||
},
|
||||
integrations: {
|
||||
name: 'settings-workspaces-slug-integrations',
|
||||
route: (slug: MaybeNullOrUndefined<string>) =>
|
||||
slug ? `/settings/workspaces/${slug}/integrations` : '/'
|
||||
},
|
||||
automation: {
|
||||
name: 'settings-workspaces-slug-automation',
|
||||
route: (slug?: string) => `/settings/workspaces/${slug}/automation`
|
||||
@@ -134,6 +139,9 @@ export const projectWebhooksRoute = (projectId: string) =>
|
||||
export const projectTokensRoute = (projectId: string) =>
|
||||
`/projects/${projectId}/settings/tokens`
|
||||
|
||||
export const projectIntegrationsRoute = (projectId: string) =>
|
||||
`/projects/${projectId}/settings/integrations`
|
||||
|
||||
export const threadRedirectRoute = (projectId: string, threadId: string) =>
|
||||
`/projects/${projectId}/threads/${threadId}`
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useApolloClient } from '@vue/apollo-composable'
|
||||
import { useAccAuthManager } from '~/lib/acc/composables/useAccAuthManager'
|
||||
import { AccIntegration } from '~/lib/acc/types'
|
||||
import { WorkspaceFeatureName } from '~/lib/common/generated/gql/graphql'
|
||||
import type { Integration } from '~/lib/integrations/types'
|
||||
import { workspaceFeatureEnabledCheckQuery } from '~/lib/workspaces/graphql/queries'
|
||||
|
||||
export function useAccIntegration() {
|
||||
const integration = ref<Integration>(AccIntegration)
|
||||
const apollo = useApolloClient().client
|
||||
const loading = ref(false)
|
||||
|
||||
const checkConnection = async (workspaceSlug: string, workspaceId: string) => {
|
||||
loading.value = true
|
||||
try {
|
||||
const isAccModuleEnabled = useIsAccModuleEnabled()
|
||||
|
||||
if (isAccModuleEnabled) {
|
||||
const accIntegationEnabled = await isAccEnabledInWorkspace(workspaceId)
|
||||
const callbackEndpoint = `settings/workspaces/${workspaceSlug}/integrations`
|
||||
|
||||
if (accIntegationEnabled) {
|
||||
const { isExpired, tokens, tryGetTokensFromCookies } = useAccAuthManager()
|
||||
await tryGetTokensFromCookies() // also refreshes the tokens - so we can rely on existance of tokens to say 'connected'
|
||||
|
||||
integration.value = {
|
||||
...AccIntegration,
|
||||
connected: tokens.value !== undefined,
|
||||
status: isExpired.value
|
||||
? 'expired'
|
||||
: tokens.value !== undefined
|
||||
? 'connected'
|
||||
: 'notConnected',
|
||||
enabled: true,
|
||||
callbackEndpoint
|
||||
}
|
||||
} else {
|
||||
integration.value = { ...AccIntegration, callbackEndpoint }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isAccEnabledInWorkspace = async (workspaceId: string) => {
|
||||
const { data } = await apollo.query({
|
||||
query: workspaceFeatureEnabledCheckQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
featureName: WorkspaceFeatureName.AccIntegration
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
return data?.workspace?.hasAccessToFeature ?? false
|
||||
}
|
||||
|
||||
const checkCredientials = async () => {
|
||||
const { tryGetTokensFromCookies } = useAccAuthManager()
|
||||
await tryGetTokensFromCookies()
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
integration,
|
||||
checkConnection,
|
||||
checkCredientials
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export type Integration = {
|
||||
name: string
|
||||
description: string
|
||||
cookieKey: string
|
||||
logo: string
|
||||
connected: boolean
|
||||
enabled: boolean
|
||||
status: 'connected' | 'expired' | 'notConnected'
|
||||
callbackEndpoint?: string
|
||||
}
|
||||
@@ -66,6 +66,12 @@ export const useSettingsMenu = () => {
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: 'Integrations',
|
||||
name: settingsWorkspaceRoutes.integrations.name,
|
||||
route: (slug?: string) => settingsWorkspaceRoutes.integrations.route(slug),
|
||||
permission: [Roles.Workspace.Admin, Roles.Workspace.Member]
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
name: settingsWorkspaceRoutes.security.name,
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
|
||||
export const workspaceFeatureEnabledCheckQuery = graphql(`
|
||||
query Workspace($featureName: WorkspaceFeatureName!, $workspaceId: String!) {
|
||||
workspace(id: $workspaceId) {
|
||||
hasAccessToFeature(featureName: $featureName)
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const workspaceAccessCheckQuery = graphql(`
|
||||
query WorkspaceAccessCheck($slug: String!) {
|
||||
workspaceBySlug(slug: $slug) {
|
||||
|
||||
@@ -104,9 +104,6 @@ graphql(`
|
||||
canReadSettings {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canReadAccIntegrationSettings {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canUpdate {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
@@ -182,9 +179,7 @@ const modelCount = computed(() => project.value?.modelCount.totalCount)
|
||||
const commentCount = computed(() => project.value?.commentThreadCount.totalCount)
|
||||
|
||||
const canReadSettings = computed(() => project.value?.permissions.canReadSettings)
|
||||
const canReadAccIntegrationSettings = computed(
|
||||
() => project.value?.permissions.canReadAccIntegrationSettings
|
||||
)
|
||||
|
||||
const canUpdate = computed(() => project.value?.permissions.canUpdate)
|
||||
const hasRole = computed(() => project.value?.role)
|
||||
const teamUsers = computed(() => project.value?.team.map((t) => t.user) || [])
|
||||
@@ -233,7 +228,6 @@ const onInviteAccepted = async (params: { accepted: boolean }) => {
|
||||
const isOwner = computed(() => project.value?.role === Roles.Stream.Owner)
|
||||
const isAutomateEnabled = useIsAutomateModuleEnabled()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const isAccEnabled = useIsAccModuleEnabled()
|
||||
|
||||
const pageTabItems = computed((): LayoutPageTabItem[] => {
|
||||
const items: LayoutPageTabItem[] = [
|
||||
@@ -260,13 +254,6 @@ const pageTabItems = computed((): LayoutPageTabItem[] => {
|
||||
})
|
||||
}
|
||||
|
||||
if (isAccEnabled.value && canReadAccIntegrationSettings.value?.authorized) {
|
||||
items.push({
|
||||
title: 'ACC',
|
||||
id: 'acc'
|
||||
})
|
||||
}
|
||||
|
||||
if (canReadSettings.value?.authorized) {
|
||||
items.push({
|
||||
title: 'Collaborators',
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<ProjectPageAccTab :project-id="projectId" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { ProjectPageProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
const route = useRoute()
|
||||
|
||||
const attrs = useAttrs() as {
|
||||
project: ProjectPageProjectFragment
|
||||
}
|
||||
|
||||
const projectName = computed(() =>
|
||||
attrs.project.name.length ? attrs.project.name : ''
|
||||
)
|
||||
|
||||
const projectId = computed(() => route.params.id as string)
|
||||
|
||||
useHead({
|
||||
title: `Acc | ${projectName.value}`
|
||||
})
|
||||
</script>
|
||||
@@ -14,7 +14,8 @@ import { LayoutTabsVertical, type LayoutPageTabItem } from '@speckle/ui-componen
|
||||
import {
|
||||
projectSettingsRoute,
|
||||
projectWebhooksRoute,
|
||||
projectTokensRoute
|
||||
projectTokensRoute,
|
||||
projectIntegrationsRoute
|
||||
} from '~~/lib/common/helpers/route'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { ProjectPageSettingsTab_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
@@ -34,6 +35,9 @@ graphql(`
|
||||
canReadEmbedTokens {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canReadAccIntegrationSettings {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -49,6 +53,10 @@ const canReadWebhooks = computed(() => attrs.project.permissions.canReadWebhooks
|
||||
const projectName = computed(() =>
|
||||
attrs.project.name.length ? attrs.project.name : ''
|
||||
)
|
||||
const isAccEnabled = useIsAccModuleEnabled() // check permission over project
|
||||
const canReadAccIntegrationSettings = computed(
|
||||
() => attrs.project.permissions.canReadAccIntegrationSettings
|
||||
)
|
||||
|
||||
useHead({
|
||||
title: `Settings | ${projectName.value}`
|
||||
@@ -70,6 +78,12 @@ const settingsTabItems = computed((): LayoutPageTabItem[] => [
|
||||
id: 'tokens',
|
||||
disabled: !canReadEmbedTokens.value.authorized,
|
||||
disabledMessage: canReadEmbedTokens.value.message
|
||||
},
|
||||
{
|
||||
title: 'Integrations',
|
||||
id: 'integrations',
|
||||
disabled: isAccEnabled && !canReadAccIntegrationSettings.value.authorized,
|
||||
disabledMessage: canReadAccIntegrationSettings.value.message
|
||||
}
|
||||
])
|
||||
|
||||
@@ -80,6 +94,7 @@ const activeSettingsPageTab = computed({
|
||||
const path = route.path
|
||||
if (path.includes('/settings/webhooks')) return settingsTabItems.value[1]
|
||||
if (path.includes('/settings/tokens')) return settingsTabItems.value[2]
|
||||
if (path.includes('/settings/integrations')) return settingsTabItems.value[3]
|
||||
return settingsTabItems.value[0]
|
||||
},
|
||||
set: (val: LayoutPageTabItem) => {
|
||||
@@ -90,6 +105,9 @@ const activeSettingsPageTab = computed({
|
||||
case 'tokens':
|
||||
router.push(projectTokensRoute(projectId.value))
|
||||
break
|
||||
case 'integrations':
|
||||
router.push(projectIntegrationsRoute(projectId.value))
|
||||
break
|
||||
case 'general':
|
||||
default:
|
||||
router.push(projectSettingsRoute(projectId.value))
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<ProjectPageSettingsAccTab :project-id="projectId" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => route.params.id as string)
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<section>
|
||||
<SettingsSectionHeader
|
||||
title="Integrations"
|
||||
text="Connect your workspace to authorized applications."
|
||||
/>
|
||||
<IntegrationsAccCard
|
||||
:workspace-id="workspaceResult?.workspaceBySlug.id || ''"
|
||||
:workspace-slug="routeSlug"
|
||||
></IntegrationsAccCard>
|
||||
<!-- <div v-for="integration in integrations" :key="integration.cookieKey">
|
||||
<IntegrationsCard
|
||||
:integration="integration"
|
||||
@handle-c-t-a="handleCTA(integration)"
|
||||
></IntegrationsCard>
|
||||
</div> -->
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { settingsWorkspaceGeneralQuery } from '~/lib/settings/graphql/queries'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'settings'
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const routeSlug = computed(() => (route.params.slug as string) || '')
|
||||
const { result: workspaceResult } = useQuery(settingsWorkspaceGeneralQuery, () => ({
|
||||
slug: routeSlug.value
|
||||
}))
|
||||
</script>
|
||||
@@ -0,0 +1,74 @@
|
||||
extend type WorkspaceIntegrations {
|
||||
acc(token: String): AccIntegration
|
||||
}
|
||||
|
||||
type AccIntegration {
|
||||
hub(id: String!): AccHub!
|
||||
hubs: AccHubCollection!
|
||||
project(hubId: String!, projectId: String!): AccProject!
|
||||
folder(projectId: String!, folderId: String!): AccFolder!
|
||||
item(projectId: String!, itemId: String!): AccItem!
|
||||
}
|
||||
|
||||
type AccHub {
|
||||
id: ID!
|
||||
name: String!
|
||||
project(id: ID!): AccProject!
|
||||
projects: AccProjectCollection!
|
||||
}
|
||||
|
||||
type AccHubCollection {
|
||||
items: [AccHub!]!
|
||||
cursor: String
|
||||
}
|
||||
|
||||
type AccProject {
|
||||
id: ID!
|
||||
name: String!
|
||||
# hub: AccHub!
|
||||
folder(id: String!): AccFolder!
|
||||
rootFolder: AccFolder!
|
||||
}
|
||||
|
||||
type AccProjectCollection {
|
||||
items: [AccProject!]!
|
||||
cursor: String
|
||||
}
|
||||
|
||||
type AccFolder {
|
||||
id: ID!
|
||||
name: String!
|
||||
# project: AccProject!
|
||||
contents: AccItemCollection!
|
||||
children: AccFolderCollection!
|
||||
}
|
||||
|
||||
type AccFolderCollection {
|
||||
items: [AccFolder!]!
|
||||
cursor: String
|
||||
}
|
||||
|
||||
type AccItem {
|
||||
"""
|
||||
lineage urn
|
||||
"""
|
||||
id: ID!
|
||||
name: String!
|
||||
# version(version: Int): AccItemVersion!
|
||||
latestVersion: AccItemVersion!
|
||||
}
|
||||
|
||||
type AccItemCollection {
|
||||
items: [AccItem!]!
|
||||
cursor: String
|
||||
}
|
||||
|
||||
type AccItemVersion {
|
||||
"""
|
||||
version urn
|
||||
"""
|
||||
id: ID!
|
||||
name: String!
|
||||
versionNumber: Int!
|
||||
fileType: String
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
extend type Workspace {
|
||||
integrations: WorkspaceIntegrations
|
||||
}
|
||||
|
||||
type WorkspaceIntegrations
|
||||
@@ -0,0 +1,3 @@
|
||||
extend type ProjectPermissionChecks {
|
||||
canReadAccIntegrationSettings: PermissionCheckResult!
|
||||
}
|
||||
+7
-3
@@ -3,6 +3,10 @@ extend type Project {
|
||||
accSyncItem(id: String!): AccSyncItem!
|
||||
}
|
||||
|
||||
extend type Model {
|
||||
accSyncItem: AccSyncItem
|
||||
}
|
||||
|
||||
type AccSyncItemCollection {
|
||||
items: [AccSyncItem!]!
|
||||
totalCount: Int!
|
||||
@@ -11,8 +15,9 @@ type AccSyncItemCollection {
|
||||
|
||||
type AccSyncItem {
|
||||
id: ID!
|
||||
projectId: String!
|
||||
modelId: String!
|
||||
project: Project!
|
||||
model: Model
|
||||
author: LimitedUser
|
||||
accRegion: String!
|
||||
accHubId: String!
|
||||
accProjectId: String!
|
||||
@@ -25,7 +30,6 @@ type AccSyncItem {
|
||||
accFileViewName: String
|
||||
accWebhookId: String
|
||||
status: AccSyncItemStatus!
|
||||
author: LimitedUser
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
}
|
||||
@@ -10,7 +10,6 @@ type ProjectPermissionChecks {
|
||||
canDelete: PermissionCheckResult!
|
||||
canUpdateAllowPublicComments: PermissionCheckResult!
|
||||
canReadSettings: PermissionCheckResult!
|
||||
canReadAccIntegrationSettings: PermissionCheckResult!
|
||||
canReadWebhooks: PermissionCheckResult!
|
||||
canLeave: PermissionCheckResult!
|
||||
canRequestRender: PermissionCheckResult!
|
||||
|
||||
@@ -186,6 +186,12 @@ const config: CodegenConfig = {
|
||||
'@/modules/core/helpers/graphTypes#RootPermissionChecksGraphQLReturn',
|
||||
WorkspacePermissionChecks:
|
||||
'@/modules/workspacesCore/helpers/graphTypes#WorkspacePermissionChecksGraphQLReturn',
|
||||
WorkspaceIntegrations:
|
||||
'@/modules/acc/helpers/graphTypes#WorkspaceIntegrationsGraphQLReturn',
|
||||
AccIntegration:
|
||||
'@/modules/acc/helpers/graphTypes#AccIntegrationGraphQLReturn',
|
||||
AccFolder: '@/modules/acc/helpers/graphTypes#AccFolderGraphQLReturn',
|
||||
AccItem: '@/modules/acc/helpers/graphTypes#AccItemGraphQLReturn',
|
||||
AccSyncItem: '@/modules/acc/helpers/graphTypes#AccSyncItemGraphQLReturn',
|
||||
AccSyncItemMutations:
|
||||
'@/modules/acc/helpers/graphTypes#AccSyncItemMutationsGraphQLReturn',
|
||||
|
||||
@@ -1,310 +0,0 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import type { AccRegion } from '@/modules/acc/domain/constants'
|
||||
import { AccRegions } from '@/modules/acc/domain/constants'
|
||||
import type { ModelDerivativeServiceDesignManifest } from '@/modules/acc/domain/types'
|
||||
import {
|
||||
getAutodeskIntegrationClientId,
|
||||
getAutodeskIntegrationClientSecret
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import { logger } from '@/observability/logging'
|
||||
import { isObjectLike } from 'lodash-es'
|
||||
import { z } from 'zod'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const invokeJsonRequest = async <T>(params: {
|
||||
url: string
|
||||
token: string
|
||||
method?: RequestInit['method']
|
||||
body?: URLSearchParams | Record<string, unknown>
|
||||
headers?: Record<string, string>
|
||||
}) => {
|
||||
const { url, method = 'get', body, headers = {}, token } = params
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type':
|
||||
body instanceof URLSearchParams
|
||||
? 'application/x-www-form-urlencoded'
|
||||
: 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
...headers
|
||||
},
|
||||
body:
|
||||
body && body instanceof URLSearchParams
|
||||
? body
|
||||
: isObjectLike(body)
|
||||
? JSON.stringify(body)
|
||||
: undefined
|
||||
})
|
||||
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
interface BuildAuthorizeUrlOptions {
|
||||
clientId: string
|
||||
redirectUri: string
|
||||
codeChallenge: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
interface ExchangeCodeOptions {
|
||||
code: string
|
||||
codeVerifier: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
redirectUri: string
|
||||
}
|
||||
|
||||
const AccTokens = z.object({
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string(),
|
||||
token_type: z.string(),
|
||||
id_token: z.string(),
|
||||
expires_in: z.number()
|
||||
})
|
||||
|
||||
export const generateCodeVerifier = () => {
|
||||
const codeVerifier = crypto.randomBytes(32).toString('base64url')
|
||||
const codeChallenge = crypto
|
||||
.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest('base64url')
|
||||
return { codeVerifier, codeChallenge }
|
||||
}
|
||||
|
||||
export const buildAuthorizeUrl = ({
|
||||
clientId,
|
||||
redirectUri,
|
||||
codeChallenge,
|
||||
scopes
|
||||
}: BuildAuthorizeUrlOptions) => {
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope: scopes.join(' '),
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256'
|
||||
})
|
||||
|
||||
return `https://developer.api.autodesk.com/authentication/v2/authorize?${params.toString()}`
|
||||
}
|
||||
|
||||
export const exchangeCodeForTokens = async ({
|
||||
code,
|
||||
codeVerifier,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri
|
||||
}: ExchangeCodeOptions): Promise<AccTokens> => {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
code,
|
||||
code_verifier: codeVerifier
|
||||
})
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return AccTokens.parse(data)
|
||||
}
|
||||
|
||||
export const exchangeRefreshTokenForTokens = async (args: {
|
||||
refresh_token: string
|
||||
}): Promise<AccTokens> => {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
client_id: getAutodeskIntegrationClientId(),
|
||||
client_secret: getAutodeskIntegrationClientSecret(),
|
||||
refresh_token: args.refresh_token
|
||||
})
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: params
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return AccTokens.parse(data)
|
||||
}
|
||||
|
||||
type AutodeskIntegrationTokenData = {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a valid token for server-side operations as our custom integration
|
||||
*/
|
||||
export const getToken = async (): Promise<AutodeskIntegrationTokenData> => {
|
||||
const clientId = getAutodeskIntegrationClientId()
|
||||
const clientSecret = getAutodeskIntegrationClientSecret()
|
||||
|
||||
const token = Buffer.from(`${clientId}:${clientSecret}`, 'utf8').toString('base64')
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
scope: 'data:read account:read viewables:read'
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return z
|
||||
.object({
|
||||
access_token: z.string(),
|
||||
token_type: z.string(),
|
||||
expires_in: z.number()
|
||||
})
|
||||
.parse(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base Autodesk API endpoint for a given region.
|
||||
* NOTE: This has been true for all endpoints used so far but should be validated for any new ones!
|
||||
*/
|
||||
const getRegionUrl = (region: AccRegion): string => {
|
||||
switch (region) {
|
||||
case AccRegions.EMEA:
|
||||
return 'https://developer.api.autodesk.com/modelderivative/v2/regions/eu'
|
||||
default:
|
||||
return 'https://developer.api.autodesk.com/modelderivative/v2'
|
||||
}
|
||||
}
|
||||
|
||||
const getApiUrl = (path: string, region: AccRegion): string => {
|
||||
return `${getRegionUrl(region)}${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a urn string in the modified url-safe base64 format expected by the Autodesk API
|
||||
*/
|
||||
const encodeUrn = (urn: string): string => {
|
||||
return btoa(urn).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
|
||||
}
|
||||
|
||||
type AccWebhookConfig = {
|
||||
// The (unencoded) ACC folder urn to subscribe to. Event will fire for all files in this folder.
|
||||
rootProjectFolderUrn: string
|
||||
// The Speckle endpoint to hit when the given event fires.
|
||||
callbackUrl: string
|
||||
// The ACC webhook event to subscribe to. You can register an event to a given callback url only once.
|
||||
event: 'dm.version.added'
|
||||
// The region where the request is executed
|
||||
region: AccRegion
|
||||
}
|
||||
|
||||
/**
|
||||
* Register relevant webhook callbacks for integration with a given ACC project.
|
||||
* @see https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/webhooks/systems-system-events-event-hooks-POST/
|
||||
* @returns null if webhook already exists
|
||||
*/
|
||||
export const tryRegisterAccWebhook = async (
|
||||
webhook: AccWebhookConfig
|
||||
): Promise<string | null> => {
|
||||
const { rootProjectFolderUrn, callbackUrl, event, region } = webhook
|
||||
|
||||
const tokenData = await getToken()
|
||||
|
||||
const response = await fetch(
|
||||
`https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access_token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-ads-region': region
|
||||
},
|
||||
body: JSON.stringify({
|
||||
callbackUrl,
|
||||
scope: {
|
||||
folder: rootProjectFolderUrn
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (response.ok && response.status === 201) {
|
||||
const webhookId = response.headers.get('Location')?.split('/').at(-1)
|
||||
|
||||
if (!webhookId) {
|
||||
logger.info({ location: response.headers.get('Location') })
|
||||
throw new Error('Webhook created but failed to parse id')
|
||||
}
|
||||
|
||||
return webhookId
|
||||
}
|
||||
|
||||
const e = await response.json().catch(() => null)
|
||||
|
||||
const isConflict =
|
||||
response.status === 409 &&
|
||||
e?.code === 'CONFLICT_ERROR' &&
|
||||
e?.detail?.includes('Failed to save duplicate webhooks scope')
|
||||
|
||||
if (isConflict) {
|
||||
logger.warn('Webhook already exists. Skipping registration.')
|
||||
return null
|
||||
}
|
||||
|
||||
throw new Error(`Webhook registration failed: ${JSON.stringify(e, null, 2)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a manifest of the available derivative assets for a given design urn
|
||||
* @see https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/manifest/urn-manifest-GET/
|
||||
*/
|
||||
export const getManifestByUrn = async (
|
||||
params: {
|
||||
urn: string
|
||||
region: AccRegion
|
||||
},
|
||||
context: {
|
||||
token: string
|
||||
}
|
||||
): Promise<ModelDerivativeServiceDesignManifest> => {
|
||||
const { urn, region = AccRegions.EMEA } = params
|
||||
const { token } = context
|
||||
|
||||
const encodedUrn = encodeUrn(urn)
|
||||
|
||||
const url = getApiUrl(`/designdata/${encodedUrn}/manifest`, region)
|
||||
|
||||
return await invokeJsonRequest({
|
||||
url,
|
||||
token,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import {
|
||||
encodeUrn,
|
||||
getApiUrl,
|
||||
invokeJsonRequest
|
||||
} from '@/modules/acc/clients/autodesk/helpers'
|
||||
import { getToken } from '@/modules/acc/clients/autodesk/tokens'
|
||||
import type { AccRegion } from '@/modules/acc/domain/acc/constants'
|
||||
import { AccRegions } from '@/modules/acc/domain/acc/constants'
|
||||
import type {
|
||||
DataManagementFolderContentsFolder,
|
||||
DataManagementFolderContentsItem,
|
||||
DataManagementFolderContentsItemVersion,
|
||||
ModelDerivativeServiceDesignManifest
|
||||
} from '@/modules/acc/domain/acc/types'
|
||||
import { logger } from '@/observability/logging'
|
||||
|
||||
type AccWebhookConfig = {
|
||||
// The (unencoded) ACC folder urn to subscribe to. Event will fire for all files in this folder.
|
||||
rootProjectFolderUrn: string
|
||||
// The Speckle endpoint to hit when the given event fires.
|
||||
callbackUrl: string
|
||||
// The ACC webhook event to subscribe to. You can register an event to a given callback url only once.
|
||||
event: 'dm.version.added'
|
||||
// The region where the request is executed
|
||||
region: AccRegion
|
||||
}
|
||||
|
||||
/**
|
||||
* Register relevant webhook callbacks for integration with a given ACC project.
|
||||
* @see https://aps.autodesk.com/en/docs/webhooks/v1/reference/http/webhooks/systems-system-events-event-hooks-POST/
|
||||
* @returns null if webhook already exists
|
||||
*/
|
||||
export const tryRegisterAccWebhook = async (
|
||||
webhook: AccWebhookConfig
|
||||
): Promise<string | null> => {
|
||||
const { rootProjectFolderUrn, callbackUrl, event, region } = webhook
|
||||
|
||||
const tokenData = await getToken()
|
||||
|
||||
const response = await fetch(
|
||||
`https://developer.api.autodesk.com/webhooks/v1/systems/data/events/${event}/hooks`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokenData.access_token}`,
|
||||
'Content-Type': 'application/json',
|
||||
'x-ads-region': region
|
||||
},
|
||||
body: JSON.stringify({
|
||||
callbackUrl,
|
||||
scope: {
|
||||
folder: rootProjectFolderUrn
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (response.ok && response.status === 201) {
|
||||
const webhookId = response.headers.get('Location')?.split('/').at(-1)
|
||||
|
||||
if (!webhookId) {
|
||||
logger.info({ location: response.headers.get('Location') })
|
||||
throw new Error('Webhook created but failed to parse id')
|
||||
}
|
||||
|
||||
return webhookId
|
||||
}
|
||||
|
||||
const e = await response.json().catch(() => null)
|
||||
|
||||
const isConflict =
|
||||
response.status === 409 &&
|
||||
e?.code === 'CONFLICT_ERROR' &&
|
||||
e?.detail?.includes('Failed to save duplicate webhooks scope')
|
||||
|
||||
if (isConflict) {
|
||||
logger.warn('Webhook already exists. Skipping registration.')
|
||||
return null
|
||||
}
|
||||
|
||||
throw new Error(`Webhook registration failed: ${JSON.stringify(e, null, 2)}`)
|
||||
}
|
||||
|
||||
type GetFolderMetadataResponse = { data: DataManagementFolderContentsFolder }
|
||||
|
||||
/**
|
||||
* Get information about a given folder, like its name and object count
|
||||
* @see https://aps.autodesk.com/en/docs/data/v2/reference/http/projects-project_id-folders-folder_id-GET/
|
||||
*/
|
||||
export const getFolderMetadata = async (
|
||||
params: {
|
||||
projectId: string
|
||||
folderId: string
|
||||
},
|
||||
context: {
|
||||
token: string
|
||||
userId?: string
|
||||
}
|
||||
): Promise<DataManagementFolderContentsFolder> => {
|
||||
const { projectId, folderId } = params
|
||||
const { token } = context
|
||||
|
||||
const { data } = (await invokeJsonRequest({
|
||||
url: `https://developer.api.autodesk.com/data/v1/projects/${projectId}/folders/${folderId}`,
|
||||
method: 'GET',
|
||||
token
|
||||
})) as GetFolderMetadataResponse
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
type GetFolderContentsResponse = {
|
||||
data: (DataManagementFolderContentsFolder | DataManagementFolderContentsItem)[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contents (subfolders and items) of a given ACC folder
|
||||
* @see https://aps.autodesk.com/en/docs/data/v2/reference/http/projects-project_id-folders-folder_id-contents-GET/
|
||||
*/
|
||||
export const getFolderContents = async (
|
||||
args: {
|
||||
projectId: string
|
||||
folderId: string
|
||||
type?: 'items' | 'folders'
|
||||
},
|
||||
context: {
|
||||
token: string
|
||||
userId?: string
|
||||
}
|
||||
): Promise<
|
||||
(DataManagementFolderContentsFolder | DataManagementFolderContentsItem)[]
|
||||
> => {
|
||||
const { projectId, folderId } = args
|
||||
const { token } = context
|
||||
|
||||
const url = new URL(
|
||||
`https://developer.api.autodesk.com/data/v1/projects/${projectId}/folders/${folderId}/contents`
|
||||
)
|
||||
const params = new URLSearchParams()
|
||||
|
||||
if (args.type) {
|
||||
params.append('filter[type]', args.type)
|
||||
}
|
||||
|
||||
url.search = params.toString()
|
||||
|
||||
const { data } = (await invokeJsonRequest({
|
||||
url: url.toString(),
|
||||
method: 'GET',
|
||||
token
|
||||
})) as GetFolderContentsResponse
|
||||
|
||||
return data ?? []
|
||||
}
|
||||
|
||||
type GetItemLatestVersionResponse = {
|
||||
data: DataManagementFolderContentsItemVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item version information, like version number and file type, for the latest version of a given file
|
||||
* @see https://aps.autodesk.com/en/docs/data/v2/reference/http/projects-project_id-items-item_id-tip-GET/
|
||||
*/
|
||||
export const getItemLatestVersion = async (
|
||||
args: {
|
||||
projectId: string
|
||||
itemId: string
|
||||
},
|
||||
context: {
|
||||
token: string
|
||||
userId?: string
|
||||
}
|
||||
): Promise<DataManagementFolderContentsItemVersion> => {
|
||||
const { projectId, itemId } = args
|
||||
const { token } = context
|
||||
|
||||
const { data } = (await invokeJsonRequest({
|
||||
url: `https://developer.api.autodesk.com/data/v1/projects/${projectId}/items/${itemId}/tip`,
|
||||
method: 'GET',
|
||||
token
|
||||
})) as GetItemLatestVersionResponse
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a manifest of the available derivative assets for a given design urn
|
||||
* @see https://aps.autodesk.com/en/docs/model-derivative/v2/reference/http/manifest/urn-manifest-GET/
|
||||
*/
|
||||
export const getManifestByUrn = async (
|
||||
params: {
|
||||
urn: string
|
||||
region: AccRegion
|
||||
},
|
||||
context: {
|
||||
token: string
|
||||
}
|
||||
): Promise<ModelDerivativeServiceDesignManifest> => {
|
||||
const { urn, region = AccRegions.EMEA } = params
|
||||
const { token } = context
|
||||
|
||||
const encodedUrn = encodeUrn(urn)
|
||||
|
||||
const url = getApiUrl(`/designdata/${encodedUrn}/manifest`, region)
|
||||
|
||||
return await invokeJsonRequest({
|
||||
url,
|
||||
token,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { AccRegion } from '@/modules/acc/domain/acc/constants'
|
||||
import { AccRegions } from '@/modules/acc/domain/acc/constants'
|
||||
import { AutodeskApiRequestError } from '@/modules/acc/errors/acc'
|
||||
import { logger } from '@/observability/logging'
|
||||
|
||||
export const invokeJsonRequest = async <T>(params: {
|
||||
url: string
|
||||
token: string
|
||||
method?: RequestInit['method']
|
||||
body?: string
|
||||
headers?: Record<string, string>
|
||||
}) => {
|
||||
const { url, method = 'get', body, headers = {}, token } = params
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
...headers
|
||||
},
|
||||
body
|
||||
})
|
||||
|
||||
return (await response.json()) as T
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
{
|
||||
...params,
|
||||
error: e
|
||||
},
|
||||
'Autodesk request failed'
|
||||
)
|
||||
throw new AutodeskApiRequestError(method, url)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base Autodesk API endpoint for a given region.
|
||||
* NOTE: This has been true for all endpoints used so far but should be validated for any new ones!
|
||||
*/
|
||||
export const getRegionUrl = (region: AccRegion): string => {
|
||||
switch (region) {
|
||||
case AccRegions.EMEA:
|
||||
return 'https://developer.api.autodesk.com/modelderivative/v2/regions/eu'
|
||||
default:
|
||||
return 'https://developer.api.autodesk.com/modelderivative/v2'
|
||||
}
|
||||
}
|
||||
|
||||
export const getApiUrl = (path: string, region: AccRegion): string => {
|
||||
return `${getRegionUrl(region)}${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a urn string in the modified url-safe base64 format expected by the Autodesk API
|
||||
*/
|
||||
export const encodeUrn = (urn: string): string => {
|
||||
return btoa(urn).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import crypto from 'crypto'
|
||||
import { z } from 'zod'
|
||||
import type { AccTokens } from '@speckle/shared/acc'
|
||||
import {
|
||||
getAutodeskIntegrationClientId,
|
||||
getAutodeskIntegrationClientSecret
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
interface BuildAuthorizeUrlOptions {
|
||||
clientId: string
|
||||
redirectUri: string
|
||||
codeChallenge: string
|
||||
scopes: string[]
|
||||
}
|
||||
|
||||
interface ExchangeCodeOptions {
|
||||
code: string
|
||||
codeVerifier: string
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
redirectUri: string
|
||||
}
|
||||
|
||||
const AccTokens = z
|
||||
.object({
|
||||
access_token: z.string(),
|
||||
refresh_token: z.string(),
|
||||
token_type: z.string(),
|
||||
id_token: z.string().optional(),
|
||||
expires_in: z.number()
|
||||
})
|
||||
.transform((data) => ({
|
||||
...data,
|
||||
timestamp: Date.now()
|
||||
}))
|
||||
|
||||
export const generateCodeVerifier = () => {
|
||||
const codeVerifier = crypto.randomBytes(32).toString('base64url')
|
||||
const codeChallenge = crypto
|
||||
.createHash('sha256')
|
||||
.update(codeVerifier)
|
||||
.digest('base64url')
|
||||
return { codeVerifier, codeChallenge }
|
||||
}
|
||||
|
||||
export const buildAuthorizeUrl = ({
|
||||
clientId,
|
||||
redirectUri,
|
||||
codeChallenge,
|
||||
scopes
|
||||
}: BuildAuthorizeUrlOptions) => {
|
||||
const params = new URLSearchParams({
|
||||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: redirectUri,
|
||||
scope: scopes.join(' '),
|
||||
code_challenge: codeChallenge,
|
||||
code_challenge_method: 'S256'
|
||||
})
|
||||
|
||||
return `https://developer.api.autodesk.com/authentication/v2/authorize?${params.toString()}`
|
||||
}
|
||||
|
||||
export const exchangeCodeForTokens = async ({
|
||||
code,
|
||||
codeVerifier,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri
|
||||
}: ExchangeCodeOptions): Promise<AccTokens> => {
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
code,
|
||||
code_verifier: codeVerifier
|
||||
})
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
body: params,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
return AccTokens.parse(data)
|
||||
}
|
||||
|
||||
export const exchangeRefreshTokenForTokens = async (args: {
|
||||
refresh_token: string
|
||||
}): Promise<AccTokens> => {
|
||||
const clientId = getAutodeskIntegrationClientId()
|
||||
const clientSecret = getAutodeskIntegrationClientSecret()
|
||||
const token = Buffer.from(`${clientId}:${clientSecret}`, 'utf8').toString('base64')
|
||||
|
||||
const params = new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: args.refresh_token
|
||||
})
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: params
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return AccTokens.parse(data)
|
||||
}
|
||||
|
||||
type AutodeskIntegrationTokenData = {
|
||||
access_token: string
|
||||
token_type: string
|
||||
expires_in: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a valid token for server-side operations as our custom integration
|
||||
*/
|
||||
export const getToken = async (): Promise<AutodeskIntegrationTokenData> => {
|
||||
const clientId = getAutodeskIntegrationClientId()
|
||||
const clientSecret = getAutodeskIntegrationClientSecret()
|
||||
|
||||
const token = Buffer.from(`${clientId}:${clientSecret}`, 'utf8').toString('base64')
|
||||
|
||||
const response = await fetch(
|
||||
'https://developer.api.autodesk.com/authentication/v2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'client_credentials',
|
||||
scope: 'data:read account:read viewables:read'
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return z
|
||||
.object({
|
||||
access_token: z.string(),
|
||||
token_type: z.string(),
|
||||
expires_in: z.number()
|
||||
})
|
||||
.parse(data)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { StringEnumValues } from '@speckle/shared'
|
||||
import { StringEnum } from '@speckle/shared'
|
||||
|
||||
export const AccSyncItemStatuses = StringEnum([
|
||||
// A new file version had been detected, and we are awaiting a processable file.
|
||||
'pending',
|
||||
// We are actively processing the new file version. (The Automate function has been triggered.)
|
||||
'syncing',
|
||||
'failed',
|
||||
'paused',
|
||||
'succeeded'
|
||||
])
|
||||
export type AccSyncItemStatus = StringEnumValues<typeof AccSyncItemStatuses>
|
||||
|
||||
export const AccRegions = StringEnum([
|
||||
'US',
|
||||
'EMEA',
|
||||
'AUS',
|
||||
'CAN',
|
||||
'DEU',
|
||||
'IND',
|
||||
'JPN',
|
||||
'GBR'
|
||||
])
|
||||
export type AccRegion = StringEnumValues<typeof AccRegions>
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
|
||||
|
||||
export const accSyncItemEventsNamespace = 'accSyncItems' as const
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
import type { AccSyncItemStatus } from '@/modules/acc/domain/constants'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
import type { AccSyncItemStatus } from '@/modules/acc/domain/acc/constants'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
|
||||
import type { Exact } from 'type-fest'
|
||||
|
||||
export type UpsertAccSyncItem = <Item extends Exact<AccSyncItem, Item>>(
|
||||
@@ -13,7 +13,14 @@ export type UpdateAccSyncItemStatus = (args: {
|
||||
|
||||
export type GetAccSyncItemById = (args: { id: string }) => Promise<AccSyncItem | null>
|
||||
|
||||
export type GetAccSyncItemByModelId = (args: {
|
||||
modelId: string
|
||||
}) => Promise<AccSyncItem | null>
|
||||
|
||||
export type GetAccSyncItemsById = (args: { ids: string[] }) => Promise<AccSyncItem[]>
|
||||
export type GetAccSyncItemsByModelId = (args: {
|
||||
ids: string[]
|
||||
}) => Promise<AccSyncItem[]>
|
||||
|
||||
export type ListAccSyncItems = (args: {
|
||||
projectId: string
|
||||
+31
-1
@@ -1,4 +1,4 @@
|
||||
import type { AccRegion, AccSyncItemStatus } from '@/modules/acc/domain/constants'
|
||||
import type { AccRegion, AccSyncItemStatus } from '@/modules/acc/domain/acc/constants'
|
||||
|
||||
export type AccSyncItem = {
|
||||
id: string
|
||||
@@ -22,6 +22,36 @@ export type AccSyncItem = {
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export type DataManagementFolderContentsFolder = {
|
||||
id: string
|
||||
type: 'folders'
|
||||
attributes: {
|
||||
name?: string
|
||||
displayName: string
|
||||
objectCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export type DataManagementFolderContentsItem = {
|
||||
id: string
|
||||
type: 'items'
|
||||
attributes: {
|
||||
name?: string
|
||||
displayName: string
|
||||
}
|
||||
}
|
||||
|
||||
export type DataManagementFolderContentsItemVersion = {
|
||||
id: string
|
||||
type: 'versions'
|
||||
attributes: {
|
||||
name?: string
|
||||
displayName: string
|
||||
versionNumber: number
|
||||
fileType?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type ModelDerivativeServiceDesignManifest = {
|
||||
type: 'manifest'
|
||||
region: string
|
||||
@@ -1,32 +1,10 @@
|
||||
import type { StringEnumValues } from '@speckle/shared'
|
||||
import { StringEnum } from '@speckle/shared'
|
||||
|
||||
export const ImporterAutomateFunctions = {
|
||||
svf2: {
|
||||
functionId: '4665e0b3ba',
|
||||
functionReleaseId: '470ec84b63'
|
||||
},
|
||||
rvt: {
|
||||
functionId: '0725cb0ac6',
|
||||
functionReleaseId: 'b5c16a1606'
|
||||
}
|
||||
}
|
||||
|
||||
export const AccSyncItemStatuses = StringEnum([
|
||||
// A new file version had been detected, and we are awaiting a processable file.
|
||||
'pending',
|
||||
// We are actively processing the new file version. (The Automate function has been triggered.)
|
||||
'syncing',
|
||||
'failed',
|
||||
'paused',
|
||||
'succeeded'
|
||||
])
|
||||
export type AccSyncItemStatus = StringEnumValues<typeof AccSyncItemStatuses>
|
||||
|
||||
export const AccRegions = StringEnum([
|
||||
'US',
|
||||
'EMEA',
|
||||
'AUS',
|
||||
'CAN',
|
||||
'DEU',
|
||||
'IND',
|
||||
'JPN',
|
||||
'GBR'
|
||||
])
|
||||
export type AccRegion = StringEnumValues<typeof AccRegions>
|
||||
|
||||
@@ -6,6 +6,30 @@ export class AccModuleDisabledError extends BaseError {
|
||||
static statusCode = 423
|
||||
}
|
||||
|
||||
export class AccNotAuthorizedError extends BaseError {
|
||||
static defaultMessage = 'ACC token missing or not authorized'
|
||||
static code = 'ACC_MODULE_NOT_AUTHORIZED'
|
||||
static statusCode = 401
|
||||
}
|
||||
|
||||
export class AccNotYetImplementedError extends BaseError {
|
||||
static defaultMessage =
|
||||
'This functionality for the ACC integration is not yet implemented'
|
||||
static code = 'ACC_MODULE_NOT_YET_IMPLEMENTED'
|
||||
static statusCode = 501
|
||||
}
|
||||
|
||||
export class AutodeskApiRequestError extends BaseError {
|
||||
static defaultMessage = 'Error during external request to Autodesk'
|
||||
static code = 'ACC_AUTODESK_REQUEST_ERROR'
|
||||
static statusCode = 500
|
||||
|
||||
constructor(method: string, endpoint: string) {
|
||||
super()
|
||||
this.message = `Failed to issue ${method} request to ${endpoint}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateSyncItemError extends BaseError {
|
||||
static defaultMessage = 'A sync item with this lineage urn already exists.'
|
||||
static code = 'ACC_DUPLICATE_SYNC_ITEM_LINEAGE_URN'
|
||||
@@ -28,3 +52,14 @@ export class SyncItemAutomationTriggerError extends BaseError {
|
||||
static code = 'ACC_SYNC_ITEM_AUTOMATION_TRIGGER_ERROR'
|
||||
static statusCode = 422
|
||||
}
|
||||
|
||||
export class SyncItemUnsupportedFileExtensionError extends BaseError {
|
||||
static defaultMessage = 'Cannot sync this file type from ACC'
|
||||
static code = 'ACC_SYNC_ITEM_UNSUPPORTED_FILE_EXTENSION'
|
||||
static statusCode = 422
|
||||
|
||||
constructor(fileExtension: string) {
|
||||
super()
|
||||
this.message = `Received sync item update with unsupported file extension ${fileExtension}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AccSyncItemEvents } from '@/modules/acc/domain/events'
|
||||
import { AccSyncItemEvents } from '@/modules/acc/domain/acc/events'
|
||||
import type { EventBusListen, EventPayload } from '@/modules/shared/services/eventBus'
|
||||
import type { PublishSubscription } from '@/modules/shared/utils/subscriptions'
|
||||
import { ProjectSubscriptions } from '@/modules/shared/utils/subscriptions'
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { getFolderContents } from '@/modules/acc/clients/autodesk/acc'
|
||||
import type {
|
||||
AccSyncItem,
|
||||
DataManagementFolderContentsFolder,
|
||||
DataManagementFolderContentsItem
|
||||
} from '@/modules/acc/domain/acc/types'
|
||||
import {
|
||||
getAccSyncItemsByIdFactory,
|
||||
getAccSyncItemsByModelIdFactory
|
||||
} from '@/modules/acc/repositories/accSyncItems'
|
||||
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
import { keyBy } from 'lodash-es'
|
||||
|
||||
declare module '@/modules/core/loaders' {
|
||||
interface ModularizedDataLoaders
|
||||
extends Partial<ReturnType<typeof dataLoadersDefinition>> {}
|
||||
}
|
||||
|
||||
const dataLoadersDefinition = defineRequestDataloaders(
|
||||
({ createLoader, deps: { db } }) => {
|
||||
const getAccSyncItemsById = getAccSyncItemsByIdFactory({ db })
|
||||
const getAccSyncItemsByModelId = getAccSyncItemsByModelIdFactory({ db })
|
||||
|
||||
return {
|
||||
acc: {
|
||||
getFolderChildren: createLoader<
|
||||
{ projectId: string; folderId: string; token: string },
|
||||
DataManagementFolderContentsFolder[],
|
||||
string
|
||||
>(
|
||||
async (folderIds) => {
|
||||
return await Promise.all(
|
||||
folderIds.map(async ({ projectId, folderId, token }) => {
|
||||
const items = await getFolderContents(
|
||||
{ projectId, folderId, type: 'folders' },
|
||||
{ token }
|
||||
)
|
||||
return items.filter(
|
||||
(item): item is DataManagementFolderContentsFolder =>
|
||||
item.type === 'folders'
|
||||
)
|
||||
})
|
||||
)
|
||||
},
|
||||
{
|
||||
cacheKeyFn: (args) => `${args.projectId}-${args.projectId}`
|
||||
}
|
||||
),
|
||||
getFolderContents: createLoader<
|
||||
{ projectId: string; folderId: string; token: string },
|
||||
DataManagementFolderContentsItem[],
|
||||
string
|
||||
>(
|
||||
async (folderIds) => {
|
||||
return await Promise.all(
|
||||
folderIds.map(async ({ projectId, folderId, token }) => {
|
||||
const items = await getFolderContents(
|
||||
{ projectId, folderId, type: 'items' },
|
||||
{ token }
|
||||
)
|
||||
return items.filter(
|
||||
(item): item is DataManagementFolderContentsItem =>
|
||||
item.type === 'items'
|
||||
)
|
||||
})
|
||||
)
|
||||
},
|
||||
{
|
||||
cacheKeyFn: (args) => `${args.projectId}-${args.projectId}`
|
||||
}
|
||||
),
|
||||
getAccSyncItem: createLoader<string, Nullable<AccSyncItem>>(async (ids) => {
|
||||
const results = keyBy(
|
||||
await getAccSyncItemsById({ ids: ids.slice() }),
|
||||
(i) => i.id
|
||||
)
|
||||
return ids.map((i) => results[i] || null)
|
||||
}),
|
||||
getAccSyncItemByModelId: createLoader<string, Nullable<AccSyncItem>>(
|
||||
async (ids) => {
|
||||
const results = keyBy(
|
||||
await getAccSyncItemsByModelId({ ids: ids.slice() }),
|
||||
(i) => i.modelId
|
||||
)
|
||||
return ids.map((i) => results[i] || null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default dataLoadersDefinition
|
||||
+148
-3
@@ -59,15 +59,117 @@ import {
|
||||
ProjectSubscriptions
|
||||
} from '@/modules/shared/utils/subscriptions'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { AccModuleDisabledError, SyncItemNotFoundError } from '@/modules/acc/errors/acc'
|
||||
import {
|
||||
AccModuleDisabledError,
|
||||
AccNotAuthorizedError,
|
||||
SyncItemNotFoundError
|
||||
} from '@/modules/acc/errors/acc'
|
||||
import { getFeatureFlags } from '@speckle/shared/environment'
|
||||
import type { AccRegion } from '@/modules/acc/domain/constants'
|
||||
import type { AccRegion } from '@/modules/acc/domain/acc/constants'
|
||||
import { ProjectNotFoundError } from '@/modules/core/errors/projects'
|
||||
import {
|
||||
mapFolderToGql,
|
||||
mapItemToGql,
|
||||
mapVersionToGql
|
||||
} from '@/modules/acc/helpers/acc'
|
||||
import {
|
||||
getFolderMetadata,
|
||||
getItemLatestVersion
|
||||
} from '@/modules/acc/clients/autodesk/acc'
|
||||
|
||||
const { FF_ACC_INTEGRATION_ENABLED, FF_AUTOMATE_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
const enableAcc = FF_ACC_INTEGRATION_ENABLED && FF_AUTOMATE_MODULE_ENABLED
|
||||
|
||||
const resolvers: Resolvers = {
|
||||
WorkspaceIntegrations: {
|
||||
acc: async (_parent, args, ctx) => {
|
||||
if (!args.token) {
|
||||
throw new AccNotAuthorizedError()
|
||||
}
|
||||
// TODO ACC: Replace with Speckle user - ACC user association
|
||||
ctx.accToken = args.token
|
||||
return {}
|
||||
}
|
||||
},
|
||||
AccIntegration: {
|
||||
folder: async (_parent, args) => {
|
||||
const { projectId, folderId } = args
|
||||
return {
|
||||
id: folderId,
|
||||
projectId
|
||||
}
|
||||
}
|
||||
},
|
||||
AccFolder: {
|
||||
name: async (parent, _args, ctx) => {
|
||||
if (parent.name) return parent.name
|
||||
|
||||
const folderMetadata = await getFolderMetadata(
|
||||
{
|
||||
projectId: parent.projectId,
|
||||
folderId: parent.id
|
||||
},
|
||||
{
|
||||
token: ctx.accToken!
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
folderMetadata.attributes.name ?? folderMetadata.attributes.displayName ?? ''
|
||||
)
|
||||
},
|
||||
contents: async (parent, _args, ctx) => {
|
||||
const { id: folderId, projectId } = parent
|
||||
const { accToken } = ctx
|
||||
|
||||
const files = await ctx.loaders.acc!.getFolderContents.load({
|
||||
projectId,
|
||||
folderId,
|
||||
token: accToken!
|
||||
})
|
||||
const items = files.map((file) => ({
|
||||
...mapItemToGql(file),
|
||||
projectId
|
||||
}))
|
||||
|
||||
return { items }
|
||||
},
|
||||
children: async (parent, _args, ctx) => {
|
||||
const { id: folderId, projectId } = parent
|
||||
const { accToken } = ctx
|
||||
|
||||
const folders = await ctx.loaders.acc!.getFolderChildren.load({
|
||||
projectId,
|
||||
folderId,
|
||||
token: accToken!
|
||||
})
|
||||
const items = folders.map((folder) => ({
|
||||
...mapFolderToGql(folder),
|
||||
projectId
|
||||
}))
|
||||
|
||||
return { items }
|
||||
}
|
||||
},
|
||||
AccItem: {
|
||||
latestVersion: async (parent, _args, ctx) => {
|
||||
const { id: itemId, projectId } = parent
|
||||
const { accToken } = ctx
|
||||
|
||||
const version = await getItemLatestVersion(
|
||||
{
|
||||
projectId,
|
||||
itemId
|
||||
},
|
||||
{
|
||||
token: accToken!
|
||||
}
|
||||
)
|
||||
|
||||
return mapVersionToGql(version)
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
accSyncItemMutations: () => ({})
|
||||
},
|
||||
@@ -193,6 +295,14 @@ const resolvers: Resolvers = {
|
||||
}
|
||||
},
|
||||
AccSyncItem: {
|
||||
project: async (parent, _args, context) => {
|
||||
const project = await context.loaders.streams.getStream.load(parent.projectId)
|
||||
if (!project) throw new ProjectNotFoundError()
|
||||
return project
|
||||
},
|
||||
model: async (parent, _args, context) => {
|
||||
return await context.loaders.branches.getById.load(parent.modelId)
|
||||
},
|
||||
author: async (parent, _args, context) => {
|
||||
return await context.loaders.users.getUser.load(parent.authorId)
|
||||
}
|
||||
@@ -237,7 +347,32 @@ const resolvers: Resolvers = {
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const syncItem = await ctx.loaders.acc.getAccSyncItem.load(id)
|
||||
const syncItem = await ctx.loaders.acc!.getAccSyncItem.load(id)
|
||||
|
||||
if (!syncItem) {
|
||||
throw new SyncItemNotFoundError()
|
||||
}
|
||||
|
||||
return syncItem
|
||||
}
|
||||
},
|
||||
Model: {
|
||||
accSyncItem: async (parent, _args, context) => {
|
||||
const authResult =
|
||||
await context.authPolicies.project.canReadAccIntegrationSettings({
|
||||
userId: context.userId,
|
||||
projectId: parent.streamId
|
||||
})
|
||||
throwIfAuthNotOk(authResult)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: parent.streamId,
|
||||
resourceAccessRules: context.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const syncItem = await context.loaders.acc!.getAccSyncItemByModelId.load(
|
||||
parent.id
|
||||
)
|
||||
|
||||
if (!syncItem) {
|
||||
throw new SyncItemNotFoundError()
|
||||
@@ -275,6 +410,11 @@ const resolvers: Resolvers = {
|
||||
}
|
||||
|
||||
const disabledResolvers: Resolvers = {
|
||||
WorkspaceIntegrations: {
|
||||
async acc() {
|
||||
throw new AccModuleDisabledError()
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
accSyncItemMutations: () => ({})
|
||||
},
|
||||
@@ -297,6 +437,11 @@ const disabledResolvers: Resolvers = {
|
||||
throw new AccModuleDisabledError()
|
||||
}
|
||||
},
|
||||
Model: {
|
||||
async accSyncItem() {
|
||||
return null
|
||||
}
|
||||
},
|
||||
Subscription: {
|
||||
projectAccSyncItemsUpdated: {
|
||||
subscribe: filteredSubscribe(
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
|
||||
const resolvers: Resolvers = {
|
||||
Workspace: {
|
||||
integrations: () => ({})
|
||||
}
|
||||
}
|
||||
|
||||
export default resolvers
|
||||
@@ -0,0 +1,50 @@
|
||||
import type {
|
||||
DataManagementFolderContentsFolder,
|
||||
DataManagementFolderContentsItem,
|
||||
DataManagementFolderContentsItemVersion
|
||||
} from '@/modules/acc/domain/acc/types'
|
||||
import type {
|
||||
AccFolderGraphQLReturn,
|
||||
AccItemGraphQLReturn,
|
||||
AccItemVersionGraphQLReturn
|
||||
} from '@/modules/acc/helpers/graphTypes'
|
||||
|
||||
export const filterContentsToFolders = (
|
||||
contents: (DataManagementFolderContentsFolder | DataManagementFolderContentsItem)[]
|
||||
): DataManagementFolderContentsFolder[] => {
|
||||
return contents.filter(
|
||||
(entry): entry is DataManagementFolderContentsFolder => entry.type === 'folders'
|
||||
)
|
||||
}
|
||||
|
||||
export const filterContentsToItems = (
|
||||
contents: (DataManagementFolderContentsFolder | DataManagementFolderContentsItem)[]
|
||||
): DataManagementFolderContentsItem[] => {
|
||||
return contents.filter(
|
||||
(entry): entry is DataManagementFolderContentsItem => entry.type === 'items'
|
||||
)
|
||||
}
|
||||
|
||||
export const mapFolderToGql = (
|
||||
folder: DataManagementFolderContentsFolder
|
||||
): Omit<AccFolderGraphQLReturn, 'projectId'> => ({
|
||||
id: folder.id,
|
||||
name: folder.attributes.name ?? folder.attributes.displayName,
|
||||
objectCount: folder.attributes.objectCount
|
||||
})
|
||||
|
||||
export const mapItemToGql = (
|
||||
item: DataManagementFolderContentsItem
|
||||
): Omit<AccItemGraphQLReturn, 'projectId'> => ({
|
||||
id: item.id,
|
||||
name: item.attributes.name ?? item.attributes.displayName
|
||||
})
|
||||
|
||||
export const mapVersionToGql = (
|
||||
version: DataManagementFolderContentsItemVersion
|
||||
): AccItemVersionGraphQLReturn => ({
|
||||
id: version.id,
|
||||
name: version.attributes.name ?? version.attributes.displayName,
|
||||
versionNumber: version.attributes.versionNumber,
|
||||
fileType: version.attributes.fileType
|
||||
})
|
||||
@@ -1,4 +1,25 @@
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
|
||||
|
||||
export type WorkspaceIntegrationsGraphQLReturn = {}
|
||||
|
||||
export type AccIntegrationGraphQLReturn = {}
|
||||
export type AccFolderGraphQLReturn = {
|
||||
id: string
|
||||
projectId: string
|
||||
// Resolver will use name provided instead of re-fetching, if possible
|
||||
name?: string
|
||||
objectCount?: number
|
||||
}
|
||||
export type AccItemGraphQLReturn = {
|
||||
id: string
|
||||
name: string
|
||||
projectId: string
|
||||
}
|
||||
export type AccItemVersionGraphQLReturn = {
|
||||
id: string
|
||||
name: string
|
||||
versionNumber: number
|
||||
fileType?: string
|
||||
}
|
||||
export type AccSyncItemGraphQLReturn = AccSyncItem
|
||||
export type AccSyncItemMutationsGraphQLReturn = {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ModelDerivativeServiceDesignManifest } from '@/modules/acc/domain/types'
|
||||
import type { ModelDerivativeServiceDesignManifest } from '@/modules/acc/domain/acc/types'
|
||||
|
||||
export const isReadyForImport = (
|
||||
manifest: ModelDerivativeServiceDesignManifest
|
||||
|
||||
@@ -3,14 +3,15 @@ import type {
|
||||
CountAccSyncItems,
|
||||
DeleteAccSyncItemById,
|
||||
GetAccSyncItemById,
|
||||
GetAccSyncItemByModelId,
|
||||
GetAccSyncItemsById,
|
||||
ListAccSyncItems,
|
||||
QueryAllAccSyncItems,
|
||||
UpdateAccSyncItemStatus,
|
||||
UpsertAccSyncItem
|
||||
} from '@/modules/acc/domain/operations'
|
||||
} from '@/modules/acc/domain/acc/operations'
|
||||
import { executeBatchedSelect } from '@/modules/shared/helpers/dbHelper'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
|
||||
import type { Knex } from 'knex'
|
||||
import { without } from 'lodash-es'
|
||||
|
||||
@@ -30,6 +31,18 @@ export const getAccSyncItemByIdFactory =
|
||||
)
|
||||
}
|
||||
|
||||
export const getAccSyncItemByModelIdFactory =
|
||||
(deps: { db: Knex }): GetAccSyncItemByModelId =>
|
||||
async ({ modelId }) => {
|
||||
return (
|
||||
(await tables
|
||||
.accSyncItems(deps.db)
|
||||
.select()
|
||||
.where(AccSyncItems.col.modelId, modelId)
|
||||
.first()) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
export const getAccSyncItemsByIdFactory =
|
||||
(deps: { db: Knex }): GetAccSyncItemsById =>
|
||||
async ({ ids }) => {
|
||||
@@ -38,6 +51,17 @@ export const getAccSyncItemsByIdFactory =
|
||||
return await tables.accSyncItems(deps.db).select().whereIn(AccSyncItems.col.id, ids)
|
||||
}
|
||||
|
||||
export const getAccSyncItemsByModelIdFactory =
|
||||
(deps: { db: Knex }): GetAccSyncItemsById =>
|
||||
async ({ ids }) => {
|
||||
if (!ids.length) return []
|
||||
|
||||
return await tables
|
||||
.accSyncItems(deps.db)
|
||||
.select()
|
||||
.whereIn(AccSyncItems.col.modelId, ids)
|
||||
}
|
||||
|
||||
export const upsertAccSyncItemFactory =
|
||||
(deps: { db: Knex }): UpsertAccSyncItem =>
|
||||
async (item) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
exchangeCodeForTokens,
|
||||
exchangeRefreshTokenForTokens,
|
||||
generateCodeVerifier
|
||||
} from '@/modules/acc/clients/autodesk'
|
||||
} from '@/modules/acc/clients/autodesk/tokens'
|
||||
import { sessionMiddlewareFactory } from '@/modules/auth/middleware'
|
||||
import { corsMiddlewareFactory } from '@/modules/core/configs/cors'
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getAutodeskIntegrationClientSecret,
|
||||
getServerOrigin
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import { logger } from '@/observability/logging'
|
||||
import type { Express } from 'express'
|
||||
|
||||
export const setupAccOidcEndpoints = (app: Express) => {
|
||||
@@ -24,8 +25,8 @@ export const setupAccOidcEndpoints = (app: Express) => {
|
||||
corsMiddleware,
|
||||
sessionMiddleware,
|
||||
async (req, res) => {
|
||||
const { projectId } = req.body
|
||||
req.session.projectId = projectId
|
||||
const { callbackEndpoint } = req.body
|
||||
req.session.callbackEndpoint = callbackEndpoint
|
||||
|
||||
const { codeVerifier, codeChallenge } = generateCodeVerifier()
|
||||
req.session.codeVerifier = codeVerifier
|
||||
@@ -66,7 +67,13 @@ export const setupAccOidcEndpoints = (app: Express) => {
|
||||
|
||||
req.session.accTokens = tokens
|
||||
|
||||
return res.redirect(`/projects/${req.session.projectId}/acc`)
|
||||
logger.warn(req.session)
|
||||
|
||||
if (!req.session.callbackEndpoint) {
|
||||
return res.status(500)
|
||||
}
|
||||
|
||||
return res.redirect(req.session.callbackEndpoint)
|
||||
} catch (error) {
|
||||
console.error('Token exchange failed:', error)
|
||||
return res.status(500).send({ error: 'Token exchange failed' })
|
||||
@@ -75,10 +82,14 @@ export const setupAccOidcEndpoints = (app: Express) => {
|
||||
)
|
||||
|
||||
app.get('/api/v1/acc/auth/status', corsMiddleware, sessionMiddleware, (req, res) => {
|
||||
if (!req.session.accTokens) {
|
||||
return res.status(404).send({ error: 'No ACC tokens found' })
|
||||
try {
|
||||
if (!req.session.accTokens) {
|
||||
return res.status(404).send({ error: 'No ACC tokens found' })
|
||||
}
|
||||
res.send(req.session.accTokens)
|
||||
} finally {
|
||||
req.session.accTokens = undefined // we wanna return it just once
|
||||
}
|
||||
res.send(req.session.accTokens)
|
||||
})
|
||||
|
||||
app.post(
|
||||
@@ -86,7 +97,7 @@ export const setupAccOidcEndpoints = (app: Express) => {
|
||||
corsMiddleware,
|
||||
sessionMiddleware,
|
||||
async (req, res) => {
|
||||
const { refresh_token } = req.session.accTokens || {}
|
||||
const { refresh_token } = req.body || {}
|
||||
if (!refresh_token) {
|
||||
return res.status(401).json({ error: 'No refresh token found' })
|
||||
}
|
||||
@@ -97,7 +108,7 @@ export const setupAccOidcEndpoints = (app: Express) => {
|
||||
res.json(newTokens)
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error)
|
||||
res.status(500).json({ error: 'Error refreshing token' })
|
||||
res.status(500).json({ error })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
AccSyncItemStatuses,
|
||||
ImporterAutomateFunctions
|
||||
} from '@/modules/acc/domain/constants'
|
||||
import type { UpdateAccSyncItemStatus } from '@/modules/acc/domain/operations'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
import { ImporterAutomateFunctions } from '@/modules/acc/domain/constants'
|
||||
import { AccSyncItemStatuses } from '@/modules/acc/domain/acc/constants'
|
||||
import type { UpdateAccSyncItemStatus } from '@/modules/acc/domain/acc/operations'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
|
||||
import {
|
||||
SyncItemAutomationTriggerError,
|
||||
SyncItemNotFoundError
|
||||
@@ -22,7 +20,9 @@ import type { CreateAndStoreAppToken } from '@/modules/core/domain/tokens/operat
|
||||
import { TokenResourceIdentifierType } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
getAutodeskIntegrationClientId,
|
||||
getAutodeskIntegrationClientSecret
|
||||
getAutodeskIntegrationClientSecret,
|
||||
getOdaUserId,
|
||||
getOdaUserSecret
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import { logger } from '@/observability/logging'
|
||||
import { Scopes } from '@speckle/shared'
|
||||
@@ -92,8 +92,8 @@ export const triggerSyncItemAutomationFactory =
|
||||
functionRuns: [
|
||||
{
|
||||
id: cryptoRandomString({ length: 15 }),
|
||||
functionId: ImporterAutomateFunctions.svf2.functionId,
|
||||
functionReleaseId: ImporterAutomateFunctions.svf2.functionReleaseId,
|
||||
functionId: ImporterAutomateFunctions.rvt.functionId,
|
||||
functionReleaseId: ImporterAutomateFunctions.rvt.functionReleaseId,
|
||||
status: 'pending' as const,
|
||||
elapsed: 0,
|
||||
results: null,
|
||||
@@ -148,9 +148,12 @@ export const triggerSyncItemAutomationFactory =
|
||||
modelId: syncItem.modelId,
|
||||
versionUrn: syncItem.accFileVersionUrn,
|
||||
viewName: syncItem.accFileViewName ?? null,
|
||||
autodeskProjectId: syncItem.accProjectId.replace('b.', ''),
|
||||
autodeskRegion: syncItem.accRegion === 'EMEA' ? 1 : 0,
|
||||
autodeskClientId: getAutodeskIntegrationClientId(),
|
||||
autodeskClientSecret: getAutodeskIntegrationClientSecret()
|
||||
autodeskClientSecret: getAutodeskIntegrationClientSecret(),
|
||||
odaUserId: getOdaUserId(),
|
||||
odaUserSignature: getOdaUserSecret()
|
||||
}
|
||||
})),
|
||||
manifests: [
|
||||
|
||||
@@ -4,9 +4,6 @@ import {
|
||||
} from '@/modules/acc/repositories/accSyncItems'
|
||||
import type { ScheduleExecution } from '@/modules/core/domain/scheduledTasks/operations'
|
||||
import { db } from '@/db/knex'
|
||||
import { getManifestByUrn, getToken } from '@/modules/acc/clients/autodesk'
|
||||
import { isReadyForImport } from '@/modules/acc/helpers/svfUtils'
|
||||
import type { Logger } from '@/observability/logging'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import {
|
||||
getAutomationFactory,
|
||||
@@ -22,7 +19,7 @@ import {
|
||||
} from '@/modules/core/repositories/tokens'
|
||||
import { createAppTokenFactory } from '@/modules/core/services/tokens'
|
||||
import { TIME_MS } from '@speckle/shared'
|
||||
import { AccSyncItemStatuses, type AccRegion } from '@/modules/acc/domain/constants'
|
||||
import { AccSyncItemStatuses } from '@/modules/acc/domain/acc/constants'
|
||||
import { triggerSyncItemAutomationFactory } from '@/modules/acc/services/automate'
|
||||
|
||||
const queryAllAccSyncItems = queryAllAccSyncItemsFactory({ db })
|
||||
@@ -30,28 +27,11 @@ const queryAllAccSyncItems = queryAllAccSyncItemsFactory({ db })
|
||||
export const schedulePendingSyncItemsCheck = (deps: {
|
||||
scheduleExecution: ScheduleExecution
|
||||
}) => {
|
||||
const callback = async (_now: Date, { logger }: { logger: Logger }) => {
|
||||
const tokenData = await getToken()
|
||||
const callback = async () => {
|
||||
for await (const items of queryAllAccSyncItems({
|
||||
filter: { status: AccSyncItemStatuses.pending }
|
||||
})) {
|
||||
for (const syncItem of items) {
|
||||
const manifest = await getManifestByUrn(
|
||||
{
|
||||
urn: syncItem.accFileVersionUrn,
|
||||
region: syncItem.accRegion as AccRegion
|
||||
},
|
||||
{ token: tokenData.access_token }
|
||||
)
|
||||
const isReady = isReadyForImport(manifest)
|
||||
|
||||
logger.info(
|
||||
{ isReady, syncItem, manifest },
|
||||
'Checking pending sync item {syncItem.id} for import readiness.'
|
||||
)
|
||||
|
||||
if (!isReady) continue
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: syncItem.projectId })
|
||||
|
||||
await triggerSyncItemAutomationFactory({
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import {
|
||||
getManifestByUrn,
|
||||
getToken,
|
||||
tryRegisterAccWebhook
|
||||
} from '@/modules/acc/clients/autodesk'
|
||||
import {
|
||||
AccSyncItemStatuses,
|
||||
ImporterAutomateFunctions
|
||||
} from '@/modules/acc/domain/constants'
|
||||
import { AccSyncItemEvents } from '@/modules/acc/domain/events'
|
||||
import { isReadyForImport } from '@/modules/acc/helpers/svfUtils'
|
||||
import { tryRegisterAccWebhook } from '@/modules/acc/clients/autodesk/acc'
|
||||
import { ImporterAutomateFunctions } from '@/modules/acc/domain/constants'
|
||||
import { AccSyncItemStatuses } from '@/modules/acc/domain/acc/constants'
|
||||
import { AccSyncItemEvents } from '@/modules/acc/domain/acc/events'
|
||||
import type {
|
||||
CountAccSyncItems,
|
||||
DeleteAccSyncItemById,
|
||||
GetAccSyncItemById,
|
||||
ListAccSyncItems,
|
||||
UpsertAccSyncItem
|
||||
} from '@/modules/acc/domain/operations'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
import { SyncItemNotFoundError } from '@/modules/acc/errors/acc'
|
||||
} from '@/modules/acc/domain/acc/operations'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/acc/types'
|
||||
import {
|
||||
SyncItemNotFoundError,
|
||||
SyncItemUnsupportedFileExtensionError
|
||||
} from '@/modules/acc/errors/acc'
|
||||
import type { TriggerSyncItemAutomation } from '@/modules/acc/services/automate'
|
||||
import type {
|
||||
CreateAutomation,
|
||||
@@ -82,8 +78,8 @@ export const createAccSyncItemFactory =
|
||||
automationId: automation.id,
|
||||
functions: [
|
||||
{
|
||||
functionId: ImporterAutomateFunctions.svf2.functionId,
|
||||
functionReleaseId: ImporterAutomateFunctions.svf2.functionReleaseId
|
||||
functionId: ImporterAutomateFunctions.rvt.functionId,
|
||||
functionReleaseId: ImporterAutomateFunctions.rvt.functionReleaseId
|
||||
}
|
||||
],
|
||||
triggerDefinitions: {
|
||||
@@ -114,7 +110,6 @@ export const createAccSyncItemFactory =
|
||||
|
||||
await deps.upsertAccSyncItem(newSyncItem)
|
||||
|
||||
// TODO ACC: somehow i could not managed to get subsriptions work, doing stupid timeout refetch in FE after create/delete/update
|
||||
// Once we have it properly TODO ogu: fix it on FE
|
||||
await deps.eventEmit({
|
||||
eventName: AccSyncItemEvents.Created,
|
||||
@@ -125,19 +120,14 @@ export const createAccSyncItemFactory =
|
||||
})
|
||||
|
||||
// Import new sync item immediately, if possible
|
||||
const tokenData = await getToken()
|
||||
const manifest = await getManifestByUrn(
|
||||
{
|
||||
urn: newSyncItem.accFileVersionUrn,
|
||||
region: newSyncItem.accRegion
|
||||
},
|
||||
{ token: tokenData.access_token }
|
||||
)
|
||||
const isReady = isReadyForImport(manifest)
|
||||
|
||||
if (!isReady) return newSyncItem
|
||||
|
||||
return await deps.triggerSyncItemAutomation({ id: newSyncItem.id })
|
||||
switch (newSyncItem.accFileExtension.toLowerCase()) {
|
||||
case 'rvt': {
|
||||
return await deps.triggerSyncItemAutomation({ id: newSyncItem.id })
|
||||
}
|
||||
default: {
|
||||
throw new SyncItemUnsupportedFileExtensionError(newSyncItem.accFileExtension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type GetPaginatedAccSyncItems = (params: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { AccSyncItemStatuses } from '@/modules/acc/domain/constants'
|
||||
import { AccSyncItemStatuses } from '@/modules/acc/domain/acc/constants'
|
||||
import type {
|
||||
QueryAllAccSyncItems,
|
||||
UpsertAccSyncItem
|
||||
} from '@/modules/acc/domain/operations'
|
||||
} from '@/modules/acc/domain/acc/operations'
|
||||
import { logger } from '@/observability/logging'
|
||||
|
||||
type OnVersionAdded = (params: {
|
||||
|
||||
@@ -92,8 +92,6 @@ import type {
|
||||
import { logger } from '@/observability/logging'
|
||||
import { getLastVersionsByProjectIdFactory } from '@/modules/core/repositories/versions'
|
||||
import type { StreamRoles } from '@speckle/shared'
|
||||
import { getAccSyncItemsByIdFactory } from '@/modules/acc/repositories/accSyncItems'
|
||||
import type { AccSyncItem } from '@/modules/acc/domain/types'
|
||||
|
||||
declare module '@/modules/core/loaders' {
|
||||
interface ModularizedDataLoaders extends ReturnType<typeof dataLoadersDefinition> {}
|
||||
@@ -141,7 +139,6 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
const getStreamsCollaboratorCounts = getStreamsCollaboratorCountsFactory({
|
||||
db
|
||||
})
|
||||
const getAccSyncItemsById = getAccSyncItemsByIdFactory({ db })
|
||||
|
||||
return {
|
||||
streams: {
|
||||
@@ -551,15 +548,6 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
return appIds.map((i) => results[i] || [])
|
||||
})
|
||||
},
|
||||
acc: {
|
||||
getAccSyncItem: createLoader<string, Nullable<AccSyncItem>>(async (ids) => {
|
||||
const results = keyBy(
|
||||
await getAccSyncItemsById({ ids: ids.slice() }),
|
||||
(i) => i.id
|
||||
)
|
||||
return ids.map((i) => results[i] || null)
|
||||
})
|
||||
},
|
||||
automations: {
|
||||
getAutomation: createLoader<string, Nullable<AutomationRecord>>(async (ids) => {
|
||||
const results = keyBy(
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { ActivityCollectionGraphQLReturn } from '@/modules/activitystream/h
|
||||
import type { ServerAppGraphQLReturn, ServerAppListItemGraphQLReturn } from '@/modules/auth/helpers/graphTypes';
|
||||
import type { GendoAIRenderGraphQLReturn } from '@/modules/gendo/helpers/types/graphTypes';
|
||||
import type { ServerRegionItemGraphQLReturn } from '@/modules/multiregion/helpers/graphTypes';
|
||||
import type { AccSyncItemGraphQLReturn, AccSyncItemMutationsGraphQLReturn } from '@/modules/acc/helpers/graphTypes';
|
||||
import type { WorkspaceIntegrationsGraphQLReturn, AccIntegrationGraphQLReturn, AccFolderGraphQLReturn, AccItemGraphQLReturn, AccSyncItemGraphQLReturn, AccSyncItemMutationsGraphQLReturn } from '@/modules/acc/helpers/graphTypes';
|
||||
import type { SavedViewGraphQLReturn, SavedViewGroupGraphQLReturn, SavedViewPermissionChecksGraphQLReturn, SavedViewGroupPermissionChecksGraphQLReturn, ProjectSavedViewsUpdatedMessageGraphQLReturn, ProjectSavedViewGroupsUpdatedMessageGraphQLReturn, BeforeChangeSavedViewGraphQLReturn, ExtendedViewerResourcesGraphQLReturn } from '@/modules/viewer/helpers/graphTypes';
|
||||
import type { DashboardGraphQLReturn, DashboardMutationsGraphQLReturn, DashboardPermissionChecksGraphQLReturn, DashboardTokenGraphQLReturn } from '@/modules/dashboards/helpers/graphTypes';
|
||||
import type { GraphQLContext } from '@/modules/shared/helpers/typeHelper';
|
||||
@@ -44,6 +44,113 @@ export type Scalars = {
|
||||
JSONObject: { input: Record<string, unknown>; output: Record<string, unknown>; }
|
||||
};
|
||||
|
||||
export type AccFolder = {
|
||||
__typename?: 'AccFolder';
|
||||
children: AccFolderCollection;
|
||||
contents: AccItemCollection;
|
||||
id: Scalars['ID']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AccFolderCollection = {
|
||||
__typename?: 'AccFolderCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<AccFolder>;
|
||||
};
|
||||
|
||||
export type AccHub = {
|
||||
__typename?: 'AccHub';
|
||||
id: Scalars['ID']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
project: AccProject;
|
||||
projects: AccProjectCollection;
|
||||
};
|
||||
|
||||
|
||||
export type AccHubProjectArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type AccHubCollection = {
|
||||
__typename?: 'AccHubCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<AccHub>;
|
||||
};
|
||||
|
||||
export type AccIntegration = {
|
||||
__typename?: 'AccIntegration';
|
||||
folder: AccFolder;
|
||||
hub: AccHub;
|
||||
hubs: AccHubCollection;
|
||||
item: AccItem;
|
||||
project: AccProject;
|
||||
};
|
||||
|
||||
|
||||
export type AccIntegrationFolderArgs = {
|
||||
folderId: Scalars['String']['input'];
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type AccIntegrationHubArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type AccIntegrationItemArgs = {
|
||||
itemId: Scalars['String']['input'];
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type AccIntegrationProjectArgs = {
|
||||
hubId: Scalars['String']['input'];
|
||||
projectId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type AccItem = {
|
||||
__typename?: 'AccItem';
|
||||
/** lineage urn */
|
||||
id: Scalars['ID']['output'];
|
||||
latestVersion: AccItemVersion;
|
||||
name: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type AccItemCollection = {
|
||||
__typename?: 'AccItemCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<AccItem>;
|
||||
};
|
||||
|
||||
export type AccItemVersion = {
|
||||
__typename?: 'AccItemVersion';
|
||||
fileType?: Maybe<Scalars['String']['output']>;
|
||||
/** version urn */
|
||||
id: Scalars['ID']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
versionNumber: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type AccProject = {
|
||||
__typename?: 'AccProject';
|
||||
folder: AccFolder;
|
||||
id: Scalars['ID']['output'];
|
||||
name: Scalars['String']['output'];
|
||||
rootFolder: AccFolder;
|
||||
};
|
||||
|
||||
|
||||
export type AccProjectFolderArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type AccProjectCollection = {
|
||||
__typename?: 'AccProjectCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<AccProject>;
|
||||
};
|
||||
|
||||
export type AccSyncItem = {
|
||||
__typename?: 'AccSyncItem';
|
||||
accFileExtension: Scalars['String']['output'];
|
||||
@@ -60,8 +167,8 @@ export type AccSyncItem = {
|
||||
author?: Maybe<LimitedUser>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
modelId: Scalars['String']['output'];
|
||||
projectId: Scalars['String']['output'];
|
||||
model?: Maybe<Model>;
|
||||
project: Project;
|
||||
status: AccSyncItemStatus;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
@@ -1762,6 +1869,7 @@ export type MarkReceivedVersionInput = {
|
||||
|
||||
export type Model = {
|
||||
__typename?: 'Model';
|
||||
accSyncItem?: Maybe<AccSyncItem>;
|
||||
author?: Maybe<LimitedUser>;
|
||||
automationsStatus?: Maybe<TriggeredAutomationsStatus>;
|
||||
/** Return a model tree of children */
|
||||
@@ -5497,6 +5605,7 @@ export type Workspace = {
|
||||
embedOptions: WorkspaceEmbedOptions;
|
||||
hasAccessToFeature: Scalars['Boolean']['output'];
|
||||
id: Scalars['ID']['output'];
|
||||
integrations?: Maybe<WorkspaceIntegrations>;
|
||||
/** Only available to workspace owners/members */
|
||||
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
|
||||
/** Exclusive workspaces do not allow their workspace members to create or join other workspaces as members. */
|
||||
@@ -5684,6 +5793,16 @@ export type WorkspaceIdentifier =
|
||||
{ id: Scalars['String']['input']; slug?: never; }
|
||||
| { id?: never; slug: Scalars['String']['input']; };
|
||||
|
||||
export type WorkspaceIntegrations = {
|
||||
__typename?: 'WorkspaceIntegrations';
|
||||
acc?: Maybe<AccIntegration>;
|
||||
};
|
||||
|
||||
|
||||
export type WorkspaceIntegrationsAccArgs = {
|
||||
token?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type WorkspaceInviteCreateInput = {
|
||||
/** Either this or userId must be filled */
|
||||
email?: InputMaybe<Scalars['String']['input']>;
|
||||
@@ -6264,6 +6383,16 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
|
||||
|
||||
/** Mapping between all available schema types and the resolvers types */
|
||||
export type ResolversTypes = {
|
||||
AccFolder: ResolverTypeWrapper<AccFolderGraphQLReturn>;
|
||||
AccFolderCollection: ResolverTypeWrapper<Omit<AccFolderCollection, 'items'> & { items: Array<ResolversTypes['AccFolder']> }>;
|
||||
AccHub: ResolverTypeWrapper<Omit<AccHub, 'project' | 'projects'> & { project: ResolversTypes['AccProject'], projects: ResolversTypes['AccProjectCollection'] }>;
|
||||
AccHubCollection: ResolverTypeWrapper<Omit<AccHubCollection, 'items'> & { items: Array<ResolversTypes['AccHub']> }>;
|
||||
AccIntegration: ResolverTypeWrapper<AccIntegrationGraphQLReturn>;
|
||||
AccItem: ResolverTypeWrapper<AccItemGraphQLReturn>;
|
||||
AccItemCollection: ResolverTypeWrapper<Omit<AccItemCollection, 'items'> & { items: Array<ResolversTypes['AccItem']> }>;
|
||||
AccItemVersion: ResolverTypeWrapper<AccItemVersion>;
|
||||
AccProject: ResolverTypeWrapper<Omit<AccProject, 'folder' | 'rootFolder'> & { folder: ResolversTypes['AccFolder'], rootFolder: ResolversTypes['AccFolder'] }>;
|
||||
AccProjectCollection: ResolverTypeWrapper<Omit<AccProjectCollection, 'items'> & { items: Array<ResolversTypes['AccProject']> }>;
|
||||
AccSyncItem: ResolverTypeWrapper<AccSyncItemGraphQLReturn>;
|
||||
AccSyncItemCollection: ResolverTypeWrapper<Omit<AccSyncItemCollection, 'items'> & { items: Array<ResolversTypes['AccSyncItem']> }>;
|
||||
AccSyncItemMutations: ResolverTypeWrapper<AccSyncItemMutationsGraphQLReturn>;
|
||||
@@ -6625,6 +6754,7 @@ export type ResolversTypes = {
|
||||
WorkspaceFeatureFlagName: WorkspaceFeatureFlagName;
|
||||
WorkspaceFeatureName: WorkspaceFeatureName;
|
||||
WorkspaceIdentifier: WorkspaceIdentifier;
|
||||
WorkspaceIntegrations: ResolverTypeWrapper<WorkspaceIntegrationsGraphQLReturn>;
|
||||
WorkspaceInviteCreateInput: WorkspaceInviteCreateInput;
|
||||
WorkspaceInviteLookupOptions: WorkspaceInviteLookupOptions;
|
||||
WorkspaceInviteMutations: ResolverTypeWrapper<WorkspaceInviteMutationsGraphQLReturn>;
|
||||
@@ -6674,6 +6804,16 @@ export type ResolversTypes = {
|
||||
|
||||
/** Mapping between all available schema types and the resolvers parents */
|
||||
export type ResolversParentTypes = {
|
||||
AccFolder: AccFolderGraphQLReturn;
|
||||
AccFolderCollection: Omit<AccFolderCollection, 'items'> & { items: Array<ResolversParentTypes['AccFolder']> };
|
||||
AccHub: Omit<AccHub, 'project' | 'projects'> & { project: ResolversParentTypes['AccProject'], projects: ResolversParentTypes['AccProjectCollection'] };
|
||||
AccHubCollection: Omit<AccHubCollection, 'items'> & { items: Array<ResolversParentTypes['AccHub']> };
|
||||
AccIntegration: AccIntegrationGraphQLReturn;
|
||||
AccItem: AccItemGraphQLReturn;
|
||||
AccItemCollection: Omit<AccItemCollection, 'items'> & { items: Array<ResolversParentTypes['AccItem']> };
|
||||
AccItemVersion: AccItemVersion;
|
||||
AccProject: Omit<AccProject, 'folder' | 'rootFolder'> & { folder: ResolversParentTypes['AccFolder'], rootFolder: ResolversParentTypes['AccFolder'] };
|
||||
AccProjectCollection: Omit<AccProjectCollection, 'items'> & { items: Array<ResolversParentTypes['AccProject']> };
|
||||
AccSyncItem: AccSyncItemGraphQLReturn;
|
||||
AccSyncItemCollection: Omit<AccSyncItemCollection, 'items'> & { items: Array<ResolversParentTypes['AccSyncItem']> };
|
||||
AccSyncItemMutations: AccSyncItemMutationsGraphQLReturn;
|
||||
@@ -7002,6 +7142,7 @@ export type ResolversParentTypes = {
|
||||
WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput;
|
||||
WorkspaceEmbedOptions: WorkspaceEmbedOptions;
|
||||
WorkspaceIdentifier: WorkspaceIdentifier;
|
||||
WorkspaceIntegrations: WorkspaceIntegrationsGraphQLReturn;
|
||||
WorkspaceInviteCreateInput: WorkspaceInviteCreateInput;
|
||||
WorkspaceInviteLookupOptions: WorkspaceInviteLookupOptions;
|
||||
WorkspaceInviteMutations: WorkspaceInviteMutationsGraphQLReturn;
|
||||
@@ -7076,6 +7217,78 @@ export type IsOwnerDirectiveArgs = { };
|
||||
|
||||
export type IsOwnerDirectiveResolver<Result, Parent, ContextType = GraphQLContext, Args = IsOwnerDirectiveArgs> = DirectiveResolverFn<Result, Parent, ContextType, Args>;
|
||||
|
||||
export type AccFolderResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccFolder'] = ResolversParentTypes['AccFolder']> = {
|
||||
children?: Resolver<ResolversTypes['AccFolderCollection'], ParentType, ContextType>;
|
||||
contents?: Resolver<ResolversTypes['AccItemCollection'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccFolderCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccFolderCollection'] = ResolversParentTypes['AccFolderCollection']> = {
|
||||
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
items?: Resolver<Array<ResolversTypes['AccFolder']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccHubResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccHub'] = ResolversParentTypes['AccHub']> = {
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
project?: Resolver<ResolversTypes['AccProject'], ParentType, ContextType, RequireFields<AccHubProjectArgs, 'id'>>;
|
||||
projects?: Resolver<ResolversTypes['AccProjectCollection'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccHubCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccHubCollection'] = ResolversParentTypes['AccHubCollection']> = {
|
||||
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
items?: Resolver<Array<ResolversTypes['AccHub']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccIntegrationResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccIntegration'] = ResolversParentTypes['AccIntegration']> = {
|
||||
folder?: Resolver<ResolversTypes['AccFolder'], ParentType, ContextType, RequireFields<AccIntegrationFolderArgs, 'folderId' | 'projectId'>>;
|
||||
hub?: Resolver<ResolversTypes['AccHub'], ParentType, ContextType, RequireFields<AccIntegrationHubArgs, 'id'>>;
|
||||
hubs?: Resolver<ResolversTypes['AccHubCollection'], ParentType, ContextType>;
|
||||
item?: Resolver<ResolversTypes['AccItem'], ParentType, ContextType, RequireFields<AccIntegrationItemArgs, 'itemId' | 'projectId'>>;
|
||||
project?: Resolver<ResolversTypes['AccProject'], ParentType, ContextType, RequireFields<AccIntegrationProjectArgs, 'hubId' | 'projectId'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccItemResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccItem'] = ResolversParentTypes['AccItem']> = {
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
latestVersion?: Resolver<ResolversTypes['AccItemVersion'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccItemCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccItemCollection'] = ResolversParentTypes['AccItemCollection']> = {
|
||||
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
items?: Resolver<Array<ResolversTypes['AccItem']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccItemVersionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccItemVersion'] = ResolversParentTypes['AccItemVersion']> = {
|
||||
fileType?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
versionNumber?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccProjectResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccProject'] = ResolversParentTypes['AccProject']> = {
|
||||
folder?: Resolver<ResolversTypes['AccFolder'], ParentType, ContextType, RequireFields<AccProjectFolderArgs, 'id'>>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
rootFolder?: Resolver<ResolversTypes['AccFolder'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccProjectCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccProjectCollection'] = ResolversParentTypes['AccProjectCollection']> = {
|
||||
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
items?: Resolver<Array<ResolversTypes['AccProject']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type AccSyncItemResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['AccSyncItem'] = ResolversParentTypes['AccSyncItem']> = {
|
||||
accFileExtension?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
accFileLineageUrn?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
@@ -7091,8 +7304,8 @@ export type AccSyncItemResolvers<ContextType = GraphQLContext, ParentType extend
|
||||
author?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
|
||||
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
modelId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
projectId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
model?: Resolver<Maybe<ResolversTypes['Model']>, ParentType, ContextType>;
|
||||
project?: Resolver<ResolversTypes['Project'], ParentType, ContextType>;
|
||||
status?: Resolver<ResolversTypes['AccSyncItemStatus'], ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@@ -7811,6 +8024,7 @@ export type LimitedWorkspaceJoinRequestCollectionResolvers<ContextType = GraphQL
|
||||
};
|
||||
|
||||
export type ModelResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Model'] = ResolversParentTypes['Model']> = {
|
||||
accSyncItem?: Resolver<Maybe<ResolversTypes['AccSyncItem']>, ParentType, ContextType>;
|
||||
author?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
|
||||
automationsStatus?: Resolver<Maybe<ResolversTypes['TriggeredAutomationsStatus']>, ParentType, ContextType>;
|
||||
childrenTree?: Resolver<Array<ResolversTypes['ModelsTreeItem']>, ParentType, ContextType>;
|
||||
@@ -8968,6 +9182,7 @@ export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends
|
||||
embedOptions?: Resolver<ResolversTypes['WorkspaceEmbedOptions'], ParentType, ContextType>;
|
||||
hasAccessToFeature?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<WorkspaceHasAccessToFeatureArgs, 'featureName'>>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
integrations?: Resolver<Maybe<ResolversTypes['WorkspaceIntegrations']>, ParentType, ContextType>;
|
||||
invitedTeam?: Resolver<Maybe<Array<ResolversTypes['PendingWorkspaceCollaborator']>>, ParentType, ContextType, Partial<WorkspaceInvitedTeamArgs>>;
|
||||
isExclusive?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
logo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
@@ -9038,6 +9253,11 @@ export type WorkspaceEmbedOptionsResolvers<ContextType = GraphQLContext, ParentT
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceIntegrationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceIntegrations'] = ResolversParentTypes['WorkspaceIntegrations']> = {
|
||||
acc?: Resolver<Maybe<ResolversTypes['AccIntegration']>, ParentType, ContextType, Partial<WorkspaceIntegrationsAccArgs>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type WorkspaceInviteMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceInviteMutations'] = ResolversParentTypes['WorkspaceInviteMutations']> = {
|
||||
batchCreate?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceInviteMutationsBatchCreateArgs, 'input' | 'workspaceId'>>;
|
||||
cancel?: Resolver<ResolversTypes['Workspace'], ParentType, ContextType, RequireFields<WorkspaceInviteMutationsCancelArgs, 'inviteId' | 'workspaceId'>>;
|
||||
@@ -9220,6 +9440,16 @@ export type WorkspaceUpdatedMessageResolvers<ContextType = GraphQLContext, Paren
|
||||
};
|
||||
|
||||
export type Resolvers<ContextType = GraphQLContext> = {
|
||||
AccFolder?: AccFolderResolvers<ContextType>;
|
||||
AccFolderCollection?: AccFolderCollectionResolvers<ContextType>;
|
||||
AccHub?: AccHubResolvers<ContextType>;
|
||||
AccHubCollection?: AccHubCollectionResolvers<ContextType>;
|
||||
AccIntegration?: AccIntegrationResolvers<ContextType>;
|
||||
AccItem?: AccItemResolvers<ContextType>;
|
||||
AccItemCollection?: AccItemCollectionResolvers<ContextType>;
|
||||
AccItemVersion?: AccItemVersionResolvers<ContextType>;
|
||||
AccProject?: AccProjectResolvers<ContextType>;
|
||||
AccProjectCollection?: AccProjectCollectionResolvers<ContextType>;
|
||||
AccSyncItem?: AccSyncItemResolvers<ContextType>;
|
||||
AccSyncItemCollection?: AccSyncItemCollectionResolvers<ContextType>;
|
||||
AccSyncItemMutations?: AccSyncItemMutationsResolvers<ContextType>;
|
||||
@@ -9414,6 +9644,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
WorkspaceCreationState?: WorkspaceCreationStateResolvers<ContextType>;
|
||||
WorkspaceDomain?: WorkspaceDomainResolvers<ContextType>;
|
||||
WorkspaceEmbedOptions?: WorkspaceEmbedOptionsResolvers<ContextType>;
|
||||
WorkspaceIntegrations?: WorkspaceIntegrationsResolvers<ContextType>;
|
||||
WorkspaceInviteMutations?: WorkspaceInviteMutationsResolvers<ContextType>;
|
||||
WorkspaceJoinRequest?: WorkspaceJoinRequestResolvers<ContextType>;
|
||||
WorkspaceJoinRequestCollection?: WorkspaceJoinRequestCollectionResolvers<ContextType>;
|
||||
|
||||
@@ -548,5 +548,13 @@ export function getAutodeskIntegrationClientSecret() {
|
||||
return getStringFromEnv('AUTODESK_INTEGRATION_CLIENT_SECRET')
|
||||
}
|
||||
|
||||
export function getOdaUserId() {
|
||||
return getStringFromEnv('ODA_USER_ID')
|
||||
}
|
||||
|
||||
export function getOdaUserSecret() {
|
||||
return getStringFromEnv('ODA_USER_SECRET')
|
||||
}
|
||||
|
||||
export const areSavedViewsEnabled = (): boolean =>
|
||||
getFeatureFlags().FF_SAVED_VIEWS_ENABLED
|
||||
|
||||
@@ -56,6 +56,8 @@ export type GraphQLContext = BaseContext &
|
||||
authPolicies: AuthPolicies & {
|
||||
clearCache: () => void
|
||||
}
|
||||
// TODO: Remove in favor of `x-user-id` header
|
||||
accToken?: string
|
||||
/**
|
||||
* Request-scoped GraphQL dataloaders
|
||||
* @see https://github.com/graphql/dataloader
|
||||
|
||||
@@ -56,7 +56,7 @@ import type {
|
||||
import type {
|
||||
accSyncItemEventsNamespace,
|
||||
AccSyncItemEventsPayloads
|
||||
} from '@/modules/acc/domain/events'
|
||||
} from '@/modules/acc/domain/acc/events'
|
||||
import type {
|
||||
emailsEventNamespace,
|
||||
EmailsEventsPayloads
|
||||
|
||||
+1
-1
@@ -14,5 +14,5 @@ declare module 'http' {
|
||||
export type AccSessionData = {
|
||||
accTokens?: AccTokens
|
||||
codeVerifier?: string
|
||||
projectId?: string
|
||||
callbackEndpoint?: string
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ export type AccTokens = {
|
||||
access_token: string
|
||||
refresh_token: string
|
||||
token_type: string
|
||||
id_token: string
|
||||
id_token?: string | undefined
|
||||
expires_in: number
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export type AccUserInfo = {
|
||||
|
||||
@@ -622,6 +622,15 @@ Generate the environment variables for Speckle server and Speckle objects deploy
|
||||
secretKeyRef:
|
||||
name: {{ default .Values.secretName .Values.server.accIntegration.clientSecret.secretName }}
|
||||
key: {{ default "acc_integration_client_secret" .Values.server.accIntegration.clientSecret.secretKey }}
|
||||
|
||||
- name: ODA_USER_ID
|
||||
value: {{ default "user_id" .Values.server.oda.userId }}
|
||||
|
||||
- name: ODA_USER_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ default .Values.secretName .Values.server.oda.userSecret.secretName }}
|
||||
key: {{ default "user_secret" .Values.server.oda.userSecret.secretKey }}
|
||||
{{- end }}
|
||||
|
||||
- name: FF_DASHBOARDS_MODULE_ENABLED
|
||||
|
||||
@@ -54,6 +54,7 @@ secrets:
|
||||
{{- end }}
|
||||
{{- if .Values.featureFlags.accIntegrationEnabled }}
|
||||
- name: {{ default .Values.secretName .Values.server.accIntegration.clientSecret.secretName }}
|
||||
- name: {{ default .Values.secretName .Values.server.oda.userSecret.secretName }}
|
||||
{{- end }}
|
||||
{{- if .Values.featureFlags.nextGenFileImporterEnabled }}
|
||||
- name: {{ default .Values.secretName .Values.ifc_import_service.db.connectionString.secretName }}
|
||||
|
||||
@@ -54,6 +54,7 @@ secrets:
|
||||
{{- end }}
|
||||
{{- if .Values.featureFlags.accIntegrationEnabled }}
|
||||
- name: {{ default .Values.secretName .Values.server.accIntegration.clientSecret.secretName }}
|
||||
- name: {{ default .Values.secretName .Values.server.oda.userSecret.secretName }}
|
||||
{{- end }}
|
||||
{{- if .Values.featureFlags.nextGenFileImporterEnabled }}
|
||||
- name: {{ default .Values.secretName .Values.ifc_import_service.db.connectionString.secretName }}
|
||||
|
||||
Reference in New Issue
Block a user