Merge branch 'main' of github.com:specklesystems/speckle-server into alessandro/web-2944-versions-limits
This commit is contained in:
@@ -68,6 +68,8 @@ minio-data/
|
||||
postgres-data/
|
||||
redis-data/
|
||||
|
||||
packages/fileimport-service/src/ifc-dotnet/output
|
||||
|
||||
.tshy-build
|
||||
obj/
|
||||
bin/
|
||||
|
||||
@@ -11,3 +11,35 @@ The File Import service can parse either STL, OBJ, or IFC files using external p
|
||||
The parsers are responsible for extracting the necessary data from the files and storing it in the database. They are also responsible for creating a new Speckle model if necessary.
|
||||
|
||||
The service is then responsible for updating the status of the `file_uploads` table, and for posting a Postgres notification.
|
||||
|
||||
## Dev setup
|
||||
|
||||
### Building/Running the .NET importer
|
||||
|
||||
Requirements:
|
||||
|
||||
- Ubuntu 24+
|
||||
|
||||
Do this on Ubuntu/OSX to install dotnet:
|
||||
|
||||
```bash
|
||||
# Add microsoft package repo
|
||||
sudo apt update && sudo apt install -y wget apt-transport-https
|
||||
wget https://packages.microsoft.com/config/ubuntu/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
|
||||
sudo dpkg -i packages-microsoft-prod.deb
|
||||
|
||||
# Install dotnet sdk 8
|
||||
sudo apt update
|
||||
sudo apt install -y dotnet-sdk-8.0
|
||||
|
||||
# Verify version
|
||||
dotnet --version
|
||||
```
|
||||
|
||||
Do this to build:
|
||||
|
||||
```bash
|
||||
cd ./packages/fileimport-service/src/ifc-dotnet
|
||||
|
||||
dotnet publish ifc-converter.csproj -c Release -o output/
|
||||
```
|
||||
|
||||
@@ -16,6 +16,7 @@ import { logger } from '@/observability/logging.js'
|
||||
import { Nullable, Scopes, wait } from '@speckle/shared'
|
||||
import { Knex } from 'knex'
|
||||
import { Logger } from 'pino'
|
||||
import { getIfcDllPath, useLegacyIfcImporter } from '@/controller/helpers/env.js'
|
||||
|
||||
const HEALTHCHECK_FILE_PATH = '/tmp/last_successful_query'
|
||||
|
||||
@@ -153,7 +154,10 @@ async function doTask(
|
||||
taskLogger.info('Triggering importer for {fileType}')
|
||||
|
||||
if (info.fileType.toLowerCase() === 'ifc') {
|
||||
if (info.fileName.toLowerCase().endsWith('.legacyimporter.ifc')) {
|
||||
if (
|
||||
info.fileName.toLowerCase().endsWith('.legacyimporter.ifc') ||
|
||||
useLegacyIfcImporter()
|
||||
) {
|
||||
await runProcessWithTimeout(
|
||||
taskLogger,
|
||||
process.env['NODE_BINARY_PATH'] || 'node',
|
||||
@@ -181,8 +185,7 @@ async function doTask(
|
||||
taskLogger,
|
||||
process.env['DOTNET_BINARY_PATH'] || 'dotnet',
|
||||
[
|
||||
process.env['IFC_DOTNET_DLL_PATH'] ||
|
||||
'/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll',
|
||||
getIfcDllPath(),
|
||||
TMP_FILE_PATH,
|
||||
TMP_RESULTS_PATH,
|
||||
info.streamId,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import path from 'node:path'
|
||||
import url from 'node:url'
|
||||
import file from 'node:fs'
|
||||
|
||||
export const isDevEnv = () => {
|
||||
return process.env.NODE_ENV === 'development'
|
||||
}
|
||||
|
||||
export const isTestEnv = () => {
|
||||
return process.env.NODE_ENV === 'test'
|
||||
}
|
||||
|
||||
export const isDevOrTestEnv = () => isDevEnv() || isTestEnv()
|
||||
|
||||
export const useLegacyIfcImporter = () => {
|
||||
return ['true', '1'].includes(process.env.USE_LEGACY_IFC_IMPORTER || 'false')
|
||||
}
|
||||
|
||||
export const getPackageRootDirPath = () => {
|
||||
const __filename = url.fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
let root = path.resolve(__dirname, '../../../')
|
||||
if (root.endsWith('dist')) {
|
||||
// Resolved path may differ depending on whether running from dist or src (w/ ts-node)
|
||||
root = path.resolve(root, '../')
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
let cachedIfcDllPath: string | undefined = undefined
|
||||
export const getIfcDllPath = () => {
|
||||
if (cachedIfcDllPath) return cachedIfcDllPath
|
||||
|
||||
const absolutePath = process.env['IFC_DOTNET_DLL_PATH']
|
||||
if (absolutePath && file.existsSync(absolutePath)) {
|
||||
cachedIfcDllPath = absolutePath
|
||||
return absolutePath
|
||||
}
|
||||
|
||||
if (isDevOrTestEnv()) {
|
||||
const possiblePath = path.resolve(
|
||||
getPackageRootDirPath(),
|
||||
'./src/ifc-dotnet/output/ifc-converter.dll'
|
||||
)
|
||||
if (file.existsSync(possiblePath)) {
|
||||
cachedIfcDllPath = absolutePath
|
||||
return possiblePath
|
||||
}
|
||||
}
|
||||
|
||||
const fallback =
|
||||
'/speckle-server/packages/fileimport-service/src/ifc-dotnet/ifc-converter.dll'
|
||||
if (file.existsSync(fallback)) {
|
||||
cachedIfcDllPath = fallback
|
||||
return fallback
|
||||
}
|
||||
|
||||
throw new Error('Could not resolve .NET IFC DLL')
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import Observability from '@speckle/shared/dist/commonjs/observability/index.js'
|
||||
export const logger = Observability.extendLoggerComponent(
|
||||
Observability.getLogger(
|
||||
process.env.LOG_LEVEL || 'info',
|
||||
process.env.LOG_PRETTY === 'true'
|
||||
process.env.LOG_PRETTY === 'true' && !process.env.FORCE_NO_PRETTY
|
||||
),
|
||||
'fileimport-service'
|
||||
)
|
||||
|
||||
@@ -2,18 +2,7 @@
|
||||
<div>
|
||||
<!-- Current State -->
|
||||
<CommonCard class="!p-2 !pr-3 !border-outline-3 !bg-foundation-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-2 rounded-full border border-outline-3 bg-foundation">
|
||||
<component :is="currentState.icon" class="w-5 h-5" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-foreground">{{ currentState.title }}</div>
|
||||
<div class="text-foreground-2 text-body-2xs">
|
||||
{{ currentState.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto text-foreground-2 font-medium">Current</div>
|
||||
</div>
|
||||
<slot name="current-state" />
|
||||
</CommonCard>
|
||||
|
||||
<!-- Arrow -->
|
||||
@@ -25,36 +14,11 @@
|
||||
<CommonCard
|
||||
class="!p-2 !pr-3 !border-blue-300 !bg-blue-50 dark:!border-blue-800 dark:!bg-blue-950"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="p-2.5 rounded-full border border-blue-300 dark:border-blue-800 bg-foundation"
|
||||
>
|
||||
<component :is="newState.icon" class="w-4 h-4" />
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-foreground">{{ newState.title }}</div>
|
||||
<div class="text-foreground-2 text-body-2xs">
|
||||
{{ newState.description }}
|
||||
</div>
|
||||
</div>
|
||||
<slot name="price" />
|
||||
</div>
|
||||
<slot name="new-state" />
|
||||
</CommonCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ArrowDownIcon } from '@heroicons/vue/20/solid'
|
||||
import type { ConcreteComponent } from 'vue'
|
||||
|
||||
type TransitionCard = {
|
||||
icon: ConcreteComponent
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
currentState: TransitionCard
|
||||
newState: TransitionCard
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
<template>
|
||||
<HeaderWorkspaceSwitcherHeader name="Projects" :to="projectsRoute">
|
||||
<p class="text-body-2xs text-foreground-2 truncate">2 projects to move</p>
|
||||
<p class="text-body-2xs text-foreground-2 truncate">
|
||||
{{ text }}
|
||||
</p>
|
||||
</HeaderWorkspaceSwitcherHeader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { projectsRoute } from '~/lib/common/helpers/route'
|
||||
import { useActiveUserProjectsToMove } from '~~/lib/auth/composables/activeUser'
|
||||
|
||||
const { projectsToMoveCount } = useActiveUserProjectsToMove()
|
||||
|
||||
const text = computed(() => {
|
||||
if (!projectsToMoveCount.value) return 'No projects to move'
|
||||
return `${projectsToMoveCount.value} project${
|
||||
projectsToMoveCount.value === 1 ? '' : 's'
|
||||
} to move`
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<HeaderNavNotificationsInvite
|
||||
:invite="invite"
|
||||
:disabled="loading"
|
||||
is-workspace-invite
|
||||
@processed="processInvite"
|
||||
>
|
||||
<template #message>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
ref="selectUsers"
|
||||
:invites="invites"
|
||||
:allowed-domains="allowedDomains"
|
||||
:target-role="selectedRole"
|
||||
>
|
||||
<p class="text-body-2xs text-foreground-2 leading-5">
|
||||
{{ infoText }}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
isEmailOrEmpty,
|
||||
canHaveRole({
|
||||
allowedDomains: props.allowedDomains,
|
||||
workspaceRole: item.value.workspaceRole
|
||||
workspaceRole: props.targetRole
|
||||
})
|
||||
]"
|
||||
:help="
|
||||
@@ -78,6 +78,7 @@ const props = defineProps<{
|
||||
invites: InviteWorkspaceItem[]
|
||||
allowedDomains: MaybeNullOrUndefined<string[]>
|
||||
showWorkspaceRoles?: boolean
|
||||
targetRole?: WorkspaceRoles
|
||||
}>()
|
||||
|
||||
const { handleSubmit } = useForm<InviteWorkspaceForm>({
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
import type {
|
||||
FullPermissionCheckResultFragment,
|
||||
ProjectPageModelsActionsFragment,
|
||||
ProjectPageModelsActions_ProjectFragment
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
@@ -86,7 +87,8 @@ const props = defineProps<{
|
||||
open?: boolean
|
||||
model: ProjectPageModelsActionsFragment
|
||||
project: ProjectPageModelsActions_ProjectFragment
|
||||
canEdit?: boolean
|
||||
canEdit?: FullPermissionCheckResultFragment
|
||||
canDelete?: FullPermissionCheckResultFragment
|
||||
menuPosition?: HorizontalDirection
|
||||
}>()
|
||||
|
||||
@@ -100,7 +102,6 @@ const showActionsMenu = ref(false)
|
||||
const openDialog = ref(null as Nullable<ActionTypes>)
|
||||
const embedDialogOpen = ref(false)
|
||||
|
||||
const isMain = computed(() => props.model.name === 'main')
|
||||
const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
...(isLoggedIn.value
|
||||
? [
|
||||
@@ -108,8 +109,8 @@ const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
{
|
||||
title: 'Edit model...',
|
||||
id: ActionTypes.Rename,
|
||||
disabled: !props.canEdit,
|
||||
disabledTooltip: 'Insufficient permissions'
|
||||
disabled: !props.canEdit?.authorized,
|
||||
disabledTooltip: props.canEdit?.message || 'Insufficient permissions'
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -119,12 +120,16 @@ const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
title: 'View versions',
|
||||
id: ActionTypes.ViewVersions
|
||||
},
|
||||
{
|
||||
title: 'Upload new version...',
|
||||
id: ActionTypes.UploadVersion,
|
||||
disabled: !props.canEdit,
|
||||
disabledTooltip: 'Insufficient permissions'
|
||||
}
|
||||
...(isLoggedIn.value
|
||||
? [
|
||||
{
|
||||
title: 'Upload new version...',
|
||||
id: ActionTypes.UploadVersion,
|
||||
disabled: !props.canEdit?.authorized,
|
||||
disabledTooltip: props.canEdit?.message || 'Insufficient permissions'
|
||||
}
|
||||
]
|
||||
: [])
|
||||
],
|
||||
[
|
||||
{ title: 'Copy link', id: ActionTypes.Share },
|
||||
@@ -137,8 +142,9 @@ const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
{
|
||||
title: 'Delete...',
|
||||
id: ActionTypes.Delete,
|
||||
disabled: isMain.value || !props.canEdit,
|
||||
disabledTooltip: 'Insufficient permissions'
|
||||
// TODO:
|
||||
disabled: !props.canDelete?.authorized,
|
||||
disabledTooltip: props.canDelete?.message || 'Insufficient permissions'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
:model="model"
|
||||
:project="project"
|
||||
:can-edit="canEdit"
|
||||
:can-delete="canDelete"
|
||||
@click.stop.prevent
|
||||
@upload-version="triggerVersionUpload"
|
||||
/>
|
||||
@@ -131,7 +132,6 @@ import type {
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { modelVersionsRoute, modelRoute } from '~~/lib/common/helpers/route'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { canModifyModels } from '~~/lib/projects/helpers/permissions'
|
||||
import { isPendingModelFragment } from '~~/lib/projects/helpers/models'
|
||||
import type { Nullable, Optional } from '@speckle/shared'
|
||||
|
||||
@@ -167,9 +167,6 @@ const props = withDefaults(
|
||||
}
|
||||
)
|
||||
|
||||
// TODO: Get rid of this, its not reactive. Is it even necessary?
|
||||
provide('projectId', props.projectId)
|
||||
|
||||
const router = useRouter()
|
||||
const isAutomateModuleEnabled = useIsAutomateModuleEnabled()
|
||||
|
||||
@@ -215,7 +212,13 @@ const updatedAtFullDate = computed(() => {
|
||||
: props.model.updatedAt
|
||||
})
|
||||
|
||||
const canEdit = computed(() => (props.project ? canModifyModels(props.project) : false))
|
||||
const canEdit = computed(() =>
|
||||
isPendingModelFragment(props.model) ? undefined : props.model.permissions.canUpdate
|
||||
)
|
||||
const canDelete = computed(() =>
|
||||
isPendingModelFragment(props.model) ? undefined : props.model.permissions.canDelete
|
||||
)
|
||||
|
||||
const versionCount = computed(() => {
|
||||
return isPendingModelFragment(props.model) ? 0 : props.model.versionCount.totalCount
|
||||
})
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
View all in 3D
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="canContribute"
|
||||
v-tippy="project?.workspace?.readOnly ? 'Workspace is read-only' : ''"
|
||||
v-tippy="
|
||||
canCreateModel?.authorized
|
||||
? undefined
|
||||
: canCreateModel?.message ||
|
||||
'You do not have permission to create models'
|
||||
"
|
||||
:disabled="!canCreateModel?.authorized"
|
||||
class="grow inline-flex sm:grow-0 lg:hidden"
|
||||
:disabled="project?.workspace?.readOnly"
|
||||
@click="showNewDialog = true"
|
||||
>
|
||||
New model
|
||||
@@ -76,10 +80,14 @@
|
||||
View all in 3D
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="canContribute"
|
||||
v-tippy="project?.workspace?.readOnly ? 'Workspace is read-only' : ''"
|
||||
v-tippy="
|
||||
canCreateModel?.authorized
|
||||
? undefined
|
||||
: canCreateModel?.message ||
|
||||
'You do not have permission to create models'
|
||||
"
|
||||
:disabled="!canCreateModel?.authorized"
|
||||
class="hidden lg:inline-flex shrink-0"
|
||||
:disabled="project?.workspace?.readOnly"
|
||||
@click="showNewDialog = true"
|
||||
>
|
||||
New model
|
||||
@@ -101,7 +109,6 @@ import type {
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { modelRoute } from '~~/lib/common/helpers/route'
|
||||
import type { GridListToggleValue } from '~~/lib/layout/helpers/components'
|
||||
import { canModifyModels } from '~~/lib/projects/helpers/permissions'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -130,6 +137,11 @@ graphql(`
|
||||
id
|
||||
readOnly
|
||||
}
|
||||
permissions {
|
||||
canCreateModel {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -160,9 +172,7 @@ const onViewAllClick = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const canContribute = computed(() =>
|
||||
props.project ? canModifyModels(props.project) : false
|
||||
)
|
||||
const canCreateModel = computed(() => props.project?.permissions.canCreateModel)
|
||||
const showNewDialog = ref(false)
|
||||
|
||||
const debouncedSearch = computed({
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
<ProjectPageModelsStructureItem
|
||||
:item="item"
|
||||
:project="project"
|
||||
:can-contribute="canContribute"
|
||||
:is-search-result="isUsingSearch"
|
||||
@model-updated="onModelUpdated"
|
||||
@create-submodel="onCreateSubmodel"
|
||||
@@ -58,7 +57,6 @@ import {
|
||||
projectModelsTreeTopLevelQuery,
|
||||
projectModelsTreeTopLevelPaginationQuery
|
||||
} from '~~/lib/projects/graphql/queries'
|
||||
import { canModifyModels } from '~~/lib/projects/helpers/permissions'
|
||||
import type { Nullable, SourceAppDefinition } from '@speckle/shared'
|
||||
import type { InfiniteLoaderState } from '~~/lib/global/helpers/components'
|
||||
import { useEvictProjectModelFields } from '~~/lib/projects/composables/modelManagement'
|
||||
@@ -156,9 +154,7 @@ const topLevelItems = computed(
|
||||
props.disablePagination ? 8 : undefined
|
||||
)
|
||||
)
|
||||
const canContribute = computed(() =>
|
||||
props.project ? canModifyModels(props.project) : false
|
||||
)
|
||||
|
||||
const isUsingSearch = computed(() => !!resultVariables.value?.filter?.search)
|
||||
const moreToLoad = computed(
|
||||
() =>
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
v-model:open="showActionsMenu"
|
||||
:model="model"
|
||||
:project="project"
|
||||
:can-edit="canContribute"
|
||||
:can-edit="canEdit"
|
||||
:can-delete="canDelete"
|
||||
:menu-position="
|
||||
itemType === StructureItemType.EmptyModel
|
||||
? HorizontalDirection.Right
|
||||
@@ -82,7 +83,7 @@
|
||||
{{ updatedAt.relative }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-x-2 flex flex-row pils">
|
||||
<div class="space-x-2 flex flex-row">
|
||||
<div class="text-body-xs text-foreground flex items-center space-x-1 pl-2">
|
||||
<IconDiscussions class="w-4 h-4" />
|
||||
<span>{{ model?.commentThreadCount.totalCount }}</span>
|
||||
@@ -204,14 +205,13 @@
|
||||
<ProjectPageModelsStructureItem
|
||||
:item="child"
|
||||
:project="project"
|
||||
:can-contribute="canContribute"
|
||||
class="flex-grow"
|
||||
@model-updated="onModelUpdated"
|
||||
@create-submodel="emit('create-submodel', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="canContribute" class="mr-8"></div>
|
||||
<div v-if="canEdit" class="mr-8"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,12 +282,9 @@ const emit = defineEmits<{
|
||||
const props = defineProps<{
|
||||
item: SingleLevelModelTreeItemFragment | PendingFileUploadFragment
|
||||
project: ProjectPageModelsStructureItem_ProjectFragment
|
||||
canContribute?: boolean
|
||||
isSearchResult?: boolean
|
||||
}>()
|
||||
|
||||
provide('projectId', props.project.id)
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const importArea = ref(
|
||||
@@ -307,6 +304,13 @@ const trackFederateModels = () =>
|
||||
|
||||
const showActionsMenu = ref(false)
|
||||
|
||||
const canEdit = computed(() =>
|
||||
isPendingFileUpload(props.item) ? undefined : props.item.model?.permissions.canUpdate
|
||||
)
|
||||
const canDelete = computed(() =>
|
||||
isPendingFileUpload(props.item) ? undefined : props.item.model?.permissions.canDelete
|
||||
)
|
||||
|
||||
const itemType = computed<StructureItemType>(() => {
|
||||
if (isPendingFileUpload(props.item)) return StructureItemType.PendingModel
|
||||
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutDialog v-model:open="open" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Confirm move</template>
|
||||
<BillingTransitionCards
|
||||
:current-state="transitionItems.project"
|
||||
:new-state="transitionItems.workspace"
|
||||
>
|
||||
<template #current-state>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-foreground-2 text-body-3xs">Project</div>
|
||||
<div class="flex items-center gap-4 justify-between">
|
||||
<div class="text-heading-sm mt-1">{{ project.name }}</div>
|
||||
<div class="text-body-2xs font-medium">(count) models</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #new-state>
|
||||
<div class="flex flex-col">
|
||||
<div class="text-foreground-2 text-body-3xs">Workspace</div>
|
||||
<div class="text-heading-sm mt-1">{{ workspace.name }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</BillingTransitionCards>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<p class="text-body-2xs text-foreground-2 mt-4">
|
||||
The project, including all its data, will be moved to the workspace, where all
|
||||
existing members will have access by default.
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-if="dryRunResultMembers.length > 0"
|
||||
class="pt-2 gap-y-2 flex flex-col border-t border-outline-3"
|
||||
>
|
||||
<p class="text-body-2xs text-foreground-2 mt-2 mb-1">
|
||||
{{
|
||||
dryRunResultMembers.length === 1
|
||||
? '1 person will also be added as a free member to the workspace.'
|
||||
: `${dryRunResultMembers.length} people will also be added as free members to the workspace.`
|
||||
}}
|
||||
</p>
|
||||
<div class="w-full">
|
||||
<div
|
||||
v-for="user in dryRunResultMembers"
|
||||
:key="`dry-run-user-${user.id}`"
|
||||
class="flex items-center py-1.5 px-2 border-t border-x last:border-b border-outline-3 first:rounded-t-lg last:rounded-b-lg gap-x-1.5"
|
||||
>
|
||||
<UserAvatar hide-tooltip :user="user" size="sm" />
|
||||
<p class="text-foreground text-body-2xs">{{ user.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="dryRunResultMembersInfoText" class="text-body-2xs text-foreground-2">
|
||||
{{ dryRunResultMembersInfoText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
<WorkspaceRegionStaticDataDisclaimer
|
||||
v-if="showRegionStaticDataDisclaimer"
|
||||
v-model:open="showRegionStaticDataDisclaimer"
|
||||
:variant="RegionStaticDataDisclaimerVariant.MoveProjectIntoWorkspace"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import type {
|
||||
ProjectsMoveToWorkspaceDialog_ProjectFragment,
|
||||
ProjectsMoveToWorkspaceDialog_WorkspaceFragment
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { moveToWorkspaceDryRunQuery } from '~/lib/projects/graphql/queries'
|
||||
import { useMoveProjectToWorkspace } from '~/lib/projects/composables/projectManagement'
|
||||
import {
|
||||
useWorkspaceCustomDataResidencyDisclaimer,
|
||||
RegionStaticDataDisclaimerVariant
|
||||
} from '~/lib/workspaces/composables/region'
|
||||
|
||||
const props = defineProps<{
|
||||
project: ProjectsMoveToWorkspaceDialog_ProjectFragment
|
||||
workspace: ProjectsMoveToWorkspaceDialog_WorkspaceFragment
|
||||
eventSource?: string
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
const moveProject = useMoveProjectToWorkspace()
|
||||
|
||||
const { showRegionStaticDataDisclaimer, triggerAction } =
|
||||
useWorkspaceCustomDataResidencyDisclaimer({
|
||||
workspace: computed(() => props.workspace),
|
||||
onConfirmAction: async () => {
|
||||
const res = await moveProject({
|
||||
projectId: props.project.id,
|
||||
workspaceId: props.workspace.id,
|
||||
workspaceName: props.workspace.name,
|
||||
eventSource: props.eventSource
|
||||
})
|
||||
if (res?.id) {
|
||||
open.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { result: dryRunResult } = useQuery(
|
||||
moveToWorkspaceDryRunQuery,
|
||||
() => ({
|
||||
projectId: props.project.id,
|
||||
workspaceId: props.workspace.id,
|
||||
limit: 20
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!props.project.id && !!props.workspace.id
|
||||
})
|
||||
)
|
||||
|
||||
const dryRunResultMembers = computed(
|
||||
() => dryRunResult.value?.project.moveToWorkspaceDryRun.addedToWorkspace || []
|
||||
)
|
||||
const dryRunResultMembersCount = computed(
|
||||
() => dryRunResult.value?.project.moveToWorkspaceDryRun.addedToWorkspaceTotalCount
|
||||
)
|
||||
const dryRunResultMembersInfoText = computed(() => {
|
||||
if (!dryRunResultMembers.value || !dryRunResultMembersCount.value) return ''
|
||||
|
||||
if (dryRunResultMembers.value?.length > 20 && dryRunResultMembersCount.value > 20) {
|
||||
const diff = dryRunResultMembersCount.value - dryRunResultMembers.value.length
|
||||
return `and ${diff} more`
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
const dialogButtons = computed<LayoutDialogButton[]>(() => [
|
||||
{
|
||||
text: 'Back',
|
||||
props: {
|
||||
color: 'outline'
|
||||
},
|
||||
onClick: () => {
|
||||
open.value = false
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Move',
|
||||
onClick: () => {
|
||||
triggerAction()
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const transitionItems = {
|
||||
project: {
|
||||
title: 'Viewer seat',
|
||||
description: 'Can view and comment on projects'
|
||||
},
|
||||
workspace: {
|
||||
title: 'Editor seat',
|
||||
description: 'Can view and comment on projects'
|
||||
}
|
||||
} as const
|
||||
</script>
|
||||
@@ -2,13 +2,14 @@
|
||||
<div>
|
||||
<Portal to="primary-actions"></Portal>
|
||||
<div v-if="!showEmptyState" class="flex flex-col gap-4">
|
||||
<ProjectsMoveToWorkspaceAlert v-if="isWorkspacesEnabled" />
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Squares2X2Icon class="h-5 w-5" />
|
||||
<h1 class="text-heading-lg">Projects</h1>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-2 sm:items-center justify-between">
|
||||
<div class="flex flex-col sm:flex-row gap-2">
|
||||
<div class="flex flex-col lg:flex-row gap-2 lg:items-center justify-between">
|
||||
<div class="flex flex-col md:flex-row gap-2">
|
||||
<FormTextInput
|
||||
name="modelsearch"
|
||||
:show-label="false"
|
||||
@@ -28,9 +29,18 @@
|
||||
fixed-height
|
||||
clearable
|
||||
/>
|
||||
<div v-if="!showEmptyState && isWorkspacesEnabled" class="md:mt-1">
|
||||
<FormCheckbox
|
||||
id="projects-to-move"
|
||||
v-model="filterProjectsToMove"
|
||||
label-classes="!font-normal select-none"
|
||||
name="Projects to move"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormButton
|
||||
v-if="canCreatePersonalProject?.authorized"
|
||||
class="!text-body-xs !font-normal"
|
||||
@click="openNewProject = true"
|
||||
>
|
||||
New project
|
||||
@@ -92,9 +102,11 @@ const logger = useLogger()
|
||||
const infiniteLoaderId = ref('')
|
||||
const cursor = ref(null as Nullable<string>)
|
||||
const selectedRoles = ref(undefined as Optional<StreamRoles[]>)
|
||||
const filterProjectsToMove = ref(false)
|
||||
const openNewProject = ref(false)
|
||||
const showLoadingBar = ref(false)
|
||||
const areQueriesLoading = useQueryLoading()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
useUserProjectsUpdatedTracking()
|
||||
|
||||
@@ -114,7 +126,11 @@ const {
|
||||
} = useQuery(projectsDashboardQuery, () => ({
|
||||
filter: {
|
||||
search: (search.value || '').trim() || null,
|
||||
onlyWithRoles: selectedRoles.value?.length ? selectedRoles.value : null,
|
||||
onlyWithRoles: filterProjectsToMove.value
|
||||
? ['stream:owner']
|
||||
: selectedRoles.value?.length
|
||||
? selectedRoles.value
|
||||
: null,
|
||||
personalOnly: isWorkspaceNewPlansEnabled.value
|
||||
},
|
||||
cursor: null as Nullable<string>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div>
|
||||
<CommonCard class="!p-3 !bg-foundation mb-4">
|
||||
<div class="flex flex-col sm:flex-row sm:gap-2 text-foreground">
|
||||
<ExclamationCircleIcon class="h-8 w-8 m-1 text-warning shrink-0" />
|
||||
<div class="flex flex-col gap-4">
|
||||
<h3 class="text-heading mt-2">
|
||||
{{
|
||||
projectId
|
||||
? `Move this project to a workspace or it will be deleted in (count) days.`
|
||||
: `Move projects to a workspace or they will be deleted in (count) days.`
|
||||
}}
|
||||
</h3>
|
||||
|
||||
<div class="text-body-xs max-w-3xl">
|
||||
<p>
|
||||
In our continuous effort to improve user experience, we are excited to
|
||||
announce the rollout of several new features designed to simplify your
|
||||
workflow and enhance navigation. Important facts:
|
||||
</p>
|
||||
<ul class="list-disc list-inside pl-2">
|
||||
<li>These updates will include customizable dashboards,</li>
|
||||
<li>Improved search functionality,</li>
|
||||
<li>And a more user-friendly interface</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-2 mb-3">
|
||||
<FormButton v-if="projectId" @click="onMoveProject">
|
||||
Move project
|
||||
</FormButton>
|
||||
<FormButton v-else @click="onShowProjectsToMove">
|
||||
Show projects to move
|
||||
</FormButton>
|
||||
<FormButton
|
||||
color="outline"
|
||||
:to="LearnMoreMoveProjectsUrl"
|
||||
external
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CommonCard>
|
||||
<ProjectsMoveToWorkspaceDialog
|
||||
v-model:open="showMoveToWorkspaceDialog"
|
||||
:project="selectedProject"
|
||||
event-source="move-to-workspace-alert"
|
||||
/>
|
||||
<WorkspaceMoveProjectsDialog v-model:open="showMoveProjectsDialog" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
|
||||
import { LearnMoreMoveProjectsUrl } from '~/lib/common/helpers/route'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import type { ProjectsMoveToWorkspaceDialog_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import { moveToWorkspaceAlertQuery } from '~/lib/workspaces/graphql/queries'
|
||||
|
||||
graphql(`
|
||||
fragment MoveToWorkspaceAlert_Project on Project {
|
||||
...ProjectsMoveToWorkspaceDialog_Project
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
projectId?: string
|
||||
}>()
|
||||
|
||||
const showMoveToWorkspaceDialog = ref(false)
|
||||
const showMoveProjectsDialog = ref(false)
|
||||
const selectedProject = ref<ProjectsMoveToWorkspaceDialog_ProjectFragment | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
const { result } = useQuery(
|
||||
moveToWorkspaceAlertQuery,
|
||||
() => ({
|
||||
id: props.projectId || ''
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!props.projectId
|
||||
})
|
||||
)
|
||||
|
||||
const onMoveProject = () => {
|
||||
if (!props.projectId) return
|
||||
selectedProject.value = result.value?.project
|
||||
showMoveToWorkspaceDialog.value = true
|
||||
}
|
||||
|
||||
const onShowProjectsToMove = () => {
|
||||
selectedProject.value = undefined
|
||||
showMoveProjectsDialog.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -1,83 +1,85 @@
|
||||
<template>
|
||||
<div>
|
||||
<LayoutDialog v-model:open="open" max-width="sm" :buttons="dialogButtons">
|
||||
<template #header>Move project to workspace</template>
|
||||
<LayoutDialog v-model:open="open" max-width="sm" prevent-close-on-click-outside>
|
||||
<template #header>Ready to move your project?</template>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<template v-if="!workspace">
|
||||
<ProjectsWorkspaceSelect
|
||||
v-if="hasWorkspaces"
|
||||
v-model="selectedWorkspace"
|
||||
:items="workspaces"
|
||||
:disabled-roles="[Roles.Workspace.Member, Roles.Workspace.Guest]"
|
||||
disabled-item-tooltip="Only workspace admins can move projects into a workspace."
|
||||
label="Select a workspace"
|
||||
help="Once a project is moved to a workspace, it cannot be moved out from it."
|
||||
show-label
|
||||
/>
|
||||
<div v-else class="flex flex-col gap-y-2">
|
||||
<p class="text-body-xs text-foreground font-medium">
|
||||
You're not a member of any workspaces.
|
||||
</p>
|
||||
<FormButton :to="workspaceCreateRoute()">Learn about workspaces</FormButton>
|
||||
<div v-if="hasWorkspaces">
|
||||
<p class="mb-4">Select an existing workspaces or create a new one.</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
v-for="ws in workspaces"
|
||||
:key="ws.id"
|
||||
class="w-full"
|
||||
@click="handleWorkspaceClick(ws)"
|
||||
>
|
||||
<WorkspaceCard
|
||||
:logo="ws.logo ?? ''"
|
||||
:name="ws.name"
|
||||
:clickable="ws.role === Roles.Workspace.Admin"
|
||||
>
|
||||
<template #text>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p>
|
||||
{{ ws.projects.totalCount }} projects,
|
||||
{{ ws.projects.totalCount }} models
|
||||
</p>
|
||||
<UserAvatarGroup
|
||||
:users="ws.team.items.map((t) => t.user)"
|
||||
:max-count="6"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<CommonBadge color="secondary" class="capitalize" rounded>
|
||||
{{ ws.plan?.name }}
|
||||
</CommonBadge>
|
||||
</template>
|
||||
</WorkspaceCard>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-body-xs text-foreground">
|
||||
Looks like you haven't created any workspaces yet. Workspaces help you
|
||||
easily organise and control your digital projects. Create one to move your
|
||||
project into.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div v-if="project && (selectedWorkspace || workspace)" class="text-body-xs">
|
||||
<div v-if="project && workspace" class="text-body-xs">
|
||||
<div class="text-body-xs text-foreground flex flex-col gap-y-4">
|
||||
<div class="rounded border bg-foundation-2 border-outline-3 py-2 px-4">
|
||||
<p>
|
||||
Move
|
||||
<span class="font-medium">{{ project.name }}</span>
|
||||
to
|
||||
<span class="font-medium">
|
||||
{{ selectedWorkspace?.name ?? workspace?.name }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="text-foreground-3">
|
||||
{{ project.modelCount.totalCount }} {{ modelText }},
|
||||
{{ project.versions.totalCount }} {{ versionsText }}
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-body-2xs text-foreground-2">
|
||||
The project, including models and versions, will be moved to the
|
||||
workspace, where all existing members and admins will have access.
|
||||
</p>
|
||||
<div
|
||||
v-if="dryRunResultMembers.length > 0"
|
||||
class="pt-2 gap-y-2 flex flex-col"
|
||||
>
|
||||
<p class="text-body-2xs text-foreground-2">
|
||||
The following people will be added to the workspace
|
||||
</p>
|
||||
<div class="w-full">
|
||||
<div
|
||||
v-for="user in dryRunResultMembers"
|
||||
:key="`dry-run-user-${user.id}`"
|
||||
class="bg-foundation flex items-center py-1.5 px-2 border-t border-x last:border-b border-outline-3 first:rounded-t-lg last:rounded-b-lg gap-x-1.5"
|
||||
>
|
||||
<UserAvatar hide-tooltip :user="user" size="sm" />
|
||||
<p class="text-foreground text-body-2xs">{{ user.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p
|
||||
v-if="dryRunResultMembersInfoText"
|
||||
class="text-body-2xs text-foreground-2"
|
||||
>
|
||||
{{ dryRunResultMembersInfoText }}
|
||||
<span class="font-medium">{{ workspace.name }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<WorkspaceRegionStaticDataDisclaimer
|
||||
v-if="showRegionStaticDataDisclaimer"
|
||||
v-model:open="showRegionStaticDataDisclaimer"
|
||||
:variant="RegionStaticDataDisclaimerVariant.MoveProjectIntoWorkspace"
|
||||
@confirm="onConfirmHandler"
|
||||
/>
|
||||
<template #buttons>
|
||||
<FormButton
|
||||
color="outline"
|
||||
class="-my-2"
|
||||
full-width
|
||||
@click="navigateTo(workspaceCreateRoute())"
|
||||
>
|
||||
Create a new workspace
|
||||
</FormButton>
|
||||
</template>
|
||||
</LayoutDialog>
|
||||
|
||||
<ProjectsConfirmMoveDialog
|
||||
v-if="project && selectedWorkspace"
|
||||
v-model:open="showConfirmDialog"
|
||||
:project="project"
|
||||
:workspace="selectedWorkspace"
|
||||
:event-source="eventSource"
|
||||
/>
|
||||
|
||||
<WorkspacePlanLimitReachedDialog
|
||||
v-if="activeLimit"
|
||||
v-model:open="showLimitReachedDialog"
|
||||
:title="dialogTitle"
|
||||
>
|
||||
@@ -93,17 +95,12 @@ import type {
|
||||
ProjectsMoveToWorkspaceDialog_WorkspaceFragment,
|
||||
ProjectsMoveToWorkspaceDialog_ProjectFragment
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { useMutationLoading, useQuery } from '@vue/apollo-composable'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useMoveProjectToWorkspace } from '~/lib/projects/composables/projectManagement'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { UserAvatarGroup } from '@speckle/ui-components'
|
||||
import { useWorkspaceLimits } from '~/lib/workspaces/composables/limits'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { workspaceCreateRoute } from '~/lib/common/helpers/route'
|
||||
import {
|
||||
useWorkspaceCustomDataResidencyDisclaimer,
|
||||
RegionStaticDataDisclaimerVariant
|
||||
} from '~/lib/workspaces/composables/region'
|
||||
import { useWorkspaceLimits } from '~/lib/workspaces/composables/limits'
|
||||
import { moveToWorkspaceDryRunQuery } from '~/lib/projects/graphql/queries'
|
||||
import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
|
||||
|
||||
graphql(`
|
||||
fragment ProjectsMoveToWorkspaceDialog_Workspace on Workspace {
|
||||
@@ -112,6 +109,21 @@ graphql(`
|
||||
name
|
||||
logo
|
||||
slug
|
||||
plan {
|
||||
name
|
||||
}
|
||||
projects {
|
||||
totalCount
|
||||
}
|
||||
team {
|
||||
items {
|
||||
user {
|
||||
id
|
||||
name
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
...WorkspaceHasCustomDataResidency_Workspace
|
||||
...ProjectsWorkspaceSelect_Workspace
|
||||
}
|
||||
@@ -150,7 +162,7 @@ const query = graphql(`
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
project: ProjectsMoveToWorkspaceDialog_ProjectFragment
|
||||
project?: ProjectsMoveToWorkspaceDialog_ProjectFragment
|
||||
workspace?: ProjectsMoveToWorkspaceDialog_WorkspaceFragment
|
||||
eventSource?: string // Used for mixpanel tracking
|
||||
}>()
|
||||
@@ -161,26 +173,10 @@ const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const { result } = useQuery(query, null, () => ({
|
||||
enabled: isWorkspacesEnabled.value
|
||||
}))
|
||||
const loading = useMutationLoading()
|
||||
const moveProject = useMoveProjectToWorkspace()
|
||||
|
||||
const selectedWorkspace = ref<ProjectsMoveToWorkspaceDialog_WorkspaceFragment>()
|
||||
|
||||
const { result: dryRunResult } = useQuery(
|
||||
moveToWorkspaceDryRunQuery,
|
||||
() => ({
|
||||
projectId: props.project.id,
|
||||
workspaceId: (selectedWorkspace.value?.id ?? props.workspace?.id)!,
|
||||
limit: 20
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!selectedWorkspace.value?.id || !!props.workspace?.id
|
||||
})
|
||||
)
|
||||
|
||||
const activeWorkspaceSlug = computed(
|
||||
() => selectedWorkspace.value?.slug || props.workspace?.slug || ''
|
||||
)
|
||||
const activeWorkspaceSlug = computed(() => selectedWorkspace.value?.slug || '')
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
if (limitType.value === 'project') return 'Project limit reached'
|
||||
@@ -193,121 +189,62 @@ const { canAddProject, canAddModels, limits } = useWorkspaceLimits(
|
||||
activeWorkspaceSlug.value
|
||||
)
|
||||
|
||||
const { plan } = useWorkspacePlan(activeWorkspaceSlug.value)
|
||||
|
||||
const showLimitReachedDialog = ref(false)
|
||||
|
||||
const workspaces = computed(() => result.value?.activeUser?.workspaces.items ?? [])
|
||||
const hasWorkspaces = computed(() => workspaces.value.length > 0)
|
||||
const modelText = computed(() =>
|
||||
props.project.modelCount.totalCount === 1 ? 'model' : 'models'
|
||||
)
|
||||
const versionsText = computed(() =>
|
||||
props.project.versions.totalCount === 1 ? 'version' : 'versions'
|
||||
)
|
||||
const dryRunResultMembers = computed(
|
||||
() => dryRunResult.value?.project.moveToWorkspaceDryRun.addedToWorkspace || []
|
||||
)
|
||||
const dryRunResultMembersCount = computed(
|
||||
() => dryRunResult.value?.project.moveToWorkspaceDryRun.addedToWorkspaceTotalCount
|
||||
)
|
||||
const dryRunResultMembersInfoText = computed(() => {
|
||||
if (!dryRunResultMembers.value || !dryRunResultMembersCount.value) return ''
|
||||
|
||||
if (dryRunResultMembers.value?.length > 20 && dryRunResultMembersCount.value > 20) {
|
||||
const diff = dryRunResultMembersCount.value - dryRunResultMembers.value.length
|
||||
return `and ${diff} more`
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
// Determine which limit type is hit
|
||||
const limitType = computed((): 'project' | 'model' | null => {
|
||||
if (!selectedWorkspace.value) return null
|
||||
if (!canAddProject.value) return 'project'
|
||||
|
||||
const projectModelCount = props.project.modelCount.totalCount
|
||||
const projectModelCount = props.project?.modelCount.totalCount
|
||||
if (!canAddModels(projectModelCount)) return 'model'
|
||||
|
||||
// Check free plan project limit
|
||||
const currentProjectCount = plan.value?.usage?.projectCount || 0
|
||||
if (plan.value?.name === 'free' && currentProjectCount >= 3) return 'project'
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
// Get the value of the limit that's hit
|
||||
const activeLimit = computed(() => {
|
||||
if (limitType.value === 'project') return limits.value.projectCount ?? 0
|
||||
if (!selectedWorkspace.value) return 0
|
||||
if (limitType.value === 'project') {
|
||||
// For free plan, show the actual limit
|
||||
if (plan.value?.name === 'free') return 3
|
||||
return limits.value.projectCount ?? 0
|
||||
}
|
||||
if (limitType.value === 'model') return limits.value.modelCount ?? 0
|
||||
return 0
|
||||
})
|
||||
|
||||
const dialogButtons = computed<LayoutDialogButton[]>(() => {
|
||||
return hasWorkspaces.value
|
||||
? [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'outline' },
|
||||
onClick: () => {
|
||||
open.value = false
|
||||
}
|
||||
},
|
||||
{
|
||||
text: 'Move',
|
||||
props: {
|
||||
color: 'primary',
|
||||
disabled: (!selectedWorkspace.value && !props.workspace) || loading.value
|
||||
},
|
||||
onClick: () => onMoveClick()
|
||||
}
|
||||
]
|
||||
: [
|
||||
{
|
||||
text: 'Close',
|
||||
props: { color: 'outline' },
|
||||
onClick: () => {
|
||||
open.value = false
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const showConfirmDialog = ref(false)
|
||||
|
||||
const onMoveProject = async () => {
|
||||
const workspaceId = selectedWorkspace.value?.id ?? props.workspace?.id
|
||||
const workspaceName = selectedWorkspace.value?.name ?? props.workspace?.name
|
||||
if (!workspaceId || !workspaceName) return
|
||||
const handleWorkspaceClick = (ws: ProjectsMoveToWorkspaceDialog_WorkspaceFragment) => {
|
||||
selectedWorkspace.value = ws
|
||||
|
||||
const res = await moveProject({
|
||||
projectId: props.project.id,
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
eventSource: props.eventSource
|
||||
const hasReachedProjectLimit = computed(() => {
|
||||
return false
|
||||
})
|
||||
if (res?.id) {
|
||||
open.value = false
|
||||
|
||||
if (hasReachedProjectLimit.value) {
|
||||
showLimitReachedDialog.value = true
|
||||
} else {
|
||||
showConfirmDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const { showRegionStaticDataDisclaimer, triggerAction, onConfirmHandler } =
|
||||
useWorkspaceCustomDataResidencyDisclaimer({
|
||||
workspace: computed(() => selectedWorkspace.value ?? props.workspace),
|
||||
onConfirmAction: onMoveProject
|
||||
})
|
||||
|
||||
watch(
|
||||
() => open.value,
|
||||
(isOpen, oldIsOpen) => {
|
||||
if (isOpen && isOpen !== oldIsOpen) {
|
||||
selectedWorkspace.value = undefined
|
||||
showRegionStaticDataDisclaimer.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const onMoveClick = () => {
|
||||
const projectModelCount = props.project.modelCount.totalCount
|
||||
|
||||
// Check if we can add this project to the workspace
|
||||
if (!canAddProject.value || !canAddModels(projectModelCount)) {
|
||||
open.value = false
|
||||
showLimitReachedDialog.value = true
|
||||
} else {
|
||||
triggerAction()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
{{ updatedAt.relative }}
|
||||
</span>
|
||||
<span class="text-body-3xs capitalize mb-2 text-foreground-2 select-none">
|
||||
{{ project.role?.split(':').reverse()[0] }}
|
||||
{{ RoleInfo.Stream[project.role as StreamRoles].title }}
|
||||
</span>
|
||||
<UserAvatarGroup :users="teamUsers" :max-count="2" />
|
||||
</div>
|
||||
@@ -95,6 +95,7 @@ import {
|
||||
import { useGeneralProjectPageUpdateTracking } from '~~/lib/projects/composables/projectPages'
|
||||
import { ChevronRightIcon } from '@heroicons/vue/20/solid'
|
||||
import { workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { RoleInfo, type StreamRoles } from '@speckle/shared'
|
||||
|
||||
const props = defineProps<{
|
||||
project: ProjectDashboardItemFragment
|
||||
|
||||
@@ -59,6 +59,7 @@ import { homeRoute, defaultZapierWebhookUrl } from '~/lib/common/helpers/route'
|
||||
import { useZapier } from '~/lib/core/composables/zapier'
|
||||
import { useForm } from 'vee-validate'
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import { useNavigation } from '~/lib/navigation/composables/navigation'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {
|
||||
@@ -81,6 +82,7 @@ const apollo = useApolloClient().client
|
||||
const mixpanel = useMixpanel()
|
||||
const { sendWebhook } = useZapier()
|
||||
const { resetForm } = useForm<{ feedback: string }>()
|
||||
const { mutateActiveWorkspaceSlug } = useNavigation()
|
||||
|
||||
const workspaceNameInput = ref('')
|
||||
const feedback = ref('')
|
||||
@@ -144,6 +146,7 @@ const onDelete = async () => {
|
||||
title: `${workspaceName} workspace deleted`
|
||||
})
|
||||
|
||||
mutateActiveWorkspaceSlug(null)
|
||||
router.push(homeRoute)
|
||||
isOpen.value = false
|
||||
} else {
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<!-- TODO: Implement final values, links and copy -->
|
||||
<template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<CommonCard class="flex flex-col gap-y-3 p-6">
|
||||
<h3 class="text-body font-medium">Unlimited projects and models</h3>
|
||||
<p class="text-foreground-2 text-body-xs flex-1 mb-3">
|
||||
Add more projects and models to your workspace.
|
||||
</p>
|
||||
<FormButton color="outline" size="sm">Contact us</FormButton>
|
||||
</CommonCard>
|
||||
|
||||
<CommonCard class="flex flex-col gap-y-3 p-6">
|
||||
<h3 class="text-body font-medium">Extra data regions</h3>
|
||||
<p class="text-foreground-2 text-body-xs flex-1 mb-3">
|
||||
Access to almost all data residency regions.
|
||||
</p>
|
||||
<FormButton v-if="planIsBusiness" color="outline" size="sm">
|
||||
Contact us
|
||||
</FormButton>
|
||||
<p v-else class="font-medium text-body-xs">Available only on Business plan</p>
|
||||
</CommonCard>
|
||||
|
||||
<CommonCard class="flex flex-col gap-y-3 p-6">
|
||||
<h3 class="text-body font-medium">Priority support</h3>
|
||||
<p class="text-foreground-2 text-body-xs flex-1 mb-3">
|
||||
Private support channel for your workspace.
|
||||
</p>
|
||||
<FormButton v-if="planIsBusiness" color="outline" size="sm">
|
||||
Contact us
|
||||
</FormButton>
|
||||
<p v-else class="font-medium text-body-xs">Available only on Business plan</p>
|
||||
</CommonCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
|
||||
import { PaidWorkspacePlansNew } from '@speckle/shared'
|
||||
|
||||
const props = defineProps<{
|
||||
slug: string
|
||||
}>()
|
||||
|
||||
const { plan } = useWorkspacePlan(props.slug)
|
||||
|
||||
const planIsBusiness = computed(() => plan.value?.name === PaidWorkspacePlansNew.Pro)
|
||||
</script>
|
||||
@@ -12,30 +12,23 @@
|
||||
</div>
|
||||
|
||||
<div class="p-5 pt-4 flex flex-col">
|
||||
<h3 class="text-body-xs text-foreground-2 pb-4">
|
||||
<template v-if="isPurchasablePlan">
|
||||
<span class="capitalize">{{ billingInterval }}</span>
|
||||
bill
|
||||
</template>
|
||||
<template v-else>Bill</template>
|
||||
</h3>
|
||||
<h3 class="text-body-xs text-foreground-2 pb-4">Billing period</h3>
|
||||
<p class="text-heading-lg text-foreground inline-block">
|
||||
TODO
|
||||
<span v-if="isPurchasablePlan">per {{ billingInterval }}</span>
|
||||
<span v-if="isPurchasablePlan">
|
||||
{{ intervalIsYearly ? 'Yearly' : 'Monthly' }}
|
||||
</span>
|
||||
<span v-else>Not applicable</span>
|
||||
</p>
|
||||
<NuxtLink
|
||||
v-if="showBillingPortalLink"
|
||||
class="text-body-xs text-foreground-2 underline hover:text-foreground cursor-pointer mt-1"
|
||||
@click="billingPortalRedirect(workspaceId)"
|
||||
>
|
||||
View cost breakdown
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<div class="p-5 pt-4 flex flex-col">
|
||||
<h3 class="text-body-xs text-foreground-2 pb-4">Billing period</h3>
|
||||
<h3 class="text-body-xs text-foreground-2 pb-4">Next payment due</h3>
|
||||
<p class="text-heading-lg text-foreground capitalize">
|
||||
{{ isPurchasablePlan ? billingInterval : 'Not applicable' }}
|
||||
{{
|
||||
currentBillingCycleEnd
|
||||
? dayjs(currentBillingCycleEnd).format('dd-mmmm-yyyy')
|
||||
: 'Not applicable'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,6 +56,7 @@ import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
|
||||
import { useBillingActions } from '~/lib/billing/composables/actions'
|
||||
import { type MaybeNullOrUndefined, WorkspacePlanStatuses } from '@speckle/shared'
|
||||
import { formatName } from '~/lib/billing/helpers/plan'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
defineProps<{
|
||||
workspaceId?: MaybeNullOrUndefined<string>
|
||||
@@ -72,7 +66,8 @@ const { billingPortalRedirect } = useBillingActions()
|
||||
const route = useRoute()
|
||||
const slug = computed(() => (route.params.slug as string) || '')
|
||||
|
||||
const { plan, isPurchasablePlan, billingInterval } = useWorkspacePlan(slug.value)
|
||||
const { plan, isPurchasablePlan, intervalIsYearly, currentBillingCycleEnd } =
|
||||
useWorkspacePlan(slug.value)
|
||||
|
||||
const showBillingPortalLink = computed(
|
||||
() =>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<SettingsWorkspacesBillingAddOnsCard
|
||||
title="Unlimited projects and models"
|
||||
info="Add unlimited projects and models to your workspace."
|
||||
disclaimer="Only on Starter & Business plans"
|
||||
:buttons="[unlimitedAddOnButton]"
|
||||
>
|
||||
<template #subtitle>
|
||||
<p class="text-body pt-1">
|
||||
<span class="font-medium">$0</span>
|
||||
per editor/month
|
||||
</p>
|
||||
<div class="flex items-center gap-x-2 mt-3 px-1">
|
||||
<FormSwitch
|
||||
v-model="isYearlyIntervalSelected"
|
||||
:show-label="false"
|
||||
name="billing-interval"
|
||||
/>
|
||||
<span class="text-body-2xs">Billed yearly</span>
|
||||
<CommonBadge rounded color-classes="text-foreground-2 bg-primary-muted">
|
||||
-10%
|
||||
</CommonBadge>
|
||||
</div>
|
||||
</template>
|
||||
</SettingsWorkspacesBillingAddOnsCard>
|
||||
|
||||
<SettingsWorkspacesBillingAddOnsCard
|
||||
title="Extra data regions"
|
||||
subtitle="Talk to us"
|
||||
info="Access to almost all data residency regions."
|
||||
disclaimer="Only on Business plan"
|
||||
:buttons="[contactButton]"
|
||||
/>
|
||||
|
||||
<SettingsWorkspacesBillingAddOnsCard
|
||||
title="Priority support"
|
||||
subtitle="Talk to us"
|
||||
info="Private support channel for your workspace."
|
||||
disclaimer="Only on Business plan"
|
||||
:buttons="[contactButton]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { BillingInterval } from '~/lib/common/generated/gql/graphql'
|
||||
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
|
||||
import { isPaidPlan } from '@speckle/shared'
|
||||
|
||||
const props = defineProps<{
|
||||
slug: string
|
||||
}>()
|
||||
|
||||
const isYearlyIntervalSelected = ref(false)
|
||||
const { billingInterval, isBusinessPlan, plan } = useWorkspacePlan(props.slug)
|
||||
|
||||
const contactButton = computed(() => ({
|
||||
text: 'Contact us',
|
||||
id: 'contact-us',
|
||||
disabled: !isBusinessPlan.value,
|
||||
disabledMessage: 'Only available on the Business plan',
|
||||
onClick: () => {
|
||||
// TODO: Implement contact us
|
||||
}
|
||||
}))
|
||||
|
||||
const unlimitedAddOnButton = computed(() => ({
|
||||
text: 'Buy add-on',
|
||||
id: 'buy-add-on',
|
||||
disabled: plan.value?.name ? !isPaidPlan(plan.value.name) : false,
|
||||
disabledMessage: 'Only available on Starter and Business plans',
|
||||
onClick: () => {
|
||||
// TODO: Implement checkout
|
||||
}
|
||||
}))
|
||||
|
||||
watch(
|
||||
() => billingInterval.value,
|
||||
(newVal) => {
|
||||
isYearlyIntervalSelected.value = newVal === BillingInterval.Yearly
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<CommonCard class="flex flex-col gap-y-4 p-6">
|
||||
<div class="flex flex-col">
|
||||
<h3 class="text-body font-medium">{{ title }}</h3>
|
||||
<p v-if="subtitle" class="text-foreground-3 text-body-sm pt-1">{{ subtitle }}</p>
|
||||
<slot name="subtitle" />
|
||||
</div>
|
||||
<p class="flex-1 mb-3">
|
||||
<span v-if="info" class="text-foreground-2 text-body-xs">{{ info }}</span>
|
||||
<slot name="info" />
|
||||
</p>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<div
|
||||
v-for="(button, index) in buttons"
|
||||
:key="button.id || index"
|
||||
v-tippy="button.disabledMessage"
|
||||
>
|
||||
<FormButton
|
||||
v-bind="button.props || {}"
|
||||
:disabled="button.props?.disabled || button.disabled"
|
||||
size="sm"
|
||||
color="outline"
|
||||
@click="($event) => button.onClick?.($event)"
|
||||
>
|
||||
{{ button.text }}
|
||||
</FormButton>
|
||||
</div>
|
||||
<p v-if="disclaimer" class="font-medium text-foreground-2 text-body-2xs">
|
||||
{{ disclaimer }}
|
||||
</p>
|
||||
</div>
|
||||
</CommonCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
info?: string
|
||||
buttons?: LayoutDialogButton[]
|
||||
disclaimer?: string
|
||||
}>()
|
||||
</script>
|
||||
+6
@@ -17,6 +17,8 @@ import { useWorkspaceUpdateRole } from '~/lib/workspaces/composables/management'
|
||||
import type { SettingsWorkspacesMembersTable_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { useActiveUser } from '~/lib/auth/composables/activeUser'
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import { useNavigation } from '~/lib/navigation/composables/navigation'
|
||||
import { homeRoute } from '~/lib/common/helpers/route'
|
||||
|
||||
const props = defineProps<{
|
||||
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersTable_WorkspaceFragment>
|
||||
@@ -31,6 +33,8 @@ const open = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const { activeUser } = useActiveUser()
|
||||
const updateUserRole = useWorkspaceUpdateRole()
|
||||
const { mutateActiveWorkspaceSlug } = useNavigation()
|
||||
const router = useRouter()
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!props.workspace?.id || !activeUser.value?.id) return
|
||||
@@ -41,6 +45,8 @@ const handleConfirm = async () => {
|
||||
workspaceId: props.workspace.id
|
||||
})
|
||||
|
||||
mutateActiveWorkspaceSlug(null)
|
||||
router.push(homeRoute)
|
||||
open.value = false
|
||||
emit('success')
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
v-model:open="showDialog"
|
||||
:workspace="workspace"
|
||||
:is-only-admin="hasSingleAdmin"
|
||||
@success="onDialogSuccess"
|
||||
/>
|
||||
|
||||
<SettingsWorkspacesMembersActionsProjectPermissionsDialog
|
||||
|
||||
+24
-10
@@ -1,16 +1,30 @@
|
||||
<template>
|
||||
<BillingTransitionCards :current-state="currentSeat" :new-state="newSeat">
|
||||
<template #price>
|
||||
<div v-if="isUpgrading" class="ml-auto flex items-center gap-1 font-medium">
|
||||
<template v-if="hasAvailableSeat || isFreePlan">
|
||||
<div class="line-through text-foreground-2">{{ seatPrice }}/month</div>
|
||||
<div class="text-primary">Free</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-primary">{{ seatPrice }}/month</div>
|
||||
</template>
|
||||
<template #current-state>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-heading-sm">{{ currentSeat.title }}</div>
|
||||
<div class="text-body-2xs">{{ currentSeat.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #new-state>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<div class="text-heading-sm">{{ newSeat.title }}</div>
|
||||
<div class="text-body-2xs">{{ newSeat.description }}</div>
|
||||
</div>
|
||||
<div v-if="isUpgrading" class="ml-auto flex items-center gap-1 font-medium">
|
||||
<template v-if="hasAvailableSeat || isFreePlan">
|
||||
<div class="line-through text-foreground-2">{{ seatPrice }}/month</div>
|
||||
<div class="text-primary">Free</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="text-primary">{{ seatPrice }}/month</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="ml-auto text-primary font-medium">Free</div>
|
||||
</div>
|
||||
<div v-else class="ml-auto text-primary font-medium">Free</div>
|
||||
</template>
|
||||
</BillingTransitionCards>
|
||||
</template>
|
||||
|
||||
+13
-14
@@ -39,20 +39,19 @@
|
||||
:has-available-seat="hasAvailableEditorSeats"
|
||||
:seat-price="editorSeatPriceFormatted"
|
||||
/>
|
||||
<p
|
||||
v-if="needsEditorUpgrade && !hasAvailableEditorSeats"
|
||||
class="text-foreground-2 text-body-xs mt-4"
|
||||
>
|
||||
You have an unused Editor seat that is already paid for, so the change will
|
||||
not incur any charges.
|
||||
</p>
|
||||
<p
|
||||
v-if="needsEditorUpgrade && !hasAvailableEditorSeats && !isUnlimitedPlan"
|
||||
class="text-foreground-2 text-body-xs mt-4"
|
||||
>
|
||||
Note that the Editor seat is a paid seat type and this change will incur
|
||||
additional charges to your subscription.
|
||||
</p>
|
||||
<template v-if="needsEditorUpgrade && !isFreePlan && !isUnlimitedPlan">
|
||||
<p
|
||||
v-if="hasAvailableEditorSeats"
|
||||
class="text-foreground-2 text-body-xs mt-4"
|
||||
>
|
||||
You have an unused Editor seat that is already paid for, so the change
|
||||
will not incur any charges.
|
||||
</p>
|
||||
<p v-else class="text-foreground-2 text-body-xs mt-4">
|
||||
Note that the Editor seat is a paid seat type and this change will incur
|
||||
additional charges to your subscription.
|
||||
</p>
|
||||
</template>
|
||||
</CommonCard>
|
||||
</template>
|
||||
|
||||
|
||||
+2
-2
@@ -59,7 +59,7 @@ const updateUserSeatType = useWorkspaceUpdateSeatType()
|
||||
const {
|
||||
hasAvailableEditorSeats,
|
||||
editorSeatPriceFormatted,
|
||||
billingCycleEnd,
|
||||
currentBillingCycleEnd,
|
||||
isPurchasablePlan,
|
||||
isFreePlan,
|
||||
intervalIsYearly,
|
||||
@@ -77,7 +77,7 @@ const billingMessage = computed(() => {
|
||||
: `This adds an extra Editor seat to your subscription, increasing your total billing by ${editorSeatPriceFormatted.value}/${annualOrMonthly.value}.`
|
||||
} else {
|
||||
return isPurchasablePlan.value
|
||||
? `The Editor seat will still be paid for until your plan renews on ${billingCycleEnd.value}. You can freely reassign it to another person.`
|
||||
? `The Editor seat will still be paid for until your plan renews on ${currentBillingCycleEnd.value}. You can freely reassign it to another person.`
|
||||
: null
|
||||
}
|
||||
})
|
||||
|
||||
@@ -195,7 +195,7 @@ import {
|
||||
ArrowLeftIcon,
|
||||
ArrowUpRightIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
import { ensureError, Roles } from '@speckle/shared'
|
||||
import { ensureError } from '@speckle/shared'
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
import { onKeyDown, useClipboard, useDraggable, onClickOutside } from '@vueuse/core'
|
||||
import { scrollToBottom } from '~~/lib/common/helpers/dom'
|
||||
@@ -216,6 +216,18 @@ import { useDisableGlobalTextSelection } from '~~/lib/common/composables/window'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useThreadUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
graphql(`
|
||||
fragment ViewerCommentThreadData on Comment {
|
||||
id
|
||||
permissions {
|
||||
canArchive {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: CommentBubbleModel): void
|
||||
@@ -235,14 +247,9 @@ const { isEmbedEnabled } = useEmbed()
|
||||
|
||||
const threadId = computed(() => props.modelValue.id)
|
||||
const { copy } = useClipboard()
|
||||
const { activeUser, isLoggedIn } = useActiveUser()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const archiveComment = useArchiveComment()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const {
|
||||
resources: {
|
||||
response: { project }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const { projectId } = useInjectedViewerState()
|
||||
const canReply = useCheckViewerCommentingAccess()
|
||||
@@ -253,6 +260,7 @@ const { ellipsis, controls } = useAnimatingEllipsis()
|
||||
const { threadResourceStatus, hasClickedFullContext, goBack, handleContextClick } =
|
||||
useCommentContext()
|
||||
const { isOpenThread, open, closeAllThreads } = useThreadUtilities()
|
||||
const router = useRouter()
|
||||
|
||||
const commentsContainer = ref(null as Nullable<HTMLElement>)
|
||||
const threadContainer = ref(null as Nullable<HTMLElement>)
|
||||
@@ -400,18 +408,23 @@ const changeExpanded = async (newVal: boolean) => {
|
||||
}
|
||||
|
||||
const canArchiveOrUnarchive = computed(
|
||||
() =>
|
||||
activeUser.value &&
|
||||
(props.modelValue.author.id === activeUser.value.id ||
|
||||
project.value?.role === Roles.Stream.Owner)
|
||||
() => props.modelValue.permissions.canArchive.authorized
|
||||
)
|
||||
|
||||
const toggleCommentResolvedStatus = async () => {
|
||||
// Remove thread ID from URL when resolving
|
||||
if (!props.modelValue.archived) {
|
||||
const query = { ...router.currentRoute.value.query }
|
||||
delete query.thread
|
||||
await router.replace({ query })
|
||||
}
|
||||
|
||||
await archiveComment({
|
||||
commentId: props.modelValue.id,
|
||||
projectId: projectId.value,
|
||||
archived: !props.modelValue.archived
|
||||
})
|
||||
|
||||
mp.track('Comment Action', {
|
||||
type: 'action',
|
||||
name: 'archive',
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
size="sm"
|
||||
:icon-left="includeArchived ? CheckCircleIcon : CheckCircleIconOutlined"
|
||||
text
|
||||
:disabled="commentThreadsMetadata?.totalArchivedCount === 0"
|
||||
class="!text-foreground"
|
||||
@click="includeArchived = includeArchived ? undefined : 'includeArchived'"
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="px-3 flex flex-col space-y-2 pb-2">
|
||||
<div class="px-3 flex flex-col space-y-2 py-2">
|
||||
<div class="flex w-full space-x-1">
|
||||
<div class="text-xs">Range:</div>
|
||||
<div class="text-xs truncate">[{{ props.filter.min.toFixed(2) }},</div>
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
<template>
|
||||
<div class="pr-3 p-2 flex flex-col space-y-2">
|
||||
<div class="sticky top-0 bg-foundation">
|
||||
<FormTextInput
|
||||
v-model="searchString"
|
||||
name="filter search"
|
||||
placeholder="Search for a value"
|
||||
size="sm"
|
||||
color="foundation"
|
||||
:show-clear="!!searchString"
|
||||
class="!text-body-2xs"
|
||||
/>
|
||||
</div>
|
||||
<ViewerExplorerStringFilterItem
|
||||
v-for="(vg, index) in groupsLimited"
|
||||
v-for="(vg, index) in filteredGroup"
|
||||
:key="index"
|
||||
:item="vg"
|
||||
:search-term="searchString"
|
||||
/>
|
||||
<div v-if="itemCount < filter.valueGroups.length" class="mb-2">
|
||||
<div v-if="itemCount < totalFilteredCount" class="mb-2">
|
||||
<FormButton size="sm" text full-width @click="itemCount += 30">
|
||||
View more ({{ filter.valueGroups.length - itemCount }})
|
||||
View more ({{ totalFilteredCount - itemCount }})
|
||||
</FormButton>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +31,20 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const itemCount = ref(30)
|
||||
const groupsLimited = computed(() => {
|
||||
return props.filter.valueGroups.slice(0, itemCount.value)
|
||||
const searchString = ref<string | undefined>(undefined)
|
||||
|
||||
const filteredGroups = computed(() => {
|
||||
if (!searchString.value) return props.filter.valueGroups
|
||||
|
||||
const searchLower = searchString.value.toLowerCase()
|
||||
return props.filter.valueGroups.filter((f) =>
|
||||
f.value.toLowerCase().includes(searchLower)
|
||||
)
|
||||
})
|
||||
|
||||
const filteredGroup = computed(() => {
|
||||
return filteredGroups.value.slice(0, itemCount.value)
|
||||
})
|
||||
|
||||
const totalFilteredCount = computed(() => filteredGroups.value.length)
|
||||
</script>
|
||||
|
||||
@@ -15,9 +15,15 @@
|
||||
class="w-3 h-3 rounded"
|
||||
:style="`background-color: #${color};`"
|
||||
></span>
|
||||
<span class="truncate">
|
||||
{{ item.value || 'No name' }}
|
||||
<span v-if="searchTerm && hasMatch" class="truncate">
|
||||
<span>{{ beforeMatch }}</span>
|
||||
<span class="font-bold">{{ match }}</span>
|
||||
<span>{{ afterMatch }}</span>
|
||||
</span>
|
||||
<span v-else-if="item.value" class="truncate">
|
||||
{{ item.value }}
|
||||
</span>
|
||||
<span v-else class="truncate">No name</span>
|
||||
<div class="flex">
|
||||
<span
|
||||
v-if="props.item.ids.length !== availableTargetIds.length"
|
||||
@@ -77,6 +83,7 @@ const props = defineProps<{
|
||||
value: string
|
||||
ids: string[]
|
||||
}
|
||||
searchTerm?: string
|
||||
}>()
|
||||
|
||||
const {
|
||||
@@ -122,6 +129,36 @@ const color = computed(() => {
|
||||
?.color
|
||||
})
|
||||
|
||||
// Simple text highlighting for search matches
|
||||
const hasMatch = computed(() => {
|
||||
if (!props.searchTerm) return false
|
||||
const value = props.item.value
|
||||
return value.toLowerCase().includes(props.searchTerm.toLowerCase())
|
||||
})
|
||||
|
||||
const beforeMatch = computed(() => {
|
||||
if (!hasMatch.value || !props.searchTerm) return ''
|
||||
const value = props.item.value
|
||||
const index = value.toLowerCase().indexOf(props.searchTerm.toLowerCase())
|
||||
return value.substring(0, index)
|
||||
})
|
||||
|
||||
const match = computed(() => {
|
||||
if (!hasMatch.value || !props.searchTerm) return ''
|
||||
const value = props.item.value
|
||||
const searchTerm = props.searchTerm.toLowerCase()
|
||||
const index = value.toLowerCase().indexOf(searchTerm)
|
||||
return value.substring(index, index + props.searchTerm.length)
|
||||
})
|
||||
|
||||
const afterMatch = computed(() => {
|
||||
if (!hasMatch.value || !props.searchTerm) return ''
|
||||
const value = props.item.value
|
||||
const searchTerm = props.searchTerm.toLowerCase()
|
||||
const index = value.toLowerCase().indexOf(searchTerm)
|
||||
return value.substring(index + props.searchTerm.length)
|
||||
})
|
||||
|
||||
// It is possible to control the visibility and isolation of objects from here, There are
|
||||
// some performance concerns here, so this is something to come back to. For now, the icons
|
||||
// are purely indicators
|
||||
|
||||
@@ -117,7 +117,6 @@ import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { CommonAlert, CommonBadge } from '@speckle/ui-components'
|
||||
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline'
|
||||
import dayjs from 'dayjs'
|
||||
import { canModifyModels } from '~/lib/projects/helpers/permissions'
|
||||
|
||||
const {
|
||||
projectId,
|
||||
@@ -149,9 +148,8 @@ const suggestedPrompts = ref<string[]>([
|
||||
|
||||
const isGendoEnabled = useIsGendoModuleEnabled()
|
||||
|
||||
const canContribute = computed(() =>
|
||||
project.value ? canModifyModels(project.value) : false
|
||||
)
|
||||
// TODO: Auth policy
|
||||
const canContribute = computed(() => project.value?.role)
|
||||
|
||||
const isGendoPanelEnabled = computed(() => !!activeUser.value && !!isGendoEnabled.value)
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<CommonCard
|
||||
class="w-full bg-foundation border-outline-2 !p-4"
|
||||
:class="{
|
||||
'cursor-pointer hover:border-outline-3 shadow-sm hover:border-zinc-400': clickable
|
||||
}"
|
||||
@click="clickable && onClick"
|
||||
>
|
||||
<div class="flex justify-between gap-4">
|
||||
<div class="flex gap-4">
|
||||
<WorkspaceAvatar :name="name" :logo="logo" size="xl" />
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-between flex-1">
|
||||
<div class="flex flex-col items-start text-body-2xs text-foreground-2">
|
||||
<h6 class="text-heading-sm text-foreground">{{ name }}</h6>
|
||||
<slot name="text"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-y-2" @click.stop>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</CommonCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
logo: string
|
||||
name: string
|
||||
clickable?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
|
||||
const onClick = () => {
|
||||
if (props.clickable) {
|
||||
emit('click')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -45,7 +45,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { workspaceCreateRoute } from '~~/lib/common/helpers/route'
|
||||
import { homeRoute } from '~~/lib/common/helpers/route'
|
||||
import { WizardSteps } from '~/lib/workspaces/helpers/types'
|
||||
import { useWorkspacesWizard } from '~/lib/workspaces/composables/wizard'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
@@ -81,7 +81,7 @@ const requiresWorkspaceCreation = computed(() => {
|
||||
|
||||
const onCancelClick = () => {
|
||||
if (isFirstStep.value) {
|
||||
navigateTo(workspaceCreateRoute())
|
||||
navigateTo(homeRoute)
|
||||
resetWizardState()
|
||||
mixpanel.stop_session_recording()
|
||||
} else {
|
||||
|
||||
@@ -16,19 +16,6 @@
|
||||
class="mb-2"
|
||||
v-on="on"
|
||||
/>
|
||||
<div class="text-body-2xs py-2">
|
||||
You can move up to
|
||||
<span class="font-medium">
|
||||
{{ Math.max(0, remainingProjectCount) }}
|
||||
{{ remainingProjectCount === 1 ? 'project' : 'projects' }}
|
||||
</span>
|
||||
and
|
||||
<span class="font-medium">
|
||||
{{ Math.max(0, remainingModelCount) }}
|
||||
{{ remainingModelCount === 1 ? 'model' : 'models' }}
|
||||
</span>
|
||||
in total.
|
||||
</div>
|
||||
<div
|
||||
v-if="hasMoveableProjects"
|
||||
class="flex flex-col mt-2 border rounded-md border-outline-3"
|
||||
@@ -66,10 +53,17 @@
|
||||
@infinite="onInfiniteLoad"
|
||||
/>
|
||||
|
||||
<ProjectsMoveToWorkspaceDialog
|
||||
v-if="selectedProject"
|
||||
v-model:open="showMoveToWorkspaceDialog"
|
||||
<ProjectsConfirmMoveDialog
|
||||
v-if="selectedProject && workspace"
|
||||
v-model:open="showConfirmDialog"
|
||||
:project="selectedProject"
|
||||
:workspace="workspace"
|
||||
event-source="move-projects-dialog"
|
||||
/>
|
||||
|
||||
<ProjectsMoveToWorkspaceDialog
|
||||
v-if="selectedProject && !workspace"
|
||||
v-model:open="showMoveToWorkspaceDialog"
|
||||
:project="selectedProject"
|
||||
event-source="move-projects-dialog"
|
||||
/>
|
||||
@@ -89,7 +83,6 @@ import type {
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
import { moveProjectsDialogQuery } from '~~/lib/workspaces/graphql/queries'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { useWorkspaceLimits } from '~/lib/workspaces/composables/limits'
|
||||
|
||||
graphql(`
|
||||
fragment MoveProjectsDialog_Workspace on Workspace {
|
||||
@@ -119,7 +112,7 @@ graphql(`
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
workspace: MoveProjectsDialog_WorkspaceFragment
|
||||
workspace?: MoveProjectsDialog_WorkspaceFragment
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
@@ -150,14 +143,11 @@ const {
|
||||
})
|
||||
|
||||
const selectedProject = ref<ProjectsMoveToWorkspaceDialog_ProjectFragment | null>(null)
|
||||
const showConfirmDialog = ref(false)
|
||||
const showMoveToWorkspaceDialog = ref(false)
|
||||
|
||||
const { remainingModelCount, remainingProjectCount } = useWorkspaceLimits(
|
||||
props.workspace.slug
|
||||
)
|
||||
|
||||
const workspaceProjects = computed(() =>
|
||||
props.workspace.projects.items.map((project) => project.id)
|
||||
const workspaceProjects = computed(
|
||||
() => props.workspace?.projects.items.map((project) => project.id) || []
|
||||
)
|
||||
const userProjects = computed(() => result.value?.activeUser?.projects.items || [])
|
||||
|
||||
@@ -178,6 +168,10 @@ const buttons = computed((): LayoutDialogButton[] => [
|
||||
|
||||
const onMoveClick = (project: ProjectsMoveToWorkspaceDialog_ProjectFragment) => {
|
||||
selectedProject.value = project
|
||||
showMoveToWorkspaceDialog.value = true
|
||||
if (props.workspace) {
|
||||
showConfirmDialog.value = true
|
||||
} else {
|
||||
showMoveToWorkspaceDialog.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,39 +1,33 @@
|
||||
<template>
|
||||
<CommonCard class="w-full bg-foundation">
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<WorkspaceAvatar :name="workspace.name" :logo="workspace.logo" size="xl" />
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-between flex-1">
|
||||
<div class="flex flex-col flex-1">
|
||||
<h6 class="text-heading-sm">{{ workspace.name }}</h6>
|
||||
<p class="text-body-2xs text-foreground-2">
|
||||
{{ workspace.team?.totalCount }}
|
||||
{{ workspace.team?.totalCount === 1 ? 'member' : 'members' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<FormButton
|
||||
v-if="workspace.requestStatus"
|
||||
color="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
class="capitalize"
|
||||
>
|
||||
{{ workspace.requestStatus }}
|
||||
</FormButton>
|
||||
<FormButton v-else color="outline" size="sm" @click="onRequest">
|
||||
Request to join
|
||||
</FormButton>
|
||||
<FormButton color="subtle" size="sm" @click="onDismiss">Dismiss</FormButton>
|
||||
<WorkspaceCard :logo="workspace.logo ?? ''" :name="workspace.name">
|
||||
<template #text>
|
||||
<div class="flex flex-col gap-y-1">
|
||||
<div class="text-body-2xs">
|
||||
{{ workspace.description }}
|
||||
</div>
|
||||
<div class="text-body-2xs">{{ workspace.team?.totalCount }} members</div>
|
||||
</div>
|
||||
</div>
|
||||
</CommonCard>
|
||||
</template>
|
||||
<template #actions>
|
||||
<FormButton
|
||||
v-if="workspace.requestStatus"
|
||||
color="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
class="capitalize"
|
||||
>
|
||||
{{ workspace.requestStatus }}
|
||||
</FormButton>
|
||||
<FormButton v-else color="outline" size="sm" @click="onRequest">
|
||||
Request to join
|
||||
</FormButton>
|
||||
<FormButton color="subtle" size="sm">Dismiss</FormButton>
|
||||
</template>
|
||||
</WorkspaceCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LimitedWorkspace } from '~/lib/common/generated/gql/graphql'
|
||||
import type { LimitedWorkspace } from '~~/lib/common/generated/gql/graphql'
|
||||
import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces'
|
||||
|
||||
type WorkspaceWithStatus = LimitedWorkspace & {
|
||||
@@ -44,14 +38,9 @@ const props = defineProps<{
|
||||
workspace: WorkspaceWithStatus
|
||||
}>()
|
||||
|
||||
const { requestToJoinWorkspace, dismissDiscoverableWorkspace } =
|
||||
useDiscoverableWorkspaces()
|
||||
const { requestToJoinWorkspace } = useDiscoverableWorkspaces()
|
||||
|
||||
const onRequest = () => {
|
||||
requestToJoinWorkspace(props.workspace.id)
|
||||
}
|
||||
|
||||
const onDismiss = () => {
|
||||
dismissDiscoverableWorkspace(props.workspace.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -32,6 +32,17 @@ export const activeUserQuery = graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
export const activeUserProjectsToMoveQuery = graphql(`
|
||||
query ActiveUserProjectsToMove($filter: UserProjectsFilter) {
|
||||
activeUser {
|
||||
id
|
||||
projects(filter: $filter) {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
/**
|
||||
* Lightweight composable to read user id from cache imperatively (useful for logging)
|
||||
*/
|
||||
@@ -51,6 +62,20 @@ export function useResolveUserDistinctId() {
|
||||
}
|
||||
}
|
||||
|
||||
export function useActiveUserProjectsToMove() {
|
||||
const { result } = useQuery(activeUserProjectsToMoveQuery, () => ({
|
||||
filter: {
|
||||
workspaceId: null,
|
||||
onlyWithRoles: [Roles.Stream.Owner]
|
||||
}
|
||||
}))
|
||||
|
||||
const projectsToMoveCount = computed(
|
||||
() => result.value?.activeUser?.projects?.totalCount
|
||||
)
|
||||
return { projectsToMoveCount }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active user.
|
||||
* undefined - not yet resolved
|
||||
|
||||
@@ -74,6 +74,9 @@ export const activeUserWorkspaceExistenceCheckQuery = graphql(`
|
||||
items {
|
||||
id
|
||||
slug
|
||||
creationState {
|
||||
completed
|
||||
}
|
||||
}
|
||||
}
|
||||
discoverableWorkspaces {
|
||||
|
||||
@@ -81,7 +81,7 @@ type Documents = {
|
||||
"\n fragment ProjectPageModelsActions on Model {\n id\n name\n }\n": typeof types.ProjectPageModelsActionsFragmentDoc,
|
||||
"\n fragment ProjectPageModelsActions_Project on Project {\n id\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.ProjectPageModelsActions_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n workspace {\n id\n readOnly\n }\n }\n": typeof types.ProjectPageModelsCardProjectFragmentDoc,
|
||||
"\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 readOnly\n }\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 readOnly\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\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 workspace {\n id\n readOnly\n }\n ...ProjectPageModelsActions_Project\n }\n": typeof types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
|
||||
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\n }\n": typeof types.SingleLevelModelTreeItemFragmentDoc,
|
||||
@@ -101,7 +101,8 @@ type Documents = {
|
||||
"\n fragment ProjectsDashboardFilledUser on UserProjectCollection {\n items {\n ...ProjectDashboardItem\n }\n }\n": typeof types.ProjectsDashboardFilledUserFragmentDoc,
|
||||
"\n fragment ProjectsDeleteDialog_Project on Project {\n id\n name\n role\n models(limit: 0) {\n totalCount\n }\n workspace {\n slug\n id\n }\n versions(limit: 0) {\n totalCount\n }\n }\n": typeof types.ProjectsDeleteDialog_ProjectFragmentDoc,
|
||||
"\n fragment ProjectsHiddenProjectWarning_User on User {\n id\n expiredSsoSessions {\n id\n slug\n name\n logo\n }\n }\n": typeof types.ProjectsHiddenProjectWarning_UserFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n logo\n slug\n ...WorkspaceHasCustomDataResidency_Workspace\n ...ProjectsWorkspaceSelect_Workspace\n }\n": typeof types.ProjectsMoveToWorkspaceDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment MoveToWorkspaceAlert_Project on Project {\n ...ProjectsMoveToWorkspaceDialog_Project\n }\n": typeof types.MoveToWorkspaceAlert_ProjectFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n logo\n slug\n plan {\n name\n }\n projects {\n totalCount\n }\n team {\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n ...WorkspaceHasCustomDataResidency_Workspace\n ...ProjectsWorkspaceSelect_Workspace\n }\n": typeof types.ProjectsMoveToWorkspaceDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_User on User {\n workspaces {\n items {\n ...ProjectsMoveToWorkspaceDialog_Workspace\n }\n }\n }\n": typeof types.ProjectsMoveToWorkspaceDialog_UserFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_Project on Project {\n id\n name\n modelCount: models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n }\n": typeof types.ProjectsMoveToWorkspaceDialog_ProjectFragmentDoc,
|
||||
"\n query ProjectsMoveToWorkspaceDialog {\n activeUser {\n id\n ...ProjectsMoveToWorkspaceDialog_User\n }\n }\n": typeof types.ProjectsMoveToWorkspaceDialogDocument,
|
||||
@@ -133,6 +134,7 @@ type Documents = {
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": typeof types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n": typeof types.SettingsWorkspacesSecuritySsoWrapper_WorkspaceFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n }\n": typeof types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ViewerCommentThreadDataFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": typeof types.ThreadCommentAttachmentFragmentDoc,
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": typeof types.ViewerCommentsListItemFragmentDoc,
|
||||
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": typeof types.ViewerModelVersionCardItemFragmentDoc,
|
||||
@@ -150,6 +152,7 @@ type Documents = {
|
||||
"\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 query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\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 }\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,
|
||||
"\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": typeof types.CreateOnboardingProjectDocument,
|
||||
"\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n": typeof types.FullPermissionCheckResultFragmentDoc,
|
||||
"\n mutation FinishOnboarding($input: OnboardingCompletionInput) {\n activeUserMutations {\n finishOnboarding(input: $input)\n }\n }\n": typeof types.FinishOnboardingDocument,
|
||||
@@ -158,7 +161,7 @@ type Documents = {
|
||||
"\n query AuthRegisterPanel($token: String) {\n serverInfo {\n inviteOnly\n authStrategies {\n id\n }\n ...AuthStategiesServerInfoFragment\n ...ServerTermsOfServicePrivacyPolicyFragment\n }\n serverInviteByToken(token: $token) {\n id\n email\n }\n }\n": typeof types.AuthRegisterPanelDocument,
|
||||
"\n query AuthLoginPanelWorkspaceInvite($token: String) {\n workspaceInvite(token: $token) {\n id\n email\n ...AuthWorkspaceInviteHeader_PendingWorkspaceCollaborator\n ...AuthLoginWithEmailBlock_PendingWorkspaceCollaborator\n }\n }\n": typeof types.AuthLoginPanelWorkspaceInviteDocument,
|
||||
"\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n": typeof types.AuthorizableAppMetadataDocument,
|
||||
"\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserWorkspaceExistenceCheckDocument,
|
||||
"\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n creationState {\n completed\n }\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserWorkspaceExistenceCheckDocument,
|
||||
"\n query ActiveUserActiveWorkspaceCheck {\n activeUser {\n id\n isProjectsActive\n activeWorkspace {\n id\n slug\n }\n }\n }\n": typeof types.ActiveUserActiveWorkspaceCheckDocument,
|
||||
"\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n": typeof types.ProjectWorkspaceAccessCheckDocument,
|
||||
"\n fragment FunctionRunStatusForSummary on AutomateFunctionRun {\n id\n status\n }\n": typeof types.FunctionRunStatusForSummaryFragmentDoc,
|
||||
@@ -215,7 +218,7 @@ type Documents = {
|
||||
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": typeof types.ProjectDashboardItemNoModelsFragmentDoc,
|
||||
"\n fragment ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n models(limit: 4) {\n totalCount\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n workspace {\n id\n slug\n name\n logo\n readOnly\n }\n pendingImportedModels(limit: 4) {\n ...PendingFileUpload\n }\n }\n": typeof types.ProjectDashboardItemFragmentDoc,
|
||||
"\n fragment PendingFileUpload on FileUpload {\n id\n projectId\n modelName\n convertedStatus\n convertedMessage\n uploadDate\n convertedLastUpdate\n fileType\n fileName\n }\n": typeof types.PendingFileUploadFragmentDoc,
|
||||
"\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n }\n": typeof types.ProjectPageLatestItemsModelItemFragmentDoc,
|
||||
"\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canDelete {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageLatestItemsModelItemFragmentDoc,
|
||||
"\n fragment ProjectUpdatableMetadata on Project {\n id\n name\n description\n visibility\n allowPublicComments\n permissions {\n canRead {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canUpdateAllowPublicComments {\n ...FullPermissionCheckResult\n }\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canLeave {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectUpdatableMetadataFragmentDoc,
|
||||
"\n fragment ProjectPageLatestItemsModels on Project {\n id\n role\n visibility\n workspace {\n id\n readOnly\n }\n modelCount: models(limit: 0) {\n totalCount\n }\n ...ProjectPageModelsStructureItem_Project\n }\n": typeof types.ProjectPageLatestItemsModelsFragmentDoc,
|
||||
"\n fragment ProjectPageLatestItemsComments on Project {\n id\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n": typeof types.ProjectPageLatestItemsCommentsFragmentDoc,
|
||||
@@ -329,8 +332,10 @@ type Documents = {
|
||||
"\n mutation verifyEmail($input: VerifyUserEmailInput!) {\n activeUserMutations {\n emailMutations {\n verify(input: $input)\n }\n }\n }\n": typeof types.VerifyEmailDocument,
|
||||
"\n fragment EmailFields on UserEmail {\n id\n email\n verified\n primary\n userId\n }\n": typeof types.EmailFieldsFragmentDoc,
|
||||
"\n query UserEmails {\n activeUser {\n id\n emails {\n ...EmailFields\n }\n hasPendingVerification\n }\n }\n": typeof types.UserEmailsDocument,
|
||||
"\n fragment UseViewerUserActivityBroadcasting_Project on Project {\n id\n permissions {\n canBroadcastActivity {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseViewerUserActivityBroadcasting_ProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\n }\n": typeof types.ViewerCommentBubblesDataFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n": typeof types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseCheckViewerCommentingAccess_ProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": typeof types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": typeof types.ViewerCommentsReplyItemFragmentDoc,
|
||||
"\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": typeof types.BroadcastViewerUserActivityDocument,
|
||||
"\n mutation MarkCommentViewed($input: MarkCommentViewedInput!) {\n commentMutations {\n markViewed(input: $input)\n }\n }\n": typeof types.MarkCommentViewedDocument,
|
||||
@@ -338,7 +343,7 @@ type Documents = {
|
||||
"\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": typeof types.CreateCommentReplyDocument,
|
||||
"\n mutation ArchiveComment($input: ArchiveCommentInput!) {\n commentMutations {\n archive(input: $input)\n }\n }\n": typeof types.ArchiveCommentDocument,
|
||||
"\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": typeof types.ProjectViewerResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n }\n }\n": typeof types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n }\n }\n": typeof types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": typeof types.ViewerModelVersionsDocument,
|
||||
"\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": typeof types.ViewerDiffVersionsDocument,
|
||||
"\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": typeof types.ViewerLoadedThreadsDocument,
|
||||
@@ -396,6 +401,7 @@ type Documents = {
|
||||
"\n query WorkspaceLastAdminCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceLastAdminCheck_Workspace\n }\n }\n": typeof types.WorkspaceLastAdminCheckDocument,
|
||||
"\n query WorkspaceLimits($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacePlanLimits_Workspace\n }\n }\n": typeof types.WorkspaceLimitsDocument,
|
||||
"\n query WorkspaceUsage($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceUsage_Workspace\n }\n }\n": typeof types.WorkspaceUsageDocument,
|
||||
"\n query MoveToWorkspaceAlert($id: String!) {\n project(id: $id) {\n ...MoveToWorkspaceAlert_Project\n }\n }\n": typeof types.MoveToWorkspaceAlertDocument,
|
||||
"\n subscription onWorkspaceUpdated(\n $workspaceId: String\n $workspaceSlug: String\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceUpdated(workspaceId: $workspaceId, workspaceSlug: $workspaceSlug) {\n id\n workspace {\n id\n ...WorkspaceProjectList_Workspace\n }\n }\n }\n": typeof types.OnWorkspaceUpdatedDocument,
|
||||
"\n query LegacyBranchRedirectMetadata($streamId: String!, $branchName: String!) {\n project(id: $streamId) {\n modelByName(name: $branchName) {\n id\n }\n }\n }\n": typeof types.LegacyBranchRedirectMetadataDocument,
|
||||
"\n query LegacyViewerCommitRedirectMetadata($streamId: String!, $commitId: String!) {\n project(id: $streamId) {\n version(id: $commitId) {\n id\n model {\n id\n }\n }\n }\n }\n": typeof types.LegacyViewerCommitRedirectMetadataDocument,
|
||||
@@ -407,10 +413,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 canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_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 }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n ...ProjectPageSettingsTab_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\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 permissions {\n canUpdate {\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 }\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 fragment SettingsServerProjects_User on User {\n permissions {\n canCreatePersonalProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.SettingsServerProjects_UserFragmentDoc,
|
||||
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": typeof types.SettingsServerRegionsDocument,
|
||||
@@ -420,7 +426,7 @@ type Documents = {
|
||||
"\n fragment SettingsWorkspacesProjects_Workspace on Workspace {\n id\n slug\n permissions {\n canCreateProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.SettingsWorkspacesProjects_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": typeof types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": typeof types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n": typeof types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
};
|
||||
const documents: Documents = {
|
||||
"\n fragment AuthLoginWithEmailBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n email\n user {\n id\n }\n }\n": types.AuthLoginWithEmailBlock_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
@@ -490,7 +496,7 @@ const documents: Documents = {
|
||||
"\n fragment ProjectPageModelsActions on Model {\n id\n name\n }\n": types.ProjectPageModelsActionsFragmentDoc,
|
||||
"\n fragment ProjectPageModelsActions_Project on Project {\n id\n ...ProjectsModelPageEmbed_Project\n }\n": types.ProjectPageModelsActions_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n workspace {\n id\n readOnly\n }\n }\n": types.ProjectPageModelsCardProjectFragmentDoc,
|
||||
"\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 readOnly\n }\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 readOnly\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\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 workspace {\n id\n readOnly\n }\n ...ProjectPageModelsActions_Project\n }\n": types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
|
||||
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
|
||||
@@ -510,7 +516,8 @@ const documents: Documents = {
|
||||
"\n fragment ProjectsDashboardFilledUser on UserProjectCollection {\n items {\n ...ProjectDashboardItem\n }\n }\n": types.ProjectsDashboardFilledUserFragmentDoc,
|
||||
"\n fragment ProjectsDeleteDialog_Project on Project {\n id\n name\n role\n models(limit: 0) {\n totalCount\n }\n workspace {\n slug\n id\n }\n versions(limit: 0) {\n totalCount\n }\n }\n": types.ProjectsDeleteDialog_ProjectFragmentDoc,
|
||||
"\n fragment ProjectsHiddenProjectWarning_User on User {\n id\n expiredSsoSessions {\n id\n slug\n name\n logo\n }\n }\n": types.ProjectsHiddenProjectWarning_UserFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n logo\n slug\n ...WorkspaceHasCustomDataResidency_Workspace\n ...ProjectsWorkspaceSelect_Workspace\n }\n": types.ProjectsMoveToWorkspaceDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment MoveToWorkspaceAlert_Project on Project {\n ...ProjectsMoveToWorkspaceDialog_Project\n }\n": types.MoveToWorkspaceAlert_ProjectFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n logo\n slug\n plan {\n name\n }\n projects {\n totalCount\n }\n team {\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n ...WorkspaceHasCustomDataResidency_Workspace\n ...ProjectsWorkspaceSelect_Workspace\n }\n": types.ProjectsMoveToWorkspaceDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_User on User {\n workspaces {\n items {\n ...ProjectsMoveToWorkspaceDialog_Workspace\n }\n }\n }\n": types.ProjectsMoveToWorkspaceDialog_UserFragmentDoc,
|
||||
"\n fragment ProjectsMoveToWorkspaceDialog_Project on Project {\n id\n name\n modelCount: models(limit: 0) {\n totalCount\n }\n versions(limit: 0) {\n totalCount\n }\n }\n": types.ProjectsMoveToWorkspaceDialog_ProjectFragmentDoc,
|
||||
"\n query ProjectsMoveToWorkspaceDialog {\n activeUser {\n id\n ...ProjectsMoveToWorkspaceDialog_User\n }\n }\n": types.ProjectsMoveToWorkspaceDialogDocument,
|
||||
@@ -542,6 +549,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n": types.SettingsWorkspacesSecuritySsoWrapper_WorkspaceFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n }\n": types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ViewerCommentThreadDataFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": types.ThreadCommentAttachmentFragmentDoc,
|
||||
"\n fragment ViewerCommentsListItem on Comment {\n id\n rawText\n archived\n author {\n ...LimitedUserAvatar\n }\n createdAt\n viewedAt\n replies {\n totalCount\n cursor\n items {\n ...ViewerCommentsReplyItem\n }\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n resources {\n resourceId\n resourceType\n }\n }\n": types.ViewerCommentsListItemFragmentDoc,
|
||||
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": types.ViewerModelVersionCardItemFragmentDoc,
|
||||
@@ -559,6 +567,7 @@ const documents: Documents = {
|
||||
"\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 query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\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 }\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,
|
||||
"\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": types.CreateOnboardingProjectDocument,
|
||||
"\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n": types.FullPermissionCheckResultFragmentDoc,
|
||||
"\n mutation FinishOnboarding($input: OnboardingCompletionInput) {\n activeUserMutations {\n finishOnboarding(input: $input)\n }\n }\n": types.FinishOnboardingDocument,
|
||||
@@ -567,7 +576,7 @@ const documents: Documents = {
|
||||
"\n query AuthRegisterPanel($token: String) {\n serverInfo {\n inviteOnly\n authStrategies {\n id\n }\n ...AuthStategiesServerInfoFragment\n ...ServerTermsOfServicePrivacyPolicyFragment\n }\n serverInviteByToken(token: $token) {\n id\n email\n }\n }\n": types.AuthRegisterPanelDocument,
|
||||
"\n query AuthLoginPanelWorkspaceInvite($token: String) {\n workspaceInvite(token: $token) {\n id\n email\n ...AuthWorkspaceInviteHeader_PendingWorkspaceCollaborator\n ...AuthLoginWithEmailBlock_PendingWorkspaceCollaborator\n }\n }\n": types.AuthLoginPanelWorkspaceInviteDocument,
|
||||
"\n query AuthorizableAppMetadata($id: String!) {\n app(id: $id) {\n id\n name\n description\n trustByDefault\n redirectUrl\n scopes {\n name\n description\n }\n author {\n name\n id\n avatar\n }\n }\n }\n": types.AuthorizableAppMetadataDocument,
|
||||
"\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserWorkspaceExistenceCheckDocument,
|
||||
"\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n creationState {\n completed\n }\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserWorkspaceExistenceCheckDocument,
|
||||
"\n query ActiveUserActiveWorkspaceCheck {\n activeUser {\n id\n isProjectsActive\n activeWorkspace {\n id\n slug\n }\n }\n }\n": types.ActiveUserActiveWorkspaceCheckDocument,
|
||||
"\n query projectWorkspaceAccessCheck($projectId: String!) {\n project(id: $projectId) {\n id\n role\n workspace {\n id\n slug\n role\n }\n }\n }\n": types.ProjectWorkspaceAccessCheckDocument,
|
||||
"\n fragment FunctionRunStatusForSummary on AutomateFunctionRun {\n id\n status\n }\n": types.FunctionRunStatusForSummaryFragmentDoc,
|
||||
@@ -624,7 +633,7 @@ const documents: Documents = {
|
||||
"\n fragment ProjectDashboardItemNoModels on Project {\n id\n name\n createdAt\n updatedAt\n role\n team {\n id\n user {\n id\n name\n avatar\n }\n }\n ...ProjectPageModelsCardProject\n }\n": types.ProjectDashboardItemNoModelsFragmentDoc,
|
||||
"\n fragment ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n models(limit: 4) {\n totalCount\n items {\n ...ProjectPageLatestItemsModelItem\n }\n }\n workspace {\n id\n slug\n name\n logo\n readOnly\n }\n pendingImportedModels(limit: 4) {\n ...PendingFileUpload\n }\n }\n": types.ProjectDashboardItemFragmentDoc,
|
||||
"\n fragment PendingFileUpload on FileUpload {\n id\n projectId\n modelName\n convertedStatus\n convertedMessage\n uploadDate\n convertedLastUpdate\n fileType\n fileName\n }\n": types.PendingFileUploadFragmentDoc,
|
||||
"\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n }\n": types.ProjectPageLatestItemsModelItemFragmentDoc,
|
||||
"\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canDelete {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageLatestItemsModelItemFragmentDoc,
|
||||
"\n fragment ProjectUpdatableMetadata on Project {\n id\n name\n description\n visibility\n allowPublicComments\n permissions {\n canRead {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canUpdateAllowPublicComments {\n ...FullPermissionCheckResult\n }\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canLeave {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectUpdatableMetadataFragmentDoc,
|
||||
"\n fragment ProjectPageLatestItemsModels on Project {\n id\n role\n visibility\n workspace {\n id\n readOnly\n }\n modelCount: models(limit: 0) {\n totalCount\n }\n ...ProjectPageModelsStructureItem_Project\n }\n": types.ProjectPageLatestItemsModelsFragmentDoc,
|
||||
"\n fragment ProjectPageLatestItemsComments on Project {\n id\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageLatestItemsCommentsFragmentDoc,
|
||||
@@ -738,8 +747,10 @@ const documents: Documents = {
|
||||
"\n mutation verifyEmail($input: VerifyUserEmailInput!) {\n activeUserMutations {\n emailMutations {\n verify(input: $input)\n }\n }\n }\n": types.VerifyEmailDocument,
|
||||
"\n fragment EmailFields on UserEmail {\n id\n email\n verified\n primary\n userId\n }\n": types.EmailFieldsFragmentDoc,
|
||||
"\n query UserEmails {\n activeUser {\n id\n emails {\n ...EmailFields\n }\n hasPendingVerification\n }\n }\n": types.UserEmailsDocument,
|
||||
"\n fragment UseViewerUserActivityBroadcasting_Project on Project {\n id\n permissions {\n canBroadcastActivity {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseViewerUserActivityBroadcasting_ProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\n }\n": types.ViewerCommentBubblesDataFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n": types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseCheckViewerCommentingAccess_ProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": types.ViewerCommentsReplyItemFragmentDoc,
|
||||
"\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": types.BroadcastViewerUserActivityDocument,
|
||||
"\n mutation MarkCommentViewed($input: MarkCommentViewedInput!) {\n commentMutations {\n markViewed(input: $input)\n }\n }\n": types.MarkCommentViewedDocument,
|
||||
@@ -747,7 +758,7 @@ const documents: Documents = {
|
||||
"\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": types.CreateCommentReplyDocument,
|
||||
"\n mutation ArchiveComment($input: ArchiveCommentInput!) {\n commentMutations {\n archive(input: $input)\n }\n }\n": types.ArchiveCommentDocument,
|
||||
"\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": types.ProjectViewerResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n }\n }\n": types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n }\n }\n": types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": types.ViewerModelVersionsDocument,
|
||||
"\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": types.ViewerDiffVersionsDocument,
|
||||
"\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": types.ViewerLoadedThreadsDocument,
|
||||
@@ -805,6 +816,7 @@ const documents: Documents = {
|
||||
"\n query WorkspaceLastAdminCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceLastAdminCheck_Workspace\n }\n }\n": types.WorkspaceLastAdminCheckDocument,
|
||||
"\n query WorkspaceLimits($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacePlanLimits_Workspace\n }\n }\n": types.WorkspaceLimitsDocument,
|
||||
"\n query WorkspaceUsage($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceUsage_Workspace\n }\n }\n": types.WorkspaceUsageDocument,
|
||||
"\n query MoveToWorkspaceAlert($id: String!) {\n project(id: $id) {\n ...MoveToWorkspaceAlert_Project\n }\n }\n": types.MoveToWorkspaceAlertDocument,
|
||||
"\n subscription onWorkspaceUpdated(\n $workspaceId: String\n $workspaceSlug: String\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceUpdated(workspaceId: $workspaceId, workspaceSlug: $workspaceSlug) {\n id\n workspace {\n id\n ...WorkspaceProjectList_Workspace\n }\n }\n }\n": types.OnWorkspaceUpdatedDocument,
|
||||
"\n query LegacyBranchRedirectMetadata($streamId: String!, $branchName: String!) {\n project(id: $streamId) {\n modelByName(name: $branchName) {\n id\n }\n }\n }\n": types.LegacyBranchRedirectMetadataDocument,
|
||||
"\n query LegacyViewerCommitRedirectMetadata($streamId: String!, $commitId: String!) {\n project(id: $streamId) {\n version(id: $commitId) {\n id\n model {\n id\n }\n }\n }\n }\n": types.LegacyViewerCommitRedirectMetadataDocument,
|
||||
@@ -816,10 +828,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 canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_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 }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n ...ProjectPageSettingsTab_Project\n }\n": types.ProjectPageProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\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 permissions {\n canUpdate {\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 }\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 fragment SettingsServerProjects_User on User {\n permissions {\n canCreatePersonalProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.SettingsServerProjects_UserFragmentDoc,
|
||||
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": types.SettingsServerRegionsDocument,
|
||||
@@ -829,7 +841,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsWorkspacesProjects_Workspace on Workspace {\n id\n slug\n permissions {\n canCreateProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.SettingsWorkspacesProjects_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1117,7 +1129,7 @@ export function graphql(source: "\n fragment ProjectPageModelsCardProject on Pr
|
||||
/**
|
||||
* 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 readOnly\n }\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 readOnly\n }\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 readOnly\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\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 readOnly\n }\n permissions {\n canCreateModel {\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.
|
||||
*/
|
||||
@@ -1197,7 +1209,11 @@ export function graphql(source: "\n fragment ProjectsHiddenProjectWarning_User
|
||||
/**
|
||||
* 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 ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n logo\n slug\n ...WorkspaceHasCustomDataResidency_Workspace\n ...ProjectsWorkspaceSelect_Workspace\n }\n"): (typeof documents)["\n fragment ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n logo\n slug\n ...WorkspaceHasCustomDataResidency_Workspace\n ...ProjectsWorkspaceSelect_Workspace\n }\n"];
|
||||
export function graphql(source: "\n fragment MoveToWorkspaceAlert_Project on Project {\n ...ProjectsMoveToWorkspaceDialog_Project\n }\n"): (typeof documents)["\n fragment MoveToWorkspaceAlert_Project on Project {\n ...ProjectsMoveToWorkspaceDialog_Project\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 ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n logo\n slug\n plan {\n name\n }\n projects {\n totalCount\n }\n team {\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n ...WorkspaceHasCustomDataResidency_Workspace\n ...ProjectsWorkspaceSelect_Workspace\n }\n"): (typeof documents)["\n fragment ProjectsMoveToWorkspaceDialog_Workspace on Workspace {\n id\n role\n name\n logo\n slug\n plan {\n name\n }\n projects {\n totalCount\n }\n team {\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n ...WorkspaceHasCustomDataResidency_Workspace\n ...ProjectsWorkspaceSelect_Workspace\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1322,6 +1338,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesSecuritySsoWrapp
|
||||
* 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 ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\n }\n }\n"): (typeof documents)["\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n workspace {\n id\n slug\n name\n role\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 ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\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.
|
||||
*/
|
||||
@@ -1390,6 +1410,10 @@ 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 query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\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 }\n }\n"): (typeof documents)["\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\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 }\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 ActiveUserProjectsToMove($filter: UserProjectsFilter) {\n activeUser {\n id\n projects(filter: $filter) {\n totalCount\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserProjectsToMove($filter: UserProjectsFilter) {\n activeUser {\n id\n projects(filter: $filter) {\n totalCount\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1425,7 +1449,7 @@ export function graphql(source: "\n query AuthorizableAppMetadata($id: String!)
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n creationState {\n completed\n }\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserWorkspaceExistenceCheck {\n activeUser {\n id\n verified\n isOnboardingFinished\n versions(limit: 0) {\n totalCount\n }\n workspaces(limit: 0) {\n totalCount\n items {\n id\n slug\n creationState {\n completed\n }\n }\n }\n discoverableWorkspaces {\n id\n }\n workspaceJoinRequests(limit: 0) {\n totalCount\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1653,7 +1677,7 @@ export function graphql(source: "\n fragment PendingFileUpload on FileUpload {\
|
||||
/**
|
||||
* 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 ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canDelete {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsModelItem on Model {\n id\n name\n displayName\n versionCount: versions(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n pendingImportedVersions(limit: 1) {\n ...PendingFileUpload\n }\n previewUrl\n createdAt\n updatedAt\n ...ProjectPageModelsCardRenameDialog\n ...ProjectPageModelsCardDeleteDialog\n ...ProjectPageModelsActions\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canDelete {\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.
|
||||
*/
|
||||
@@ -2106,6 +2130,10 @@ export function graphql(source: "\n fragment EmailFields on UserEmail {\n id
|
||||
* 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 UserEmails {\n activeUser {\n id\n emails {\n ...EmailFields\n }\n hasPendingVerification\n }\n }\n"): (typeof documents)["\n query UserEmails {\n activeUser {\n id\n emails {\n ...EmailFields\n }\n hasPendingVerification\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 UseViewerUserActivityBroadcasting_Project on Project {\n id\n permissions {\n canBroadcastActivity {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment UseViewerUserActivityBroadcasting_Project on Project {\n id\n permissions {\n canBroadcastActivity {\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.
|
||||
*/
|
||||
@@ -2113,7 +2141,11 @@ export function graphql(source: "\n fragment ViewerCommentBubblesData on Commen
|
||||
/**
|
||||
* 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 ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n"): (typeof documents)["\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n }\n"];
|
||||
export function graphql(source: "\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\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.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n"): (typeof documents)["\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2145,7 +2177,7 @@ export function graphql(source: "\n query ProjectViewerResources($projectId: St
|
||||
/**
|
||||
* 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 ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n }\n }\n"];
|
||||
export function graphql(source: "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2374,6 +2406,10 @@ export function graphql(source: "\n query WorkspaceLimits($slug: String!) {\n
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(source: "\n query WorkspaceUsage($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceUsage_Workspace\n }\n }\n"): (typeof documents)["\n query WorkspaceUsage($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceUsage_Workspace\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 MoveToWorkspaceAlert($id: String!) {\n project(id: $id) {\n ...MoveToWorkspaceAlert_Project\n }\n }\n"): (typeof documents)["\n query MoveToWorkspaceAlert($id: String!) {\n project(id: $id) {\n ...MoveToWorkspaceAlert_Project\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2421,7 +2457,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 canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_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 }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_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 }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n ...ProjectPageSettingsTab_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 }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...ProjectsMoveToWorkspaceDialog_Project\n ...ProjectPageSettingsTab_Project\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2433,7 +2469,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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n permissions {\n canUpdate {\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 }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\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.
|
||||
*/
|
||||
@@ -2473,7 +2509,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesRegions_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 SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"];
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -213,3 +213,4 @@ export const doesRouteFitTarget = (fullPathA: string, fullPathB: string) => {
|
||||
// Link to Workspace roles and seats documentation
|
||||
// TODO: Add link when ready
|
||||
export const LearnMoreRolesSeatsUrl = 'https://speckle.guide/'
|
||||
export const LearnMoreMoveProjectsUrl = 'https://speckle.guide/'
|
||||
|
||||
@@ -306,6 +306,9 @@ function createCache(): InMemoryCache {
|
||||
},
|
||||
subscription: {
|
||||
merge: mergeAsObjectsFunction
|
||||
},
|
||||
creationState: {
|
||||
merge: mergeAsObjectsFunction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export const useNavigation = () => {
|
||||
)
|
||||
|
||||
// Set state and mutate
|
||||
const mutateActiveWorkspaceSlug = async (newVal: string) => {
|
||||
const mutateActiveWorkspaceSlug = async (newVal: string | null) => {
|
||||
state.value.activeWorkspaceSlug = newVal
|
||||
state.value.isProjectsActive = false
|
||||
await mutate({ slug: newVal, isProjectsActive: false })
|
||||
|
||||
@@ -108,6 +108,14 @@ export const projectPageLatestItemsModelItemFragment = graphql(`
|
||||
automationsStatus {
|
||||
...AutomateRunsTriggerStatus_TriggeredAutomationsStatus
|
||||
}
|
||||
permissions {
|
||||
canUpdate {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
canDelete {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { Roles } from '@speckle/shared'
|
||||
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
||||
|
||||
export const canEditProject = (project: { role?: MaybeNullOrUndefined<string> }) =>
|
||||
export const canInviteToProject = (project: { role?: MaybeNullOrUndefined<string> }) =>
|
||||
([Roles.Stream.Owner] as Array<MaybeNullOrUndefined<string>>).includes(project.role)
|
||||
|
||||
export const canInviteToProject = canEditProject
|
||||
|
||||
export const canModifyModels = (project: { role?: MaybeNullOrUndefined<string> }) =>
|
||||
(
|
||||
[Roles.Stream.Contributor, Roles.Stream.Owner] as Array<
|
||||
MaybeNullOrUndefined<string>
|
||||
>
|
||||
).includes(project.role)
|
||||
|
||||
@@ -54,7 +54,8 @@ export function useAddWorkspaceDomain() {
|
||||
domains: WorkspaceDomain[],
|
||||
discoverabilityEnabled: boolean,
|
||||
domainBasedMembershipProtectionEnabled: boolean,
|
||||
hasAccessToSSO: boolean
|
||||
hasAccessToSSO: boolean,
|
||||
hasAccessToDomainBasedSecurityPolicies: boolean
|
||||
) => {
|
||||
const result = await apollo
|
||||
.mutate({
|
||||
@@ -90,7 +91,8 @@ export function useAddWorkspaceDomain() {
|
||||
],
|
||||
discoverabilityEnabled,
|
||||
domainBasedMembershipProtectionEnabled,
|
||||
hasAccessToSSO
|
||||
hasAccessToSSO,
|
||||
hasAccessToDomainBasedSecurityPolicies
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
useStateSerialization
|
||||
} from '~~/lib/viewer/composables/serialization'
|
||||
import type { Merge } from 'type-fest'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
/**
|
||||
* How often we send out an "activity" message even if user hasn't made any clicks (just to keep him active)
|
||||
@@ -66,6 +67,17 @@ function useCollectMainMetadata() {
|
||||
})
|
||||
}
|
||||
|
||||
graphql(`
|
||||
fragment UseViewerUserActivityBroadcasting_Project on Project {
|
||||
id
|
||||
permissions {
|
||||
canBroadcastActivity {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export function useViewerUserActivityBroadcasting(
|
||||
options?: Partial<{
|
||||
state: InjectableViewerState
|
||||
@@ -74,14 +86,18 @@ export function useViewerUserActivityBroadcasting(
|
||||
const {
|
||||
projectId,
|
||||
resources: {
|
||||
request: { resourceIdString }
|
||||
request: { resourceIdString },
|
||||
response: { project }
|
||||
}
|
||||
} = options?.state || useInjectedViewerState()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const getMainMetadata = useCollectMainMetadata()
|
||||
const apollo = useApolloClient().client
|
||||
const { isEnabled: isEmbedEnabled } = useEmbed()
|
||||
|
||||
const canBroadcast = computed(
|
||||
() => project.value?.permissions.canBroadcastActivity.authorized
|
||||
)
|
||||
|
||||
const isSameMessage = (
|
||||
previousSerializedMessage: Optional<string>,
|
||||
newMessage: ViewerUserActivityMessageInput
|
||||
@@ -118,7 +134,7 @@ export function useViewerUserActivityBroadcasting(
|
||||
}
|
||||
|
||||
const invoke = async (message: ViewerUserActivityMessageInput) => {
|
||||
if (!isLoggedIn.value || isEmbedEnabled.value) return false
|
||||
if (!canBroadcast.value || isEmbedEnabled.value) return false
|
||||
return await Promise.all([
|
||||
invokeMutation(message),
|
||||
invokeObservabilityEvent(message)
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
StateApplyMode
|
||||
} from '~~/lib/viewer/composables/serialization'
|
||||
import type { CommentBubbleModel } from '~/lib/viewer/composables/commentBubbles'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
|
||||
export function useViewerCommentUpdateTracking(
|
||||
params: {
|
||||
@@ -229,21 +230,26 @@ export function useArchiveComment() {
|
||||
}
|
||||
}
|
||||
|
||||
graphql(`
|
||||
fragment UseCheckViewerCommentingAccess_Project on Project {
|
||||
id
|
||||
permissions {
|
||||
canCreateComment {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export function useCheckViewerCommentingAccess() {
|
||||
const {
|
||||
resources: {
|
||||
response: { project }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
const { activeUser } = useActiveUser()
|
||||
|
||||
return computed(() => {
|
||||
if (!activeUser.value) return false
|
||||
|
||||
const hasRole = !!project.value?.role
|
||||
const allowPublicComments = !!project.value?.allowPublicComments
|
||||
|
||||
return hasRole || allowPublicComments
|
||||
return project.value?.permissions.canCreateComment.authorized
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,7 @@ import {
|
||||
SpeckleLoader
|
||||
} from '@speckle/viewer'
|
||||
import { useAuthCookie } from '~~/lib/auth/composables/auth'
|
||||
import type {
|
||||
Project,
|
||||
ProjectCommentThreadsArgs,
|
||||
ViewerResourceItem
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import type { ViewerResourceItem } from '~~/lib/common/generated/gql/graphql'
|
||||
import { ProjectCommentsUpdatedMessageType } from '~~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
useInjectedViewer,
|
||||
@@ -39,11 +35,7 @@ import {
|
||||
useViewerEventListener
|
||||
} from '~~/lib/viewer/composables/viewer'
|
||||
import { useViewerCommentUpdateTracking } from '~~/lib/viewer/composables/commentManagement'
|
||||
import {
|
||||
getCacheId,
|
||||
getObjectReference,
|
||||
modifyObjectFields
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import { getCacheId } from '~~/lib/common/helpers/graphql'
|
||||
import {
|
||||
useViewerOpenedThreadUpdateEmitter,
|
||||
useViewerThreadTracking
|
||||
@@ -264,21 +256,19 @@ function useViewerSubscriptionEventTracker() {
|
||||
})
|
||||
|
||||
// Remove from project.commentThreads
|
||||
modifyObjectFields<ProjectCommentThreadsArgs, Project['commentThreads']>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('Project', projectId.value),
|
||||
(fieldName, variables, data) => {
|
||||
if (fieldName !== 'commentThreads') return
|
||||
if (variables.filter?.includeArchived) return
|
||||
'commentThreads',
|
||||
({ variables, helpers: { createUpdatedValue, readField } }) => {
|
||||
if (variables.filter?.includeArchived) return // we want it in that list
|
||||
|
||||
const newItems = (data.items || []).filter(
|
||||
(i) => i.__ref !== getObjectReference('Comment', event.id).__ref
|
||||
)
|
||||
return {
|
||||
...data,
|
||||
...(data.items ? { items: newItems } : {}),
|
||||
...(data.totalCount ? { totalCount: data.totalCount - 1 } : {})
|
||||
}
|
||||
return createUpdatedValue(({ update }) => {
|
||||
update('totalCount', (totalCount) => totalCount - 1)
|
||||
update('items', (items) =>
|
||||
items.filter((i) => readField(i, 'id') !== event.id)
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
} else if (isNew && comment) {
|
||||
@@ -298,21 +288,22 @@ function useViewerSubscriptionEventTracker() {
|
||||
)
|
||||
} else {
|
||||
// Add comment thread
|
||||
modifyObjectFields<ProjectCommentThreadsArgs, Project['commentThreads']>(
|
||||
modifyObjectField(
|
||||
cache,
|
||||
getCacheId('Project', projectId.value),
|
||||
(fieldName, _variables, data) => {
|
||||
if (fieldName !== 'commentThreads') return
|
||||
'commentThreads',
|
||||
({ helpers: { ref, createUpdatedValue, readField }, value }) => {
|
||||
// In case this is actually an unarchived comment, we only want to add it if it doesnt
|
||||
// exist in the includesArchived list already
|
||||
const includesItem = value.items?.find(
|
||||
(i) => readField(i, 'id') === comment.id
|
||||
)
|
||||
if (includesItem) return
|
||||
|
||||
const newItems = [
|
||||
getObjectReference('Comment', comment.id),
|
||||
...(data.items || [])
|
||||
]
|
||||
return {
|
||||
...data,
|
||||
...(data.items ? { items: newItems } : {}),
|
||||
...(data.totalCount ? { totalCount: data.totalCount + 1 } : {})
|
||||
}
|
||||
return createUpdatedValue(({ update }) => {
|
||||
update('totalCount', (totalCount) => totalCount + 1)
|
||||
update('items', (items) => [ref('Comment', comment.id), ...items])
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export const viewerCommentThreadFragment = graphql(`
|
||||
...ViewerCommentsListItem
|
||||
...ViewerCommentBubblesData
|
||||
...ViewerCommentsReplyItem
|
||||
...ViewerCommentThreadData
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ export const viewerLoadedResourcesQuery = graphql(`
|
||||
...ProjectPageLatestItemsModels
|
||||
...ModelPageProject
|
||||
...HeaderNavShare_Project
|
||||
...UseCheckViewerCommentingAccess_Project
|
||||
...UseViewerUserActivityBroadcasting_Project
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -62,6 +62,11 @@ export const useWorkspacePlan = (slug: string) => {
|
||||
const plan = computed(() => result.value?.workspaceBySlug?.plan)
|
||||
|
||||
const isFreePlan = computed(() => plan.value?.name === UnpaidWorkspacePlans.Free)
|
||||
const isBusinessPlan = computed(
|
||||
() =>
|
||||
plan.value?.name === PaidWorkspacePlansNew.Pro ||
|
||||
plan.value?.name === PaidWorkspacePlansNew.ProUnlimited
|
||||
)
|
||||
const isUnlimitedPlan = computed(
|
||||
() => plan.value?.name === UnpaidWorkspacePlans.Unlimited
|
||||
)
|
||||
@@ -87,13 +92,18 @@ export const useWorkspacePlan = (slug: string) => {
|
||||
const intervalIsYearly = computed(
|
||||
() => billingInterval.value === BillingInterval.Yearly
|
||||
)
|
||||
const billingCycleEnd = computed(() => subscription.value?.currentBillingCycleEnd)
|
||||
const currentBillingCycleEnd = computed(
|
||||
() => subscription.value?.currentBillingCycleEnd
|
||||
)
|
||||
|
||||
// Seat information
|
||||
const seats = computed(() => subscription.value?.seats)
|
||||
const hasAvailableEditorSeats = computed(() =>
|
||||
seats.value?.editors.available && seats.value?.editors.available > 0 ? true : false
|
||||
)
|
||||
const hasAvailableEditorSeats = computed(() => {
|
||||
if (seats.value?.editors.available && seats.value?.editors.assigned) {
|
||||
return seats.value?.editors.available - seats.value?.editors.assigned > 0
|
||||
}
|
||||
return false
|
||||
})
|
||||
const editorSeatPriceFormatted = computed(() => {
|
||||
if (
|
||||
plan.value?.name === WorkspacePlans.Team ||
|
||||
@@ -118,12 +128,13 @@ export const useWorkspacePlan = (slug: string) => {
|
||||
isFreePlan,
|
||||
billingInterval,
|
||||
intervalIsYearly,
|
||||
billingCycleEnd,
|
||||
currentBillingCycleEnd,
|
||||
statusIsCancelationScheduled,
|
||||
subscription,
|
||||
seats,
|
||||
hasAvailableEditorSeats,
|
||||
editorSeatPriceFormatted,
|
||||
isUnlimitedPlan
|
||||
isUnlimitedPlan,
|
||||
isBusinessPlan
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,3 +181,11 @@ export const workspaceUsageQuery = graphql(`
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const moveToWorkspaceAlertQuery = graphql(`
|
||||
query MoveToWorkspaceAlert($id: String!) {
|
||||
project(id: $id) {
|
||||
...MoveToWorkspaceAlert_Project
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -105,7 +105,10 @@ export default defineNuxtRouteMiddleware(async (to) => {
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
const workspaces = workspaceExistenceData?.activeUser?.workspaces?.items ?? []
|
||||
const workspaces =
|
||||
workspaceExistenceData?.activeUser?.workspaces?.items.filter(
|
||||
(w) => w.creationState?.completed !== false
|
||||
) ?? []
|
||||
const hasWorkspaces = workspaces.length > 0
|
||||
const hasDiscoverableWorkspaces =
|
||||
(workspaceExistenceData?.activeUser?.discoverableWorkspaces?.length ?? 0) > 0 ||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/** TODO: Improve when composable is updated */
|
||||
|
||||
import { projectsRoute, workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import {
|
||||
activeUserWorkspaceExistenceCheckQuery,
|
||||
activeUserActiveWorkspaceCheckQuery
|
||||
} from '~/lib/auth/graphql/queries'
|
||||
import { useApolloClientFromNuxt } from '~~/lib/common/composables/graphql'
|
||||
import { convertThrowIntoFetchResult } from '~~/lib/common/helpers/graphql'
|
||||
import { useNavigation } from '~/lib/navigation/composables/navigation'
|
||||
|
||||
export default defineNuxtRouteMiddleware(async (to) => {
|
||||
const isAuthPage = to.path.startsWith('/authn/')
|
||||
const isSSOPath = to.path.includes('/sso/')
|
||||
if (isAuthPage || isSSOPath) return
|
||||
|
||||
const client = useApolloClientFromNuxt()
|
||||
const {
|
||||
activeWorkspaceSlug,
|
||||
isProjectsActive,
|
||||
mutateActiveWorkspaceSlug,
|
||||
mutateIsProjectsActive
|
||||
} = useNavigation()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
|
||||
const { data: workspaceExistenceData } = await client
|
||||
.query({
|
||||
query: activeUserWorkspaceExistenceCheckQuery
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
const { data: navigationCheckData } = await client
|
||||
.query({
|
||||
query: activeUserActiveWorkspaceCheckQuery
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
const workspaces =
|
||||
workspaceExistenceData?.activeUser?.workspaces?.items.filter(
|
||||
(w) => w.creationState?.completed !== false
|
||||
) ?? []
|
||||
const hasWorkspaces = workspaces.length > 0
|
||||
const activeUserActiveWorkspaceSlug =
|
||||
navigationCheckData?.activeUser?.activeWorkspace?.slug
|
||||
|
||||
if (isWorkspacesEnabled.value) {
|
||||
if (activeUserActiveWorkspaceSlug) {
|
||||
activeWorkspaceSlug.value = activeUserActiveWorkspaceSlug
|
||||
return navigateTo(workspaceRoute(activeUserActiveWorkspaceSlug))
|
||||
} else if (isProjectsActive.value) {
|
||||
return navigateTo(projectsRoute)
|
||||
} else if (hasWorkspaces) {
|
||||
mutateActiveWorkspaceSlug(workspaces[0].slug)
|
||||
return navigateTo(workspaceRoute(workspaces[0].slug))
|
||||
}
|
||||
}
|
||||
|
||||
mutateIsProjectsActive(true)
|
||||
return navigateTo(projectsRoute)
|
||||
})
|
||||
@@ -6,6 +6,6 @@
|
||||
useHead({ title: 'Dashboard' })
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth']
|
||||
middleware: ['auth', 'dashboard-redirect']
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -7,6 +7,11 @@
|
||||
:show-project-name="false"
|
||||
@processed="onInviteAccepted"
|
||||
/>
|
||||
<ProjectsMoveToWorkspaceAlert
|
||||
v-if="isWorkspacesEnabled && !project.workspace"
|
||||
:project-id="project.id"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex flex-col md:flex-row md:justify-between md:items-center gap-6 mt-2 mb-6"
|
||||
>
|
||||
@@ -14,9 +19,7 @@
|
||||
<div class="flex gap-x-3 items-center justify-between">
|
||||
<div class="flex flex-row gap-x-3">
|
||||
<CommonBadge v-if="project.role" rounded color="secondary">
|
||||
<span class="capitalize">
|
||||
{{ project.role?.split(':').reverse()[0] }}
|
||||
</span>
|
||||
{{ RoleInfo.Stream[project.role as StreamRoles].title }}
|
||||
</CommonBadge>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-3">
|
||||
@@ -69,7 +72,7 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import { Roles, type Optional } from '@speckle/shared'
|
||||
import { Roles, type Optional, RoleInfo, type StreamRoles } from '@speckle/shared'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { projectPageQuery } from '~~/lib/projects/graphql/queries'
|
||||
import { useGeneralProjectPageUpdateTracking } from '~~/lib/projects/composables/projectPages'
|
||||
@@ -105,6 +108,7 @@ graphql(`
|
||||
...ProjectPageProjectHeader
|
||||
...ProjectPageTeamDialog
|
||||
...ProjectsMoveToWorkspaceDialog_Project
|
||||
...ProjectPageSettingsTab_Project
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { LayoutTabsVertical, type LayoutPageTabItem } from '@speckle/ui-components'
|
||||
import { projectSettingsRoute, projectWebhooksRoute } from '~~/lib/common/helpers/route'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import type { ProjectPageProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import type { ProjectPageSettingsTab_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['can-view-settings']
|
||||
@@ -22,8 +22,9 @@ definePageMeta({
|
||||
graphql(`
|
||||
fragment ProjectPageSettingsTab_Project on Project {
|
||||
id
|
||||
name
|
||||
permissions {
|
||||
canUpdate {
|
||||
canReadWebhooks {
|
||||
...FullPermissionCheckResult
|
||||
}
|
||||
}
|
||||
@@ -31,12 +32,12 @@ graphql(`
|
||||
`)
|
||||
|
||||
const attrs = useAttrs() as {
|
||||
project: ProjectPageProjectFragment
|
||||
project: ProjectPageSettingsTab_ProjectFragment
|
||||
}
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const canUpdate = computed(() => attrs.project.permissions.canUpdate)
|
||||
const canReadWebhooks = computed(() => attrs.project.permissions.canReadWebhooks)
|
||||
const projectName = computed(() =>
|
||||
attrs.project.name.length ? attrs.project.name : ''
|
||||
)
|
||||
@@ -53,8 +54,8 @@ const settingsTabItems = computed((): LayoutPageTabItem[] => [
|
||||
{
|
||||
title: 'Webhooks',
|
||||
id: 'webhooks',
|
||||
disabled: !canUpdate.value.authorized,
|
||||
disabledMessage: canUpdate.value.message
|
||||
disabled: !canReadWebhooks.value.authorized,
|
||||
disabledMessage: canReadWebhooks.value.message
|
||||
}
|
||||
])
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ import {
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { Roles, WorkspacePlans } from '@speckle/shared'
|
||||
import { workspaceRoute } from '~/lib/common/helpers/route'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { WorkspacePlanStatuses } from '~/lib/common/generated/gql/graphql'
|
||||
@@ -223,7 +223,8 @@ const canDeleteWorkspace = computed(
|
||||
] as string[]
|
||||
).includes(
|
||||
workspaceResult.value?.workspaceBySlug?.plan?.status as WorkspacePlanStatuses
|
||||
))
|
||||
) ||
|
||||
workspaceResult.value?.workspaceBySlug?.plan?.name === WorkspacePlans.Free)
|
||||
)
|
||||
const deleteWorkspaceTooltip = computed(() => {
|
||||
if (needsSsoLogin.value)
|
||||
|
||||
@@ -75,18 +75,26 @@
|
||||
<div class="flex flex-col space-y-8">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1 flex-col pr-6 gap-y-1">
|
||||
<p class="text-body-xs font-medium text-foreground">Domain protection</p>
|
||||
<div class="flex items-center">
|
||||
<p class="text-body-xs font-medium text-foreground">
|
||||
Domain protection
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-body-2xs text-foreground-2 leading-5 max-w-md">
|
||||
Only users with email addresses from your verified domains can be added
|
||||
as workspace members or administrators.
|
||||
</p>
|
||||
</div>
|
||||
<FormSwitch
|
||||
v-model="isDomainProtectionEnabled"
|
||||
:show-label="false"
|
||||
:disabled="!hasWorkspaceDomains"
|
||||
name="domain-protection"
|
||||
/>
|
||||
<div key="tooltipText" v-tippy="switchDisabled ? tooltipText : undefined">
|
||||
<!-- Never disable switch when domain protection is enabled to
|
||||
allow expired workspaces ability to downgrade-->
|
||||
<FormSwitch
|
||||
v-model="isDomainProtectionEnabled"
|
||||
:show-label="false"
|
||||
:disabled="switchDisabled"
|
||||
name="domain-protection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1 flex-col pr-6 gap-y-1">
|
||||
@@ -138,6 +146,10 @@ graphql(`
|
||||
fragment SettingsWorkspacesSecurity_Workspace on Workspace {
|
||||
id
|
||||
slug
|
||||
plan {
|
||||
name
|
||||
status
|
||||
}
|
||||
domains {
|
||||
id
|
||||
domain
|
||||
@@ -146,6 +158,9 @@ graphql(`
|
||||
...SettingsWorkspacesSecuritySsoWrapper_Workspace
|
||||
domainBasedMembershipProtectionEnabled
|
||||
discoverabilityEnabled
|
||||
hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(
|
||||
featureName: domainBasedSecurityPolicies
|
||||
)
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -187,6 +202,10 @@ const workspace = computed(() => result.value?.workspaceBySlug)
|
||||
const workspaceDomains = computed(() => {
|
||||
return workspace.value?.domains || []
|
||||
})
|
||||
const hasAccessToDomainBasedSecurityPolicies = computed(
|
||||
() => workspace.value?.hasAccessToDomainBasedSecurityPolicies
|
||||
)
|
||||
|
||||
const hasWorkspaceDomains = computed(() => workspaceDomains.value.length > 0)
|
||||
const verifiedUserDomains = computed(() => {
|
||||
const workspaceDomainSet = new Set(workspaceDomains.value.map((item) => item.domain))
|
||||
@@ -242,6 +261,21 @@ const isDomainDiscoverabilityEnabled = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const switchDisabled = computed(() => {
|
||||
if (isDomainProtectionEnabled.value) return false
|
||||
if (!hasAccessToDomainBasedSecurityPolicies.value) return true
|
||||
if (!hasWorkspaceDomains.value) return true
|
||||
return false
|
||||
})
|
||||
|
||||
const tooltipText = computed(() => {
|
||||
if (isDomainProtectionEnabled.value) return undefined
|
||||
if (!hasAccessToDomainBasedSecurityPolicies.value) return 'Business plan required'
|
||||
if (!hasWorkspaceDomains.value)
|
||||
return 'Your workspace must have at least one verified domain'
|
||||
return undefined
|
||||
})
|
||||
|
||||
const addDomain = async () => {
|
||||
if (!selectedDomain.value || !workspace.value) return
|
||||
await addWorkspaceDomain.mutate(
|
||||
@@ -252,7 +286,8 @@ const addDomain = async () => {
|
||||
workspace.value.domains ?? [],
|
||||
workspace.value.discoverabilityEnabled,
|
||||
workspace.value.domainBasedMembershipProtectionEnabled,
|
||||
workspace.value.hasAccessToSSO
|
||||
workspace.value.hasAccessToSSO,
|
||||
workspace.value.hasAccessToDomainBasedSecurityPolicies
|
||||
)
|
||||
|
||||
mixpanel.track('Workspace Domain Added', {
|
||||
|
||||
@@ -26,6 +26,8 @@ const app = express()
|
||||
const host = HOST
|
||||
const port = PORT
|
||||
|
||||
const JobQueueName = 'preview-service-jobs'
|
||||
|
||||
let appState: AppState = AppState.STARTING
|
||||
|
||||
// serve the preview-frontend
|
||||
@@ -64,7 +66,7 @@ const opts = {
|
||||
}
|
||||
}
|
||||
}
|
||||
const jobQueue = new Bull('preview-service-jobs', opts)
|
||||
const jobQueue = new Bull(JobQueueName, opts)
|
||||
|
||||
// store this callback, so on shutdown we can error the job
|
||||
let currentJob: { logger: Logger; done: Bull.DoneCallback } | undefined = undefined
|
||||
@@ -81,7 +83,18 @@ const server = app.listen(port, host, async () => {
|
||||
const gpuArgs = ['--use-gl=angle', '--use-angle=gl-egl']
|
||||
|
||||
const launchBrowser = async (): Promise<Browser> => {
|
||||
logger.debug('Starting browser')
|
||||
const launchArguments = [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-session-crashed-bubble',
|
||||
...(GPU_ENABLED ? gpuArgs : [])
|
||||
]
|
||||
logger.debug(
|
||||
`Starting browser, located at "${CHROMIUM_EXECUTABLE_PATH}", with the following arguments: ${JSON.stringify(
|
||||
launchArguments
|
||||
)}`
|
||||
)
|
||||
return await puppeteer.launch({
|
||||
headless: !PREVIEWS_HEADED,
|
||||
executablePath: CHROMIUM_EXECUTABLE_PATH,
|
||||
@@ -89,13 +102,7 @@ const server = app.listen(port, host, async () => {
|
||||
// slowMo: 3000, // Use for debugging during development
|
||||
// we trust the web content that is running, so can disable the sandbox
|
||||
// disabling the sandbox allows us to run the docker image without linux kernel privileges
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-session-crashed-bubble',
|
||||
...(GPU_ENABLED ? gpuArgs : [])
|
||||
],
|
||||
args: launchArguments,
|
||||
protocolTimeout: PREVIEW_TIMEOUT,
|
||||
// handle closing of the browser by the preview-service, not puppeteer
|
||||
// this is important for the preview-service to be able to shut down gracefully,
|
||||
@@ -105,7 +112,7 @@ const server = app.listen(port, host, async () => {
|
||||
handleSIGTERM: false
|
||||
})
|
||||
}
|
||||
logger.debug('Starting message queues')
|
||||
logger.debug(`Starting processing of "${JobQueueName}" message queue`)
|
||||
|
||||
// nothing after this line is getting called, this blocks
|
||||
await jobQueue.process(async (payload, done) => {
|
||||
@@ -116,7 +123,7 @@ const server = app.listen(port, host, async () => {
|
||||
})
|
||||
|
||||
if (browser) {
|
||||
const message = 'Starting job but Browser is already open.'
|
||||
const message = 'Tried to start job but Browser is already open.'
|
||||
done(new Error(message))
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
type CommentPermissionChecks {
|
||||
canArchive: PermissionCheckResult!
|
||||
}
|
||||
|
||||
extend type Comment {
|
||||
permissions: CommentPermissionChecks!
|
||||
}
|
||||
|
||||
extend type ProjectPermissionChecks {
|
||||
canCreateComment: PermissionCheckResult!
|
||||
canBroadcastActivity: PermissionCheckResult!
|
||||
}
|
||||
@@ -4,7 +4,7 @@ extend type Project {
|
||||
|
||||
type ProjectPermissionChecks {
|
||||
canCreateModel: PermissionCheckResult!
|
||||
canMoveToWorkspace(workspaceId: String!): PermissionCheckResult!
|
||||
canMoveToWorkspace(workspaceId: String): PermissionCheckResult!
|
||||
canRead: PermissionCheckResult!
|
||||
canUpdate: PermissionCheckResult!
|
||||
canUpdateAllowPublicComments: PermissionCheckResult!
|
||||
@@ -20,3 +20,12 @@ type RootPermissionChecks {
|
||||
extend type User {
|
||||
permissions: RootPermissionChecks! @isOwner
|
||||
}
|
||||
|
||||
type ModelPermissionChecks {
|
||||
canUpdate: PermissionCheckResult!
|
||||
canDelete: PermissionCheckResult!
|
||||
}
|
||||
|
||||
extend type Model {
|
||||
permissions: ModelPermissionChecks!
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ extend type Workspace {
|
||||
|
||||
type WorkspacePermissionChecks {
|
||||
canCreateProject: PermissionCheckResult!
|
||||
canMoveProjectToWorkspace(projectId: String!): PermissionCheckResult!
|
||||
canMoveProjectToWorkspace(projectId: String): PermissionCheckResult!
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ generates:
|
||||
ServerStats: '@/modules/core/helpers/graphTypes#GraphQLEmptyReturn'
|
||||
CommentReplyAuthorCollection: '@/modules/comments/helpers/graphTypes#CommentReplyAuthorCollectionGraphQLReturn'
|
||||
Comment: '@/modules/comments/helpers/graphTypes#CommentGraphQLReturn'
|
||||
CommentPermissionChecks: '@/modules/comments/helpers/graphTypes#CommentPermissionChecksGraphQLReturn'
|
||||
PendingStreamCollaborator: '@/modules/serverinvites/helpers/graphTypes#PendingStreamCollaboratorGraphQLReturn'
|
||||
StreamCollaborator: '@/modules/core/helpers/graphTypes#StreamCollaboratorGraphQLReturn'
|
||||
ProjectCollaborator: '@/modules/core/helpers/graphTypes#ProjectCollaboratorGraphQLReturn'
|
||||
@@ -91,6 +92,7 @@ generates:
|
||||
Price: '@/modules/gatekeeperCore/helpers/graphTypes#PriceGraphQLReturn'
|
||||
WorkspaceSubscription: '@/modules/gatekeeper/helpers/graphTypes#WorkspaceSubscriptionGraphQLReturn'
|
||||
ProjectPermissionChecks: '@/modules/core/helpers/graphTypes#ProjectPermissionChecksGraphQLReturn'
|
||||
ModelPermissionChecks: '@/modules/core/helpers/graphTypes#ModelPermissionChecksGraphQLReturn'
|
||||
RootPermissionChecks: '@/modules/core/helpers/graphTypes#RootPermissionChecksGraphQLReturn'
|
||||
WorkspacePermissionChecks: '@/modules/workspacesCore/helpers/graphTypes#WorkspacePermissionChecksGraphQLReturn'
|
||||
modules/cross-server-sync/graph/generated/graphql.ts:
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
import { ServerScope } from '@speckle/shared'
|
||||
import { Merge } from 'type-fest'
|
||||
|
||||
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
|
||||
|
||||
const workspaceScopes = FF_WORKSPACES_MODULE_ENABLED ? [Scopes.Workspaces.Read] : []
|
||||
|
||||
export enum DefaultAppIds {
|
||||
Web = 'spklwebapp',
|
||||
Explorer = 'explorer',
|
||||
@@ -56,7 +60,8 @@ const SpeckleDesktopApp = {
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Invite
|
||||
Scopes.Users.Invite,
|
||||
...workspaceScopes
|
||||
]
|
||||
}
|
||||
|
||||
@@ -74,7 +79,8 @@ const SpeckleConnectorApp = {
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Invite
|
||||
Scopes.Users.Invite,
|
||||
...workspaceScopes
|
||||
]
|
||||
}
|
||||
|
||||
@@ -94,7 +100,8 @@ const SpeckleDesktopAuthService = {
|
||||
Scopes.Profile.Read,
|
||||
Scopes.Profile.Email,
|
||||
Scopes.Users.Read,
|
||||
Scopes.Users.Invite
|
||||
Scopes.Users.Invite,
|
||||
...workspaceScopes
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { defineModuleLoaders } from '@/modules/loaders'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
|
||||
export default defineModuleLoaders(async () => {
|
||||
return {
|
||||
getComment: async ({ commentId, projectId }, { dataLoaders }) => {
|
||||
const db = await getProjectDbClient({ projectId })
|
||||
const comment = await dataLoaders
|
||||
.forRegion({ db })
|
||||
.comments.getComment.load(commentId)
|
||||
if (!comment) return null
|
||||
|
||||
return {
|
||||
...comment,
|
||||
projectId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -57,6 +57,8 @@ export type GetComment = (params: {
|
||||
userId?: string
|
||||
}) => Promise<Optional<ExtendedComment>>
|
||||
|
||||
export type GetComments = (params: { ids: string[] }) => Promise<CommentRecord[]>
|
||||
|
||||
export type CheckStreamResourceAccess = (
|
||||
res: ResourceIdentifier,
|
||||
streamId: string
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import { keyBy } from 'lodash'
|
||||
import { Nullable } from '@/modules/shared/helpers/typeHelper'
|
||||
import { ResourceIdentifier } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
getCommentParentsFactory,
|
||||
getCommentReplyAuthorIdsFactory,
|
||||
getCommentReplyCountsFactory,
|
||||
getCommentsFactory,
|
||||
getCommentsResourcesFactory,
|
||||
getCommentsViewedAtFactory
|
||||
} from '@/modules/comments/repositories/comments'
|
||||
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
|
||||
declare module '@/modules/core/loaders' {
|
||||
interface ModularizedDataLoaders extends ReturnType<typeof dataLoadersDefinition> {}
|
||||
}
|
||||
|
||||
const dataLoadersDefinition = defineRequestDataloaders(
|
||||
({ ctx, createLoader, deps: { db } }) => {
|
||||
const userId = ctx.userId
|
||||
|
||||
const getCommentsResources = getCommentsResourcesFactory({ db })
|
||||
const getCommentsViewedAt = getCommentsViewedAtFactory({ db })
|
||||
const getCommentReplyCounts = getCommentReplyCountsFactory({ db })
|
||||
const getCommentReplyAuthorIds = getCommentReplyAuthorIdsFactory({ db })
|
||||
const getCommentParents = getCommentParentsFactory({ db })
|
||||
const getComments = getCommentsFactory({ db })
|
||||
|
||||
return {
|
||||
comments: {
|
||||
getComment: createLoader<string, Nullable<CommentRecord>>(
|
||||
async (commentIds) => {
|
||||
const results = keyBy(await getComments({ ids: commentIds.slice() }), 'id')
|
||||
return commentIds.map((id) => results[id] || null)
|
||||
}
|
||||
),
|
||||
getViewedAt: createLoader<string, Nullable<Date>>(async (commentIds) => {
|
||||
if (!userId) return commentIds.slice().map(() => null)
|
||||
|
||||
const results = keyBy(
|
||||
await getCommentsViewedAt(commentIds.slice(), userId),
|
||||
'commentId'
|
||||
)
|
||||
return commentIds.map((id) => results[id]?.viewedAt || null)
|
||||
}),
|
||||
getResources: createLoader<string, ResourceIdentifier[]>(async (commentIds) => {
|
||||
const results = await getCommentsResources(commentIds.slice())
|
||||
return commentIds.map((id) => results[id]?.resources || [])
|
||||
}),
|
||||
getReplyCount: createLoader<string, number>(async (threadIds) => {
|
||||
const results = keyBy(
|
||||
await getCommentReplyCounts(threadIds.slice()),
|
||||
'threadId'
|
||||
)
|
||||
return threadIds.map((id) => results[id]?.count || 0)
|
||||
}),
|
||||
getReplyAuthorIds: createLoader<string, string[]>(async (threadIds) => {
|
||||
const results = await getCommentReplyAuthorIds(threadIds.slice())
|
||||
return threadIds.map((id) => results[id] || [])
|
||||
}),
|
||||
getReplyParent: createLoader<string, Nullable<CommentRecord>>(
|
||||
async (replyIds) => {
|
||||
const results = keyBy(await getCommentParents(replyIds.slice()), 'replyId')
|
||||
return replyIds.map((id) => results[id] || null)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export default dataLoadersDefinition
|
||||
@@ -58,8 +58,6 @@ import {
|
||||
getViewerResourceGroupsFactory
|
||||
} from '@/modules/core/services/commit/viewerResources'
|
||||
import {
|
||||
authorizeProjectCommentsAccessFactory,
|
||||
authorizeCommentAccessFactory,
|
||||
createCommentThreadAndNotifyFactory,
|
||||
createCommentReplyAndNotifyFactory,
|
||||
editCommentAndNotifyFactory,
|
||||
@@ -83,7 +81,6 @@ import {
|
||||
getCommitsAndTheirBranchIdsFactory,
|
||||
getSpecificBranchCommitsFactory
|
||||
} from '@/modules/core/repositories/commits'
|
||||
import { adminOverrideEnabled } from '@/modules/shared/helpers/envHelper'
|
||||
import {
|
||||
getBranchLatestCommitsFactory,
|
||||
getStreamBranchesByNameFactory
|
||||
@@ -93,20 +90,10 @@ import { getStreamFactory } from '@/modules/core/repositories/streams'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { Knex } from 'knex'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
|
||||
// We can use the main DB for these
|
||||
const getStream = getStreamFactory({ db })
|
||||
const authorizeProjectCommentsAccess = authorizeProjectCommentsAccessFactory({
|
||||
getStream,
|
||||
adminOverrideEnabled
|
||||
})
|
||||
|
||||
const buildAuthorizeCommentAccess = (deps: { db: Knex; mainDb: Knex }) =>
|
||||
authorizeCommentAccessFactory({
|
||||
getStream: getStreamFactory({ db: deps.mainDb }),
|
||||
adminOverrideEnabled,
|
||||
getComment: getCommentFactory(deps)
|
||||
})
|
||||
|
||||
const buildGetViewerResourcesFromLegacyIdentifiers = (deps: { db: Knex }) => {
|
||||
const getViewerResourcesFromLegacyIdentifiers =
|
||||
@@ -133,20 +120,17 @@ const buildGetViewerResourceItemsUngrouped = (deps: { db: Knex }) =>
|
||||
})
|
||||
})
|
||||
|
||||
const getStreamCommentFactory =
|
||||
const getAuthorizedStreamCommentFactory =
|
||||
(deps: { db: Knex; mainDb: Knex }) =>
|
||||
async (
|
||||
{ streamId, commentId }: { streamId: string; commentId: string },
|
||||
ctx: GraphQLContext
|
||||
) => {
|
||||
const authorizeProjectCommentsAccess = authorizeProjectCommentsAccessFactory({
|
||||
getStream: getStreamFactory(deps),
|
||||
adminOverrideEnabled
|
||||
})
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: streamId,
|
||||
authCtx: ctx
|
||||
const canReadProject = await ctx.authPolicies.project.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId: streamId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const getComment = getCommentFactory(deps)
|
||||
const comment = await getComment({ id: commentId, userId: ctx.userId })
|
||||
@@ -161,7 +145,10 @@ export = {
|
||||
async comment(_parent, args, context) {
|
||||
const projectId = args.streamId
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getStreamComment = getStreamCommentFactory({ db: projectDb, mainDb })
|
||||
const getStreamComment = getAuthorizedStreamCommentFactory({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
|
||||
return await getStreamComment(
|
||||
{ streamId: args.streamId, commentId: args.id },
|
||||
@@ -171,12 +158,13 @@ export = {
|
||||
|
||||
async comments(_parent, args, context) {
|
||||
const projectId = args.streamId
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: args.streamId,
|
||||
authCtx: context
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getComments = getCommentsLegacyFactory({ db: projectDb })
|
||||
return {
|
||||
...(await getComments({
|
||||
@@ -356,12 +344,7 @@ export = {
|
||||
}
|
||||
},
|
||||
Project: {
|
||||
async commentThreads(parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: parent.id,
|
||||
authCtx: context
|
||||
})
|
||||
|
||||
async commentThreads(parent, args) {
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
||||
const getPaginatedProjectComments = getPaginatedProjectCommentsFactory({
|
||||
resolvePaginatedProjectCommentsLatestModelResources:
|
||||
@@ -388,7 +371,10 @@ export = {
|
||||
async comment(parent, args, context) {
|
||||
const projectId = parent.id
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getStreamComment = getStreamCommentFactory({ db: projectDb, mainDb })
|
||||
const getStreamComment = getAuthorizedStreamCommentFactory({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
return await getStreamComment(
|
||||
{ streamId: parent.id, commentId: args.id },
|
||||
context
|
||||
@@ -396,13 +382,8 @@ export = {
|
||||
}
|
||||
},
|
||||
Version: {
|
||||
async commentThreads(parent, args, context) {
|
||||
async commentThreads(parent, args) {
|
||||
const projectId = parent.streamId
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId,
|
||||
authCtx: context
|
||||
})
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getPaginatedCommitComments = getPaginatedCommitCommentsFactory({
|
||||
getPaginatedCommitCommentsPage: getPaginatedCommitCommentsPageFactory({
|
||||
@@ -425,12 +406,8 @@ export = {
|
||||
}
|
||||
},
|
||||
Model: {
|
||||
async commentThreads(parent, args, context) {
|
||||
async commentThreads(parent, args) {
|
||||
const projectId = parent.streamId
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId,
|
||||
authCtx: context
|
||||
})
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
|
||||
const getPaginatedBranchComments = getPaginatedBranchCommentsFactory({
|
||||
@@ -498,16 +475,13 @@ export = {
|
||||
},
|
||||
CommentMutations: {
|
||||
async markViewed(_parent, args, ctx) {
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const authorizeCommentAccess = buildAuthorizeCommentAccess({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
await authorizeCommentAccess({
|
||||
authCtx: ctx,
|
||||
commentId: args.input.commentId
|
||||
const canReadProject = await ctx.authPolicies.project.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const markCommentViewed = markCommentViewedFactory({ db: projectDb })
|
||||
await markCommentViewed(args.input.commentId, ctx.userId!)
|
||||
|
||||
@@ -515,12 +489,11 @@ export = {
|
||||
},
|
||||
async create(_parent, args, ctx) {
|
||||
const projectId = args.input.projectId
|
||||
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId,
|
||||
authCtx: ctx,
|
||||
requireProjectRole: true
|
||||
const canCreate = await ctx.authPolicies.project.comment.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canCreate)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
|
||||
@@ -547,17 +520,13 @@ export = {
|
||||
return await createCommentThreadAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async reply(_parent, args, ctx) {
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const authorizeCommentAccess = buildAuthorizeCommentAccess({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
await authorizeCommentAccess({
|
||||
commentId: args.input.threadId,
|
||||
authCtx: ctx,
|
||||
requireProjectRole: true
|
||||
const canCreateComment = await ctx.authPolicies.project.comment.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canCreateComment)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const getComment = getCommentFactory({ db: projectDb })
|
||||
const validateInputAttachments = validateInputAttachmentsFactory({
|
||||
getBlobs: getBlobsFactory({ db: projectDb })
|
||||
@@ -583,19 +552,16 @@ export = {
|
||||
return await createCommentReplyAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async edit(_parent, args, ctx) {
|
||||
const canEditComment = await ctx.authPolicies.project.comment.canEdit({
|
||||
projectId: args.input.projectId,
|
||||
userId: ctx.userId,
|
||||
commentId: args.input.commentId
|
||||
})
|
||||
throwIfAuthNotOk(canEditComment)
|
||||
|
||||
const projectDb = await getProjectDbClient({
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
const authorizeCommentAccess = buildAuthorizeCommentAccess({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
await authorizeCommentAccess({
|
||||
authCtx: ctx,
|
||||
commentId: args.input.commentId,
|
||||
requireProjectRole: true
|
||||
})
|
||||
|
||||
const getComment = getCommentFactory({ db: projectDb })
|
||||
const validateInputAttachments = validateInputAttachmentsFactory({
|
||||
getBlobs: getBlobsFactory({ db: projectDb })
|
||||
@@ -612,19 +578,16 @@ export = {
|
||||
return await editCommentAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async archive(_parent, args, ctx) {
|
||||
const canArchive = await ctx.authPolicies.project.comment.canArchive({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId,
|
||||
commentId: args.input.commentId
|
||||
})
|
||||
throwIfAuthNotOk(canArchive)
|
||||
|
||||
const projectDb = await getProjectDbClient({
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
const authorizeCommentAccess = buildAuthorizeCommentAccess({
|
||||
db: projectDb,
|
||||
mainDb
|
||||
})
|
||||
await authorizeCommentAccess({
|
||||
authCtx: ctx,
|
||||
commentId: args.input.commentId,
|
||||
requireProjectRole: true
|
||||
})
|
||||
|
||||
const getComment = getCommentFactory({ db: projectDb })
|
||||
const getStream = getStreamFactory({ db: projectDb })
|
||||
const updateComment = updateCommentFactory({ db: projectDb })
|
||||
@@ -654,10 +617,12 @@ export = {
|
||||
commentMutations: () => ({}),
|
||||
async broadcastViewerUserActivity(_parent, args, context) {
|
||||
const projectId = args.projectId
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId,
|
||||
authCtx: context
|
||||
})
|
||||
const canBroadcastActivity =
|
||||
await context.authPolicies.project.canBroadcastActivity({
|
||||
projectId,
|
||||
userId: context.userId
|
||||
})
|
||||
throwIfAuthNotOk(canBroadcastActivity)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({
|
||||
@@ -674,10 +639,13 @@ export = {
|
||||
},
|
||||
|
||||
async userViewerActivityBroadcast(_parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: args.streamId,
|
||||
authCtx: context
|
||||
})
|
||||
const projectId = args.streamId
|
||||
const canBroadcastActivity =
|
||||
await context.authPolicies.project.canBroadcastActivity({
|
||||
projectId,
|
||||
userId: context.userId
|
||||
})
|
||||
throwIfAuthNotOk(canBroadcastActivity)
|
||||
|
||||
await pubsub.publish(CommentSubscriptions.ViewerActivity, {
|
||||
userViewerActivity: args.data,
|
||||
@@ -707,16 +675,11 @@ export = {
|
||||
},
|
||||
|
||||
async commentCreate(_parent, args, context) {
|
||||
if (!context.userId)
|
||||
throw new ForbiddenError('Only registered users can comment.')
|
||||
|
||||
const stream = await getStream({
|
||||
streamId: args.input.streamId,
|
||||
userId: context.userId
|
||||
const canCreate = await context.authPolicies.project.comment.canCreate({
|
||||
userId: context.userId,
|
||||
projectId: args.input.streamId
|
||||
})
|
||||
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
throwIfAuthNotOk(canCreate)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.streamId })
|
||||
const getViewerResourcesFromLegacyIdentifiers =
|
||||
@@ -737,7 +700,7 @@ export = {
|
||||
getViewerResourcesFromLegacyIdentifiers
|
||||
})
|
||||
const comment = await createComment({
|
||||
userId: context.userId,
|
||||
userId: context.userId!,
|
||||
input: args.input
|
||||
})
|
||||
|
||||
@@ -745,13 +708,12 @@ export = {
|
||||
},
|
||||
|
||||
async commentEdit(_parent, args, context) {
|
||||
// NOTE: This is NOT in use anywhere
|
||||
const stream = await authorizeProjectCommentsAccess({
|
||||
const canEdit = await context.authPolicies.project.comment.canEdit({
|
||||
userId: context.userId,
|
||||
projectId: args.input.streamId,
|
||||
authCtx: context,
|
||||
requireProjectRole: true
|
||||
commentId: args.input.id
|
||||
})
|
||||
const matchUser = !stream.role
|
||||
throwIfAuthNotOk(canEdit)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.input.streamId })
|
||||
const editComment = editCommentFactory({
|
||||
@@ -763,16 +725,17 @@ export = {
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
|
||||
await editComment({ userId: context.userId!, input: args.input, matchUser })
|
||||
await editComment({ userId: context.userId!, input: args.input })
|
||||
return true
|
||||
},
|
||||
|
||||
// used for flagging a comment as viewed
|
||||
async commentView(_parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
projectId: args.streamId,
|
||||
authCtx: context
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: args.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.streamId })
|
||||
const markCommentViewed = markCommentViewedFactory({ db: projectDb })
|
||||
@@ -782,11 +745,12 @@ export = {
|
||||
},
|
||||
|
||||
async commentArchive(_parent, args, context) {
|
||||
await authorizeProjectCommentsAccess({
|
||||
const canArchive = await context.authPolicies.project.comment.canArchive({
|
||||
userId: context.userId,
|
||||
projectId: args.streamId,
|
||||
authCtx: context,
|
||||
requireProjectRole: true
|
||||
commentId: args.commentId
|
||||
})
|
||||
throwIfAuthNotOk(canArchive)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: args.streamId })
|
||||
const archiveComment = archiveCommentFactory({
|
||||
@@ -849,15 +813,13 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.ViewerActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
|
||||
// dont report users activity to himself
|
||||
// dont report user's activity to themselves
|
||||
if (context.userId && context.userId === payload.authorId) {
|
||||
return false
|
||||
}
|
||||
@@ -873,13 +835,11 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.CommentActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
// if we're listening for a stream's root comments events
|
||||
if (!variables.resourceIds) {
|
||||
@@ -929,13 +889,11 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
CommentSubscriptions.CommentThreadActivity,
|
||||
async (payload, variables, context) => {
|
||||
const stream = await getStream({
|
||||
streamId: payload.streamId,
|
||||
userId: context.userId
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
|
||||
if (!stream?.allowPublicComments && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
return (
|
||||
payload.streamId === variables.streamId &&
|
||||
@@ -955,21 +913,17 @@ export = {
|
||||
if (!target.resourceIdString.trim().length) return false
|
||||
if (payload.projectId !== target.projectId) return false
|
||||
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: payload.projectId })
|
||||
const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({
|
||||
db: projectDb
|
||||
})
|
||||
|
||||
const [stream, requestedResourceItems] = await Promise.all([
|
||||
getStream({
|
||||
streamId: payload.projectId,
|
||||
userId: context.userId
|
||||
}),
|
||||
getViewerResourceItemsUngrouped(target)
|
||||
])
|
||||
|
||||
if (!stream?.isPublic && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
const requestedResourceItems = await getViewerResourceItemsUngrouped(target)
|
||||
|
||||
// dont report users activity to himself
|
||||
if (
|
||||
@@ -995,21 +949,18 @@ export = {
|
||||
const target = variables.target
|
||||
if (payload.projectId !== target.projectId) return false
|
||||
|
||||
const canReadProject = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: payload.projectId })
|
||||
const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({
|
||||
db: projectDb
|
||||
})
|
||||
|
||||
const [stream, requestedResourceItems] = await Promise.all([
|
||||
getStream({
|
||||
streamId: payload.projectId,
|
||||
userId: context.userId
|
||||
}),
|
||||
getViewerResourceItemsUngrouped(target)
|
||||
])
|
||||
|
||||
if (!(stream?.isDiscoverable || stream?.isPublic) && !stream?.role)
|
||||
throw new ForbiddenError('You are not authorized.')
|
||||
const requestedResourceItems = await getViewerResourceItemsUngrouped(target)
|
||||
|
||||
if (!target.resourceIdString) {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { Authz } from '@speckle/shared'
|
||||
|
||||
export default {
|
||||
Comment: {
|
||||
permissions: async (parent) => ({
|
||||
commentId: parent.id,
|
||||
projectId: parent.streamId
|
||||
})
|
||||
},
|
||||
CommentPermissionChecks: {
|
||||
canArchive: async (parent, _args, ctx) => {
|
||||
const canArchive = await ctx.authPolicies.project.comment.canArchive({
|
||||
...parent,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canArchive)
|
||||
}
|
||||
},
|
||||
ProjectPermissionChecks: {
|
||||
canCreateComment: async (parent, _args, ctx) => {
|
||||
const canCreateComment = await ctx.authPolicies.project.comment.canCreate({
|
||||
...parent,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canCreateComment)
|
||||
},
|
||||
canBroadcastActivity: async (parent, _args, ctx) => {
|
||||
const canBroadcastActivity = await ctx.authPolicies.project.canBroadcastActivity({
|
||||
...parent,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canBroadcastActivity)
|
||||
}
|
||||
}
|
||||
} as Resolvers
|
||||
@@ -14,3 +14,8 @@ export type CommentReplyAuthorCollectionGraphQLReturn = {
|
||||
}
|
||||
|
||||
export type CommentGraphQLReturn = CommentRecord
|
||||
|
||||
export type CommentPermissionChecksGraphQLReturn = {
|
||||
commentId: string
|
||||
projectId: string
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
GetCommentParents,
|
||||
GetCommentReplyAuthorIds,
|
||||
GetCommentReplyCounts,
|
||||
GetComments,
|
||||
GetCommentsResources,
|
||||
GetCommitCommentCounts,
|
||||
GetPaginatedBranchCommentsPage,
|
||||
@@ -106,6 +107,18 @@ export const getCommentFactory =
|
||||
return await query
|
||||
}
|
||||
|
||||
export const getCommentsFactory =
|
||||
(deps: { db: Knex }): GetComments =>
|
||||
async (params) => {
|
||||
const { ids } = params
|
||||
if (!ids.length) return []
|
||||
|
||||
const query = tables.comments(deps.db).select<CommentRecord[]>('*')
|
||||
query.whereIn(Comments.col.id, ids)
|
||||
|
||||
return await query
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resources array for the specified comments. Results object is keyed by comment ID.
|
||||
*/
|
||||
|
||||
@@ -247,7 +247,7 @@ export const editCommentFactory =
|
||||
}: {
|
||||
userId: string
|
||||
input: CommentEditInput
|
||||
matchUser: boolean
|
||||
matchUser?: boolean
|
||||
}) => {
|
||||
const editedComment = await deps.getComment({ id: input.id })
|
||||
if (!editedComment) throw new CommentNotFoundError("The comment doesn't exist")
|
||||
|
||||
@@ -90,33 +90,6 @@ export const authorizeProjectCommentsAccessFactory =
|
||||
return project
|
||||
}
|
||||
|
||||
export const authorizeCommentAccessFactory =
|
||||
(
|
||||
deps: {
|
||||
getComment: GetComment
|
||||
} & AuthorizeProjectCommentsAccessDeps
|
||||
) =>
|
||||
async (params: {
|
||||
authCtx: AuthContext
|
||||
commentId: string
|
||||
requireProjectRole?: boolean
|
||||
}) => {
|
||||
const { authCtx, commentId, requireProjectRole } = params
|
||||
const comment = await deps.getComment({
|
||||
id: commentId,
|
||||
userId: authCtx.userId
|
||||
})
|
||||
if (!comment) {
|
||||
throw new StreamInvalidAccessError('Attempting to access a nonexistant comment')
|
||||
}
|
||||
|
||||
return authorizeProjectCommentsAccessFactory(deps)({
|
||||
projectId: comment.streamId,
|
||||
authCtx,
|
||||
requireProjectRole
|
||||
})
|
||||
}
|
||||
|
||||
export const createCommentThreadAndNotifyFactory =
|
||||
(deps: {
|
||||
getViewerResourceItemsUngrouped: GetViewerResourceItemsUngrouped
|
||||
@@ -311,10 +284,7 @@ export const archiveCommentAndNotifyFactory =
|
||||
}
|
||||
|
||||
const stream = await deps.getStream({ streamId: comment.streamId, userId })
|
||||
if (
|
||||
!stream ||
|
||||
(comment.authorId !== userId && stream.role !== Roles.Stream.Owner)
|
||||
) {
|
||||
if (!stream) {
|
||||
throw new CommentUpdateError(
|
||||
'You do not have permissions to archive this comment'
|
||||
)
|
||||
|
||||
@@ -855,7 +855,7 @@ describe('Graphql @comments', () => {
|
||||
[archiveMyComment, true],
|
||||
[archiveOthersComment, true],
|
||||
[editMyComment, true],
|
||||
[editOthersComment, true],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, true],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
@@ -875,7 +875,7 @@ describe('Graphql @comments', () => {
|
||||
[archiveMyComment, true],
|
||||
[archiveOthersComment, false],
|
||||
[editMyComment, true],
|
||||
[editOthersComment, true],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, true],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
@@ -895,7 +895,7 @@ describe('Graphql @comments', () => {
|
||||
[archiveMyComment, true],
|
||||
[archiveOthersComment, false],
|
||||
[editMyComment, true],
|
||||
[editOthersComment, true],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, true],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
@@ -978,8 +978,8 @@ describe('Graphql @comments', () => {
|
||||
[archiveOthersComment, false],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, false],
|
||||
[queryComment, false],
|
||||
[queryComments, false],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
[queryStreamCommentCount, false],
|
||||
[queryObjectCommentCount, false],
|
||||
[queryCommitCommentCount, false],
|
||||
@@ -996,8 +996,8 @@ describe('Graphql @comments', () => {
|
||||
[archiveOthersComment, false],
|
||||
[editOthersComment, false],
|
||||
[replyToAComment, false],
|
||||
[queryComment, false],
|
||||
[queryComments, false],
|
||||
[queryComment, true],
|
||||
[queryComments, true],
|
||||
[queryStreamCommentCount, false],
|
||||
[queryObjectCommentCount, false],
|
||||
[queryCommitCommentCount, false],
|
||||
@@ -1164,13 +1164,13 @@ describe('Graphql @comments', () => {
|
||||
}
|
||||
})
|
||||
|
||||
describe(`testing ${streamContext.cases.length} cases of acting on ${
|
||||
describe(`testing ${streamContext.cases.length} cases of acting on "${
|
||||
stream.name
|
||||
} stream where I'm a ${
|
||||
user && stream.role ? stream.role : 'trouble:maker'
|
||||
}" stream where I ${
|
||||
user && stream.role ? 'have the role ' + stream.role : 'have no role'
|
||||
}`, () => {
|
||||
streamContext.cases.forEach(([testCase, shouldSucceed]) => {
|
||||
it(`${shouldSucceed ? 'can' : 'am not allowed to'} ${
|
||||
it(`${shouldSucceed ? 'should' : 'should not be allowed to'} ${
|
||||
testCase.name
|
||||
}`, async () => {
|
||||
await testCase({ apollo, streamId: stream.id, resources, shouldSucceed })
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getFeatureFlags
|
||||
} from '@/modules/shared/helpers/envHelper'
|
||||
import { db } from '@/db/knex'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
|
||||
// TODO: Move everything to use dataLoaders
|
||||
export default defineModuleLoaders(async () => {
|
||||
@@ -26,6 +27,16 @@ export default defineModuleLoaders(async () => {
|
||||
getProjectRoleCounts: async ({ projectId, role }, { dataLoaders }) => {
|
||||
const counts = await dataLoaders.streams.getCollaboratorCounts.load(projectId)
|
||||
return counts?.[role] || 0
|
||||
},
|
||||
getModel: async ({ projectId, modelId }, { dataLoaders }) => {
|
||||
const db = await getProjectDbClient({ projectId })
|
||||
const model = await dataLoaders.forRegion({ db }).branches.getById.load(modelId)
|
||||
if (!model) return null
|
||||
|
||||
return {
|
||||
...model,
|
||||
projectId: model.streamId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -33,14 +33,9 @@ import {
|
||||
getUserAuthoredCommitCountsFactory,
|
||||
getUserStreamCommitCountsFactory
|
||||
} from '@/modules/core/repositories/commits'
|
||||
import { ResourceIdentifier, Scope } from '@/modules/core/graph/generated/graphql'
|
||||
import { Scope } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
getBranchCommentCountsFactory,
|
||||
getCommentParentsFactory,
|
||||
getCommentReplyAuthorIdsFactory,
|
||||
getCommentReplyCountsFactory,
|
||||
getCommentsResourcesFactory,
|
||||
getCommentsViewedAtFactory,
|
||||
getCommitCommentCountsFactory,
|
||||
getStreamCommentCountsFactory
|
||||
} from '@/modules/comments/repositories/comments'
|
||||
@@ -51,7 +46,6 @@ import {
|
||||
getStreamBranchCountsFactory,
|
||||
getStreamBranchesByNameFactory
|
||||
} from '@/modules/core/repositories/branches'
|
||||
import { CommentRecord } from '@/modules/comments/helpers/types'
|
||||
import { metaHelpers } from '@/modules/core/helpers/meta'
|
||||
import { Users } from '@/modules/core/dbSchema'
|
||||
import { getStreamPendingModelsFactory } from '@/modules/fileuploads/repositories/fileUploads'
|
||||
@@ -119,13 +113,8 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
const getRevisionsFunctions = getRevisionsFunctionsFactory({ db })
|
||||
const getStreamCommentCounts = getStreamCommentCountsFactory({ db })
|
||||
const getAutomationRunsTriggers = getAutomationRunsTriggersFactory({ db })
|
||||
const getCommentsResources = getCommentsResourcesFactory({ db })
|
||||
const getCommentsViewedAt = getCommentsViewedAtFactory({ db })
|
||||
const getCommitCommentCounts = getCommitCommentCountsFactory({ db })
|
||||
const getBranchCommentCounts = getBranchCommentCountsFactory({ db })
|
||||
const getCommentReplyCounts = getCommentReplyCountsFactory({ db })
|
||||
const getCommentReplyAuthorIds = getCommentReplyAuthorIdsFactory({ db })
|
||||
const getCommentParents = getCommentParentsFactory({ db })
|
||||
const getBranchesByIds = getBranchesByIdsFactory({ db })
|
||||
const getStreamBranchesByName = getStreamBranchesByNameFactory({ db })
|
||||
const getBranchLatestCommits = getBranchLatestCommitsFactory({ db })
|
||||
@@ -469,38 +458,6 @@ const dataLoadersDefinition = defineRequestDataloaders(
|
||||
return commitIds.map((i) => results[i] || null)
|
||||
})
|
||||
},
|
||||
comments: {
|
||||
getViewedAt: createLoader<string, Nullable<Date>>(async (commentIds) => {
|
||||
if (!userId) return commentIds.slice().map(() => null)
|
||||
|
||||
const results = keyBy(
|
||||
await getCommentsViewedAt(commentIds.slice(), userId),
|
||||
'commentId'
|
||||
)
|
||||
return commentIds.map((id) => results[id]?.viewedAt || null)
|
||||
}),
|
||||
getResources: createLoader<string, ResourceIdentifier[]>(async (commentIds) => {
|
||||
const results = await getCommentsResources(commentIds.slice())
|
||||
return commentIds.map((id) => results[id]?.resources || [])
|
||||
}),
|
||||
getReplyCount: createLoader<string, number>(async (threadIds) => {
|
||||
const results = keyBy(
|
||||
await getCommentReplyCounts(threadIds.slice()),
|
||||
'threadId'
|
||||
)
|
||||
return threadIds.map((id) => results[id]?.count || 0)
|
||||
}),
|
||||
getReplyAuthorIds: createLoader<string, string[]>(async (threadIds) => {
|
||||
const results = await getCommentReplyAuthorIds(threadIds.slice())
|
||||
return threadIds.map((id) => results[id] || [])
|
||||
}),
|
||||
getReplyParent: createLoader<string, Nullable<CommentRecord>>(
|
||||
async (replyIds) => {
|
||||
const results = keyBy(await getCommentParents(replyIds.slice()), 'replyId')
|
||||
return replyIds.map((id) => results[id] || null)
|
||||
}
|
||||
)
|
||||
},
|
||||
users: {
|
||||
/**
|
||||
* Get user from DB
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
|
||||
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn, ProjectPermissionChecksGraphQLReturn, RootPermissionChecksGraphQLReturn } from '@/modules/core/helpers/graphTypes';
|
||||
import { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn, ProjectPermissionChecksGraphQLReturn, ModelPermissionChecksGraphQLReturn, RootPermissionChecksGraphQLReturn } from '@/modules/core/helpers/graphTypes';
|
||||
import { StreamAccessRequestGraphQLReturn, ProjectAccessRequestGraphQLReturn } from '@/modules/accessrequests/helpers/graphTypes';
|
||||
import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
|
||||
import { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn, CommentPermissionChecksGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
|
||||
import { PendingStreamCollaboratorGraphQLReturn } from '@/modules/serverinvites/helpers/graphTypes';
|
||||
import { FileUploadGraphQLReturn } from '@/modules/fileuploads/helpers/types';
|
||||
import { AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
|
||||
@@ -631,6 +631,7 @@ export type Comment = {
|
||||
id: Scalars['String']['output'];
|
||||
/** Parent thread, if there's any */
|
||||
parent?: Maybe<Comment>;
|
||||
permissions: CommentPermissionChecks;
|
||||
/** Plain-text version of the comment text, ideal for previews */
|
||||
rawText: Scalars['String']['output'];
|
||||
/** @deprecated Not actually implemented */
|
||||
@@ -764,6 +765,11 @@ export type CommentMutationsReplyArgs = {
|
||||
input: CreateCommentReplyInput;
|
||||
};
|
||||
|
||||
export type CommentPermissionChecks = {
|
||||
__typename?: 'CommentPermissionChecks';
|
||||
canArchive: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type CommentReplyAuthorCollection = {
|
||||
__typename?: 'CommentReplyAuthorCollection';
|
||||
items: Array<LimitedUser>;
|
||||
@@ -1268,6 +1274,7 @@ export type Model = {
|
||||
name: Scalars['String']['output'];
|
||||
/** Returns a list of versions that are being created from a file import */
|
||||
pendingImportedVersions: Array<FileUpload>;
|
||||
permissions: ModelPermissionChecks;
|
||||
previewUrl?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
version: Version;
|
||||
@@ -1326,6 +1333,12 @@ export type ModelMutationsUpdateArgs = {
|
||||
input: UpdateModelInput;
|
||||
};
|
||||
|
||||
export type ModelPermissionChecks = {
|
||||
__typename?: 'ModelPermissionChecks';
|
||||
canDelete: PermissionCheckResult;
|
||||
canUpdate: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type ModelVersionsFilter = {
|
||||
/** Make sure these specified versions are always loaded first */
|
||||
priorityIds?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
@@ -2560,6 +2573,8 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
@@ -2572,7 +2587,7 @@ export type ProjectPermissionChecks = {
|
||||
|
||||
|
||||
export type ProjectPermissionChecksCanMoveToWorkspaceArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectRole = {
|
||||
@@ -4781,7 +4796,7 @@ export type WorkspacePermissionChecks = {
|
||||
|
||||
|
||||
export type WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
projectId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
@@ -5189,6 +5204,7 @@ export type ResolversTypes = {
|
||||
CommentDataFiltersInput: CommentDataFiltersInput;
|
||||
CommentEditInput: CommentEditInput;
|
||||
CommentMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
|
||||
CommentPermissionChecks: ResolverTypeWrapper<CommentPermissionChecksGraphQLReturn>;
|
||||
CommentReplyAuthorCollection: ResolverTypeWrapper<CommentReplyAuthorCollectionGraphQLReturn>;
|
||||
CommentThreadActivityMessage: ResolverTypeWrapper<Omit<CommentThreadActivityMessage, 'reply'> & { reply?: Maybe<ResolversTypes['Comment']> }>;
|
||||
Commit: ResolverTypeWrapper<CommitGraphQLReturn>;
|
||||
@@ -5239,6 +5255,7 @@ export type ResolversTypes = {
|
||||
Model: ResolverTypeWrapper<ModelGraphQLReturn>;
|
||||
ModelCollection: ResolverTypeWrapper<Omit<ModelCollection, 'items'> & { items: Array<ResolversTypes['Model']> }>;
|
||||
ModelMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
|
||||
ModelPermissionChecks: ResolverTypeWrapper<ModelPermissionChecksGraphQLReturn>;
|
||||
ModelVersionsFilter: ModelVersionsFilter;
|
||||
ModelsTreeItem: ResolverTypeWrapper<ModelsTreeItemGraphQLReturn>;
|
||||
ModelsTreeItemCollection: ResolverTypeWrapper<Omit<ModelsTreeItemCollection, 'items'> & { items: Array<ResolversTypes['ModelsTreeItem']> }>;
|
||||
@@ -5516,6 +5533,7 @@ export type ResolversParentTypes = {
|
||||
CommentDataFiltersInput: CommentDataFiltersInput;
|
||||
CommentEditInput: CommentEditInput;
|
||||
CommentMutations: MutationsObjectGraphQLReturn;
|
||||
CommentPermissionChecks: CommentPermissionChecksGraphQLReturn;
|
||||
CommentReplyAuthorCollection: CommentReplyAuthorCollectionGraphQLReturn;
|
||||
CommentThreadActivityMessage: Omit<CommentThreadActivityMessage, 'reply'> & { reply?: Maybe<ResolversParentTypes['Comment']> };
|
||||
Commit: CommitGraphQLReturn;
|
||||
@@ -5565,6 +5583,7 @@ export type ResolversParentTypes = {
|
||||
Model: ModelGraphQLReturn;
|
||||
ModelCollection: Omit<ModelCollection, 'items'> & { items: Array<ResolversParentTypes['Model']> };
|
||||
ModelMutations: MutationsObjectGraphQLReturn;
|
||||
ModelPermissionChecks: ModelPermissionChecksGraphQLReturn;
|
||||
ModelVersionsFilter: ModelVersionsFilter;
|
||||
ModelsTreeItem: ModelsTreeItemGraphQLReturn;
|
||||
ModelsTreeItemCollection: Omit<ModelsTreeItemCollection, 'items'> & { items: Array<ResolversParentTypes['ModelsTreeItem']> };
|
||||
@@ -6094,6 +6113,7 @@ export type CommentResolvers<ContextType = GraphQLContext, ParentType extends Re
|
||||
hasParent?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
parent?: Resolver<Maybe<ResolversTypes['Comment']>, ParentType, ContextType>;
|
||||
permissions?: Resolver<ResolversTypes['CommentPermissionChecks'], ParentType, ContextType>;
|
||||
rawText?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
reactions?: Resolver<Maybe<Array<Maybe<ResolversTypes['String']>>>, ParentType, ContextType>;
|
||||
replies?: Resolver<ResolversTypes['CommentCollection'], ParentType, ContextType, RequireFields<CommentRepliesArgs, 'limit'>>;
|
||||
@@ -6140,6 +6160,11 @@ export type CommentMutationsResolvers<ContextType = GraphQLContext, ParentType e
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CommentPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CommentPermissionChecks'] = ResolversParentTypes['CommentPermissionChecks']> = {
|
||||
canArchive?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type CommentReplyAuthorCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CommentReplyAuthorCollection'] = ResolversParentTypes['CommentReplyAuthorCollection']> = {
|
||||
items?: Resolver<Array<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
|
||||
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
@@ -6314,6 +6339,7 @@ export type ModelResolvers<ContextType = GraphQLContext, ParentType extends Reso
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
pendingImportedVersions?: Resolver<Array<ResolversTypes['FileUpload']>, ParentType, ContextType, RequireFields<ModelPendingImportedVersionsArgs, 'limit'>>;
|
||||
permissions?: Resolver<ResolversTypes['ModelPermissionChecks'], ParentType, ContextType>;
|
||||
previewUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
version?: Resolver<ResolversTypes['Version'], ParentType, ContextType, RequireFields<ModelVersionArgs, 'id'>>;
|
||||
@@ -6335,6 +6361,12 @@ export type ModelMutationsResolvers<ContextType = GraphQLContext, ParentType ext
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type ModelPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ModelPermissionChecks'] = ResolversParentTypes['ModelPermissionChecks']> = {
|
||||
canDelete?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canUpdate?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type ModelsTreeItemResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ModelsTreeItem'] = ResolversParentTypes['ModelsTreeItem']> = {
|
||||
children?: Resolver<Array<ResolversTypes['ModelsTreeItem']>, ParentType, ContextType>;
|
||||
fullName?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
@@ -6659,9 +6691,11 @@ export type ProjectPendingVersionsUpdatedMessageResolvers<ContextType = GraphQLC
|
||||
};
|
||||
|
||||
export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['ProjectPermissionChecks'] = ResolversParentTypes['ProjectPermissionChecks']> = {
|
||||
canBroadcastActivity?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateComment?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canCreateModel?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canLeave?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canMoveToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, RequireFields<ProjectPermissionChecksCanMoveToWorkspaceArgs, 'workspaceId'>>;
|
||||
canMoveToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, Partial<ProjectPermissionChecksCanMoveToWorkspaceArgs>>;
|
||||
canRead?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadSettings?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canReadWebhooks?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
@@ -7375,7 +7409,7 @@ export type WorkspaceMutationsResolvers<ContextType = GraphQLContext, ParentType
|
||||
|
||||
export type WorkspacePermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspacePermissionChecks'] = ResolversParentTypes['WorkspacePermissionChecks']> = {
|
||||
canCreateProject?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
|
||||
canMoveProjectToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, RequireFields<WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs, 'projectId'>>;
|
||||
canMoveProjectToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, Partial<WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
@@ -7529,6 +7563,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
CommentCollection?: CommentCollectionResolvers<ContextType>;
|
||||
CommentDataFilters?: CommentDataFiltersResolvers<ContextType>;
|
||||
CommentMutations?: CommentMutationsResolvers<ContextType>;
|
||||
CommentPermissionChecks?: CommentPermissionChecksResolvers<ContextType>;
|
||||
CommentReplyAuthorCollection?: CommentReplyAuthorCollectionResolvers<ContextType>;
|
||||
CommentThreadActivityMessage?: CommentThreadActivityMessageResolvers<ContextType>;
|
||||
Commit?: CommitResolvers<ContextType>;
|
||||
@@ -7549,6 +7584,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
Model?: ModelResolvers<ContextType>;
|
||||
ModelCollection?: ModelCollectionResolvers<ContextType>;
|
||||
ModelMutations?: ModelMutationsResolvers<ContextType>;
|
||||
ModelPermissionChecks?: ModelPermissionChecksResolvers<ContextType>;
|
||||
ModelsTreeItem?: ModelsTreeItemResolvers<ContextType>;
|
||||
ModelsTreeItemCollection?: ModelsTreeItemCollectionResolvers<ContextType>;
|
||||
Mutation?: MutationResolvers<ContextType>;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { authorizeResolver, BranchPubsubEvents } from '@/modules/shared'
|
||||
import { BranchPubsubEvents } from '@/modules/shared'
|
||||
import {
|
||||
createBranchAndNotifyFactory,
|
||||
updateBranchAndNotifyFactory,
|
||||
deleteBranchAndNotifyFactory
|
||||
} from '@/modules/core/services/branch/management'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import {
|
||||
getBranchByIdFactory,
|
||||
getStreamBranchByNameFactory,
|
||||
@@ -28,7 +27,10 @@ import { getPaginatedStreamBranchesFactory } from '@/modules/core/services/branc
|
||||
import { filteredSubscribe } from '@/modules/shared/utils/subscriptions'
|
||||
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { mapAuthToServerError } from '@/modules/shared/helpers/errorHelper'
|
||||
import {
|
||||
mapAuthToServerError,
|
||||
throwIfAuthNotOk
|
||||
} from '@/modules/shared/helpers/errorHelper'
|
||||
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
|
||||
|
||||
export = {
|
||||
@@ -81,7 +83,7 @@ export = {
|
||||
resourceAccessRules: context.resourceAccessRules
|
||||
})
|
||||
|
||||
const canCreate = await context.authPolicies.project.canCreateModel({
|
||||
const canCreate = await context.authPolicies.project.model.canCreate({
|
||||
userId: context.userId,
|
||||
projectId: args.branch.streamId
|
||||
})
|
||||
@@ -102,13 +104,18 @@ export = {
|
||||
return id
|
||||
},
|
||||
|
||||
async branchUpdate(_parent, args, context) {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.branch.streamId,
|
||||
Roles.Stream.Contributor,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
async branchUpdate(_parent, args, ctx) {
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: args.branch.streamId,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canUpdate = await ctx.authPolicies.project.model.canUpdate({
|
||||
userId: ctx.userId,
|
||||
projectId: args.branch.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canUpdate)
|
||||
|
||||
const projectDB = await getProjectDbClient({ projectId: args.branch.streamId })
|
||||
const getBranchById = getBranchByIdFactory({ db: projectDB })
|
||||
@@ -117,17 +124,23 @@ export = {
|
||||
updateBranch: updateBranchFactory({ db: projectDB }),
|
||||
eventEmit: getEventBus().emit
|
||||
})
|
||||
const newBranch = await updateBranchAndNotify(args.branch, context.userId!)
|
||||
const newBranch = await updateBranchAndNotify(args.branch, ctx.userId!)
|
||||
return !!newBranch
|
||||
},
|
||||
|
||||
async branchDelete(_parent, args, context) {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
args.branch.streamId,
|
||||
Roles.Stream.Contributor,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: args.branch.streamId,
|
||||
resourceAccessRules: context.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canDelete = await context.authPolicies.project.model.canDelete({
|
||||
userId: context.userId,
|
||||
projectId: args.branch.streamId,
|
||||
modelId: args.branch.id
|
||||
})
|
||||
throwIfAuthNotOk(canDelete)
|
||||
|
||||
const projectDB = await getProjectDbClient({ projectId: args.branch.streamId })
|
||||
const markBranchStreamUpdated = markBranchStreamUpdatedFactory({ db: projectDB })
|
||||
@@ -148,12 +161,17 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
BranchPubsubEvents.BranchCreated,
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: payload.streamId,
|
||||
resourceAccessRules: context.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canRead = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
|
||||
return payload.streamId === variables.streamId
|
||||
}
|
||||
@@ -163,12 +181,17 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
BranchPubsubEvents.BranchUpdated,
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: payload.streamId,
|
||||
resourceAccessRules: context.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canRead = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
|
||||
const streamMatch = payload.streamId === variables.streamId
|
||||
if (streamMatch && variables.branchId) {
|
||||
@@ -183,12 +206,17 @@ export = {
|
||||
subscribe: filteredSubscribe(
|
||||
BranchPubsubEvents.BranchDeleted,
|
||||
async (payload, variables, context) => {
|
||||
await authorizeResolver(
|
||||
context.userId,
|
||||
payload.streamId,
|
||||
Roles.Stream.Reviewer,
|
||||
context.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: payload.streamId,
|
||||
resourceAccessRules: context.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canRead = await context.authPolicies.project.canRead({
|
||||
userId: context.userId,
|
||||
projectId: payload.streamId
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
|
||||
return payload.streamId === variables.streamId
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
createBranchAndNotifyFactory,
|
||||
@@ -9,7 +8,6 @@ import {
|
||||
getPaginatedProjectModelsFactory,
|
||||
getProjectTopLevelModelsTreeFactory
|
||||
} from '@/modules/core/services/branch/retrieval'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { getServerOrigin } from '@/modules/shared/helpers/envHelper'
|
||||
import { last } from 'lodash'
|
||||
|
||||
@@ -58,7 +56,9 @@ import {
|
||||
getRegisteredRegionClients
|
||||
} from '@/modules/multiregion/utils/dbSelector'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { mapAuthToServerError } from '@/modules/shared/helpers/errorHelper'
|
||||
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
|
||||
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
|
||||
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
|
||||
|
||||
export = {
|
||||
User: {
|
||||
@@ -297,14 +297,17 @@ export = {
|
||||
},
|
||||
ModelMutations: {
|
||||
async create(_parent, args, ctx) {
|
||||
const canCreate = await ctx.authPolicies.project.canCreateModel({
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: args.input.projectId,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canCreate = await ctx.authPolicies.project.model.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
|
||||
if (!canCreate.isOk) {
|
||||
throw mapAuthToServerError(canCreate.error)
|
||||
}
|
||||
throwIfAuthNotOk(canCreate)
|
||||
|
||||
const projectDB = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
|
||||
@@ -326,12 +329,18 @@ export = {
|
||||
return await createBranchAndNotify(sanitizedInput, ctx.userId!)
|
||||
},
|
||||
async update(_parent, args, ctx) {
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.input.projectId,
|
||||
Roles.Stream.Contributor,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: args.input.projectId,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canUpdate = await ctx.authPolicies.project.model.canUpdate({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId
|
||||
})
|
||||
throwIfAuthNotOk(canUpdate)
|
||||
|
||||
const projectDB = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const updateBranchAndNotify = updateBranchAndNotifyFactory({
|
||||
getBranchById: getBranchByIdFactory({ db: projectDB }),
|
||||
@@ -341,12 +350,19 @@ export = {
|
||||
return await updateBranchAndNotify(args.input, ctx.userId!)
|
||||
},
|
||||
async delete(_parent, args, ctx) {
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.input.projectId,
|
||||
Roles.Stream.Contributor,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: args.input.projectId,
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canDelete = await ctx.authPolicies.project.model.canDelete({
|
||||
userId: ctx.userId,
|
||||
projectId: args.input.projectId,
|
||||
modelId: args.input.id
|
||||
})
|
||||
throwIfAuthNotOk(canDelete)
|
||||
|
||||
const projectDB = await getProjectDbClient({ projectId: args.input.projectId })
|
||||
const markBranchStreamUpdated = markBranchStreamUpdatedFactory({ db: projectDB })
|
||||
const getStream = getStreamFactory({ db })
|
||||
@@ -368,12 +384,18 @@ export = {
|
||||
const { id: projectId, modelIds } = args
|
||||
if (payload.projectId !== projectId) return false
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
projectId,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceAccessRules: ctx.resourceAccessRules,
|
||||
resourceId: projectId,
|
||||
resourceType: TokenResourceIdentifierType.Project
|
||||
})
|
||||
|
||||
const canReadProject = await ctx.authPolicies.project.canRead({
|
||||
userId: ctx.userId,
|
||||
projectId
|
||||
})
|
||||
throwIfAuthNotOk(canReadProject)
|
||||
|
||||
if (!modelIds?.length) return true
|
||||
return modelIds.includes(payload.projectModelsUpdated.id)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,18 @@ export default {
|
||||
Project: {
|
||||
permissions: (parent) => ({ projectId: parent.id })
|
||||
},
|
||||
Model: {
|
||||
permissions: (parent) => ({
|
||||
projectId: parent.streamId,
|
||||
modelId: parent.id
|
||||
})
|
||||
},
|
||||
User: {
|
||||
permissions: () => ({})
|
||||
},
|
||||
ProjectPermissionChecks: {
|
||||
canCreateModel: async (parent, _args, ctx) => {
|
||||
const canCreateModel = await ctx.authPolicies.project.canCreateModel({
|
||||
const canCreateModel = await ctx.authPolicies.project.model.canCreate({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.projectId
|
||||
})
|
||||
@@ -20,7 +26,7 @@ export default {
|
||||
const canMoveToWorkspace = await ctx.authPolicies.project.canMoveToWorkspace({
|
||||
userId: ctx.userId,
|
||||
projectId: parent.projectId,
|
||||
workspaceId: args.workspaceId
|
||||
workspaceId: args.workspaceId ?? undefined
|
||||
})
|
||||
return Authz.toGraphqlResult(canMoveToWorkspace)
|
||||
},
|
||||
@@ -68,6 +74,23 @@ export default {
|
||||
return Authz.toGraphqlResult(canLeave)
|
||||
}
|
||||
},
|
||||
ModelPermissionChecks: {
|
||||
canUpdate: async (parent, _args, ctx) => {
|
||||
const canUpdate = await ctx.authPolicies.project.model.canUpdate({
|
||||
projectId: parent.projectId,
|
||||
userId: ctx.userId
|
||||
})
|
||||
return Authz.toGraphqlResult(canUpdate)
|
||||
},
|
||||
canDelete: async (parent, _args, ctx) => {
|
||||
const canDelete = await ctx.authPolicies.project.model.canDelete({
|
||||
projectId: parent.projectId,
|
||||
userId: ctx.userId,
|
||||
modelId: parent.modelId
|
||||
})
|
||||
return Authz.toGraphqlResult(canDelete)
|
||||
}
|
||||
},
|
||||
RootPermissionChecks: {
|
||||
canCreatePersonalProject: async (_parent, _args, ctx) => {
|
||||
const canCreatePersonalProject = await ctx.authPolicies.project.canCreatePersonal(
|
||||
|
||||
@@ -469,12 +469,13 @@ export = {
|
||||
ProjectSubscriptions.ProjectUpdated,
|
||||
async (payload, args, ctx) => {
|
||||
if (args.id !== payload.projectUpdated.id) return false
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
payload.projectUpdated.id,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
const canRead = await ctx.authPolicies.project.canRead({
|
||||
projectId: payload.projectUpdated.id,
|
||||
userId: ctx.userId
|
||||
})
|
||||
throwIfAuthNotOk(canRead)
|
||||
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
@@ -142,3 +142,8 @@ export type ProjectPermissionChecksGraphQLReturn = {
|
||||
}
|
||||
|
||||
export type RootPermissionChecksGraphQLReturn = GraphQLEmptyReturn
|
||||
|
||||
export type ModelPermissionChecksGraphQLReturn = {
|
||||
modelId: string
|
||||
projectId: string
|
||||
}
|
||||
|
||||
@@ -1129,8 +1129,7 @@ describe('GraphQL API Core @core-api', () => {
|
||||
})
|
||||
expect(res).to.be.json
|
||||
expect(res.body.errors).to.exist
|
||||
expect(res.body.errors[0].extensions.code).to.equal('BRANCH_UPDATE_ERROR')
|
||||
expect(res.body.errors[0].message).to.equal('Branch not found')
|
||||
expect(res.body.errors[0].extensions.code).to.equal('NOT_FOUND_ERROR')
|
||||
|
||||
const res1 = await sendRequest(userC.token, {
|
||||
query:
|
||||
@@ -1139,10 +1138,7 @@ describe('GraphQL API Core @core-api', () => {
|
||||
})
|
||||
expect(res1).to.be.json
|
||||
expect(res1.body.errors).to.exist
|
||||
expect(res1.body.errors[0].extensions.code).to.equal('BRANCH_UPDATE_ERROR')
|
||||
expect(res1.body.errors[0].message).to.equal(
|
||||
'Only the branch creator or stream owners are allowed to delete branches'
|
||||
)
|
||||
expect(res1.body.errors[0].extensions.code).to.equal('FORBIDDEN')
|
||||
|
||||
const res2 = await sendRequest(userA.token, {
|
||||
query:
|
||||
|
||||
@@ -611,6 +611,7 @@ export type Comment = {
|
||||
id: Scalars['String']['output'];
|
||||
/** Parent thread, if there's any */
|
||||
parent?: Maybe<Comment>;
|
||||
permissions: CommentPermissionChecks;
|
||||
/** Plain-text version of the comment text, ideal for previews */
|
||||
rawText: Scalars['String']['output'];
|
||||
/** @deprecated Not actually implemented */
|
||||
@@ -744,6 +745,11 @@ export type CommentMutationsReplyArgs = {
|
||||
input: CreateCommentReplyInput;
|
||||
};
|
||||
|
||||
export type CommentPermissionChecks = {
|
||||
__typename?: 'CommentPermissionChecks';
|
||||
canArchive: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type CommentReplyAuthorCollection = {
|
||||
__typename?: 'CommentReplyAuthorCollection';
|
||||
items: Array<LimitedUser>;
|
||||
@@ -1248,6 +1254,7 @@ export type Model = {
|
||||
name: Scalars['String']['output'];
|
||||
/** Returns a list of versions that are being created from a file import */
|
||||
pendingImportedVersions: Array<FileUpload>;
|
||||
permissions: ModelPermissionChecks;
|
||||
previewUrl?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
version: Version;
|
||||
@@ -1306,6 +1313,12 @@ export type ModelMutationsUpdateArgs = {
|
||||
input: UpdateModelInput;
|
||||
};
|
||||
|
||||
export type ModelPermissionChecks = {
|
||||
__typename?: 'ModelPermissionChecks';
|
||||
canDelete: PermissionCheckResult;
|
||||
canUpdate: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type ModelVersionsFilter = {
|
||||
/** Make sure these specified versions are always loaded first */
|
||||
priorityIds?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
@@ -2540,6 +2553,8 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
@@ -2552,7 +2567,7 @@ export type ProjectPermissionChecks = {
|
||||
|
||||
|
||||
export type ProjectPermissionChecksCanMoveToWorkspaceArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectRole = {
|
||||
@@ -4761,7 +4776,7 @@ export type WorkspacePermissionChecks = {
|
||||
|
||||
|
||||
export type WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
projectId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
|
||||
@@ -79,13 +79,6 @@ export = FF_GENDOAI_MODULE_ENABLED
|
||||
},
|
||||
VersionMutations: {
|
||||
async requestGendoAIRender(__parent, args, ctx) {
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.input.projectId,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
const rateLimitResult = await getRateLimitResult(
|
||||
'GENDO_AI_RENDER_REQUEST',
|
||||
ctx.userId as string
|
||||
@@ -94,6 +87,13 @@ export = FF_GENDOAI_MODULE_ENABLED
|
||||
throw new RateLimitError(rateLimitResult)
|
||||
}
|
||||
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
args.input.projectId,
|
||||
Roles.Stream.Reviewer,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
const userId = ctx.userId!
|
||||
|
||||
const projectId = args.input.projectId
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { StreamNotFoundError } from '@/modules/core/errors/stream'
|
||||
import { WorkspacesModuleDisabledError } from '@/modules/core/errors/workspaces'
|
||||
import { BadRequestError, BaseError, ForbiddenError } from '@/modules/shared/errors'
|
||||
import {
|
||||
BadRequestError,
|
||||
BaseError,
|
||||
ForbiddenError,
|
||||
NotFoundError
|
||||
} from '@/modules/shared/errors'
|
||||
import { SsoSessionMissingOrExpiredError } from '@/modules/workspacesCore/errors'
|
||||
import { Authz, ensureError, throwUncoveredError } from '@speckle/shared'
|
||||
import { VError } from 'verror'
|
||||
@@ -34,6 +39,7 @@ export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
|
||||
case Authz.WorkspaceLimitsReachedError.code:
|
||||
case Authz.WorkspaceNoEditorSeatError.code:
|
||||
case Authz.WorkspaceProjectMoveInvalidError.code:
|
||||
case Authz.CommentNoAccessError.code:
|
||||
return new ForbiddenError(e.message)
|
||||
case Authz.WorkspaceSsoSessionNoAccessError.code:
|
||||
throw new SsoSessionMissingOrExpiredError(e.message, {
|
||||
@@ -47,7 +53,11 @@ export const mapAuthToServerError = (e: Authz.AllAuthErrors): BaseError => {
|
||||
case Authz.WorkspacesNotEnabledError.code:
|
||||
return new WorkspacesModuleDisabledError()
|
||||
case Authz.ProjectLastOwnerError.code:
|
||||
case Authz.ReservedModelNotDeletableError.code:
|
||||
return new BadRequestError(e.message)
|
||||
case Authz.CommentNotFoundError.code:
|
||||
case Authz.ModelNotFoundError.code:
|
||||
return new NotFoundError(e.message)
|
||||
default:
|
||||
throwUncoveredError(e)
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default {
|
||||
const canMoveProjectToWorkspace =
|
||||
await ctx.authPolicies.project.canMoveToWorkspace({
|
||||
userId: ctx.userId,
|
||||
projectId: args.projectId,
|
||||
projectId: args.projectId ?? undefined,
|
||||
workspaceId: parent.workspaceId
|
||||
})
|
||||
return Authz.toGraphqlResult(canMoveProjectToWorkspace)
|
||||
|
||||
@@ -612,6 +612,7 @@ export type Comment = {
|
||||
id: Scalars['String']['output'];
|
||||
/** Parent thread, if there's any */
|
||||
parent?: Maybe<Comment>;
|
||||
permissions: CommentPermissionChecks;
|
||||
/** Plain-text version of the comment text, ideal for previews */
|
||||
rawText: Scalars['String']['output'];
|
||||
/** @deprecated Not actually implemented */
|
||||
@@ -745,6 +746,11 @@ export type CommentMutationsReplyArgs = {
|
||||
input: CreateCommentReplyInput;
|
||||
};
|
||||
|
||||
export type CommentPermissionChecks = {
|
||||
__typename?: 'CommentPermissionChecks';
|
||||
canArchive: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type CommentReplyAuthorCollection = {
|
||||
__typename?: 'CommentReplyAuthorCollection';
|
||||
items: Array<LimitedUser>;
|
||||
@@ -1249,6 +1255,7 @@ export type Model = {
|
||||
name: Scalars['String']['output'];
|
||||
/** Returns a list of versions that are being created from a file import */
|
||||
pendingImportedVersions: Array<FileUpload>;
|
||||
permissions: ModelPermissionChecks;
|
||||
previewUrl?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
version: Version;
|
||||
@@ -1307,6 +1314,12 @@ export type ModelMutationsUpdateArgs = {
|
||||
input: UpdateModelInput;
|
||||
};
|
||||
|
||||
export type ModelPermissionChecks = {
|
||||
__typename?: 'ModelPermissionChecks';
|
||||
canDelete: PermissionCheckResult;
|
||||
canUpdate: PermissionCheckResult;
|
||||
};
|
||||
|
||||
export type ModelVersionsFilter = {
|
||||
/** Make sure these specified versions are always loaded first */
|
||||
priorityIds?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
@@ -2541,6 +2554,8 @@ export const ProjectPendingVersionsUpdatedMessageType = {
|
||||
export type ProjectPendingVersionsUpdatedMessageType = typeof ProjectPendingVersionsUpdatedMessageType[keyof typeof ProjectPendingVersionsUpdatedMessageType];
|
||||
export type ProjectPermissionChecks = {
|
||||
__typename?: 'ProjectPermissionChecks';
|
||||
canBroadcastActivity: PermissionCheckResult;
|
||||
canCreateComment: PermissionCheckResult;
|
||||
canCreateModel: PermissionCheckResult;
|
||||
canLeave: PermissionCheckResult;
|
||||
canMoveToWorkspace: PermissionCheckResult;
|
||||
@@ -2553,7 +2568,7 @@ export type ProjectPermissionChecks = {
|
||||
|
||||
|
||||
export type ProjectPermissionChecksCanMoveToWorkspaceArgs = {
|
||||
workspaceId: Scalars['String']['input'];
|
||||
workspaceId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type ProjectRole = {
|
||||
@@ -4762,7 +4777,7 @@ export type WorkspacePermissionChecks = {
|
||||
|
||||
|
||||
export type WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs = {
|
||||
projectId: Scalars['String']['input'];
|
||||
projectId?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
|
||||
export type WorkspacePlan = {
|
||||
|
||||
@@ -127,6 +127,26 @@ export const ServerNoSessionError = defineAuthError({
|
||||
message: 'You are not logged in to this server'
|
||||
})
|
||||
|
||||
export const CommentNotFoundError = defineAuthError({
|
||||
code: 'CommentNotFound',
|
||||
message: 'Comment not found'
|
||||
})
|
||||
|
||||
export const CommentNoAccessError = defineAuthError({
|
||||
code: 'CommentNoAccess',
|
||||
message: 'You do not have access to this comment'
|
||||
})
|
||||
|
||||
export const ModelNotFoundError = defineAuthError({
|
||||
code: 'ModelNotFound',
|
||||
message: 'Model not found'
|
||||
})
|
||||
|
||||
export const ReservedModelNotDeletableError = defineAuthError({
|
||||
code: 'ReservedModelNotDeletable',
|
||||
message: 'This model is reserved and cannot be deleted'
|
||||
})
|
||||
|
||||
// Resolve all exported error types
|
||||
export type AllAuthErrors = ValueOf<{
|
||||
[key in keyof typeof import('./authErrors.js')]: typeof import('./authErrors.js')[key] extends new (
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Comment } from './types.js'
|
||||
|
||||
export type GetComment = (args: {
|
||||
commentId: string
|
||||
projectId: string
|
||||
}) => Promise<Comment | null>
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Comment = {
|
||||
id: string
|
||||
authorId: string
|
||||
projectId: string
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
export type ProjectContext = { projectId: string }
|
||||
export type MaybeProjectContext = { projectId?: string }
|
||||
|
||||
export type UserContext = { userId: string }
|
||||
export type MaybeUserContext = { userId?: string }
|
||||
|
||||
export type WorkspaceContext = { workspaceId: string }
|
||||
export type MaybeWorkspaceContext = { workspaceId?: string }
|
||||
|
||||
export type CommentContext = { commentId: string }
|
||||
|
||||
export type ModelContext = { modelId: string }
|
||||
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
GetWorkspaceSsoProvider,
|
||||
GetWorkspaceSsoSession
|
||||
} from './workspaces/operations.js'
|
||||
import { GetComment } from './comments/operations.js'
|
||||
import { GetModel } from './models/operations.js'
|
||||
|
||||
// utility type that ensures all properties functions that return promises
|
||||
type PromiseAll<T> = {
|
||||
@@ -63,7 +65,9 @@ export const AuthCheckContextLoaderKeys = <const>{
|
||||
getWorkspaceLimits: 'getWorkspaceLimits',
|
||||
getWorkspaceSsoProvider: 'getWorkspaceSsoProvider',
|
||||
getWorkspaceSsoSession: 'getWorkspaceSsoSession',
|
||||
getAdminOverrideEnabled: 'getAdminOverrideEnabled'
|
||||
getAdminOverrideEnabled: 'getAdminOverrideEnabled',
|
||||
getComment: 'getComment',
|
||||
getModel: 'getModel'
|
||||
}
|
||||
export const Loaders = AuthCheckContextLoaderKeys // shorter alias
|
||||
/* v8 ignore end */
|
||||
@@ -87,6 +91,8 @@ export type AllAuthCheckContextLoaders = AuthContextLoaderMappingDefinition<{
|
||||
getWorkspaceModelCount: GetWorkspaceModelCount
|
||||
getWorkspaceSsoProvider: GetWorkspaceSsoProvider
|
||||
getWorkspaceSsoSession: GetWorkspaceSsoSession
|
||||
getComment: GetComment
|
||||
getModel: GetModel
|
||||
}>
|
||||
|
||||
export type AuthCheckContextLoaders<
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Model } from './types.js'
|
||||
|
||||
export type GetModel = (args: {
|
||||
projectId: string
|
||||
modelId: string
|
||||
}) => Promise<Model | null>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user