Merge branch 'main' of github.com:specklesystems/speckle-server into alessandro/web-2944-versions-limits

This commit is contained in:
Alessandro Magionami
2025-04-11 14:54:13 +02:00
133 changed files with 5055 additions and 1278 deletions
+2
View File
@@ -68,6 +68,8 @@ minio-data/
postgres-data/
redis-data/
packages/fileimport-service/src/ifc-dotnet/output
.tshy-build
obj/
bin/
+32
View File
@@ -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>
@@ -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
@@ -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>
@@ -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>
@@ -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)
})
+1 -1
View File
@@ -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', {
+18 -11
View File
@@ -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!
}
+2
View File
@@ -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:
+10 -3
View File
@@ -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 }
+7 -1
View File
@@ -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