Merge branch 'main' into andrew/show-admins-in-discoverable-workspace-card

This commit is contained in:
andrewwallacespeckle
2025-05-22 12:51:46 +02:00
40 changed files with 463 additions and 152 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ def speckle_image(package,original_package_name=None):
original_package_name = package
package_dir = 'packages/{}'.format(original_package_name)
if package == 'test-deployment' or package == 'monitor-deployment' or package == 'docker-compose-ingress':
if package == 'test-deployment' or package == 'docker-compose-ingress':
package_dir = 'utils/{}'.format(package)
docker_build('speckle/speckle-{}'.format(package),
context='../..',
+11 -11
View File
@@ -66,14 +66,14 @@ helm_resource('minio',
deps=['./values/minio.values.yaml'],
labels=['speckle-dependencies'])
helm_resource('redis',
release_name='redis',
namespace='redis',
chart='oci://registry-1.docker.io/bitnamicharts/redis',
flags=['--version=18.7.1',
'--values=./values/redis.values.yaml',
helm_resource('valkey',
release_name='valkey',
namespace='valkey',
chart='oci://registry-1.docker.io/bitnamicharts/valkey',
flags=['--version=3.0.8',
'--values=./values/valkey.values.yaml',
'--kube-context=kind-speckle-server'],
deps=['./values/redis.values.yaml'],
deps=['./values/valkey.values.yaml'],
labels=['speckle-dependencies'])
#FIXME this helm chart does not deploy any containers, so tilt incorrectly believes it never gets to a final state
@@ -98,11 +98,11 @@ helm_resource('ingress-nginx',
release_name='ingress-nginx',
namespace='ingress-nginx',
chart='ingress-nginx-repo/ingress-nginx',
flags=['--version=^4.8.0',
flags=['--version=4.8.0',
'--values=./values/nginx.values.yaml',
'--kube-context=kind-speckle-server'],
deps=['./values/nginx.values.yaml'],
resource_deps=['postgresql', 'minio', 'redis', 'ingress-nginx-repo'],
resource_deps=['postgresql', 'minio', 'valkey', 'ingress-nginx-repo'],
labels=['speckle-dependencies'])
# Uncomment the below, and comment out the other helm_resource('speckle-server'...) to use the speckle-server helm chart
@@ -113,7 +113,7 @@ helm_resource('ingress-nginx',
# namespace='speckle-server',
# chart='speckle-server-repo/speckle-server',
# deps=['./values/speckle-server.values.yaml'],
# resource_deps=['postgresql', 'minio', 'redis', 'ingress-nginx','speckle-server-repo'],
# resource_deps=['postgresql', 'minio', 'valkey', 'ingress-nginx','speckle-server-repo'],
# flags=['--values=./values/speckle-server.values.yaml', '--devel'],
# labels=['speckle-server'])
@@ -144,7 +144,7 @@ helm_resource('speckle-server',
'webhook_service.image',
],
deps=['../../utils/helm', './values/speckle-server.values.yaml'],
resource_deps=['postgresql', 'minio', 'redis', 'ingress-nginx'],
resource_deps=['postgresql', 'minio', 'valkey', 'ingress-nginx'],
labels=['speckle-server'])
# TODO this is not yet working as it is expecting the updated version of the test container which is not yet released as of 2.17.16
@@ -2,7 +2,7 @@ apiVersion: v1
data:
s3_secret_key: 'bWluaW9hZG1pbg=='
session_secret: 'c3BvcmtsZXNzcHJlY2tsZXNzcGVrbGU='
redis_url: 'cmVkaXM6Ly86cmVkaXNAcmVkaXMtbWFzdGVyLnJlZGlzLnN2Yy5jbHVzdGVyLmxvY2FsOjYzNzk='
redis_url: 'cmVkaXM6Ly86dmFsa2V5QHZhbGtleS1wcmltYXJ5LnZhbGtleS5zdmMuY2x1c3Rlci5sb2NhbDo2Mzc5Cg=='
postgres_url: 'cG9zdGdyZXNxbDovL3NwZWNrbGU6c3BlY2tsZUBwb3N0Z3Jlc3FsLnBvc3RncmVzLnN2Yy5jbHVzdGVyLmxvY2FsOjU0MzIvc3BlY2tsZQo='
kind: Secret
metadata:
@@ -1,4 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: 'redis'
name: 'valkey'
@@ -3,7 +3,8 @@ controller:
hostNetwork: true
admissionWebhooks:
enabled: false
# progressDeadlineSeconds: 600 #HACK helm chart was complaining that this was less than minReadySeconds https://github.com/kubernetes/ingress-nginx/blob/c72441585e1ab1a32df86e760613d36fa804315d/charts/ingress-nginx/templates/controller-deployment.yaml#L26
tcp:
5433: 'postgres/postgresql:5432'
6380: 'redis/redis-master:6379'
6380: 'valkey/valkey-primary:6379'
9002: 'minio/minio:9000'
@@ -1,3 +1,3 @@
architecture: standalone
auth:
password: redis
password: valkey
+1 -1
View File
@@ -24,7 +24,7 @@
This monorepo is the home of the Speckle v2 web packages:
- [`packages/server`](https://github.com/specklesystems/speckle-server/blob/main/packages/server): the Server, a nodejs app. Core external dependencies are a Redis and Postgresql db.
- [`packages/server`](https://github.com/specklesystems/speckle-server/blob/main/packages/server): the Server, a nodejs app.
- [`packages/frontend-2`](https://github.com/specklesystems/speckle-server/blob/main/packages/frontend-2): the Frontend, a Nuxt/Vue app.
- [`packages/viewer`](https://github.com/specklesystems/speckle-server/blob/main/packages/viewer): a threejs extension that allows you to display 3D data [![npm version](https://camo.githubusercontent.com/dc69232cc57b77de6554e752dd6dfc60ca0ecdfbe91bdfcbf7c7531a511ec200/68747470733a2f2f62616467652e667572792e696f2f6a732f253430737065636b6c652532467669657765722e737667)](https://www.npmjs.com/package/@speckle/viewer)
- [`packages/objectloader`](https://github.com/specklesystems/speckle-server/blob/main/packages/objectloader): a small js utility class that helps you stream an object and all its sub-components from the Speckle Server API. [![npm version](https://camo.githubusercontent.com/4d4f1e38ce50aaf11b4a3ad8e01ce3eaaa561dc5fd08febbae556f52f1d41097/68747470733a2f2f62616467652e667572792e696f2f6a732f253430737065636b6c652532466f626a6563746c6f616465722e737667)](https://www.npmjs.com/package/@speckle/objectloader)
+1 -1
View File
@@ -50,7 +50,7 @@ services:
- '127.0.0.1:5402:5432'
redis:
image: 'redis:7-alpine'
image: 'valkey/valkey:8.1-alpine'
restart: always
volumes:
- redis-data:/data
@@ -4,7 +4,7 @@
<FormFileUploadZone
ref="uploadZone"
v-slot="{ isDraggingFiles, openFilePicker }"
:disabled="isUploading || disabled"
:disabled="isUploading || isDisabled"
:size-limit="maxSizeInBytes"
:accept="accept"
class="flex items-center h-full"
@@ -41,14 +41,10 @@
</div>
<div>
<p v-if="showEmptyState" class="text-foreground-2 text-heading-sm p-0 m-0">
{{
emptyStateVariant === 'modelsSection'
? 'The project has no models, yet.'
: 'No models, yet.'
}}
<p v-if="emptyStateHeading" :class="emptyStateHeadingClasses">
{{ emptyStateHeading }}
</p>
<p :class="paragraphClasses">
<p v-if="!isDisabled" :class="paragraphClasses">
Use
<NuxtLink :to="connectorsRoute" class="font-medium">
<span class="underline">connectors</span>
@@ -57,7 +53,7 @@
{{ modelName ? 'this model' : 'this project' }}, or drag and drop a
IFC/OBJ/STL file here.
</p>
<div v-if="showEmptyState" :class="buttonsClasses">
<div v-if="showEmptyState && !isDisabled" :class="buttonsClasses">
<FormButton :to="connectorsRoute" size="sm" color="outline">
Install connectors
</FormButton>
@@ -76,13 +72,43 @@ import { useFileUploadProgressCore } from '~~/lib/form/composables/fileUpload'
import { ExclamationTriangleIcon } from '@heroicons/vue/24/solid'
import { connectorsRoute } from '~/lib/common/helpers/route'
import type { Nullable } from '@speckle/shared'
import { graphql } from '~/lib/common/generated/gql'
import type {
ProjectCardImportFileArea_ModelFragment,
ProjectCardImportFileArea_ProjectFragment
} from '~/lib/common/generated/gql/graphql'
type EmptyStateVariants = 'modelGrid' | 'modelList' | 'modelsSection'
graphql(`
fragment ProjectCardImportFileArea_Project on Project {
id
permissions {
canCreateModel {
...FullPermissionCheckResult
}
}
...UseFileImport_Project
}
`)
graphql(`
fragment ProjectCardImportFileArea_Model on Model {
id
name
permissions {
canCreateVersion {
...FullPermissionCheckResult
}
}
...UseFileImport_Model
}
`)
const props = defineProps<{
projectId: string
project: ProjectCardImportFileArea_ProjectFragment
model?: ProjectCardImportFileArea_ModelFragment
modelName?: string
disabled?: boolean
emptyStateVariant?: EmptyStateVariants
}>()
@@ -105,10 +131,43 @@ const uploadZone = ref(
}>
)
const modelName = computed(() => props.modelName || props.model?.name)
const accessCheck = computed(() => {
return props.model
? props.model.permissions.canCreateVersion
: props.project.permissions.canCreateModel
})
const isDisabled = computed(() => !accessCheck.value.authorized)
const showEmptyState = computed(
() =>
props.emptyStateVariant !== 'modelGrid' && props.emptyStateVariant !== 'modelList'
)
const emptyStateHeading = computed(() => {
if (showEmptyState.value) {
return props.emptyStateVariant === 'modelsSection'
? 'The project has no models, yet.'
: 'No models, yet.'
}
if (isDisabled.value) {
return modelName.value
? 'The model has no versions, yet.'
: 'The project has no models, yet.'
}
return undefined
})
const emptyStateHeadingClasses = computed(() => {
const classParts = ['text-foreground-2 text-heading-sm p-0 m-0 ']
if (isDisabled.value) {
classParts.push('text-balance text-center')
}
return classParts.join(' ')
})
const containerClasses = computed(() => {
const classParts = ['w-full flex justify-center items-center']
@@ -60,9 +60,8 @@
<div v-else>
<ProjectCardImportFileArea
ref="importArea"
:project-id="project.id"
:model-name="project.model.name"
:disabled="project.workspace?.readOnly"
:project="project"
:model="project.model"
class="h-full w-full"
/>
</div>
@@ -127,8 +126,10 @@ graphql(`
...ProjectModelPageVersionsCardVersion
}
}
...ProjectCardImportFileArea_Model
}
...ProjectsModelPageEmbed_Project
...ProjectCardImportFileArea_Project
}
`)
@@ -78,17 +78,16 @@
</NuxtLink>
</template>
<div
v-if="!isPendingModelFragment(model)"
v-if="!isPendingModelFragment(model) && project"
v-show="!previewUrl && !pendingVersion"
class="h-48 w-full relative z-30"
>
<ProjectCardImportFileArea
ref="importArea"
empty-state-variant="modelGrid"
:project-id="projectId"
:model-name="model.name"
:project="project"
:model="model"
class="w-full h-full"
:disabled="!canCreateModel?.authorized"
/>
</div>
</div>
@@ -140,6 +139,7 @@ graphql(`
role
visibility
...ProjectPageModelsActions_Project
...ProjectCardImportFileArea_Project
permissions {
canCreateModel {
...FullPermissionCheckResult
@@ -178,7 +178,6 @@ const importArea = ref(
const showActionsMenu = ref(false)
const hovered = ref(false)
const canCreateModel = computed(() => props.project?.permissions.canCreateModel)
const containerClasses = computed(() => {
const classParts = [
'group rounded-xl bg-foundation border border-outline-3 hover:border-outline-5 w-full z-[0]'
@@ -24,8 +24,8 @@
/>
<div v-else-if="!hideFileUpload">
<ProjectCardImportFileArea
:disabled="project?.workspace?.readOnly"
:project-id="projectId"
v-if="project"
:project="project"
class="h-36 col-span-4"
/>
</div>
@@ -26,8 +26,8 @@
/>
<div v-else>
<ProjectCardImportFileArea
:project-id="projectId"
:disabled="project?.workspace?.readOnly"
v-if="project"
:project="project"
class="h-36 col-span-4"
/>
</div>
@@ -55,9 +55,9 @@
<ProjectCardImportFileArea
v-if="!isPendingFileUpload(item)"
ref="importArea"
:project-id="project.id"
:project="project"
:model-name="item.fullName"
:disabled="!canCreateModel.authorized"
:model="item.model || undefined"
class="hidden"
/>
<div
@@ -78,9 +78,9 @@
:empty-state-variant="
props.gridOrList === GridListToggleValue.Grid ? 'modelGrid' : 'modelList'
"
:project-id="project.id"
:project="project"
:model-name="item.fullName"
:disabled="!canCreateModel.authorized"
:model="item.model || undefined"
class="h-full w-full"
/>
</div>
@@ -259,6 +259,7 @@ graphql(`
fragment ProjectPageModelsStructureItem_Project on Project {
id
...ProjectPageModelsActions_Project
...ProjectCardImportFileArea_Project
permissions {
canCreateModel {
...FullPermissionCheckResult
@@ -274,6 +275,7 @@ graphql(`
fullName
model {
...ProjectPageLatestItemsModelItem
...ProjectCardImportFileArea_Model
}
hasChildren
updatedAt
@@ -109,11 +109,7 @@
<ProjectCardImportFileArea
v-if="hasNoModels"
empty-state-variant="modelsSection"
:project-id="project.id"
:disabled="
project?.workspace?.readOnly ||
!project.permissions.canCreateModel.authorized
"
:project="project"
class="h-28 col-span-4"
/>
</div>
@@ -53,8 +53,10 @@ type Documents = {
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n workspaceId\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc,
"\n query InviteDialogProjectRowProjectCollaborators(\n $projectId: String!\n $filter: InvitableCollaboratorsFilter\n ) {\n project(id: $projectId) {\n invitableCollaborators(filter: $filter) {\n items {\n user {\n id\n name\n }\n }\n }\n }\n }\n": typeof types.InviteDialogProjectRowProjectCollaboratorsDocument,
"\n fragment ProjectCardImportFileArea_Project on Project {\n id\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Project\n }\n": typeof types.ProjectCardImportFileArea_ProjectFragmentDoc,
"\n fragment ProjectCardImportFileArea_Model on Model {\n id\n name\n permissions {\n canCreateVersion {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Model\n }\n": typeof types.ProjectCardImportFileArea_ModelFragmentDoc,
"\n fragment ProjectModelPageHeaderProject on Project {\n id\n name\n model(id: $modelId) {\n id\n name\n description\n }\n workspace {\n id\n slug\n name\n role\n }\n }\n": typeof types.ProjectModelPageHeaderProjectFragmentDoc,
"\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n }\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.ProjectModelPageVersionsPaginationFragmentDoc,
"\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n ...ProjectCardImportFileArea_Model\n }\n ...ProjectsModelPageEmbed_Project\n ...ProjectCardImportFileArea_Project\n }\n": typeof types.ProjectModelPageVersionsPaginationFragmentDoc,
"\n fragment ProjectModelPageVersionsProject on Project {\n ...ProjectPageProjectHeader\n model(id: $modelId) {\n id\n name\n pendingImportedVersions {\n ...PendingFileUpload\n }\n }\n ...ProjectModelPageVersionsPagination\n ...ProjectsModelPageEmbed_Project\n workspace {\n id\n readOnly\n }\n }\n": typeof types.ProjectModelPageVersionsProjectFragmentDoc,
"\n fragment ProjectModelPageDialogDeleteVersion on Version {\n id\n message\n }\n": typeof types.ProjectModelPageDialogDeleteVersionFragmentDoc,
"\n fragment ProjectModelPageDialogEditMessageVersion on Version {\n id\n message\n }\n": typeof types.ProjectModelPageDialogEditMessageVersionFragmentDoc,
@@ -80,11 +82,11 @@ type Documents = {
"\n fragment ProjectDiscussionsPageResults_Project on Project {\n id\n }\n": typeof types.ProjectDiscussionsPageResults_ProjectFragmentDoc,
"\n fragment ProjectPageModelsActions on Model {\n id\n name\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canDelete {\n ...FullPermissionCheckResult\n }\n canCreateVersion {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsActionsFragmentDoc,
"\n fragment ProjectPageModelsActions_Project on Project {\n id\n workspace {\n id\n slug\n }\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.ProjectPageModelsActions_ProjectFragmentDoc,
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsCardProjectFragmentDoc,
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsCardProjectFragmentDoc,
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectModelsPageHeader_ProjectFragmentDoc,
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": typeof types.ProjectModelsPageResults_ProjectFragmentDoc,
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\n }\n": typeof types.SingleLevelModelTreeItemFragmentDoc,
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n }\n hasChildren\n updatedAt\n }\n": typeof types.SingleLevelModelTreeItemFragmentDoc,
"\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": typeof types.ProjectPageModelsCardDeleteDialogFragmentDoc,
"\n fragment ProjectPageModelsCardRenameDialog on Model {\n id\n name\n description\n }\n": typeof types.ProjectPageModelsCardRenameDialogFragmentDoc,
"\n query ProjectPageSettingsGeneral($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectPageSettingsGeneralBlockProjectInfo_Project\n ...ProjectPageSettingsGeneralBlockAccess_Project\n ...ProjectPageSettingsGeneralBlockDiscussions_Project\n ...ProjectPageSettingsGeneralBlockLeave_Project\n ...ProjectPageSettingsGeneralBlockDelete_Project\n ...ProjectPageTeamInternals_Project\n }\n }\n": typeof types.ProjectPageSettingsGeneralDocument,
@@ -186,6 +188,8 @@ type Documents = {
"\n query ServerInfoBlobSizeLimit {\n serverInfo {\n configuration {\n blobSizeLimitBytes\n }\n }\n }\n": typeof types.ServerInfoBlobSizeLimitDocument,
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": typeof types.ServerInfoAllScopesDocument,
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": typeof types.ProjectModelsSelectorValuesDocument,
"\n fragment UseFileImport_Project on Project {\n id\n }\n": typeof types.UseFileImport_ProjectFragmentDoc,
"\n fragment UseFileImport_Model on Model {\n id\n name\n }\n": typeof types.UseFileImport_ModelFragmentDoc,
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n configuration {\n isEmailEnabled\n }\n }\n }\n": typeof types.MainServerInfoDataDocument,
"\n mutation DeleteAccessToken($token: String!) {\n apiTokenRevoke(token: $token)\n }\n": typeof types.DeleteAccessTokenDocument,
"\n mutation CreateAccessToken($token: ApiTokenCreateInput!) {\n apiTokenCreate(token: $token)\n }\n": typeof types.CreateAccessTokenDocument,
@@ -216,11 +220,11 @@ type Documents = {
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n seatType\n workspaceRole\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": typeof types.ProjectPageTeamInternals_ProjectFragmentDoc,
"\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": typeof types.ProjectPageTeamDialogFragmentDoc,
"\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 ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n ...ProjectCardImportFileArea_Project\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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canDelete {\n ...FullPermissionCheckResult\n }\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 ...ProjectCardImportFileArea_Model\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 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 ...ProjectCardImportFileArea_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,
"\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n": typeof types.ProjectPageLatestItemsCommentItemFragmentDoc,
"\n mutation CreateModel($input: CreateModelInput!) {\n modelMutations {\n create(input: $input) {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n": typeof types.CreateModelDocument,
@@ -468,8 +472,10 @@ const documents: Documents = {
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n workspaceId\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
"\n query InviteDialogProjectRowProjectCollaborators(\n $projectId: String!\n $filter: InvitableCollaboratorsFilter\n ) {\n project(id: $projectId) {\n invitableCollaborators(filter: $filter) {\n items {\n user {\n id\n name\n }\n }\n }\n }\n }\n": types.InviteDialogProjectRowProjectCollaboratorsDocument,
"\n fragment ProjectCardImportFileArea_Project on Project {\n id\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Project\n }\n": types.ProjectCardImportFileArea_ProjectFragmentDoc,
"\n fragment ProjectCardImportFileArea_Model on Model {\n id\n name\n permissions {\n canCreateVersion {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Model\n }\n": types.ProjectCardImportFileArea_ModelFragmentDoc,
"\n fragment ProjectModelPageHeaderProject on Project {\n id\n name\n model(id: $modelId) {\n id\n name\n description\n }\n workspace {\n id\n slug\n name\n role\n }\n }\n": types.ProjectModelPageHeaderProjectFragmentDoc,
"\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n }\n ...ProjectsModelPageEmbed_Project\n }\n": types.ProjectModelPageVersionsPaginationFragmentDoc,
"\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n ...ProjectCardImportFileArea_Model\n }\n ...ProjectsModelPageEmbed_Project\n ...ProjectCardImportFileArea_Project\n }\n": types.ProjectModelPageVersionsPaginationFragmentDoc,
"\n fragment ProjectModelPageVersionsProject on Project {\n ...ProjectPageProjectHeader\n model(id: $modelId) {\n id\n name\n pendingImportedVersions {\n ...PendingFileUpload\n }\n }\n ...ProjectModelPageVersionsPagination\n ...ProjectsModelPageEmbed_Project\n workspace {\n id\n readOnly\n }\n }\n": types.ProjectModelPageVersionsProjectFragmentDoc,
"\n fragment ProjectModelPageDialogDeleteVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogDeleteVersionFragmentDoc,
"\n fragment ProjectModelPageDialogEditMessageVersion on Version {\n id\n message\n }\n": types.ProjectModelPageDialogEditMessageVersionFragmentDoc,
@@ -495,11 +501,11 @@ const documents: Documents = {
"\n fragment ProjectDiscussionsPageResults_Project on Project {\n id\n }\n": types.ProjectDiscussionsPageResults_ProjectFragmentDoc,
"\n fragment ProjectPageModelsActions on Model {\n id\n name\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canDelete {\n ...FullPermissionCheckResult\n }\n canCreateVersion {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsActionsFragmentDoc,
"\n fragment ProjectPageModelsActions_Project on Project {\n id\n workspace {\n id\n slug\n }\n ...ProjectsModelPageEmbed_Project\n }\n": types.ProjectPageModelsActions_ProjectFragmentDoc,
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsCardProjectFragmentDoc,
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsCardProjectFragmentDoc,
"\n fragment ProjectModelsPageHeader_Project on Project {\n id\n name\n sourceApps\n role\n models {\n totalCount\n }\n team {\n id\n user {\n ...FormUsersSelectItem\n }\n }\n workspace {\n id\n role\n slug\n name\n readOnly\n plan {\n name\n }\n }\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectModelsPageHeader_ProjectFragmentDoc,
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": types.ProjectModelsPageResults_ProjectFragmentDoc,
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
"\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": types.ProjectPageModelsCardDeleteDialogFragmentDoc,
"\n fragment ProjectPageModelsCardRenameDialog on Model {\n id\n name\n description\n }\n": types.ProjectPageModelsCardRenameDialogFragmentDoc,
"\n query ProjectPageSettingsGeneral($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectPageSettingsGeneralBlockProjectInfo_Project\n ...ProjectPageSettingsGeneralBlockAccess_Project\n ...ProjectPageSettingsGeneralBlockDiscussions_Project\n ...ProjectPageSettingsGeneralBlockLeave_Project\n ...ProjectPageSettingsGeneralBlockDelete_Project\n ...ProjectPageTeamInternals_Project\n }\n }\n": types.ProjectPageSettingsGeneralDocument,
@@ -601,6 +607,8 @@ const documents: Documents = {
"\n query ServerInfoBlobSizeLimit {\n serverInfo {\n configuration {\n blobSizeLimitBytes\n }\n }\n }\n": types.ServerInfoBlobSizeLimitDocument,
"\n query ServerInfoAllScopes {\n serverInfo {\n scopes {\n name\n description\n }\n }\n }\n": types.ServerInfoAllScopesDocument,
"\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectModelsSelectorValuesDocument,
"\n fragment UseFileImport_Project on Project {\n id\n }\n": types.UseFileImport_ProjectFragmentDoc,
"\n fragment UseFileImport_Model on Model {\n id\n name\n }\n": types.UseFileImport_ModelFragmentDoc,
"\n query MainServerInfoData {\n serverInfo {\n adminContact\n canonicalUrl\n company\n description\n guestModeEnabled\n inviteOnly\n name\n termsOfService\n version\n automateUrl\n configuration {\n isEmailEnabled\n }\n }\n }\n": types.MainServerInfoDataDocument,
"\n mutation DeleteAccessToken($token: String!) {\n apiTokenRevoke(token: $token)\n }\n": types.DeleteAccessTokenDocument,
"\n mutation CreateAccessToken($token: ApiTokenCreateInput!) {\n apiTokenCreate(token: $token)\n }\n": types.CreateAccessTokenDocument,
@@ -631,11 +639,11 @@ const documents: Documents = {
"\n fragment ProjectPageTeamInternals_Project on Project {\n id\n role\n invitedTeam {\n id\n title\n role\n inviteId\n user {\n role\n ...LimitedUserAvatar\n }\n }\n team {\n role\n seatType\n workspaceRole\n user {\n id\n role\n ...LimitedUserAvatar\n }\n }\n }\n": types.ProjectPageTeamInternals_ProjectFragmentDoc,
"\n fragment ProjectPageTeamDialog on Project {\n id\n name\n role\n allowPublicComments\n visibility\n team {\n id\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n invitedTeam {\n id\n title\n inviteId\n role\n user {\n ...LimitedUserAvatar\n role\n }\n }\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": types.ProjectPageTeamDialogFragmentDoc,
"\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 ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n ...ProjectCardImportFileArea_Project\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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canDelete {\n ...FullPermissionCheckResult\n }\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 ...ProjectCardImportFileArea_Model\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 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 ...ProjectCardImportFileArea_Project\n }\n": types.ProjectPageLatestItemsModelsFragmentDoc,
"\n fragment ProjectPageLatestItemsComments on Project {\n id\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageLatestItemsCommentsFragmentDoc,
"\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n": types.ProjectPageLatestItemsCommentItemFragmentDoc,
"\n mutation CreateModel($input: CreateModelInput!) {\n modelMutations {\n create(input: $input) {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n": types.CreateModelDocument,
@@ -1014,6 +1022,14 @@ export function graphql(source: "\n fragment InviteDialogProject_Project on Pro
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query InviteDialogProjectRowProjectCollaborators(\n $projectId: String!\n $filter: InvitableCollaboratorsFilter\n ) {\n project(id: $projectId) {\n invitableCollaborators(filter: $filter) {\n items {\n user {\n id\n name\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query InviteDialogProjectRowProjectCollaborators(\n $projectId: String!\n $filter: InvitableCollaboratorsFilter\n ) {\n project(id: $projectId) {\n invitableCollaborators(filter: $filter) {\n items {\n user {\n id\n name\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectCardImportFileArea_Project on Project {\n id\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Project\n }\n"): (typeof documents)["\n fragment ProjectCardImportFileArea_Project on Project {\n id\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_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 ProjectCardImportFileArea_Model on Model {\n id\n name\n permissions {\n canCreateVersion {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Model\n }\n"): (typeof documents)["\n fragment ProjectCardImportFileArea_Model on Model {\n id\n name\n permissions {\n canCreateVersion {\n ...FullPermissionCheckResult\n }\n }\n ...UseFileImport_Model\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1021,7 +1037,7 @@ export function graphql(source: "\n fragment ProjectModelPageHeaderProject on P
/**
* 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 ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n }\n ...ProjectsModelPageEmbed_Project\n }\n"): (typeof documents)["\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n }\n ...ProjectsModelPageEmbed_Project\n }\n"];
export function graphql(source: "\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n ...ProjectCardImportFileArea_Model\n }\n ...ProjectsModelPageEmbed_Project\n ...ProjectCardImportFileArea_Project\n }\n"): (typeof documents)["\n fragment ProjectModelPageVersionsPagination on Project {\n id\n visibility\n model(id: $modelId) {\n id\n versions(limit: 16, cursor: $versionsCursor) {\n cursor\n totalCount\n items {\n ...ProjectModelPageVersionsCardVersion\n }\n }\n ...ProjectCardImportFileArea_Model\n }\n ...ProjectsModelPageEmbed_Project\n ...ProjectCardImportFileArea_Project\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1125,7 +1141,7 @@ export function graphql(source: "\n fragment ProjectPageModelsActions_Project o
/**
* 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 ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n visibility\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1137,11 +1153,11 @@ export function graphql(source: "\n fragment ProjectModelsPageResults_Project o
/**
* 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 ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\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.
*/
export function graphql(source: "\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\n }\n"): (typeof documents)["\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n }\n hasChildren\n updatedAt\n }\n"];
export function graphql(source: "\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n }\n hasChildren\n updatedAt\n }\n"): (typeof documents)["\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n }\n hasChildren\n updatedAt\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1546,6 +1562,14 @@ export function graphql(source: "\n query ServerInfoAllScopes {\n serverInfo
* 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 ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectModelsSelectorValues($projectId: String!, $cursor: String) {\n project(id: $projectId) {\n id\n models(limit: 100, cursor: $cursor) {\n cursor\n totalCount\n items {\n ...CommonModelSelectorModel\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment UseFileImport_Project on Project {\n id\n }\n"): (typeof documents)["\n fragment UseFileImport_Project on Project {\n id\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 UseFileImport_Model on Model {\n id\n name\n }\n"): (typeof documents)["\n fragment UseFileImport_Model on Model {\n id\n name\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1669,7 +1693,7 @@ export function graphql(source: "\n fragment ProjectDashboardItemNoModels 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 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 documents)["\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"];
export function graphql(source: "\n fragment ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n ...ProjectCardImportFileArea_Project\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 documents)["\n fragment ProjectDashboardItem on Project {\n id\n ...ProjectDashboardItemNoModels\n ...ProjectCardImportFileArea_Project\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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1677,7 +1701,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 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"];
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 ...ProjectCardImportFileArea_Model\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 ...ProjectCardImportFileArea_Model\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.
*/
@@ -1685,7 +1709,7 @@ export function graphql(source: "\n fragment ProjectUpdatableMetadata on Projec
/**
* 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 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 documents)["\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"];
export function graphql(source: "\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 ...ProjectCardImportFileArea_Project\n }\n"): (typeof documents)["\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 ...ProjectCardImportFileArea_Project\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -10,12 +10,35 @@ import { importFile } from '~~/lib/core/api/fileImport'
import { useAuthCookie } from '~~/lib/auth/composables/auth'
import { BlobUploadStatus } from '~~/lib/core/api/blobStorage'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { graphql } from '~/lib/common/generated/gql'
import type {
UseFileImport_ModelFragment,
UseFileImport_ProjectFragment
} from '~/lib/common/generated/gql/graphql'
graphql(`
fragment UseFileImport_Project on Project {
id
}
`)
graphql(`
fragment UseFileImport_Model on Model {
id
name
}
`)
export function useFileImport(params: {
projectId: MaybeRef<string>
project: MaybeRef<UseFileImport_ProjectFragment>
model?: MaybeRef<MaybeNullOrUndefined<UseFileImport_ModelFragment>>
/**
* Sometimes we don't have a model, but we still want to specify a target model name (e.g. for
* model list view uploads, where list items don't necessarily represent real models)
*/
modelName?: MaybeRef<MaybeNullOrUndefined<string>>
}) {
const { projectId, modelName } = params
const { project, model } = params
const { maxSizeInBytes } = useServerFileUploadLimit()
const authToken = useAuthCookie()
@@ -25,6 +48,8 @@ export function useFileImport(params: {
const upload = ref(null as Nullable<UploadFileItem>)
const isUploading = ref(false)
const modelName = computed(() => unref(params.modelName) || unref(model)?.name)
let onFileUploadedCb: Optional<(file: UploadFileItem) => void> = undefined
const onFileUploaded = (cb: (file: UploadFileItem) => void) => {
onFileUploadedCb = cb
@@ -58,8 +83,8 @@ export function useFileImport(params: {
const res = await importFile(
{
file: upload.value.file,
projectId: unref(projectId),
modelName: unref(modelName) || undefined,
projectId: unref(project).id,
modelName: modelName.value || undefined,
authToken: authToken.value,
apiOrigin
},
@@ -75,7 +100,7 @@ export function useFileImport(params: {
mp.track('Upload Action', {
type: 'action',
name: 'create',
source: unref(modelName) ? 'model card' : 'empty card'
source: modelName.value ? 'model card' : 'empty card'
// extension
})
@@ -52,6 +52,7 @@ export const projectDashboardItemFragment = graphql(`
fragment ProjectDashboardItem on Project {
id
...ProjectDashboardItemNoModels
...ProjectCardImportFileArea_Project
models(limit: 4) {
totalCount
items {
@@ -105,6 +106,7 @@ export const projectPageLatestItemsModelItemFragment = graphql(`
...ProjectPageModelsCardRenameDialog
...ProjectPageModelsCardDeleteDialog
...ProjectPageModelsActions
...ProjectCardImportFileArea_Model
automationsStatus {
...AutomateRunsTriggerStatus_TriggeredAutomationsStatus
}
@@ -162,6 +164,7 @@ export const projectPageLatestItemsModelsFragment = graphql(`
totalCount
}
...ProjectPageModelsStructureItem_Project
...ProjectCardImportFileArea_Project
}
`)
@@ -64,6 +64,7 @@ input WorkspaceUpdateInput {
@deprecated(reason: "Always the reviewer role. Will be removed in the future.")
domainBasedMembershipProtectionEnabled: Boolean
discoverabilityEnabled: Boolean
discoverabilityAutoJoinEnabled: Boolean
}
input WorkspaceRoleUpdateInput {
@@ -306,6 +307,10 @@ type Workspace {
"""
discoverabilityEnabled: Boolean!
"""
If true, allow users to automatically join discoverable workspaces (instead of requesting to join)
"""
discoverabilityAutoJoinEnabled: Boolean!
"""
Info about the workspace creation state
"""
creationState: WorkspaceCreationState
@@ -390,6 +395,10 @@ type LimitedWorkspace {
"""
logo: String
"""
If true, the users with a matching domain may join the workspace directly
"""
discoverabilityAutoJoinEnabled: Boolean!
"""
Workspace members visible to people with verified email domain
"""
team(cursor: String, limit: Int! = 25): LimitedWorkspaceCollaboratorCollection
@@ -1224,6 +1224,8 @@ export type LimitedWorkspace = {
adminTeam: Array<LimitedWorkspaceCollaborator>;
/** Workspace description */
description?: Maybe<Scalars['String']['output']>;
/** If true, the users with a matching domain may join the workspace directly */
discoverabilityAutoJoinEnabled: Scalars['Boolean']['output'];
/** Workspace id */
id: Scalars['ID']['output'];
/** Optional base64 encoded workspace logo image */
@@ -4503,6 +4505,8 @@ export type Workspace = {
*/
defaultRegion?: Maybe<ServerRegionItem>;
description?: Maybe<Scalars['String']['output']>;
/** If true, allow users to automatically join discoverable workspaces (instead of requesting to join) */
discoverabilityAutoJoinEnabled: Scalars['Boolean']['output'];
/** Enable/Disable discovery of the workspace */
discoverabilityEnabled: Scalars['Boolean']['output'];
/** Enable/Disable restriction to invite users to workspace as Guests only */
@@ -5144,6 +5148,7 @@ export type WorkspaceUpdateInput = {
/** @deprecated Always the reviewer role. Will be removed in the future. */
defaultProjectRole?: InputMaybe<Scalars['String']['input']>;
description?: InputMaybe<Scalars['String']['input']>;
discoverabilityAutoJoinEnabled?: InputMaybe<Scalars['Boolean']['input']>;
discoverabilityEnabled?: InputMaybe<Scalars['Boolean']['input']>;
domainBasedMembershipProtectionEnabled?: InputMaybe<Scalars['Boolean']['input']>;
id: Scalars['String']['input'];
@@ -6431,6 +6436,7 @@ export type LimitedUserResolvers<ContextType = GraphQLContext, ParentType extend
export type LimitedWorkspaceResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['LimitedWorkspace'] = ResolversParentTypes['LimitedWorkspace']> = {
adminTeam?: Resolver<Array<ResolversTypes['LimitedWorkspaceCollaborator']>, ParentType, ContextType>;
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
discoverabilityAutoJoinEnabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
logo?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -7466,6 +7472,7 @@ export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends
defaultProjectRole?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
defaultRegion?: Resolver<Maybe<ResolversTypes['ServerRegionItem']>, ParentType, ContextType>;
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
discoverabilityAutoJoinEnabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
discoverabilityEnabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
domainBasedMembershipProtectionEnabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
domains?: Resolver<Maybe<Array<ResolversTypes['WorkspaceDomain']>>, ParentType, ContextType>;
@@ -1,5 +1,8 @@
import crs from 'crypto-random-string'
/**
* Returns a random email from example.org domain
*/
export function createRandomEmail() {
return randomizeCase(`${crs({ length: 6 })}@example.org`)
}
@@ -1204,6 +1204,8 @@ export type LimitedWorkspace = {
adminTeam: Array<LimitedWorkspaceCollaborator>;
/** Workspace description */
description?: Maybe<Scalars['String']['output']>;
/** If true, the users with a matching domain may join the workspace directly */
discoverabilityAutoJoinEnabled: Scalars['Boolean']['output'];
/** Workspace id */
id: Scalars['ID']['output'];
/** Optional base64 encoded workspace logo image */
@@ -4483,6 +4485,8 @@ export type Workspace = {
*/
defaultRegion?: Maybe<ServerRegionItem>;
description?: Maybe<Scalars['String']['output']>;
/** If true, allow users to automatically join discoverable workspaces (instead of requesting to join) */
discoverabilityAutoJoinEnabled: Scalars['Boolean']['output'];
/** Enable/Disable discovery of the workspace */
discoverabilityEnabled: Scalars['Boolean']['output'];
/** Enable/Disable restriction to invite users to workspace as Guests only */
@@ -5124,6 +5128,7 @@ export type WorkspaceUpdateInput = {
/** @deprecated Always the reviewer role. Will be removed in the future. */
defaultProjectRole?: InputMaybe<Scalars['String']['input']>;
description?: InputMaybe<Scalars['String']['input']>;
discoverabilityAutoJoinEnabled?: InputMaybe<Scalars['Boolean']['input']>;
discoverabilityEnabled?: InputMaybe<Scalars['Boolean']['input']>;
domainBasedMembershipProtectionEnabled?: InputMaybe<Scalars['Boolean']['input']>;
id: Scalars['String']['input'];
@@ -6,7 +6,6 @@ import {
WorkspaceDomain
} from '@/modules/workspacesCore/domain/types'
import { Roles, WorkspaceRoles } from '@speckle/shared'
import { pick } from 'lodash'
export const userEmailsCompliantWithWorkspaceDomains = ({
userEmails,
@@ -45,5 +44,12 @@ export const isWorkspaceRole = (role: string): role is WorkspaceRoles => {
}
export const toLimitedWorkspace = (workspace: Workspace): LimitedWorkspace => {
return pick(workspace, ['id', 'slug', 'name', 'description', 'logo'])
return {
id: workspace.id,
slug: workspace.slug,
name: workspace.name,
description: workspace.description,
logo: workspace.logo,
discoverabilityAutoJoinEnabled: workspace.discoverabilityAutoJoinEnabled
}
}
@@ -1,5 +1,6 @@
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
import {
LimitedWorkspace,
Workspace,
WorkspaceAcl,
WorkspaceDomain,
@@ -45,7 +46,7 @@ export type UpsertWorkspace = (args: UpsertWorkspaceArgs) => Promise<void>
export type GetUserDiscoverableWorkspaces = (args: {
domains: string[]
userId: string
}) => Promise<Pick<Workspace, 'id' | 'name' | 'slug' | 'description' | 'logo'>[]>
}) => Promise<LimitedWorkspace[]>
export type GetWorkspace = (args: {
workspaceId: string
@@ -1136,7 +1136,20 @@ export = FF_WORKSPACES_MODULE_ENABLED
sendWorkspaceJoinRequestReceivedEmail,
getUserById: getUserFactory({ db }),
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
getUserEmails: findEmailsByUserIdFactory({ db })
getUserEmails: findEmailsByUserIdFactory({ db }),
addOrUpdateWorkspaceRole: addOrUpdateWorkspaceRoleFactory({
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
emitWorkspaceEvent: getEventBus().emit,
ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }),
eventEmit: getEventBus().emit
})
}),
getWorkspaceTeam: getWorkspaceCollaboratorsFactory({ db })
})
}
})
@@ -1,4 +1,5 @@
import {
LimitedWorkspace,
Workspace,
WorkspaceAcl,
WorkspaceDomain,
@@ -102,6 +103,7 @@ export const getUserDiscoverableWorkspacesFactory =
'slug',
'description',
'logo',
'discoverabilityAutoJoinEnabled',
tables
.workspacesAcl(db)
.select(knex.raw('count(*)::integer'))
@@ -129,10 +131,10 @@ export const getUserDiscoverableWorkspacesFactory =
.where('discoverabilityEnabled', true)
.where('verified', true)
.where('role', null)
.orderBy([{ column: 'teamCount', order: 'desc' }, 'workspaces.id'])) as Pick<
Workspace,
'id' | 'name' | 'slug' | 'description' | 'logo'
>[]
.orderBy([
{ column: 'teamCount', order: 'desc' },
'workspaces.id'
])) as LimitedWorkspace[]
return workspaces
}
@@ -300,6 +302,7 @@ export const upsertWorkspaceFactory =
'updatedAt',
'domainBasedMembershipProtectionEnabled',
'discoverabilityEnabled',
'discoverabilityAutoJoinEnabled',
'isEmbedSpeckleBrandingHidden'
])
}
@@ -164,8 +164,9 @@ export const createWorkspaceFactory =
updatedAt: new Date(),
domainBasedMembershipProtectionEnabled: false,
discoverabilityEnabled: false,
discoverabilityAutoJoinEnabled: false,
isEmbedSpeckleBrandingHidden: false
}
} satisfies Workspace
await upsertWorkspace({ workspace })
// assign the creator as workspace administrator
@@ -4,7 +4,7 @@ import {
GetWorkspaceRolesForUser,
GetWorkspaces
} from '@/modules/workspaces/domain/operations'
import { Workspace } from '@/modules/workspacesCore/domain/types'
import { LimitedWorkspace, Workspace } from '@/modules/workspacesCore/domain/types'
type GetDiscoverableWorkspaceForUserArgs = {
userId: string
@@ -20,9 +20,7 @@ export const getDiscoverableWorkspacesForUserFactory =
}) =>
async ({
userId
}: GetDiscoverableWorkspaceForUserArgs): Promise<
Pick<Workspace, 'id' | 'name' | 'slug' | 'description' | 'logo'>[]
> => {
}: GetDiscoverableWorkspaceForUserArgs): Promise<LimitedWorkspace[]> => {
const userEmails = await findEmailsByUserId({ userId })
const userVerifiedDomains = userEmails
.filter((email) => email.verified)
@@ -12,6 +12,7 @@ import {
CreateWorkspaceJoinRequest,
DenyWorkspaceJoinRequest,
GetWorkspace,
GetWorkspaceCollaborators,
GetWorkspaceJoinRequest,
GetWorkspaceWithDomains,
SendWorkspaceJoinRequestApprovedEmail,
@@ -50,14 +51,18 @@ export const requestToJoinWorkspaceFactory =
({
createWorkspaceJoinRequest,
sendWorkspaceJoinRequestReceivedEmail,
addOrUpdateWorkspaceRole,
getUserById,
getWorkspaceWithDomains,
getWorkspaceTeam,
getUserEmails
}: {
createWorkspaceJoinRequest: CreateWorkspaceJoinRequest
sendWorkspaceJoinRequestReceivedEmail: SendWorkspaceJoinRequestReceivedEmail
addOrUpdateWorkspaceRole: AddOrUpdateWorkspaceRole
getUserById: GetUser
getWorkspaceWithDomains: GetWorkspaceWithDomains
getWorkspaceTeam: GetWorkspaceCollaborators
getUserEmails: FindEmailsByUserId
}) =>
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
@@ -84,6 +89,23 @@ export const requestToJoinWorkspaceFactory =
throw new WorkspaceProtectedError()
}
if (workspace.discoverabilityAutoJoinEnabled) {
const [workspaceAdmin] = await getWorkspaceTeam({
workspaceId,
limit: 1,
filter: {
roles: [Roles.Workspace.Admin]
}
})
await addOrUpdateWorkspaceRole({
userId,
workspaceId,
role: Roles.Workspace.Member,
updatedByUserId: workspaceAdmin.id
})
return true
}
const joinRequest = await createWorkspaceJoinRequest({
workspaceJoinRequest: {
userId,
@@ -151,6 +151,7 @@ export type BasicTestWorkspace = {
description?: string
logo?: string
discoverabilityEnabled?: boolean
discoverabilityAutoJoinEnabled?: boolean
domainBasedMembershipProtectionEnabled?: boolean
}
@@ -326,13 +327,14 @@ export const createTestWorkspace = async (
emitWorkspaceEvent: (...args) => getEventBus().emit(...args)
})
if (workspace.discoverabilityEnabled) {
if (workspace.discoverabilityEnabled || workspace.discoverabilityAutoJoinEnabled) {
if (!domain) throw new Error('Domain is needed for discoverability')
await updateWorkspace({
workspaceId: newWorkspace.id,
workspaceInput: {
discoverabilityEnabled: true
discoverabilityEnabled: workspace.discoverabilityEnabled,
discoverabilityAutoJoinEnabled: workspace.discoverabilityAutoJoinEnabled
}
})
}
@@ -8,8 +8,15 @@ import {
WorkspaceNotDiscoverableError,
WorkspaceNotFoundError
} from '@/modules/workspaces/errors/workspace'
import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
import { UserWithOptionalRole } from '@/modules/core/repositories/users'
import {
getWorkspaceCollaboratorsFactory,
getWorkspaceFactory,
getWorkspaceRoleForUserFactory,
getWorkspaceRolesFactory,
getWorkspaceWithDomainsFactory,
upsertWorkspaceRoleFactory
} from '@/modules/workspaces/repositories/workspaces'
import { getUserFactory, UserWithOptionalRole } from '@/modules/core/repositories/users'
import {
AddOrUpdateWorkspaceRole,
CreateWorkspaceJoinRequest,
@@ -35,7 +42,7 @@ import {
} from '@/modules/workspacesCore/domain/types'
import { WorkspaceJoinRequests } from '@/modules/workspacesCore/helpers/db'
import { expectToThrow } from '@/test/assertionHelper'
import { BasicTestUser, createTestUser } from '@/test/authHelper'
import { BasicTestUser, createTestUser, createTestUsers } from '@/test/authHelper'
import { Roles } from '@speckle/shared'
import { expect } from 'chai'
import cryptoRandomString from 'crypto-random-string'
@@ -44,6 +51,17 @@ import {
updateWorkspaceJoinRequestStatusFactory
} from '@/modules/workspaces/repositories/workspaceJoinRequests'
import { UserEmail } from '@/modules/core/domain/userEmails/types'
import {
findEmailsByUserIdFactory,
findVerifiedEmailsByUserIdFactory
} from '@/modules/core/repositories/userEmails'
import { addOrUpdateWorkspaceRoleFactory } from '@/modules/workspaces/services/management'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { ensureValidWorkspaceRoleSeatFactory } from '@/modules/workspaces/services/workspaceSeat'
import {
createWorkspaceSeatFactory,
getWorkspaceUserSeatFactory
} from '@/modules/gatekeeper/repositories/workspaceSeat'
const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
@@ -109,7 +127,9 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(),
getUserById: async () => null,
getWorkspaceWithDomains: async () => null,
getUserEmails: async () => []
getUserEmails: async () => [],
addOrUpdateWorkspaceRole: async () => {},
getWorkspaceTeam: async () => []
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -124,7 +144,9 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
sendWorkspaceJoinRequestReceivedEmail: async () => Promise.resolve(),
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspaceWithDomains: async () => null,
getUserEmails: async () => []
getUserEmails: async () => [],
addOrUpdateWorkspaceRole: async () => {},
getWorkspaceTeam: async () => []
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -157,7 +179,9 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspaceWithDomains: async () =>
workspace as unknown as WorkspaceWithDomains,
getUserEmails: async () => []
getUserEmails: async () => [],
addOrUpdateWorkspaceRole: async () => {},
getWorkspaceTeam: async () => []
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -213,7 +237,9 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
domains: [domain]
} as unknown as WorkspaceWithDomains),
getUserEmails: async () =>
[{ email: user.email, verified: true }] as unknown as UserEmail[]
[{ email: user.email, verified: true }] as unknown as UserEmail[],
addOrUpdateWorkspaceRole: async () => {},
getWorkspaceTeam: async () => []
})({ workspaceId: workspace.id, userId: user.id })
).to.equal(true)
@@ -282,7 +308,9 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
domains: [domain]
} as unknown as WorkspaceWithDomains),
getUserEmails: async () =>
[{ email: user.email, verified: true }] as unknown as UserEmail[]
[{ email: user.email, verified: true }] as unknown as UserEmail[],
addOrUpdateWorkspaceRole: async () => {},
getWorkspaceTeam: async () => []
})
expect(
@@ -322,6 +350,64 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
expect(sendWorkspaceJoinRequestReceivedEmailCalls).to.have.length(1)
})
it('adds user to workspace if discoverable auto-join is enabled', async () => {
const userA: BasicTestUser = {
id: '',
name: 'John Speckle',
email: createRandomEmail(),
role: Roles.Server.Admin,
verified: true
}
const userB: BasicTestUser = {
id: '',
name: 'Jimothy Speckle',
email: createRandomEmail(),
verified: true
}
await createTestUsers([userA, userB])
const workspace: BasicTestWorkspace = {
id: '',
slug: '',
ownerId: '',
name: cryptoRandomString({ length: 6 }),
description: cryptoRandomString({ length: 12 }),
discoverabilityEnabled: true,
discoverabilityAutoJoinEnabled: true
}
await createTestWorkspace(workspace, userA, { domain: 'example.org' })
await requestToJoinWorkspaceFactory({
createWorkspaceJoinRequest: createWorkspaceJoinRequestFactory({ db }),
sendWorkspaceJoinRequestReceivedEmail: async () => {},
getUserById: getUserFactory({ db }),
addOrUpdateWorkspaceRole: addOrUpdateWorkspaceRoleFactory({
getWorkspaceRoles: getWorkspaceRolesFactory({ db }),
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
findVerifiedEmailsByUserId: findVerifiedEmailsByUserIdFactory({ db }),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
emitWorkspaceEvent: getEventBus().emit,
ensureValidWorkspaceRoleSeat: ensureValidWorkspaceRoleSeatFactory({
createWorkspaceSeat: createWorkspaceSeatFactory({ db }),
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }),
eventEmit: getEventBus().emit
})
}),
getWorkspaceWithDomains: getWorkspaceWithDomainsFactory({ db }),
getWorkspaceTeam: getWorkspaceCollaboratorsFactory({ db }),
getUserEmails: findEmailsByUserIdFactory({ db })
})({
userId: userB.id,
workspaceId: workspace.id
})
const workspaceRole = await getWorkspaceRoleForUserFactory({ db })({
userId: userB.id,
workspaceId: workspace.id
})
expect(workspaceRole).to.not.be.null
})
})
describe('approveWorkspaceJoinRequestFactory, returns a function that ', () => {
@@ -259,6 +259,7 @@ describe('Workspace services', () => {
updatedAt: new Date(),
logo: null,
discoverabilityEnabled: false,
discoverabilityAutoJoinEnabled: false,
domainBasedMembershipProtectionEnabled: false,
isEmbedSpeckleBrandingHidden: false,
domains: []
@@ -1140,6 +1141,7 @@ describe('Workspace role services', () => {
updatedAt: new Date(),
description: null,
discoverabilityEnabled: false,
discoverabilityAutoJoinEnabled: false,
domainBasedMembershipProtectionEnabled: false,
isEmbedSpeckleBrandingHidden: false
}
@@ -1180,6 +1182,7 @@ describe('Workspace role services', () => {
updatedAt: new Date(),
description: null,
discoverabilityEnabled: false,
discoverabilityAutoJoinEnabled: false,
domainBasedMembershipProtectionEnabled: false,
isEmbedSpeckleBrandingHidden: false
}
@@ -383,6 +383,7 @@ describe('Workspace SSO services', () => {
logo: null,
domainBasedMembershipProtectionEnabled: false,
discoverabilityEnabled: false,
discoverabilityAutoJoinEnabled: false,
isEmbedSpeckleBrandingHidden: false,
createdAt: new Date(),
updatedAt: new Date()
@@ -29,13 +29,14 @@ export type Workspace = {
logo: string | null
domainBasedMembershipProtectionEnabled: boolean
discoverabilityEnabled: boolean
// TODO: Create new table/structure if embeds get more configuration
discoverabilityAutoJoinEnabled: boolean
// TODO: Create new table/structure if embeds get more workspace-level configuration
isEmbedSpeckleBrandingHidden: boolean
}
export type LimitedWorkspace = Pick<
Workspace,
'id' | 'slug' | 'name' | 'description' | 'logo'
'id' | 'slug' | 'name' | 'description' | 'logo' | 'discoverabilityAutoJoinEnabled'
>
export type WorkspaceWithDomains = Workspace & { domains: WorkspaceDomain[] }
@@ -10,6 +10,7 @@ export const Workspaces = buildTableHelper('workspaces', [
'logo',
'domainBasedMembershipProtectionEnabled',
'discoverabilityEnabled',
'discoverabilityAutoJoinEnabled',
'isEmbedSpeckleBrandingHidden'
])
@@ -2,7 +2,11 @@ import { MutationsObjectGraphQLReturn } from '@/modules/core/helpers/graphTypes'
import { LimitedUserRecord } from '@/modules/core/helpers/types'
import { WorkspaceSsoProviderRecord } from '@/modules/workspaces/domain/sso/types'
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
import { Workspace, WorkspaceJoinRequest } from '@/modules/workspacesCore/domain/types'
import {
LimitedWorkspace,
Workspace,
WorkspaceJoinRequest
} from '@/modules/workspacesCore/domain/types'
import { WorkspaceRoles } from '@speckle/shared'
export type WorkspaceGraphQLReturn = Workspace
@@ -37,10 +41,7 @@ export type PendingWorkspaceCollaboratorGraphQLReturn = {
export type WorkspaceCollaboratorGraphQLReturn = WorkspaceTeamMember
export type LimitedWorkspaceGraphQLReturn = Pick<
Workspace,
'id' | 'name' | 'slug' | 'description' | 'logo'
>
export type LimitedWorkspaceGraphQLReturn = LimitedWorkspace
export type LimitedWorkspaceCollaboratorGraphQLReturn = WorkspaceTeamMember
export type WorkspacePermissionChecksGraphQLReturn = {
@@ -0,0 +1,13 @@
import { Knex } from 'knex'
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('workspaces', (table) => {
table.boolean('discoverabilityAutoJoinEnabled').notNullable().defaultTo(false)
})
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('workspaces', (table) => {
table.dropColumn('discoverabilityAutoJoinEnabled')
})
}
@@ -1205,6 +1205,8 @@ export type LimitedWorkspace = {
adminTeam: Array<LimitedWorkspaceCollaborator>;
/** Workspace description */
description?: Maybe<Scalars['String']['output']>;
/** If true, the users with a matching domain may join the workspace directly */
discoverabilityAutoJoinEnabled: Scalars['Boolean']['output'];
/** Workspace id */
id: Scalars['ID']['output'];
/** Optional base64 encoded workspace logo image */
@@ -4484,6 +4486,8 @@ export type Workspace = {
*/
defaultRegion?: Maybe<ServerRegionItem>;
description?: Maybe<Scalars['String']['output']>;
/** If true, allow users to automatically join discoverable workspaces (instead of requesting to join) */
discoverabilityAutoJoinEnabled: Scalars['Boolean']['output'];
/** Enable/Disable discovery of the workspace */
discoverabilityEnabled: Scalars['Boolean']['output'];
/** Enable/Disable restriction to invite users to workspace as Guests only */
@@ -5125,6 +5129,7 @@ export type WorkspaceUpdateInput = {
/** @deprecated Always the reviewer role. Will be removed in the future. */
defaultProjectRole?: InputMaybe<Scalars['String']['input']>;
description?: InputMaybe<Scalars['String']['input']>;
discoverabilityAutoJoinEnabled?: InputMaybe<Scalars['Boolean']['input']>;
discoverabilityEnabled?: InputMaybe<Scalars['Boolean']['input']>;
domainBasedMembershipProtectionEnabled?: InputMaybe<Scalars['Boolean']['input']>;
id: Scalars['String']['input'];
@@ -15,6 +15,7 @@ export const createAndStoreTestWorkspaceFactory =
logo: null,
domainBasedMembershipProtectionEnabled: false,
discoverabilityEnabled: false,
discoverabilityAutoJoinEnabled: false,
isEmbedSpeckleBrandingHidden: false,
...workspaceOverrides
}