Merge main

This commit is contained in:
andrewwallacespeckle
2025-09-01 17:00:16 +01:00
227 changed files with 7401 additions and 5665 deletions
-7
View File
@@ -1,7 +0,0 @@
{
"compilerOptions": {
"target": "es2021",
"module": "commonJS"
},
"exclude": ["node_modules"]
}
+3
View File
@@ -40,6 +40,9 @@ NUXT_PUBLIC_INTERCOM_APP_ID=
# Enable Autodesk construction cloud integration
NUXT_PUBLIC_FF_ACC_INTEGRATION_ENABLED=false
# Local or remote URL for dashboards
NUXT_PUBLIC_DASHBOARDS_ORIGIN=http://localhost:8083
##########################################################
# Local dev settings
##########################################################
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

@@ -14,13 +14,7 @@
show-label
:disabled="isEmailDisabled"
auto-focus
:help="
emailIsBlocked
? 'A work email makes it easier to discover and collaborate with your coworkers on Speckle.'
: ''
"
autocomplete="email"
@blur="onEmailChange"
/>
<FormTextInput
type="text"
@@ -75,11 +69,13 @@ import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables
import { ensureError } from '@speckle/shared'
import { useAuthManager } from '~~/lib/auth/composables/auth'
import { loginRoute } from '~~/lib/common/helpers/route'
import { passwordRules } from '~~/lib/auth/helpers/validation'
import { graphql } from '~~/lib/common/generated/gql'
import type { ServerTermsOfServicePrivacyPolicyFragmentFragment } from '~~/lib/common/generated/gql/graphql'
import { useMounted } from '@vueuse/core'
import { checkIfEmailIsBlocked } from '~~/lib/auth/helpers/checkBlockedDomain'
import {
passwordRules,
doesNotContainBlockedDomain
} from '~~/lib/auth/helpers/validation'
graphql(`
fragment ServerTermsOfServicePrivacyPolicyFragment on ServerInfo {
@@ -100,15 +96,18 @@ const router = useRouter()
const { signUpWithEmail, inviteToken } = useAuthManager()
const { triggerNotification } = useGlobalToast()
const isMounted = useMounted()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const isNoPersonalEmailsEnabled = useIsNoPersonalEmailsEnabled()
const newsletterConsent = defineModel<boolean>('newsletterConsent', { required: true })
const loading = ref(false)
const password = ref('')
const email = ref('')
const emailIsBlocked = ref(false)
const emailRules = [isEmail]
const emailRules = computed(() =>
inviteToken.value || !isNoPersonalEmailsEnabled.value
? [isEmail]
: [isEmail, doesNotContainBlockedDomain]
)
const nameRules = [isRequired]
const isEmailDisabled = computed(() => !!props.inviteEmail?.length || loading.value)
@@ -121,11 +120,6 @@ const finalLoginRoute = computed(() => {
return result.fullPath
})
const onEmailChange = () => {
if (!isWorkspacesEnabled.value) return
emailIsBlocked.value = checkIfEmailIsBlocked(email.value)
}
const onSubmit = handleSubmit(async (fullUser) => {
try {
loading.value = true
@@ -1,6 +1,6 @@
<template>
<LayoutDialog v-model:open="open" max-width="xs" :buttons="dialogButtons">
<template #header>Discard changes?</template>
<template #header>{{ title ?? 'Discard changes?' }}</template>
<p v-if="text" class="mb-2">{{ text }}</p>
<p v-else class="mb-2">You have unsaved changes. Are you sure you want to leave?</p>
</LayoutDialog>
@@ -8,8 +8,10 @@
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
defineProps<{
const props = defineProps<{
title?: string
text?: string
confirmText?: string
}>()
const emit = defineEmits(['confirm'])
@@ -26,7 +28,7 @@ const dialogButtons = computed((): LayoutDialogButton[] => {
}
},
{
text: 'Continue',
text: props.confirmText ?? 'Confirm',
onClick: () => {
open.value = false
emit('confirm')
@@ -0,0 +1,66 @@
<template>
<CommonCard class="relative !px-2 !py-2.5 bg-foundation shadow-sm">
<FormButton
class="absolute top-1 right-1"
size="sm"
color="subtle"
:icon-right="XMarkIcon"
hide-text
@click="dismissBanner"
>
<span class="sr-only">Close</span>
</FormButton>
<div class="flex flex-col gap-y-2 text-foreground">
<span class="text-[10px] font-mono uppercase tracking-widest">
Upcoming event
</span>
<h3 class="text-body-xs font-semibold leading-tight tracking-tight">
Speckle Intelligence Live
</h3>
<p v-if="dateIsSetp9" class="text-body-3xs leading-tight">
Community StandUp happening tomorrow!
</p>
<p v-else class="text-body-3xs leading-tight">
Tune into our Community
<br />
StandUp - Sept 10.
</p>
<NuxtLink
to="https://streamyard.com/watch/RhTZBgkzRcRe"
target="_blank"
external
class="flex gap-1 items-center border-b border-transparent hover:border-highlight-3 max-w-max -mb-0.5"
@click="onCTAClick"
>
<span class="text-body-3xs font-semibold">Register</span>
<ArrowUpRightIcon class="h-2 w-2 mt-px stroke-2 stroke-foreground" />
</NuxtLink>
</div>
</CommonCard>
</template>
<script setup lang="ts">
import { ArrowUpRightIcon, XMarkIcon } from '@heroicons/vue/24/solid'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useActiveUserMeta } from '~~/lib/user/composables/meta'
import dayjs from 'dayjs'
const mixpanel = useMixpanel()
const { updateIntelligenceCommunityStandUpBannerDismissed } = useActiveUserMeta()
const onCTAClick = () => {
mixpanel.track('Intelligence Community StandUp CTA Clicked')
}
const dateIsSetp9 = computed(() => {
return dayjs().isSame('2025-09-09', 'day')
})
const dismissBanner = async () => {
await updateIntelligenceCommunityStandUpBannerDismissed(true)
}
onMounted(() => {
mixpanel.track('Intelligence Community StandUp Banner Shown')
})
</script>
@@ -40,7 +40,7 @@
<div class="flex flex-col gap-y-2 lg:gap-y-4">
<LayoutSidebarMenuGroup>
<NuxtLink
v-if="showProjectsLink"
v-if="showWorkspaceLinks"
:to="projectsLink"
@click="isOpenMobile = false"
>
@@ -56,6 +56,21 @@
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<NuxtLink
v-if="showWorkspaceLinks && canListDashboards"
:to="dashboardsRoute(activeWorkspaceSlug)"
@click="isOpenMobile = false"
>
<LayoutSidebarMenuGroupItem
label="Intelligence"
:active="isActive(dashboardsRoute(activeWorkspaceSlug))"
>
<template #icon>
<IconProjects class="size-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<NuxtLink :to="connectorsRoute" @click="isOpenMobile = false">
<LayoutSidebarMenuGroupItem
label="Connectors"
@@ -142,6 +157,9 @@
</LayoutSidebarMenuGroup>
</div>
</LayoutSidebarMenu>
<template v-if="showIntelligenceCommunityStandUpPromo" #promo>
<DashboardIntelligencePromo />
</template>
</LayoutSidebar>
</div>
</template>
@@ -160,7 +178,8 @@ import {
connectorsRoute,
workspaceRoute,
tutorialsRoute,
docsPageUrl
docsPageUrl,
dashboardsRoute
} from '~/lib/common/helpers/route'
import { useRoute } from 'vue-router'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
@@ -168,6 +187,8 @@ import { useMixpanel } from '~~/lib/core/composables/mp'
import { useActiveWorkspaceSlug } from '~/lib/user/composables/activeWorkspace'
import { graphql } from '~/lib/common/generated/gql'
import { useQuery } from '@vue/apollo-composable'
import dayjs from 'dayjs'
import { useActiveUserMeta } from '~/lib/user/composables/meta'
const dashboardSidebarQuery = graphql(`
query DashboardSidebar {
@@ -181,21 +202,53 @@ const dashboardSidebarQuery = graphql(`
}
`)
const sidebarPermissionsQuery = graphql(`
query SidebarPermissions($slug: String!) {
workspaceBySlug(slug: $slug) {
permissions {
canListDashboards {
...FullPermissionCheckResult
}
}
}
}
`)
const { isLoggedIn } = useActiveUser()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const isDashboardsEnabled = useIsDashboardsModuleEnabled()
const route = useRoute()
const activeWorkspaceSlug = useActiveWorkspaceSlug()
const { $intercom } = useNuxtApp()
const mixpanel = useMixpanel()
const { result: permissionsResult } = useQuery(
sidebarPermissionsQuery,
() => ({
slug: activeWorkspaceSlug.value || ''
}),
() => ({
enabled: isDashboardsEnabled.value && !!activeWorkspaceSlug.value
})
)
const { result } = useQuery(dashboardSidebarQuery, () => ({}), {
enabled: isWorkspacesEnabled.value
})
const { hasDismissedIntelligenceCommunityStandUpBanner } = useActiveUserMeta()
const isOpenMobile = ref(false)
const showExplainerVideoDialog = ref(false)
const showIntelligenceCommunityStandUpPromo = computed(() => {
if (hasDismissedIntelligenceCommunityStandUpBanner.value) return false
return dayjs().isBefore('2025-09-10', 'day')
})
const activeWorkspace = computed(() => result.value?.activeUser?.activeWorkspace)
const showProjectsLink = computed(() => {
const canListDashboards = computed(() => {
return permissionsResult.value?.workspaceBySlug?.permissions?.canListDashboards
?.authorized
})
const showWorkspaceLinks = computed(() => {
return isWorkspacesEnabled.value
? activeWorkspace.value
? !!activeWorkspace.value?.role
@@ -0,0 +1,103 @@
<template>
<div>
<CommonCard
class="bg-foundation cursor-pointer"
@click="navigateTo(dashboardRoute(activeWorkspaceSlug, dashboard.id))"
>
<div class="flex justify-between items-center gap-x-2">
<div>
<h1 class="break-words text-heading line-clamp-2">
{{ dashboard.name }}
</h1>
<span class="text-body-3xs text-foreground-2 select-none">
{{ updatedAt.full }}
</span>
</div>
<div class="flex items-center gap-x-2">
<UserAvatar
v-if="dashboard.createdBy"
:user="dashboard.createdBy"
size="sm"
/>
<Trash
v-tippy="canDelete ? undefined : 'You can only delete your own dashboards'"
class="size-3.5"
:class="
canDelete
? 'cursor-pointer text-foreground-2 hover:text-foreground'
: 'cursor-not-allowed text-foreground-3'
"
@click.stop="toggleDeleteDialog"
/>
</div>
</div>
</CommonCard>
<CommonConfirmDialog
v-model:open="isDeleteDialogOpen"
title="Delete dashboard"
text="Are you sure you want to delete this dashboard? This action cannot be undone."
confirm-text="Delete"
@confirm="handleDelete"
/>
</div>
</template>
<script setup lang="ts">
import { graphql } from '~~/lib/common/generated/gql'
import type { DashboardsCard_DashboardFragment } from '~~/lib/common/generated/gql/graphql'
import { dashboardRoute } from '~/lib/common/helpers/route'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { Trash } from 'lucide-vue-next'
import { useDeleteDashboard } from '~/lib/dashboards/composables/management'
graphql(`
fragment DashboardsCard_Dashboard on Dashboard {
id
name
createdAt
workspace {
id
}
createdBy {
id
name
avatar
}
permissions {
canDelete {
...FullPermissionCheckResult
}
}
}
`)
const props = defineProps<{
dashboard: DashboardsCard_DashboardFragment
activeWorkspaceSlug: MaybeNullOrUndefined<string>
}>()
const deleteDashboard = useDeleteDashboard()
const { formattedFullDate } = useDateFormatters()
const isDeleteDialogOpen = ref(false)
const updatedAt = computed(() => {
return {
full: formattedFullDate(props.dashboard.createdAt)
}
})
const canDelete = computed(() => {
return props.dashboard.permissions?.canDelete?.authorized
})
const toggleDeleteDialog = () => {
isDeleteDialogOpen.value = !isDeleteDialogOpen.value
}
const handleDelete = async () => {
if (!canDelete.value || !props.dashboard.id) return
await deleteDashboard(props.dashboard.id, props.dashboard.workspace.id)
}
</script>
@@ -0,0 +1,70 @@
<template>
<LayoutDialog
v-model:open="open"
title="Create new dashboard"
:buttons="dialogButtons"
:on-submit="onSubmit"
max-width="xs"
>
<FormTextInput
v-model="dashboardName"
name="name"
label="Dashboard name"
placeholder="Name"
color="foundation"
:rules="[isRequired, isStringOfLength({ maxLength: 512 })]"
auto-focus
autocomplete="off"
show-label
class="mb-2"
/>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useCreateDashboard } from '~/lib/dashboards/composables/management'
const props = defineProps<{
workspaceSlug?: MaybeNullOrUndefined<string>
}>()
const open = defineModel<boolean>('open', { required: true })
const createDashboard = useCreateDashboard()
const dashboardName = ref('')
watch(open, (newValue, oldValue) => {
if (newValue && !oldValue) {
dashboardName.value = ''
}
})
const dialogButtons = computed((): LayoutDialogButton[] => [
{
text: 'Cancel',
props: { color: 'outline' },
onClick: () => (open.value = false)
},
{
text: 'Create',
props: {
submit: true
},
onClick: () => {
open.value = false
}
}
])
const onSubmit = async () => {
await createDashboard({
identifier: { slug: props.workspaceSlug },
input: { name: dashboardName.value }
})
open.value = false
}
</script>
@@ -0,0 +1,85 @@
<template>
<LayoutDialog
v-model:open="open"
title="Edit dashboard"
:buttons="dialogButtons"
:on-submit="onSubmit"
max-width="xs"
>
<FormTextInput
v-model="dashboardName"
name="name"
label="Dashboard name"
placeholder="Name"
color="foundation"
:rules="[isRequired, isStringOfLength({ maxLength: 512 })]"
auto-focus
autocomplete="off"
show-label
class="mb-2"
/>
</LayoutDialog>
</template>
<script setup lang="ts">
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useUpdateDashboard } from '~/lib/dashboards/composables/management'
import { graphql } from '~~/lib/common/generated/gql'
import type { DashboardsEditDialog_DashboardFragment } from '~~/lib/common/generated/gql/graphql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
graphql(`
fragment DashboardsEditDialog_Dashboard on Dashboard {
id
name
workspace {
id
}
}
`)
const props = defineProps<{
dashboard: MaybeNullOrUndefined<DashboardsEditDialog_DashboardFragment>
}>()
const open = defineModel<boolean>('open', { required: true })
const updateDashboard = useUpdateDashboard()
const dashboardName = ref()
watch(open, (newValue, oldValue) => {
if (newValue && !oldValue) {
dashboardName.value = props.dashboard?.name || ''
}
})
const dialogButtons = computed((): LayoutDialogButton[] => [
{
text: 'Cancel',
props: { color: 'outline' },
onClick: () => (open.value = false)
},
{
text: 'Create',
props: {
submit: true
}
}
])
const onSubmit = async () => {
if (!props.dashboard || !props.dashboard.id || !props.dashboard.workspace.id) return
await updateDashboard(
{
id: props.dashboard.id,
name: dashboardName.value
},
props.dashboard.workspace.id
)
open.value = false
}
</script>
@@ -0,0 +1,105 @@
<template>
<div class="flex flex-col gap-y-6">
<section class="flex items-center gap-2 justify-between">
<h1 class="text-heading-sm md:text-heading">Dashboards</h1>
<FormButton color="outline" @click="showCreateDashboardDialog = true">
Add dashboard
</FormButton>
</section>
<div
v-if="!isVeryFirstLoading && !result?.workspaceBySlug?.dashboards.items.length"
class="flex flex-col items-center justify-center gap-y-4 mx-auto my-14"
>
<h2 class="text-heading-sm text-foreground-2">
This workspace has no dashboards yet
</h2>
<FormButton
v-if="canCreateDashboards"
color="outline"
@click="showCreateDashboardDialog = true"
>
Add dashboard
</FormButton>
</div>
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div
v-for="dashboard in result?.workspaceBySlug?.dashboards.items"
:key="dashboard.id"
>
<DashboardsCard :dashboard="dashboard" :active-workspace-slug="workspaceSlug" />
</div>
</div>
<InfiniteLoading :settings="{ identifier }" @infinite="onInfiniteLoad" />
<DashboardsCreateDialog
v-model:open="showCreateDashboardDialog"
:workspace-slug="workspaceSlug"
/>
</div>
</template>
<script setup lang="ts">
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
import { workspaceDashboardsQuery } from '~/lib/dashboards/graphql/queries'
import type { Nullable } from '@speckle/shared'
import { graphql } from '~~/lib/common/generated/gql'
import { useQuery } from '@vue/apollo-composable'
const canCreateDashboardsQuery = graphql(`
query DashboardsListCanCreateDashboards($slug: String!) {
workspaceBySlug(slug: $slug) {
permissions {
canCreateDashboards {
...FullPermissionCheckResult
}
}
}
}
`)
const route = useRoute()
const workspaceSlug = computed(() => route.params.slug as string)
const { result: canCreateDashboardsResult } = useQuery(
canCreateDashboardsQuery,
() => ({
slug: workspaceSlug.value
})
)
const {
identifier,
onInfiniteLoad,
isVeryFirstLoading,
query: { result }
} = usePaginatedQuery({
query: workspaceDashboardsQuery,
options: computed(() => ({
enabled: !!workspaceSlug.value
})),
baseVariables: computed(() => ({
workspaceSlug: workspaceSlug.value || '',
cursor: null as Nullable<string>
})),
resolveKey: () => [''],
resolveCurrentResult: (res) =>
res?.workspaceBySlug?.dashboards
? {
totalCount: res.workspaceBySlug.dashboards.items.length,
items: res.workspaceBySlug.dashboards.items
}
: undefined,
resolveNextPageVariables: (baseVars, cursor) => ({
...baseVars,
cursor
}),
resolveCursorFromVariables: (vars) => vars.cursor
})
const showCreateDashboardDialog = ref(false)
const canCreateDashboards = computed(
() =>
canCreateDashboardsResult.value?.workspaceBySlug?.permissions?.canCreateDashboards
?.authorized
)
</script>
@@ -0,0 +1,133 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<div>
<Menu v-if="canShare || urlToken" as="div" class="flex items-center relative">
<MenuButton :id="menuButtonId" v-slot="{ open }" as="div">
<FormButton
color="outline"
class="hidden sm:flex"
size="sm"
:icon-right="open ? ChevronUpIcon : ChevronDownIcon"
>
Share
</FormButton>
</MenuButton>
<Transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute z-50 flex flex-col gap-1 right-0 top-7 min-w-max w-full sm:w-32 py-1 origin-top-right bg-foundation outline outline-1 outline-primary-muted rounded-md shadow-lg overflow-hidden mt-1"
>
<MenuItem v-slot="{ active }">
<div
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="handleCopyLink"
@keypress="keyboardClick(handleCopyLink)"
>
Copy link
</div>
</MenuItem>
<MenuItem v-slot="{ active }">
<div
:class="[
active ? 'bg-highlight-1' : '',
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="handleCopyEmbedLink"
@keypress="keyboardClick(handleCopyEmbedLink)"
>
Copy embed link
</div>
</MenuItem>
</MenuItems>
</Transition>
</Menu>
</div>
</template>
<script setup lang="ts">
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/20/solid'
import { keyboardClick } from '@speckle/ui-components'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { graphql } from '~~/lib/common/generated/gql'
import { useQuery, useMutation } from '@vue/apollo-composable'
const dashboardsSharePermissionsQuery = graphql(`
query DashboardsSharePermissions($id: String!) {
dashboard(id: $id) {
id
permissions {
canCreateToken {
...FullPermissionCheckResult
}
}
}
}
`)
const dashboardsShareTokenMutation = graphql(`
mutation DashboardsShareToken($dashboardId: String!) {
dashboardMutations {
createToken(dashboardId: $dashboardId) {
token
}
}
}
`)
const props = defineProps<{
id: MaybeNullOrUndefined<string>
}>()
const { copy } = useClipboard()
const menuButtonId = useId()
const { token: urlToken } = useRoute().query
const route = useRoute()
const { result } = useQuery(dashboardsSharePermissionsQuery, () => ({
id: props.id || ''
}))
const { mutate: createToken } = useMutation(dashboardsShareTokenMutation)
const canShare = computed(
() => result.value?.dashboard?.permissions?.canCreateToken?.authorized
)
const handleCopyLink = async () => {
if (!urlToken && canShare.value) {
const result = await createToken({ dashboardId: props.id || '' })
if (result?.data?.dashboardMutations?.createToken?.token) {
const token = result.data.dashboardMutations.createToken.token
const url = `${window.location.origin}${route.path}?token=${token}`
copy(url, { successMessage: 'Link copied to clipboard' })
}
} else {
const url = `${window.location.origin}${route.path}`
copy(url, { successMessage: 'Link copied to clipboard' })
}
}
const handleCopyEmbedLink = async () => {
if (!urlToken && canShare.value) {
const result = await createToken({ dashboardId: props.id || '' })
if (result?.data?.dashboardMutations?.createToken?.token) {
const token = result.data.dashboardMutations.createToken.token
const url = `${window.location.origin}${route.path}?token=${token}&embed=true`
copy(url, { successMessage: 'Embed link copied to clipboard' })
}
} else {
const url = `${window.location.origin}${route.path}?embed=true`
copy(url, { successMessage: 'Embed link copied to clipboard' })
}
}
</script>
@@ -25,10 +25,10 @@ const props = defineProps<{
isGenericErrorPage?: boolean
}>()
const route = useRoute()
const route = useCurrentRouteTillNavigated()
const isProjectRoute = computed(() => route.path.match(/\/projects\/[^/]+/))
const isWorkspaceRoute = computed(() => route.path.match(/\/workspaces\/[^/]+/))
const isProjectRoute = computed(() => route.value.path.match(/\/projects\/[^/]+/))
const isWorkspaceRoute = computed(() => route.value.path.match(/\/workspaces\/[^/]+/))
const finalError = computed(() => formatAppError(props.error))
const isNoProjectAccessError = computed(
@@ -6,6 +6,9 @@
<div>
<slot name="header-left" />
</div>
<div>
<slot name="header-center" />
</div>
<div>
<slot name="header-right" />
</div>
@@ -27,7 +27,7 @@
<PortalTarget name="primary-actions"></PortalTarget>
</ClientOnly>
<HeaderNavNotifications v-if="isLoggedIn" />
<div class="flex justify-end gap-x-2">
<div v-if="!hideUserNav" class="flex justify-end gap-x-2">
<FormButton
v-if="!activeUser"
:to="loginUrl.fullPath"
@@ -50,6 +50,10 @@ import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { loginRoute } from '~~/lib/common/helpers/route'
import type { Optional } from '@speckle/shared'
defineProps<{
hideUserNav?: boolean
}>()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const { activeUser, isLoggedIn } = useActiveUser()
const route = useRoute()
@@ -1,9 +1,13 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<Menu as="div" class="flex items-center relative">
<MenuButton :id="menuButtonId" as="div">
<MenuButton :id="menuButtonId" v-slot="{ open }" as="div">
<!-- Desktop Button -->
<FormButton class="hidden sm:flex" :icon-right="ChevronDownIcon">
<FormButton
color="outline"
class="hidden sm:flex"
:icon-right="open ? ChevronUpIcon : ChevronDownIcon"
>
Share
</FormButton>
<!-- Mobile Button -->
@@ -26,13 +30,13 @@
leave-to-class="transform opacity-0 scale-95"
>
<MenuItems
class="absolute z-50 flex flex-col gap-1 right-0 sm:right-4 top-8 min-w-max w-full sm:w-32 py-1 origin-top-right bg-foundation outline outline-1 outline-primary-muted rounded-md shadow-lg overflow-hidden mt-1"
class="absolute z-50 flex flex-col gap-1 right-0 top-11 min-w-max w-full sm:w-32 py-1 origin-top-right bg-foundation outline outline-1 outline-primary-muted rounded-md shadow-lg overflow-hidden mt-1"
>
<MenuItem v-slot="{ active }">
<div
:class="[
active ? 'bg-highlight-1' : '',
'text-body-sm flex px-2 py-1.5 text-foreground cursor-pointer transition mx-1.5 rounded'
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="handleCopyLink"
@keypress="keyboardClick(handleCopyLink)"
@@ -44,7 +48,7 @@
<div
:class="[
active ? 'bg-highlight-1' : '',
'text-body-sm flex px-2 py-1.5 text-foreground cursor-pointer transition mx-1.5 rounded'
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="handleCopyId"
@keypress="keyboardClick(handleCopyId)"
@@ -56,7 +60,7 @@
<div
:class="[
active ? 'bg-highlight-1' : '',
'text-body-sm flex px-2 py-1.5 text-foreground cursor-pointer transition mx-1.5 rounded'
'text-body-xs flex px-2 py-1 text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="handleEmbed"
@keypress="keyboardClick(handleEmbed)"
@@ -72,7 +76,7 @@
<script setup lang="ts">
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
import { ShareIcon } from '@heroicons/vue/24/outline'
import { ChevronDownIcon } from '@heroicons/vue/20/solid'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/20/solid'
import { SpeckleViewer } from '@speckle/shared'
import { keyboardClick } from '@speckle/ui-components'
import { graphql } from '~/lib/common/generated/gql/gql'
@@ -112,8 +112,7 @@ const {
shouldLoadPanorama,
isLoadingPanorama,
hasDoneFirstLoad,
isPanoramaPlaceholder,
init
isPanoramaPlaceholder
} = usePreviewImageBlob(basePreviewUrl, {
enabled: computed(() => props.eagerLoad || isInViewport.value),
eagerLoad: props.eagerLoad
@@ -197,5 +196,5 @@ if (import.meta.client) {
})
}
await init()
// await init()
</script>
@@ -188,15 +188,6 @@
<div class="text-heading text-foreground">
{{ name }}
</div>
<NuxtLink
v-if="showLastUploadFailed"
v-tippy="'Last upload failed'"
v-keyboard-clickable
class="text-body-3xs text-danger hover:text-danger-lighter cursor-pointer"
@click.stop="actions?.showUploads()"
>
<ExclamationCircleIcon class="w-4 h-4" />
</NuxtLink>
</div>
<!-- Preview -->
@@ -1,11 +1,7 @@
<template>
<button
class="flex items-center justify-center rounded-lg text-foreground"
:class="[
isActive
? 'shadow-[0px_1px_2px_rgba(0,0,0,0.05),0px_0px_1px_rgba(0,0,0,0.05)] bg-foundation'
: 'hover:bg-foundation-page'
]"
class="flex items-center justify-center rounded-[5px] text-foreground"
:class="[isActive ? 'shadow-md bg-foundation' : 'hover:bg-foundation-page']"
>
<slot />
</button>
@@ -1,6 +1,6 @@
<template>
<div
class="flex rounded-lg bg-highlight-1 border border-outline-2 self-start overflow-hidden gap-x-0.5"
class="flex rounded-md bg-highlight-1 border border-outline-2 overflow-hidden self-start gap-x-0.5"
>
<slot />
</div>
@@ -93,7 +93,10 @@ const {
const { getActiveMeasurement, removeMeasurement, enableMeasurements, hasMeasurements } =
useMeasurementUtilities()
const { resetExplode } = useFilterUtilities()
const { currentViewMode, setViewMode } = useViewModeUtilities()
const {
viewMode: { mode: currentViewMode },
setViewMode
} = useViewModeUtilities()
const {
ui: { explodeFactor }
} = useInjectedViewerState()
@@ -54,7 +54,14 @@
<!-- Saved views -->
<ViewerControlsButtonToggle
v-if="isSavedViewsEnabled"
v-tippy="getShortcutDisplayText(shortcuts.ToggleSavedViews)"
v-tippy="
getTooltipProps(
getShortcutDisplayText(shortcuts.ToggleSavedViews, { format: 'separate' }),
{
placement: 'right'
}
)
"
:active="activePanel === 'savedViews'"
:icon="Camera"
@click="toggleActivePanel('savedViews')"
@@ -68,7 +68,9 @@ import { useViewModeUtilities } from '~/lib/viewer/composables/ui'
import { TIME_MS } from '@speckle/shared'
const mp = useMixpanel()
const { currentViewMode } = useViewModeUtilities()
const {
viewMode: { mode: currentViewMode }
} = useViewModeUtilities()
const isLightingSupported = computed(() => {
const supported = currentViewMode.value === ViewMode.DEFAULT
@@ -8,6 +8,7 @@
<template #actions>
<div v-if="!isLowerPlan" class="flex items-center gap-0.5">
<FormButton
v-tippy="getTooltipProps('Search views')"
size="sm"
color="subtle"
:icon-left="Search"
@@ -16,17 +17,19 @@
/>
<div v-tippy="canCreateViewOrGroup?.errorMessage" class="flex items-center">
<FormButton
v-tippy="getTooltipProps('Create group')"
size="sm"
color="subtle"
:icon-left="FolderPlus"
hide-text
name="addGroup"
:disabled="!canCreateViewOrGroup?.authorized || isLoading"
@click="onAddGroup"
@click="() => (showCreateGroupDialog = true)"
/>
</div>
<div v-tippy="canCreateViewOrGroup?.errorMessage" class="flex items-center">
<FormButton
v-tippy="getTooltipProps('Create view')"
size="sm"
color="subtle"
:icon-left="Plus"
@@ -39,17 +42,18 @@
</div>
</template>
<template v-if="searchMode" #fullTitle>
<div class="self-center w-full pr-2 flex gap-2 items-center">
<div class="self-center w-full pr-1 flex gap-2 items-center">
<FormTextInput
v-bind="bind"
name="search"
placeholder="Search"
placeholder="Search views..."
color="foundation"
auto-focus
size="sm"
wrapper-classes="flex-1 -ml-1"
v-on="on"
/>
<FormButton
v-tippy="'Exit search'"
size="sm"
color="subtle"
:icon-left="X"
@@ -60,7 +64,7 @@
</div>
</template>
<template v-if="!isLowerPlan">
<div class="px-4 pt-2">
<div class="px-3 pt-3">
<ViewerButtonGroup>
<ViewerButtonGroupButton
v-for="viewsType in Object.values(ViewsType)"
@@ -87,14 +91,16 @@
>
<CommonPromoAlert
title="Save your views"
text="With an editor seat, unlock the option to save your own views."
:button="{ title: 'Learn more' }"
text="With an Editor seat, unlock the option to save views. A workspace admin can update your seat type."
show-closer
@close="hideViewerSeatDisclaimer = true"
/>
</div>
</template>
<ViewerSavedViewsPlanUpsell v-else />
<ViewerSavedViewsPanelGroupsCreateDialog
v-model:open="showCreateGroupDialog"
@success="onAddGroup"
/>
</ViewerLayoutSidePanel>
</template>
<script setup lang="ts">
@@ -102,14 +108,8 @@ import { useMutationLoading } from '@vue/apollo-composable'
import { Search, FolderPlus, Plus, X } from 'lucide-vue-next'
import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
import { graphql } from '~/lib/common/generated/gql'
import {
SavedViewVisibility,
WorkspaceSeatType
} from '~/lib/common/generated/gql/graphql'
import {
useCreateSavedView,
useCreateSavedViewGroup
} from '~/lib/viewer/composables/savedViews/management'
import { WorkspaceSeatType } from '~/lib/common/generated/gql/graphql'
import { useCreateSavedView } from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { ViewsType, viewsTypeLabels } from '~/lib/viewer/helpers/savedViews'
import { useDebouncedTextInput } from '@speckle/ui-components'
@@ -135,21 +135,18 @@ defineEmits<{
}>()
const {
projectId,
resources: {
request: { resourceIdString },
response: { project }
},
ui: {
savedViews: { openedGroupState }
}
} = useInjectedViewerState()
const createGroup = useCreateSavedViewGroup()
const createSavedView = useCreateSavedView()
const isLoading = useMutationLoading()
const { on, bind, value: search } = useDebouncedTextInput()
const selectedViewsType = ref<ViewsType>(ViewsType.Personal)
const selectedViewsType = ref<ViewsType>(ViewsType.All)
const hideViewerSeatDisclaimer = useSynchronizedCookie<boolean>(
'hideViewerSeatSavedViewsDisclaimer',
{
@@ -157,6 +154,9 @@ const hideViewerSeatDisclaimer = useSynchronizedCookie<boolean>(
}
)
const searchMode = ref(false)
const showCreateGroupDialog = ref(false)
const { getTooltipProps } = useSmartTooltipDelay()
const canCreateViewOrGroup = computed(
() => project.value?.permissions.canCreateSavedView
@@ -168,28 +168,15 @@ const isLowerPlan = computed(() => !project.value?.workspace?.planSupportsSavedV
const onAddView = async () => {
if (isLoading.value) return
const view = await createSavedView({
visibility:
selectedViewsType.value === ViewsType.Shared
? SavedViewVisibility.Public
: undefined
})
const view = await createSavedView({})
if (view) {
// Auto-open the group that the view created to
openedGroupState.value.set(view.group.id, true)
}
}
const onAddGroup = async () => {
if (isLoading.value) return
const group = await createGroup({
projectId: projectId.value,
resourceIdString: resourceIdString.value
})
if (group) {
// Auto-open the group
openedGroupState.value.set(group.id, true)
}
const onAddGroup = async (group: { id: string }) => {
openedGroupState.value.set(group.id, true)
}
const setSearchMode = (val: boolean) => {
@@ -1,20 +0,0 @@
<template>
<div class="flex flex-col gap-4 p-4">
<img src="~/assets/images/viewer/saved-views/plan_upsell.webp" alt="Saved Views" />
<div>
<div class="text-foreground text-body font-semibold">Save custom views</div>
<div class="text-body-2xs font-medium text-foreground-2">
<p class="pb-3">Upgrade to a business plan to save, organise and present</p>
<ul class="flex flex-col gap-2 list-disc list-inside">
<li>It's cool</li>
<li>It's nice</li>
<li>It's got enough spice</li>
</ul>
</div>
</div>
<div class="flex gap-2">
<FormButton size="sm">Upgrade</FormButton>
<FormButton size="sm" color="outline">Learn more</FormButton>
</div>
</div>
</template>
@@ -7,7 +7,7 @@
v-if="!hasGroups || !project"
:type="emptyStateType"
/>
<div v-else class="p-2">
<div v-else class="p-1.5 pt-2">
<ViewerSavedViewsPanelViewsGroup
v-for="group in groups"
:key="group.id"
@@ -47,7 +47,11 @@
/>
</LayoutMenu>
<div
v-tippy="canUpdate?.errorMessage"
v-tippy="
getTooltipProps(
canUpdate?.authorized ? 'Edit view' : canUpdate?.errorMessage
)
"
class="shrink-0 opacity-0 group-hover:opacity-100"
>
<FormButton
@@ -64,12 +68,13 @@
</div>
</div>
<div class="w-full flex items-center gap-1">
<Globe
v-if="!isOnlyVisibleToMe"
<Component
:is="isOnlyVisibleToMe ? User : Globe"
v-tippy="getTooltipProps(isOnlyVisibleToMe ? 'Private' : 'Shared')"
:size="12"
:stroke-width="1.5"
:absolute-stroke-width="true"
class="w-3 h-3 text-foreground-2"
class="w-3 h-3 text-foreground-3"
/>
<div
v-tippy="{
@@ -81,7 +86,7 @@
}"
class="text-body-2xs text-foreground-3 truncate pr-1.5"
>
{{ formattedRelativeDate(view.updatedAt) }}
{{ formattedRelativeDate(view.updatedAt, { capitalize: true }) }}
</div>
</div>
</div>
@@ -97,7 +102,7 @@ import {
import type { LayoutMenuItem } from '@speckle/ui-components'
import { useMutationLoading } from '@vue/apollo-composable'
import { difference } from 'lodash-es'
import { Ellipsis, SquarePen, Bookmark, Globe } from 'lucide-vue-next'
import { Ellipsis, SquarePen, Bookmark, Globe, User } from 'lucide-vue-next'
import { graphql } from '~/lib/common/generated/gql'
import {
SavedViewVisibility,
@@ -108,6 +113,7 @@ import {
useCollectNewSavedViewViewerData,
useUpdateSavedView
} from '~/lib/viewer/composables/savedViews/management'
import { useSavedViewValidationHelpers } from '~/lib/viewer/composables/savedViews/validation'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
const MenuItems = StringEnum([
@@ -121,6 +127,8 @@ const MenuItems = StringEnum([
])
type MenuItems = StringEnumValues<typeof MenuItems>
const { getTooltipProps } = useSmartTooltipDelay()
graphql(`
fragment ViewerSavedViewsPanelView_SavedView on SavedView {
id
@@ -143,6 +151,7 @@ graphql(`
...UseDeleteSavedView_SavedView
...UseUpdateSavedView_SavedView
...ViewerSavedViewsPanelViewEditDialog_SavedView
...UseSavedViewValidationHelpers_SavedView
}
`)
@@ -161,15 +170,19 @@ const isLoading = useMutationLoading()
const { copyLink, applyView } = useViewerSavedViewsUtils()
const eventBus = useEventBus()
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
const {
canUpdate,
isOnlyVisibleToMe,
canSetHomeView,
isHomeView,
canToggleVisibility
} = useSavedViewValidationHelpers({
view: computed(() => props.view)
})
const showMenu = ref(false)
const menuId = useId()
const canUpdate = computed(() => props.view.permissions.canUpdate)
const isOnlyVisibleToMe = computed(
() => props.view.visibility === SavedViewVisibility.AuthorOnly
)
const isHomeView = computed(() => props.view.isHomeView)
const isActive = computed(() => props.view.id === savedView.value?.id)
const isOriginalVersionAlreadyLoaded = computed(() => {
@@ -188,52 +201,29 @@ const canLoadOriginal = computed(
}
)
const canSetHomeView = computed(
(): { authorized: boolean; message: Optional<string> } => {
if (!canUpdate.value?.authorized || isLoading.value) {
return { authorized: false, message: canUpdate.value.errorMessage || undefined }
}
if (isFederatedView.value) {
return {
authorized: false,
message: "Home view settings can't be updated while in a federated view"
}
}
if (isOnlyVisibleToMe.value) {
return {
authorized: false,
message: 'A view must be shared to be set as home view'
}
}
return { authorized: true, message: undefined }
}
)
const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
[
{
id: MenuItems.LoadOriginalVersions,
title: 'Load with original model version',
disabled: !canLoadOriginal.value.authorized || isLoading.value,
disabledTooltip: canLoadOriginal.value.message
id: MenuItems.MoveToGroup,
title: 'Move to group',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value?.errorMessage
},
{
id: MenuItems.ReplaceView,
title: 'Replace view',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
},
{
id: MenuItems.MoveToGroup,
title: 'Move to group',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
disabledTooltip: canUpdate.value?.errorMessage
},
{
id: MenuItems.CopyLink,
title: 'Copy link'
},
{
id: MenuItems.LoadOriginalVersions,
title: 'Load with original model version',
disabled: !canLoadOriginal.value.authorized || isLoading.value,
disabledTooltip: canLoadOriginal.value.message
}
],
[
@@ -246,24 +236,23 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
},
{
id: MenuItems.ChangeVisibility,
title: 'Share view to workspace',
active: !isOnlyVisibleToMe.value,
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
title: isOnlyVisibleToMe.value ? 'Make view shared' : 'Make view private',
disabled: !canToggleVisibility.value.authorized,
disabledTooltip: canToggleVisibility.value.message
}
],
[
{
id: MenuItems.Delete,
title: 'Delete',
title: 'Delete view...',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
disabledTooltip: canUpdate.value?.errorMessage
}
]
])
const wrapperClasses = computed(() => {
const classParts = ['flex gap-2 p-2 pr-0.5 w-full group rounded-md cursor-pointer']
const classParts = ['flex gap-2 p-1.5 w-full group rounded-md cursor-pointer']
if (isActive.value) {
classParts.push('bg-highlight-2 hover:bg-highlight-3')
@@ -0,0 +1,92 @@
<template>
<LayoutDialog
v-model:open="open"
title="Create group"
max-width="sm"
:buttons="buttons"
:on-submit="onSubmit"
>
<div class="flex flex-col gap-4">
<FormTextInput
name="name"
label="Group name"
show-label
color="foundation"
placeholder="Enter group name"
:rules="[isRequired, isStringOfLength({ maxLength: 255 })]"
auto-focus
/>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useMutationLoading } from '@vue/apollo-composable'
import { useForm } from 'vee-validate'
import { isRequired, isStringOfLength } from '~/lib/common/helpers/validation'
import { useCreateSavedViewGroup } from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
type FormType = {
name: string
}
const emit = defineEmits<{
success: [{ id: string }]
}>()
const open = defineModel<boolean>('open', {
required: true
})
const {
projectId,
resources: {
request: { resourceIdString }
}
} = useInjectedViewerState()
const isLoading = useMutationLoading()
const createGroup = useCreateSavedViewGroup()
const { handleSubmit, setValues } = useForm<FormType>()
const buttons = computed((): LayoutDialogButton[] => [
{
id: 'cancel',
text: 'Cancel',
props: {
color: 'outline'
},
onClick: () => {
open.value = false
}
},
{
id: 'create',
text: 'Create',
submit: true
}
])
const onSubmit = handleSubmit(async (values) => {
if (isLoading.value) return
const group = await createGroup({
projectId: projectId.value,
resourceIdString: resourceIdString.value,
groupName: values.name
})
if (group) {
emit('success', {
id: group.id
})
open.value = false
}
})
watch(open, (newVal, oldVal) => {
if (newVal && !oldVal) {
// Reset form state when dialog opens
setValues({
name: ''
})
}
})
</script>
@@ -31,28 +31,28 @@
:rules="[isRequired]"
/>
<FormRadioGroup
:options="radioOptions"
:options="visibilityOptions"
size="sm"
name="visibility"
:rules="[isRequired]"
:rules="[isRequired, validateVisibility]"
/>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { FormRadioGroupItem, LayoutDialogButton } from '@speckle/ui-components'
import { Globe, Lock } from 'lucide-vue-next'
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useForm } from 'vee-validate'
import { graphql } from '~/lib/common/generated/gql'
import {
SavedViewVisibility,
type FormSelectSavedViewGroup_SavedViewGroupFragment,
type ViewerSavedViewsPanelViewEditDialog_SavedViewFragment
} from '~/lib/common/generated/gql/graphql'
import { isRequired, isStringOfLength } from '~/lib/common/helpers/validation'
import { useUpdateSavedView } from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { isUndefined } from 'lodash-es'
import { useSavedViewValidationHelpers } from '~/lib/viewer/composables/savedViews/validation'
import type {
FormSelectSavedViewGroup_SavedViewGroupFragment,
SavedViewVisibility,
ViewerSavedViewsPanelViewEditDialog_SavedViewFragment
} from '~/lib/common/generated/gql/graphql'
graphql(`
fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {
@@ -64,6 +64,7 @@ graphql(`
...FormSelectSavedViewGroup_SavedViewGroup
}
...UseUpdateSavedView_SavedView
...UseSavedViewValidationHelpers_SavedView
}
`)
@@ -89,6 +90,9 @@ const {
}
} = useInjectedViewerState()
const updateView = useUpdateSavedView()
const { validateVisibility, visibilityOptions } = useSavedViewValidationHelpers({
view: computed(() => props.view)
})
const buttons = computed((): LayoutDialogButton[] => [
{
@@ -108,21 +112,6 @@ const buttons = computed((): LayoutDialogButton[] => [
}
])
const radioOptions = computed((): FormRadioGroupItem<SavedViewVisibility>[] => [
{
value: SavedViewVisibility.Public,
title: 'Public',
introduction: 'Visible to anyone with access to the model.',
icon: Globe
},
{
value: SavedViewVisibility.AuthorOnly,
title: 'Private',
introduction: 'Visible only to the view author.',
icon: Lock
}
])
const onSubmit = handleSubmit(async (values) => {
if (!props.view) return
@@ -16,8 +16,8 @@ const props = withDefaults(
const message = computed(() => {
if (props.type === 'search') {
return 'No saved scenes match your search criteria'
return 'No views match your search criteria'
}
return 'There are no saved scenes yet'
return 'No saved views yet'
})
</script>
@@ -37,6 +37,7 @@
</LayoutMenu>
<div v-tippy="canCreateView?.errorMessage">
<FormButton
v-tippy="getTooltipProps('Create view in group')"
size="sm"
color="subtle"
:icon-left="Plus"
@@ -63,18 +64,19 @@ import type { LayoutMenuItem } from '@speckle/ui-components'
import { useMutationLoading } from '@vue/apollo-composable'
import { Ellipsis, Plus } from 'lucide-vue-next'
import { graphql } from '~/lib/common/generated/gql'
import {
SavedViewVisibility,
type UseUpdateSavedViewGroup_SavedViewGroupFragment,
type ViewerSavedViewsPanelViewsGroup_ProjectFragment,
type ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment,
type ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment
import type {
UseUpdateSavedViewGroup_SavedViewGroupFragment,
ViewerSavedViewsPanelViewsGroup_ProjectFragment,
ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment,
ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment
} from '~/lib/common/generated/gql/graphql'
import {
useCreateSavedView,
useUpdateSavedViewGroup
} from '~/lib/viewer/composables/savedViews/management'
import { ViewsType } from '~/lib/viewer/helpers/savedViews'
import type { ViewsType } from '~/lib/viewer/helpers/savedViews'
const { getTooltipProps } = useSmartTooltipDelay()
const MenuItems = StringEnum(['Delete', 'Rename'])
type MenuItems = StringEnumValues<typeof MenuItems>
@@ -152,7 +154,7 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
[
{
id: MenuItems.Rename,
title: 'Rename',
title: 'Rename group',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
}
@@ -160,7 +162,7 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
[
{
id: MenuItems.Delete,
title: 'Delete',
title: 'Delete group...',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
}
@@ -182,9 +184,7 @@ const onActionChosen = async (item: LayoutMenuItem<MenuItems>) => {
const onAddGroupView = async () => {
await createView({
groupId: props.group.id,
visibility:
props.viewsType === ViewsType.Shared ? SavedViewVisibility.Public : undefined
groupId: props.group.id
})
open.value = true
}
@@ -24,7 +24,7 @@
</template>
<template v-else>
<span
class="min-w-full flex justify-center items-center bg-foundation-page text-body-xs rounded-md text-foreground-2 border border-dashed border-outline-2 w-full text-center mx-auto my-2 px-4 h-10"
class="flex justify-center items-center bg-foundation-page text-body-2xs rounded-md text-foreground-2 border border-dashed border-outline-2 text-center my-2 mx-1.5 px-4 h-10"
>
No views in group
</span>
@@ -52,17 +52,17 @@
<div
v-for="(kvp, index) in categorisedValuePairs.nonPrimitiveArrays"
:key="index"
class="text-xs"
class="text-body-3xs"
>
<div class="text-foreground-2 grid grid-cols-3 pl-2">
<div
class="col-span-1 truncate text-xs font-medium"
class="col-span-1 truncate text-body-3xs font-medium"
:title="(kvp.key as string)"
>
{{ kvp.key }}
</div>
<div
class="col-span-2 flex w-full min-w-0 truncate text-xs pl-1 text-foreground"
class="col-span-2 flex w-full min-w-0 truncate text-body-3xs pl-1 text-foreground"
>
<div class="flex-grow truncate">{{ kvp.innerType }} array</div>
<div class="text-foreground-2">({{ kvp.arrayLength }})</div>
@@ -72,16 +72,16 @@
<div v-for="(kvp, index) in categorisedValuePairs.primitiveArrays" :key="index">
<div class="grid grid-cols-3">
<div
class="col-span-1 truncate text-xs font-medium pl-2 text-foreground-2"
class="col-span-1 truncate text-body-3xs font-medium pl-2 text-foreground-2"
:title="(kvp.key as string)"
>
{{ kvp.key }}
</div>
<div
class="col-span-2 flex w-full min-w-0 truncate text-xs text-foreground"
class="col-span-2 flex w-full min-w-0 truncate text-body-3xs text-foreground"
:title="(kvp.value as string)"
>
<div class="flex-grow truncate">{{ kvp.arrayPreview }}</div>
<div class="pl-2.5 flex-grow truncate">{{ kvp.arrayPreview }}</div>
<div class="text-foreground-2">({{ kvp.arrayLength }})</div>
</div>
</div>
@@ -95,19 +95,15 @@ import { ViewMode } from '@speckle/viewer'
import { useViewModeUtilities } from '~~/lib/viewer/composables/ui'
import { ViewModeShortcuts } from '~/lib/viewer/helpers/shortcuts/shortcuts'
import { FormSwitch } from '@speckle/ui-components'
import { useTheme } from '~/lib/core/composables/theme'
import { defaultEdgeColorValue } from '~/lib/viewer/composables/setup/viewMode'
const {
setViewMode,
currentViewMode,
edgesEnabled,
toggleEdgesEnabled,
setEdgesWeight,
edgesWeight,
setEdgesColor,
edgesColor
viewMode: { edgesColor, edgesWeight, edgesEnabled, mode: currentViewMode }
} = useViewModeUtilities()
const { isLightTheme } = useTheme()
const showSettings = ref(false)
@@ -115,14 +111,17 @@ const isActiveMode = (mode: ViewMode) => mode === currentViewMode.value
const viewModeShortcuts = Object.values(ViewModeShortcuts)
const edgesColorOptions = computed(() => [
isLightTheme.value || currentViewMode.value !== ViewMode.PEN ? 0x1a1a1a : 0xffffff, // black or white
0x3b82f6, // blue-500
0x8b5cf6, // violet-500
0x65a30d, // lime-600
0xf97316, // orange-500
0xf43f5e //rose-500
])
const edgesColorOptions = computed(
() =>
[
defaultEdgeColorValue, // black or white
0x3b82f6, // blue-500
0x8b5cf6, // violet-500
0x65a30d, // lime-600
0xf97316, // orange-500
0xf43f5e //rose-500
] as const
)
const handleViewModeChange = (mode: ViewMode) => {
setViewMode(mode)
@@ -84,4 +84,19 @@ export const useIsRhinoFileImporterEnabled = () => {
return ref(FF_RHINO_FILE_IMPORTER_ENABLED)
}
export const useIsNoPersonalEmailsEnabled = () => {
const {
public: { FF_NO_PERSONAL_EMAILS_ENABLED }
} = useRuntimeConfig()
return ref(FF_NO_PERSONAL_EMAILS_ENABLED)
}
export const useIsDashboardsModuleEnabled = () => {
const {
public: { FF_DASHBOARDS_MODULE_ENABLED }
} = useRuntimeConfig()
return ref(FF_DASHBOARDS_MODULE_ENABLED)
}
export { useGlobalToast, useActiveUser, usePageQueryStandardFetchPolicy, useEventBus }
+36 -19
View File
@@ -4,9 +4,10 @@ import type {
RouteLocationAsPathGeneric
} from '#vue-router'
import { buildManualPromise } from '@speckle/shared'
import { useScopedState } from '~/lib/common/composables/scopedState'
const useRouterNavigatingState = () =>
useState('use_router_navigating_state', () => ({
useScopedState('use_router_navigating_state', () => ({
allActiveWaits: <Array<Promise<unknown>>>[],
/**
* Used for debugging to assign an incrementing id to each invocation
@@ -20,8 +21,8 @@ const useRouterNavigatingDevUtils = () => {
const ret = {
getLogId: () => {
const newVal = state.value.logId + 1
state.value.logId = newVal
const newVal = state.logId + 1
state.logId = newVal
return newVal + ''
},
@@ -40,6 +41,16 @@ const useRouterNavigatingDevUtils = () => {
devTrace(...args)
})
},
waitForNavigationsClear: async () =>
await until($isNavigating)
.toBe(false, {
throwOnTimeout: true,
timeout: 500
})
.catch((err) => {
// Swallow throw, just log and continue
$logger.error({ err }, 'Waiting for nuxt navigations to clear timed out')
}),
isNuxtNavigating: $isNavigating,
logger: $logger
}
@@ -69,7 +80,7 @@ type SafeRouterNavigationOptions<
* Supports debugRoutes=1 query param for debug logs
*/
export const useSafeRouter = () => {
const { getLogId, debugLog, debugTrace, isNuxtNavigating, logger } =
const { getLogId, debugLog, debugTrace, waitForNavigationsClear } =
useRouterNavigatingDevUtils()
const router = useRouter()
const state = useRouterNavigatingState()
@@ -87,27 +98,16 @@ export const useSafeRouter = () => {
const waitPromise = buildManualPromise<void>()
const logId = getLogId()
const waitForNavigationsClear = async () =>
await until(isNuxtNavigating)
.toBe(false, {
throwOnTimeout: true,
timeout: 500
})
.catch((err) => {
// Swallow throw, just log and continue
logger.error({ err }, 'Waiting for nuxt navigations to clear timed out')
})
debugTrace(`[{logId}] Safe router ${action} registered`, {
initialTo: to(),
logId
})
try {
const activeWaits = state.value.allActiveWaits.slice()
const activeWaits = state.allActiveWaits.slice()
// Queue up another wait
state.value.allActiveWaits = [...state.value.allActiveWaits, waitPromise.promise]
state.allActiveWaits = [...state.allActiveWaits, waitPromise.promise]
// Wait for all previously queued up waits
await Promise.allSettled(activeWaits)
@@ -150,7 +150,7 @@ export const useSafeRouter = () => {
logId,
navResult
})
state.value.allActiveWaits = state.value.allActiveWaits.filter(
state.allActiveWaits = state.allActiveWaits.filter(
(p) => p !== waitPromise.promise
)
waitPromise.resolve()
@@ -160,7 +160,7 @@ export const useSafeRouter = () => {
waitPromise.reject(e)
throw e
} finally {
state.value.allActiveWaits = state.value.allActiveWaits.filter(
state.allActiveWaits = state.allActiveWaits.filter(
(p) => p !== waitPromise.promise
)
}
@@ -186,3 +186,20 @@ export const useSafeRouter = () => {
return { ...router, push, replace }
}
/**
* Similar to useRoute, but will not change the value until the new/incoming route has fully finished navigating
*/
export const useCurrentRouteTillNavigated = () => {
const baseRoute = useRoute()
const { $isNavigating } = useNuxtApp()
const route = shallowRef({ ...toRaw(baseRoute) })
watch($isNavigating, (newVal, oldVal) => {
if (!newVal && oldVal) {
route.value = { ...toRaw(baseRoute) }
}
})
return route
}
+28
View File
@@ -0,0 +1,28 @@
<template>
<div>
<ClientOnly>
<HeaderNavBar v-if="!isEmbedEnabled" hide-user-nav />
</ClientOnly>
<div class="h-dvh w-dvh overflow-hidden flex flex-col">
<!-- Static Spacer to allow for absolutely positioned HeaderNavBar -->
<div v-if="!isEmbedEnabled" class="h-12 w-full shrink-0"></div>
<div
class="relative flex"
:class="isEmbedEnabled ? 'h-[100dvh]' : 'h-[calc(100dvh-3rem)]'"
>
<main class="w-full h-full overflow-y-auto simple-scrollbar">
<div class="container w-full">
<slot />
</div>
</main>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useDashboardEmbed } from '~/lib/dashboards/composables/embed'
const { isEmbedEnabled } = useDashboardEmbed()
</script>
@@ -247,9 +247,16 @@ export const useAuthManager = (
const embedToken = computed(() => route.query.embedToken as Optional<string>)
/**
* Get the effective auth token (embed token takes precedence)
* Token used for dashboard sharing
*/
const effectiveAuthToken = computed(() => embedToken.value || authToken.value)
const dashboardToken = computed(() => route.query.token as Optional<string>)
/**
* Get the effective auth token
*/
const effectiveAuthToken = computed(
() => dashboardToken.value || embedToken.value || authToken.value
)
/**
* Set/clear new token value and redirect to home
@@ -1,4 +1,5 @@
import { isStringOfLength, stringContains } from '~~/lib/common/helpers/validation'
import { blockedDomains } from '@speckle/shared'
export const passwordLongEnough = isStringOfLength({ minLength: 8 })
export const passwordHasAtLeastOneNumber = stringContains({
@@ -13,6 +14,12 @@ export const passwordHasAtLeastOneUppercaseLetter = stringContains({
match: /[A-Z]/,
message: 'Must have at least one uppercase letter'
})
export const doesNotContainBlockedDomain = (val: string) => {
const domain = val.split('@')[1]?.toLowerCase()
return domain && blockedDomains.includes(domain)
? 'Please use your work email instead of a personal email address'
: true
}
export const passwordRules = [
passwordLongEnough,
@@ -42,6 +42,12 @@ type Documents = {
"\n fragment BillingAlert_Workspace on Workspace {\n id\n role\n slug\n plan {\n name\n status\n createdAt\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n": typeof types.BillingAlert_WorkspaceFragmentDoc,
"\n fragment CommonModelSelectorModel on Model {\n id\n name\n }\n": typeof types.CommonModelSelectorModelFragmentDoc,
"\n query DashboardSidebar {\n activeUser {\n id\n activeWorkspace {\n id\n role\n }\n }\n }\n": typeof types.DashboardSidebarDocument,
"\n query SidebarPermissions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canListDashboards {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.SidebarPermissionsDocument,
"\n fragment DashboardsCard_Dashboard on Dashboard {\n id\n name\n createdAt\n workspace {\n id\n }\n createdBy {\n id\n name\n avatar\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.DashboardsCard_DashboardFragmentDoc,
"\n fragment DashboardsEditDialog_Dashboard on Dashboard {\n id\n name\n workspace {\n id\n }\n }\n": typeof types.DashboardsEditDialog_DashboardFragmentDoc,
"\n query DashboardsListCanCreateDashboards($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canCreateDashboards {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.DashboardsListCanCreateDashboardsDocument,
"\n query DashboardsSharePermissions($id: String!) {\n dashboard(id: $id) {\n id\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": typeof types.DashboardsSharePermissionsDocument,
"\n mutation DashboardsShareToken($dashboardId: String!) {\n dashboardMutations {\n createToken(dashboardId: $dashboardId) {\n token\n }\n }\n }\n": typeof types.DashboardsShareTokenDocument,
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": typeof types.FormSelectModels_ModelFragmentDoc,
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": typeof types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n": typeof types.FormSelectSavedViewGroup_SavedViewGroupFragmentDoc,
@@ -169,9 +175,9 @@ type Documents = {
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n": typeof types.ViewerSavedViewsPanel_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelGroups_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n ...ViewerSavedViewsPanelViewsGroup_Project\n }\n": typeof types.ViewerSavedViewsPanelGroups_ProjectFragmentDoc,
"\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n": typeof types.ViewerSavedViewsPanelGroups_SavedViewGroupsDocument,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewDeleteDialog_SavedView on SavedView {\n id\n name\n ...UseDeleteSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewMoveDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ViewerSavedViewsPanelViewsGroup_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n isUngroupedViewsGroup\n title\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n": typeof types.ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragmentDoc,
@@ -251,6 +257,11 @@ type Documents = {
"\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 CreateDashboard(\n $workspace: WorkspaceIdentifier!\n $input: DashboardCreateInput!\n ) {\n dashboardMutations {\n create(workspace: $workspace, input: $input) {\n id\n workspace {\n id\n }\n }\n }\n }\n": typeof types.CreateDashboardDocument,
"\n mutation UpdateDashboard($input: DashboardUpdateInput!) {\n dashboardMutations {\n update(input: $input) {\n id\n name\n }\n }\n }\n": typeof types.UpdateDashboardDocument,
"\n mutation DeleteDashboard($id: String!) {\n dashboardMutations {\n delete(id: $id)\n }\n }\n": typeof types.DeleteDashboardDocument,
"\n query Dashboard($id: String!) {\n dashboard(id: $id) {\n id\n ...WorkspaceDashboards_Dashboard\n }\n }\n": typeof types.DashboardDocument,
"\n query WorkspaceDashboards($workspaceSlug: String!, $cursor: String) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n dashboards(cursor: $cursor) {\n cursor\n items {\n id\n ...DashboardsCard_Dashboard\n }\n }\n }\n }\n": typeof types.WorkspaceDashboardsDocument,
"\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,
"\n mutation DeleteApplication($appId: String!) {\n appDelete(appId: $appId)\n }\n": typeof types.DeleteApplicationDocument,
@@ -396,8 +407,9 @@ type Documents = {
"\n fragment AppAuthorAvatar on AppAuthor {\n id\n name\n avatar\n }\n": typeof types.AppAuthorAvatarFragmentDoc,
"\n fragment LimitedUserAvatar on LimitedUser {\n id\n name\n avatar\n }\n": typeof types.LimitedUserAvatarFragmentDoc,
"\n fragment ActiveUserAvatar on User {\n id\n name\n avatar\n }\n": typeof types.ActiveUserAvatarFragmentDoc,
"\n query ActiveUserMeta {\n activeUser {\n meta {\n legacyProjectsExplainerCollapsed\n }\n }\n }\n": typeof types.ActiveUserMetaDocument,
"\n query ActiveUserMeta {\n activeUser {\n meta {\n legacyProjectsExplainerCollapsed\n intelligenceCommunityStandUpBannerDismissed\n }\n }\n }\n": typeof types.ActiveUserMetaDocument,
"\n mutation UpdateLegacyProjectsExplainer($value: Boolean!) {\n activeUserMutations {\n meta {\n setLegacyProjectsExplainerCollapsed(value: $value)\n }\n }\n }\n": typeof types.UpdateLegacyProjectsExplainerDocument,
"\n mutation UpdateIntelligenceCommunityStandUpBannerDismissed($value: Boolean!) {\n activeUserMutations {\n meta {\n setIntelligenceCommunityStandUpBannerDismissed(value: $value)\n }\n }\n }\n": typeof types.UpdateIntelligenceCommunityStandUpBannerDismissedDocument,
"\n subscription OnUserProjectsUpdate {\n userProjectsUpdated {\n type\n id\n project {\n ...ProjectDashboardItem\n workspaceId\n }\n }\n }\n ": typeof types.OnUserProjectsUpdateDocument,
"\n mutation UpdateUser($input: UserUpdateInput!) {\n activeUserMutations {\n update(user: $input) {\n id\n name\n bio\n company\n avatar\n }\n }\n }\n": typeof types.UpdateUserDocument,
"\n mutation UpdateNotificationPreferences($input: JSONObject!) {\n userNotificationPreferencesUpdate(preferences: $input)\n }\n": typeof types.UpdateNotificationPreferencesDocument,
@@ -415,12 +427,13 @@ type Documents = {
"\n mutation DeleteSavedView($input: DeleteSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n deleteView(input: $input)\n }\n }\n }\n": typeof types.DeleteSavedViewDocument,
"\n fragment UseDeleteSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n": typeof types.UseDeleteSavedView_SavedViewFragmentDoc,
"\n mutation UpdateSavedView($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n": typeof types.UpdateSavedViewDocument,
"\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n visibility\n group {\n id\n }\n }\n": typeof types.UseUpdateSavedView_SavedViewFragmentDoc,
"\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n isHomeView\n groupResourceIds\n group {\n id\n }\n }\n": typeof types.UseUpdateSavedView_SavedViewFragmentDoc,
"\n mutation CreateSavedViewGroup($input: CreateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n createGroup(input: $input) {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n": typeof types.CreateSavedViewGroupDocument,
"\n mutation DeleteSavedViewGroup($input: DeleteSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n deleteGroup(input: $input)\n }\n }\n }\n": typeof types.DeleteSavedViewGroupDocument,
"\n fragment UseDeleteSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n groupId\n projectId\n isUngroupedViewsGroup\n }\n": typeof types.UseDeleteSavedViewGroup_SavedViewGroupFragmentDoc,
"\n mutation UpdateSavedViewGroup($input: UpdateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n updateGroup(input: $input) {\n id\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n }\n }\n }\n": typeof types.UpdateSavedViewGroupDocument,
"\n fragment UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n projectId\n groupId\n title\n isUngroupedViewsGroup\n }\n": typeof types.UseUpdateSavedViewGroup_SavedViewGroupFragmentDoc,
"\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseSavedViewValidationHelpers_SavedViewFragmentDoc,
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n": typeof types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": typeof types.ViewerCommentThreadFragmentDoc,
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": typeof types.ViewerCommentsReplyItemFragmentDoc,
@@ -505,7 +518,7 @@ type Documents = {
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": typeof types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": typeof types.ProjectPageAutomationPage_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": typeof types.ProjectPageAutomationPage_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": typeof types.SettingsServerProjects_ProjectCollectionFragmentDoc,
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": typeof types.SettingsServerRegionsDocument,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n plan {\n status\n name\n }\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
@@ -515,6 +528,7 @@ type Documents = {
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": typeof types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n ...SettingsWorkspacesSecurityDefaultSeat_Workspace\n ...SettingsWorkspacesSecurityDomainManagement_Workspace\n ...SettingsWorkspacesSecurityDiscoverability_Workspace\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n ...SettingsWorkspacesSecurityDomainProtection_Workspace\n ...SettingsWorkspacesSecurityWorkspaceCreation_Workspace\n id\n slug\n }\n": typeof types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
"\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n }\n": typeof types.WorkspaceDashboards_DashboardFragmentDoc,
"\n fragment WorkspacePage_Workspace on Workspace {\n ...WorkspaceDashboard_Workspace\n ...WorkspaceSidebar_Workspace\n }\n": typeof types.WorkspacePage_WorkspaceFragmentDoc,
};
const documents: Documents = {
@@ -546,6 +560,12 @@ const documents: Documents = {
"\n fragment BillingAlert_Workspace on Workspace {\n id\n role\n slug\n plan {\n name\n status\n createdAt\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n }\n }\n": types.BillingAlert_WorkspaceFragmentDoc,
"\n fragment CommonModelSelectorModel on Model {\n id\n name\n }\n": types.CommonModelSelectorModelFragmentDoc,
"\n query DashboardSidebar {\n activeUser {\n id\n activeWorkspace {\n id\n role\n }\n }\n }\n": types.DashboardSidebarDocument,
"\n query SidebarPermissions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canListDashboards {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.SidebarPermissionsDocument,
"\n fragment DashboardsCard_Dashboard on Dashboard {\n id\n name\n createdAt\n workspace {\n id\n }\n createdBy {\n id\n name\n avatar\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.DashboardsCard_DashboardFragmentDoc,
"\n fragment DashboardsEditDialog_Dashboard on Dashboard {\n id\n name\n workspace {\n id\n }\n }\n": types.DashboardsEditDialog_DashboardFragmentDoc,
"\n query DashboardsListCanCreateDashboards($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canCreateDashboards {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.DashboardsListCanCreateDashboardsDocument,
"\n query DashboardsSharePermissions($id: String!) {\n dashboard(id: $id) {\n id\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n": types.DashboardsSharePermissionsDocument,
"\n mutation DashboardsShareToken($dashboardId: String!) {\n dashboardMutations {\n createToken(dashboardId: $dashboardId) {\n token\n }\n }\n }\n": types.DashboardsShareTokenDocument,
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": types.FormSelectModels_ModelFragmentDoc,
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n": types.FormSelectSavedViewGroup_SavedViewGroupFragmentDoc,
@@ -673,9 +693,9 @@ const documents: Documents = {
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n": types.ViewerSavedViewsPanel_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelGroups_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n ...ViewerSavedViewsPanelViewsGroup_Project\n }\n": types.ViewerSavedViewsPanelGroups_ProjectFragmentDoc,
"\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n": types.ViewerSavedViewsPanelGroups_SavedViewGroupsDocument,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewDeleteDialog_SavedView on SavedView {\n id\n name\n ...UseDeleteSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewMoveDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ViewerSavedViewsPanelViewsGroup_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n isUngroupedViewsGroup\n title\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n": types.ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragmentDoc,
@@ -755,6 +775,11 @@ const documents: Documents = {
"\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 CreateDashboard(\n $workspace: WorkspaceIdentifier!\n $input: DashboardCreateInput!\n ) {\n dashboardMutations {\n create(workspace: $workspace, input: $input) {\n id\n workspace {\n id\n }\n }\n }\n }\n": types.CreateDashboardDocument,
"\n mutation UpdateDashboard($input: DashboardUpdateInput!) {\n dashboardMutations {\n update(input: $input) {\n id\n name\n }\n }\n }\n": types.UpdateDashboardDocument,
"\n mutation DeleteDashboard($id: String!) {\n dashboardMutations {\n delete(id: $id)\n }\n }\n": types.DeleteDashboardDocument,
"\n query Dashboard($id: String!) {\n dashboard(id: $id) {\n id\n ...WorkspaceDashboards_Dashboard\n }\n }\n": types.DashboardDocument,
"\n query WorkspaceDashboards($workspaceSlug: String!, $cursor: String) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n dashboards(cursor: $cursor) {\n cursor\n items {\n id\n ...DashboardsCard_Dashboard\n }\n }\n }\n }\n": types.WorkspaceDashboardsDocument,
"\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,
"\n mutation DeleteApplication($appId: String!) {\n appDelete(appId: $appId)\n }\n": types.DeleteApplicationDocument,
@@ -900,8 +925,9 @@ const documents: Documents = {
"\n fragment AppAuthorAvatar on AppAuthor {\n id\n name\n avatar\n }\n": types.AppAuthorAvatarFragmentDoc,
"\n fragment LimitedUserAvatar on LimitedUser {\n id\n name\n avatar\n }\n": types.LimitedUserAvatarFragmentDoc,
"\n fragment ActiveUserAvatar on User {\n id\n name\n avatar\n }\n": types.ActiveUserAvatarFragmentDoc,
"\n query ActiveUserMeta {\n activeUser {\n meta {\n legacyProjectsExplainerCollapsed\n }\n }\n }\n": types.ActiveUserMetaDocument,
"\n query ActiveUserMeta {\n activeUser {\n meta {\n legacyProjectsExplainerCollapsed\n intelligenceCommunityStandUpBannerDismissed\n }\n }\n }\n": types.ActiveUserMetaDocument,
"\n mutation UpdateLegacyProjectsExplainer($value: Boolean!) {\n activeUserMutations {\n meta {\n setLegacyProjectsExplainerCollapsed(value: $value)\n }\n }\n }\n": types.UpdateLegacyProjectsExplainerDocument,
"\n mutation UpdateIntelligenceCommunityStandUpBannerDismissed($value: Boolean!) {\n activeUserMutations {\n meta {\n setIntelligenceCommunityStandUpBannerDismissed(value: $value)\n }\n }\n }\n": types.UpdateIntelligenceCommunityStandUpBannerDismissedDocument,
"\n subscription OnUserProjectsUpdate {\n userProjectsUpdated {\n type\n id\n project {\n ...ProjectDashboardItem\n workspaceId\n }\n }\n }\n ": types.OnUserProjectsUpdateDocument,
"\n mutation UpdateUser($input: UserUpdateInput!) {\n activeUserMutations {\n update(user: $input) {\n id\n name\n bio\n company\n avatar\n }\n }\n }\n": types.UpdateUserDocument,
"\n mutation UpdateNotificationPreferences($input: JSONObject!) {\n userNotificationPreferencesUpdate(preferences: $input)\n }\n": types.UpdateNotificationPreferencesDocument,
@@ -919,12 +945,13 @@ const documents: Documents = {
"\n mutation DeleteSavedView($input: DeleteSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n deleteView(input: $input)\n }\n }\n }\n": types.DeleteSavedViewDocument,
"\n fragment UseDeleteSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n": types.UseDeleteSavedView_SavedViewFragmentDoc,
"\n mutation UpdateSavedView($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n": types.UpdateSavedViewDocument,
"\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n visibility\n group {\n id\n }\n }\n": types.UseUpdateSavedView_SavedViewFragmentDoc,
"\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n isHomeView\n groupResourceIds\n group {\n id\n }\n }\n": types.UseUpdateSavedView_SavedViewFragmentDoc,
"\n mutation CreateSavedViewGroup($input: CreateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n createGroup(input: $input) {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n": types.CreateSavedViewGroupDocument,
"\n mutation DeleteSavedViewGroup($input: DeleteSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n deleteGroup(input: $input)\n }\n }\n }\n": types.DeleteSavedViewGroupDocument,
"\n fragment UseDeleteSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n groupId\n projectId\n isUngroupedViewsGroup\n }\n": types.UseDeleteSavedViewGroup_SavedViewGroupFragmentDoc,
"\n mutation UpdateSavedViewGroup($input: UpdateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n updateGroup(input: $input) {\n id\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n }\n }\n }\n": types.UpdateSavedViewGroupDocument,
"\n fragment UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n projectId\n groupId\n title\n isUngroupedViewsGroup\n }\n": types.UseUpdateSavedViewGroup_SavedViewGroupFragmentDoc,
"\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseSavedViewValidationHelpers_SavedViewFragmentDoc,
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n": types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": types.ViewerCommentThreadFragmentDoc,
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": types.ViewerCommentsReplyItemFragmentDoc,
@@ -1009,7 +1036,7 @@ const documents: Documents = {
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n workspace {\n id\n }\n permissions {\n canReadSettings {\n ...FullPermissionCheckResult\n }\n canReadAccIntegrationSettings {\n ...FullPermissionCheckResult\n }\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMoveToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n ...WorkspaceMoveProjectManager_ProjectBase\n ...ProjectPageSettingsTab_Project\n ...WorkspaceMoveProject_Project\n }\n": types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": types.ProjectPageAutomationPage_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n workspaceId\n ...ProjectPageAutomationHeader_Project\n }\n": types.ProjectPageAutomationPage_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
"\n fragment SettingsServerProjects_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...SettingsSharedProjects_Project\n }\n }\n": types.SettingsServerProjects_ProjectCollectionFragmentDoc,
"\n query SettingsServerRegions {\n serverInfo {\n multiRegion {\n regions {\n id\n ...SettingsServerRegionsTable_ServerRegionItem\n }\n availableKeys\n }\n }\n }\n": types.SettingsServerRegionsDocument,
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n ...SettingsWorkspacesGeneralEditSlugDialog_Workspace\n id\n name\n slug\n description\n logo\n role\n plan {\n status\n name\n }\n embedOptions {\n hideSpeckleBranding\n }\n permissions {\n canEditEmbedOptions {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
@@ -1019,6 +1046,7 @@ const documents: Documents = {
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n ...SettingsWorkspacesSecurityDefaultSeat_Workspace\n ...SettingsWorkspacesSecurityDomainManagement_Workspace\n ...SettingsWorkspacesSecurityDiscoverability_Workspace\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n ...SettingsWorkspacesSecurityDomainProtection_Workspace\n ...SettingsWorkspacesSecurityWorkspaceCreation_Workspace\n id\n slug\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
"\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n }\n": types.WorkspaceDashboards_DashboardFragmentDoc,
"\n fragment WorkspacePage_Workspace on Workspace {\n ...WorkspaceDashboard_Workspace\n ...WorkspaceSidebar_Workspace\n }\n": types.WorkspacePage_WorkspaceFragmentDoc,
};
@@ -1148,6 +1176,30 @@ export function graphql(source: "\n fragment CommonModelSelectorModel on Model
* 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 DashboardSidebar {\n activeUser {\n id\n activeWorkspace {\n id\n role\n }\n }\n }\n"): (typeof documents)["\n query DashboardSidebar {\n activeUser {\n id\n activeWorkspace {\n id\n role\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 SidebarPermissions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canListDashboards {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"): (typeof documents)["\n query SidebarPermissions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canListDashboards {\n ...FullPermissionCheckResult\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 DashboardsCard_Dashboard on Dashboard {\n id\n name\n createdAt\n workspace {\n id\n }\n createdBy {\n id\n name\n avatar\n }\n permissions {\n canDelete {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment DashboardsCard_Dashboard on Dashboard {\n id\n name\n createdAt\n workspace {\n id\n }\n createdBy {\n id\n name\n avatar\n }\n permissions {\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.
*/
export function graphql(source: "\n fragment DashboardsEditDialog_Dashboard on Dashboard {\n id\n name\n workspace {\n id\n }\n }\n"): (typeof documents)["\n fragment DashboardsEditDialog_Dashboard on Dashboard {\n id\n name\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.
*/
export function graphql(source: "\n query DashboardsListCanCreateDashboards($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canCreateDashboards {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"): (typeof documents)["\n query DashboardsListCanCreateDashboards($slug: String!) {\n workspaceBySlug(slug: $slug) {\n permissions {\n canCreateDashboards {\n ...FullPermissionCheckResult\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 DashboardsSharePermissions($id: String!) {\n dashboard(id: $id) {\n id\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\n }\n }\n }\n }\n"): (typeof documents)["\n query DashboardsSharePermissions($id: String!) {\n dashboard(id: $id) {\n id\n permissions {\n canCreateToken {\n ...FullPermissionCheckResult\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 mutation DashboardsShareToken($dashboardId: String!) {\n dashboardMutations {\n createToken(dashboardId: $dashboardId) {\n token\n }\n }\n }\n"): (typeof documents)["\n mutation DashboardsShareToken($dashboardId: String!) {\n dashboardMutations {\n createToken(dashboardId: $dashboardId) {\n token\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1659,7 +1711,7 @@ export function graphql(source: "\n query ViewerSavedViewsPanelGroups_SavedView
/**
* 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 ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n"];
export function graphql(source: "\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1667,7 +1719,7 @@ export function graphql(source: "\n fragment ViewerSavedViewsPanelViewDeleteDia
/**
* 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 ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n"];
export function graphql(source: "\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1984,6 +2036,26 @@ export function graphql(source: "\n fragment UseFileImport_Model on Model {\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 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 documents)["\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"];
/**
* 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 CreateDashboard(\n $workspace: WorkspaceIdentifier!\n $input: DashboardCreateInput!\n ) {\n dashboardMutations {\n create(workspace: $workspace, input: $input) {\n id\n workspace {\n id\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateDashboard(\n $workspace: WorkspaceIdentifier!\n $input: DashboardCreateInput!\n ) {\n dashboardMutations {\n create(workspace: $workspace, input: $input) {\n id\n workspace {\n id\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 mutation UpdateDashboard($input: DashboardUpdateInput!) {\n dashboardMutations {\n update(input: $input) {\n id\n name\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateDashboard($input: DashboardUpdateInput!) {\n dashboardMutations {\n update(input: $input) {\n id\n name\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation DeleteDashboard($id: String!) {\n dashboardMutations {\n delete(id: $id)\n }\n }\n"): (typeof documents)["\n mutation DeleteDashboard($id: String!) {\n dashboardMutations {\n delete(id: $id)\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 Dashboard($id: String!) {\n dashboard(id: $id) {\n id\n ...WorkspaceDashboards_Dashboard\n }\n }\n"): (typeof documents)["\n query Dashboard($id: String!) {\n dashboard(id: $id) {\n id\n ...WorkspaceDashboards_Dashboard\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 WorkspaceDashboards($workspaceSlug: String!, $cursor: String) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n dashboards(cursor: $cursor) {\n cursor\n items {\n id\n ...DashboardsCard_Dashboard\n }\n }\n }\n }\n"): (typeof documents)["\n query WorkspaceDashboards($workspaceSlug: String!, $cursor: String) {\n workspaceBySlug(slug: $workspaceSlug) {\n id\n dashboards(cursor: $cursor) {\n cursor\n items {\n id\n ...DashboardsCard_Dashboard\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2567,11 +2639,15 @@ export function graphql(source: "\n fragment ActiveUserAvatar on User {\n id
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query ActiveUserMeta {\n activeUser {\n meta {\n legacyProjectsExplainerCollapsed\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserMeta {\n activeUser {\n meta {\n legacyProjectsExplainerCollapsed\n }\n }\n }\n"];
export function graphql(source: "\n query ActiveUserMeta {\n activeUser {\n meta {\n legacyProjectsExplainerCollapsed\n intelligenceCommunityStandUpBannerDismissed\n }\n }\n }\n"): (typeof documents)["\n query ActiveUserMeta {\n activeUser {\n meta {\n legacyProjectsExplainerCollapsed\n intelligenceCommunityStandUpBannerDismissed\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 UpdateLegacyProjectsExplainer($value: Boolean!) {\n activeUserMutations {\n meta {\n setLegacyProjectsExplainerCollapsed(value: $value)\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateLegacyProjectsExplainer($value: Boolean!) {\n activeUserMutations {\n meta {\n setLegacyProjectsExplainerCollapsed(value: $value)\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 UpdateIntelligenceCommunityStandUpBannerDismissed($value: Boolean!) {\n activeUserMutations {\n meta {\n setIntelligenceCommunityStandUpBannerDismissed(value: $value)\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateIntelligenceCommunityStandUpBannerDismissed($value: Boolean!) {\n activeUserMutations {\n meta {\n setIntelligenceCommunityStandUpBannerDismissed(value: $value)\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2643,7 +2719,7 @@ export function graphql(source: "\n mutation UpdateSavedView($input: UpdateSave
/**
* 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 UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n visibility\n group {\n id\n }\n }\n"): (typeof documents)["\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n visibility\n group {\n id\n }\n }\n"];
export function graphql(source: "\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n isHomeView\n groupResourceIds\n group {\n id\n }\n }\n"): (typeof documents)["\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n isHomeView\n groupResourceIds\n group {\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2664,6 +2740,10 @@ export function graphql(source: "\n mutation UpdateSavedViewGroup($input: Updat
* 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 UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n projectId\n groupId\n title\n isUngroupedViewsGroup\n }\n"): (typeof documents)["\n fragment UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n projectId\n groupId\n title\n isUngroupedViewsGroup\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 UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\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.
*/
@@ -3003,7 +3083,7 @@ export function graphql(source: "\n fragment ProjectPageAutomationPage_Project
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
export function graphql(source: "\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageSettingsTab_Project on Project {\n id\n name\n permissions {\n canReadWebhooks {\n ...FullPermissionCheckResult\n }\n canReadEmbedTokens {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -3040,6 +3120,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesRegions_ServerIn
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n ...SettingsWorkspacesSecurityDefaultSeat_Workspace\n ...SettingsWorkspacesSecurityDomainManagement_Workspace\n ...SettingsWorkspacesSecurityDiscoverability_Workspace\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n ...SettingsWorkspacesSecurityDomainProtection_Workspace\n ...SettingsWorkspacesSecurityWorkspaceCreation_Workspace\n id\n slug\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n ...SettingsWorkspacesSecurityDefaultSeat_Workspace\n ...SettingsWorkspacesSecurityDomainManagement_Workspace\n ...SettingsWorkspacesSecurityDiscoverability_Workspace\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n ...SettingsWorkspacesSecurityDomainProtection_Workspace\n ...SettingsWorkspacesSecurityWorkspaceCreation_Workspace\n id\n slug\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 WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\n }\n }\n"): (typeof documents)["\n fragment WorkspaceDashboards_Dashboard on Dashboard {\n ...DashboardsEditDialog_Dashboard\n id\n name\n createdBy {\n id\n name\n avatar\n }\n createdAt\n updatedAt\n workspace {\n id\n name\n slug\n logo\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
@@ -90,6 +90,8 @@ export function getCacheId<Type extends keyof AllObjectTypes>(
return cachedId as ApolloCacheObjectKey<Type>
}
export const getCacheKey = getCacheId
export function isInvalidAuth(error: ApolloError | NetworkError) {
const networkError = error instanceof ApolloError ? error.networkError : error
if (
@@ -311,6 +313,14 @@ export function getObjectReference<Type extends keyof AllObjectTypes>(
} as CacheObjectReference<Type>
}
export const keyToRef = <Type extends keyof AllObjectTypes>(
key: ApolloCacheObjectKey<Type>
): CacheObjectReference<Type> => {
return {
__ref: key
} as CacheObjectReference<Type>
}
export function isReference(obj: unknown): obj is CacheObjectReference {
return has(obj, '__ref')
}
@@ -584,6 +594,18 @@ type ModifyObjectFieldValue<
Field extends keyof AllObjectTypes[Type]
> = ModifyFnCacheData<AllObjectTypes[Type][Field]>
/**
* Get keys of all cached objects by type
*/
export const getCachedObjectKeys = <Type extends keyof AllObjectTypes>(
cache: ApolloCache<unknown>,
type: Type
): ApolloCacheObjectKey<Type>[] => {
const data = cache.extract() as Record<string, unknown>
const objectIds = Object.keys(data).filter((k) => k.startsWith(`${type}:`))
return objectIds as ApolloCacheObjectKey<Type>[]
}
/**
* Simplified & improved version of modifyObjectFields, just targetting a single field for a cache modification
* @see modifyObjectFields
@@ -644,11 +666,23 @@ export const modifyObjectField = <
>(
ref: CacheObjectReference<ReadFieldType>,
fieldName: ReadFieldName
) => Optional<AllObjectTypes[ReadFieldType][ReadFieldName]>
) => Optional<ModifyFnCacheData<AllObjectTypes[ReadFieldType][ReadFieldName]>>
/**
* Get the object we're modifying as a readable object
*/
readObject: () => Partial<{
[prop in keyof AllObjectTypes[Type]]: ModifyFnCacheData<
AllObjectTypes[Type][prop]
>
}>
/**
* Build a reference object for a specific object in the cache
*/
ref: typeof getObjectReference
/**
* Build a reference object from a key
*/
keyToRef: typeof keyToRef
/**
* Parse a reference object to get its type and id separately
*/
@@ -736,6 +770,7 @@ export const modifyObjectField = <
path: Path
) => getFromPathIfExists<ModifyObjectFieldValue<Type, Field>, Path>(value, path)
const evict = () => details.DELETE
const readField = <
ReadFieldType extends keyof AllObjectTypes,
ReadFieldName extends keyof AllObjectTypes[ReadFieldType] & string
@@ -743,10 +778,26 @@ export const modifyObjectField = <
ref: CacheObjectReference<ReadFieldType>,
fieldName: ReadFieldName
) =>
details.readField(
fieldName,
ref
) as AllObjectTypes[ReadFieldType][ReadFieldName]
details.readField(fieldName, ref) as Optional<
ModifyFnCacheData<AllObjectTypes[ReadFieldType][ReadFieldName]>
>
const readObject = () =>
new Proxy(
{} as Partial<{
[prop in keyof AllObjectTypes[Type]]: ModifyFnCacheData<
AllObjectTypes[Type][prop]
>
}>,
{
get(_target, prop) {
if (!isString(prop)) return undefined
const ref = keyToRef(key)
return details.readField(prop, ref)
}
}
)
return updater({
fieldName: field,
@@ -758,7 +809,9 @@ export const modifyObjectField = <
evict,
readField,
ref: getObjectReference,
fromRef: parseObjectReference
fromRef: parseObjectReference,
keyToRef,
readObject
}
})
},
@@ -803,7 +856,15 @@ export const iterateObjectField = <
>(
ref: CacheObjectReference<ReadFieldType>,
fieldName: ReadFieldName
) => Optional<AllObjectTypes[ReadFieldType][ReadFieldName]>
) => Optional<ModifyFnCacheData<AllObjectTypes[ReadFieldType][ReadFieldName]>>
/**
* Get the object we're modifying as a readable object
*/
readObject: () => Partial<{
[prop in keyof AllObjectTypes[Type]]: ModifyFnCacheData<
AllObjectTypes[Type][prop]
>
}>
/**
* Build a reference object for a specific object in the cache
*/
@@ -152,11 +152,18 @@ export const workspaceRoute = (slug: MaybeNullOrUndefined<string>) =>
slug ? `/workspaces/${slug}` : '/'
export const workspaceSsoRoute = (slug: string) => `/workspaces/${slug}/sso`
export const dashboardsRoute = (slug?: MaybeNullOrUndefined<string>) =>
`/workspaces/${slug}/dashboards`
export const dashboardRoute = (slug?: MaybeNullOrUndefined<string>, id?: string) =>
`/workspaces/${slug}/dashboards/${id}`
export const workspaceCreateRoute = '/workspaces/actions/create'
export const workspaceJoinRoute = '/workspaces/actions/join'
export const workspaceFunctionsRoute = (slug: string) => `/workspaces/${slug}/functions`
export const workspaceFunctionsRoute = (slug?: string) =>
`/workspaces/${slug}/functions`
const buildNavigationComposable = (route: string) => () => {
const router = useRouter()
@@ -0,0 +1,52 @@
import { computed } from 'vue'
import { useRoute } from 'vue-router'
export function useDashboardEmbed() {
const route = useRoute()
const isEmbedEnabled = computed(() => {
// Check for embed parameter in query string
if (route.query.embed === 'true') {
return true
}
// Check for embed parameter in hash fragment
if (route.hash) {
const hashParams = new URLSearchParams(route.hash.substring(1))
const embedParam = hashParams.get('embed')
if (embedParam === 'true') {
return true
}
}
// Check if the page is being loaded in an iframe
if (import.meta.client) {
try {
return window.self !== window.top
} catch {
// If we can't access window.top due to cross-origin restrictions,
// it's likely an embed
return true
}
}
return false
})
const isCrossOriginEmbed = computed(() => {
if (!isEmbedEnabled.value || !import.meta.client) return false
try {
return window.location.origin !== window.top?.location.origin
} catch {
// If we can't access window.top.location.origin due to cross-origin restrictions,
// it's definitely a cross-origin embed
return true
}
})
return {
isEmbedEnabled,
isCrossOriginEmbed
}
}
@@ -0,0 +1,148 @@
import { useApolloClient, useMutation } from '@vue/apollo-composable'
import {
getFirstErrorMessage,
modifyObjectField,
getCacheId
} from '~/lib/common/helpers/graphql'
import type {
DashboardCreateInput,
DashboardUpdateInput,
WorkspaceIdentifier
} from '~/lib/common/generated/gql/graphql'
import {
createDashboardMutation,
updateDashboardMutation,
deleteDashboardMutation
} from '~/lib/dashboards/graphql/mutations'
import { useMixpanel } from '~/lib/core/composables/mp'
export function useCreateDashboard() {
const apollo = useApolloClient().client
const { triggerNotification } = useGlobalToast()
const { activeUser } = useActiveUser()
const { track } = useMixpanel()
return async (options: {
identifier: WorkspaceIdentifier
input: DashboardCreateInput
}) => {
const userId = activeUser.value?.id
if (!userId) return
const { identifier, input } = options
const res = await apollo
.mutate({
mutation: createDashboardMutation,
variables: { workspace: identifier, input },
update: (cache, { data }) => {
const dashboardId = data?.dashboardMutations.create.id
const workspaceId = data?.dashboardMutations.create.workspace.id
if (!dashboardId || !workspaceId) return
modifyObjectField(
cache,
getCacheId('Workspace', workspaceId),
'dashboards',
({ helpers: { createUpdatedValue, ref } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (totalCount) => totalCount + 1)
update('items', (items) => [ref('Dashboard', dashboardId), ...items])
})
},
{
autoEvictFiltered: true
}
)
}
})
.catch(convertThrowIntoFetchResult)
if (res.data?.dashboardMutations.create) {
track('Dashboard Created', {
// eslint-disable-next-line camelcase
workspace_id: res.data.dashboardMutations.create.workspace.id
})
triggerNotification({
type: ToastNotificationType.Success,
title: 'Dashboard successfully created'
})
} else {
const err = getFirstErrorMessage(res.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Dashboard creation failed',
description: err
})
}
return res
}
}
export function useUpdateDashboard() {
const { mutate } = useMutation(updateDashboardMutation)
const { triggerNotification } = useGlobalToast()
const { track } = useMixpanel()
return async (input: DashboardUpdateInput, workspaceId: string) => {
const result = await mutate({ input }).catch(convertThrowIntoFetchResult)
if (result?.data?.dashboardMutations.update) {
track('Dashboard Updated', {
// eslint-disable-next-line camelcase
workspace_id: workspaceId
})
triggerNotification({
type: ToastNotificationType.Success,
title: 'Dashboard successfully updated'
})
} else {
const err = getFirstErrorMessage(result?.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Dashboard update failed',
description: err
})
}
}
}
export function useDeleteDashboard() {
const apollo = useApolloClient().client
const { triggerNotification } = useGlobalToast()
const { track } = useMixpanel()
return async (id: string, workspaceId: string) => {
const res = await apollo
.mutate({
mutation: deleteDashboardMutation,
variables: { id },
update: (cache, { data }) => {
if (!data?.dashboardMutations?.delete) return
cache.evict({ id: getCacheId('Dashboard', id) })
}
})
.catch(convertThrowIntoFetchResult)
if (res.data?.dashboardMutations.delete) {
track('Dashboard Deleted', {
// eslint-disable-next-line camelcase
workspace_id: workspaceId
})
triggerNotification({
type: ToastNotificationType.Success,
title: 'Dashboard successfully deleted'
})
} else {
const err = getFirstErrorMessage(res.errors)
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Dashboard deletion failed',
description: err
})
}
}
}
@@ -0,0 +1,36 @@
import { graphql } from '~~/lib/common/generated/gql'
export const createDashboardMutation = graphql(`
mutation CreateDashboard(
$workspace: WorkspaceIdentifier!
$input: DashboardCreateInput!
) {
dashboardMutations {
create(workspace: $workspace, input: $input) {
id
workspace {
id
}
}
}
}
`)
export const updateDashboardMutation = graphql(`
mutation UpdateDashboard($input: DashboardUpdateInput!) {
dashboardMutations {
update(input: $input) {
id
name
}
}
}
`)
export const deleteDashboardMutation = graphql(`
mutation DeleteDashboard($id: String!) {
dashboardMutations {
delete(id: $id)
}
}
`)
@@ -0,0 +1,25 @@
import { graphql } from '~~/lib/common/generated/gql'
export const dashboardQuery = graphql(`
query Dashboard($id: String!) {
dashboard(id: $id) {
id
...WorkspaceDashboards_Dashboard
}
}
`)
export const workspaceDashboardsQuery = graphql(`
query WorkspaceDashboards($workspaceSlug: String!, $cursor: String) {
workspaceBySlug(slug: $workspaceSlug) {
id
dashboards(cursor: $cursor) {
cursor
items {
id
...DashboardsCard_Dashboard
}
}
}
}
`)
@@ -37,6 +37,8 @@ const usePreviewsState = () =>
/**
* Get authenticated preview image URL and subscribes to preview image generation events so that the preview image URL
* is updated whenever generation finishes
*
* TODO: Refactor, the internals have gotten very messy and overly complicated
*/
export function usePreviewImageBlob(
previewUrl: MaybeRef<string | null | undefined>,
@@ -281,22 +283,16 @@ export function usePreviewImageBlob(
})
}
return {
...ret,
/**
* Run this at the bottom of the component to fully initialize it
*/
init: async () => {
if (!eagerLoad && import.meta.server) {
return // don't do anything - show spinner
}
const promise = regeneratePreviews()
if (eagerLoad) {
await promise
}
const init = () => {
if (!eagerLoad && import.meta.server) {
return // don't do anything - show spinner
}
void regeneratePreviews()
}
init()
return ret
}
export function useCommentScreenshotImage(
@@ -6,6 +6,7 @@ export const activeUserMetaQuery = graphql(`
activeUser {
meta {
legacyProjectsExplainerCollapsed
intelligenceCommunityStandUpBannerDismissed
}
}
}
@@ -21,11 +22,24 @@ export const updateLegacyProjectsExplainerMutation = graphql(`
}
`)
export const updateIntelligenceCommunityStandUpBannerDismissedMutation = graphql(`
mutation UpdateIntelligenceCommunityStandUpBannerDismissed($value: Boolean!) {
activeUserMutations {
meta {
setIntelligenceCommunityStandUpBannerDismissed(value: $value)
}
}
}
`)
export function useActiveUserMeta() {
const { result } = useQuery(activeUserMetaQuery)
const { mutate: updateLegacyProjectsExplainer } = useMutation(
updateLegacyProjectsExplainerMutation
)
const { mutate: updateIntelligenceCommunityStandUpBanner } = useMutation(
updateIntelligenceCommunityStandUpBannerDismissedMutation
)
const apollo = useApolloClient().client
const cache = apollo.cache
const { activeUser } = useActiveUser()
@@ -37,6 +51,10 @@ export function useActiveUserMeta() {
() => meta.value?.legacyProjectsExplainerCollapsed
)
const hasDismissedIntelligenceCommunityStandUpBanner = computed(
() => meta.value?.intelligenceCommunityStandUpBannerDismissed
)
const updateLegacyProjectsExplainerCollapsed = async (value: boolean) => {
await updateLegacyProjectsExplainer({ value })
@@ -51,8 +69,24 @@ export function useActiveUserMeta() {
)
}
const updateIntelligenceCommunityStandUpBannerDismissed = async (value: boolean) => {
await updateIntelligenceCommunityStandUpBanner({ value })
modifyObjectField(
cache,
getCacheId('User', activeUserId.value),
'meta',
({ helpers: { createUpdatedValue } }) =>
createUpdatedValue(({ update }) => {
update('intelligenceCommunityStandUpBannerDismissed', () => value)
})
)
}
return {
hasCollapsedLegacyProjectsExplainer,
updateLegacyProjectsExplainerCollapsed
updateLegacyProjectsExplainerCollapsed,
hasDismissedIntelligenceCommunityStandUpBanner,
updateIntelligenceCommunityStandUpBannerDismissed
}
}
@@ -20,7 +20,10 @@ import {
} from '~/lib/viewer/helpers/savedViews/cache'
import { isUngroupedGroup } from '@speckle/shared/saved-views'
import type { Optional } from '@speckle/shared'
import type { CacheObjectReference } from '~/lib/common/helpers/graphql'
import {
getCachedObjectKeys,
type CacheObjectReference
} from '~/lib/common/helpers/graphql'
const createSavedViewMutation = graphql(`
mutation CreateSavedView($input: CreateSavedViewInput!) {
@@ -220,7 +223,8 @@ graphql(`
fragment UseUpdateSavedView_SavedView on SavedView {
id
projectId
visibility
isHomeView
groupResourceIds
group {
id
}
@@ -240,7 +244,6 @@ export const useUpdateSavedView = () => {
const { input } = params
const oldGroupId = params.view.group.id
const oldVisibility = params.view.visibility
const result = await mutate(
{ input },
@@ -267,16 +270,43 @@ export const useUpdateSavedView = () => {
})
}
const newVisibility = update.visibility
const visibilityChanged = oldVisibility !== newVisibility
if (visibilityChanged) {
// Update all SavedViewGroup.views to see if it now should appear in there or not
modifyObjectField(
cache,
getCacheId('SavedViewGroup', newGroupId),
'views',
({ helpers: { evict } }) => evict()
)
// W/ current filter setup, if u can change visibility, you're gonna see it in all filtered groups
// const newVisibility = update.visibility
// const visibilityChanged = oldVisibility !== newVisibility
// if (visibilityChanged) {
// // Update all SavedViewGroup.views to see if it now should appear in there or not
// modifyObjectField(
// cache,
// getCacheId('SavedViewGroup', newGroupId),
// 'views',
// ({ helpers: { evict } }) => evict()
// )
// }
// If set to home view, clear home view on all other views related to the same resourceIdString
if (update.isHomeView && update.groupResourceIds.length === 1) {
const allSavedViewKeys = getCachedObjectKeys(cache, 'SavedView')
const modelId = update.groupResourceIds[0]
for (const savedViewKey of allSavedViewKeys) {
modifyObjectField(
cache,
savedViewKey,
'isHomeView',
({ value: isHomeView, helpers: { readObject } }) => {
const view = readObject()
const groupIds = view.groupResourceIds
const viewId = view.id
const projectId = view.projectId
if (viewId === update.id) return
if (update.projectId !== projectId) return
if (isHomeView && groupIds?.length === 1 && groupIds[0] === modelId) {
return false
}
}
)
}
}
}
}
@@ -291,7 +321,7 @@ export const useUpdateSavedView = () => {
} else {
const err = getFirstGqlErrorMessage(result?.errors)
triggerNotification({
title: "Couldn't update saved view",
title: "Couldn't update view",
description: err,
type: ToastNotificationType.Danger
})
@@ -0,0 +1,134 @@
import type { MaybeNullOrUndefined, Optional } from '@speckle/shared'
import type { GenericValidateFunction } from 'vee-validate'
import { graphql } from '~/lib/common/generated/gql/gql'
import {
SavedViewVisibility,
type UseSavedViewValidationHelpers_SavedViewFragment
} from '~/lib/common/generated/gql/graphql'
import { Globe, Lock } from 'lucide-vue-next'
import type { FormRadioGroupItem } from '@speckle/ui-components'
import { useMutationLoading } from '@vue/apollo-composable'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
graphql(`
fragment UseSavedViewValidationHelpers_SavedView on SavedView {
id
isHomeView
visibility
permissions {
canUpdate {
...FullPermissionCheckResult
}
}
}
`)
export const useSavedViewValidationHelpers = (params: {
view: ComputedRef<
MaybeNullOrUndefined<UseSavedViewValidationHelpers_SavedViewFragment>
>
}) => {
const homeViewPrivateError = 'A home view must be shared'
const isLoading = useMutationLoading()
const {
resources: {
response: { isFederatedView }
}
} = useInjectedViewerState()
const canUpdate = computed(() => params.view.value?.permissions.canUpdate)
const isOnlyVisibleToMe = computed(
() => params.view.value?.visibility === SavedViewVisibility.AuthorOnly
)
const isHomeView = computed(() => params.view.value?.isHomeView)
/**
* Visibility options for visibility radio group
*/
const visibilityOptions = computed((): FormRadioGroupItem<SavedViewVisibility>[] => [
{
value: SavedViewVisibility.Public,
title: 'Shared',
introduction: 'Visible to anyone with access to the model',
icon: Globe
},
{
value: SavedViewVisibility.AuthorOnly,
title: 'Private',
introduction: 'Visible only to the view author',
icon: Lock,
...(params.view.value?.isHomeView
? {
disabled: true,
help: homeViewPrivateError
}
: {})
}
])
const canSetHomeView = computed(
(): { authorized: boolean; message: Optional<string> } => {
if (!canUpdate.value?.authorized || isLoading.value) {
return {
authorized: false,
message: canUpdate.value?.errorMessage || undefined
}
}
if (isFederatedView.value) {
return {
authorized: false,
message: "Home view settings can't be updated while in a federated view"
}
}
if (isOnlyVisibleToMe.value) {
return {
authorized: false,
message: 'A view must be shared to be set as home view'
}
}
return { authorized: true, message: undefined }
}
)
const canToggleVisibility = computed(() => {
if (!canUpdate.value?.authorized || isLoading.value) {
return {
authorized: false,
message: canUpdate.value?.errorMessage || undefined
}
}
if (isHomeView.value && !isOnlyVisibleToMe.value) {
return {
authorized: false,
message: homeViewPrivateError
}
}
return { authorized: true, message: undefined }
})
/**
* Vee-validate rule for visibility checks
*/
const validateVisibility: GenericValidateFunction<SavedViewVisibility> = (value) => {
if (!params.view.value) return true
if (!params.view.value.isHomeView) return true
return value === SavedViewVisibility.AuthorOnly ? homeViewPrivateError : true
}
return {
validateVisibility,
visibilityOptions,
canUpdate,
isOnlyVisibleToMe,
canSetHomeView,
isHomeView,
canToggleVisibility
}
}
@@ -2,7 +2,7 @@ import {
useInjectedViewerState,
useResetUiState
} from '~~/lib/viewer/composables/setup'
import { SpeckleViewer } from '@speckle/shared'
import { isUndefinedOrVoid, SpeckleViewer } from '@speckle/shared'
import { get } from 'lodash-es'
import { Vector3 } from 'three'
import {
@@ -10,10 +10,8 @@ import {
useFilterUtilities,
useSelectionUtilities
} from '~~/lib/viewer/composables/ui'
import { CameraController, ViewMode, VisualDiffMode } from '@speckle/viewer'
import { CameraController, VisualDiffMode } from '@speckle/viewer'
import type { Merge, PartialDeep } from 'type-fest'
import type { SectionBoxData } from '@speckle/shared/viewer/state'
import { useViewerRealtimeActivityTracker } from '~/lib/viewer/composables/activity'
import {
isModelResource,
@@ -117,7 +115,13 @@ export function useStateSerialization() {
isOrthoProjection: state.ui.camera.isOrthoProjection.value,
zoom: (get(camControls, '_zoom') as unknown as number) || 1 // kinda hacky, _zoom is a protected prop
},
viewMode: state.ui.viewMode.value,
viewMode: {
mode: state.ui.viewMode.mode.value,
edgesEnabled: state.ui.viewMode.edgesEnabled.value,
edgesWeight: state.ui.viewMode.edgesWeight.value,
outlineOpacity: state.ui.viewMode.outlineOpacity.value,
edgesColor: state.ui.viewMode.edgesColor.value
},
sectionBox: state.ui.sectionBox.value ? box : null,
lightConfig: { ...state.ui.lightConfig.value },
explodeFactor: state.ui.explodeFactor.value,
@@ -160,7 +164,8 @@ export function useApplySerializedState() {
explodeFactor,
lightConfig,
diff,
viewMode
viewMode,
sectionBoxContext
},
resources: {
request: { resourceIdString }
@@ -236,9 +241,16 @@ export function useApplySerializedState() {
isOrthoProjection.value = !!state.ui?.camera?.isOrthoProjection
sectionBox.value = state.ui?.sectionBox
? // It's complaining otherwise
(state.ui.sectionBox as SectionBoxData)
? {
min: state.ui.sectionBox.min || [],
max: state.ui.sectionBox.max || [],
rotation: state.ui.sectionBox.rotation || []
}
: null
sectionBoxContext.visible.value = false
if (!sectionBox.value) {
sectionBoxContext.edited.value = false
}
const filters = state.ui?.filters || {}
if (filters.hiddenObjectIds?.length) {
@@ -308,11 +320,16 @@ export function useApplySerializedState() {
}
// Restore view mode
if (state.ui?.viewMode) {
viewMode.value = state.ui.viewMode
} else {
viewMode.value = ViewMode.DEFAULT
}
if (!isUndefinedOrVoid(state.ui?.viewMode?.mode))
viewMode.mode.value = state.ui!.viewMode!.mode
if (!isUndefinedOrVoid(state.ui?.viewMode?.edgesEnabled))
viewMode.edgesEnabled.value = state.ui!.viewMode!.edgesEnabled
if (!isUndefinedOrVoid(state.ui?.viewMode?.edgesWeight))
viewMode.edgesWeight.value = state.ui!.viewMode!.edgesWeight
if (!isUndefinedOrVoid(state.ui?.viewMode?.outlineOpacity))
viewMode.outlineOpacity.value = state.ui!.viewMode!.outlineOpacity
if (!isUndefinedOrVoid(state.ui?.viewMode?.edgesColor))
viewMode.edgesColor.value = state.ui!.viewMode!.edgesColor
explodeFactor.value = state.ui?.explodeFactor || 0
lightConfig.value = {
@@ -6,17 +6,17 @@ import {
MeasurementType,
FilteringExtension
} from '@speckle/viewer'
import {
type FilteringState,
type PropertyInfo,
type SunLightConfiguration,
type SpeckleView,
type MeasurementOptions,
type DiffResult,
type Viewer,
type WorldTree,
type VisualDiffMode,
ViewMode
import type {
ViewMode,
FilteringState,
PropertyInfo,
SunLightConfiguration,
SpeckleView,
MeasurementOptions,
DiffResult,
Viewer,
WorldTree,
VisualDiffMode
} from '@speckle/viewer'
import { inject, ref, provide } from 'vue'
import type { ComputedRef, WritableComputedRef, Raw, Ref, ShallowRef } from 'vue'
@@ -87,6 +87,8 @@ import {
useBuildSavedViewsUIState,
type SavedViewsUIState
} from '~/lib/viewer/composables/savedViews/state'
import type { defaultEdgeColorValue } from '~/lib/viewer/composables/setup/viewMode'
import { useViewModesSetup } from '~/lib/viewer/composables/setup/viewMode'
export type LoadedModel = NonNullable<
Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>
@@ -315,7 +317,16 @@ export type InjectableViewerState = Readonly<{
target: Ref<Vector3>
isOrthoProjection: Ref<boolean>
}
viewMode: Ref<ViewMode>
viewMode: {
mode: Ref<ViewMode>
edgesEnabled: Ref<boolean>
edgesWeight: Ref<number>
outlineOpacity: Ref<number>
edgesColor: Ref<typeof defaultEdgeColorValue | number>
finalEdgesColor: ComputedRef<number>
defaultEdgesColor: ComputedRef<number>
resetViewMode: () => void
}
diff: {
newVersion: ComputedRef<ViewerModelVersionCardItemFragment | undefined>
oldVersion: ComputedRef<ViewerModelVersionCardItemFragment | undefined>
@@ -1117,7 +1128,7 @@ function setupInterfaceState(
return true
return false
})
const viewMode = ref<ViewMode>(ViewMode.DEFAULT)
const { viewMode } = useViewModesSetup()
const highlightedObjectIds = ref([] as string[])
const spotlightUserSessionId = ref(null as Nullable<string>)
@@ -1156,6 +1167,7 @@ function setupInterfaceState(
return {
...state,
ui: {
viewMode,
diff: {
...diffState
},
@@ -1181,7 +1193,6 @@ function setupInterfaceState(
target,
isOrthoProjection
},
viewMode,
sectionBox: ref(null as Nullable<SectionBoxData>),
sectionBoxContext: {
visible: ref(false),
@@ -1274,7 +1285,7 @@ export function useResetUiState() {
sectionBox.value = null
highlightedObjectIds.value = []
lightConfig.value = { ...DefaultLightConfiguration }
viewMode.value = ViewMode.DEFAULT
viewMode.resetViewMode()
resetFilters()
endDiff()
}
@@ -42,12 +42,18 @@ function useDebugViewer() {
// Get current viewer state
window.VIEWER_STATE = () => fullViewerState
// Get serialized version of current state
// Get serialized version of current state as string
window.VIEWER_SERIALIZED_STATE = (...args: Parameters<typeof serialize>) => {
const serialized = serialize(...args)
return JSON.stringify(serialized)
}
// Get serialized version of current state as object
window.VIEWER_SERIALIZED_STATE_OBJECT = (...args: Parameters<typeof serialize>) => {
const serialized = serialize(...args)
return serialized
}
// Apply viewer state
window.APPLY_VIEWER_STATE = (
state: SpeckleViewer.ViewerState.SerializedViewerState
@@ -52,7 +52,6 @@ import { SafeLocalStorage } from '@speckle/shared'
import {
useCameraUtilities,
useMeasurementUtilities,
useViewModeUtilities,
useFilterUtilities
} from '~~/lib/viewer/composables/ui'
import { setupDebugMode } from '~~/lib/viewer/composables/setup/dev'
@@ -62,6 +61,7 @@ import type { SectionBoxData } from '@speckle/shared/viewer/state'
import { graphql } from '~/lib/common/generated/gql'
import { useTreeManagement } from '~~/lib/viewer/composables/tree'
import { useViewerSavedViewIntegration } from '~/lib/viewer/composables/savedViews/state'
import { useViewModesPostSetup } from '~/lib/viewer/composables/setup/viewMode'
function useViewerLoadCompleteEventHandler() {
const state = useInjectedViewerState()
@@ -877,14 +877,6 @@ function useViewerCursorIntegration() {
})
}
function useViewerViewModesIntegration() {
const { resetViewMode } = useViewModeUtilities()
onBeforeUnmount(() => {
resetViewMode()
})
}
export function useViewerPostSetup() {
if (import.meta.server) return
useViewerObjectAutoLoading()
@@ -905,6 +897,6 @@ export function useViewerPostSetup() {
useDisableZoomOnEmbed()
useViewerCursorIntegration()
useViewerTreeIntegration()
useViewerViewModesIntegration()
useViewModesPostSetup()
setupDebugMode()
}
@@ -0,0 +1,127 @@
import { defaultViewModeEdgeColorValue } from '@speckle/shared/viewer/state'
import { ViewMode, ViewModes } from '@speckle/viewer'
import { watchTriggerable } from '@vueuse/core'
import { useTheme } from '~/lib/core/composables/theme'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { useOnViewerLoadComplete } from '~/lib/viewer/composables/viewer'
export const defaultEdgeColorValue = defaultViewModeEdgeColorValue
export const edgeColorDark = 0x1a1a1a
export const edgeColorLight = 0xffffff
export const useViewModesSetup = () => {
const { isLightTheme } = useTheme()
const mode = ref<ViewMode>(ViewMode.DEFAULT)
const edgesEnabled = ref(true)
const edgesWeight = ref(1)
const outlineOpacity = ref(0.75)
const edgesColor = ref<typeof defaultEdgeColorValue | number>(defaultEdgeColorValue)
const defaultEdgesColor = computed(() => {
// Always default to dark edges, only use light edges in PEN mode + dark theme
if (mode.value === ViewMode.PEN && !isLightTheme.value) {
return edgeColorLight
}
return edgeColorDark
})
const finalEdgesColor = computed(() => {
if (edgesColor.value !== defaultEdgeColorValue) return edgesColor.value
return defaultEdgesColor.value
})
const resetViewMode = () => {
mode.value = ViewMode.DEFAULT
edgesEnabled.value = true
edgesWeight.value = 1
outlineOpacity.value = 0.75
edgesColor.value = defaultEdgeColorValue
}
return {
viewMode: {
mode,
edgesEnabled,
edgesWeight,
outlineOpacity,
edgesColor,
finalEdgesColor,
defaultEdgesColor,
resetViewMode
}
}
}
export const useViewModesPostSetup = () => {
const {
ui: { viewMode },
viewer: { instance }
} = useInjectedViewerState()
const {
mode,
edgesEnabled,
edgesWeight,
outlineOpacity,
finalEdgesColor,
resetViewMode
} = viewMode
const updateViewMode = () => {
const viewModes = instance.getExtension(ViewModes)
if (viewModes) {
viewModes.setViewMode(mode.value, {
edges: edgesEnabled.value,
outlineThickness: edgesWeight.value,
outlineOpacity: outlineOpacity.value,
outlineColor: finalEdgesColor.value
})
}
}
// state -> viewer
useOnViewerLoadComplete(
() => {
updateViewMode()
},
{ initialOnly: true }
)
const { ignoreUpdates: ignoreEdgesEnabledUpdates } = watchTriggerable(
edgesEnabled,
(newVal, oldVal) => {
if (oldVal === newVal) return
updateViewMode()
}
)
watchTriggerable(edgesWeight, (newVal, oldVal) => {
if (oldVal === newVal) return
updateViewMode()
})
const { ignoreUpdates: ignoreOutlineOpacityUpdates } = watchTriggerable(
outlineOpacity,
(newVal, oldVal) => {
if (oldVal === newVal) return
updateViewMode()
}
)
watchTriggerable(finalEdgesColor, (newVal, oldVal) => {
if (oldVal === newVal) return
updateViewMode()
})
watchTriggerable(mode, (newVal, oldVal) => {
if (oldVal === newVal) return
if (newVal === ViewMode.PEN) {
ignoreOutlineOpacityUpdates(() => (outlineOpacity.value = 1))
ignoreEdgesEnabledUpdates(() => (edgesEnabled.value = true))
} else {
ignoreOutlineOpacityUpdates(() => (outlineOpacity.value = 0.75))
}
updateViewMode()
})
onBeforeUnmount(() => {
resetViewMode()
})
}
@@ -1,6 +1,6 @@
import { SpeckleViewer } from '@speckle/shared'
import { type TreeNode, type MeasurementOptions, ViewMode } from '@speckle/viewer'
import { MeasurementsExtension, ViewModes, MeasurementEvent } from '@speckle/viewer'
import type { TreeNode, MeasurementOptions, ViewMode } from '@speckle/viewer'
import { MeasurementsExtension, MeasurementEvent } from '@speckle/viewer'
import { until } from '@vueuse/shared'
import { useActiveElement } from '@vueuse/core'
import { isString } from 'lodash-es'
@@ -19,9 +19,8 @@ import type {
ViewerShortcut,
ViewerShortcutAction
} from '~/lib/viewer/helpers/shortcuts/types'
import { useTheme } from '~/lib/core/composables/theme'
import { useMixpanel } from '~/lib/core/composables/mp'
import type { defaultEdgeColorValue } from '~/lib/viewer/composables/setup/viewMode'
// Re-export filtering utilities
export { useFilterUtilities } from './filtering'
@@ -400,49 +399,11 @@ export function useHighlightedObjectsUtilities() {
}
export function useViewModeUtilities() {
const { instance } = useInjectedViewer()
const { viewMode } = useInjectedViewerInterfaceState()
const { isLightTheme } = useTheme()
const mp = useMixpanel()
const edgesEnabled = ref(true)
const edgesWeight = ref(1)
const outlineOpacity = ref(0.75)
const defaultColor = ref(0x1a1a1a)
const edgesColor = ref(defaultColor.value)
const currentViewMode = computed(() => viewMode.value)
const updateViewMode = () => {
const viewModes = instance.getExtension(ViewModes)
if (viewModes) {
viewModes.setViewMode(currentViewMode.value, {
edges: edgesEnabled.value,
outlineThickness: edgesWeight.value,
outlineOpacity: outlineOpacity.value,
outlineColor: edgesColor.value
})
}
}
const setViewMode = (mode: ViewMode) => {
viewMode.value = mode
if (mode === ViewMode.PEN) {
outlineOpacity.value = 1
edgesEnabled.value = true
if (edgesColor.value === defaultColor.value) {
if (!isLightTheme.value) {
edgesColor.value = 0xffffff
}
}
} else {
outlineOpacity.value = 0.75
if (edgesColor.value === 0xffffff) {
edgesColor.value = isLightTheme.value ? 0xffffff : defaultColor.value
}
}
updateViewMode()
viewMode.mode.value = mode
mp.track('Viewer Action', {
type: 'action',
name: 'set-view-mode',
@@ -451,28 +412,25 @@ export function useViewModeUtilities() {
}
const toggleEdgesEnabled = () => {
edgesEnabled.value = !edgesEnabled.value
updateViewMode()
viewMode.edgesEnabled.value = !viewMode.edgesEnabled.value
mp.track('Viewer Action', {
type: 'action',
name: 'toggle-edges',
enabled: edgesEnabled.value
enabled: viewMode.edgesEnabled.value
})
}
const setEdgesWeight = (weight: number) => {
edgesWeight.value = Number(weight)
updateViewMode()
viewMode.edgesWeight.value = Number(weight)
mp.track('Viewer Action', {
type: 'action',
name: 'set-edges-weight',
weight: edgesWeight.value
weight: viewMode.edgesWeight.value
})
}
const setEdgesColor = (color: number) => {
edgesColor.value = color
updateViewMode()
const setEdgesColor = (color: number | typeof defaultEdgeColorValue) => {
viewMode.edgesColor.value = color
mp.track('Viewer Action', {
type: 'action',
name: 'set-edges-color',
@@ -480,24 +438,13 @@ export function useViewModeUtilities() {
})
}
const resetViewMode = () => {
setViewMode(ViewMode.DEFAULT)
edgesEnabled.value = true
edgesWeight.value = 1
outlineOpacity.value = 0.75
edgesColor.value = defaultColor.value
}
return {
currentViewMode,
viewMode,
setViewMode,
edgesEnabled,
toggleEdgesEnabled,
edgesWeight,
setEdgesWeight,
setEdgesColor,
edgesColor,
resetViewMode
resetViewMode: viewMode.resetViewMode
}
}
@@ -1,16 +1,15 @@
import { throwUncoveredError, type StringEnumValues } from '@speckle/shared'
import { isObjectLike, isString } from 'lodash-es'
import { SavedViewVisibility } from '~/lib/common/generated/gql/graphql'
export const ViewsType = {
Personal: 'personal',
Shared: 'shared'
All: 'all',
Mine: 'mine'
} as const
export type ViewsType = StringEnumValues<typeof ViewsType>
export const viewsTypeLabels: Record<ViewsType, string> = {
[ViewsType.Personal]: 'Personal',
[ViewsType.Shared]: 'Shared'
[ViewsType.All]: 'All views',
[ViewsType.Mine]: 'My views'
}
/**
@@ -45,14 +44,12 @@ export const serializeSavedViewUrlSettings = (
}
export const viewsTypeToFilters = (type: ViewsType) => {
if (type === ViewsType.Personal) {
if (type === ViewsType.Mine) {
return {
onlyAuthored: true
}
} else if (type === ViewsType.Shared) {
return {
onlyVisibility: SavedViewVisibility.Public
}
} else if (type === ViewsType.All) {
return {}
} else {
throwUncoveredError(type)
}
@@ -192,9 +192,11 @@ export const useProcessWorkspaceInvite = () => {
'workspaceInvite',
({ value, variables, helpers: { readField } }) => {
if (value) {
const workspace = readField(value, 'workspace')
if (workspace) {
const inviteWorkspaceId = workspace.id
const workspaceRef = readField(value, 'workspace')
if (workspaceRef) {
const workspaceId = readField(workspaceRef, 'id')
const inviteWorkspaceId = workspaceId
if (inviteWorkspaceId === workspaceId) return null
}
} else {
+1
View File
@@ -84,6 +84,7 @@ export default defineNuxtConfig({
datadogService: '',
datadogEnv: '',
intercomAppId: '',
dashboardsOrigin: '',
parallelMiddlewares: true
}
},
@@ -31,6 +31,9 @@ graphql(`
canReadWebhooks {
...FullPermissionCheckResult
}
canReadEmbedTokens {
...FullPermissionCheckResult
}
}
}
`)
@@ -41,6 +44,7 @@ const attrs = useAttrs() as {
const route = useRoute()
const router = useRouter()
const canReadEmbedTokens = computed(() => attrs.project.permissions.canReadEmbedTokens)
const canReadWebhooks = computed(() => attrs.project.permissions.canReadWebhooks)
const projectName = computed(() =>
attrs.project.name.length ? attrs.project.name : ''
@@ -64,8 +68,8 @@ const settingsTabItems = computed((): LayoutPageTabItem[] => [
{
title: 'Tokens',
id: 'tokens',
disabled: !canReadWebhooks.value.authorized,
disabledMessage: canReadWebhooks.value.message
disabled: !canReadEmbedTokens.value.authorized,
disabledMessage: canReadEmbedTokens.value.message
}
])
@@ -0,0 +1,126 @@
<template>
<div>
<Portal to="navigation">
<div class="flex items-center">
<HeaderNavLink
:to="dashboardsRoute(workspace?.slug)"
name="Dashboard"
:separator="false"
/>
<HeaderNavLink
:to="dashboardRoute(workspace?.slug, id as string)"
:name="dashboard?.name"
/>
<FormButton
v-tippy="'Edit name'"
size="sm"
color="subtle"
class="ml-2"
hide-text
:icon-right="Pencil"
@click="toggleEditDialog"
/>
</div>
</Portal>
<Portal to="primary-actions">
<div class="flex items-center gap-2">
<DashboardsShare :id="dashboard?.id" />
<FormButton
v-tippy="'Toggle fullscreen'"
size="sm"
color="outline"
:icon-right="Fullscreen"
hide-text
@click="toggleFullScreen()"
>
Fullscreen
</FormButton>
</div>
</Portal>
<div class="w-screen h-screen">
<iframe
:src="dashboardUrl"
class="w-full h-full border-0"
frameborder="0"
:title="dashboard?.name"
/>
</div>
<DashboardsEditDialog v-model:open="editDialogOpen" :dashboard="dashboard" />
</div>
</template>
<script setup lang="ts">
import { dashboardsRoute, dashboardRoute } from '~/lib/common/helpers/route'
import { dashboardQuery } from '~/lib/dashboards/graphql/queries'
import { useQuery } from '@vue/apollo-composable'
import { graphql } from '~~/lib/common/generated/gql'
import { useAuthManager } from '~/lib/auth/composables/auth'
import { Fullscreen, Pencil } from 'lucide-vue-next'
import { useTheme } from '~/lib/core/composables/theme'
graphql(`
fragment WorkspaceDashboards_Dashboard on Dashboard {
...DashboardsEditDialog_Dashboard
id
name
createdBy {
id
name
avatar
}
createdAt
updatedAt
workspace {
id
name
slug
logo
}
}
`)
definePageMeta({
layout: 'dashboard'
})
const { id } = useRoute().params
const { token: urlToken } = useRoute().query
const { result } = useQuery(dashboardQuery, () => ({ id: id as string }))
const { effectiveAuthToken } = useAuthManager()
const logger = useLogger()
const { isDarkTheme } = useTheme()
const {
public: { dashboardsOrigin }
} = useRuntimeConfig()
const editDialogOpen = ref(false)
const workspace = computed(() => result.value?.dashboard?.workspace)
const dashboard = computed(() => result.value?.dashboard)
const dashboardUrl = computed(() => {
return urlToken
? `${dashboardsOrigin}/view/${id}?token=${urlToken}&isEmbed=true&theme=${
isDarkTheme.value ? 'dark' : 'light'
}`
: `${dashboardsOrigin}/dashboards/${id}?token=${
effectiveAuthToken.value
}&isEmbed=true&theme=${isDarkTheme.value ? 'dark' : 'light'}`
})
const toggleFullScreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch((err) => {
logger.warn(`Error attempting to enable fullscreen: ${err.message}`)
})
} else {
document.exitFullscreen().catch((err) => {
logger.warn(`Error attempting to exit fullscreen: ${err.message}`)
})
}
}
const toggleEditDialog = () => {
editDialogOpen.value = !editDialogOpen.value
}
</script>
@@ -0,0 +1,29 @@
<template>
<div>
<Portal to="navigation">
<HeaderNavLink
:to="dashboardsRoute(activeWorkspaceSlug)"
name="Intelligence"
:separator="false"
/>
</Portal>
<div>
<DashboardsList />
</div>
<DashboardsCreateDialog
v-model:open="showCreateDashboardDialog"
:workspace-slug="activeWorkspaceSlug"
/>
</div>
</template>
<script setup lang="ts">
import { dashboardsRoute } from '~/lib/common/helpers/route'
import { useActiveWorkspaceSlug } from '~/lib/user/composables/activeWorkspace'
const activeWorkspaceSlug = useActiveWorkspaceSlug()
const showCreateDashboardDialog = ref(false)
</script>
+1
View File
@@ -14,6 +14,7 @@ declare global {
VIEWER?: any
VIEWER_STATE?: any
VIEWER_SERIALIZED_STATE?: any
VIEWER_SERIALIZED_STATE_OBJECT?: any
APPLY_VIEWER_STATE?: any
APPLY_VIEWER_DD_EVENT?: any
}
@@ -16,7 +16,7 @@ from specklepy.core.api.inputs.file_import_inputs import (
from specklepy.core.api.models import Version
from ifc_importer.domain import FileimportPayload, JobStatus
from ifc_importer.repository import get_next_job, set_job_status, setup_connection
from ifc_importer.repository import get_next_job, return_job_to_queued, setup_connection
IDLE_TIMEOUT = 1
@@ -125,7 +125,8 @@ async def job_processor(logger: structlog.stdlib.BoundLogger):
),
)
)
# the server is responsible for moving successful jobs to the succeeded state
# mark it as succeeded so we do not enter any error handling routines on finalisation
job_status = JobStatus.SUCCEEDED
except TimeoutError as te:
@@ -142,7 +143,9 @@ async def job_processor(logger: structlog.stdlib.BoundLogger):
ex = e
job_status = JobStatus.FAILED
finally:
if job_status == JobStatus.FAILED:
if job_status == JobStatus.QUEUED:
await return_job_to_queued(connection, job_id)
elif job_status == JobStatus.FAILED:
# we should be reporting the failure to the server
logger.error("job processing failed", exc_info=ex)
try:
@@ -162,11 +165,13 @@ async def job_processor(logger: structlog.stdlib.BoundLogger):
),
)
)
# if the reporting of the failure does not succeed, we're requeueing
# unless we've reached the max attempts
# the server is responsible for moving failed jobs to the failed state, so the worker does not have to do anything further
except Exception as ex:
if attempt >= job.max_attempt:
job_status = JobStatus.FAILED
else:
job_status = JobStatus.QUEUED
await set_job_status(connection, job_id, job_status)
logger.error("failed to report job failure", exc_info=ex)
await return_job_to_queued(connection, job_id)
# The client will not pick up a queued job if it now has exceeded max attempts.
# The server is responsible for moving queued jobs which have exceeded maximum attempts to failed status.
elif job_status == JobStatus.SUCCEEDED:
# do nothing
# we expect the job to already be marked as succeeded in the database by the server (when the worker reported the results back to the server)
continue
@@ -33,6 +33,7 @@ async def get_next_job(connection: Connection) -> FileimportJob | None:
WHERE ( --queued job
payload ->> 'fileType' = 'ifc'
AND status = $2
AND "attempt" < "maxAttempt"
)
OR ( --timed job left on processing state
payload ->> 'fileType' = 'ifc'
@@ -55,6 +56,11 @@ async def get_next_job(connection: Connection) -> FileimportJob | None:
return FileimportJob.model_validate(dict(job))
async def return_job_to_queued(connection: Connection, job_id: str) -> None:
print(f"returning job: {job_id} to queued")
return await set_job_status(connection, job_id, JobStatus.QUEUED)
async def set_job_status(
connection: Connection, job_id: str, job_status: JobStatus
) -> None:
+5 -2
View File
@@ -1,5 +1,8 @@
{
"extends": "../../jsconfig.base.json",
"compilerOptions": {},
"compilerOptions": {
"target": "es2021",
"module": "commonJS"
},
"exclude": ["node_modules", "dist"],
"include": ["src", "examples"]
}
@@ -1,12 +1,14 @@
type UserMeta {
newWorkspaceExplainerDismissed: Boolean!
speckleConBannerDismissed: Boolean!
intelligenceCommunityStandUpBannerDismissed: Boolean!
legacyProjectsExplainerCollapsed: Boolean!
}
type UserMetaMutations {
setNewWorkspaceExplainerDismissed(value: Boolean!): Boolean!
setSpeckleConBannerDismissed(value: Boolean!): Boolean!
setIntelligenceCommunityStandUpBannerDismissed(value: Boolean!): Boolean!
setLegacyProjectsExplainerCollapsed(value: Boolean!): Boolean!
}
@@ -0,0 +1,47 @@
extend type Query {
dashboard(id: String!): Dashboard!
}
extend type Mutation {
dashboardMutations: DashboardMutations! @hasServerRole(role: SERVER_GUEST)
}
type Dashboard {
id: String!
name: String!
workspace: LimitedWorkspace!
createdBy: LimitedUser
"""
If null, this is a new dashboard and should be initialized by the client
"""
state: String
createdAt: DateTime!
updatedAt: DateTime!
}
type DashboardCollection {
items: [Dashboard!]!
cursor: String
totalCount: Int!
}
extend type Workspace {
dashboards(limit: Int! = 50, cursor: String): DashboardCollection!
}
type DashboardMutations {
create(workspace: WorkspaceIdentifier!, input: DashboardCreateInput!): Dashboard!
delete(id: String!): Boolean!
update(input: DashboardUpdateInput!): Dashboard!
}
input DashboardCreateInput {
name: String!
}
input DashboardUpdateInput {
id: String!
name: String
projectIds: [String!]
state: String
}
@@ -0,0 +1,15 @@
extend type WorkspacePermissionChecks {
canCreateDashboards: PermissionCheckResult!
canListDashboards: PermissionCheckResult!
}
extend type Dashboard {
permissions: DashboardPermissionChecks!
}
type DashboardPermissionChecks {
canCreateToken: PermissionCheckResult!
canDelete: PermissionCheckResult!
canEdit: PermissionCheckResult!
canRead: PermissionCheckResult!
}
@@ -0,0 +1,33 @@
type DashboardToken {
tokenId: String!
dashboard: Dashboard!
projects: [Project!]!
user: LimitedUser
createdAt: DateTime!
lifespan: BigInt!
lastUsed: DateTime!
}
extend type DashboardMutations {
createToken(dashboardId: String!): CreateDashboardTokenReturn!
}
input DashboardTokenCreateInput {
dashboardId: String!
lifespan: BigInt
}
type CreateDashboardTokenReturn {
token: String!
tokenMetadata: DashboardToken!
}
extend type Project {
dashboardTokens(cursor: String, limit: Int): DashboardTokenCollection!
}
type DashboardTokenCollection {
items: [DashboardToken!]!
totalCount: Int!
cursor: String
}
@@ -27,6 +27,11 @@ type SavedView {
Same as resourceIdString, but split into an array of resource IDs.
"""
resourceIds: [String!]!
"""
Truncated resourceIds w/o specific version data that is used to associate the view w/
specific groups
"""
groupResourceIds: [String!]!
isHomeView: Boolean!
visibility: SavedViewVisibility!
"""
@@ -260,6 +260,11 @@ input PendingWorkspaceCollaboratorsFilter {
search: String
}
input WorkspaceIdentifier @oneOf {
id: String
slug: String
}
type Workspace {
id: ID!
name: String!
+8 -1
View File
@@ -201,7 +201,14 @@ const config: CodegenConfig = {
SavedViewGroupPermissionChecks:
'@/modules/viewer/helpers/graphTypes#SavedViewGroupPermissionChecksGraphQLReturn',
ExtendedViewerResources:
'@/modules/viewer/helpers/graphTypes#ExtendedViewerResourcesGraphQLReturn'
'@/modules/viewer/helpers/graphTypes#ExtendedViewerResourcesGraphQLReturn',
Dashboard: '@/modules/dashboards/helpers/graphTypes#DashboardGraphQLReturn',
DashboardMutations:
'@/modules/dashboards/helpers/graphTypes#DashboardMutationsGraphQLReturn',
DashboardPermissionChecks:
'@/modules/dashboards/helpers/graphTypes#DashboardPermissionChecksGraphQLReturn',
DashboardToken:
'@/modules/dashboards/helpers/graphTypes#DashboardTokenGraphQLReturn'
}
}
}
@@ -16,28 +16,7 @@ import {
getStreamRolesFactory,
grantStreamPermissionsFactory
} from '@/modules/core/repositories/streams'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getUserFactory } from '@/modules/core/repositories/users'
import { createPersonalAccessTokenFactory } from '@/modules/core/services/tokens'
import {
storePersonalApiTokenFactory,
@@ -45,7 +24,6 @@ import {
storeTokenScopesFactory,
storeTokenResourceAccessDefinitionsFactory
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { createObjectFactory } from '@/modules/core/services/objects/management'
import { storeSingleObjectIfNotFoundFactory } from '@/modules/core/repositories/objects'
import { getEventBus } from '@/modules/shared/services/eventBus'
@@ -55,6 +33,8 @@ import { createTestStream } from '@/test/speckle-helpers/streamHelper'
import type { BasicTestBranch } from '@/test/speckle-helpers/branchHelper'
import { createTestBranch } from '@/test/speckle-helpers/branchHelper'
import { getActivitiesFactory } from '@/modules/activitystream/repositories/index'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
const getUser = getUserFactory({ db })
const getUserActivity = getUserActivityFactory({ db })
@@ -67,34 +47,6 @@ const addOrUpdateStreamCollaborator = addOrUpdateStreamCollaboratorFactory({
emitEvent: getEventBus().emit
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo: getServerInfoFactory({ db }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo: getServerInfoFactory({ db }),
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -113,29 +65,13 @@ let server: http.Server
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
describe('Activity @activity', () => {
const userIz = {
name: 'Izzy Lyseggen',
email: 'izzybizzi@speckle.systems',
password: 'sp0ckle sucks 9001',
id: '',
token: ''
}
let userIz: BasicTestUser
let userCr: BasicTestUser
let userX: BasicTestUser
const userCr = {
name: 'Cristi Balas',
email: 'cristib@speckle.systems',
password: 'hack3r man 666',
id: '',
token: ''
}
const userX = {
name: 'Mystery User',
email: 'mysteriousDude@speckle.systems',
password: 'super $ecret pw0rd',
id: '',
token: ''
}
let userIzToken: string
let userCrToken: string
let userXToken: string
const streamPublic: BasicTestStream = {
name: 'a fun stream for sharing',
@@ -197,30 +133,42 @@ describe('Activity @activity', () => {
]
// create users
await Promise.all([
createUser(userIz).then((id) => (userIz.id = id)),
createUser(userCr).then((id) => (userCr.id = id)),
createUser(userX).then((id) => (userX.id = id))
])
userIz = await createTestUser({
name: 'Izzy Lyseggen',
email: 'izzybizzi@speckle.systems',
password: 'sp0ckle sucks 9001'
})
userCr = await createTestUser({
name: 'Cristi Balas',
email: 'cristib@speckle.systems',
password: 'hack3r man 666'
})
userX = await createTestUser({
name: 'Mystery User',
email: 'mysteriousDude@speckle.systems',
password: 'super $ecret pw0rd'
})
// create tokens and streams
await Promise.all([
// tokens
createPersonalAccessToken(userIz.id, 'izz test token', normalScopesList).then(
(token) => (userIz.token = `Bearer ${token}`)
),
createPersonalAccessToken(userCr.id, 'cristi test token', normalScopesList).then(
(token) => (userCr.token = `Bearer ${token}`)
),
createPersonalAccessToken(userX.id, 'no users:read test token', [
Scopes.Streams.Read,
Scopes.Streams.Write
]).then((token) => (userX.token = `Bearer ${token}`))
// streams
// createStream({ ...collaboratorTestStream, ownerId: userIz.id }).then(
// (id) => (collaboratorTestStream.id = id)
// )
])
userIzToken = `Bearer ${await createPersonalAccessToken(
userIz.id,
'izz test token',
normalScopesList
)}`
userCrToken = `Bearer ${await createPersonalAccessToken(
userCr.id,
'cristi test token',
normalScopesList
)}`
userXToken = `Bearer ${createPersonalAccessToken(
userX.id,
'no users:read test token',
[Scopes.Streams.Read, Scopes.Streams.Write]
)}`
// streams
// createStream({ ...collaboratorTestStream, ownerId: userIz.id }).then(
// (id) => (collaboratorTestStream.id = id)
// )
// It's definitely not great that there's a full on test case in the before() hook, but that's because
// these tests were originally written incorrectly - they depend on each other. So this is a temporary fix that
@@ -232,7 +180,7 @@ describe('Activity @activity', () => {
// create commit (cr2)
testObj2.id = await createObject({ streamId: streamSecret.id, object: testObj2 })
const resCommit1 = await sendRequest(userCr.token, {
const resCommit1 = await sendRequest(userCrToken, {
query: `mutation { commitCreate(commit: {streamId: "${streamSecret.id}", branchName: "main", objectId: "${testObj2.id}", message: "first commit"})}`
})
expect(noErrors(resCommit1))
@@ -249,7 +197,7 @@ describe('Activity @activity', () => {
// create commit #2 (iz3)
testObj.id = await createObject({ streamId: streamPublic.id, object: testObj })
const resCommit2 = await sendRequest(userIz.token, {
const resCommit2 = await sendRequest(userIzToken, {
query: `mutation { commitCreate(commit: { streamId: "${streamPublic.id}", branchName: "${branchPublic.name}", objectId: "${testObj.id}", message: "first commit" })}`
})
expect(noErrors(resCommit2))
@@ -263,7 +211,7 @@ describe('Activity @activity', () => {
)
// update collaborator (iz4)
const resCollab = await sendRequest(userIz.token, {
const resCollab = await sendRequest(userIzToken, {
query: `mutation { streamUpdatePermission( permissionParams: { streamId: "${streamPublic.id}", userId: "${userCr.id}", role: "stream:contributor" } ) }`
})
expect(noErrors(resCollab))
@@ -312,7 +260,7 @@ describe('Activity @activity', () => {
})
it("Should get a user's own activity", async () => {
const res = await sendRequest(userIz.token, {
const res = await sendRequest(userIzToken, {
query: `query {activeUser { name activity { totalCount items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(noErrors(res))
@@ -325,7 +273,7 @@ describe('Activity @activity', () => {
})
it("Should get another user's activity", async () => {
const res = await sendRequest(userIz.token, {
const res = await sendRequest(userIzToken, {
query: `query {otherUser(id:"${userCr.id}") { name activity { totalCount items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(noErrors(res))
@@ -334,7 +282,7 @@ describe('Activity @activity', () => {
})
it("Should get a user's timeline", async () => {
const res = await sendRequest(userIz.token, {
const res = await sendRequest(userIzToken, {
query: `query {otherUser(id:"${userCr.id}") { name timeline { totalCount items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(noErrors(res))
@@ -343,7 +291,7 @@ describe('Activity @activity', () => {
})
it("Should get a stream's activity", async () => {
const res = await sendRequest(userCr.token, {
const res = await sendRequest(userCrToken, {
query: `query { stream(id: "${streamPublic.id}") { activity { totalCount items {id streamId resourceId actionType message} } } }`
})
expect(noErrors(res))
@@ -354,7 +302,7 @@ describe('Activity @activity', () => {
})
it("Should get a branch's activity", async () => {
const res = await sendRequest(userCr.token, {
const res = await sendRequest(userCrToken, {
query: `query { stream(id: "${streamPublic.id}") { branch(name: "${branchPublic.name}") { activity { totalCount items {id streamId resourceId actionType message} } } } }`
})
expect(noErrors(res))
@@ -365,7 +313,7 @@ describe('Activity @activity', () => {
})
it("Should *not* get a stream's activity if you don't have access to it", async () => {
const res = await sendRequest(userIz.token, {
const res = await sendRequest(userIzToken, {
query: `query {stream(id:"${streamSecret.id}") {name activity {items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(res.body.errors?.length).to.equal(1)
@@ -379,16 +327,18 @@ describe('Activity @activity', () => {
})
it("Should *not* get a user's activity without the `users:read` scope", async () => {
const res = await sendRequest(userX.token, {
const res = await sendRequest(userXToken, {
query: `query {otherUser(id:"${userCr.id}") { name activity {items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(res.body.errors?.length).to.equal(1)
expect(res.body.error).to.exist
})
it("Should *not* get a user's timeline without the `users:read` scope", async () => {
const res = await sendRequest(userX.token, {
const res = await sendRequest(userXToken, {
query: `query {otherUser(id:"${userCr.id}") { name timeline {items {id streamId resourceType resourceId actionType userId message time}}} }`
})
expect(res.body.errors?.length).to.equal(1)
expect(res.body.error).to.exist
})
})
@@ -22,64 +22,17 @@ import {
} from '@/modules/activitystream/repositories'
import { db } from '@/db/knex'
import {
createStreamFactory,
deleteStreamFactory,
getStreamFactory,
getStreamRolesFactory,
grantStreamPermissionsFactory
getStreamFactory
} from '@/modules/core/repositories/streams'
import {
createStreamReturnRecordFactory,
legacyCreateStreamFactory
} from '@/modules/core/services/streams/management'
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
import {
deleteInvitesByTargetFactory,
deleteServerOnlyInvitesFactory,
findInviteFactory,
findUserByTargetFactory,
insertInviteAndDeleteOldFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import { getUserFactory, getUsersFactory } from '@/modules/core/repositories/users'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import {
finalizeInvitedServerRegistrationFactory,
finalizeResourceInviteFactory
} from '@/modules/serverinvites/services/processing'
import {
processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory
} from '@/modules/serverinvites/services/coreFinalization'
import {
addOrUpdateStreamCollaboratorFactory,
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import {
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory,
findEmailFactory
} from '@/modules/core/repositories/userEmails'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
import { getUserFactory } from '@/modules/core/repositories/users'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
const cleanup = async () => {
await truncateTables([StreamActivity.name, Users.name])
}
const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db })
const getUsers = getUsersFactory({ db })
const getStream = getStreamFactory({ db })
const saveActivity = saveStreamActivityFactory({ db })
const createActivitySummary = createActivitySummaryFactory({
@@ -87,82 +40,6 @@ const createActivitySummary = createActivitySummaryFactory({
getActivity: geUserStreamActivityFactory({ db }),
getUser
})
const buildFinalizeProjectInvite = () =>
finalizeResourceInviteFactory({
findInvite: findInviteFactory({ db }),
validateInvite: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
}),
processInvite: processFinalizedProjectInviteFactory({
getProject: getStream,
addProjectRole: addOrUpdateStreamCollaboratorFactory({
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
getUser,
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
getStreamRoles: getStreamRolesFactory({ db }),
emitEvent: getEventBus().emit
})
}),
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
emitEvent: (...args) => getEventBus().emit(...args),
findEmail: findEmailFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
db
}),
renderEmail,
sendEmail
})
}),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
getUser,
getServerInfo
})
const createStream = legacyCreateStreamFactory({
createStreamReturnRecord: createStreamReturnRecordFactory({
inviteUsersToProject: inviteUsersToProjectFactory({
createAndSendInvite: createAndSendInviteFactory({
findUserByTarget: findUserByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
}),
getUser,
getServerInfo,
finalizeInvite: buildFinalizeProjectInvite()
}),
getUsers
}),
createStream: createStreamFactory({ db }),
createBranch: createBranchFactory({ db }),
storeProjectRole: storeProjectRoleFactory({ db }),
emitEvent: getEventBus().emit
})
})
const deleteStream = deleteStreamFactory({ db })
describe('Activity summary @activity', () => {
@@ -188,8 +65,8 @@ describe('Activity summary @activity', () => {
it('no activity returns empty summary', async () => {
const start = new Date()
const streamIds = await Promise.all(
[{ name: 'foo' }, { name: 'bar' }].map(async (stream) =>
createStream({ ...stream, ownerId: userA.id })
[{ name: 'foo' }, { name: 'bar' }].map(
async (stream) => (await createTestStream(stream, userA)).id
)
)
@@ -206,8 +83,8 @@ describe('Activity summary @activity', () => {
it('gets activities for the user', async () => {
const start = new Date()
const streamIds = await Promise.all(
[{ name: 'foo' }, { name: 'bar' }].map(async (stream) =>
createStream({ ...stream, ownerId: userA.id })
[{ name: 'foo' }, { name: 'bar' }].map(
async (stream) => (await createTestStream(stream, userA)).id
)
)
const summary = await createActivitySummary({
@@ -223,8 +100,8 @@ describe('Activity summary @activity', () => {
it('if stream is deleted, activity summary returns with null as stream value', async () => {
const start = new Date()
const [streamId] = await Promise.all(
[{ name: 'foo' }].map(async (stream) =>
createStream({ ...stream, ownerId: userA.id })
[{ name: 'foo' }].map(
async (stream) => (await createTestStream(stream, userA)).id
)
)
await saveActivity({
+60 -38
View File
@@ -1,7 +1,7 @@
import type { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { registerOrUpdateScopeFactory } from '@/modules/shared/repositories/scopes'
import { moduleLogger } from '@/observability/logging'
import { logger, moduleLogger } from '@/observability/logging'
import db from '@/db/knex'
import { initializeDefaultAppsFactory } from '@/modules/auth/services/serverApps'
import {
@@ -60,42 +60,9 @@ import { sendEmail } from '@/modules/emails/services/sending'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { isRateLimiterEnabled } from '@/modules/shared/helpers/envHelper'
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo: getServerInfoFactory({ db }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
db
}),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo: getServerInfoFactory({ db }),
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const findOrCreateUser = findOrCreateUserFactory({
createUser,
findPrimaryEmailForUser: findPrimaryEmailForUserFactory({ db })
})
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
import type { CreateValidatedUser } from '@/modules/core/domain/users/operations'
import { asMultiregionalOperation } from '@/modules/shared/command'
const initializeDefaultApps = initializeDefaultAppsFactory({
getAllScopes: getAllScopesFactory({ db }),
@@ -113,10 +80,65 @@ const finalizeInvitedServerRegistration = finalizeInvitedServerRegistrationFacto
})
const resolveAuthRedirectPath = resolveAuthRedirectPathFactory()
const createUser: CreateValidatedUser = async (...input) =>
asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const createUser = createUserFactory({
getServerInfo: getServerInfoFactory({ db: mainDb }),
findEmail: findEmailFactory({ db: mainDb }),
storeUser: async (...params) => {
const [user] = await Promise.all(
allDbs.map((db) => storeUserFactory({ db })(...params))
)
return user
},
countAdminUsers: countAdminUsersFactory({ db: mainDb }),
storeUserAcl: storeUserAclFactory({ db: mainDb }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db: mainDb }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({
db: mainDb
}),
findEmail: findEmailFactory({ db: mainDb }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db: mainDb }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db: mainDb })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
getServerInfo: getServerInfoFactory({ db }),
findEmail: findEmailFactory({ db: mainDb }),
getUser: getUserFactory({ db: mainDb }),
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory(
{
db: mainDb
}
),
renderEmail,
sendEmail
})
}),
emitEvent: emit
})
return createUser(...input)
},
{
dbs: await getAllRegisteredDbs(),
name: 'create user',
logger
}
)
const commonBuilderDeps = {
getServerInfo: getServerInfoFactory({ db }),
getUserByEmail: legacyGetUserByEmailFactory({ db }),
findOrCreateUser,
buildFindOrCreateUser: async () => {
return findOrCreateUserFactory({
createUser,
findPrimaryEmailForUser: findPrimaryEmailForUserFactory({ db })
})
},
validateServerInvite,
finalizeInvitedServerRegistration,
resolveAuthRedirectPath,
@@ -40,7 +40,7 @@ const azureAdStrategyBuilderFactory =
(deps: {
getServerInfo: GetServerInfo
getUserByEmail: LegacyGetUserByEmail
findOrCreateUser: FindOrCreateValidatedUser
buildFindOrCreateUser: () => Promise<FindOrCreateValidatedUser>
validateServerInvite: ValidateServerInvite
finalizeInvitedServerRegistration: FinalizeInvitedServerRegistration
resolveAuthRedirectPath: ResolveAuthRedirectPath
@@ -102,6 +102,8 @@ const azureAdStrategyBuilderFactory =
serverVersion: serverInfo.version
})
const findOrCreateUser = await deps.buildFindOrCreateUser()
try {
// This is the only strategy that does its own type for req.user - easier to force type cast for now
// than to refactor everything
@@ -130,7 +132,7 @@ const azureAdStrategyBuilderFactory =
// if there is an existing user, go ahead and log them in (regardless of
// whether the server is invite only or not).
if (existingUser) {
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user
})
// ID is used later for verifying access token
@@ -156,7 +158,7 @@ const azureAdStrategyBuilderFactory =
}
// create the user
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user: {
...user,
role: invite
@@ -44,7 +44,7 @@ const githubStrategyBuilderFactory =
(deps: {
getServerInfo: GetServerInfo
getUserByEmail: LegacyGetUserByEmail
findOrCreateUser: FindOrCreateValidatedUser
buildFindOrCreateUser: () => Promise<FindOrCreateValidatedUser>
validateServerInvite: ValidateServerInvite
finalizeInvitedServerRegistration: FinalizeInvitedServerRegistration
resolveAuthRedirectPath: ResolveAuthRedirectPath
@@ -91,6 +91,8 @@ const githubStrategyBuilderFactory =
serverVersion: serverInfo.version
})
const findOrCreateUser = await deps.buildFindOrCreateUser()
try {
const email = profile.emails?.[0].value
if (!email) {
@@ -115,7 +117,7 @@ const githubStrategyBuilderFactory =
// if there is an existing user, go ahead and log them in (regardless of
// whether the server is invite only or not).
if (existingUser) {
const myUser = await deps.findOrCreateUser({ user })
const myUser = await findOrCreateUser({ user })
return done(null, myUser)
}
@@ -133,7 +135,7 @@ const githubStrategyBuilderFactory =
}
// create the user
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user: {
...user,
role: invite
@@ -39,7 +39,7 @@ const googleStrategyBuilderFactory =
(deps: {
getServerInfo: GetServerInfo
getUserByEmail: LegacyGetUserByEmail
findOrCreateUser: FindOrCreateValidatedUser
buildFindOrCreateUser: () => Promise<FindOrCreateValidatedUser>
validateServerInvite: ValidateServerInvite
finalizeInvitedServerRegistration: FinalizeInvitedServerRegistration
resolveAuthRedirectPath: ResolveAuthRedirectPath
@@ -75,6 +75,7 @@ const googleStrategyBuilderFactory =
profileId: profile.id,
serverVersion: serverInfo.version
})
const findOrCreateUser = await deps.buildFindOrCreateUser()
try {
// seems very weird that the Google strategy is not parsing 'error' query params
@@ -117,7 +118,7 @@ const googleStrategyBuilderFactory =
// if there is an existing user, go ahead and log them in (regardless of
// whether the server is invite only or not).
if (existingUser) {
const myUser = await deps.findOrCreateUser({ user })
const myUser = await findOrCreateUser({ user })
return done(null, myUser)
}
@@ -135,7 +136,7 @@ const googleStrategyBuilderFactory =
}
// create the user
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user: {
...user,
role: invite
@@ -38,7 +38,7 @@ const oidcStrategyBuilderFactory =
(deps: {
getServerInfo: GetServerInfo
getUserByEmail: LegacyGetUserByEmail
findOrCreateUser: FindOrCreateValidatedUser
buildFindOrCreateUser: () => Promise<FindOrCreateValidatedUser>
validateServerInvite: ValidateServerInvite
finalizeInvitedServerRegistration: FinalizeInvitedServerRegistration
resolveAuthRedirectPath: ResolveAuthRedirectPath
@@ -78,6 +78,8 @@ const oidcStrategyBuilderFactory =
serverVersion: serverInfo.version
})
const findOrCreateUser = await deps.buildFindOrCreateUser()
// TODO: req.session.inviteId doesn't appear to exist, but i'm not removing it to not break things
const token: Optional<string> =
get(req.session, 'inviteId') || req.session.token
@@ -107,7 +109,7 @@ const oidcStrategyBuilderFactory =
// if there is an existing user, go ahead and log them in (regardless of
// whether the server is invite only or not).
if (existingUser) {
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user
})
@@ -128,7 +130,7 @@ const oidcStrategyBuilderFactory =
}
// create the user
const myUser = await deps.findOrCreateUser({
const myUser = await findOrCreateUser({
user: {
...user,
role: invite
@@ -20,28 +20,6 @@ import {
} from '@/modules/auth/repositories/apps'
import { db } from '@/db/knex'
import { createAppTokenFromAccessCodeFactory } from '@/modules/auth/services/serverApps'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import {
storeApiTokenFactory,
storeTokenScopesFactory,
@@ -49,9 +27,7 @@ import {
storeUserServerAppTokenFactory,
storePersonalApiTokenFactory
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
@@ -73,34 +49,6 @@ const createAppTokenFromAccessCode = createAppTokenFromAccessCodeFactory({
createBareToken
})
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createPersonalAccessToken = createPersonalAccessTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
storeTokenScopes: storeTokenScopesFactory({ db }),
@@ -119,28 +67,25 @@ describe('GraphQL @apps-api', () => {
before(async () => {
const ctx = await beforeEachContext()
;({ sendRequest } = await initializeTestServer(ctx))
testUser = {
testUser = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf',
id: ''
}
testUser.id = await createUser(testUser)
})
testToken = `Bearer ${await createPersonalAccessToken(testUser.id, 'test token', [
Scopes.Profile.Read,
Scopes.Apps.Read,
Scopes.Apps.Write
])}`
testUser2 = {
testUser2 = await createTestUser({
name: 'Mr. Mac',
email: 'steve@jobs.com',
password: 'wtfwtfwtf',
id: ''
}
testUser2.id = await createUser(testUser2)
})
testToken2 = `Bearer ${await createPersonalAccessToken(testUser2.id, 'test token', [
Scopes.Profile.Read,
Scopes.Apps.Read,
+11 -65
View File
@@ -31,29 +31,7 @@ import {
createAppTokenFromAccessCodeFactory,
refreshAppTokenFactory
} from '@/modules/auth/services/serverApps'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory,
getUserRoleFactory
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getUserRoleFactory } from '@/modules/core/repositories/users'
import {
storeApiTokenFactory,
storeTokenScopesFactory,
@@ -65,9 +43,7 @@ import {
getTokenResourceAccessDefinitionsByIdFactory,
updateApiTokenFactory
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
import type { AppScopes } from '@speckle/shared'
import { ensureError } from '@speckle/shared'
import type { ValidTokenResult } from '@/modules/core/helpers/types'
@@ -115,34 +91,6 @@ const refreshAppToken = refreshAppTokenFactory({
createBareToken
})
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const validateToken = validateTokenFactory({
revokeUserTokenById: revokeUserTokenByIdFactory({ db }),
getApiTokenById: getApiTokenByIdFactory({ db }),
@@ -156,16 +104,16 @@ const validateToken = validateTokenFactory({
})
describe('Services @apps-services', () => {
const actor: BasicTestUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf',
id: ''
}
let actor: BasicTestUser
before(async () => {
await beforeEachContext()
actor.id = await createUser(actor)
actor = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf',
id: ''
})
})
it('Should register an app', async () => {
@@ -507,14 +455,12 @@ describe('Services @apps-services', () => {
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const secondUser: BasicTestUser = {
const secondUser = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie.wow@example.org',
password: 'wtfwtfwtf',
id: ''
}
secondUser.id = await createUser(secondUser)
})
const accessCode = await createAuthorizationCode({
appId: myTestApp.id,
userId: secondUser.id,
+27 -198
View File
@@ -3,58 +3,9 @@ import chai from 'chai'
import request from 'supertest'
import { beforeEachContext, initializeTestServer } from '@/test/hooks'
import { createStreamInviteDirectly } from '@/test/speckle-helpers/inviteHelper'
import {
findInviteFactory,
findUserByTargetFactory,
insertInviteAndDeleteOldFactory,
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory,
deleteInvitesByTargetFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { db } from '@/db/knex'
import {
legacyCreateStreamFactory,
createStreamReturnRecordFactory
} from '@/modules/core/services/streams/management'
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import {
getStreamFactory,
createStreamFactory,
grantStreamPermissionsFactory,
getStreamRolesFactory
} from '@/modules/core/repositories/streams'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory,
legacyGetUserByEmailFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
finalizeResourceInviteFactory
} from '@/modules/serverinvites/services/processing'
import {
getServerInfoFactory,
updateServerInfoFactory
} from '@/modules/core/repositories/server'
import { legacyGetUserByEmailFactory } from '@/modules/core/repositories/users'
import { updateServerInfoFactory } from '@/modules/core/repositories/server'
import { isRateLimiterEnabled } from '@/modules/shared/helpers/envHelper'
import { RATE_LIMITERS, createConsumer } from '@/modules/core/utils/ratelimiter'
import httpMocks from 'node-mocks-http'
@@ -63,131 +14,22 @@ import { TIME } from '@speckle/shared'
import type { Application } from 'express'
import { passportAuthenticationCallbackFactory } from '@/modules/auth/services/passportService'
import { extendLoggerComponent, logger as baseLogger } from '@/observability/logging'
import {
processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory
} from '@/modules/serverinvites/services/coreFinalization'
import {
addOrUpdateStreamCollaboratorFactory,
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { UserInputError } from '@/modules/core/errors/userinput'
import { createRandomEmail } from '@/modules/core/helpers/testHelpers'
import cryptoRandomString from 'crypto-random-string'
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import {
type BasicTestStream,
createTestStream
} from '@/test/speckle-helpers/streamHelper'
import { findInviteFactory } from '@/modules/serverinvites/repositories/serverInvites'
const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db })
const getUsers = getUsersFactory({ db })
const createInviteDirectly = createStreamInviteDirectly
const findInvite = findInviteFactory({ db })
const getStream = getStreamFactory({ db })
const buildFinalizeProjectInvite = () =>
finalizeResourceInviteFactory({
findInvite: findInviteFactory({ db }),
validateInvite: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
}),
processInvite: processFinalizedProjectInviteFactory({
getProject: getStream,
addProjectRole: addOrUpdateStreamCollaboratorFactory({
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
getUser,
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
getStreamRoles: getStreamRolesFactory({ db }),
emitEvent: getEventBus().emit
})
}),
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
emitEvent: (...args) => getEventBus().emit(...args),
findEmail: findEmailFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
db
}),
renderEmail,
sendEmail
})
}),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
getUser,
getServerInfo
})
const createStream = legacyCreateStreamFactory({
createStreamReturnRecord: createStreamReturnRecordFactory({
inviteUsersToProject: inviteUsersToProjectFactory({
createAndSendInvite: createAndSendInviteFactory({
findUserByTarget: findUserByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
}),
getUser,
getServerInfo,
finalizeInvite: buildFinalizeProjectInvite()
}),
getUsers
}),
createStream: createStreamFactory({ db }),
createBranch: createBranchFactory({ db }),
storeProjectRole: storeProjectRoleFactory({ db }),
emitEvent: getEventBus().emit
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const getUserByEmail = legacyGetUserByEmailFactory({ db })
const updateServerInfo = updateServerInfoFactory({ db })
const logger = extendLoggerComponent(baseLogger, 'auth-tests')
@@ -201,29 +43,8 @@ describe('Auth @auth', () => {
describe('Local authN & authZ (token endpoints)', () => {
const registeredUserEmail = 'registered@speckle.systems'
const me: {
name: string
company: string
email: string
password: string
id?: string
} = {
name: 'dimitrie stefanescu',
company: 'speckle',
email: registeredUserEmail,
password: 'roll saving throws',
id: undefined
}
const myPrivateStream: {
name: string
isPublic: boolean
id?: string
} = {
name: 'My Private Stream 1',
isPublic: false,
id: undefined
}
let me: BasicTestUser
let myPrivateStream: BasicTestStream
before(async () => {
const ctx = await beforeEachContext()
@@ -231,15 +52,23 @@ describe('Auth @auth', () => {
;({ sendRequest } = await initializeTestServer(ctx))
// Register a user for testing login flows
const meId = await createUser(me)
me.id = meId
me = await createTestUser({
name: 'dimitrie stefanescu',
company: 'speckle',
email: registeredUserEmail,
password: 'roll saving throws',
id: undefined
})
// Create a test stream for testing stream invites
const myPrivateStreamId = await createStream({
...myPrivateStream,
ownerId: me.id
})
myPrivateStream.id = myPrivateStreamId
myPrivateStream = await createTestStream(
{
name: 'My Private Stream 1',
isPublic: false,
ownerId: me.id
},
me
)
})
it('Should register a new user (speckle frontend)', async () => {
@@ -1,9 +1,5 @@
import { z } from 'zod'
export const BackgroundJobType = {
FileImport: 'fileImport'
} as const
export const BackgroundJobStatus = {
Queued: 'queued',
Processing: 'processing', // this status does not exist in db
@@ -14,11 +10,8 @@ export const BackgroundJobStatus = {
export type BackgroundJobStatus =
(typeof BackgroundJobStatus)[keyof typeof BackgroundJobStatus]
export type BackgroundJobType =
(typeof BackgroundJobType)[keyof typeof BackgroundJobType]
export const BackgroundJobPayload = z.object({
jobType: z.nativeEnum(BackgroundJobType),
jobType: z.string(),
payloadVersion: z.number()
})
@@ -46,6 +39,16 @@ export type StoreBackgroundJob = (args: {
export type GetBackgroundJob<T extends BackgroundJobPayload = BackgroundJobPayload> =
(args: { jobId: string }) => Promise<BackgroundJob<T> | null>
export type FailQueuedBackgroundJobsWhichExceedMaximumAttempts<
T extends BackgroundJobPayload = BackgroundJobPayload
> = (args: { originServerUrl: string; jobType: string }) => Promise<BackgroundJob<T>[]>
export type UpdateBackgroundJob<T extends BackgroundJobPayload = BackgroundJobPayload> =
(args: {
jobId: string
status: BackgroundJobStatus
}) => Promise<BackgroundJob<T> | null>
export type GetBackgroundJobCount<
T extends BackgroundJobPayload = BackgroundJobPayload
> = (args: {
@@ -1,10 +1,15 @@
import type { Knex } from 'knex'
import type {
FailQueuedBackgroundJobsWhichExceedMaximumAttempts,
UpdateBackgroundJob
} from '@/modules/backgroundjobs/domain'
import {
type BackgroundJob,
type BackgroundJobPayload,
type GetBackgroundJob,
type GetBackgroundJobCount,
type StoreBackgroundJob
type StoreBackgroundJob,
BackgroundJobStatus
} from '@/modules/backgroundjobs/domain'
import { buildTableHelper } from '@/modules/core/dbSchema'
@@ -48,6 +53,48 @@ export const getBackgroundJobFactory =
return job ?? null
}
export const failQueuedBackgroundJobsWhichExceedMaximumAttemptsFactory =
<T extends BackgroundJobPayload = BackgroundJobPayload>({
db
}: {
db: Knex
}): FailQueuedBackgroundJobsWhichExceedMaximumAttempts<T> =>
async ({ jobType, originServerUrl }) => {
const query = tables
.backgroundJobs(db)
.where(BackgroundJobs.withoutTablePrefix.col.originServerUrl, originServerUrl)
.andWhere(
BackgroundJobs.withoutTablePrefix.col.status,
BackgroundJobStatus.Queued
)
.andWhere(BackgroundJobs.withoutTablePrefix.col.jobType, jobType)
.andWhere(
BackgroundJobs.withoutTablePrefix.col.attempt,
'>=',
db.raw('"maxAttempt"') // camel-case requires the column name to be wrapped in double quotes
)
.orderBy(BackgroundJobs.withoutTablePrefix.col.createdAt, 'desc')
.update({
[BackgroundJobs.withoutTablePrefix.col.status]: BackgroundJobStatus.Failed
})
.returning<BackgroundJob<T>[]>('*')
return await query
}
export const updateBackgroundJobFactory =
({ db }: { db: Knex }): UpdateBackgroundJob =>
async ({ jobId, status }) => {
const query = tables
.backgroundJobs(db)
.update({ status })
.where({ id: jobId })
.returning('*')
const rows = await query
if (rows.length === 0) return null
return rows[0]
}
export const getBackgroundJobCountFactory =
({ db }: { db: Knex }): GetBackgroundJobCount =>
async ({ status, jobType, minAttempts }) => {
@@ -3,7 +3,8 @@ import {
storeBackgroundJobFactory,
getBackgroundJobFactory,
BackgroundJobs,
getBackgroundJobCountFactory
getBackgroundJobCountFactory,
failQueuedBackgroundJobsWhichExceedMaximumAttemptsFactory
} from '@/modules/backgroundjobs/repositories'
import type {
BackgroundJob,
@@ -15,6 +16,31 @@ import { createRandomString } from '@/modules/core/helpers/testHelpers'
const originServerUrl = 'http://example.org'
type TestJobPayload = BackgroundJobPayload & {
jobType: 'fileImport'
payloadVersion: 1
testData: string
}
const createTestJob = (
overrides: Partial<BackgroundJob<TestJobPayload>> = {}
): BackgroundJob<TestJobPayload> => ({
id: createRandomString(10),
jobType: 'fileImport',
payload: {
jobType: 'fileImport',
payloadVersion: 1,
testData: 'test-data-value'
},
status: BackgroundJobStatus.Queued,
attempt: 0,
maxAttempt: 3,
timeoutMs: 30000,
createdAt: new Date(),
updatedAt: new Date(),
...overrides
})
describe('Background Jobs repositories @backgroundjobs', () => {
const storeBackgroundJob = storeBackgroundJobFactory({
db,
@@ -23,31 +49,6 @@ describe('Background Jobs repositories @backgroundjobs', () => {
const getBackgroundJob = getBackgroundJobFactory({ db })
const getBackgroundJobCount = getBackgroundJobCountFactory({ db })
type TestJobPayload = BackgroundJobPayload & {
jobType: 'fileImport'
payloadVersion: 1
testData: string
}
const createTestJob = (
overrides: Partial<BackgroundJob<TestJobPayload>> = {}
): BackgroundJob<TestJobPayload> => ({
id: createRandomString(10),
jobType: 'fileImport',
payload: {
jobType: 'fileImport',
payloadVersion: 1,
testData: 'test-data-value'
},
status: BackgroundJobStatus.Queued,
attempt: 0,
maxAttempt: 3,
timeoutMs: 30000,
createdAt: new Date(),
updatedAt: new Date(),
...overrides
})
beforeEach(async () => {
// Clean up background jobs table
await db(BackgroundJobs.name).del()
@@ -171,4 +172,24 @@ describe('Background Jobs repositories @backgroundjobs', () => {
expect(count).to.equal(1)
})
})
describe('failQueuedBackgroundJobsWhichExceedMaximumAttempts', () => {
it('should fail queued background jobs that exceed maximum attempts', async () => {
const job = createTestJob({
status: BackgroundJobStatus.Queued,
attempt: 2,
maxAttempt: 2
})
await storeBackgroundJob({ job })
const SUT = failQueuedBackgroundJobsWhichExceedMaximumAttemptsFactory({
db
})
await SUT({ originServerUrl, jobType: 'fileImport' })
const updatedJob = await db(BackgroundJobs.name).where({ id: job.id }).first()
expect(updatedJob.status).to.equal(BackgroundJobStatus.Failed)
})
})
})
@@ -6,187 +6,23 @@ import { expect } from 'chai'
import { Users, Streams } from '@/modules/core/dbSchema'
import type { ServerAndContext } from '@/test/graphqlHelper'
import { createAuthedTestContext, executeOperation } from '@/test/graphqlHelper'
import {
getStreamFactory,
createStreamFactory,
grantStreamPermissionsFactory,
getStreamRolesFactory
} from '@/modules/core/repositories/streams'
import { db } from '@/db/knex'
import {
legacyCreateStreamFactory,
createStreamReturnRecordFactory
} from '@/modules/core/services/streams/management'
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
import {
findUserByTargetFactory,
insertInviteAndDeleteOldFactory,
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory,
findInviteFactory,
deleteInvitesByTargetFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { createBranchFactory } from '@/modules/core/repositories/branches'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
finalizeResourceInviteFactory
} from '@/modules/serverinvites/services/processing'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import {
processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory
} from '@/modules/serverinvites/services/coreFinalization'
import {
addOrUpdateStreamCollaboratorFactory,
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db })
const getUsers = getUsersFactory({ db })
const getStream = getStreamFactory({ db })
const buildFinalizeProjectInvite = () =>
finalizeResourceInviteFactory({
findInvite: findInviteFactory({ db }),
validateInvite: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
}),
processInvite: processFinalizedProjectInviteFactory({
getProject: getStream,
addProjectRole: addOrUpdateStreamCollaboratorFactory({
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
getUser,
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
getStreamRoles: getStreamRolesFactory({ db }),
emitEvent: getEventBus().emit
})
}),
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
emitEvent: (...args) => getEventBus().emit(...args),
findEmail: findEmailFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
db
}),
renderEmail,
sendEmail
})
}),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
getUser,
getServerInfo
})
const createStream = legacyCreateStreamFactory({
createStreamReturnRecord: createStreamReturnRecordFactory({
inviteUsersToProject: inviteUsersToProjectFactory({
createAndSendInvite: createAndSendInviteFactory({
findUserByTarget: findUserByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
}),
getUser,
getServerInfo,
finalizeInvite: buildFinalizeProjectInvite()
}),
getUsers
}),
createStream: createStreamFactory({ db }),
createBranch: createBranchFactory({ db }),
storeProjectRole: storeProjectRoleFactory({ db }),
emitEvent: getEventBus().emit
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
import { buildBasicTestProject } from '@/modules/core/tests/helpers/creation'
describe('Blobs graphql @blobstorage', () => {
let graphqlServer: ServerAndContext
const user = {
name: 'Baron Von Blubba',
email: 'zebarron@bubble.bobble',
password: 'bubblesAreMyBlobs',
id: ''
}
let user: BasicTestUser
before(async () => {
await truncateTables(['blob_storage', Users.name, Streams.name])
user.id = await createUser(user)
user = await createTestUser({
name: 'Baron Von Blubba',
email: 'zebarron@bubble.bobble',
password: 'bubblesAreMyBlobs',
id: ''
})
graphqlServer = {
apollo: await buildApolloServer(),
context: await createAuthedTestContext(user.id)
@@ -208,7 +44,7 @@ describe('Blobs graphql @blobstorage', () => {
}
}
`
const streamId = await createStream({ ownerId: user.id })
const { id: streamId } = await createTestStream(buildBasicTestProject(), user)
const [blob] = await createBlobs({ streamId, number: 1 })
const result = await executeOperation(graphqlServer, query, {
@@ -234,7 +70,7 @@ describe('Blobs graphql @blobstorage', () => {
}
}
`
const streamId = await createStream({ ownerId: user.id })
const { id: streamId } = await createTestStream(buildBasicTestProject(), user)
const number = 10
const fileSize = 123
await createBlobs({ streamId, number, fileSize })
@@ -4,87 +4,29 @@ import { expect } from 'chai'
import { beforeEachContext, getMainTestRegionKeyIfMultiRegion } from '@/test/hooks'
import { Scopes } from '@/modules/core/helpers/mainConstants'
import { db } from '@/db/knex'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import { createTokenFactory } from '@/modules/core/services/tokens'
import {
storeApiTokenFactory,
storeTokenScopesFactory,
storeTokenResourceAccessDefinitionsFactory
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
import { waitForRegionUser } from '@/test/speckle-helpers/regions'
import { createTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import { faker } from '@faker-js/faker'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser, type BasicTestUser } from '@/test/authHelper'
import cryptoRandomString from 'crypto-random-string'
import type { BlobStorageItem } from '@/modules/blobstorage/domain/types'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { fileURLToPath } from 'url'
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createRandomUser = async (): Promise<BasicTestUser> => {
const userDetails = {
name: cryptoRandomString({ length: 10 }),
email: `${cryptoRandomString({ length: 10, type: 'url-safe' })}@example.org`,
password: cryptoRandomString({ length: 12 })
}
return {
...userDetails,
id: await createUser(userDetails)
}
return createTestUser(userDetails)
}
const createToken = createTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
@@ -148,7 +148,13 @@ export const convertLegacyDataToStateFactory =
isOrthoProjection: !!data.camPos?.[6],
zoom: data.camPos?.[7] || 1
},
viewMode: 0,
viewMode: {
mode: 0,
edgesColor: 0,
edgesEnabled: true,
outlineOpacity: 0.75,
edgesWeight: 1
},
sectionBox: sectionBox
? {
min: (sectionBox.min as number[]) || [0, 0, 0],
@@ -53,30 +53,6 @@ import {
getStreamObjectsFactory
} from '@/modules/core/repositories/objects'
import { legacyUpdateStreamFactory } from '@/modules/core/services/streams/management'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
ensureNoPrimaryEmailForUserFactory,
createUserEmailFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { createObjectFactory } from '@/modules/core/services/objects/management'
import {
getViewerResourcesFromLegacyIdentifiersFactory,
@@ -84,8 +60,10 @@ import {
} from '@/modules/core/services/commit/viewerResources'
import type { SetNonNullable } from 'type-fest'
import { createProject } from '@/test/projectHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
const getServerInfo = getServerInfoFactory({ db })
const markCommitStreamUpdated = markCommitStreamUpdatedFactory({ db })
const streamResourceCheck = streamResourceCheckFactory({
checkStreamResourceAccess: checkStreamResourceAccessFactory({ db })
@@ -139,33 +117,6 @@ const updateStream = legacyUpdateStreamFactory({
})
const grantPermissionsStream = grantStreamPermissionsFactory({ db })
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createObject = createObjectFactory({
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
})
@@ -718,7 +669,7 @@ describe('Graphql @comments', () => {
// this user will be admin by default
// it will be used to create all resources, that the other actors can
// be tested against
const myTestActor = {
let myTestActor: BasicTestUser = {
name: 'Gergo Jedlicska',
email: 'gergo@jedlicska.com',
password: 'sn3aky-1337-b1m',
@@ -1011,11 +962,11 @@ describe('Graphql @comments', () => {
before(async () => {
await beforeEachContext()
myTestActor.id = await createUser(myTestActor)
myTestActor = await createTestUser(myTestActor)
await Promise.all(
[chadTheEngineer, archived].map((user) =>
createUser({ name: user.name, email: user.email, password: user.password })
.then((id) => (user.id = id))
createTestUser({ name: user.name, email: user.email, password: user.password })
.then(({ id }) => (user.id = id))
.catch((err) => {
throw err
})
@@ -21,7 +21,8 @@ import {
import { get, range } from 'lodash-es'
import { buildApolloServer } from '@/app'
import { AllScopes } from '@/modules/core/helpers/mainConstants'
import { createAuthTokenForUser } from '@/test/authHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { createAuthTokenForUser, createTestUser } from '@/test/authHelper'
import type { UploadedBlob } from '@/test/blobHelper'
import { uploadBlob } from '@/test/blobHelper'
import { Comments } from '@/modules/core/dbSchema'
@@ -52,10 +53,7 @@ import { db } from '@/db/knex'
import { getBlobsFactory } from '@/modules/blobstorage/repositories'
import {
getStreamFactory,
createStreamFactory,
markCommitStreamUpdatedFactory,
grantStreamPermissionsFactory,
getStreamRolesFactory
markCommitStreamUpdatedFactory
} from '@/modules/core/repositories/streams'
import {
createCommitByBranchIdFactory,
@@ -70,54 +68,14 @@ import {
import {
getBranchByIdFactory,
markCommitBranchUpdatedFactory,
getStreamBranchByNameFactory,
createBranchFactory
getStreamBranchByNameFactory
} from '@/modules/core/repositories/branches'
import {
getObjectFactory,
storeSingleObjectIfNotFoundFactory,
getStreamObjectsFactory
} from '@/modules/core/repositories/objects'
import {
legacyCreateStreamFactory,
createStreamReturnRecordFactory
} from '@/modules/core/services/streams/management'
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
import {
findUserByTargetFactory,
insertInviteAndDeleteOldFactory,
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory,
findInviteFactory,
deleteInvitesByTargetFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
finalizeResourceInviteFactory
} from '@/modules/serverinvites/services/processing'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { createObjectFactory } from '@/modules/core/services/objects/management'
import type express from 'express'
import { ResourceType } from '@/modules/comments/domain/types'
@@ -134,24 +92,16 @@ import {
getViewerResourcesForCommentsFactory,
getViewerResourcesFromLegacyIdentifiersFactory
} from '@/modules/core/services/commit/viewerResources'
import {
processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory
} from '@/modules/serverinvites/services/coreFinalization'
import {
addOrUpdateStreamCollaboratorFactory,
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import type { TestEmailListener } from '@/test/speckle-helpers/email'
import { createEmailListener } from '@/test/speckle-helpers/email'
import { buildTestProject } from '@/modules/core/tests/helpers/creation'
import {
buildBasicTestProject,
buildTestProject
} from '@/modules/core/tests/helpers/creation'
import type { GetCommentsQueryVariables } from '@/modules/core/graph/generated/graphql'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db })
const getUsers = getUsersFactory({ db })
const getStream = getStreamFactory({ db })
const streamResourceCheck = streamResourceCheckFactory({
checkStreamResourceAccess: checkStreamResourceAccessFactory({ db })
@@ -236,110 +186,6 @@ const createCommitByBranchName = createCommitByBranchNameFactory({
getBranchById: getBranchByIdFactory({ db })
})
const buildFinalizeProjectInvite = () =>
finalizeResourceInviteFactory({
findInvite: findInviteFactory({ db }),
validateInvite: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
}),
processInvite: processFinalizedProjectInviteFactory({
getProject: getStream,
addProjectRole: addOrUpdateStreamCollaboratorFactory({
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
getUser,
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
getStreamRoles: getStreamRolesFactory({ db }),
emitEvent: getEventBus().emit
})
}),
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
emitEvent: (...args) => getEventBus().emit(...args),
findEmail: findEmailFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
db
}),
renderEmail,
sendEmail
})
}),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
getUser,
getServerInfo
})
const createStreamReturnRecord = createStreamReturnRecordFactory({
inviteUsersToProject: inviteUsersToProjectFactory({
createAndSendInvite: createAndSendInviteFactory({
findUserByTarget: findUserByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
}),
getUser,
getServerInfo,
finalizeInvite: buildFinalizeProjectInvite()
}),
getUsers
}),
createStream: createStreamFactory({ db }),
createBranch: createBranchFactory({ db }),
storeProjectRole: storeProjectRoleFactory({ db }),
emitEvent: getEventBus().emit
})
const createStream = legacyCreateStreamFactory({
createStreamReturnRecord
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const createObject = createObjectFactory({
storeSingleObjectIfNotFoundFactory: storeSingleObjectIfNotFoundFactory({ db })
})
@@ -360,25 +206,9 @@ describe('Comments @comments', () => {
let notificationsState: NotificationsStateManager
const user = {
name: 'The comment wizard',
email: 'comment@wizard.ry',
password: 'i did not like Rivendel wine :(',
id: ''
}
const otherUser = {
name: 'Fondalf The Brey',
email: 'totalnotfakegandalf87@mordor.com',
password: 'what gandalf puts in his pipe stays in his pipe',
id: ''
}
const stream = {
name: 'Commented stream',
description: 'Chit chats over here',
id: ''
}
let user: BasicTestUser
let otherUser: BasicTestUser
let stream: BasicTestStream
const testObject1 = {
foo: 'bar',
@@ -400,10 +230,27 @@ describe('Comments @comments', () => {
const { app: express } = await beforeEachContext()
app = express
user.id = await createUser(user)
otherUser.id = await createUser(otherUser)
user = await createTestUser({
name: 'The comment wizard',
email: 'comment@wizard.ry',
password: 'i did not like Rivendel wine :(',
id: ''
})
otherUser = await createTestUser({
name: 'Fondalf The Brey',
email: 'totalnotfakegandalf87@mordor.com',
password: 'what gandalf puts in his pipe stays in his pipe',
id: ''
})
stream.id = await createStream({ ...stream, ownerId: user.id })
stream = await createTestStream(
{
name: 'Commented stream',
description: 'Chit chats over here',
id: ''
},
user
)
testObject1.id = await createObject({ streamId: stream.id, object: testObject1 })
testObject2.id = await createObject({ streamId: stream.id, object: testObject2 })
@@ -512,8 +359,10 @@ describe('Comments @comments', () => {
const throwawayCommentText = buildCommentInputFromString('whatever')
// Stream A belongs to user
const streamA = { name: 'Stream A', id: '' }
streamA.id = await createStream({ ...streamA, ownerId: user.id })
const streamA = await createTestStream(
buildBasicTestProject({ name: 'Stream A' }),
user
)
const objA = { foo: 'bar', id: '' }
objA.id = await createObject({ streamId: streamA.id, object: objA })
const commA = { id: '' }
@@ -528,8 +377,10 @@ describe('Comments @comments', () => {
).id
// Stream B belongs to otherUser
const streamB = { name: 'Stream B', id: '' }
streamB.id = await createStream({ ...streamB, ownerId: otherUser.id })
const streamB = await createTestStream(
buildBasicTestProject({ name: 'Stream B' }),
otherUser
)
const objB = { qux: 'mux', id: '' }
objB.id = await createObject({ streamId: streamB.id, object: objB })
const commB = { id: '' }
@@ -629,14 +480,17 @@ describe('Comments @comments', () => {
})
it('Should return comment counts for streams, commits and objects', async () => {
const stream = { name: 'Bean Counter', id: '' }
stream.id = await createStream({ ...stream, ownerId: user.id })
const newStream = await createTestStream(
buildBasicTestProject({ name: 'Bean Counter' }),
user
)
const obj = { foo: 'bar', id: '' }
obj.id = await createObject({ streamId: stream.id, object: obj })
obj.id = await createObject({ streamId: newStream.id, object: obj })
const commit = { id: '' }
commit.id = (
await createCommitByBranchName({
streamId: stream.id,
streamId: newStream.id,
branchName: 'main',
message: 'baz',
objectId: obj.id,
@@ -653,7 +507,7 @@ describe('Comments @comments', () => {
userId: user.id,
input: {
text: buildCommentInputFromString('bar'),
streamId: stream.id,
streamId: newStream.id,
resources: [
{ resourceId: commit.id, resourceType: ResourceType.Commit },
{ resourceId: obj.id, resourceType: ResourceType.Object }
@@ -669,7 +523,7 @@ describe('Comments @comments', () => {
userId: user.id,
input: {
text: buildCommentInputFromString('baz'),
streamId: stream.id,
streamId: newStream.id,
resources: [{ resourceId: commit.id, resourceType: ResourceType.Commit }],
blobIds: [],
data: {}
@@ -682,7 +536,7 @@ describe('Comments @comments', () => {
userId: user.id,
input: {
text: buildCommentInputFromString('qux'),
streamId: stream.id,
streamId: newStream.id,
resources: [{ resourceId: obj.id, resourceType: ResourceType.Object }],
blobIds: [],
data: {}
@@ -695,7 +549,7 @@ describe('Comments @comments', () => {
await createCommentReply({
authorId: user.id,
parentCommentId: commentIds[0],
streamId: stream.id,
streamId: newStream.id,
text: buildCommentInputFromString(),
data: {},
blobIds: []
@@ -703,7 +557,7 @@ describe('Comments @comments', () => {
await createCommentReply({
authorId: user.id,
parentCommentId: commentIds[1],
streamId: stream.id,
streamId: newStream.id,
text: buildCommentInputFromString(),
data: {},
blobIds: []
@@ -711,7 +565,7 @@ describe('Comments @comments', () => {
await createCommentReply({
authorId: user.id,
parentCommentId: commentIds[2],
streamId: stream.id,
streamId: newStream.id,
text: buildCommentInputFromString(),
data: {},
blobIds: []
@@ -721,11 +575,11 @@ describe('Comments @comments', () => {
await archiveComment({
commentId: commentIds[commentIds.length - 1],
userId: user.id,
streamId: stream.id,
streamId: newStream.id,
archived: true
})
const count = await getStreamCommentCount(stream.id, { threadsOnly: true }) // should be 30
const count = await getStreamCommentCount(newStream.id, { threadsOnly: true }) // should be 30
expect(count).to.equal(commCount * 3 - 1)
const objCount = await getResourceCommentCount({ resourceId: obj.id })
@@ -734,8 +588,10 @@ describe('Comments @comments', () => {
const commitCount = await getResourceCommentCount({ resourceId: commit.id })
expect(commitCount).to.equal(commCount * 2)
const streamOther = { name: 'Bean Counter', id: '' }
streamOther.id = await createStream({ ...streamOther, ownerId: user.id })
const streamOther = await createTestStream(
buildBasicTestProject({ name: 'Bean Counter' }),
user
)
const objOther = { 'are you bored': 'yes', id: '' }
objOther.id = await createObject({ streamId: streamOther.id, object: objOther })
const commitOther = { id: '' }
@@ -1558,7 +1414,7 @@ describe('Comments @comments', () => {
})
it('both legacy (string) comments and new (ProseMirror) documents are formatted as SmartTextEditorValue values', async () => {
const streamId = await createStream({ ...buildTestStream(), ownerId: user.id })
const { id: streamId } = await createTestStream(buildTestStream(), user)
await Promise.all([
// Legacy
@@ -1626,7 +1482,7 @@ describe('Comments @comments', () => {
})
it('legacy comment with a single link is formatted correctly', async () => {
const streamId = await createStream({ ...buildTestStream(), ownerId: user.id })
const { id: streamId } = await createTestStream(buildTestStream(), user)
// Low-level insert cause all we need are just the main DB entries
const item = {
@@ -1672,7 +1528,7 @@ describe('Comments @comments', () => {
})
it('legacy comment with multiple links formats them correctly', async () => {
const streamId = await createStream({ ...buildTestStream(), ownerId: user.id })
const { id: streamId } = await createTestStream(buildTestStream(), user)
const textParts = [
"Here's one ",
+1
View File
@@ -308,6 +308,7 @@ export const UsersMeta = buildMetaTableHelper(
'isProjectsActive',
'newWorkspaceExplainerDismissed',
'speckleConBannerDismissed',
'intelligenceCommunityStandUpBannerDismissed',
'legacyProjectsExplainerCollapsed',
// Used in tests
'foo',
@@ -17,6 +17,7 @@ import type { GendoAIRenderGraphQLReturn } from '@/modules/gendo/helpers/types/g
import type { ServerRegionItemGraphQLReturn } from '@/modules/multiregion/helpers/graphTypes';
import type { AccSyncItemGraphQLReturn, AccSyncItemMutationsGraphQLReturn } from '@/modules/acc/helpers/graphTypes';
import type { SavedViewGraphQLReturn, SavedViewGroupGraphQLReturn, SavedViewPermissionChecksGraphQLReturn, SavedViewGroupPermissionChecksGraphQLReturn, ExtendedViewerResourcesGraphQLReturn } from '@/modules/viewer/helpers/graphTypes';
import type { DashboardGraphQLReturn, DashboardMutationsGraphQLReturn, DashboardPermissionChecksGraphQLReturn, DashboardTokenGraphQLReturn } from '@/modules/dashboards/helpers/graphTypes';
import type { GraphQLContext } from '@/modules/shared/helpers/typeHelper';
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
@@ -1055,6 +1056,12 @@ export type CreateCommentReplyInput = {
threadId: Scalars['String']['input'];
};
export type CreateDashboardTokenReturn = {
__typename?: 'CreateDashboardTokenReturn';
token: Scalars['String']['output'];
tokenMetadata: DashboardToken;
};
export type CreateEmbedTokenReturn = {
__typename?: 'CreateEmbedTokenReturn';
token: Scalars['String']['output'];
@@ -1127,6 +1134,97 @@ export type CurrencyBasedPrices = {
usd: WorkspacePaidPlanPrices;
};
export type Dashboard = {
__typename?: 'Dashboard';
createdAt: Scalars['DateTime']['output'];
createdBy?: Maybe<LimitedUser>;
id: Scalars['String']['output'];
name: Scalars['String']['output'];
permissions: DashboardPermissionChecks;
/** If null, this is a new dashboard and should be initialized by the client */
state?: Maybe<Scalars['String']['output']>;
updatedAt: Scalars['DateTime']['output'];
workspace: LimitedWorkspace;
};
export type DashboardCollection = {
__typename?: 'DashboardCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<Dashboard>;
totalCount: Scalars['Int']['output'];
};
export type DashboardCreateInput = {
name: Scalars['String']['input'];
};
export type DashboardMutations = {
__typename?: 'DashboardMutations';
create: Dashboard;
createToken: CreateDashboardTokenReturn;
delete: Scalars['Boolean']['output'];
update: Dashboard;
};
export type DashboardMutationsCreateArgs = {
input: DashboardCreateInput;
workspace: WorkspaceIdentifier;
};
export type DashboardMutationsCreateTokenArgs = {
dashboardId: Scalars['String']['input'];
};
export type DashboardMutationsDeleteArgs = {
id: Scalars['String']['input'];
};
export type DashboardMutationsUpdateArgs = {
input: DashboardUpdateInput;
};
export type DashboardPermissionChecks = {
__typename?: 'DashboardPermissionChecks';
canCreateToken: PermissionCheckResult;
canDelete: PermissionCheckResult;
canEdit: PermissionCheckResult;
canRead: PermissionCheckResult;
};
export type DashboardToken = {
__typename?: 'DashboardToken';
createdAt: Scalars['DateTime']['output'];
dashboard: Dashboard;
lastUsed: Scalars['DateTime']['output'];
lifespan: Scalars['BigInt']['output'];
projects: Array<Project>;
tokenId: Scalars['String']['output'];
user?: Maybe<LimitedUser>;
};
export type DashboardTokenCollection = {
__typename?: 'DashboardTokenCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<DashboardToken>;
totalCount: Scalars['Int']['output'];
};
export type DashboardTokenCreateInput = {
dashboardId: Scalars['String']['input'];
lifespan?: InputMaybe<Scalars['BigInt']['input']>;
};
export type DashboardUpdateInput = {
id: Scalars['String']['input'];
name?: InputMaybe<Scalars['String']['input']>;
projectIds?: InputMaybe<Array<Scalars['String']['input']>>;
state?: InputMaybe<Scalars['String']['input']>;
};
export type DeleteAccSyncItemInput = {
id: Scalars['ID']['input'];
projectId: Scalars['String']['input'];
@@ -1799,6 +1897,7 @@ export type Mutation = {
* @deprecated Part of the old API surface and will be removed in the future. Use VersionMutations.moveToModel instead.
*/
commitsMove: Scalars['Boolean']['output'];
dashboardMutations: DashboardMutations;
fileUploadMutations: FileUploadMutations;
/**
* Delete a pending invite
@@ -2375,6 +2474,7 @@ export type Project = {
/** All comment threads in this project */
commentThreads: ProjectCommentCollection;
createdAt: Scalars['DateTime']['output'];
dashboardTokens: DashboardTokenCollection;
description?: Maybe<Scalars['String']['output']>;
/** Public project-level configuration for embedded viewer */
embedOptions: ProjectEmbedOptions;
@@ -2483,6 +2583,12 @@ export type ProjectCommentThreadsArgs = {
};
export type ProjectDashboardTokensArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
limit?: InputMaybe<Scalars['Int']['input']>;
};
export type ProjectEmbedTokensArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
limit?: InputMaybe<Scalars['Int']['input']>;
@@ -3203,6 +3309,7 @@ export type Query = {
* @deprecated Use Project/Version/Model 'commentThreads' fields instead
*/
comments?: Maybe<CommentCollection>;
dashboard: Dashboard;
/**
* All of the discoverable streams of the server
* @deprecated Part of the old API surface and will be removed in the future.
@@ -3344,6 +3451,11 @@ export type QueryCommentsArgs = {
};
export type QueryDashboardArgs = {
id: Scalars['String']['input'];
};
export type QueryDiscoverableStreamsArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
limit?: Scalars['Int']['input'];
@@ -3502,6 +3614,11 @@ export type SavedView = {
group: SavedViewGroup;
/** Empty ID means default/ungrouped view */
groupId?: Maybe<Scalars['ID']['output']>;
/**
* Truncated resourceIds w/o specific version data that is used to associate the view w/
* specific groups
*/
groupResourceIds: Array<Scalars['String']['output']>;
id: Scalars['ID']['output'];
isHomeView: Scalars['Boolean']['output'];
name: Scalars['String']['output'];
@@ -4790,6 +4907,7 @@ export type UserGendoAiCredits = {
export type UserMeta = {
__typename?: 'UserMeta';
intelligenceCommunityStandUpBannerDismissed: Scalars['Boolean']['output'];
legacyProjectsExplainerCollapsed: Scalars['Boolean']['output'];
newWorkspaceExplainerDismissed: Scalars['Boolean']['output'];
speckleConBannerDismissed: Scalars['Boolean']['output'];
@@ -4797,12 +4915,18 @@ export type UserMeta = {
export type UserMetaMutations = {
__typename?: 'UserMetaMutations';
setIntelligenceCommunityStandUpBannerDismissed: Scalars['Boolean']['output'];
setLegacyProjectsExplainerCollapsed: Scalars['Boolean']['output'];
setNewWorkspaceExplainerDismissed: Scalars['Boolean']['output'];
setSpeckleConBannerDismissed: Scalars['Boolean']['output'];
};
export type UserMetaMutationsSetIntelligenceCommunityStandUpBannerDismissedArgs = {
value: Scalars['Boolean']['input'];
};
export type UserMetaMutationsSetLegacyProjectsExplainerCollapsedArgs = {
value: Scalars['Boolean']['input'];
};
@@ -5134,6 +5258,7 @@ export type Workspace = {
/** Info about the workspace creation state */
creationState?: Maybe<WorkspaceCreationState>;
customerPortalUrl?: Maybe<Scalars['String']['output']>;
dashboards: DashboardCollection;
/**
* The default role workspace members will receive for workspace projects.
* @deprecated Always the reviewer role. Will be removed in the future.
@@ -5202,6 +5327,12 @@ export type WorkspaceAutomateFunctionsArgs = {
};
export type WorkspaceDashboardsArgs = {
cursor?: InputMaybe<Scalars['String']['input']>;
limit?: Scalars['Int']['input'];
};
export type WorkspaceHasAccessToFeatureArgs = {
featureName: WorkspaceFeatureName;
};
@@ -5334,6 +5465,10 @@ export const WorkspaceFeatureName = {
} as const;
export type WorkspaceFeatureName = typeof WorkspaceFeatureName[keyof typeof WorkspaceFeatureName];
export type WorkspaceIdentifier =
{ id: Scalars['String']['input']; slug?: never; }
| { id?: never; slug: Scalars['String']['input']; };
export type WorkspaceInviteCreateInput = {
/** Either this or userId must be filled */
email?: InputMaybe<Scalars['String']['input']>;
@@ -5559,9 +5694,11 @@ export const WorkspacePaymentMethod = {
export type WorkspacePaymentMethod = typeof WorkspacePaymentMethod[keyof typeof WorkspacePaymentMethod];
export type WorkspacePermissionChecks = {
__typename?: 'WorkspacePermissionChecks';
canCreateDashboards: PermissionCheckResult;
canCreateProject: PermissionCheckResult;
canEditEmbedOptions: PermissionCheckResult;
canInvite: PermissionCheckResult;
canListDashboards: PermissionCheckResult;
canMakeWorkspaceExclusive: PermissionCheckResult;
canMoveProjectToWorkspace: PermissionCheckResult;
canReadMemberEmail: PermissionCheckResult;
@@ -6007,6 +6144,7 @@ export type ResolversTypes = {
CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput;
CreateCommentInput: CreateCommentInput;
CreateCommentReplyInput: CreateCommentReplyInput;
CreateDashboardTokenReturn: ResolverTypeWrapper<Omit<CreateDashboardTokenReturn, 'tokenMetadata'> & { tokenMetadata: ResolversTypes['DashboardToken'] }>;
CreateEmbedTokenReturn: ResolverTypeWrapper<Omit<CreateEmbedTokenReturn, 'tokenMetadata'> & { tokenMetadata: ResolversTypes['EmbedToken'] }>;
CreateModelInput: CreateModelInput;
CreateSavedViewGroupInput: CreateSavedViewGroupInput;
@@ -6016,6 +6154,15 @@ export type ResolversTypes = {
CreateVersionInput: CreateVersionInput;
Currency: Currency;
CurrencyBasedPrices: ResolverTypeWrapper<Omit<CurrencyBasedPrices, 'gbp' | 'usd'> & { gbp: ResolversTypes['WorkspacePaidPlanPrices'], usd: ResolversTypes['WorkspacePaidPlanPrices'] }>;
Dashboard: ResolverTypeWrapper<DashboardGraphQLReturn>;
DashboardCollection: ResolverTypeWrapper<Omit<DashboardCollection, 'items'> & { items: Array<ResolversTypes['Dashboard']> }>;
DashboardCreateInput: DashboardCreateInput;
DashboardMutations: ResolverTypeWrapper<DashboardMutationsGraphQLReturn>;
DashboardPermissionChecks: ResolverTypeWrapper<DashboardPermissionChecksGraphQLReturn>;
DashboardToken: ResolverTypeWrapper<DashboardTokenGraphQLReturn>;
DashboardTokenCollection: ResolverTypeWrapper<Omit<DashboardTokenCollection, 'items'> & { items: Array<ResolversTypes['DashboardToken']> }>;
DashboardTokenCreateInput: DashboardTokenCreateInput;
DashboardUpdateInput: DashboardUpdateInput;
DateTime: ResolverTypeWrapper<Scalars['DateTime']['output']>;
DeleteAccSyncItemInput: DeleteAccSyncItemInput;
DeleteModelInput: DeleteModelInput;
@@ -6250,6 +6397,7 @@ export type ResolversTypes = {
WorkspaceEmbedOptions: ResolverTypeWrapper<WorkspaceEmbedOptions>;
WorkspaceFeatureFlagName: WorkspaceFeatureFlagName;
WorkspaceFeatureName: WorkspaceFeatureName;
WorkspaceIdentifier: WorkspaceIdentifier;
WorkspaceInviteCreateInput: WorkspaceInviteCreateInput;
WorkspaceInviteLookupOptions: WorkspaceInviteLookupOptions;
WorkspaceInviteMutations: ResolverTypeWrapper<WorkspaceInviteMutationsGraphQLReturn>;
@@ -6389,6 +6537,7 @@ export type ResolversParentTypes = {
CreateAutomateFunctionWithoutVersionInput: CreateAutomateFunctionWithoutVersionInput;
CreateCommentInput: CreateCommentInput;
CreateCommentReplyInput: CreateCommentReplyInput;
CreateDashboardTokenReturn: Omit<CreateDashboardTokenReturn, 'tokenMetadata'> & { tokenMetadata: ResolversParentTypes['DashboardToken'] };
CreateEmbedTokenReturn: Omit<CreateEmbedTokenReturn, 'tokenMetadata'> & { tokenMetadata: ResolversParentTypes['EmbedToken'] };
CreateModelInput: CreateModelInput;
CreateSavedViewGroupInput: CreateSavedViewGroupInput;
@@ -6397,6 +6546,15 @@ export type ResolversParentTypes = {
CreateUserEmailInput: CreateUserEmailInput;
CreateVersionInput: CreateVersionInput;
CurrencyBasedPrices: Omit<CurrencyBasedPrices, 'gbp' | 'usd'> & { gbp: ResolversParentTypes['WorkspacePaidPlanPrices'], usd: ResolversParentTypes['WorkspacePaidPlanPrices'] };
Dashboard: DashboardGraphQLReturn;
DashboardCollection: Omit<DashboardCollection, 'items'> & { items: Array<ResolversParentTypes['Dashboard']> };
DashboardCreateInput: DashboardCreateInput;
DashboardMutations: DashboardMutationsGraphQLReturn;
DashboardPermissionChecks: DashboardPermissionChecksGraphQLReturn;
DashboardToken: DashboardTokenGraphQLReturn;
DashboardTokenCollection: Omit<DashboardTokenCollection, 'items'> & { items: Array<ResolversParentTypes['DashboardToken']> };
DashboardTokenCreateInput: DashboardTokenCreateInput;
DashboardUpdateInput: DashboardUpdateInput;
DateTime: Scalars['DateTime']['output'];
DeleteAccSyncItemInput: DeleteAccSyncItemInput;
DeleteModelInput: DeleteModelInput;
@@ -6606,6 +6764,7 @@ export type ResolversParentTypes = {
WorkspaceDomain: WorkspaceDomain;
WorkspaceDomainDeleteInput: WorkspaceDomainDeleteInput;
WorkspaceEmbedOptions: WorkspaceEmbedOptions;
WorkspaceIdentifier: WorkspaceIdentifier;
WorkspaceInviteCreateInput: WorkspaceInviteCreateInput;
WorkspaceInviteLookupOptions: WorkspaceInviteLookupOptions;
WorkspaceInviteMutations: WorkspaceInviteMutationsGraphQLReturn;
@@ -7147,6 +7306,12 @@ export type CountOnlyCollectionResolvers<ContextType = GraphQLContext, ParentTyp
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateDashboardTokenReturnResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CreateDashboardTokenReturn'] = ResolversParentTypes['CreateDashboardTokenReturn']> = {
token?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
tokenMetadata?: Resolver<ResolversTypes['DashboardToken'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type CreateEmbedTokenReturnResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['CreateEmbedTokenReturn'] = ResolversParentTypes['CreateEmbedTokenReturn']> = {
token?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
tokenMetadata?: Resolver<ResolversTypes['EmbedToken'], ParentType, ContextType>;
@@ -7159,6 +7324,59 @@ export type CurrencyBasedPricesResolvers<ContextType = GraphQLContext, ParentTyp
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DashboardResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Dashboard'] = ResolversParentTypes['Dashboard']> = {
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
createdBy?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
permissions?: Resolver<ResolversTypes['DashboardPermissionChecks'], ParentType, ContextType>;
state?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
workspace?: Resolver<ResolversTypes['LimitedWorkspace'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DashboardCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['DashboardCollection'] = ResolversParentTypes['DashboardCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['Dashboard']>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DashboardMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['DashboardMutations'] = ResolversParentTypes['DashboardMutations']> = {
create?: Resolver<ResolversTypes['Dashboard'], ParentType, ContextType, RequireFields<DashboardMutationsCreateArgs, 'input' | 'workspace'>>;
createToken?: Resolver<ResolversTypes['CreateDashboardTokenReturn'], ParentType, ContextType, RequireFields<DashboardMutationsCreateTokenArgs, 'dashboardId'>>;
delete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<DashboardMutationsDeleteArgs, 'id'>>;
update?: Resolver<ResolversTypes['Dashboard'], ParentType, ContextType, RequireFields<DashboardMutationsUpdateArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DashboardPermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['DashboardPermissionChecks'] = ResolversParentTypes['DashboardPermissionChecks']> = {
canCreateToken?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canDelete?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canEdit?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canRead?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DashboardTokenResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['DashboardToken'] = ResolversParentTypes['DashboardToken']> = {
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
dashboard?: Resolver<ResolversTypes['Dashboard'], ParentType, ContextType>;
lastUsed?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
lifespan?: Resolver<ResolversTypes['BigInt'], ParentType, ContextType>;
projects?: Resolver<Array<ResolversTypes['Project']>, ParentType, ContextType>;
tokenId?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
user?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type DashboardTokenCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['DashboardTokenCollection'] = ResolversParentTypes['DashboardTokenCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['DashboardToken']>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['DateTime'], any> {
name: 'DateTime';
}
@@ -7424,6 +7642,7 @@ export type MutationResolvers<ContextType = GraphQLContext, ParentType extends R
commitUpdate?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationCommitUpdateArgs, 'commit'>>;
commitsDelete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationCommitsDeleteArgs, 'input'>>;
commitsMove?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationCommitsMoveArgs, 'input'>>;
dashboardMutations?: Resolver<ResolversTypes['DashboardMutations'], ParentType, ContextType>;
fileUploadMutations?: Resolver<ResolversTypes['FileUploadMutations'], ParentType, ContextType>;
inviteDelete?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationInviteDeleteArgs, 'inviteId'>>;
inviteResend?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationInviteResendArgs, 'inviteId'>>;
@@ -7552,6 +7771,7 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
comment?: Resolver<Maybe<ResolversTypes['Comment']>, ParentType, ContextType, RequireFields<ProjectCommentArgs, 'id'>>;
commentThreads?: Resolver<ResolversTypes['ProjectCommentCollection'], ParentType, ContextType, Partial<ProjectCommentThreadsArgs>>;
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
dashboardTokens?: Resolver<ResolversTypes['DashboardTokenCollection'], ParentType, ContextType, Partial<ProjectDashboardTokensArgs>>;
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
embedOptions?: Resolver<ResolversTypes['ProjectEmbedOptions'], ParentType, ContextType>;
embedTokens?: Resolver<ResolversTypes['EmbedTokenCollection'], ParentType, ContextType, Partial<ProjectEmbedTokensArgs>>;
@@ -7805,6 +8025,7 @@ export type QueryResolvers<ContextType = GraphQLContext, ParentType extends Reso
automateValidateAuthCode?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<QueryAutomateValidateAuthCodeArgs, 'payload'>>;
comment?: Resolver<Maybe<ResolversTypes['Comment']>, ParentType, ContextType, RequireFields<QueryCommentArgs, 'id' | 'streamId'>>;
comments?: Resolver<Maybe<ResolversTypes['CommentCollection']>, ParentType, ContextType, RequireFields<QueryCommentsArgs, 'archived' | 'limit' | 'streamId'>>;
dashboard?: Resolver<ResolversTypes['Dashboard'], ParentType, ContextType, RequireFields<QueryDashboardArgs, 'id'>>;
discoverableStreams?: Resolver<Maybe<ResolversTypes['StreamCollection']>, ParentType, ContextType, RequireFields<QueryDiscoverableStreamsArgs, 'limit'>>;
otherUser?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType, RequireFields<QueryOtherUserArgs, 'id'>>;
project?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<QueryProjectArgs, 'id'>>;
@@ -7854,6 +8075,7 @@ export type SavedViewResolvers<ContextType = GraphQLContext, ParentType extends
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
group?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType>;
groupId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
groupResourceIds?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isHomeView?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -8264,6 +8486,7 @@ export type UserGendoAiCreditsResolvers<ContextType = GraphQLContext, ParentType
};
export type UserMetaResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['UserMeta'] = ResolversParentTypes['UserMeta']> = {
intelligenceCommunityStandUpBannerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
legacyProjectsExplainerCollapsed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
newWorkspaceExplainerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
speckleConBannerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
@@ -8271,6 +8494,7 @@ export type UserMetaResolvers<ContextType = GraphQLContext, ParentType extends R
};
export type UserMetaMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['UserMetaMutations'] = ResolversParentTypes['UserMetaMutations']> = {
setIntelligenceCommunityStandUpBannerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaMutationsSetIntelligenceCommunityStandUpBannerDismissedArgs, 'value'>>;
setLegacyProjectsExplainerCollapsed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaMutationsSetLegacyProjectsExplainerCollapsedArgs, 'value'>>;
setNewWorkspaceExplainerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaMutationsSetNewWorkspaceExplainerDismissedArgs, 'value'>>;
setSpeckleConBannerDismissed?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<UserMetaMutationsSetSpeckleConBannerDismissedArgs, 'value'>>;
@@ -8426,6 +8650,7 @@ export type WorkspaceResolvers<ContextType = GraphQLContext, ParentType extends
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
creationState?: Resolver<Maybe<ResolversTypes['WorkspaceCreationState']>, ParentType, ContextType>;
customerPortalUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
dashboards?: Resolver<ResolversTypes['DashboardCollection'], ParentType, ContextType, RequireFields<WorkspaceDashboardsArgs, 'limit'>>;
defaultProjectRole?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
defaultRegion?: Resolver<Maybe<ResolversTypes['ServerRegionItem']>, ParentType, ContextType>;
defaultSeatType?: Resolver<ResolversTypes['WorkspaceSeatType'], ParentType, ContextType>;
@@ -8569,9 +8794,11 @@ export type WorkspacePaidPlanPricesResolvers<ContextType = GraphQLContext, Paren
};
export type WorkspacePermissionChecksResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspacePermissionChecks'] = ResolversParentTypes['WorkspacePermissionChecks']> = {
canCreateDashboards?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canCreateProject?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canEditEmbedOptions?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canInvite?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canListDashboards?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canMakeWorkspaceExclusive?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canMoveProjectToWorkspace?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType, Partial<WorkspacePermissionChecksCanMoveProjectToWorkspaceArgs>>;
canReadMemberEmail?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
@@ -8739,8 +8966,15 @@ export type Resolvers<ContextType = GraphQLContext> = {
Commit?: CommitResolvers<ContextType>;
CommitCollection?: CommitCollectionResolvers<ContextType>;
CountOnlyCollection?: CountOnlyCollectionResolvers<ContextType>;
CreateDashboardTokenReturn?: CreateDashboardTokenReturnResolvers<ContextType>;
CreateEmbedTokenReturn?: CreateEmbedTokenReturnResolvers<ContextType>;
CurrencyBasedPrices?: CurrencyBasedPricesResolvers<ContextType>;
Dashboard?: DashboardResolvers<ContextType>;
DashboardCollection?: DashboardCollectionResolvers<ContextType>;
DashboardMutations?: DashboardMutationsResolvers<ContextType>;
DashboardPermissionChecks?: DashboardPermissionChecksResolvers<ContextType>;
DashboardToken?: DashboardTokenResolvers<ContextType>;
DashboardTokenCollection?: DashboardTokenCollectionResolvers<ContextType>;
DateTime?: GraphQLScalarType;
EmbedToken?: EmbedTokenResolvers<ContextType>;
EmbedTokenCollection?: EmbedTokenCollectionResolvers<ContextType>;
@@ -9063,6 +9297,18 @@ export type SetSpeckleConBannerDismissedMutationVariables = Exact<{
export type SetSpeckleConBannerDismissedMutation = { __typename?: 'Mutation', activeUserMutations: { __typename?: 'ActiveUserMutations', meta: { __typename?: 'UserMetaMutations', setSpeckleConBannerDismissed: boolean } } };
export type GetIntelligenceCommunityStandUpBannerDismissedQueryVariables = Exact<{ [key: string]: never; }>;
export type GetIntelligenceCommunityStandUpBannerDismissedQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', meta: { __typename?: 'UserMeta', intelligenceCommunityStandUpBannerDismissed: boolean } } | null };
export type SetIntelligenceCommunityStandUpBannerDismissedMutationVariables = Exact<{
input: Scalars['Boolean']['input'];
}>;
export type SetIntelligenceCommunityStandUpBannerDismissedMutation = { __typename?: 'Mutation', activeUserMutations: { __typename?: 'ActiveUserMutations', meta: { __typename?: 'UserMetaMutations', setIntelligenceCommunityStandUpBannerDismissed: boolean } } };
export type GetLegacyProjectsExplainerCollapsedQueryVariables = Exact<{ [key: string]: never; }>;
@@ -10401,6 +10647,8 @@ export const GetNewWorkspaceExplainerDismissedDocument = {"kind":"Document","def
export const SetNewWorkspaceExplainerDismissedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetNewWorkspaceExplainerDismissed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setNewWorkspaceExplainerDismissed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<SetNewWorkspaceExplainerDismissedMutation, SetNewWorkspaceExplainerDismissedMutationVariables>;
export const GetSpeckleConBannerDismissedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSpeckleConBannerDismissed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"speckleConBannerDismissed"}}]}}]}}]}}]} as unknown as DocumentNode<GetSpeckleConBannerDismissedQuery, GetSpeckleConBannerDismissedQueryVariables>;
export const SetSpeckleConBannerDismissedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetSpeckleConBannerDismissed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setSpeckleConBannerDismissed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<SetSpeckleConBannerDismissedMutation, SetSpeckleConBannerDismissedMutationVariables>;
export const GetIntelligenceCommunityStandUpBannerDismissedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetIntelligenceCommunityStandUpBannerDismissed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"intelligenceCommunityStandUpBannerDismissed"}}]}}]}}]}}]} as unknown as DocumentNode<GetIntelligenceCommunityStandUpBannerDismissedQuery, GetIntelligenceCommunityStandUpBannerDismissedQueryVariables>;
export const SetIntelligenceCommunityStandUpBannerDismissedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetIntelligenceCommunityStandUpBannerDismissed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setIntelligenceCommunityStandUpBannerDismissed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<SetIntelligenceCommunityStandUpBannerDismissedMutation, SetIntelligenceCommunityStandUpBannerDismissedMutationVariables>;
export const GetLegacyProjectsExplainerCollapsedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLegacyProjectsExplainerCollapsed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"legacyProjectsExplainerCollapsed"}}]}}]}}]}}]} as unknown as DocumentNode<GetLegacyProjectsExplainerCollapsedQuery, GetLegacyProjectsExplainerCollapsedQueryVariables>;
export const SetLegacyProjectsExplainerCollapsedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetLegacyProjectsExplainerCollapsed"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"meta"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setLegacyProjectsExplainerCollapsed"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"value"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<SetLegacyProjectsExplainerCollapsedMutation, SetLegacyProjectsExplainerCollapsedMutationVariables>;
export const GetLimitedPersonalProjectVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLimitedPersonalProjectVersions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectVersion"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}},{"kind":"Field","name":{"kind":"Name","value":"type"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalProjectVersion"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedPersonalProjectComment"}}]}}]}}]}}]} as unknown as DocumentNode<GetLimitedPersonalProjectVersionsQuery, GetLimitedPersonalProjectVersionsQueryVariables>;
@@ -40,7 +40,6 @@ import { dbLogger } from '@/observability/logging'
import { getAdminUsersListCollectionFactory } from '@/modules/core/services/users/legacyAdminUsersList'
import type { Resolvers } from '@/modules/core/graph/generated/graphql'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getMailchimpStatus,
getMailchimpOnboardingIds
@@ -48,36 +47,19 @@ import {
import { updateMailchimpMemberTags } from '@/modules/auth/services/mailchimp'
import { withOperationLogging } from '@/observability/domain/businessLogging'
import { metaHelpers } from '@/modules/core/helpers/meta'
import { asOperation } from '@/modules/shared/command'
import { asMultiregionalOperation, asOperation } from '@/modules/shared/command'
import { setUserOnboardingChoicesFactory } from '@/modules/core/services/users/tracking'
import { getMixpanelClient } from '@/modules/shared/utils/mixpanel'
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
import { getUserWorkspaceSeatsFactory } from '@/modules/workspacesCore/repositories/workspaces'
import { queryAllProjectsFactory } from '@/modules/core/services/projects'
import { getAllRegisteredDbs } from '@/modules/multiregion/utils/dbSelector'
const getUser = legacyGetUserFactory({ db })
const getUserByEmail = legacyGetUserByEmailFactory({ db })
const updateUserAndNotify = updateUserAndNotifyFactory({
getUser: getUserFactory({ db }),
updateUser: updateUserFactory({ db }),
emitEvent: getEventBus().emit
})
const getServerInfo = getServerInfoFactory({ db })
const deleteUser = deleteUserFactory({
deleteStream: deleteStreamFactory({ db }),
logger: dbLogger,
isLastAdminUser: isLastAdminUserFactory({ db }),
getUserDeletableStreams: getUserDeletableStreamsFactory({ db }),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db })
}),
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db }),
deleteAllUserInvites: deleteAllUserInvitesFactory({ db }),
deleteUserRecord: deleteUserRecordFactory({ db }),
emitEvent: getEventBus().emit
})
const getUserRole = getUserRoleFactory({ db })
const changeUserRole = changeUserRoleFactory({
getServerInfo,
@@ -242,6 +224,13 @@ export default {
})
return !!metaVal?.value
},
intelligenceCommunityStandUpBannerDismissed: async (parent, _args, ctx) => {
const metaVal = await ctx.loaders.users.getUserMeta.load({
userId: parent.userId,
key: UsersMeta.metaKey.intelligenceCommunityStandUpBannerDismissed
})
return !!metaVal?.value
},
legacyProjectsExplainerCollapsed: async (parent, _args, ctx) => {
const metaVal = await ctx.loaders.users.getUserMeta.load({
userId: parent.userId,
@@ -261,14 +250,31 @@ export default {
const logger = context.log.child({
userIdToOperateOn: context.userId
})
await withOperationLogging(
async () => await updateUserAndNotify(context.userId!, args.user),
await asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const updateUserAndNotify = updateUserAndNotifyFactory({
getUser: getUserFactory({ db: mainDb }),
updateUser: async (...params) => {
const [res] = await Promise.all(
allDbs.map((db) => updateUserFactory({ db })(...params))
)
return res
},
emitEvent: emit
})
return await updateUserAndNotify(context.userId!, args.user)
},
{
dbs: await getAllRegisteredDbs(),
logger,
operationName: 'updateUser',
operationDescription: `Update user`
name: 'updateUser',
description: `Update user`
}
)
return true
},
@@ -299,14 +305,39 @@ export default {
const logger = context.log.child({
userIdToOperateOn: user.id
})
await withOperationLogging(
async () => await deleteUser(user.id, context.userId),
await asMultiregionalOperation(
({ mainDb, allDbs, emit }) => {
const deleteUser = deleteUserFactory({
deleteStream: deleteStreamFactory({ db: mainDb }),
logger: dbLogger,
isLastAdminUser: isLastAdminUserFactory({ db: mainDb }),
getUserDeletableStreams: getUserDeletableStreamsFactory({ db: mainDb }),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db: mainDb })
}),
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db: mainDb }),
deleteAllUserInvites: deleteAllUserInvitesFactory({ db: mainDb }),
deleteUserRecord: async (params) => {
const [res] = await Promise.all(
allDbs.map((db) => deleteUserRecordFactory({ db })(params))
)
return res
},
emitEvent: emit
})
return deleteUser(user.id, context.userId)
},
{
logger,
operationName: 'adminDeleteUser',
operationDescription: `Admin deletion of an user`
name: 'adminDeleteUser',
description: 'Admin deletion of an user',
dbs: await getAllRegisteredDbs()
}
)
return true
},
@@ -325,19 +356,40 @@ export default {
// Since I am paranoid, I'll leave them here too.
await throwForNotHavingServerRole(context, Roles.Server.Guest)
await validateScopes(context.scopes, Scopes.Profile.Delete)
await asMultiregionalOperation(
({ mainDb, allDbs, emit }) => {
const deleteUser = deleteUserFactory({
deleteStream: deleteStreamFactory({ db: mainDb }),
logger: dbLogger,
isLastAdminUser: isLastAdminUserFactory({ db: mainDb }),
getUserDeletableStreams: getUserDeletableStreamsFactory({ db: mainDb }),
queryAllProjects: queryAllProjectsFactory({
getExplicitProjects: getExplicitProjects({ db: mainDb })
}),
getUserWorkspaceSeats: getUserWorkspaceSeatsFactory({ db: mainDb }),
deleteAllUserInvites: deleteAllUserInvitesFactory({ db: mainDb }),
deleteUserRecord: async (params) => {
const [res] = await Promise.all(
allDbs.map((db) => deleteUserRecordFactory({ db })(params))
)
await withOperationLogging(
async () => await deleteUser(context.userId!, context.userId!),
return res
},
emitEvent: emit
})
return deleteUser(user.id, context.userId)
},
{
logger,
operationName: 'deleteUser',
operationDescription: `Delete user`
name: 'deleteUser',
description: 'Delete user',
dbs: await getAllRegisteredDbs()
}
)
return true
},
activeUserMutations: () => ({})
},
ActiveUserMutations: {
@@ -394,14 +446,31 @@ export default {
},
async update(_parent, args, context) {
const logger = context.log
const newUser = await withOperationLogging(
async () => await updateUserAndNotify(context.userId!, args.user),
const newUser = await asMultiregionalOperation(
async ({ mainDb, allDbs, emit }) => {
const updateUserAndNotify = updateUserAndNotifyFactory({
getUser: getUserFactory({ db: mainDb }),
updateUser: async (...params) => {
const [res] = await Promise.all(
allDbs.map((db) => updateUserFactory({ db })(...params))
)
return res
},
emitEvent: emit
})
return await updateUserAndNotify(context.userId!, args.user)
},
{
dbs: await getAllRegisteredDbs(),
logger,
operationName: 'updateUser',
operationDescription: 'Update user'
name: 'updateUser',
description: `Update user`
}
)
return newUser
},
meta: () => ({})
@@ -435,6 +504,16 @@ export default {
args.value
)
return !!res.value
},
setIntelligenceCommunityStandUpBannerDismissed: async (_parent, args, ctx) => {
const meta = metaHelpers(Users, db)
const res = await meta.set(
ctx.userId!,
UsersMeta.metaKey.intelligenceCommunityStandUpBannerDismissed,
args.value
)
return !!res.value
}
}
@@ -0,0 +1,23 @@
import type { Knex } from 'knex'
const tableName = 'users'
const colUuid = 'suuid'
const colCreatedAt = 'createdAt'
const colVerified = 'verified'
export async function up(knex: Knex): Promise<void> {
await knex.schema.raw(`
ALTER TABLE "${tableName}"
ALTER COLUMN "${colUuid}" DROP DEFAULT,
ALTER COLUMN "${colCreatedAt}" DROP DEFAULT,
ALTER COLUMN "${colVerified}" DROP DEFAULT;
`)
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(tableName, (table) => {
table.string(colUuid).defaultTo(knex.raw('gen_random_uuid()')).alter()
table.timestamp(colCreatedAt).defaultTo(knex.fn.now()).alter()
table.boolean(colVerified).defaultTo(false).alter()
})
}
@@ -21,8 +21,6 @@ import { UserValidationError } from '@/modules/core/errors/user'
import type { Knex } from 'knex'
import type { ServerRoles } from '@speckle/shared'
import { Roles } from '@speckle/shared'
import { updateUserEmailFactory } from '@/modules/core/repositories/userEmails'
import { markUserEmailAsVerifiedFactory } from '@/modules/core/services/users/emailVerification'
import type { UserWithOptionalRole } from '@/modules/core/domain/users/types'
import type {
BulkLookupUsers,
@@ -228,11 +226,7 @@ export const markUserAsVerifiedFactory =
[UserCols.verified]: true
})
const userEmailsUpdate = await markUserEmailAsVerifiedFactory({
updateUserEmail: updateUserEmailFactory({ db: deps.db })
})({ email: email.toLowerCase().trim() })
return !!(usersUpdate || userEmailsUpdate)
return !!usersUpdate
}
export const markOnboardingCompleteFactory =
@@ -285,13 +279,6 @@ export const updateUserFactory =
.where(Users.col.id, userId)
.update(update, '*')
if (update.email) {
await updateUserEmailFactory(deps)({
query: { userId, primary: true },
update: { email: update.email }
})
}
return newUser as Nullable<UserRecord>
}
@@ -54,6 +54,7 @@ import { WorkspaceEvents } from '@/modules/workspacesCore/domain/events'
import { ProjectEvents } from '@/modules/core/domain/projects/events'
import type { QueryAllProjects } from '@/modules/core/domain/projects/operations'
import type { StreamWithOptionalRole } from '@/modules/core/repositories/streams'
import { v4 } from 'uuid'
const { FF_NO_PERSONAL_EMAILS_ENABLED } = getFeatureFlags()
@@ -169,11 +170,12 @@ export const createUserFactory =
const signUpCtx = user.signUpContext
let finalUser: typeof user &
Omit<NullableKeysToOptional<UserRecord>, 'suuid' | 'createdAt'> = {
let finalUser: typeof user & NullableKeysToOptional<UserRecord> = {
...user,
id: crs({ length: 10 }),
verified: user.verified || false
verified: user.verified || false,
createdAt: new Date(),
suuid: v4()
}
delete finalUser.signUpContext
@@ -207,7 +209,10 @@ export const createUserFactory =
'name',
'company',
'verified',
'avatar'
'avatar',
'verified',
'createdAt',
'suuid'
]) as typeof finalUser)
finalUser.email = finalUser.email.toLowerCase()
@@ -25,11 +25,8 @@ import {
} from '@/modules/core/repositories/branches'
import {
getStreamFactory,
createStreamFactory,
markBranchStreamUpdatedFactory,
markCommitStreamUpdatedFactory,
grantStreamPermissionsFactory,
getStreamRolesFactory
markCommitStreamUpdatedFactory
} from '@/modules/core/repositories/streams'
import {
createCommitByBranchIdFactory,
@@ -44,66 +41,21 @@ import {
getObjectFactory,
storeSingleObjectIfNotFoundFactory
} from '@/modules/core/repositories/objects'
import {
legacyCreateStreamFactory,
createStreamReturnRecordFactory
} from '@/modules/core/services/streams/management'
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
import {
findUserByTargetFactory,
insertInviteAndDeleteOldFactory,
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory,
findInviteFactory,
deleteInvitesByTargetFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
finalizeResourceInviteFactory
} from '@/modules/serverinvites/services/processing'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getPaginatedStreamBranchesFactory } from '@/modules/core/services/branch/retrieval'
import { createObjectFactory } from '@/modules/core/services/objects/management'
import { ensureError } from '@speckle/shared'
import { ModelEvents } from '@/modules/core/domain/branches/events'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import {
processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory
} from '@/modules/serverinvites/services/coreFinalization'
import {
addOrUpdateStreamCollaboratorFactory,
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
createTestStream,
type BasicTestStream
} from '@/test/speckle-helpers/streamHelper'
import { buildBasicTestProject } from '@/modules/core/tests/helpers/creation'
const db = knex
const Commits = () => knex('commits')
const getUser = getUserFactory({ db })
const getUsers = getUsersFactory({ db })
const markCommitStreamUpdated = markCommitStreamUpdatedFactory({ db })
const markBranchStreamUpdated = markBranchStreamUpdatedFactory({ db })
const getStream = getStreamFactory({ db: knex })
@@ -123,7 +75,6 @@ const deleteBranchAndNotify = deleteBranchAndNotifyFactory({
deleteBranchById: deleteBranchByIdFactory({ db: knex })
})
const getServerInfo = getServerInfoFactory({ db })
const getObject = getObjectFactory({ db: knex })
const createCommitByBranchId = createCommitByBranchIdFactory({
createCommit: createCommitFactory({ db }),
@@ -142,109 +93,6 @@ const createCommitByBranchName = createCommitByBranchNameFactory({
getBranchById: getBranchByIdFactory({ db })
})
const buildFinalizeProjectInvite = () =>
finalizeResourceInviteFactory({
findInvite: findInviteFactory({ db }),
validateInvite: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
}),
processInvite: processFinalizedProjectInviteFactory({
getProject: getStream,
addProjectRole: addOrUpdateStreamCollaboratorFactory({
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
getUser,
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
getStreamRoles: getStreamRolesFactory({ db }),
emitEvent: getEventBus().emit
})
}),
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
emitEvent: (...args) => getEventBus().emit(...args),
findEmail: findEmailFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
db
}),
renderEmail,
sendEmail
})
}),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
getUser,
getServerInfo
})
const createStream = legacyCreateStreamFactory({
createStreamReturnRecord: createStreamReturnRecordFactory({
inviteUsersToProject: inviteUsersToProjectFactory({
createAndSendInvite: createAndSendInviteFactory({
findUserByTarget: findUserByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
}),
getUser,
getServerInfo,
finalizeInvite: buildFinalizeProjectInvite()
}),
getUsers
}),
createStream: createStreamFactory({ db }),
createBranch: createBranchFactory({ db }),
storeProjectRole: storeProjectRoleFactory({ db }),
emitEvent: getEventBus().emit
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const getBranchesByStreamId = getPaginatedStreamBranchesFactory({
getPaginatedStreamBranchesPage: getPaginatedStreamBranchesPageFactory({ db }),
getStreamBranchCount: getStreamBranchCountFactory({ db })
@@ -254,19 +102,8 @@ const createObject = createObjectFactory({
})
describe('Branches @core-branches', () => {
const user = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
const stream = {
name: 'Test Stream References',
description: 'Whatever goes in here usually...',
id: ''
}
let user: BasicTestUser
let stream: BasicTestStream
const testObject = {
foo: 'bar',
baz: 'qux',
@@ -278,8 +115,19 @@ describe('Branches @core-branches', () => {
before(async () => {
await beforeEachContext()
user.id = await createUser(user)
stream.id = await createStream({ ...stream, ownerId: user.id })
user = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
stream = await createTestStream(
buildBasicTestProject({
name: 'Test Stream References',
description: 'Whatever goes in here usually...'
}),
user
)
testObject.id = await createObject({ streamId: stream.id, object: testObject })
})
@@ -36,55 +36,13 @@ import {
import {
getStreamFactory,
getCommitStreamFactory,
createStreamFactory,
markCommitStreamUpdatedFactory,
grantStreamPermissionsFactory,
getStreamRolesFactory
markCommitStreamUpdatedFactory
} from '@/modules/core/repositories/streams'
import {
getObjectFactory,
storeSingleObjectIfNotFoundFactory
} from '@/modules/core/repositories/objects'
import {
legacyCreateStreamFactory,
createStreamReturnRecordFactory
} from '@/modules/core/services/streams/management'
import { inviteUsersToProjectFactory } from '@/modules/serverinvites/services/projectInviteManagement'
import { createAndSendInviteFactory } from '@/modules/serverinvites/services/creation'
import {
findUserByTargetFactory,
insertInviteAndDeleteOldFactory,
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory,
findInviteFactory,
deleteInvitesByTargetFactory
} from '@/modules/serverinvites/repositories/serverInvites'
import { collectAndValidateCoreTargetsFactory } from '@/modules/serverinvites/services/coreResourceCollection'
import { buildCoreInviteEmailContentsFactory } from '@/modules/serverinvites/services/coreEmailContents'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getUsersFactory,
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} from '@/modules/core/repositories/users'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import {
finalizeInvitedServerRegistrationFactory,
finalizeResourceInviteFactory
} from '@/modules/serverinvites/services/processing'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import {
getBranchCommitsTotalCountByNameFactory,
getPaginatedBranchCommitsItemsByNameFactory
@@ -92,20 +50,14 @@ import {
import { createObjectFactory } from '@/modules/core/services/objects/management'
import { ensureError } from '@speckle/shared'
import { VersionEvents } from '@/modules/core/domain/commits/events'
import type { BasicTestUser } from '@/test/authHelper'
import { createTestUser } from '@/test/authHelper'
import {
processFinalizedProjectInviteFactory,
validateProjectInviteBeforeFinalizationFactory
} from '@/modules/serverinvites/services/coreFinalization'
import {
addOrUpdateStreamCollaboratorFactory,
validateStreamAccessFactory
} from '@/modules/core/services/streams/access'
import { authorizeResolver } from '@/modules/shared'
import { storeProjectRoleFactory } from '@/modules/core/repositories/projects'
createTestStream,
type BasicTestStream
} from '@/test/speckle-helpers/streamHelper'
import { buildBasicTestProject } from '@/modules/core/tests/helpers/creation'
const getServerInfo = getServerInfoFactory({ db })
const getUser = getUserFactory({ db })
const getUsers = getUsersFactory({ db })
const markCommitStreamUpdated = markCommitStreamUpdatedFactory({ db })
const getCommitStream = getCommitStreamFactory({ db })
const getStream = getStreamFactory({ db })
@@ -155,110 +107,6 @@ const updateCommitAndNotify = updateCommitAndNotifyFactory({
markCommitBranchUpdated: markCommitBranchUpdatedFactory({ db })
})
const getStreamCommitCount = getStreamCommitCountFactory({ db })
const buildFinalizeProjectInvite = () =>
finalizeResourceInviteFactory({
findInvite: findInviteFactory({ db }),
validateInvite: validateProjectInviteBeforeFinalizationFactory({
getProject: getStream
}),
processInvite: processFinalizedProjectInviteFactory({
getProject: getStream,
addProjectRole: addOrUpdateStreamCollaboratorFactory({
validateStreamAccess: validateStreamAccessFactory({ authorizeResolver }),
getUser,
grantStreamPermissions: grantStreamPermissionsFactory({ db }),
getStreamRoles: getStreamRolesFactory({ db }),
emitEvent: getEventBus().emit
})
}),
deleteInvitesByTarget: deleteInvitesByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
emitEvent: (...args) => getEventBus().emit(...args),
findEmail: findEmailFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail: findEmailFactory({ db }),
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification: requestNewEmailVerificationFactory({
findEmail: findEmailFactory({ db }),
getUser,
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({
db
}),
renderEmail,
sendEmail
})
}),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
getUser,
getServerInfo
})
const createStream = legacyCreateStreamFactory({
createStreamReturnRecord: createStreamReturnRecordFactory({
inviteUsersToProject: inviteUsersToProjectFactory({
createAndSendInvite: createAndSendInviteFactory({
findUserByTarget: findUserByTargetFactory({ db }),
insertInviteAndDeleteOld: insertInviteAndDeleteOldFactory({ db }),
collectAndValidateResourceTargets: collectAndValidateCoreTargetsFactory({
getStream
}),
buildInviteEmailContents: buildCoreInviteEmailContentsFactory({
getStream
}),
emitEvent: ({ eventName, payload }) =>
getEventBus().emit({
eventName,
payload
}),
getUser,
getServerInfo,
finalizeInvite: buildFinalizeProjectInvite()
}),
getUsers
}),
createStream: createStreamFactory({ db }),
createBranch: createBranchFactory({ db }),
storeProjectRole: storeProjectRoleFactory({ db }),
emitEvent: getEventBus().emit
})
})
const findEmail = findEmailFactory({ db })
const requestNewEmailVerification = requestNewEmailVerificationFactory({
findEmail,
getUser: getUserFactory({ db }),
getServerInfo,
deleteOldAndInsertNewVerification: deleteOldAndInsertNewVerificationFactory({ db }),
renderEmail,
sendEmail
})
const createUser = createUserFactory({
getServerInfo,
findEmail,
storeUser: storeUserFactory({ db }),
countAdminUsers: countAdminUsersFactory({ db }),
storeUserAcl: storeUserAclFactory({ db }),
validateAndCreateUserEmail: validateAndCreateUserEmailFactory({
createUserEmail: createUserEmailFactory({ db }),
ensureNoPrimaryEmailForUser: ensureNoPrimaryEmailForUserFactory({ db }),
findEmail,
updateEmailInvites: finalizeInvitedServerRegistrationFactory({
deleteServerOnlyInvites: deleteServerOnlyInvitesFactory({ db }),
updateAllInviteTargets: updateAllInviteTargetsFactory({ db })
}),
requestNewEmailVerification
}),
emitEvent: getEventBus().emit
})
const getCommitsByUserId = legacyGetPaginatedUserCommitsPage({ db })
const getCommitsByStreamId = legacyGetPaginatedStreamCommitsPageFactory({ db })
const getCommitsTotalCountByBranchName = getBranchCommitsTotalCountByNameFactory({
@@ -274,18 +122,8 @@ const createObject = createObjectFactory({
})
describe('Commits @core-commits', () => {
const user = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
}
const stream = {
name: 'Test Stream References',
description: 'Whatever goes in here usually...',
id: ''
}
let user: BasicTestUser
let stream: BasicTestStream
const testObject = {
foo: 'bar',
@@ -306,17 +144,25 @@ describe('Commits @core-commits', () => {
const generateObject = async (streamId = stream.id, object = testObject) =>
await createObject({ streamId, object })
const generateStream = async (streamBase = stream, ownerId = user.id) =>
await createStream({ ...streamBase, ownerId })
let commitId1: string, commitId2: string, commitId3: string
before(async () => {
await beforeEachContext()
user.id = await createUser(user)
stream.id = await createStream({ ...stream, ownerId: user.id })
user = await createTestUser({
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@example.org',
password: 'sn3aky-1337-b1m',
id: ''
})
stream = await createTestStream(
{
name: 'Test Stream References',
description: 'Whatever goes in here usually...'
},
user
)
const testObjectId = await createObject({ streamId: stream.id, object: testObject })
const testObject2Id = await createObject({
streamId: stream.id,
@@ -503,7 +349,7 @@ describe('Commits @core-commits', () => {
})
it('Should get the commits and their total count from a branch', async () => {
const streamId = await generateStream()
const { id: streamId } = await createTestStream(buildBasicTestProject(), user)
for (let i = 0; i < 10; i++) {
const t = { qux: i, id: '' }
@@ -542,7 +388,7 @@ describe('Commits @core-commits', () => {
})
it('Should get the commits and their total count from a stream', async () => {
const streamId = await generateStream()
const { id: streamId } = await createTestStream(buildBasicTestProject(), user)
await createBranch({
name: 'dim/dev',
streamId,

Some files were not shown because too many files have changed in this diff Show More