Merge branch 'main' of github.com:specklesystems/speckle-server into alessandro/web-2488-create-email-verification-mutation

This commit is contained in:
Alessandro Magionami
2025-01-24 14:34:47 +01:00
41 changed files with 901 additions and 273 deletions
@@ -28,7 +28,6 @@
:size="buttonSize"
color="outline"
class="px-4"
:icon-left="CheckIcon"
:disabled="loading"
@click="onAcceptClick(token)"
>
@@ -52,7 +51,6 @@
<script setup lang="ts">
import type { MaybeNullOrUndefined, Optional } from '@speckle/shared'
import type { AvatarUserType } from '~/lib/user/composables/avatar'
import { CheckIcon } from '@heroicons/vue/24/solid'
import { usePostAuthRedirect } from '~/lib/auth/composables/postAuthRedirect'
import {
useNavigateToLogin,
@@ -125,7 +123,9 @@ const mainInfoBlockClasses = computed(() => {
const avatarSize = computed(() => (props.block ? 'xxl' : 'base'))
const buttonSize = computed(() => (props.block ? 'lg' : 'sm'))
const isForRegisteredUser = computed(() => !!props.invite.user?.id)
const acceptMessage = computed(() => (props.invite.workspace ? 'Join' : 'Accept'))
const acceptMessage = computed(() =>
props.invite.workspace ? 'Request to join' : 'Accept'
)
const declineMessage = computed(() => (props.invite.workspace ? 'Dismiss' : 'Decline'))
const onLoginSignupClick = async () => {
@@ -112,6 +112,8 @@ graphql(`
parameters
release {
id
versionTag
createdAt
inputSchema
function {
id
@@ -157,7 +159,9 @@ const mixpanel = useMixpanel()
const jsonForm = ref<{ triggerChange: () => Promise<Optional<JsonFormsChangeEvent>> }>()
const selectedModel = ref<CommonModelSelectorModelFragment>()
const selectedRelease = ref<SearchAutomateFunctionReleaseItemFragment>()
const selectedRelease = ref<SearchAutomateFunctionReleaseItemFragment | undefined>(
props.revisionFn?.release
)
const inputSchema = computed(() =>
formattedJsonFormSchema(selectedRelease.value?.inputSchema)
)
@@ -198,6 +202,7 @@ const resolveFirstModelValue = (items: SearchAutomateFunctionReleaseItemFragment
functionId: functionId.value,
functionVersionId: currentReleaseId.value
})
return props.revisionFn?.release
}
return modelValue
@@ -47,6 +47,7 @@ graphql(`
functions {
release {
id
inputSchema
function {
id
...AutomationsFunctionsCard_AutomateFunction
@@ -73,12 +74,12 @@ const props = defineProps<{
const dialogOpen = ref(false)
const dialogFunction = ref<Optional<EditableFunctionRevision>>()
const functionRevisions = computed(
const revisionFunctions = computed(
() => props.automation.currentRevision?.functions || []
)
const functions = computed(
() =>
functionRevisions.value.map((f) => ({
revisionFunctions.value.map((f) => ({
fn: f.release.function,
fnReleaseId: f.release.id
})) || []
@@ -86,11 +87,11 @@ const functions = computed(
const onEdit = (fn: EditableFunction) => {
const fid = fn.id
const revision = functionRevisions.value.find((f) => f.release.function.id === fid)
const revision = revisionFunctions.value.find((f) => f.release.function.id === fid)
if (revision) {
dialogOpen.value = true
dialogFunction.value = revision
dialogOpen.value = true
}
}
@@ -4,6 +4,7 @@
v-model:search="search"
search-placeholder="Search guests..."
:workspace="workspace"
show-invite-button
/>
<LayoutTable
class="mt-6 md:mt-8"
@@ -4,6 +4,7 @@
v-model:search="search"
search-placeholder="Search pending invites..."
:workspace="workspace"
show-invite-button
/>
<LayoutTable
class="mt-6 md:mt-8 mb-12"
@@ -0,0 +1,110 @@
<template>
<div>
<LayoutTable
class="mt-2 mb-12"
:columns="[
{ id: 'name', header: 'Name', classes: 'col-span-3' },
{ id: 'createdAt', header: 'Requested at', classes: 'col-span-3' },
{
id: 'actions',
header: '',
classes: 'col-span-3 lg:col-span-6 flex items-center justify-end'
}
]"
:items="joinRequests"
empty-message="There are no pending join requests"
>
<template #name="{ item }">
<div class="flex items-center gap-2">
<UserAvatar hide-tooltip :user="item.user" />
<span class="truncate text-body-xs text-foreground">
{{ item.user.name }}
</span>
</div>
</template>
<template #createdAt="{ item }">
<p class="text-body-xs text-foreground">
{{ formattedFullDate(item.createdAt) }}
</p>
</template>
<template #actions="{ item }">
<div class="flex items-center gap-x-2">
<FormButton color="outline" size="sm" @click="onApprove(item)">
Approve
</FormButton>
<FormButton color="outline" size="sm" @click="onDeny(item)">Deny</FormButton>
</div>
</template>
</LayoutTable>
<WorkspaceJoinRequestApproveDialog
v-model:open="showApproveJoinRequestDialog"
:join-request="requestToApprove"
/>
</div>
</template>
<script setup lang="ts">
import { type MaybeNullOrUndefined } from '@speckle/shared'
import type {
SettingsWorkspacesMembersRequestsTable_WorkspaceFragment,
WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment
} from '~~/lib/common/generated/gql/graphql'
import { graphql } from '~/lib/common/generated/gql'
import { useWorkspaceJoinRequest } from '~/lib/workspaces/composables/joinRequests'
graphql(`
fragment SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {
...SettingsWorkspacesMembersTableHeader_Workspace
id
adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {
totalCount
items {
...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest
id
createdAt
status
user {
id
avatar
name
}
}
}
}
`)
const props = defineProps<{
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersRequestsTable_WorkspaceFragment>
}>()
const { deny } = useWorkspaceJoinRequest()
const showApproveJoinRequestDialog = ref(false)
const requestToApprove =
ref<WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment>()
const joinRequests = computed(
() => props.workspace?.adminWorkspacesJoinRequests?.items || []
)
const onApprove = (
request: WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment
) => {
requestToApprove.value = request
showApproveJoinRequestDialog.value = true
}
const onDeny = async (
request: WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment
) => {
if (!props.workspace?.id) return
await deny(
{
workspaceId: props.workspace?.id,
userId: request.user.id
},
request.id
)
}
</script>
@@ -6,6 +6,7 @@
search-placeholder="Search members..."
:workspace="workspace"
show-role-filter
show-invite-button
/>
<LayoutTable
class="mt-6 md:mt-8 mb-12"
@@ -24,12 +24,14 @@
:hide-items="[Roles.Workspace.Guest]"
/>
</div>
<div v-if="!isWorkspaceAdmin" v-tippy="'You must be a workspace admin'">
<FormButton :disabled="!isWorkspaceAdmin">Invite</FormButton>
</div>
<FormButton v-else @click="() => (isInviteDialogOpen = !isInviteDialogOpen)">
Invite
</FormButton>
<template v-if="showInviteButton">
<div v-if="!isWorkspaceAdmin" v-tippy="'You must be a workspace admin'">
<FormButton :disabled="!isWorkspaceAdmin">Invite</FormButton>
</div>
<FormButton v-else @click="() => (isInviteDialogOpen = !isInviteDialogOpen)">
Invite
</FormButton>
</template>
</div>
<InviteDialogWorkspace
v-if="workspace"
@@ -57,6 +59,7 @@ const props = defineProps<{
searchPlaceholder: string
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersTableHeader_WorkspaceFragment>
showRoleFilter?: boolean
showInviteButton?: boolean
}>()
const search = defineModel<string>('search')
@@ -1,7 +1,7 @@
<template>
<InviteBanner :invite="invite" @processed="processJoin">
<InviteBanner :invite="invite" @processed="processRequest">
<template #message>
Your team is already using Workspaces! Collaborate in the
Your team is already using Workspaces, request to join the
<span class="font-medium">{{ workspace.name }}</span>
space!
</template>
@@ -9,21 +9,17 @@
</template>
<script setup lang="ts">
import { useApolloClient } from '@vue/apollo-composable'
import { useMutation } from '@vue/apollo-composable'
import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
import { graphql } from '~/lib/common/generated/gql'
import {
DashboardJoinWorkspaceDocument,
type WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import { type WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragment } from '~/lib/common/generated/gql/graphql'
import { CookieKeys } from '~/lib/common/helpers/constants'
import {
getCacheId,
getFirstErrorMessage,
modifyObjectField
} from '~/lib/common/helpers/graphql'
import { workspaceRoute } from '~/lib/common/helpers/route'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { dashboardRequestToJoinWorkspaceMutation } from '~~/lib/dashboard/graphql/mutations'
import {
convertThrowIntoFetchResult,
getFirstErrorMessage
} from '~~/lib/common/helpers/graphql'
graphql(`
fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {
@@ -33,27 +29,15 @@ graphql(`
description
logo
}
fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {
id
name
description
createdAt
updatedAt
logo
domainBasedMembershipProtectionEnabled
discoverabilityEnabled
}
`)
const props = defineProps<{
workspace: WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragment
}>()
const { mutate: requestToJoin } = useMutation(dashboardRequestToJoinWorkspaceMutation)
const mixpanel = useMixpanel()
const { client: apollo } = useApolloClient()
const { activeUser } = useActiveUser()
const { triggerNotification } = useGlobalToast()
const router = useRouter()
const dismissedDiscoverableWorkspaces = useSynchronizedCookie<string[]>(
CookieKeys.DismissedDiscoverableWorkspaces,
{
@@ -69,73 +53,55 @@ const invite = computed(() => ({
}
}))
const processJoin = async (accept: boolean) => {
if (!accept) {
const processRequest = async (accept: boolean) => {
if (accept) {
const result = await requestToJoin({
input: { workspaceId: props.workspace.id }
}).catch(convertThrowIntoFetchResult)
if (result?.data) {
// Dismiss it show it doesnt show again
dismissedDiscoverableWorkspaces.value = [
...dismissedDiscoverableWorkspaces.value,
props.workspace.id
]
mixpanel.track('Workspace Join Request Sent', {
workspaceId: props.workspace.id,
location: 'discovery banner',
// eslint-disable-next-line camelcase
workspace_id: props.workspace.id
})
triggerNotification({
title: 'Request sent',
description: 'Your request to join the workspace has been sent.',
type: ToastNotificationType.Success
})
} else {
const errorMessage = getFirstErrorMessage(result?.errors)
triggerNotification({
title: 'Failed to send request',
description: errorMessage,
type: ToastNotificationType.Danger
})
}
} else {
dismissedDiscoverableWorkspaces.value = [
...dismissedDiscoverableWorkspaces.value,
props.workspace.id
]
apollo.cache.evict({
id: getCacheId('LimitedWorkspace', props.workspace.id)
})
return
}
const userId = activeUser.value?.id
if (!userId) return
const result = await apollo
.mutate({
mutation: DashboardJoinWorkspaceDocument,
variables: {
input: {
workspaceId: props.workspace.id
}
},
update(cache, { data }) {
const workspaceId = data?.workspaceMutations.join.id
if (!workspaceId) return
modifyObjectField(
cache,
getCacheId('User', userId),
'workspaces',
({ variables, helpers: { evict, createUpdatedValue, ref } }) => {
if (variables.filter?.search?.length) return evict()
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount + 1)
update('items', (items) => [...items, ref('Workspace', workspaceId)])
})
}
)
}
})
.catch(convertThrowIntoFetchResult)
if (result?.data) {
apollo.cache.evict({
id: getCacheId('LimitedWorkspace', props.workspace.id)
})
triggerNotification({
type: ToastNotificationType.Success,
title: 'Joined workspace',
description: 'Successfully joined workspace'
})
mixpanel.track('Workspace Joined', {
mixpanel.track('Workspace Discovery Banner Dismissed', {
workspaceId: props.workspace.id,
location: 'discovery banner',
// eslint-disable-next-line camelcase
workspace_id: props.workspace.id
})
router.push(workspaceRoute(props.workspace.slug))
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to join workspace',
description: getFirstErrorMessage(result?.errors)
title: 'Discoverable workspace dismissed',
type: ToastNotificationType.Info
})
}
}
@@ -0,0 +1,72 @@
<template>
<LayoutDialog v-model:open="open" max-width="xs" :buttons="dialogButtons">
<template #header>Approve join request</template>
<p class="text-body-xs text-foreground">
Are you sure you want to approve
<span class="font-semibold">{{ joinRequest?.user.name }}'s</span>
join request?
</p>
<div class="text-body-2xs text-foreground-2 leading-5 mt-4 mb-1">
<p>
Adding users may add seats to your current billing cycle. If there are available
seats, they will be used first.
</p>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useWorkspaceJoinRequest } from '~/lib/workspaces/composables/joinRequests'
import { graphql } from '~/lib/common/generated/gql'
import type { WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment } from '~/lib/common/generated/gql/graphql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
graphql(`
fragment WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {
id
user {
id
name
}
workspace {
id
}
}
`)
const props = defineProps<{
joinRequest: MaybeNullOrUndefined<WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragment>
}>()
const open = defineModel<boolean>('open', { required: true })
const { approve } = useWorkspaceJoinRequest()
const dialogButtons = computed((): LayoutDialogButton[] => {
return [
{
text: 'Cancel',
props: { color: 'outline' },
onClick: () => {
open.value = false
}
},
{
text: 'Approve',
onClick: async () => {
if (props.joinRequest?.workspace.id && props.joinRequest?.user.id) {
await approve(
{
workspaceId: props.joinRequest.workspace.id,
userId: props.joinRequest.user.id
},
props.joinRequest.id
)
open.value = false
}
}
}
]
})
</script>
@@ -61,9 +61,9 @@ const documents = {
"\n fragment ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n }\n": types.ProjectModelPageVersionsCardVersionFragmentDoc,
"\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n workspace {\n id\n slug\n name\n logo\n }\n }\n": types.ProjectPageProjectHeaderFragmentDoc,
"\n fragment ProjectPageInviteDialog_Project on Project {\n id\n workspaceId\n workspace {\n id\n defaultProjectRole\n team {\n items {\n role\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n }\n ...ProjectPageTeamInternals_Project\n workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": types.ProjectPageInviteDialog_ProjectFragmentDoc,
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n inputSchema\n function {\n id\n }\n }\n }\n": types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunctionFragmentDoc,
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n versionTag\n createdAt\n inputSchema\n function {\n id\n }\n }\n }\n": types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunctionFragmentDoc,
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevision on AutomationRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n type\n model {\n id\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFragmentDoc,
"\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n": types.ProjectPageAutomationFunctions_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n inputSchema\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n": types.ProjectPageAutomationFunctions_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationHeader_Automation on Automation {\n id\n name\n enabled\n isTestAutomation\n currentRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n model {\n ...ProjectPageLatestItemsModelItem\n }\n }\n }\n }\n }\n": types.ProjectPageAutomationHeader_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationHeader_Project on Project {\n id\n role\n workspaceId\n ...ProjectPageModelsCardProject\n }\n": types.ProjectPageAutomationHeader_ProjectFragmentDoc,
"\n fragment ProjectPageAutomationModels_Project on Project {\n id\n ...ProjectPageModelsCardProject\n }\n": types.ProjectPageAutomationModels_ProjectFragmentDoc,
@@ -125,6 +125,7 @@ const documents = {
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersChangeRoleDialog_Workspace\n team {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n inviteId\n role\n title\n updatedAt\n user {\n id\n ...LimitedUserAvatar\n }\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n }\n": types.SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n invitedTeam(filter: $invitesFilter) {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n": types.SettingsWorkspacesMembersInvitesTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {\n ...SettingsWorkspacesMembersTableHeader_Workspace\n id\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n items {\n ...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest\n id\n createdAt\n status\n user {\n id\n avatar\n name\n }\n }\n }\n }\n": types.SettingsWorkspacesMembersRequestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n user {\n id\n avatar\n name\n company\n verified\n workspaceDomainPolicyCompliant\n }\n }\n": types.SettingsWorkspacesMembersMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersMembersTable_Workspace on Workspace {\n id\n name\n ...SettingsSharedDeleteUserDialog_Workspace\n ...SettingsWorkspacesMembersTableHeader_Workspace\n ...SettingsWorkspacesMembersChangeRoleDialog_Workspace\n team {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersMembersTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...InviteDialogWorkspace_Workspace\n }\n": types.SettingsWorkspacesMembersTableHeader_WorkspaceFragmentDoc,
@@ -143,7 +144,8 @@ const documents = {
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceBase_Workspace\n ...WorkspaceTeam_Workspace\n ...BillingAlert_Workspace\n slug\n readOnly\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
"\n fragment WorkspaceInviteBanner_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBanner_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment WorkspaceInviteBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceName\n token\n user {\n id\n name\n ...LimitedUserAvatar\n }\n title\n email\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBlock_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": types.WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragmentDoc,
"\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n": types.WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspaceFragmentDoc,
"\n fragment WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {\n id\n user {\n id\n name\n }\n workspace {\n id\n }\n }\n": types.WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequestFragmentDoc,
"\n fragment WorkspaceSidebarAbout_Workspace on Workspace {\n ...WorkspaceDashboardAbout_Workspace\n }\n": types.WorkspaceSidebarAbout_WorkspaceFragmentDoc,
"\n fragment WorkspaceSidebarMembers_Workspace on Workspace {\n ...WorkspaceTeam_Workspace\n }\n": types.WorkspaceSidebarMembers_WorkspaceFragmentDoc,
"\n fragment WorkspaceSidebarSecurity_Workspace on Workspace {\n ...WorkspaceSecurity_Workspace\n }\n": types.WorkspaceSidebarSecurity_WorkspaceFragmentDoc,
@@ -179,7 +181,7 @@ const documents = {
"\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 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 }\n }\n": types.MainServerInfoDataDocument,
"\n mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n": types.DashboardJoinWorkspaceDocument,
"\n mutation DashboardRequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {\n workspaceMutations {\n requestToJoin(input: $input)\n }\n }\n": types.DashboardRequestToJoinWorkspaceDocument,
"\n query DashboardProjectsPageQuery {\n activeUser {\n id\n projects(limit: 3) {\n items {\n ...DashboardProjectCard_Project\n }\n }\n ...ProjectsDashboardHeaderProjects_User\n }\n }\n": types.DashboardProjectsPageQueryDocument,
"\n query DashboardProjectsPageWorkspaceQuery {\n activeUser {\n id\n ...ProjectsDashboardHeaderWorkspaces_User\n }\n }\n": types.DashboardProjectsPageWorkspaceQueryDocument,
"\n mutation DeleteAccessToken($token: String!) {\n apiTokenRevoke(token: $token)\n }\n": types.DeleteAccessTokenDocument,
@@ -306,8 +308,9 @@ const documents = {
"\n query SettingsWorkspaceBilling($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesBilling_Workspace\n }\n }\n": types.SettingsWorkspaceBillingDocument,
"\n query SettingsWorkspaceBillingCustomerPortal($workspaceId: String!) {\n workspace(id: $workspaceId) {\n customerPortalUrl\n }\n }\n": types.SettingsWorkspaceBillingCustomerPortalDocument,
"\n query SettingsWorkspaceRegions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n serverInfo {\n ...SettingsWorkspacesRegions_ServerInfo\n }\n }\n": types.SettingsWorkspaceRegionsDocument,
"\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersDocument,
"\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersDocument,
"\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter) {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n": types.SettingsWorkspacesMembersSearchDocument,
"\n query SettingsWorkspacesJoinRequestsSearch(\n $slug: String!\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n": types.SettingsWorkspacesJoinRequestsSearchDocument,
"\n query SettingsWorkspacesInvitesSearch(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesInvitesSearchDocument,
"\n query SettingsUserEmailsQuery {\n activeUser {\n ...SettingsUserEmails_User\n }\n }\n": types.SettingsUserEmailsQueryDocument,
"\n query SettingsWorkspacesProjects(\n $slug: String!\n $limit: Int!\n $cursor: String\n $filter: WorkspaceProjectsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n slug\n readOnly\n projects(limit: $limit, cursor: $cursor, filter: $filter) {\n cursor\n ...SettingsWorkspacesProjects_ProjectCollection\n }\n }\n }\n": types.SettingsWorkspacesProjectsDocument,
@@ -357,6 +360,8 @@ const documents = {
"\n mutation SetWorkspaceCreationState($input: WorkspaceCreationStateInput!) {\n workspaceMutations {\n updateCreationState(input: $input)\n }\n }\n": types.SetWorkspaceCreationStateDocument,
"\n mutation WorkspaceUpdateDomainProtectionMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n domainBasedMembershipProtectionEnabled\n }\n }\n }\n": types.WorkspaceUpdateDomainProtectionMutationDocument,
"\n mutation WorkspaceUpdateDiscoverabilityMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityEnabled\n }\n }\n }\n": types.WorkspaceUpdateDiscoverabilityMutationDocument,
"\n mutation ApproveWorkspaceJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n approve(input: $input)\n }\n }\n": types.ApproveWorkspaceJoinRequestDocument,
"\n mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n deny(input: $input)\n }\n }\n": types.DenyWorkspaceJoinRequestDocument,
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n": types.WorkspaceAccessCheckDocument,
"\n query WorkspacePageQuery(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $token: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspaceProjectList_Workspace\n }\n workspaceInvite(\n workspaceId: $workspaceSlug\n token: $token\n options: { useSlug: true }\n ) {\n id\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n ...WorkspaceInviteBlock_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspacePageQueryDocument,
"\n query WorkspaceProjectsQuery(\n $workspaceSlug: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n }\n": types.WorkspaceProjectsQueryDocument,
@@ -386,7 +391,7 @@ const documents = {
"\n fragment SettingsUserEmails_User on User {\n id\n emails {\n ...SettingsUserEmailCards_UserEmail\n }\n }\n": types.SettingsUserEmails_UserFragmentDoc,
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n ...BillingAlert_Workspace\n id\n role\n plan {\n name\n status\n createdAt\n paymentMethod\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n seats {\n guest\n plan\n }\n }\n team {\n items {\n id\n role\n }\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n defaultProjectRole\n plan {\n status\n name\n }\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n }\n": types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n }\n }\n": types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsWorkspacesProjects_ProjectCollectionFragmentDoc,
"\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,
@@ -598,7 +603,7 @@ export function graphql(source: "\n fragment ProjectPageInviteDialog_Project on
/**
* 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 ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n inputSchema\n function {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n inputSchema\n function {\n id\n }\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n versionTag\n createdAt\n inputSchema\n function {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n versionTag\n createdAt\n inputSchema\n function {\n id\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -606,7 +611,7 @@ export function graphql(source: "\n fragment ProjectPageAutomationFunctionSetti
/**
* 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 ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n inputSchema\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n inputSchema\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -851,6 +856,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembersInvitesTa
* 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 SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n invitedTeam(filter: $invitesFilter) {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n invitedTeam(filter: $invitesFilter) {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\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 SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {\n ...SettingsWorkspacesMembersTableHeader_Workspace\n id\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n items {\n ...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest\n id\n createdAt\n status\n user {\n id\n avatar\n name\n }\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {\n ...SettingsWorkspacesMembersTableHeader_Workspace\n id\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n items {\n ...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest\n id\n createdAt\n status\n user {\n id\n avatar\n name\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -926,7 +935,11 @@ export function graphql(source: "\n fragment WorkspaceInviteBlock_PendingWorksp
/**
* 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 WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"];
export function graphql(source: "\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_LimitedWorkspace on LimitedWorkspace {\n id\n name\n slug\n description\n logo\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 WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {\n id\n user {\n id\n name\n }\n workspace {\n id\n }\n }\n"): (typeof documents)["\n fragment WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest on WorkspaceJoinRequest {\n id\n user {\n id\n name\n }\n workspace {\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1070,7 +1083,7 @@ export function graphql(source: "\n query MainServerInfoData {\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 mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n"): (typeof documents)["\n mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {\n workspaceMutations {\n join(input: $input) {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace\n }\n }\n }\n"];
export function graphql(source: "\n mutation DashboardRequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {\n workspaceMutations {\n requestToJoin(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DashboardRequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {\n workspaceMutations {\n requestToJoin(input: $input)\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1578,11 +1591,15 @@ export function graphql(source: "\n query SettingsWorkspaceRegions($slug: Strin
/**
* 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 SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n"];
export function graphql(source: "\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesMembers(\n $slug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n ...SettingsWorkspacesMembersMembersTable_Workspace\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n ...SettingsWorkspacesMembersRequestsTable_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 SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter) {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter) {\n items {\n id\n ...SettingsWorkspacesMembersMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsWorkspacesJoinRequestsSearch(\n $slug: String!\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesJoinRequestsSearch(\n $slug: String!\n $joinRequestsFilter: AdminWorkspaceJoinRequestFilter\n ) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1779,6 +1796,14 @@ export function graphql(source: "\n mutation WorkspaceUpdateDomainProtectionMut
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation WorkspaceUpdateDiscoverabilityMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityEnabled\n }\n }\n }\n"): (typeof documents)["\n mutation WorkspaceUpdateDiscoverabilityMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityEnabled\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation ApproveWorkspaceJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n approve(input: $input)\n }\n }\n"): (typeof documents)["\n mutation ApproveWorkspaceJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n approve(input: $input)\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n deny(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n deny(input: $input)\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1898,7 +1923,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesGeneral_Workspac
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n role\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n }\n"];
export function graphql(source: "\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n team {\n items {\n id\n }\n }\n invitedTeam(filter: $invitesFilter) {\n user {\n id\n }\n }\n adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {\n totalCount\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -1,11 +1,9 @@
import { graphql } from '~~/lib/common/generated/gql'
export const dashboardJoinWorkspaceMutation = graphql(`
mutation DashboardJoinWorkspace($input: JoinWorkspaceInput!) {
export const dashboardRequestToJoinWorkspaceMutation = graphql(`
mutation DashboardRequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {
workspaceMutations {
join(input: $input) {
...WorkspaceInviteDiscoverableWorkspaceBanner_Workspace
}
requestToJoin(input: $input)
}
}
`)
@@ -57,12 +57,14 @@ export const settingsWorkspacesMembersQuery = graphql(`
query SettingsWorkspacesMembers(
$slug: String!
$invitesFilter: PendingWorkspaceCollaboratorsFilter
$joinRequestsFilter: AdminWorkspaceJoinRequestFilter
) {
workspaceBySlug(slug: $slug) {
...SettingsWorkspacesMembers_Workspace
...SettingsWorkspacesMembersMembersTable_Workspace
...SettingsWorkspacesMembersGuestsTable_Workspace
...SettingsWorkspacesMembersInvitesTable_Workspace
...SettingsWorkspacesMembersRequestsTable_Workspace
}
}
`)
@@ -81,6 +83,18 @@ export const settingsWorkspacesMembersSearchQuery = graphql(`
}
`)
export const settingsWorkspacesJoinRequestsSearchQuery = graphql(`
query SettingsWorkspacesJoinRequestsSearch(
$slug: String!
$joinRequestsFilter: AdminWorkspaceJoinRequestFilter
) {
workspaceBySlug(slug: $slug) {
id
...SettingsWorkspacesMembersRequestsTable_Workspace
}
}
`)
export const settingsWorkspacesInvitesSearchQuery = graphql(`
query SettingsWorkspacesInvitesSearch(
$slug: String!
@@ -0,0 +1,109 @@
import { useMutation } from '@vue/apollo-composable'
import {
approveWorkspaceJoinRequestMutation,
denyWorkspaceJoinRequestMutation
} from '~/lib/workspaces/graphql/mutations'
import type {
ApproveWorkspaceJoinRequestInput,
DenyWorkspaceJoinRequestInput
} from '~~/lib/common/generated/gql/graphql'
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
import {
convertThrowIntoFetchResult,
getFirstErrorMessage,
modifyObjectField,
getCacheId
} from '~~/lib/common/helpers/graphql'
export const useWorkspaceJoinRequest = () => {
const { mutate: approveMutation } = useMutation(approveWorkspaceJoinRequestMutation)
const { mutate: denyMutation } = useMutation(denyWorkspaceJoinRequestMutation)
const { triggerNotification } = useGlobalToast()
const approve = async (
input: ApproveWorkspaceJoinRequestInput,
requestId: string
) => {
const result = await approveMutation(
{ input },
{
update: (cache) => {
cache.evict({
id: getCacheId('WorkspaceJoinRequest', requestId)
})
modifyObjectField(
cache,
getCacheId('Workspace', input.workspaceId),
'adminWorkspacesJoinRequests',
({ helpers: { createUpdatedValue } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount - 1)
})
},
{
autoEvictFiltered: true
}
)
}
}
).catch(convertThrowIntoFetchResult)
if (result?.data) {
triggerNotification({
type: ToastNotificationType.Success,
title: 'Workspace join request approved'
})
} else {
const errorMessage = getFirstErrorMessage(result?.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Workspace join request approval failed',
description: errorMessage
})
}
}
const deny = async (input: DenyWorkspaceJoinRequestInput, requestId: string) => {
const result = await denyMutation(
{ input },
{
update: (cache) => {
cache.evict({
id: getCacheId('WorkspaceJoinRequest', requestId)
})
modifyObjectField(
cache,
getCacheId('Workspace', input.workspaceId),
'adminWorkspacesJoinRequests',
({ helpers: { createUpdatedValue } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount - 1)
})
},
{
autoEvictFiltered: true
}
)
}
}
).catch(convertThrowIntoFetchResult)
if (result?.data) {
triggerNotification({
type: ToastNotificationType.Success,
title: 'Workspace join request denied'
})
} else {
const errorMessage = getFirstErrorMessage(result?.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Workspace join request denial failed',
description: errorMessage
})
}
}
return { approve, deny }
}
@@ -105,3 +105,19 @@ export const workspaceUpdateDiscoverabilityMutation = graphql(`
}
}
`)
export const approveWorkspaceJoinRequestMutation = graphql(`
mutation ApproveWorkspaceJoinRequest($input: ApproveWorkspaceJoinRequestInput!) {
workspaceJoinRequestMutations {
approve(input: $input)
}
}
`)
export const denyWorkspaceJoinRequestMutation = graphql(`
mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {
workspaceJoinRequestMutations {
deny(input: $input)
}
}
`)
@@ -24,6 +24,11 @@
:workspace="workspace"
:workspace-slug="slug"
/>
<SettingsWorkspacesMembersJoinRequestsTable
v-if="activeItem.id === 'joinRequests'"
:workspace="workspace"
:workspace-slug="slug"
/>
</template>
</LayoutTabsHorizontal>
</div>
@@ -36,6 +41,8 @@ import { useQuery } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import { settingsWorkspacesMembersQuery } from '~/lib/settings/graphql/queries'
import type { LayoutPageTabItem } from '~~/lib/layout/helpers/components'
import { useOnWorkspaceUpdated } from '~/lib/workspaces/composables/management'
import { WorkspaceJoinRequestStatus } from '~/lib/common/generated/gql/graphql'
graphql(`
fragment SettingsWorkspacesMembers_Workspace on Workspace {
@@ -44,7 +51,6 @@ graphql(`
team {
items {
id
role
}
}
invitedTeam(filter: $invitesFilter) {
@@ -52,6 +58,9 @@ graphql(`
id
}
}
adminWorkspacesJoinRequests(filter: $joinRequestsFilter) {
totalCount
}
}
`)
@@ -63,11 +72,14 @@ useHead({
title: 'Settings | Workspace - Members'
})
const route = useRoute()
const slug = computed(() => (route.params.slug as string) || '')
const route = useRoute()
const { result } = useQuery(settingsWorkspacesMembersQuery, () => ({
slug: slug.value
slug: slug.value,
joinRequestsFilter: {
status: WorkspaceJoinRequestStatus.Pending
}
}))
const workspace = computed(() => result.value?.workspaceBySlug)
@@ -83,6 +95,9 @@ const guestCount = computed(
.length
)
const invitedCount = computed(() => workspace.value?.invitedTeam?.length)
const joinRequestCount = computed(
() => workspace.value?.adminWorkspacesJoinRequests?.totalCount
)
const tabItems = computed<LayoutPageTabItem[]>(() => [
{ title: 'Members', id: 'members', count: memberCount.value },
{ title: 'Guests', id: 'guests', count: guestCount.value },
@@ -92,8 +107,17 @@ const tabItems = computed<LayoutPageTabItem[]>(() => [
disabled: !isAdmin.value,
disabledMessage: 'Only workspace admins can manage invites',
count: invitedCount.value
},
{
title: 'Join requests',
id: 'joinRequests',
disabled: !isAdmin.value,
disabledMessage: 'Only workspace admins can manage join requests',
count: joinRequestCount.value
}
])
const activeTab = ref(tabItems.value[0])
useOnWorkspaceUpdated({ workspaceSlug: slug })
</script>
@@ -123,7 +123,6 @@ import type { ShallowRef } from 'vue'
import { useQuery, useMutation } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import type { SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment } from '~/lib/common/generated/gql/graphql'
import { getFirstErrorMessage } from '~/lib/common/helpers/graphql'
import { settingsWorkspacesSecurityQuery } from '~/lib/settings/graphql/queries'
import { useAddWorkspaceDomain } from '~/lib/settings/composables/management'
import { useMixpanel } from '~/lib/core/composables/mp'
@@ -170,7 +169,6 @@ const slug = computed(() => (route.params.slug as string) || '')
const route = useRoute()
const addWorkspaceDomain = useAddWorkspaceDomain()
const { triggerNotification } = useGlobalToast()
const isSsoEnabled = useIsWorkspacesSsoEnabled()
const mixpanel = useMixpanel()
const { mutate: updateDomainProtection } = useMutation(
@@ -226,17 +224,6 @@ const isDomainProtectionEnabled = computed({
// eslint-disable-next-line camelcase
workspace_id: workspace.value?.id
})
triggerNotification({
type: ToastNotificationType.Success,
title: `Domain protection ${newVal ? 'enabled' : 'disabled'}`
})
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to update',
description: getFirstErrorMessage(result?.errors)
})
}
}
})
@@ -259,17 +246,6 @@ const isDomainDiscoverabilityEnabled = computed({
// eslint-disable-next-line camelcase
workspace_id: workspace.value?.id
})
triggerNotification({
type: ToastNotificationType.Success,
title: `Discoverability ${newVal ? 'enabled' : 'disabled'}`
})
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to update',
description: getFirstErrorMessage(result?.errors)
})
}
}
})
@@ -1,8 +1,17 @@
import prometheusClient from 'prom-client'
import { join } from 'lodash-es'
import type { MetricInitializer } from '@/observability/types.js'
import Environment from '@speckle/shared/dist/commonjs/environment/index.js'
const { FF_WORKSPACES_MULTI_REGION_ENABLED } = Environment.getFeatureFlags()
export const init: MetricInitializer = (config) => {
if (!FF_WORKSPACES_MULTI_REGION_ENABLED) {
return async () => {
// Do nothing
}
}
const { labelNames, namePrefix, logger } = config
const promMetric = new prometheusClient.Gauge({
name: join([namePrefix, 'db_replication_slot_lag'], '_'),
+1 -1
View File
@@ -462,8 +462,8 @@ export async function init() {
// Log errors
app.use(errorLoggingMiddleware)
app.use(createRateLimiterMiddleware()) // Rate limiting by IP address for all users
app.use(authContextMiddleware)
app.use(createRateLimiterMiddleware())
app.use(
async (
_req: express.Request,
@@ -51,7 +51,7 @@ const getRandomModelVersion = async (offset?: number) => {
const mocks: SpeckleModuleMocksConfig = FF_AUTOMATE_MODULE_ENABLED
? {
resolvers: ({ store }) => ({
resolvers: ({ store, helpers: { getMockRef, resolveAndCache } }) => ({
AutomationRevisionTriggerDefinition: {
__resolveType: () => 'VersionCreatedTriggerDefinition'
},
@@ -246,14 +246,10 @@ const mocks: SpeckleModuleMocksConfig = FF_AUTOMATE_MODULE_ENABLED
}
},
AutomateFunction: {
// creator: async (_parent, args, ctx) => {
// const rand = faker.datatype.boolean()
// const activeUser = ctx.userId
// ? await ctx.loaders.users.getUser.load(ctx.userId)
// : null
// return rand ? (store.get('LimitedUser') as any) : activeUser
// }
creator: resolveAndCache((parent, args, ctx) => {
const rand = faker.datatype.boolean()
return getMockRef('LimitedUser', { id: !rand ? ctx.userId : undefined })
}),
releases: () => store.get('AutomateFunctionReleaseCollection') as any
},
AutomateFunctionRelease: {
@@ -338,30 +334,39 @@ const mocks: SpeckleModuleMocksConfig = FF_AUTOMATE_MODULE_ENABLED
},
commitId: () => '0c259d384a4df3cce3f24667560e5124e68f202f',
inputSchema: () => {
// random fro 1 to 3
const rand = faker.number.int({ min: 1, max: 3 })
switch (rand) {
case 1:
return {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://example.com/product.schema.json',
title: 'Product',
description: "A product from Acme's catalog",
type: 'object',
properties: {
name: {
desciption: 'Random name',
type: 'string'
},
productId: {
description: 'The unique identifier for a product',
type: 'integer'
}
},
required: ['productId']
return {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://example.com/product.schema.json',
title: 'Product',
description: "A product from Acme's catalog",
type: 'object',
properties: {
Boolean: {
description: faker.lorem.sentence(5),
type: 'boolean'
},
'Required Boolean': {
description: faker.lorem.sentence(5),
type: 'boolean'
},
Integer: {
description: faker.lorem.sentence(5),
type: 'integer'
},
'Required Integer': {
description: faker.lorem.sentence(5),
type: 'integer'
},
String: {
description: faker.lorem.sentence(5),
type: 'string'
},
'Required String': {
description: faker.lorem.sentence(5),
type: 'string'
}
default:
return null
},
required: ['Required Boolean', 'Required Integer', 'Required String']
}
}
}),
@@ -107,37 +107,44 @@ export const loggingPluginFactory: (deps: {
apollo_query_duration_ms: Date.now() - apolloRequestStart
})
for (const err of ctx.errors) {
const operationName = ctx.request.operationName || null
const query = ctx.request.query
const variables = redactSensitiveVariables(ctx.request.variables)
const operationName = ctx.request.operationName || null
const query = ctx.request.query
const variables = redactSensitiveVariables(ctx.request.variables)
const reqCtx = getRequestContext()
if (reqCtx) {
logger = logger.child({
dbMetrics: reqCtx.dbMetrics
})
}
const reqCtx = getRequestContext()
if (reqCtx) {
logger = logger.child({
dbMetrics: reqCtx.dbMetrics
})
}
if (err.path) {
logger = logger.child({
'query-path': err.path.join(' > '),
graphql_operation_name: operationName,
graphql_query: query,
graphql_variables: variables
})
}
if (shouldLogAsInfoLevel(err)) {
logger.info(
{ err },
'{graphql_operation_title} failed after {apollo_query_duration_ms} ms'
)
} else {
logger.error(
err,
'{graphql_operation_title} failed after {apollo_query_duration_ms} ms'
)
}
const importantError = ctx.errors.find((err) => !shouldLogAsInfoLevel(err))
const firstError = ctx.errors[0]
const loggableError = importantError || firstError
logger = logger.child({
error_count: loggableError ? ctx.errors.length : undefined,
first_error: loggableError
? {
message: loggableError.message,
path: loggableError.path?.join(' > ')
}
: {},
graphql_operation_name: operationName,
graphql_query: query,
graphql_variables: variables
})
if (!importantError) {
logger.info(
{ err: firstError },
'{graphql_operation_title} failed after {apollo_query_duration_ms} ms'
)
} else {
logger.error(
{ err: importantError },
'{graphql_operation_title} failed after {apollo_query_duration_ms} ms'
)
}
},
willSendResponse: async (ctx) => {
@@ -306,8 +306,20 @@ export = {
}
},
Branch: {
async commits(parent, args) {
async commits(parent, args, ctx) {
const projectDB = await getProjectDbClient({ projectId: parent.streamId })
// If limit=0 & no filter, short-cut full execution and use data loader
if (args.limit === 0) {
return {
totalCount: await ctx.loaders
.forRegion({ db: projectDB })
.branches.getCommitCount.load(parent.id),
items: [],
cursor: null
}
}
const getPaginatedBranchCommits = getPaginatedBranchCommitsFactory({
getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDB }),
getPaginatedBranchCommitsItems: getPaginatedBranchCommitsItemsFactory({
@@ -206,6 +206,8 @@ export = {
},
async streams(_, args, ctx) {
const countOnly = args.limit === 0 && !args.query
const [totalCount, visibleCount, { cursor, streams }] = await Promise.all([
getUserStreamsCount({
userId: ctx.userId!,
@@ -220,15 +222,17 @@ export = {
streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules),
onlyWithActiveSsoSession: true
}),
getUserStreams({
userId: ctx.userId!,
limit: args.limit,
cursor: args.cursor || undefined,
searchQuery: args.query || undefined,
forOtherUser: false,
streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules),
onlyWithActiveSsoSession: true
})
!countOnly
? getUserStreams({
userId: ctx.userId!,
limit: args.limit,
cursor: args.cursor || undefined,
searchQuery: args.query || undefined,
forOtherUser: false,
streamIdWhitelist: toProjectIdWhitelist(ctx.resourceAccessRules),
onlyWithActiveSsoSession: true
})
: { cursor: null, streams: [] }
])
return {
@@ -17,6 +17,7 @@ import { RateLimitError } from '@/modules/core/errors/ratelimit'
import { rateLimiterLogger } from '@/logging/logging'
import { createRedisClient } from '@/modules/shared/redis/redis'
import { getRequestPath } from '@/modules/core/helpers/server'
import { getTokenFromRequest } from '@/modules/shared/middleware'
export interface RateLimitResult {
isWithinLimits: boolean
@@ -297,7 +298,10 @@ export const getActionForPath = (path: string, verb: string): RateLimitAction =>
}
export const getSourceFromRequest = (req: express.Request): string => {
let source: string | null = req?.context?.userId || getIpFromRequest(req)
let source: string | null =
req?.context?.userId ||
getTokenFromRequest(req)?.substring(10) || // token ID
getIpFromRequest(req)
if (!source) source = 'unknown'
return source
@@ -5,7 +5,6 @@ const TABLE_NAME = 'email_verifications'
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TABLE_NAME, (table) => {
table.string('code')
table.unique('email')
})
}
@@ -0,0 +1,11 @@
import { Knex } from 'knex'
const TABLE_NAME = 'email_verifications'
export async function up(knex: Knex): Promise<void> {
await knex.raw(
`ALTER TABLE ${TABLE_NAME} DROP CONSTRAINT IF EXISTS email_verifications_email_unique`
)
}
export async function down(): Promise<void> {}
@@ -6,6 +6,7 @@ import { getUserFactory } from '@/modules/core/repositories/users'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { commandFactory } from '@/modules/shared/command'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { getPaginatedItemsFactory } from '@/modules/shared/services/paginatedItems'
import {
ApproveWorkspaceJoinRequest,
@@ -30,6 +31,8 @@ import {
import { WorkspaceJoinRequestStatus } from '@/modules/workspacesCore/domain/types'
import { WorkspaceJoinRequestGraphQLReturn } from '@/modules/workspacesCore/helpers/graphTypes'
const eventBus = getEventBus()
export default {
Workspace: {
adminWorkspacesJoinRequests: async (parent, args, ctx) => {
@@ -78,7 +81,8 @@ export default {
approve: async (_parent, args) => {
const approveWorkspaceJoinRequest = commandFactory<ApproveWorkspaceJoinRequest>({
db,
operationFactory: ({ db }) => {
eventBus,
operationFactory: ({ db, emit }) => {
const updateWorkspaceJoinRequestStatus =
updateWorkspaceJoinRequestStatusFactory({
db
@@ -98,7 +102,8 @@ export default {
getWorkspaceJoinRequest: getWorkspaceJoinRequestFactory({
db
}),
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db })
upsertWorkspaceRole: upsertWorkspaceRoleFactory({ db }),
emit
})
}
})
@@ -1423,12 +1423,12 @@ export = FF_WORKSPACES_MODULE_ENABLED
context.resourceAccessRules
)
const userId = parent.id
return await getWorkspaceRoleForUserFactory({ db })({
userId,
const role = await getWorkspaceRoleForUserFactory({ db })({
userId: parent.id,
workspaceId
})
return role?.role ?? null
}
},
ServerInfo: {
@@ -21,6 +21,8 @@ import {
import { Roles } from '@speckle/shared'
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
import { EventBus } from '@/modules/shared/services/eventBus'
import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
export const dismissWorkspaceJoinRequestFactory =
({
@@ -104,7 +106,8 @@ export const approveWorkspaceJoinRequestFactory =
getUserById,
getWorkspace,
getWorkspaceJoinRequest,
upsertWorkspaceRole
upsertWorkspaceRole,
emit
}: {
updateWorkspaceJoinRequestStatus: UpdateWorkspaceJoinRequestStatus
sendWorkspaceJoinRequestApprovedEmail: SendWorkspaceJoinRequestApprovedEmail
@@ -112,6 +115,7 @@ export const approveWorkspaceJoinRequestFactory =
getWorkspace: GetWorkspace
getWorkspaceJoinRequest: GetWorkspaceJoinRequest
upsertWorkspaceRole: UpsertWorkspaceRole
emit: EventBus['emit']
}) =>
async ({ userId, workspaceId }: { userId: string; workspaceId: string }) => {
const requester = await getUserById(userId)
@@ -142,6 +146,8 @@ export const approveWorkspaceJoinRequestFactory =
const role = Roles.Workspace.Member
await upsertWorkspaceRole({ userId, workspaceId, role, createdAt: new Date() })
await emit({ eventName: WorkspaceEvents.Updated, payload: { workspace } })
await sendWorkspaceJoinRequestApprovedEmail({
workspace,
requester
@@ -245,7 +245,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => null,
getWorkspace: async () => null,
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
upsertWorkspaceRole: async () => Promise.resolve(),
emit: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -261,7 +262,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => null,
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
upsertWorkspaceRole: async () => Promise.resolve(),
emit: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -285,7 +287,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => workspace as unknown as Workspace,
getWorkspaceJoinRequest: async () => undefined,
upsertWorkspaceRole: async () => Promise.resolve()
upsertWorkspaceRole: async () => Promise.resolve(),
emit: async () => Promise.resolve()
})({ workspaceId: createRandomString(), userId: createRandomString() })
)
@@ -343,7 +346,8 @@ const { FF_WORKSPACES_MODULE_ENABLED } = getFeatureFlags()
getUserById: async () => user as unknown as UserWithOptionalRole,
getWorkspace: async () => workspace as unknown as Workspace,
getWorkspaceJoinRequest: async () => request,
upsertWorkspaceRole
upsertWorkspaceRole,
emit: async () => Promise.resolve()
})({ workspaceId: workspace.id, userId: user.id })
).to.equal(true)
+4 -1
View File
@@ -113,7 +113,7 @@ const getStream = () => {
// prettier-ignore
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8?c=%5B-7.66134,10.82932,6.41935,-0.07739,-13.88552,1.8697,0,1%5D'
// Revit sample house (good for bim-like stuff with many display meshes)
'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
// 'https://latest.speckle.systems/streams/c1faab5c62/commits/ab1a1ab2b6'
// 'https://app.speckle.systems/streams/da9e320dad/commits/5388ef24b8'
// 'https://latest.speckle.systems/streams/58b5648c4d/commits/60371ecb2d'
@@ -470,6 +470,9 @@ const getStream = () => {
// 'https://latest.speckle.systems/projects/3fe1880c36/models/65bb4287a8'
// 'https://latest.speckle.systems/projects/db06488e1c/models/21f3930771'
// FAR OFF
'https://app.speckle.systems/projects/bdd828221e/models/eb99326dc3'
)
}
@@ -127,6 +127,29 @@ export const speckleBasicVert = /* glsl */ `
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
#if defined(BILLBOARD) || defined(BILLBOARD_FIXED)
@@ -165,7 +188,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);
@@ -83,6 +83,30 @@ varying vec2 vHighPrecisionZW;
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
#ifdef USE_RTE
@@ -161,7 +185,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);
@@ -79,6 +79,29 @@ uniform float displacement;
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
#ifdef USE_RTE
@@ -159,7 +182,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);
@@ -77,6 +77,28 @@ export const speckleGhostVert = /* glsl */ `
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
#ifdef USE_RTE
@@ -157,7 +179,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);
@@ -129,6 +129,29 @@ export const speckleNormalVert = /* glsl */ `
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
void main() {
@@ -156,7 +179,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);
@@ -91,6 +91,29 @@ varying vec3 vViewPosition;
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
attribute float gradientIndex;
@@ -176,7 +199,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);
@@ -224,7 +247,7 @@ void main() {
#endif
#ifdef TRANSFORM_STORAGE
vec4 rtePivotShadow = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uShadowViewer_low, uShadowViewer_high);
shadowPosition.xyz = rotate_vertex_position((shadowPosition - rtePivotShadow).xyz, tQuaternion) * tScale.xyz + rtePivotShadow.xyz + tTranslation.xyz;
shadowPosition.xyz = rotate_vertex_position_delta(shadowPosition, rtePivotShadow, tQuaternion) * tScale.xyz + rtePivotShadow.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 rtePivotShadow = computeRelativePositionSeparate(ZERO3, ZERO3, uShadowViewer_low, uShadowViewer_high);
@@ -87,10 +87,34 @@ varying vec3 vViewPosition;
#endif
}
vec3 rotate_vertex_position(vec3 position, vec4 quat)
{
highp vec3 rotate_vertex_position(highp vec3 position, highp vec4 quat)
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
#ifdef USE_RTE
@@ -143,8 +167,6 @@ varying vec3 vViewPosition;
}
#endif
void main() {
#include <uv_vertex>
@@ -165,7 +187,7 @@ void main() {
//#include <project_vertex> // EDITED CHUNK
#ifdef TRANSFORM_STORAGE
vec4 tQuaternion, tPivotLow, tPivotHigh, tTranslation, tScale;
highp vec4 tQuaternion, tPivotLow, tPivotHigh, tTranslation, tScale;
objectTransform(tQuaternion, tPivotLow, tPivotHigh, tTranslation, tScale);
#endif
#ifdef USE_RTE
@@ -176,7 +198,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);
@@ -223,7 +245,7 @@ void main() {
#endif
#ifdef TRANSFORM_STORAGE
vec4 rtePivotShadow = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uShadowViewer_low, uShadowViewer_high);
shadowPosition.xyz = rotate_vertex_position((shadowPosition - rtePivotShadow).xyz, tQuaternion) * tScale.xyz + rtePivotShadow.xyz + tTranslation.xyz;
shadowPosition.xyz = rotate_vertex_position_delta(shadowPosition, rtePivotShadow, tQuaternion) * tScale.xyz + rtePivotShadow.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 rtePivotShadow = computeRelativePositionSeparate(ZERO3, ZERO3, uShadowViewer_low, uShadowViewer_high);
@@ -127,6 +127,29 @@ export const speckleTextVert = /* glsl */ `
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
#if defined(BILLBOARD) || defined(BILLBOARD_FIXED)
@@ -165,7 +188,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);
@@ -130,6 +130,29 @@ varying vec3 vViewPosition;
{
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
}
highp vec3 rotate_vertex_position_delta(highp vec4 v0, highp vec4 v1, highp vec4 quat)
{
/** !!! WORKAROUND FOR Intel IrisXe CARDS !!! */
/** The code below will not produce correct results in intel IrisXE integrated GPUs.
* The geometry will turn mangled, albeit stable
* I can't know for sure what is going on, but rotating the difference seems to
* force the result into a lower precision?
*/
// highp vec4 position = v0 - v1;
// return position.xyz + 2.0 * cross(quat.xyz, cross(quat.xyz, position.xyz) + quat.w * position.xyz);
/** Subtracting the rotated vectors works. */
return rotate_vertex_position(v0.xyz, quat) - rotate_vertex_position(v1.xyz, quat);
/** An alternate workaround is
* highp vec3 position = (v0.xyz * (1. + 1e-7)) - (v1.xyz * (1. _ 1e-7));
return position + 2.0 * cross(quat.xyz, cross(quat.xyz, position) + quat.w * position);
However I'm not such a fan of the (1. + 1e-7) part
*/
}
#endif
#if defined(BILLBOARD) || defined(BILLBOARD_FIXED)
@@ -169,7 +192,7 @@ void main() {
vec4 rteLocalPosition = computeRelativePositionSeparate(position_lowT.xyz, position_highT.xyz, uViewer_low, uViewer_high);
#ifdef TRANSFORM_STORAGE
vec4 rtePivot = computeRelativePositionSeparate(tPivotLow.xyz, tPivotHigh.xyz, uViewer_low, uViewer_high);
rteLocalPosition.xyz = rotate_vertex_position((rteLocalPosition - rtePivot).xyz, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
rteLocalPosition.xyz = rotate_vertex_position_delta(rteLocalPosition, rtePivot, tQuaternion) * tScale.xyz + rtePivot.xyz + tTranslation.xyz;
#endif
#ifdef USE_INSTANCING
vec4 instancePivot = computeRelativePositionSeparate(ZERO3, ZERO3, uViewer_low, uViewer_high);