Merge main
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"module": "commonJS"
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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:
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ",
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user