feat(fe2 & server): saved views foundation (list & view) + bits n bobs (#5163)

* init db migration

* WIP store view

* create service call

* WIP insertion

* insert sort of works

* moving code arounmd

* creation tests

* avoid duplicate entries

* fixes from main

* basic group retrieval works

* group filtering works

* WIP view listing

* filter by acl

* fixes + WIP single group retrieval

* wip pivot

* more pivot query fixes

* tests fixed after pivot

* views list tests

* fixing test command

* business plan only checks

* more tests for coverage

* .dts import fix

* cli fix

* anutha one

* auth policy tests for business plan access

* WIP saved views panel base

* BE listing adjustments

* WIP group rendering

* group render done

* WIP post create cache updates

* listing fine?

* my vs theirs

* auto open

* minor fixes

* click load omg

* nicely loading views

* type fix

* less spammy loading

* another type fix:

* more lint fix

* test fix

* codecov disable

* moar coverage

* fix sidebar flashin

* more test coverage

* more test cvoverage

* minor adfjustments

* adj

* saved view wipe fixes

* CSR viewer

* more improvements

* extra feature flag checks

* lint fix

* feature flags fix

* more test fixes
This commit is contained in:
Kristaps Fabians Geikins
2025-08-05 11:52:50 +03:00
committed by GitHub
parent 23e9cd31b9
commit a6287fc06d
137 changed files with 8225 additions and 1049 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ orbs:
aliases:
- &docker-node-image
docker:
- image: cimg/node:22.6.0
- image: cimg/node:22.17.1
- &yarn
run:
+1 -11
View File
@@ -5,17 +5,7 @@ codecov:
coverage:
status:
project:
default:
target: 70% #overall project/ repo coverage
server:
target: 70%
flags:
- server
shared:
target: 70%
flags:
- shared
project: off
patch:
default:
target: 90% #overall project/ repo coverage
+1 -1
View File
@@ -6,7 +6,7 @@
"name": "root",
"private": true,
"engines": {
"node": "^22.6.0"
"node": "^22.17.1"
},
"scripts": {
"build": "yarn workspaces foreach --parallel --topological --verbose --worktree run build",
+1 -1
View File
@@ -13,7 +13,7 @@
"url": "git+https://github.com/specklesystems/speckle-server.git"
},
"engines": {
"node": "^22.6.0"
"node": "^22.17.1"
},
"scripts": {
"build:tsc:watch": "tsc -p ./tsconfig.build.json --watch",
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Speckle</title>
<script>
function getCookie(name) {
const value = `; ${document.cookie}`
const parts = value.split(`; ${name}=`)
if (parts.length === 2) return parts.pop().split(';').shift()
}
const theme = getCookie('theme')
// Add 'dark' class to root html node, if dark theme
if (theme === 'dark') {
document.documentElement.classList.add('dark')
}
</script>
<style>
:root {
--speckle-bg: #fafafa;
}
html.dark {
--speckle-bg: #101012;
}
body {
background-color: var(--speckle-bg);
}
</style>
</head>
<body></body>
</html>
@@ -98,6 +98,7 @@ import {
import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices'
import { formatPrice, formatName } from '~/lib/billing/helpers/plan'
import type { SetupContext } from 'vue'
import { useFeatureFlags } from '~/lib/common/composables/env'
const emit = defineEmits<{
(e: 'onUpgradeClick'): void
@@ -120,9 +121,15 @@ const isYearlyIntervalSelected = defineModel<boolean>('isYearlyIntervalSelected'
const slots: SetupContext['slots'] = useSlots()
const { prices } = useWorkspacePlanPrices()
const featureFlags = useFeatureFlags()
const planLimits = computed(
() => WorkspacePlanConfigs({ featureFlags })[props.plan].limits
)
const planFeatures = computed(
() => WorkspacePlanConfigs({ featureFlags })[props.plan].features
)
const planLimits = computed(() => WorkspacePlanConfigs[props.plan].limits)
const planFeatures = computed(() => WorkspacePlanConfigs[props.plan].features)
const commonFeatures = shallowRef([
{
displayName: 'Unlimited members and guests',
@@ -68,6 +68,7 @@ import {
BillingInterval,
WorkspacePlanStatuses
} from '~/lib/common/generated/gql/graphql'
import { useFeatureFlags } from '~/lib/common/composables/env'
graphql(`
fragment WorkspaceBillingPage_Workspace on Workspace {
@@ -91,6 +92,8 @@ graphql(`
const route = useRoute()
const slug = computed(() => (route.params.slug as string) || '')
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
const featureFlags = useFeatureFlags()
const { isFreePlan } = useWorkspacePlan(slug.value)
const { result: workspaceResult } = useQuery(
settingsWorkspaceBillingQuery,
@@ -110,11 +113,12 @@ const showBillingAlert = computed(
workspace.value?.plan?.status === WorkspacePlanStatuses.CancelationScheduled
)
const reachedPlanLimit = computed(() =>
workspaceReachedPlanLimit(
workspace.value?.plan?.name,
workspace.value?.plan?.usage?.projectCount,
workspace.value?.plan?.usage?.modelCount
)
workspaceReachedPlanLimit({
plan: workspace.value?.plan?.name,
projectCount: workspace.value?.plan?.usage?.projectCount,
modelCount: workspace.value?.plan?.usage?.modelCount,
featureFlags
})
)
const showPricingInfo = computed(() => {
if (!workspace.value?.plan?.name) return false
@@ -36,6 +36,7 @@ import type { BillingInterval } from '~/lib/common/generated/gql/graphql'
import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
import { useWorkspaceUsage } from '~/lib/workspaces/composables/usage'
import { useMixpanel } from '~/lib/core/composables/mp'
import { useFeatureFlags } from '~/lib/common/composables/env'
type AddonIncludedSelect = 'yes' | 'no'
@@ -59,6 +60,7 @@ const { hasUnlimitedAddon, plan, subscription, statusIsCanceled, seats } =
useWorkspacePlan(props.slug)
const mixpanel = useMixpanel()
const { projectCount, modelCount } = useWorkspaceUsage(props.slug)
const featureFlags = useFeatureFlags()
const showAddonSelect = ref<boolean>(true)
const isLoading = ref<boolean>(false)
@@ -78,7 +80,7 @@ const title = computed(() => {
})
const usageExceedsNewPlanLimit = computed(() => {
const limits = WorkspacePlanConfigs[props.plan].limits
const limits = WorkspacePlanConfigs({ featureFlags })[props.plan].limits
const modelLimit = limits.modelCount
const projectLimit = limits.projectCount
@@ -32,6 +32,16 @@
<ChatBubbleLeftRightIcon class="h-4 w-4 md:h-5 md:w-5" />
</ViewerControlsButtonToggle>
<!-- Saved views -->
<ViewerControlsButtonToggle
v-if="isSavedViewsEnabled"
v-tippy="getShortcutDisplayText(shortcuts.ToggleSavedViews)"
:active="activePanel === 'savedViews'"
@click="toggleActivePanel('savedViews')"
>
<Camera class="h-4 w-4 md:h-5 md:w-5" />
</ViewerControlsButtonToggle>
<!-- Automation runs -->
<ViewerControlsButtonToggle
v-if="allAutomationRuns.length !== 0"
@@ -192,6 +202,12 @@
<div><ViewerMeasurementsOptions @close="toggleMeasurements" /></div>
</KeepAlive>
</div>
<div v-if="activePanel === 'savedViews'">
<ViewerSavedViewsPanel
v-if="isSavedViewsEnabled"
@close="activePanel = 'none'"
/>
</div>
<div v-show="activePanel === 'models'">
<KeepAlive>
<div>
@@ -287,6 +303,8 @@ import {
} from '@vueuse/core'
import { useFunctionRunsStatusSummary } from '~/lib/automate/composables/runStatus'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
import { Camera } from 'lucide-vue-next'
import { useAreSavedViewsEnabled } from '~/lib/viewer/composables/savedViews/general'
type ActivePanel =
| 'none'
@@ -297,6 +315,7 @@ type ActivePanel =
| 'measurements'
| 'gendo'
| 'mobileOverflow'
| 'savedViews'
type ActiveControl =
| 'none'
@@ -308,6 +327,7 @@ type ActiveControl =
| 'explode'
| 'settings'
const isSavedViewsEnabled = useAreSavedViewsEnabled()
const isGendoEnabled = useIsGendoModuleEnabled()
const { width: windowWidth } = useWindowSize()
@@ -427,6 +447,7 @@ registerShortcuts({
ToggleMeasurements: () => toggleMeasurements(),
ToggleProjection: () => trackAndtoggleProjection(),
ToggleSectionBox: () => toggleSectionBox(),
ToggleSavedViews: () => isSavedViewsEnabled && toggleActivePanel('savedViews'),
ZoomExtentsOrSelection: () => trackAndzoomExtentsOrSelection()
})
@@ -19,7 +19,7 @@
<div
class="flex items-center h-full w-full pr-8 text-body-xs text-foreground font-medium"
>
<span class="truncate">
<span class="truncate grow">
<slot name="title"></slot>
</span>
</div>
@@ -0,0 +1,119 @@
<template>
<ViewerLayoutPanel @close="$emit('close')">
<template #title>
<div class="flex justify-between items-center">
<div>Views</div>
<div class="flex">
<FormButton size="sm" color="subtle" :icon-left="Search" hide-text />
<div v-tippy="canCreateViewOrGroup?.errorMessage">
<FormButton
size="sm"
color="subtle"
:icon-left="FolderPlus"
hide-text
name="addGroup"
:disabled="!canCreateViewOrGroup?.authorized || isLoading"
/>
</div>
<div v-tippy="canCreateViewOrGroup?.errorMessage">
<FormButton
size="sm"
color="subtle"
:icon-left="Plus"
hide-text
name="addView"
:disabled="!canCreateViewOrGroup?.authorized || isLoading"
@click="onAddView"
/>
</div>
</div>
</div>
</template>
<template #actions>
<FormSelectBase
v-model="selectedViewsType"
mount-menu-on-body
label="Views Type"
name="viewsType"
button-style="simple"
:menu-max-width="150"
menu-open-direction="right"
:allow-unset="false"
:items="viewsTypeItems"
>
<template #nothing-selected>Views Type</template>
<template #option="{ item }">
<span>{{ viewsTypeLabels[item] }}</span>
</template>
<template #something-selected="{ value }">
<span v-if="!isArray(value)" class="flex items-center gap-2">
{{ viewsTypeLabels[value] }}
</span>
</template>
</FormSelectBase>
</template>
<div class="text-body-sm">
<ViewerSavedViewsPanelConnectorViews
v-if="selectedViewsType === ViewsType.Connector"
/>
<ViewerSavedViewsPanelViews
v-else
v-model:selected-group-id="selectedGroupId"
:views-type="selectedViewsType"
/>
</div>
</ViewerLayoutPanel>
</template>
<script setup lang="ts">
import { useMutationLoading } from '@vue/apollo-composable'
import { isArray } from 'lodash-es'
import { Search, FolderPlus, Plus } from 'lucide-vue-next'
import { graphql } from '~/lib/common/generated/gql'
import { useCreateSavedView } from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { ViewsType, viewsTypeLabels } from '~/lib/viewer/helpers/savedViews'
graphql(`
fragment ViewerSavedViewsPanel_Project on Project {
id
permissions {
canCreateSavedView {
...FullPermissionCheckResult
}
}
}
`)
defineEmits<{
close: []
}>()
const {
resources: {
response: { project }
}
} = useInjectedViewerState()
const createSavedView = useCreateSavedView()
const isLoading = useMutationLoading()
const selectedViewsType = ref<ViewsType>(ViewsType.All)
const selectedGroupId = ref<string | null>(null)
const viewsTypeItems = computed((): ViewsType[] => [
ViewsType.All,
ViewsType.My,
ViewsType.Connector
])
const canCreateViewOrGroup = computed(
() => project.value?.permissions.canCreateSavedView
)
const onAddView = async () => {
if (isLoading.value) return
const view = await createSavedView({})
if (view) {
// Auto-open the group that the view created to
selectedGroupId.value = view.group.id
}
}
</script>
@@ -0,0 +1,3 @@
<template>
<div>TODO: Connector Views</div>
</template>
@@ -0,0 +1,61 @@
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<div class="flex gap-2 p-2 w-full hover:bg-foundation-2 rounded" :view-id="view.id">
<img
v-keyboard-clickable
:src="view.screenshot"
alt="View screenshot"
class="w-20 h-14 object-cover rounded border border-outline-3 bg-foundation-page cursor-pointer"
@click="apply"
/>
<div class="flex flex-col gap-1 min-w-0">
<div class="text-body-2xs font-medium text-foreground truncate grow-0">
{{ view.name }}
</div>
<div class="text-body-2xs text-foreground-3 truncate">
{{ view.author?.name }}
</div>
<div
v-tippy="formattedFullDate(view.updatedAt)"
class="text-body-2xs text-foreground-3 truncate"
>
{{ formattedRelativeDate(view.updatedAt) }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { graphql } from '~/lib/common/generated/gql'
import type { ViewerSavedViewsPanelView_SavedViewFragment } from '~/lib/common/generated/gql/graphql'
import { useEventBus } from '~/lib/core/composables/eventBus'
import { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus'
graphql(`
fragment ViewerSavedViewsPanelView_SavedView on SavedView {
id
name
description
screenshot
author {
id
name
}
updatedAt
}
`)
const props = defineProps<{
view: ViewerSavedViewsPanelView_SavedViewFragment
}>()
const eventBus = useEventBus()
const apply = async () => {
// Force update, even if the view id is already set
// (in case this is a frustration click w/ the state not applying)
eventBus.emit(ViewerEventBusKeys.UpdateSavedView, {
viewId: props.view.id
})
}
</script>
@@ -0,0 +1,124 @@
<template>
<div v-if="isVeryFirstLoading" class="flex justify-center">
<CommonLoadingIcon class="m-16" />
</div>
<div v-else>
<ViewerSavedViewsPanelViewsEmptyState v-if="!hasGroups" :type="emptyStateType" />
<div v-else class="p-2">
<ViewerSavedViewsPanelViewsGroup
v-for="group in groups"
:key="group.id"
:group="group"
:is-selected="group.id === selectedGroupId"
:only-authored="viewsType === ViewsType.My"
/>
<InfiniteLoading
v-if="groups.length"
:settings="{ identifier }"
hide-when-complete
@infinite="onInfiniteLoad"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { omit } from 'lodash-es'
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
import { graphql } from '~/lib/common/generated/gql'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { ViewsType } from '~/lib/viewer/helpers/savedViews'
graphql(`
fragment ViewerSavedViewsPanelViews_Project on Project {
id
savedViewGroups(input: $savedViewGroupsInput) {
totalCount
cursor
items {
id
...ViewerSavedViewsPanelViewsGroup_SavedViewGroup
}
}
}
`)
const paginableGroupsQuery = graphql(`
query ViewerSavedViewsPanelViews_Groups(
$projectId: String!
$savedViewGroupsInput: SavedViewGroupsInput!
) {
project(id: $projectId) {
id
...ViewerSavedViewsPanelViews_Project
}
}
`)
defineProps<{
viewsType: ViewsType
}>()
const selectedGroupId = defineModel<string | null>('selectedGroupId', {
required: true
})
const {
projectId,
resources: {
request: { resourceIdString }
}
} = useInjectedViewerState()
const search = ref('')
const {
identifier,
onInfiniteLoad,
query: { result },
isVeryFirstLoading
} = usePaginatedQuery({
query: paginableGroupsQuery,
baseVariables: computed(() => ({
projectId: projectId.value,
savedViewGroupsInput: {
limit: 1,
resourceIdString: resourceIdString.value,
cursor: null as null | string,
search: search.value?.trim() || null
}
})),
resolveKey: (vars) => ({
projectId: vars.projectId,
savedViewGroupsInput: omit(vars.savedViewGroupsInput, ['cursor'])
}),
resolveCurrentResult: (res) => res?.project.savedViewGroups,
resolveNextPageVariables: (baseVars, cursor) => ({
...baseVars,
savedViewGroupsInput: {
...baseVars.savedViewGroupsInput,
cursor
}
}),
resolveCursorFromVariables: (vars) => vars.savedViewGroupsInput.cursor
})
const hasGroups = computed(
() => (result.value?.project.savedViewGroups.items.length || 0) > 0
)
const isSearch = computed(() => search.value?.trim().length > 0)
const emptyStateType = computed(() => (isSearch.value ? 'search' : 'base'))
const groups = computed(() => {
return result.value?.project.savedViewGroups.items || []
})
watch(
groups,
(newGroups) => {
if (newGroups.length && !selectedGroupId.value) {
selectedGroupId.value = newGroups[0].id
}
},
{ immediate: true }
)
</script>
@@ -0,0 +1,23 @@
<template>
<div class="flex flex-col gap-8 items-center my-16">
<ViewerSavedViewsPanelViewsEmptyStateImage />
<div class="text-foreground-2">{{ message }}</div>
</div>
</template>
<script setup lang="ts">
const props = withDefaults(
defineProps<{
type?: 'base' | 'search'
}>(),
{
type: 'base'
}
)
const message = computed(() => {
if (props.type === 'search') {
return 'No saved scenes match your search criteria'
}
return 'There are no saved scenes yet'
})
</script>
@@ -0,0 +1,130 @@
<template>
<LayoutDisclosure v-model:open="open" :title="group.title" lazy-load>
<div>
<div v-if="isVeryFirstLoading" class="flex justify-center">
<CommonLoadingIcon class="m-4" />
</div>
<div v-else>
<div
v-if="views.length"
class="flex flex-col gap-3 max-h-64 overflow-y-auto simple-scrollbar"
>
<ViewerSavedViewsPanelView
v-for="view in views"
:key="view.id"
:view="view"
></ViewerSavedViewsPanelView>
</div>
<InfiniteLoading
v-if="views.length"
:settings="{ identifier }"
hide-when-complete
@infinite="onInfiniteLoad"
/>
</div>
</div>
</LayoutDisclosure>
</template>
<script setup lang="ts">
import { omit } from 'lodash-es'
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
import { graphql } from '~/lib/common/generated/gql'
import type { ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment } from '~/lib/common/generated/gql/graphql'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
graphql(`
fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {
id
title
}
`)
graphql(`
fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated on SavedViewGroup {
id
views(input: $savedViewsInput) {
cursor
totalCount
items {
id
...ViewerSavedViewsPanelView_SavedView
}
}
}
`)
const viewsQuery = graphql(`
query ViewerSavedViewsPanelViewsGroup_Views(
$projectId: String!
$groupId: ID!
$savedViewsInput: SavedViewGroupViewsInput!
) {
project(id: $projectId) {
id
savedViewGroup(id: $groupId) {
id
...ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated
}
}
}
`)
const props = defineProps<{
group: ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment
search?: string
onlyAuthored?: boolean
isSelected?: boolean
}>()
const { projectId } = useInjectedViewerState()
const open = ref(false)
const {
identifier,
onInfiniteLoad,
query: { result },
isVeryFirstLoading
} = usePaginatedQuery({
query: viewsQuery,
options: {
enabled: open
},
baseVariables: computed(() => ({
projectId: projectId.value,
groupId: props.group.id,
savedViewsInput: {
limit: 10,
cursor: null as null | string,
search: props.search?.trim() || null,
onlyAuthored: props.onlyAuthored
}
})),
resolveKey: (vars) => ({
projectId: vars.projectId,
groupId: vars.groupId,
savedViewsInput: omit(vars.savedViewsInput, ['cursor'])
}),
resolveCurrentResult: (res) => res?.project.savedViewGroup.views,
resolveNextPageVariables: (baseVars, cursor) => ({
...baseVars,
savedViewsInput: {
...baseVars.savedViewsInput,
cursor
}
}),
resolveCursorFromVariables: (vars) => vars.savedViewsInput.cursor
})
const views = computed(() => result.value?.project.savedViewGroup.views.items || [])
watch(
() => props.isSelected,
(isSelected) => {
if (isSelected) {
open.value = true
}
},
{ immediate: true }
)
</script>
@@ -0,0 +1,163 @@
<template>
<svg
v-if="isLightTheme"
width="194"
height="141"
viewBox="0 0 194 141"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.781312"
y="0.236562"
width="136.25"
height="102.5"
rx="5.5"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 -0.105141 58.9668)"
fill="#F5F5F5"
/>
<rect
x="0.781312"
y="0.236562"
width="136.25"
height="102.5"
rx="5.5"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 -0.105141 58.9668)"
stroke="#C4C4C4"
/>
<g clip-path="url(#clip0_460_265265)">
<path
d="M70.4361 42.9116L88.1415 37.0098C90.1203 36.3504 92.7625 37.1508 94.0431 38.7972C95.6629 40.8795 99.005 41.891 101.508 41.0567L114.418 36.7534C117.415 35.7543 121.418 36.9668 123.358 39.4608L145.637 68.1056C147.577 70.5996 146.719 73.4311 143.722 74.4303L83.9661 94.349C80.9687 95.3479 76.9666 94.1362 75.0268 91.6424L52.7469 62.9967C50.8075 60.5028 51.6647 57.6712 54.6618 56.672L67.5719 52.3687C70.0746 51.5344 70.7911 49.1701 69.1717 47.0877C67.891 45.4412 68.4572 43.5712 70.4361 42.9116Z"
fill="white"
stroke="#C4C4C4"
/>
<circle
cx="19.8295"
cy="19.8295"
r="19.3295"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 67.3618 55.0841)"
fill="#FAFAFA"
stroke="#C4C4C4"
stroke-linecap="round"
/>
<circle
cx="13.6085"
cy="13.6085"
r="13.1085"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 77.0829 58.0275)"
fill="#F5F5F5"
stroke="#C4C4C4"
stroke-linecap="round"
/>
<path
d="M91.8971 58.3619C88.8257 60.0022 87.9419 63.2331 89.7792 66.2916"
stroke="#C4C4C4"
stroke-linecap="round"
/>
</g>
<rect
x="0.781312"
y="0.236562"
width="136.25"
height="102.5"
rx="5.5"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 -0.105141 43.9669)"
stroke="#C4C4C4"
stroke-dasharray="3 4"
/>
<defs>
<clipPath id="clip0_460_265265">
<rect
width="137.25"
height="103.5"
rx="6"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 0 43.67)"
fill="white"
/>
</clipPath>
</defs>
</svg>
<svg
v-else
width="194"
height="141"
viewBox="0 0 194 141"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.781312"
y="0.236562"
width="136.25"
height="102.5"
rx="5.5"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 -0.105141 58.9668)"
fill="#191A22"
/>
<rect
x="0.781312"
y="0.236562"
width="136.25"
height="102.5"
rx="5.5"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 -0.105141 58.9668)"
stroke="#434559"
/>
<g clip-path="url(#clip0_864_35861)">
<path
d="M70.4361 42.9116L88.1415 37.0098C90.1203 36.3504 92.7625 37.1508 94.0431 38.7972C95.6629 40.8795 99.005 41.891 101.508 41.0567L114.418 36.7534C117.415 35.7543 121.418 36.9668 123.358 39.4608L145.637 68.1056C147.577 70.5996 146.719 73.4311 143.722 74.4303L83.9661 94.349C80.9687 95.3479 76.9666 94.1362 75.0268 91.6424L52.7469 62.9967C50.8075 60.5028 51.6647 57.6712 54.6618 56.672L67.5719 52.3687C70.0746 51.5344 70.7911 49.1701 69.1717 47.0877C67.891 45.4412 68.4572 43.5712 70.4361 42.9116Z"
fill="#15161C"
stroke="#434559"
/>
<circle
cx="19.8295"
cy="19.8295"
r="19.3295"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 67.3618 55.0841)"
fill="#101012"
stroke="#434559"
stroke-linecap="round"
/>
<circle
cx="13.6085"
cy="13.6085"
r="13.1085"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 77.0829 58.0275)"
fill="#191A22"
stroke="#434559"
stroke-linecap="round"
/>
<path
d="M91.8971 58.3619C88.8257 60.0022 87.9419 63.2331 89.7792 66.2916"
stroke="#434559"
stroke-linecap="round"
/>
</g>
<rect
x="0.781312"
y="0.236562"
width="136.25"
height="102.5"
rx="5.5"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 -0.105141 43.9669)"
stroke="#434559"
stroke-dasharray="3 4"
/>
<defs>
<clipPath id="clip0_864_35861">
<rect
width="137.25"
height="103.5"
rx="6"
transform="matrix(0.948683 -0.316228 0.613941 0.789352 0 43.67)"
fill="white"
/>
</clipPath>
</defs>
</svg>
</template>
<script setup lang="ts">
import { useTheme } from '~/lib/core/composables/theme'
const { isLightTheme } = useTheme()
</script>
@@ -68,7 +68,6 @@ import {
ArrowTopRightOnSquareIcon
} from '@heroicons/vue/24/solid'
import { FunnelIcon as FunnelIconOutline } from '@heroicons/vue/24/outline'
import { onKeyStroke } from '@vueuse/core'
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
import { getTargetObjectIds } from '~~/lib/object-sidebar/helpers'
@@ -205,36 +204,30 @@ onKeyStroke('Escape', () => {
})
watch(
() => objects.value.length,
(newLength) => {
// Dont open sidebar if a comment is open
if (newLength !== 0 && !focusedThreadId.value) {
sidebarOpen.value = true
} else if (newLength === 0) {
[
() => objects.value.length,
() => focusedThreadId.value,
() => threads.openThread.newThreadEditor.value,
() => isSmallerOrEqualSm.value
],
([objLen, threadId, isNewThreadEditorOpen, isSmSm]) => {
// Close sidebar if a thread is focused
if (threadId) {
sidebarOpen.value = false
return
}
}
)
// Close sidebar when a new thread is being added and screen is smaller than md breakpoint
watch(
() => threads.openThread.newThreadEditor.value,
(isNewThreadEditorOpen) => {
if (isNewThreadEditorOpen && isSmallerOrEqualSm.value) {
// Close sidebar if new thread editor is open and screen is small
if (isNewThreadEditorOpen && isSmSm) {
sidebarOpen.value = false
return
}
}
)
watch(
() => focusedThreadId.value,
(newThreadId) => {
if (newThreadId) {
// If a thread is focused, close the sidebar
sidebarOpen.value = false
} else if (objects.value.length > 0) {
// If no thread is focused and we have objects selected, open the sidebar
// Open sidebar if objects are selected and no thread is focused
if (objLen !== 0 && !threadId) {
sidebarOpen.value = true
} else if (objLen === 0) {
sidebarOpen.value = false
}
}
)
@@ -24,6 +24,7 @@ import type { LayoutDialogButton } from '@speckle/ui-components'
import { settingsWorkspaceRoutes } from '~/lib/common/helpers/route'
import { formatName } from '~/lib/billing/helpers/plan'
import { useMixpanel } from '~/lib/core/composables/mp'
import { useFeatureFlags } from '~/lib/common/composables/env'
const props = defineProps<{
workspaceSlug: string
@@ -39,10 +40,11 @@ const dialogOpen = defineModel<boolean>('open', {
})
const mixpanel = useMixpanel()
const featureFlags = useFeatureFlags()
const planConfig = computed(() => {
if (!props.plan) return null
return WorkspacePlanConfigs[props.plan]
return WorkspacePlanConfigs({ featureFlags })[props.plan]
})
const explorePlansButton: LayoutDialogButton = {
@@ -6,5 +6,6 @@ export const permissionCheckResultFragment = graphql(`
code
message
payload
errorMessage
}
`)
@@ -0,0 +1,4 @@
export const useFeatureFlags = () => {
const config = useRuntimeConfig()
return config.public
}
@@ -165,6 +165,13 @@ type Documents = {
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": typeof types.ViewerModelVersionCardItemFragmentDoc,
"\n fragment ViewerResourcesPersonalLimitAlert_Project on Project {\n id\n ...WorkspaceMoveProject_Project\n }\n": typeof types.ViewerResourcesPersonalLimitAlert_ProjectFragmentDoc,
"\n fragment ViewerResourcesWorkspaceLimitAlert_Workspace on Workspace {\n id\n slug\n }\n": typeof types.ViewerResourcesWorkspaceLimitAlert_WorkspaceFragmentDoc,
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ViewerSavedViewsPanel_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n author {\n id\n name\n }\n updatedAt\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViews_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n": typeof types.ViewerSavedViewsPanelViews_ProjectFragmentDoc,
"\n query ViewerSavedViewsPanelViews_Groups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelViews_Project\n }\n }\n": typeof types.ViewerSavedViewsPanelViews_GroupsDocument,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": typeof types.ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated on SavedViewGroup {\n id\n views(input: $savedViewsInput) {\n cursor\n totalCount\n items {\n id\n ...ViewerSavedViewsPanelView_SavedView\n }\n }\n }\n": typeof types.ViewerSavedViewsPanelViewsGroup_SavedViewGroup_PaginatedFragmentDoc,
"\n query ViewerSavedViewsPanelViewsGroup_Views(\n $projectId: String!\n $groupId: ID!\n $savedViewsInput: SavedViewGroupViewsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroup(id: $groupId) {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated\n }\n }\n }\n": typeof types.ViewerSavedViewsPanelViewsGroup_ViewsDocument,
"\n fragment WorkspaceAddProjectMenu_Workspace on Workspace {\n id\n name\n slug\n role\n plan {\n name\n }\n permissions {\n canCreateProject {\n ...FullPermissionCheckResult\n }\n canMoveProjectToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectsAdd_Workspace\n ...WorkspaceMoveProject_Workspace\n ...UseCanCreateWorkspaceProject_Workspace\n ...UseCanMoveProjectIntoWorkspace_Workspace\n }\n": typeof types.WorkspaceAddProjectMenu_WorkspaceFragmentDoc,
"\n fragment WorkspaceDashboard_Workspace on Workspace {\n ...WorkspaceSidebarMembers_Workspace\n ...WorkspaceDashboardHeader_Workspace\n ...WorkspaceDashboardProjectList_Workspace\n ...BillingActions_Workspace\n id\n name\n role\n creationState {\n completed\n state\n }\n }\n": typeof types.WorkspaceDashboard_WorkspaceFragmentDoc,
"\n fragment WorkspaceDashboardHeader_Workspace on Workspace {\n ...WorkspaceSidebarMembers_Workspace\n ...WorkspaceAddProjectMenu_Workspace\n ...BillingAlert_Workspace\n id\n role\n }\n": typeof types.WorkspaceDashboardHeader_WorkspaceFragmentDoc,
@@ -190,7 +197,7 @@ type Documents = {
"\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n primary\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n ...ProjectsAdd_User\n }\n }\n": typeof types.ActiveUserMainMetadataDocument,
"\n query ActiveUserProjectsToMove($filter: UserProjectsFilter) {\n activeUser {\n id\n projects(filter: $filter) {\n totalCount\n }\n }\n }\n": typeof types.ActiveUserProjectsToMoveDocument,
"\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n": typeof types.FullPermissionCheckResultFragmentDoc,
"\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n errorMessage\n }\n": typeof types.FullPermissionCheckResultFragmentDoc,
"\n mutation FinishOnboarding($input: OnboardingCompletionInput) {\n activeUserMutations {\n finishOnboarding(input: $input)\n }\n }\n": typeof types.FinishOnboardingDocument,
"\n mutation RequestVerificationByEmail($email: String!) {\n requestVerificationByEmail(email: $email)\n }\n": typeof types.RequestVerificationByEmailDocument,
"\n query AuthLoginPanel {\n serverInfo {\n authStrategies {\n id\n }\n ...AuthStategiesServerInfoFragment\n }\n }\n": typeof types.AuthLoginPanelDocument,
@@ -394,6 +401,8 @@ type Documents = {
"\n fragment ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\n }\n": typeof types.ViewerCommentBubblesDataFragmentDoc,
"\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseCheckViewerCommentingAccess_ProjectFragmentDoc,
"\n fragment UseLoadLatestVersion_Project on Project {\n id\n workspace {\n slug\n }\n }\n": typeof types.UseLoadLatestVersion_ProjectFragmentDoc,
"\n mutation CreateSavedView($input: CreateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n createView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n": typeof types.CreateSavedViewDocument,
"\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,
"\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": typeof types.BroadcastViewerUserActivityDocument,
@@ -401,8 +410,9 @@ type Documents = {
"\n mutation CreateCommentThread($input: CreateCommentInput!) {\n commentMutations {\n create(input: $input) {\n ...ViewerCommentThread\n }\n }\n }\n": typeof types.CreateCommentThreadDocument,
"\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": typeof types.CreateCommentReplyDocument,
"\n mutation ArchiveComment($input: ArchiveCommentInput!) {\n commentMutations {\n archive(input: $input)\n }\n }\n": typeof types.ArchiveCommentDocument,
"\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": typeof types.ProjectViewerResourcesDocument,
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerGendoPanel_Project\n ...ViewerResourcesLimitAlert_Project\n }\n }\n": typeof types.ViewerLoadedResourcesDocument,
"\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: String\n ) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": typeof types.ProjectViewerResourcesDocument,
"\n query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {\n project(id: $projectId) {\n id\n savedView(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n": typeof types.ViewerActiveSavedViewDocument,
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerGendoPanel_Project\n ...ViewerResourcesLimitAlert_Project\n ...ViewerSavedViewsPanel_Project\n }\n }\n": typeof types.ViewerLoadedResourcesDocument,
"\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": typeof types.ViewerModelVersionsDocument,
"\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": typeof types.ViewerDiffVersionsDocument,
"\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": typeof types.ViewerLoadedThreadsDocument,
@@ -641,6 +651,13 @@ const documents: Documents = {
"\n fragment ViewerModelVersionCardItem on Version {\n id\n message\n referencedObject\n sourceApplication\n createdAt\n previewUrl\n authorUser {\n ...LimitedUserAvatar\n }\n }\n": types.ViewerModelVersionCardItemFragmentDoc,
"\n fragment ViewerResourcesPersonalLimitAlert_Project on Project {\n id\n ...WorkspaceMoveProject_Project\n }\n": types.ViewerResourcesPersonalLimitAlert_ProjectFragmentDoc,
"\n fragment ViewerResourcesWorkspaceLimitAlert_Workspace on Workspace {\n id\n slug\n }\n": types.ViewerResourcesWorkspaceLimitAlert_WorkspaceFragmentDoc,
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ViewerSavedViewsPanel_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n author {\n id\n name\n }\n updatedAt\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViews_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n": types.ViewerSavedViewsPanelViews_ProjectFragmentDoc,
"\n query ViewerSavedViewsPanelViews_Groups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelViews_Project\n }\n }\n": types.ViewerSavedViewsPanelViews_GroupsDocument,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": types.ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated on SavedViewGroup {\n id\n views(input: $savedViewsInput) {\n cursor\n totalCount\n items {\n id\n ...ViewerSavedViewsPanelView_SavedView\n }\n }\n }\n": types.ViewerSavedViewsPanelViewsGroup_SavedViewGroup_PaginatedFragmentDoc,
"\n query ViewerSavedViewsPanelViewsGroup_Views(\n $projectId: String!\n $groupId: ID!\n $savedViewsInput: SavedViewGroupViewsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroup(id: $groupId) {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated\n }\n }\n }\n": types.ViewerSavedViewsPanelViewsGroup_ViewsDocument,
"\n fragment WorkspaceAddProjectMenu_Workspace on Workspace {\n id\n name\n slug\n role\n plan {\n name\n }\n permissions {\n canCreateProject {\n ...FullPermissionCheckResult\n }\n canMoveProjectToWorkspace {\n ...FullPermissionCheckResult\n }\n }\n ...ProjectsAdd_Workspace\n ...WorkspaceMoveProject_Workspace\n ...UseCanCreateWorkspaceProject_Workspace\n ...UseCanMoveProjectIntoWorkspace_Workspace\n }\n": types.WorkspaceAddProjectMenu_WorkspaceFragmentDoc,
"\n fragment WorkspaceDashboard_Workspace on Workspace {\n ...WorkspaceSidebarMembers_Workspace\n ...WorkspaceDashboardHeader_Workspace\n ...WorkspaceDashboardProjectList_Workspace\n ...BillingActions_Workspace\n id\n name\n role\n creationState {\n completed\n state\n }\n }\n": types.WorkspaceDashboard_WorkspaceFragmentDoc,
"\n fragment WorkspaceDashboardHeader_Workspace on Workspace {\n ...WorkspaceSidebarMembers_Workspace\n ...WorkspaceAddProjectMenu_Workspace\n ...BillingAlert_Workspace\n id\n role\n }\n": types.WorkspaceDashboardHeader_WorkspaceFragmentDoc,
@@ -666,7 +683,7 @@ const documents: Documents = {
"\n fragment WorkspaceWizardStepRegion_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.WorkspaceWizardStepRegion_ServerInfoFragmentDoc,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n emails {\n id\n email\n verified\n primary\n }\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n ...ProjectsAdd_User\n }\n }\n": types.ActiveUserMainMetadataDocument,
"\n query ActiveUserProjectsToMove($filter: UserProjectsFilter) {\n activeUser {\n id\n projects(filter: $filter) {\n totalCount\n }\n }\n }\n": types.ActiveUserProjectsToMoveDocument,
"\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n": types.FullPermissionCheckResultFragmentDoc,
"\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n errorMessage\n }\n": types.FullPermissionCheckResultFragmentDoc,
"\n mutation FinishOnboarding($input: OnboardingCompletionInput) {\n activeUserMutations {\n finishOnboarding(input: $input)\n }\n }\n": types.FinishOnboardingDocument,
"\n mutation RequestVerificationByEmail($email: String!) {\n requestVerificationByEmail(email: $email)\n }\n": types.RequestVerificationByEmailDocument,
"\n query AuthLoginPanel {\n serverInfo {\n authStrategies {\n id\n }\n ...AuthStategiesServerInfoFragment\n }\n }\n": types.AuthLoginPanelDocument,
@@ -870,6 +887,8 @@ const documents: Documents = {
"\n fragment ViewerCommentBubblesData on Comment {\n id\n viewedAt\n viewerState\n }\n": types.ViewerCommentBubblesDataFragmentDoc,
"\n fragment UseCheckViewerCommentingAccess_Project on Project {\n id\n permissions {\n canCreateComment {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseCheckViewerCommentingAccess_ProjectFragmentDoc,
"\n fragment UseLoadLatestVersion_Project on Project {\n id\n workspace {\n slug\n }\n }\n": types.UseLoadLatestVersion_ProjectFragmentDoc,
"\n mutation CreateSavedView($input: CreateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n createView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n": types.CreateSavedViewDocument,
"\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,
"\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": types.BroadcastViewerUserActivityDocument,
@@ -877,8 +896,9 @@ const documents: Documents = {
"\n mutation CreateCommentThread($input: CreateCommentInput!) {\n commentMutations {\n create(input: $input) {\n ...ViewerCommentThread\n }\n }\n }\n": types.CreateCommentThreadDocument,
"\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": types.CreateCommentReplyDocument,
"\n mutation ArchiveComment($input: ArchiveCommentInput!) {\n commentMutations {\n archive(input: $input)\n }\n }\n": types.ArchiveCommentDocument,
"\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": types.ProjectViewerResourcesDocument,
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerGendoPanel_Project\n ...ViewerResourcesLimitAlert_Project\n }\n }\n": types.ViewerLoadedResourcesDocument,
"\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: String\n ) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": types.ProjectViewerResourcesDocument,
"\n query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {\n project(id: $projectId) {\n id\n savedView(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n": types.ViewerActiveSavedViewDocument,
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerGendoPanel_Project\n ...ViewerResourcesLimitAlert_Project\n ...ViewerSavedViewsPanel_Project\n }\n }\n": types.ViewerLoadedResourcesDocument,
"\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": types.ViewerModelVersionsDocument,
"\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": types.ViewerDiffVersionsDocument,
"\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": types.ViewerLoadedThreadsDocument,
@@ -1584,6 +1604,34 @@ export function graphql(source: "\n fragment ViewerResourcesPersonalLimitAlert_
* 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 ViewerResourcesWorkspaceLimitAlert_Workspace on Workspace {\n id\n slug\n }\n"): (typeof documents)["\n fragment ViewerResourcesWorkspaceLimitAlert_Workspace on 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 ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\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 ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n author {\n id\n name\n }\n updatedAt\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n author {\n id\n name\n }\n updatedAt\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 ViewerSavedViewsPanelViews_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViews_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\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 ViewerSavedViewsPanelViews_Groups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelViews_Project\n }\n }\n"): (typeof documents)["\n query ViewerSavedViewsPanelViews_Groups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelViews_Project\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 ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n title\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 ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated on SavedViewGroup {\n id\n views(input: $savedViewsInput) {\n cursor\n totalCount\n items {\n id\n ...ViewerSavedViewsPanelView_SavedView\n }\n }\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated on SavedViewGroup {\n id\n views(input: $savedViewsInput) {\n cursor\n totalCount\n items {\n id\n ...ViewerSavedViewsPanelView_SavedView\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 ViewerSavedViewsPanelViewsGroup_Views(\n $projectId: String!\n $groupId: ID!\n $savedViewsInput: SavedViewGroupViewsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroup(id: $groupId) {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated\n }\n }\n }\n"): (typeof documents)["\n query ViewerSavedViewsPanelViewsGroup_Views(\n $projectId: String!\n $groupId: ID!\n $savedViewsInput: SavedViewGroupViewsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroup(id: $groupId) {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup_Paginated\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1687,7 +1735,7 @@ export function graphql(source: "\n query ActiveUserProjectsToMove($filter: Use
/**
* 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 FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n"): (typeof documents)["\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n }\n"];
export function graphql(source: "\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n errorMessage\n }\n"): (typeof documents)["\n fragment FullPermissionCheckResult on PermissionCheckResult {\n authorized\n code\n message\n payload\n errorMessage\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2500,6 +2548,14 @@ export function graphql(source: "\n fragment UseCheckViewerCommentingAccess_Pro
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment UseLoadLatestVersion_Project on Project {\n id\n workspace {\n slug\n }\n }\n"): (typeof documents)["\n fragment UseLoadLatestVersion_Project on Project {\n id\n workspace {\n slug\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 CreateSavedView($input: CreateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n createView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n"): (typeof documents)["\n mutation CreateSavedView($input: CreateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n createView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n"): (typeof documents)["\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2531,11 +2587,15 @@ export function graphql(source: "\n mutation ArchiveComment($input: ArchiveComm
/**
* 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 ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n"];
export function graphql(source: "\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: String\n ) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: String\n ) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {\n identifier\n items {\n modelId\n versionId\n objectId\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 ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerGendoPanel_Project\n ...ViewerResourcesLimitAlert_Project\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerGendoPanel_Project\n ...ViewerResourcesLimitAlert_Project\n }\n }\n"];
export function graphql(source: "\n query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {\n project(id: $projectId) {\n id\n savedView(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n"): (typeof documents)["\n query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {\n project(id: $projectId) {\n id\n savedView(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\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 ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerGendoPanel_Project\n ...ViewerResourcesLimitAlert_Project\n ...ViewerSavedViewsPanel_Project\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerGendoPanel_Project\n ...ViewerResourcesLimitAlert_Project\n ...ViewerSavedViewsPanel_Project\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
@@ -380,8 +380,7 @@ export function modifyObjectFields<
debug: boolean
}>
) {
const { fieldNameWhitelist, debug = !!(import.meta.dev && import.meta.client) } =
options || {}
const { fieldNameWhitelist, debug = false } = options || {}
const logger = useLogger()
const invocationId = nanoid()
@@ -0,0 +1,15 @@
import { RelativeURL, type MaybeNullOrUndefined } from '@speckle/shared'
import type { RouteLocationNormalized } from 'vue-router'
export const checkIfIsInPlaceNavigation = (
to?: MaybeNullOrUndefined<RouteLocationNormalized>,
from?: MaybeNullOrUndefined<RouteLocationNormalized>
): boolean => {
if (!to || !from) return false
// if only hash state or querystring changed, its not a full on navigation to a new page
const toUrl = new RelativeURL(to.fullPath)
const fromUrl = new RelativeURL(from.fullPath)
return toUrl.pathOnly === fromUrl.pathOnly
}
@@ -1,13 +1,48 @@
export enum EventBusKeys {
import type {
ViewerEventBusKeyPayloadMap,
ViewerEventBusKeys
} from '~/lib/viewer/helpers/eventBus'
export enum CoreEventBusKeys {
TestKey = 'test_event_bus'
}
export type EventBusKeys = CoreEventBusKeys | ViewerEventBusKeys
// Add mappings between event keys and expected payloads here
export type EventBusKeyPayloadMap = {
[EventBusKeys.TestKey]: { foo: string; bar: string }
} & { [k in EventBusKeys]: unknown } & Record<string, unknown>
[CoreEventBusKeys.TestKey]: { foo: string; bar: string }
} & ViewerEventBusKeyPayloadMap & { [k in EventBusKeys]: unknown } & Record<
string,
unknown
>
export function useEventBus() {
const nuxt = useNuxtApp()
return nuxt.$eventBus
const $eventBus = nuxt.$eventBus
const handles = shallowRef<Array<() => void>>([])
const on = <T extends EventBusKeys>(
key: T,
handler: (event: EventBusKeyPayloadMap[T]) => void
) => {
$eventBus.on(key, handler)
const offHandle = () => $eventBus.off(key, handler)
handles.value = [...handles.value, offHandle]
return offHandle
}
onUnmounted(() => {
handles.value.forEach((quit) => quit())
handles.value = []
})
return {
/**
* Event subscribe w/ automatic cleanup on unmount.
* Returns a function to manually unsubscribe if needed.
*/
on,
emit: $eventBus.emit
}
}
@@ -220,6 +220,21 @@ function createCache(): InMemoryCache {
},
permissions: {
merge: mergeAsObjectsFunction
},
savedViewGroups: {
keyArgs: ['input', ['limit', 'search', 'onlyAuthored', 'resourceIdString']],
merge: buildAbstractCollectionMergeFunction('SavedViewGroupCollection')
}
}
},
SavedViewGroup: {
fields: {
views: {
keyArgs: [
'input',
['limit', 'search', 'sortBy', 'sortDirection', 'onlyAuthored']
],
merge: buildAbstractCollectionMergeFunction('SavedViewCollection')
}
}
},
@@ -300,6 +315,9 @@ function createCache(): InMemoryCache {
ServerInfo: {
merge: true
},
ServerConfiguration: {
merge: true
},
CommentThreadActivityMessage: {
merge: true
},
@@ -13,7 +13,7 @@ import {
useSelectionEvents,
useViewerCameraControlEndTracker
} from '~~/lib/viewer/composables/viewer'
import { SpeckleViewer, xor, TIME_MS } from '@speckle/shared'
import { xor, TIME_MS } from '@speckle/shared'
import type { Nullable, Optional } from '@speckle/shared'
import { Vector3 } from 'three'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
@@ -35,8 +35,13 @@ import {
useApplySerializedState,
useStateSerialization
} from '~~/lib/viewer/composables/serialization'
import type { Merge } from 'type-fest'
import type { Merge, OverrideProperties } from 'type-fest'
import { graphql } from '~/lib/common/generated/gql'
import {
isSerializedViewerState,
type SerializedViewerState
} from '@speckle/shared/viewer/state'
import { omit } from 'lodash-es'
/**
* How often we send out an "activity" message even if user hasn't made any clicks (just to keep him active)
@@ -55,11 +60,16 @@ const USER_STALE_AFTER_PERIOD = 20 * OWN_ACTIVITY_UPDATE_INTERVAL
*/
const USER_REMOVABLE_AFTER_PERIOD = USER_STALE_AFTER_PERIOD * 2
type ViewerActivityMetadata = OverrideProperties<
Required<Omit<ViewerUserActivityMessageInput, 'status'>>,
{ state: SerializedViewerState }
>
function useCollectMainMetadata() {
const { sessionId } = useInjectedViewerState()
const { activeUser } = useActiveUser()
const { serialize } = useStateSerialization()
return (): Omit<ViewerUserActivityMessageInput, 'status' | 'selection'> => ({
return (): ViewerActivityMetadata => ({
userId: activeUser.value?.id || null,
userName: activeUser.value?.name || 'Anonymous Viewer',
state: serialize(),
@@ -78,6 +88,63 @@ graphql(`
}
`)
const useViewerRealtimeActivityState = () =>
useState('viewer_realtime_activity_state', () => ({
activity: undefined as Optional<ViewerActivityMetadata>,
status: ViewerUserActivityStatus.Viewing as ViewerUserActivityStatus
}))
export const useViewerRealtimeActivityTracker = () => {
const state = useViewerRealtimeActivityState()
const getMainMetadata = useCollectMainMetadata()
const activity = computed({
get: () => state.value.activity || getMainMetadata(),
set: (value) => {
state.value.activity = value
}
})
const status = computed({
get: () => state.value.status,
set: (value) => {
state.value.status = value
}
})
const serializedState = computed(() => activity.value.state)
// Ids for easy equality comparisons
const serializedStateId = computed(() => JSON.stringify(serializedState.value))
const activityId = computed(() => {
const stateId = serializedStateId.value
const otherActivity: Omit<ViewerActivityMetadata, 'state'> = omit(activity.value, [
'state'
])
const otherActivityId = JSON.stringify(otherActivity)
return `${stateId}-${otherActivityId}-${status.value}`
})
const update = (params?: {
newActivity?: ViewerActivityMetadata
status?: ViewerUserActivityStatus
}) => {
activity.value = params?.newActivity || getMainMetadata()
if (params?.status) {
status.value = params.status
}
}
onUnmounted(() => {
// Reset activity state on unmount
state.value.activity = undefined
state.value.status = ViewerUserActivityStatus.Viewing
})
return { activity, serializedState, status, update, serializedStateId, activityId }
}
export function useViewerUserActivityBroadcasting(
options?: Partial<{
state: InjectableViewerState
@@ -90,7 +157,7 @@ export function useViewerUserActivityBroadcasting(
response: { project }
}
} = options?.state || useInjectedViewerState()
const getMainMetadata = useCollectMainMetadata()
const { update, activity, status, activityId } = useViewerRealtimeActivityTracker()
const apollo = useApolloClient().client
const { isEnabled: isEmbedEnabled } = useEmbed()
@@ -98,22 +165,25 @@ export function useViewerUserActivityBroadcasting(
() => project.value?.permissions.canBroadcastActivity.authorized
)
const isSameMessage = (
previousSerializedMessage: Optional<string>,
newMessage: ViewerUserActivityMessageInput
const isSameActivity = (
previousActivityId: Optional<string>,
newActivityId: string
) => {
if (xor(previousSerializedMessage, newMessage)) return false
if (!previousSerializedMessage && !newMessage) return false
return previousSerializedMessage === JSON.stringify(newMessage)
if (xor(previousActivityId, newActivityId)) return false
if (!previousActivityId && !newActivityId) return false
return previousActivityId === newActivityId
}
const invokeMutation = async (message: ViewerUserActivityMessageInput) => {
const invokeMutation = async () => {
const result = await apollo
.mutate({
mutation: broadcastViewerUserActivityMutation,
variables: {
resourceIdString: resourceIdString.value,
message,
message: {
...activity.value,
status: status.value
},
projectId: projectId.value
}
})
@@ -122,43 +192,43 @@ export function useViewerUserActivityBroadcasting(
return result.data?.broadcastViewerUserActivity || false
}
let serializedPreviousMessage: Optional<string> = undefined
const invokeObservabilityEvent = async (message: ViewerUserActivityMessageInput) => {
let previousActivityId: Optional<string> = undefined
const invokeObservabilityEvent = async () => {
const dd = window.DD_RUM
if (!dd || !('addAction' in dd)) return
if (isSameMessage(serializedPreviousMessage, message)) return
const message = {
...activity.value,
status: status.value
}
serializedPreviousMessage = JSON.stringify(message)
if (isSameActivity(previousActivityId, activityId.value)) return
previousActivityId = activityId.value
dd.addAction('Viewer User Activity', { message })
}
const invoke = async (message: ViewerUserActivityMessageInput) => {
const invoke = async () => {
if (!canBroadcast.value || isEmbedEnabled.value) return false
return await Promise.all([
invokeMutation(message),
invokeObservabilityEvent(message)
])
return await Promise.all([invokeMutation(), invokeObservabilityEvent()])
}
return {
emitDisconnected: async () =>
await invoke({
...getMainMetadata(),
status: ViewerUserActivityStatus.Disconnected
}),
emitDisconnected: async () => {
update({ status: ViewerUserActivityStatus.Disconnected })
await invoke()
},
emitViewing: async () => {
await invoke({
...getMainMetadata(),
status: ViewerUserActivityStatus.Viewing
})
update({ status: ViewerUserActivityStatus.Viewing })
await invoke()
}
}
}
export type UserActivityModel = Merge<
OnViewerUserActivityBroadcastedSubscription['viewerUserActivityBroadcasted'],
{ state: SpeckleViewer.ViewerState.SerializedViewerState }
{ state: SerializedViewerState }
> & {
isStale: boolean
isOccluded: boolean
@@ -239,9 +309,7 @@ export function useViewerUserActivityTracking(params: {
return
}
const state = SpeckleViewer.ViewerState.isSerializedViewerState(event.state)
? event.state
: null
const state = isSerializedViewerState(event.state) ? event.state : null
if (!state) return
const userData: UserActivityModel = {
@@ -429,7 +497,7 @@ function useViewerSpotlightTracking() {
type UserTypingInfo = {
userId: string
userName: string
thread: SpeckleViewer.ViewerState.SerializedViewerState['ui']['threads']['openThread']
thread: SerializedViewerState['ui']['threads']['openThread']
lastSeen: Dayjs
}
@@ -471,9 +539,7 @@ export function useViewerThreadTypingTracking(threadId: MaybeRef<string>) {
usersTyping.value.splice(existingItemIdx, 1)
}
const state = SpeckleViewer.ViewerState.isSerializedViewerState(event.state)
? event.state
: null
const state = isSerializedViewerState(event.state) ? event.state : null
if (!state) return
const typingPayload = state.ui.threads.openThread
if (typingPayload.threadId !== unref(threadId)) {
@@ -0,0 +1,7 @@
export const useAreSavedViewsEnabled = () => {
const {
public: { FF_SAVED_VIEWS_ENABLED }
} = useRuntimeConfig()
return FF_SAVED_VIEWS_ENABLED
}
@@ -0,0 +1,114 @@
import { useMutation } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import type { CreateSavedViewInput } from '~/lib/common/generated/gql/graphql'
import { useStateSerialization } from '~/lib/viewer/composables/serialization'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
const createSavedViewMutation = graphql(`
mutation CreateSavedView($input: CreateSavedViewInput!) {
projectMutations {
savedViewMutations {
createView(input: $input) {
id
...ViewerSavedViewsPanelView_SavedView
group {
id
...ViewerSavedViewsPanelViewsGroup_SavedViewGroup
}
}
}
}
}
`)
export const useCreateSavedView = () => {
const { mutate } = useMutation(createSavedViewMutation)
const { userId } = useActiveUser()
const {
projectId,
viewer: { instance: viewerInstance }
} = useInjectedViewerState()
const { serialize, buildConcreteResourceIdString } = useStateSerialization()
const { triggerNotification } = useGlobalToast()
return async (
input: Omit<
CreateSavedViewInput,
'projectId' | 'resourceIdString' | 'viewerState' | 'screenshot'
>
) => {
if (!userId.value) return
const screenshot = await viewerInstance.screenshot()
const result = await mutate(
{
input: {
...input,
projectId: projectId.value,
resourceIdString: buildConcreteResourceIdString(),
viewerState: serialize({ concreteResourceIdString: true }),
screenshot
}
},
{
update: (cache, { data }) => {
const res = data?.projectMutations.savedViewMutations.createView
if (!res) return
const viewId = res.id
const groupId = res.group.id
// Project.savedViewGroups + 1, if it is a new group
modifyObjectField(
cache,
getCacheId('Project', projectId.value),
'savedViewGroups',
({ helpers: { createUpdatedValue, ref, readField }, value }) => {
const isNewGroup = !value?.items?.some(
(group) => readField(group, 'id') === groupId
)
if (!isNewGroup) return
return createUpdatedValue(({ update }) => {
update('totalCount', (count) => count + 1)
update('items', (items) => [...items, ref('SavedViewGroup', groupId)])
})
},
{ autoEvictFiltered: true }
)
// SavedViewGroup.views + 1
modifyObjectField(
cache,
getCacheId('SavedViewGroup', groupId),
'views',
({ helpers: { createUpdatedValue, ref } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (count) => count + 1)
update('items', (items) => [ref('SavedView', viewId), ...items])
})
},
{ autoEvictFiltered: true }
)
}
}
).catch(convertThrowIntoFetchResult)
const res = result?.data?.projectMutations.savedViewMutations.createView
if (res?.id) {
triggerNotification({
title: 'Saved View Created',
type: ToastNotificationType.Success
})
} else {
const err = getFirstGqlErrorMessage(result?.errors)
triggerNotification({
title: "Couldn't create saved view",
description: err,
type: ToastNotificationType.Danger
})
}
return res
}
}
@@ -14,6 +14,7 @@ import { CameraController, ViewMode, VisualDiffMode } from '@speckle/viewer'
import type { NumericPropertyInfo } from '@speckle/viewer'
import type { PartialDeep } from 'type-fest'
import type { SectionBoxData } from '@speckle/shared/viewer/state'
import { useViewerRealtimeActivityTracker } from '~/lib/viewer/composables/activity'
type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState
@@ -125,7 +126,7 @@ export function useStateSerialization() {
return ret
}
return { serialize }
return { serialize, buildConcreteResourceIdString }
}
export enum StateApplyMode {
@@ -133,7 +134,8 @@ export enum StateApplyMode {
ThreadOpen,
ThreadFullContextOpen,
Reset,
FederatedContext
FederatedContext,
SavedView
}
export function useApplySerializedState() {
@@ -167,10 +169,12 @@ export function useApplySerializedState() {
const { diffModelVersions, deserializeDiffCommand, endDiff } = useDiffUtilities()
const { setSelectionFromObjectIds } = useSelectionUtilities()
const logger = useLogger()
const { update } = useViewerRealtimeActivityTracker()
return async (state: PartialDeep<SerializedViewerState>, mode: StateApplyMode) => {
if (mode === StateApplyMode.Reset) {
resetState()
update() // Trigger activity update
return
}
@@ -251,15 +255,6 @@ export function useApplySerializedState() {
})
}
const selectedObjectIds = Object.keys(filters.selectedObjectApplicationIds ?? {})
if (mode === StateApplyMode.Spotlight) {
highlightedObjectIds.value = selectedObjectIds
} else {
if (selectedObjectIds.length) {
setSelectionFromObjectIds(selectedObjectIds)
}
}
// Handle resource string updates
if (
[StateApplyMode.Spotlight, StateApplyMode.ThreadFullContextOpen].includes(mode)
@@ -288,12 +283,21 @@ export function useApplySerializedState() {
}
}
if ([StateApplyMode.Spotlight].includes(mode)) {
if ([StateApplyMode.Spotlight, StateApplyMode.SavedView].includes(mode)) {
await urlHashState.focusedThreadId.update(
state.ui?.threads?.openThread?.threadId || null
)
}
const selectedObjectIds = Object.keys(filters.selectedObjectApplicationIds ?? {})
if (mode === StateApplyMode.Spotlight) {
highlightedObjectIds.value = selectedObjectIds
} else {
if (selectedObjectIds.length || mode === StateApplyMode.SavedView) {
setSelectionFromObjectIds(selectedObjectIds)
}
}
const command = state.ui?.diff?.command
? deserializeDiffCommand(state.ui.diff.command)
: null
@@ -324,5 +328,8 @@ export function useApplySerializedState() {
...lightConfig.value,
...(state.ui?.lightConfig || {})
}
// Trigger activity update
update()
}
}
@@ -22,10 +22,11 @@ import { inject, ref, provide } from 'vue'
import type { ComputedRef, WritableComputedRef, Raw, Ref, ShallowRef } from 'vue'
import { useScopedState } from '~~/lib/common/composables/scopedState'
import type { MaybeNullOrUndefined, Nullable, Optional } from '@speckle/shared'
import { SpeckleViewer, isNonNullable } from '@speckle/shared'
import { isNonNullable } from '@speckle/shared'
import { useApolloClient, useLazyQuery, useQuery } from '@vue/apollo-composable'
import {
projectViewerResourcesQuery,
viewerActiveSavedViewQuery,
viewerLoadedResourcesQuery,
viewerLoadedThreadsQuery,
viewerModelVersionsQuery
@@ -38,7 +39,8 @@ import type {
ViewerResourceItem,
ViewerLoadedThreadsQueryVariables,
ProjectCommentsFilter,
ViewerModelVersionCardItemFragment
ViewerModelVersionCardItemFragment,
UseViewerSavedViewSetup_SavedViewFragment
} from '~~/lib/common/generated/gql/graphql'
import type { SetNonNullable, Get } from 'type-fest'
import {
@@ -66,6 +68,18 @@ import { useSynchronizedCookie } from '~~/lib/common/composables/reactiveCookie'
import { buildManualPromise } from '@speckle/ui-components'
import { PassReader } from '../extensions/PassReader'
import type { SectionBoxData } from '@speckle/shared/viewer/state'
import {
createGetParamFromResources,
isAllModelsResource,
isModelFolderResource,
isModelResource,
isObjectResource,
parseUrlParameters,
resourceBuilder,
ViewerModelResource,
type ViewerResource
} from '@speckle/shared/viewer/route'
import { useAreSavedViewsEnabled } from '~/lib/viewer/composables/savedViews/general'
export type LoadedModel = NonNullable<
Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>
@@ -75,6 +89,8 @@ export type LoadedThreadsMetadata = NonNullable<
Get<ViewerLoadedThreadsQuery, 'project.commentThreads'>
>
export type LoadedSavedView = UseViewerSavedViewSetup_SavedViewFragment
export type LoadedCommentThread = NonNullable<Get<LoadedThreadsMetadata, 'items[0]'>>
export type InjectableViewerState = Readonly<{
@@ -82,6 +98,11 @@ export type InjectableViewerState = Readonly<{
* The project which we're opening in the viewer (all loaded models should belong to it)
*/
projectId: AsyncWritableComputedRef<string>
/**
* Core source of truth for the view id (other is in hash state). This allows you to
* set a view to load, without it showing up in the URL.
*/
savedViewId: Ref<Nullable<string>>
/**
* User viewer session ID. The same user will have different IDs in different tabs if multiple are open.
* This is used to ignore user activity messages from the same tab.
@@ -136,7 +157,7 @@ export type InjectableViewerState = Readonly<{
* All currently requested identifiers. You
* can write to this to change which resources should be loaded.
*/
items: AsyncWritableComputedRef<SpeckleViewer.ViewerRoute.ViewerResource[]>
items: AsyncWritableComputedRef<ViewerResource[]>
/**
* All currently requested identifiers in a comma-delimited string, the way it's
* represented in the URL. Is writable also.
@@ -161,6 +182,10 @@ export type InjectableViewerState = Readonly<{
* are resolved from multiple GQL requests and update whenever resources.request updates.
*/
response: {
/**
* Resource id string w/ saved view applied, if any
*/
resolvedResourceIdString: ComputedRef<string>
/**
* Metadata about loaded items
*/
@@ -218,6 +243,10 @@ export type InjectableViewerState = Readonly<{
*/
loadMoreVersions: (modelId: string) => Promise<void>
resourcesLoading: ComputedRef<boolean>
/**
* Loaded saved view, if any
*/
savedView: ComputedRef<Optional<LoadedSavedView>>
}
}
/**
@@ -290,6 +319,7 @@ export type InjectableViewerState = Readonly<{
urlHashState: {
focusedThreadId: AsyncWritableComputedRef<Nullable<string>>
diff: AsyncWritableComputedRef<Nullable<DiffStateCommand>>
savedViewId: AsyncWritableComputedRef<Nullable<string>>
}
}>
@@ -302,7 +332,7 @@ type CachedViewerState = Pick<
type InitialSetupState = Pick<
InjectableViewerState,
'projectId' | 'viewer' | 'sessionId' | 'urlHashState'
'projectId' | 'viewer' | 'sessionId' | 'urlHashState' | 'savedViewId'
>
type InitialStateWithRequest = InitialSetupState & {
@@ -410,6 +440,7 @@ function setupInitialState(params: UseSetupViewerParams): InitialSetupState {
return {
projectId: params.projectId,
savedViewId: ref<string | null>(null),
sessionId,
viewer: import.meta.server
? ({
@@ -450,10 +481,9 @@ function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest
const getParam = computed(() => route.params.modelId as string)
const resources = writableAsyncComputed({
get: () => SpeckleViewer.ViewerRoute.parseUrlParameters(getParam.value),
get: () => parseUrlParameters(getParam.value),
set: async (newResources) => {
const modelId =
SpeckleViewer.ViewerRoute.createGetParamFromResources(newResources)
const modelId = createGetParamFromResources(newResources)
await router.push({
params: { modelId },
query: route.query,
@@ -465,11 +495,15 @@ function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest
})
// we could use getParam, but `createGetParamFromResources` does sorting and de-duplication AFAIK
// + we can skip duplicate updates
const resourceIdString = writableAsyncComputed({
get: () => SpeckleViewer.ViewerRoute.createGetParamFromResources(resources.value),
get: () => createGetParamFromResources(resources.value),
set: async (newVal) => {
const newResources = SpeckleViewer.ViewerRoute.parseUrlParameters(newVal)
await resources.update(newResources)
const newResources = resourceBuilder().addResources(parseUrlParameters(newVal))
const currentResources = resourceBuilder().addResources(resources.value)
if (newResources.toString() === currentResources.toString()) return
await resources.update(newResources.toResources())
},
initialState: '',
asyncRead: false
@@ -488,23 +522,19 @@ function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest
const resourceArr = resources.value.slice()
const resourceIdx = resourceArr.findIndex(
(r) => SpeckleViewer.ViewerRoute.isModelResource(r) && r.modelId === modelId
(r) => isModelResource(r) && r.modelId === modelId
)
if (resourceIdx !== -1) {
// Replace
const newResources = resources.value.slice()
newResources.splice(
resourceIdx,
1,
new SpeckleViewer.ViewerRoute.ViewerModelResource(modelId, versionId)
)
newResources.splice(resourceIdx, 1, new ViewerModelResource(modelId, versionId))
await resources.update(newResources)
} else {
// Add new one and allow de-duplication to do its thing
await resources.update([
new SpeckleViewer.ViewerRoute.ViewerModelResource(modelId, versionId),
new ViewerModelResource(modelId, versionId),
...resources.value
])
}
@@ -540,11 +570,15 @@ function setupResponseResourceItems(
state: InitialStateWithRequest
): Pick<
InjectableViewerState['resources']['response'],
'resourceItems' | 'resourceItemsQueryVariables' | 'resourceItemsLoaded'
| 'resourceItems'
| 'resourceItemsQueryVariables'
| 'resourceItemsLoaded'
| 'resolvedResourceIdString'
> {
const globalError = useError()
const {
projectId,
savedViewId,
resources: {
request: { resourceIdString }
}
@@ -560,7 +594,8 @@ function setupResponseResourceItems(
projectViewerResourcesQuery,
() => ({
projectId: projectId.value,
resourceUrlString: resourceIdString.value
resourceUrlString: resourceIdString.value,
savedViewId: savedViewId.value
}),
{ keepPreviousResult: true }
)
@@ -595,20 +630,20 @@ function setupResponseResourceItems(
const objectItems: ViewerResourceItem[] = []
const allModelItems: ViewerResourceItem[] = []
for (const group of resolvedResourceGroups.value) {
const [resource] = SpeckleViewer.ViewerRoute.parseUrlParameters(group.identifier)
const [resource] = parseUrlParameters(group.identifier)
for (const item of group.items) {
if (SpeckleViewer.ViewerRoute.isModelResource(resource)) {
if (isModelResource(resource)) {
if (resource.versionId) {
versionItems.push(item)
} else {
modelItems.push(item)
}
} else if (SpeckleViewer.ViewerRoute.isAllModelsResource(resource)) {
} else if (isAllModelsResource(resource)) {
allModelItems.push(item)
} else if (SpeckleViewer.ViewerRoute.isModelFolderResource(resource)) {
} else if (isModelFolderResource(resource)) {
folderItems.push(item)
} else if (SpeckleViewer.ViewerRoute.isObjectResource(resource)) {
} else if (isObjectResource(resource)) {
objectItems.push(item)
}
}
@@ -645,10 +680,18 @@ function setupResponseResourceItems(
const resourceItemsLoaded = computed(() => initLoadDone.value)
const resolvedResourceIdString = computed(() =>
resourceBuilder()
// Combined group identifiers should result in the final resource id string
.addFromString(resolvedResourceGroups.value.map((group) => group.identifier))
.toString()
)
return {
resourceItems,
resourceItemsQueryVariables: computed(() => resourceItemsQueryVariables.value),
resourceItemsLoaded
resourceItemsLoaded,
resolvedResourceIdString
}
}
@@ -657,14 +700,19 @@ function setupResponseResourceData(
resourceItemsData: ReturnType<typeof setupResponseResourceItems>
): Omit<
InjectableViewerState['resources']['response'],
'resourceItems' | 'resourceItemsQueryVariables' | 'resourceItemsLoaded'
| 'resourceItems'
| 'resourceItemsQueryVariables'
| 'resourceItemsLoaded'
| 'resolvedResourceIdString'
> {
const apollo = useApolloClient().client
const globalError = useError()
const { triggerNotification } = useGlobalToast()
const logger = useLogger()
const savedViewsEnabled = useAreSavedViewsEnabled()
const {
savedViewId,
projectId,
resources: {
request: { resourceIdString, threadFilters }
@@ -867,6 +915,36 @@ function setupResponseResourceData(
logger.error(err)
})
// SAVED VIEW
const { result: viewerActiveSavedViewResult, onError: onViewerActiveSavedViewError } =
useQuery(
viewerActiveSavedViewQuery,
() => ({
projectId: projectId.value,
savedViewId: savedViewId.value!
}),
{
enabled: computed(() => !!savedViewId.value && savedViewsEnabled)
}
)
onViewerActiveSavedViewError((err) => {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Saved view loading failed',
description: `${err.message}`
})
logger.error(err)
})
// Shows only the one matching the savedViewId. If the query is still loading/stale, it will return undefined
const savedView = computed(() =>
savedViewId.value &&
viewerActiveSavedViewResult.value?.project?.savedView.id === savedViewId.value
? viewerActiveSavedViewResult.value?.project?.savedView
: undefined
)
onServerPrefetch(async () => {
await Promise.all([serverResourcesLoadedPromise.promise])
})
@@ -882,7 +960,8 @@ function setupResponseResourceData(
threadsQueryVariables: computed(() => threadsQueryVariables.value),
loadMoreVersions,
resourcesLoaded: computed(() => initLoadDone.value),
resourcesLoading: computed(() => viewerLoadedResourcesLoading.value)
resourcesLoading: computed(() => viewerLoadedResourcesLoading.value),
savedView
}
}
@@ -1050,9 +1129,7 @@ export function useSetupViewer(params: UseSetupViewerParams): InjectableViewerSt
return rawState
}
/**
* COMPOSABLES FOR RETRIEVING (PARTS OF) INJECTABLE STATE
*/
// COMPOSABLES FOR RETRIEVING (PARTS OF) INJECTABLE STATE
export function useInjectedViewerState(): InjectableViewerState {
// we're forcing TS to ignore the scenario where this data can't be found and returns undefined
@@ -52,7 +52,20 @@ import {
import { setupDebugMode } from '~~/lib/viewer/composables/setup/dev'
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
import { useMixpanel } from '~~/lib/core/composables/mp'
import type { SectionBoxData } from '@speckle/shared/viewer/state'
import {
isSerializedViewerState,
type SectionBoxData,
type SerializedViewerState
} from '@speckle/shared/viewer/state'
import { graphql } from '~/lib/common/generated/gql'
import {
StateApplyMode,
useApplySerializedState
} from '~/lib/viewer/composables/serialization'
import { useViewerRealtimeActivityTracker } from '~/lib/viewer/composables/activity'
import { resourceBuilder } from '@speckle/shared/viewer/route'
import { useEventBus } from '~/lib/core/composables/eventBus'
import { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus'
function useViewerLoadCompleteEventHandler() {
const state = useInjectedViewerState()
@@ -435,8 +448,8 @@ function useViewerCameraIntegration() {
useViewerCameraTracker(
() => {
loadCameraDataFromViewer()
}
// { debounceWait: 100 }
},
{ throttleWait: 100 }
)
useOnViewerLoadComplete(({ isInitial }) => {
@@ -900,9 +913,115 @@ function useDisableZoomOnEmbed() {
)
}
graphql(`
fragment UseViewerSavedViewSetup_SavedView on SavedView {
id
viewerState
}
`)
const useViewerSavedViewSetup = () => {
const {
savedViewId,
resources: {
request: { resourceIdString },
response: { savedView, resolvedResourceIdString }
},
urlHashState: { savedViewId: urlHashSavedViewId }
} = useInjectedViewerState()
const applyState = useApplySerializedState()
const { serializedStateId } = useViewerRealtimeActivityTracker()
const { on } = useEventBus()
// Saved View ID will be unset, once the user does anything to the viewer that
// changes it from the saved view
const savedViewStateId = ref<string>()
const validState = (state: unknown) => (isSerializedViewerState(state) ? state : null)
const apply = async (state: SerializedViewerState) => {
// Combine resolved w/ old, resolved taking precedence - we dont want to unload
// other federated resources that are not a part of the saved view
const combinedIdString = resourceBuilder()
.addResources(resolvedResourceIdString.value)
.addNew(resourceIdString.value)
.toString()
await resourceIdString.update(combinedIdString)
await applyState(state, StateApplyMode.SavedView)
savedViewStateId.value = serializedStateId.value
}
const update = (params: { viewId?: string }) => {
// If passing in viewId and it differs, apply and wait for that to finish
if (params.viewId && params.viewId !== savedViewId.value) {
savedViewId.value = params.viewId
return
}
// Re-apply current state
const state = validState(savedView.value?.viewerState)
if (!state) return
apply(state)
}
// Allow force update
on(ViewerEventBusKeys.UpdateSavedView, (params) => {
update(params)
})
// Apply saved view state on initial load
useOnViewerLoadComplete(async ({ isInitial }) => {
const state = validState(savedView.value?.viewerState)
if (isInitial && state) {
await apply(state)
}
})
// Saved view changed, apply
watch(savedView, (newVal, oldVal) => {
if (!newVal || newVal.id === oldVal?.id) return
const state = validState(newVal.viewerState)
if (!state) return
// If the saved view has changed, apply it
apply(state)
})
// If the URL hash saved view ID has changed, update the saved view ID
watch(
urlHashSavedViewId,
async (newVal, oldVal) => {
if (newVal === oldVal) return
savedViewId.value = newVal
},
{ immediate: true }
)
// Did state change after applying saved view? Undo view
watch(
serializedStateId,
(newVal, oldVal) => {
if (newVal === oldVal) return
// If the saved view state ID is different from the current serialized state ID, reset the saved view
if (savedViewStateId.value && newVal !== savedViewStateId.value) {
savedViewId.value = null
void urlHashSavedViewId.update(null)
savedViewStateId.value = undefined
}
},
{ immediate: true }
)
}
export function useViewerPostSetup() {
if (import.meta.server) return
useViewerObjectAutoLoading()
useViewerSavedViewSetup()
useViewerReceiveTracking()
useViewerSelectionEventHandler()
useViewerLoadCompleteEventHandler()
@@ -6,7 +6,8 @@ import { useDiffBuilderUtilities } from '~~/lib/viewer/composables/setup/diff'
export enum ViewerHashStateKeys {
FocusedThreadId = 'threadId',
Diff = 'diff',
EmbedOptions = 'embed'
EmbedOptions = 'embed',
SavedViewId = 'savedViewId'
}
export function setupUrlHashState(): InjectableViewerState['urlHashState'] {
@@ -40,8 +41,21 @@ export function setupUrlHashState(): InjectableViewerState['urlHashState'] {
asyncRead: false
})
const savedViewId = writableAsyncComputed({
get: () => hashState.value[ViewerHashStateKeys.SavedViewId] || null,
set: async (newVal) => {
await hashState.update({
...hashState.value,
[ViewerHashStateKeys.SavedViewId]: newVal
})
},
initialState: null,
asyncRead: false
})
return {
focusedThreadId,
diff
diff,
savedViewId
}
}
@@ -1,10 +1,14 @@
import { graphql } from '~~/lib/common/generated/gql'
export const projectViewerResourcesQuery = graphql(`
query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {
query ProjectViewerResources(
$projectId: String!
$resourceUrlString: String!
$savedViewId: String
) {
project(id: $projectId) {
id
viewerResources(resourceIdString: $resourceUrlString) {
viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {
identifier
items {
modelId
@@ -16,6 +20,18 @@ export const projectViewerResourcesQuery = graphql(`
}
`)
export const viewerActiveSavedViewQuery = graphql(`
query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {
project(id: $projectId) {
id
savedView(id: $savedViewId) {
id
...UseViewerSavedViewSetup_SavedView
}
}
}
`)
/**
* Query to load all metadata needed for loaded models (& their versions) in the viewer, for
* all sidebar panels and everything
@@ -65,6 +81,7 @@ export const viewerLoadedResourcesQuery = graphql(`
...UseViewerUserActivityBroadcasting_Project
...ViewerGendoPanel_Project
...ViewerResourcesLimitAlert_Project
...ViewerSavedViewsPanel_Project
}
}
`)
@@ -0,0 +1,8 @@
export enum ViewerEventBusKeys {
UpdateSavedView = 'aaa'
}
// Add mappings between event keys and expected payloads here
export type ViewerEventBusKeyPayloadMap = {
[ViewerEventBusKeys.UpdateSavedView]: { viewId?: string }
} & { [k in ViewerEventBusKeys]: unknown } & Record<string, unknown>
@@ -0,0 +1,14 @@
import type { StringEnumValues } from '@speckle/shared'
export const ViewsType = {
All: 'all-views',
My: 'my-views',
Connector: 'connector-views'
} as const
export type ViewsType = StringEnumValues<typeof ViewsType>
export const viewsTypeLabels: Record<ViewsType, string> = {
[ViewsType.All]: 'All Views',
[ViewsType.My]: 'My Views',
[ViewsType.Connector]: 'From connectors'
}
@@ -22,6 +22,13 @@ export const PanelShortcuts = {
modifiers: [ModifierKeys.Shift],
key: 'D',
action: 'ToggleDiscussions'
},
ToggleSavedViews: {
name: 'Saved Views',
description: 'Toggle saved views panel',
modifiers: [ModifierKeys.Shift],
key: 'V',
action: 'ToggleSavedViews'
}
} as const
@@ -3,6 +3,7 @@ import { useQuery } from '@vue/apollo-composable'
import { workspaceLimitsQuery } from '~/lib/workspaces/graphql/queries'
import { WorkspacePlanConfigs, type MaybeNullOrUndefined } from '@speckle/shared'
import type { WorkspacePlanLimits_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
import { useFeatureFlags } from '~/lib/common/composables/env'
graphql(`
fragment WorkspacePlanLimits_Workspace on Workspace {
@@ -19,6 +20,8 @@ export const useWorkspaceLimits = (params: {
workspace?: MaybeRef<MaybeNullOrUndefined<WorkspacePlanLimits_WorkspaceFragment>>
}) => {
const { slug } = params
const featureFlags = useFeatureFlags()
const { result } = useQuery(
workspaceLimitsQuery,
() => ({
@@ -44,7 +47,7 @@ export const useWorkspaceLimits = (params: {
commentHistory: null
}
const planConfig = WorkspacePlanConfigs[planName]
const planConfig = WorkspacePlanConfigs({ featureFlags })[planName]
return planConfig?.limits
})
@@ -18,25 +18,30 @@ import { activeUserWorkspaceExistenceCheckQuery } from '~/lib/auth/graphql/queri
import { useApolloClientFromNuxt } from '~~/lib/common/composables/graphql'
import { convertThrowIntoFetchResult } from '~~/lib/common/helpers/graphql'
export default defineNuxtRouteMiddleware(async (to) => {
export default defineNuxtRouteMiddleware(async (to, from) => {
const isAuthPage = to.path.startsWith('/authn/')
const isSSOPath = to.path.includes('/sso/')
if (isAuthPage || isSSOPath) return
const client = useApolloClientFromNuxt()
// Fetch required data
const { data: serverInfoData } = await client
.query({
query: mainServerInfoDataQuery
})
.catch(convertThrowIntoFetchResult)
const isInPlaceNavigation = checkIfIsInPlaceNavigation(to, from)
const { data: userData } = await client
.query({
query: activeUserQuery
})
.catch(convertThrowIntoFetchResult)
// Fetch required data
const [{ data: serverInfoData }, { data: userData }] = await Promise.all([
client
.query({
query: mainServerInfoDataQuery,
fetchPolicy: isInPlaceNavigation ? 'cache-first' : undefined
})
.catch(convertThrowIntoFetchResult),
client
.query({
query: activeUserQuery,
fetchPolicy: isInPlaceNavigation ? 'cache-first' : undefined
})
.catch(convertThrowIntoFetchResult)
])
// If user is not logged in, skip all checks
if (!userData?.activeUser?.id) return
@@ -12,7 +12,7 @@ import { useSetActiveWorkspace } from '~/lib/user/composables/activeWorkspace'
/**
* Used in project page to validate that project ID refers to a valid project and redirects to 404 if not
*/
export default defineNuxtRouteMiddleware(async (to) => {
export default defineNuxtRouteMiddleware(async (to, from) => {
const projectId = to.params.id as string
// Check if embed token is present in URL
@@ -28,6 +28,8 @@ export default defineNuxtRouteMiddleware(async (to) => {
const { isLoggedIn } = useActiveUser()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const isInPlaceNavigation = checkIfIsInPlaceNavigation(to, from)
const { data, errors } = await client
.query({
query: projectAccessCheckQuery,
@@ -35,7 +37,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
context: {
skipLoggingErrors: true
},
fetchPolicy: 'network-only'
fetchPolicy: isInPlaceNavigation ? 'cache-first' : 'network-only'
})
.catch(convertThrowIntoFetchResult)
@@ -79,7 +81,7 @@ export default defineNuxtRouteMiddleware(async (to) => {
}
}
if (isLoggedIn.value && isWorkspacesEnabled.value) {
if (isLoggedIn.value && isWorkspacesEnabled.value && !isInPlaceNavigation) {
await setActiveWorkspace({ id: data?.project.workspaceId })
}
})
+4
View File
@@ -220,6 +220,10 @@ export default defineNuxtConfig({
to: '/workspaces/actions/create',
statusCode: 301
}
},
// CSR only viewer, we cant preload much because of the url hash state which is CSR only
'/projects/:id/models/:modelId': {
ssr: false
}
},
+1
View File
@@ -66,6 +66,7 @@
"js-cookie": "^3.0.1",
"jsdom": "^22.1.0",
"lodash-es": "^4.17.21",
"lucide-vue-next": "^0.535.0",
"marked": "^5.1.0",
"marked-plaintext": "^0.0.2",
"mitt": "^3.0.0",
+4
View File
@@ -14,6 +14,10 @@ export default defineNuxtPlugin(() => {
key: T,
handler: (event: EventBusKeyPayloadMap[T]) => void
) => emitter.on(key, handler),
off: <T extends EventBusKeys>(
key: T,
handler?: (event: EventBusKeyPayloadMap[T]) => void
) => emitter.off(key, handler),
emit: <T extends EventBusKeys>(key: T, payload: EventBusKeyPayloadMap[T]) =>
emitter.emit(key, payload)
}
+2 -1
View File
@@ -11,6 +11,7 @@ import {
ROOT_QUERY,
ROOT_SUBSCRIPTION
} from '~/lib/common/helpers/graphql'
import { checkIfIsInPlaceNavigation } from '~/lib/common/helpers/navigation'
/**
* Debugging helper to ensure variables are available in debugging scope
@@ -24,7 +25,6 @@ export const getRouteDefinition = (route?: RouteLocationNormalized) => {
const matchedPath = route ? route.matched[route.matched.length - 1]?.path : undefined
return matchedPath || '/404'
}
export {
ToastNotificationType,
wrapRefWithTracking,
@@ -33,6 +33,7 @@ export {
getFirstGqlErrorMessage,
modifyObjectField,
getCacheId,
checkIfIsInPlaceNavigation,
ROOT_QUERY,
ROOT_MUTATION,
ROOT_SUBSCRIPTION
+1 -1
View File
@@ -12,7 +12,7 @@
},
"type": "module",
"engines": {
"node": "^22.6.0"
"node": "^22.17.1"
},
"scripts": {
"build:tsc:watch": "tsc -p ./tsconfig.build.json --watch",
+1 -1
View File
@@ -12,7 +12,7 @@
},
"type": "module",
"engines": {
"node": "^22.6.0"
"node": "^22.17.1"
},
"scripts": {
"build:frontend": "yarn workspace @speckle/preview-frontend build",
+7 -2
View File
@@ -1,6 +1,11 @@
{
"mochaExplorer.env": { "NODE_ENV": "test" },
"mochaExplorer.nodeArgv": ["--import", "tsx"],
"mochaExplorer.env": { "NODE_ENV": "test", "TSX": "true" },
"mochaExplorer.nodeArgv": [
"--experimental-strip-types",
"--experimental-transform-types",
"--import",
"./esmLoader.js"
],
"javascript.suggest.autoImports": true,
"typescript.suggest.autoImports": true,
"typescript.preferences.importModuleSpecifier": "non-relative",
@@ -45,4 +45,8 @@ type PermissionCheckResult {
code: String!
message: String!
payload: JSONObject
"""
Same as message, or undefined if check is authorized
"""
errorMessage: String
}
@@ -29,6 +29,10 @@ extend type Project {
"""
viewerResources(
resourceIdString: String!
"""
If a saved view ID is specified, the returned resources will be adjusted to return the view's resources instead
"""
savedViewId: String
loadedVersionsOnly: Boolean = true
): [ViewerResourceGroup!]!
"""
@@ -0,0 +1,179 @@
enum SavedViewVisibility {
public
authorOnly
}
type SavedView {
id: ID!
name: String!
description: String
"""
Empty ID means default/ungrouped view
"""
groupId: ID
"""
Always available because even ungrouped views show up in a fake "Ungrouped" group
"""
group: SavedViewGroup!
author: LimitedUser
createdAt: DateTime!
updatedAt: DateTime!
projectId: ID!
"""
Original resource ID string that this view is associated with.
"""
resourceIdString: String!
"""
Same as resourceIdString, but split into an array of resource IDs.
"""
resourceIds: [String!]!
isHomeView: Boolean!
visibility: SavedViewVisibility!
"""
Viewer state, the actual view configuration
"""
viewerState: JSONObject!
"""
Encoded screenshot of the view
"""
screenshot: String!
"""
For figuring out position in the group
"""
position: Float!
}
type SavedViewCollection {
cursor: String
totalCount: Int!
items: [SavedView!]!
}
input SavedViewGroupViewsInput {
"""
Whether to only views authored by the current user
"""
onlyAuthored: Boolean
"""
Whether to only include views matching this search term
"""
search: String
"""
Optionally specify sort direction. Default: descending
"""
sortDirection: SortDirection
"""
Optionally specify sort by field. Default: updatedAt
Options: updatedAt, createdAt, name
"""
sortBy: String
limit: Int
cursor: String
}
type SavedViewGroup {
"""
This is always set even for fake/not persisted groups for Apollo caching
"""
id: ID!
"""
Only set if this is a real/persisted group.
"""
groupId: ID
projectId: ID!
"""
Resources that were used to find this group
"""
resourceIds: [String!]!
title: String!
isUngroupedViewsGroup: Boolean!
views(input: SavedViewGroupViewsInput!): SavedViewCollection!
}
type SavedViewGroupCollection {
cursor: String
totalCount: Int!
items: [SavedViewGroup!]!
}
input SavedViewGroupsInput {
"""
Viewer resource ID string that identifies which resources should be loaded
"""
resourceIdString: String!
"""
Whether to only include groups w/ views authored by the current user
"""
onlyAuthored: Boolean
"""
Whether to only include groups that have views matching this search term
"""
search: String
limit: Int
cursor: String
}
input GetUngroupedViewGroupInput {
"""
Viewer resource ID string that identifies which resources should be loaded
"""
resourceIdString: String!
}
extend type Project {
savedViewGroups(input: SavedViewGroupsInput!): SavedViewGroupCollection!
savedViewGroup(id: ID!): SavedViewGroup!
ungroupedViewGroup(input: GetUngroupedViewGroupInput!): SavedViewGroup!
savedView(id: ID!): SavedView!
}
input CreateSavedViewInput {
projectId: ID!
resourceIdString: String!
"""
Group id, if grouping necessary
"""
groupId: ID
"""
Auto-generated name, if not specified
"""
name: String
description: String
"""
SerializedViewerState. If omitted, comment won't render (correctly) inside the
viewer, but will still be retrievable through the API
"""
viewerState: JSONObject!
"""
Encoded screenshot of the view
"""
screenshot: String!
"""
Optionally also set this as the home/default view for the target model
"""
isHomeView: Boolean
"""
Set visibility of the view. Default: public
"""
visibility: SavedViewVisibility
}
input CreateSavedViewGroupInput {
projectId: ID!
resourceIdString: String!
groupName: String!
}
type SavedViewMutations {
createGroup(input: CreateSavedViewGroupInput!): SavedViewGroup!
createView(input: CreateSavedViewInput!): SavedView!
}
extend type ProjectMutations {
savedViewMutations: SavedViewMutations!
}
extend type ProjectPermissionChecks {
canCreateSavedView: PermissionCheckResult!
}
+1
View File
@@ -44,6 +44,7 @@ if ((isTestEnv() || isDevEnv()) && startDebugger) {
}
}
// Load dotenv
dotenv.config({ path: `${packageRoot}/.env` })
// knex is a singleton controlled by module so can't wait til app init
+8 -1
View File
@@ -185,7 +185,14 @@ const config: CodegenConfig = {
RootPermissionChecks:
'@/modules/core/helpers/graphTypes#RootPermissionChecksGraphQLReturn',
WorkspacePermissionChecks:
'@/modules/workspacesCore/helpers/graphTypes#WorkspacePermissionChecksGraphQLReturn'
'@/modules/workspacesCore/helpers/graphTypes#WorkspacePermissionChecksGraphQLReturn',
SavedViewMutations:
'@/modules/core/helpers/graphTypes#MutationsObjectGraphQLReturn',
SavedView: '@/modules/viewer/helpers/graphTypes#SavedViewGraphQLReturn',
SavedViewGroup:
'@/modules/viewer/helpers/graphTypes#SavedViewGroupGraphQLReturn',
PermissionCheckResult:
'@/modules/core/helpers/graphTypes#PermissionCheckResultGraphQLReturn'
}
}
}
+2 -1
View File
@@ -109,7 +109,8 @@ const configs = [
{
files: ['**/*.spec.ts', '**/tests/**/*.{js,ts}', 'test/**/*.{js,ts}'],
rules: {
'@typescript-eslint/no-unused-expressions': 'off'
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off'
}
},
prettierConfig
+1 -1
View File
@@ -21,7 +21,7 @@ const aliases = {
/**
* EXTENSIONS TO EVALUATE FOR EXTENSIONLESS IMPORTS
*/
const extensions = ['.ts', '.js', '.mjs', '.cjs', '.json']
const extensions = ['.ts', '.js', '.mjs', '.cjs', '.json', '.d.ts']
// Register the module hooks
register('./esmLoader.js', {
@@ -3,7 +3,6 @@ import type {
StreamResourceTypes,
StreamScopeActivity
} from '@/modules/activitystream/helpers/types'
import type { ViewerResourceItem } from '@/modules/comments/domain/types'
import type {
CommentCreateInput,
CreateCommentInput,
@@ -11,6 +10,7 @@ import type {
ReplyCreateInput
} from '@/modules/core/graph/generated/graphql'
import type { StreamRecord, UserRecord } from '@/modules/core/helpers/types'
import type { ViewerResourceItem } from '@/modules/viewer/domain/types/resources'
import z from 'zod'
// Activity
@@ -259,7 +259,8 @@ const mocks: SpeckleModuleMocksConfig = FF_AUTOMATE_MODULE_ENABLED
canRegenerateToken: () => ({
authorized: faker.datatype.boolean(),
code: faker.string.alphanumeric(10),
message: faker.lorem.words(10)
message: faker.lorem.words(10),
payload: null
})
},
AutomateFunctionRelease: {
@@ -50,6 +50,7 @@ export default {
userId: context.userId,
projectId: parent.projectId
})
return Authz.toGraphqlResult(canCreateAutomation)
}
}
@@ -8,6 +8,7 @@ import {
} from '@/modules/core/repositories/streams'
import {
getBranchByIdFactory,
getBranchesByIdsFactory,
getBranchLatestCommitsFactory,
getStreamBranchByNameFactory,
getStreamBranchesByNameFactory,
@@ -23,8 +24,6 @@ import {
createCommentThreadAndNotifyFactory
} from '@/modules/comments/services/management'
import {
getViewerResourceGroupsFactory,
getViewerResourceItemsUngroupedFactory,
getViewerResourcesForCommentFactory,
getViewerResourcesForCommentsFactory,
getViewerResourcesFromLegacyIdentifiersFactory
@@ -53,6 +52,10 @@ import { createObjectFactory } from '@/modules/core/services/objects/management'
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import { db } from '@/db/knex'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getViewerResourceGroupsFactory,
getViewerResourceItemsUngroupedFactory
} from '@/modules/viewer/services/viewerResources'
const command: CommandModule<
unknown,
@@ -114,7 +117,8 @@ const command: CommandModule<
getBranchLatestCommits,
getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDb }),
getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDb }),
getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb })
getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb }),
getBranchesByIds: getBranchesByIdsFactory({ db: projectDb })
})
})
const getViewerResourcesFromLegacyIdentifiers =
@@ -10,6 +10,7 @@ import {
import {
createBranchFactory,
getBranchByIdFactory,
getBranchesByIdsFactory,
getBranchLatestCommitsFactory,
getStreamBranchByNameFactory,
getStreamBranchesByNameFactory,
@@ -35,8 +36,6 @@ import {
insertStreamCommitsFactory
} from '@/modules/core/repositories/commits'
import {
getViewerResourceGroupsFactory,
getViewerResourceItemsUngroupedFactory,
getViewerResourcesForCommentFactory,
getViewerResourcesForCommentsFactory,
getViewerResourcesFromLegacyIdentifiersFactory
@@ -70,6 +69,10 @@ import {
} from '@/modules/core/repositories/projects'
import { storeModelFactory } from '@/modules/core/repositories/models'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getViewerResourceGroupsFactory,
getViewerResourceItemsUngroupedFactory
} from '@/modules/viewer/services/viewerResources'
const command: CommandModule<
unknown,
@@ -142,7 +145,8 @@ const command: CommandModule<
getBranchLatestCommits: getBranchLatestCommitsFactory({ db: projectDb }),
getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDb }),
getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDb }),
getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb })
getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb }),
getBranchesByIds: getBranchesByIdsFactory({ db: projectDb })
})
})
const getViewerResourcesFromLegacyIdentifiers =
@@ -2,7 +2,7 @@ import type {
CommentCreatedActivityInput,
ReplyCreatedActivityInput
} from '@/modules/activitystream/domain/types'
import type { ViewerResourceItem } from '@/modules/comments/domain/types'
import type { ViewerResourceItem } from '@/modules/viewer/domain/types/resources'
import type { CommentRecord } from '@/modules/comments/helpers/types'
import type { MutationCommentArchiveArgs } from '@/modules/core/graph/generated/graphql'
@@ -1,8 +1,6 @@
import type {
ExtendedComment,
ResourceIdentifier,
ViewerResourceGroup,
ViewerResourceItem
ResourceIdentifier
} from '@/modules/comments/domain/types'
import type {
CommentLinkRecord,
@@ -14,8 +12,7 @@ import type {
CreateCommentInput,
CreateCommentReplyInput,
EditCommentInput,
LegacyCommentViewerData,
ViewerUpdateTrackingTarget
LegacyCommentViewerData
} from '@/modules/core/graph/generated/graphql'
import type { SmartTextEditorValueSchema } from '@/modules/core/services/richTextEditorService'
import type { BatchedSelectOptions } from '@/modules/shared/helpers/dbHelper'
@@ -26,6 +23,7 @@ import type {
import type { MaybeNullOrUndefined, SpeckleViewer } from '@speckle/shared'
import type { Knex } from 'knex'
import type { Merge } from 'type-fest'
import type { ViewerResourceItem } from '@/modules/viewer/domain/types/resources'
type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState
@@ -229,14 +227,6 @@ export type GetViewerResourcesFromLegacyIdentifiers = (
resources: Array<ResourceIdentifier>
) => Promise<ViewerResourceItem[]>
export type GetViewerResourceGroups = (
target: ViewerUpdateTrackingTarget
) => Promise<ViewerResourceGroup[]>
export type GetViewerResourceItemsUngrouped = (
target: ViewerUpdateTrackingTarget
) => Promise<ViewerResourceItem[]>
export type ConvertLegacyDataToState = (
data: Partial<LegacyCommentViewerData>,
comment: CommentRecord
@@ -1,5 +1,4 @@
import type { CommentLinkRecord, CommentRecord } from '@/modules/comments/helpers/types'
import type { Nullable } from '@speckle/shared'
export type ResourceIdentifier = {
resourceId: string
@@ -27,18 +26,3 @@ export type ExtendedComment = CommentRecord & {
*/
viewedAt?: Date
}
export type ViewerResourceItem = {
/** Null if resource represents an object */
modelId?: Nullable<string>
objectId: string
/** Null if resource represents an object */
versionId?: Nullable<string>
}
export type ViewerResourceGroup = {
/** Resource identifier used to refer to a collection of resource items */
identifier: string
/** Viewer resources that the identifier refers to */
items: Array<ViewerResourceItem>
}
@@ -48,12 +48,9 @@ import {
ProjectSubscriptions
} from '@/modules/shared/utils/subscriptions'
import {
doViewerResourcesFit,
getViewerResourcesForCommentFactory,
getViewerResourcesFromLegacyIdentifiersFactory,
getViewerResourcesForCommentsFactory,
getViewerResourceItemsUngroupedFactory,
getViewerResourceGroupsFactory
getViewerResourcesForCommentsFactory
} from '@/modules/core/services/commit/viewerResources'
import {
createCommentThreadAndNotifyFactory,
@@ -81,6 +78,7 @@ import {
getSpecificBranchCommitsFactory
} from '@/modules/core/repositories/commits'
import {
getBranchesByIdsFactory,
getBranchLatestCommitsFactory,
getStreamBranchesByNameFactory
} from '@/modules/core/repositories/branches'
@@ -93,6 +91,11 @@ import { StreamNotFoundError } from '@/modules/core/errors/stream'
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
import { withOperationLogging } from '@/observability/domain/businessLogging'
import { isCreatedBeyondHistoryLimitCutoffFactory } from '@/modules/gatekeeperCore/utils/limits'
import {
doViewerResourcesFit,
getViewerResourceGroupsFactory,
getViewerResourceItemsUngroupedFactory
} from '@/modules/viewer/services/viewerResources'
// We can use the main DB for these
const getStream = getStreamFactory({ db })
@@ -118,7 +121,8 @@ const buildGetViewerResourceItemsUngrouped = (deps: { db: Knex }) =>
getBranchLatestCommits: getBranchLatestCommitsFactory(deps),
getStreamBranchesByName: getStreamBranchesByNameFactory(deps),
getSpecificBranchCommits: getSpecificBranchCommitsFactory(deps),
getAllBranchCommits: getAllBranchCommitsFactory(deps)
getAllBranchCommits: getAllBranchCommitsFactory(deps),
getBranchesByIds: getBranchesByIdsFactory(deps)
})
})
@@ -3,45 +3,24 @@ import type {
GetViewerResourcesForComments
} from '@/modules/comments/domain/operations'
import type { LegacyCommentViewerData } from '@/modules/core/graph/generated/graphql'
import { viewerResourcesToString } from '@/modules/core/services/commit/viewerResources'
import type { Nullable } from '@speckle/shared'
import { SpeckleViewer } from '@speckle/shared'
import { has, get, intersection, isObjectLike } from 'lodash-es'
type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState
import { viewerResourcesToString } from '@/modules/viewer/services/viewerResources'
import type {
VersionedSerializedViewerState,
SerializedViewerState
} from '@speckle/shared/viewer/state'
import {
formatSerializedViewerState,
isVersionedSerializedViewerState,
inputToVersionedState
} from '@speckle/shared/viewer/state'
import { intersection, isObjectLike } from 'lodash-es'
export type LegacyData = Partial<LegacyCommentViewerData>
export type DataStruct = VersionedSerializedViewerState
export type DataStruct = {
version: number
state: SerializedViewerState
}
export function inputToDataStruct(
inputSerializedViewerState: unknown
): Nullable<DataStruct> {
const state = SpeckleViewer.ViewerState.isSerializedViewerState(
inputSerializedViewerState
)
? inputSerializedViewerState
: null
if (!state) return null
return {
version: SpeckleViewer.ViewerState.SERIALIZED_VIEWER_STATE_VERSION,
state
}
}
export function isDataStruct(data: unknown): data is DataStruct {
if (!data) return false
if (!has(data, 'version')) return false
const stateRaw = get(data, 'state')
return SpeckleViewer.ViewerState.isSerializedViewerState(stateRaw)
}
export const formatSerializedViewerState =
SpeckleViewer.ViewerState.formatSerializedViewerState
export { formatSerializedViewerState }
export const inputToDataStruct = inputToVersionedState
export const isDataStruct = isVersionedSerializedViewerState
export function isLegacyData(data: unknown): data is LegacyData {
if (!data) return false
@@ -20,7 +20,6 @@ import type {
CreateCommentThreadAndNotify,
EditCommentAndNotify,
GetComment,
GetViewerResourceItemsUngrouped,
GetViewerResourcesForComment,
InsertCommentLinks,
InsertCommentPayload,
@@ -33,6 +32,7 @@ import type {
import type { GetStream } from '@/modules/core/domain/streams/operations'
import type { EventBusEmit } from '@/modules/shared/services/eventBus'
import { CommentEvents } from '@/modules/comments/domain/events'
import type { GetViewerResourceItemsUngrouped } from '@/modules/viewer/domain/operations/resources'
export const createCommentThreadAndNotifyFactory =
(deps: {
@@ -1,5 +1,5 @@
import type { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql';
import type { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, EmbedTokenGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn, UserMetaGraphQLReturn, ProjectPermissionChecksGraphQLReturn, ModelPermissionChecksGraphQLReturn, VersionPermissionChecksGraphQLReturn, RootPermissionChecksGraphQLReturn } from '@/modules/core/helpers/graphTypes';
import type { StreamGraphQLReturn, CommitGraphQLReturn, ProjectGraphQLReturn, ObjectGraphQLReturn, VersionGraphQLReturn, ServerInviteGraphQLReturnType, ModelGraphQLReturn, ModelsTreeItemGraphQLReturn, MutationsObjectGraphQLReturn, LimitedUserGraphQLReturn, UserGraphQLReturn, EmbedTokenGraphQLReturn, GraphQLEmptyReturn, StreamCollaboratorGraphQLReturn, ProjectCollaboratorGraphQLReturn, ServerInfoGraphQLReturn, BranchGraphQLReturn, UserMetaGraphQLReturn, ProjectPermissionChecksGraphQLReturn, ModelPermissionChecksGraphQLReturn, VersionPermissionChecksGraphQLReturn, RootPermissionChecksGraphQLReturn, PermissionCheckResultGraphQLReturn } from '@/modules/core/helpers/graphTypes';
import type { StreamAccessRequestGraphQLReturn, ProjectAccessRequestGraphQLReturn } from '@/modules/accessrequests/helpers/graphTypes';
import type { AutomateFunctionPermissionChecksGraphQLReturn, AutomateFunctionGraphQLReturn, AutomateFunctionReleaseGraphQLReturn, AutomationGraphQLReturn, AutomationPermissionChecksGraphQLReturn, AutomationRevisionGraphQLReturn, AutomationRevisionFunctionGraphQLReturn, AutomateRunGraphQLReturn, AutomationRunTriggerGraphQLReturn, AutomationRevisionTriggerDefinitionGraphQLReturn, AutomateFunctionRunGraphQLReturn, TriggeredAutomationsStatusGraphQLReturn, ProjectAutomationMutationsGraphQLReturn, ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn, ProjectAutomationsUpdatedMessageGraphQLReturn, UserAutomateInfoGraphQLReturn } from '@/modules/automate/helpers/graphTypes';
import type { CommentReplyAuthorCollectionGraphQLReturn, CommentGraphQLReturn, CommentPermissionChecksGraphQLReturn } from '@/modules/comments/helpers/graphTypes';
@@ -15,6 +15,7 @@ import type { ActivityCollectionGraphQLReturn } from '@/modules/activitystream/h
import type { ServerAppGraphQLReturn, ServerAppListItemGraphQLReturn } from '@/modules/auth/helpers/graphTypes';
import type { GendoAIRenderGraphQLReturn } from '@/modules/gendo/helpers/types/graphTypes';
import type { ServerRegionItemGraphQLReturn } from '@/modules/multiregion/helpers/graphTypes';
import type { SavedViewGraphQLReturn, SavedViewGroupGraphQLReturn } from '@/modules/viewer/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;
@@ -971,6 +972,33 @@ export type CreateModelInput = {
projectId: Scalars['ID']['input'];
};
export type CreateSavedViewGroupInput = {
groupName: Scalars['String']['input'];
projectId: Scalars['ID']['input'];
resourceIdString: Scalars['String']['input'];
};
export type CreateSavedViewInput = {
description?: InputMaybe<Scalars['String']['input']>;
/** Group id, if grouping necessary */
groupId?: InputMaybe<Scalars['ID']['input']>;
/** Optionally also set this as the home/default view for the target model */
isHomeView?: InputMaybe<Scalars['Boolean']['input']>;
/** Auto-generated name, if not specified */
name?: InputMaybe<Scalars['String']['input']>;
projectId: Scalars['ID']['input'];
resourceIdString: Scalars['String']['input'];
/** Encoded screenshot of the view */
screenshot: Scalars['String']['input'];
/**
* SerializedViewerState. If omitted, comment won't render (correctly) inside the
* viewer, but will still be retrievable through the API
*/
viewerState: Scalars['JSONObject']['input'];
/** Set visibility of the view. Default: public */
visibility?: InputMaybe<SavedViewVisibility>;
};
export type CreateServerRegionInput = {
description?: InputMaybe<Scalars['String']['input']>;
key: Scalars['String']['input'];
@@ -1220,6 +1248,11 @@ export type GetModelUploadsInput = {
limit?: InputMaybe<Scalars['Int']['input']>;
};
export type GetUngroupedViewGroupInput = {
/** Viewer resource ID string that identifies which resources should be loaded */
resourceIdString: Scalars['String']['input'];
};
export type InvitableCollaboratorsFilter = {
search?: InputMaybe<Scalars['String']['input']>;
};
@@ -2177,6 +2210,8 @@ export type PermissionCheckResult = {
__typename?: 'PermissionCheckResult';
authorized: Scalars['Boolean']['output'];
code: Scalars['String']['output'];
/** Same as message, or undefined if check is authorized */
errorMessage?: Maybe<Scalars['String']['output']>;
message: Scalars['String']['output'];
payload?: Maybe<Scalars['JSONObject']['output']>;
};
@@ -2235,9 +2270,13 @@ export type Project = {
permissions: ProjectPermissionChecks;
/** Active user's role for this project. `null` if request is not authenticated, or the project is not explicitly shared with you. */
role?: Maybe<Scalars['String']['output']>;
savedView: SavedView;
savedViewGroup: SavedViewGroup;
savedViewGroups: SavedViewGroupCollection;
/** Source apps used in any models of this project */
sourceApps: Array<Scalars['String']['output']>;
team: Array<ProjectCollaborator>;
ungroupedViewGroup: SavedViewGroup;
updatedAt: Scalars['DateTime']['output'];
/** Retrieve a specific project version by its ID */
version: Version;
@@ -2350,6 +2389,26 @@ export type ProjectPendingImportedModelsArgs = {
};
export type ProjectSavedViewArgs = {
id: Scalars['ID']['input'];
};
export type ProjectSavedViewGroupArgs = {
id: Scalars['ID']['input'];
};
export type ProjectSavedViewGroupsArgs = {
input: SavedViewGroupsInput;
};
export type ProjectUngroupedViewGroupArgs = {
input: GetUngroupedViewGroupInput;
};
export type ProjectVersionArgs = {
id: Scalars['String']['input'];
};
@@ -2364,6 +2423,7 @@ export type ProjectVersionsArgs = {
export type ProjectViewerResourcesArgs = {
loadedVersionsOnly?: InputMaybe<Scalars['Boolean']['input']>;
resourceIdString: Scalars['String']['input'];
savedViewId?: InputMaybe<Scalars['String']['input']>;
};
@@ -2707,6 +2767,7 @@ export type ProjectMutations = {
leave: Scalars['Boolean']['output'];
revokeEmbedToken: Scalars['Boolean']['output'];
revokeEmbedTokens: Scalars['Boolean']['output'];
savedViewMutations: SavedViewMutations;
/** Updates an existing project */
update: Project;
/** Update role for a collaborator */
@@ -2799,6 +2860,7 @@ export type ProjectPermissionChecks = {
canCreateComment: PermissionCheckResult;
canCreateEmbedTokens: PermissionCheckResult;
canCreateModel: PermissionCheckResult;
canCreateSavedView: PermissionCheckResult;
canDelete: PermissionCheckResult;
canInvite: PermissionCheckResult;
canLeave: PermissionCheckResult;
@@ -3246,6 +3308,115 @@ export type RootPermissionChecks = {
canCreateWorkspace: PermissionCheckResult;
};
export type SavedView = {
__typename?: 'SavedView';
author?: Maybe<LimitedUser>;
createdAt: Scalars['DateTime']['output'];
description?: Maybe<Scalars['String']['output']>;
/** Always available because even ungrouped views show up in a fake "Ungrouped" group */
group: SavedViewGroup;
/** Empty ID means default/ungrouped view */
groupId?: Maybe<Scalars['ID']['output']>;
id: Scalars['ID']['output'];
isHomeView: Scalars['Boolean']['output'];
name: Scalars['String']['output'];
/** For figuring out position in the group */
position: Scalars['Float']['output'];
projectId: Scalars['ID']['output'];
/** Original resource ID string that this view is associated with. */
resourceIdString: Scalars['String']['output'];
/** Same as resourceIdString, but split into an array of resource IDs. */
resourceIds: Array<Scalars['String']['output']>;
/** Encoded screenshot of the view */
screenshot: Scalars['String']['output'];
updatedAt: Scalars['DateTime']['output'];
/** Viewer state, the actual view configuration */
viewerState: Scalars['JSONObject']['output'];
visibility: SavedViewVisibility;
};
export type SavedViewCollection = {
__typename?: 'SavedViewCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<SavedView>;
totalCount: Scalars['Int']['output'];
};
export type SavedViewGroup = {
__typename?: 'SavedViewGroup';
/** Only set if this is a real/persisted group. */
groupId?: Maybe<Scalars['ID']['output']>;
/** This is always set even for fake/not persisted groups for Apollo caching */
id: Scalars['ID']['output'];
isUngroupedViewsGroup: Scalars['Boolean']['output'];
projectId: Scalars['ID']['output'];
/** Resources that were used to find this group */
resourceIds: Array<Scalars['String']['output']>;
title: Scalars['String']['output'];
views: SavedViewCollection;
};
export type SavedViewGroupViewsArgs = {
input: SavedViewGroupViewsInput;
};
export type SavedViewGroupCollection = {
__typename?: 'SavedViewGroupCollection';
cursor?: Maybe<Scalars['String']['output']>;
items: Array<SavedViewGroup>;
totalCount: Scalars['Int']['output'];
};
export type SavedViewGroupViewsInput = {
cursor?: InputMaybe<Scalars['String']['input']>;
limit?: InputMaybe<Scalars['Int']['input']>;
/** Whether to only views authored by the current user */
onlyAuthored?: InputMaybe<Scalars['Boolean']['input']>;
/** Whether to only include views matching this search term */
search?: InputMaybe<Scalars['String']['input']>;
/**
* Optionally specify sort by field. Default: updatedAt
* Options: updatedAt, createdAt, name
*/
sortBy?: InputMaybe<Scalars['String']['input']>;
/** Optionally specify sort direction. Default: descending */
sortDirection?: InputMaybe<SortDirection>;
};
export type SavedViewGroupsInput = {
cursor?: InputMaybe<Scalars['String']['input']>;
limit?: InputMaybe<Scalars['Int']['input']>;
/** Whether to only include groups w/ views authored by the current user */
onlyAuthored?: InputMaybe<Scalars['Boolean']['input']>;
/** Viewer resource ID string that identifies which resources should be loaded */
resourceIdString: Scalars['String']['input'];
/** Whether to only include groups that have views matching this search term */
search?: InputMaybe<Scalars['String']['input']>;
};
export type SavedViewMutations = {
__typename?: 'SavedViewMutations';
createGroup: SavedViewGroup;
createView: SavedView;
};
export type SavedViewMutationsCreateGroupArgs = {
input: CreateSavedViewGroupInput;
};
export type SavedViewMutationsCreateViewArgs = {
input: CreateSavedViewInput;
};
export const SavedViewVisibility = {
AuthorOnly: 'authorOnly',
Public: 'public'
} as const;
export type SavedViewVisibility = typeof SavedViewVisibility[keyof typeof SavedViewVisibility];
/** Available scopes. */
export type Scope = {
__typename?: 'Scope';
@@ -5546,6 +5717,8 @@ export type ResolversTypes = {
CreateCommentReplyInput: CreateCommentReplyInput;
CreateEmbedTokenReturn: ResolverTypeWrapper<Omit<CreateEmbedTokenReturn, 'tokenMetadata'> & { tokenMetadata: ResolversTypes['EmbedToken'] }>;
CreateModelInput: CreateModelInput;
CreateSavedViewGroupInput: CreateSavedViewGroupInput;
CreateSavedViewInput: CreateSavedViewInput;
CreateServerRegionInput: CreateServerRegionInput;
CreateUserEmailInput: CreateUserEmailInput;
CreateVersionInput: CreateVersionInput;
@@ -5575,6 +5748,7 @@ export type ResolversTypes = {
GenerateFileUploadUrlInput: GenerateFileUploadUrlInput;
GenerateFileUploadUrlOutput: ResolverTypeWrapper<GenerateFileUploadUrlOutput>;
GetModelUploadsInput: GetModelUploadsInput;
GetUngroupedViewGroupInput: GetUngroupedViewGroupInput;
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
Int: ResolverTypeWrapper<Scalars['Int']['output']>;
InvitableCollaboratorsFilter: InvitableCollaboratorsFilter;
@@ -5609,7 +5783,7 @@ export type ResolversTypes = {
PendingStreamCollaborator: ResolverTypeWrapper<PendingStreamCollaboratorGraphQLReturn>;
PendingWorkspaceCollaborator: ResolverTypeWrapper<PendingWorkspaceCollaboratorGraphQLReturn>;
PendingWorkspaceCollaboratorsFilter: PendingWorkspaceCollaboratorsFilter;
PermissionCheckResult: ResolverTypeWrapper<PermissionCheckResult>;
PermissionCheckResult: ResolverTypeWrapper<PermissionCheckResultGraphQLReturn>;
Price: ResolverTypeWrapper<PriceGraphQLReturn>;
Project: ResolverTypeWrapper<ProjectGraphQLReturn>;
ProjectAccessRequest: ResolverTypeWrapper<ProjectAccessRequestGraphQLReturn>;
@@ -5663,6 +5837,14 @@ export type ResolversTypes = {
ResourceType: ResourceType;
Role: ResolverTypeWrapper<Role>;
RootPermissionChecks: ResolverTypeWrapper<RootPermissionChecksGraphQLReturn>;
SavedView: ResolverTypeWrapper<SavedViewGraphQLReturn>;
SavedViewCollection: ResolverTypeWrapper<Omit<SavedViewCollection, 'items'> & { items: Array<ResolversTypes['SavedView']> }>;
SavedViewGroup: ResolverTypeWrapper<SavedViewGroupGraphQLReturn>;
SavedViewGroupCollection: ResolverTypeWrapper<Omit<SavedViewGroupCollection, 'items'> & { items: Array<ResolversTypes['SavedViewGroup']> }>;
SavedViewGroupViewsInput: SavedViewGroupViewsInput;
SavedViewGroupsInput: SavedViewGroupsInput;
SavedViewMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
SavedViewVisibility: SavedViewVisibility;
Scope: ResolverTypeWrapper<Scope>;
ServerApp: ResolverTypeWrapper<ServerAppGraphQLReturn>;
ServerAppListItem: ResolverTypeWrapper<ServerAppListItemGraphQLReturn>;
@@ -5898,6 +6080,8 @@ export type ResolversParentTypes = {
CreateCommentReplyInput: CreateCommentReplyInput;
CreateEmbedTokenReturn: Omit<CreateEmbedTokenReturn, 'tokenMetadata'> & { tokenMetadata: ResolversParentTypes['EmbedToken'] };
CreateModelInput: CreateModelInput;
CreateSavedViewGroupInput: CreateSavedViewGroupInput;
CreateSavedViewInput: CreateSavedViewInput;
CreateServerRegionInput: CreateServerRegionInput;
CreateUserEmailInput: CreateUserEmailInput;
CreateVersionInput: CreateVersionInput;
@@ -5925,6 +6109,7 @@ export type ResolversParentTypes = {
GenerateFileUploadUrlInput: GenerateFileUploadUrlInput;
GenerateFileUploadUrlOutput: GenerateFileUploadUrlOutput;
GetModelUploadsInput: GetModelUploadsInput;
GetUngroupedViewGroupInput: GetUngroupedViewGroupInput;
ID: Scalars['ID']['output'];
Int: Scalars['Int']['output'];
InvitableCollaboratorsFilter: InvitableCollaboratorsFilter;
@@ -5957,7 +6142,7 @@ export type ResolversParentTypes = {
PendingStreamCollaborator: PendingStreamCollaboratorGraphQLReturn;
PendingWorkspaceCollaborator: PendingWorkspaceCollaboratorGraphQLReturn;
PendingWorkspaceCollaboratorsFilter: PendingWorkspaceCollaboratorsFilter;
PermissionCheckResult: PermissionCheckResult;
PermissionCheckResult: PermissionCheckResultGraphQLReturn;
Price: PriceGraphQLReturn;
Project: ProjectGraphQLReturn;
ProjectAccessRequest: ProjectAccessRequestGraphQLReturn;
@@ -6000,6 +6185,13 @@ export type ResolversParentTypes = {
ResourceIdentifierInput: ResourceIdentifierInput;
Role: Role;
RootPermissionChecks: RootPermissionChecksGraphQLReturn;
SavedView: SavedViewGraphQLReturn;
SavedViewCollection: Omit<SavedViewCollection, 'items'> & { items: Array<ResolversParentTypes['SavedView']> };
SavedViewGroup: SavedViewGroupGraphQLReturn;
SavedViewGroupCollection: Omit<SavedViewGroupCollection, 'items'> & { items: Array<ResolversParentTypes['SavedViewGroup']> };
SavedViewGroupViewsInput: SavedViewGroupViewsInput;
SavedViewGroupsInput: SavedViewGroupsInput;
SavedViewMutations: MutationsObjectGraphQLReturn;
Scope: Scope;
ServerApp: ServerAppGraphQLReturn;
ServerAppListItem: ServerAppListItemGraphQLReturn;
@@ -6959,6 +7151,7 @@ export type PendingWorkspaceCollaboratorResolvers<ContextType = GraphQLContext,
export type PermissionCheckResultResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['PermissionCheckResult'] = ResolversParentTypes['PermissionCheckResult']> = {
authorized?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
code?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
errorMessage?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
message?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
payload?: Resolver<Maybe<ResolversTypes['JSONObject']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@@ -6999,8 +7192,12 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
pendingImportedModels?: Resolver<Array<ResolversTypes['FileUpload']>, ParentType, ContextType, RequireFields<ProjectPendingImportedModelsArgs, 'limit'>>;
permissions?: Resolver<ResolversTypes['ProjectPermissionChecks'], ParentType, ContextType>;
role?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
savedView?: Resolver<ResolversTypes['SavedView'], ParentType, ContextType, RequireFields<ProjectSavedViewArgs, 'id'>>;
savedViewGroup?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType, RequireFields<ProjectSavedViewGroupArgs, 'id'>>;
savedViewGroups?: Resolver<ResolversTypes['SavedViewGroupCollection'], ParentType, ContextType, RequireFields<ProjectSavedViewGroupsArgs, 'input'>>;
sourceApps?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
team?: Resolver<Array<ResolversTypes['ProjectCollaborator']>, ParentType, ContextType>;
ungroupedViewGroup?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType, RequireFields<ProjectUngroupedViewGroupArgs, 'input'>>;
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
version?: Resolver<ResolversTypes['Version'], ParentType, ContextType, RequireFields<ProjectVersionArgs, 'id'>>;
versions?: Resolver<ResolversTypes['VersionCollection'], ParentType, ContextType, RequireFields<ProjectVersionsArgs, 'limit'>>;
@@ -7124,6 +7321,7 @@ export type ProjectMutationsResolvers<ContextType = GraphQLContext, ParentType e
leave?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectMutationsLeaveArgs, 'id'>>;
revokeEmbedToken?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectMutationsRevokeEmbedTokenArgs, 'projectId' | 'token'>>;
revokeEmbedTokens?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<ProjectMutationsRevokeEmbedTokensArgs, 'projectId'>>;
savedViewMutations?: Resolver<ResolversTypes['SavedViewMutations'], ParentType, ContextType>;
update?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<ProjectMutationsUpdateArgs, 'update'>>;
updateRole?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<ProjectMutationsUpdateRoleArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@@ -7149,6 +7347,7 @@ export type ProjectPermissionChecksResolvers<ContextType = GraphQLContext, Paren
canCreateComment?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canCreateEmbedTokens?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canCreateModel?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canCreateSavedView?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canDelete?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canInvite?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
canLeave?: Resolver<ResolversTypes['PermissionCheckResult'], ParentType, ContextType>;
@@ -7260,6 +7459,57 @@ export type RootPermissionChecksResolvers<ContextType = GraphQLContext, ParentTy
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type SavedViewResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['SavedView'] = ResolversParentTypes['SavedView']> = {
author?: Resolver<Maybe<ResolversTypes['LimitedUser']>, ParentType, ContextType>;
createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
group?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType>;
groupId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isHomeView?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
position?: Resolver<ResolversTypes['Float'], ParentType, ContextType>;
projectId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
resourceIdString?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
resourceIds?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
screenshot?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
viewerState?: Resolver<ResolversTypes['JSONObject'], ParentType, ContextType>;
visibility?: Resolver<ResolversTypes['SavedViewVisibility'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type SavedViewCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['SavedViewCollection'] = ResolversParentTypes['SavedViewCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['SavedView']>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type SavedViewGroupResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['SavedViewGroup'] = ResolversParentTypes['SavedViewGroup']> = {
groupId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
isUngroupedViewsGroup?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
projectId?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
resourceIds?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
title?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
views?: Resolver<ResolversTypes['SavedViewCollection'], ParentType, ContextType, RequireFields<SavedViewGroupViewsArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type SavedViewGroupCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['SavedViewGroupCollection'] = ResolversParentTypes['SavedViewGroupCollection']> = {
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
items?: Resolver<Array<ResolversTypes['SavedViewGroup']>, ParentType, ContextType>;
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type SavedViewMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['SavedViewMutations'] = ResolversParentTypes['SavedViewMutations']> = {
createGroup?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType, RequireFields<SavedViewMutationsCreateGroupArgs, 'input'>>;
createView?: Resolver<ResolversTypes['SavedView'], ParentType, ContextType, RequireFields<SavedViewMutationsCreateViewArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
export type ScopeResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['Scope'] = ResolversParentTypes['Scope']> = {
description?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -8141,6 +8391,11 @@ export type Resolvers<ContextType = GraphQLContext> = {
ResourceIdentifier?: ResourceIdentifierResolvers<ContextType>;
Role?: RoleResolvers<ContextType>;
RootPermissionChecks?: RootPermissionChecksResolvers<ContextType>;
SavedView?: SavedViewResolvers<ContextType>;
SavedViewCollection?: SavedViewCollectionResolvers<ContextType>;
SavedViewGroup?: SavedViewGroupResolvers<ContextType>;
SavedViewGroupCollection?: SavedViewGroupCollectionResolvers<ContextType>;
SavedViewMutations?: SavedViewMutationsResolvers<ContextType>;
Scope?: ScopeResolvers<ContextType>;
ServerApp?: ServerAppResolvers<ContextType>;
ServerAppListItem?: ServerAppListItemResolvers<ContextType>;
@@ -8506,6 +8761,60 @@ export type CrossSyncClientTestQueryVariables = Exact<{ [key: string]: never; }>
export type CrossSyncClientTestQuery = { __typename?: 'Query', _?: string | null };
export type BasicSavedViewFragment = { __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } };
export type BasicSavedViewGroupFragment = { __typename?: 'SavedViewGroup', id: string, projectId: string, resourceIds: Array<string>, title: string, isUngroupedViewsGroup: boolean, views: { __typename?: 'SavedViewCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } }> } };
export type CreateSavedViewMutationVariables = Exact<{
input: CreateSavedViewInput;
}>;
export type CreateSavedViewMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', savedViewMutations: { __typename?: 'SavedViewMutations', createView: { __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } } } } };
export type CreateSavedViewGroupMutationVariables = Exact<{
input: CreateSavedViewGroupInput;
viewsInput?: SavedViewGroupViewsInput;
}>;
export type CreateSavedViewGroupMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', savedViewMutations: { __typename?: 'SavedViewMutations', createGroup: { __typename?: 'SavedViewGroup', id: string, projectId: string, resourceIds: Array<string>, title: string, isUngroupedViewsGroup: boolean, views: { __typename?: 'SavedViewCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } }> } } } } };
export type GetProjectSavedViewGroupsQueryVariables = Exact<{
projectId: Scalars['String']['input'];
input: SavedViewGroupsInput;
viewsInput?: SavedViewGroupViewsInput;
}>;
export type GetProjectSavedViewGroupsQuery = { __typename?: 'Query', project: { __typename?: 'Project', savedViewGroups: { __typename?: 'SavedViewGroupCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'SavedViewGroup', id: string, projectId: string, resourceIds: Array<string>, title: string, isUngroupedViewsGroup: boolean, views: { __typename?: 'SavedViewCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } }> } }> } } };
export type GetProjectSavedViewGroupQueryVariables = Exact<{
projectId: Scalars['String']['input'];
groupId: Scalars['ID']['input'];
viewsInput?: SavedViewGroupViewsInput;
}>;
export type GetProjectSavedViewGroupQuery = { __typename?: 'Query', project: { __typename?: 'Project', savedViewGroup: { __typename?: 'SavedViewGroup', id: string, projectId: string, resourceIds: Array<string>, title: string, isUngroupedViewsGroup: boolean, views: { __typename?: 'SavedViewCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } }> } } } };
export type GetProjectUngroupedViewGroupQueryVariables = Exact<{
projectId: Scalars['String']['input'];
input: GetUngroupedViewGroupInput;
viewsInput?: SavedViewGroupViewsInput;
}>;
export type GetProjectUngroupedViewGroupQuery = { __typename?: 'Query', project: { __typename?: 'Project', ungroupedViewGroup: { __typename?: 'SavedViewGroup', id: string, projectId: string, resourceIds: Array<string>, title: string, isUngroupedViewsGroup: boolean, views: { __typename?: 'SavedViewCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } }> } } } };
export type GetProjectSavedViewQueryVariables = Exact<{
projectId: Scalars['String']['input'];
viewId: Scalars['ID']['input'];
}>;
export type GetProjectSavedViewQuery = { __typename?: 'Query', project: { __typename?: 'Project', savedView: { __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } } } };
export type BasicWorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: Date, createdAt: Date, role?: string | null, readOnly: boolean };
export type BasicPendingWorkspaceCollaboratorFragment = { __typename?: 'PendingWorkspaceCollaborator', id: string, inviteId: string, title: string, role: string, token?: string | null, workspace: { __typename?: 'LimitedWorkspace', id: string, name: string }, invitedBy: { __typename?: 'LimitedUser', id: string, name: string }, user?: { __typename?: 'LimitedUser', id: string, name: string } | null };
@@ -8710,9 +9019,9 @@ export type GetProjectInvitableCollaboratorsQueryVariables = Exact<{
export type GetProjectInvitableCollaboratorsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, name: string, invitableCollaborators: { __typename?: 'WorkspaceCollaboratorCollection', totalCount: number, items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, user: { __typename?: 'LimitedUser', name: string } }> } } };
export type FullPermissionCheckResultFragment = { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null };
export type FullPermissionCheckResultFragment = { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null };
export type ProjectImplicitRoleCheckFragment = { __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null } } };
export type ProjectImplicitRoleCheckFragment = { __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null } } };
export type GetUserWorkspaceAccessQueryVariables = Exact<{
id: Scalars['String']['input'];
@@ -8729,7 +9038,7 @@ export type GetUserWorkspaceProjectsWithAccessChecksQueryVariables = Exact<{
}>;
export type GetUserWorkspaceProjectsWithAccessChecksQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', role?: string | null, seatType?: WorkspaceSeatType | null, id: string, name: string, slug: string, updatedAt: Date, createdAt: Date, readOnly: boolean, projects: { __typename?: 'ProjectCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null } } }> } } };
export type GetUserWorkspaceProjectsWithAccessChecksQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', role?: string | null, seatType?: WorkspaceSeatType | null, id: string, name: string, slug: string, updatedAt: Date, createdAt: Date, readOnly: boolean, projects: { __typename?: 'ProjectCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null } } }> } } };
export type GetUserProjectsWithAccessChecksQueryVariables = Exact<{
limit?: InputMaybe<Scalars['Int']['input']>;
@@ -8738,7 +9047,7 @@ export type GetUserProjectsWithAccessChecksQueryVariables = Exact<{
}>;
export type GetUserProjectsWithAccessChecksQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, projects: { __typename?: 'UserProjectCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null } } }> } } | null };
export type GetUserProjectsWithAccessChecksQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', id: string, projects: { __typename?: 'UserProjectCollection', cursor?: string | null, totalCount: number, items: Array<{ __typename?: 'Project', id: string, role?: string | null, permissions: { __typename?: 'ProjectPermissionChecks', canRead: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canReadSettings: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canReadWebhooks: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null }, canCreateModel: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null, errorMessage?: string | null } } }> } } | null };
export type BasicStreamAccessRequestFieldsFragment = { __typename?: 'StreamAccessRequest', id: string, requesterId: string, streamId: string, createdAt: Date, requester: { __typename?: 'LimitedUser', id: string, name: string } };
@@ -9565,11 +9874,13 @@ export const LimitedPersonalProjectCommentFragmentDoc = {"kind":"Document","defi
export const LimitedPersonalProjectVersionFragmentDoc = {"kind":"Document","definitions":[{"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"}}]}}]}}]}},{"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"}}]}}]}}]} as unknown as DocumentNode<LimitedPersonalProjectVersionFragment, unknown>;
export const LimitedPersonalStreamCommitFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedPersonalStreamCommit"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Commit"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<LimitedPersonalStreamCommitFragment, unknown>;
export const DownloadbleCommentMetadataFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DownloadbleCommentMetadata"},"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":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}}]}}]} as unknown as DocumentNode<DownloadbleCommentMetadataFragment, unknown>;
export const BasicSavedViewFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<BasicSavedViewFragment, unknown>;
export const BasicSavedViewGroupFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<BasicSavedViewGroupFragment, unknown>;
export const BasicWorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}}]} as unknown as DocumentNode<BasicWorkspaceFragment, unknown>;
export const BasicPendingWorkspaceCollaboratorFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<BasicPendingWorkspaceCollaboratorFragment, unknown>;
export const WorkspaceProjectsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceProjects"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCollection"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]} as unknown as DocumentNode<WorkspaceProjectsFragment, unknown>;
export const FullPermissionCheckResultFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}}]} as unknown as DocumentNode<FullPermissionCheckResultFragment, unknown>;
export const ProjectImplicitRoleCheckFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}}]} as unknown as DocumentNode<ProjectImplicitRoleCheckFragment, unknown>;
export const FullPermissionCheckResultFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}}]}}]} as unknown as DocumentNode<FullPermissionCheckResultFragment, unknown>;
export const ProjectImplicitRoleCheckFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}}]}}]} as unknown as DocumentNode<ProjectImplicitRoleCheckFragment, unknown>;
export const BasicStreamAccessRequestFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<BasicStreamAccessRequestFieldsFragment, unknown>;
export const TestAutomateFunctionFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestAutomateFunction"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"repo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"owner"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"releases"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"versionTag"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"inputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"commitId"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"supportedSourceApps"}},{"kind":"Field","name":{"kind":"Name","value":"tags"}}]}}]} as unknown as DocumentNode<TestAutomateFunctionFragment, unknown>;
export const TestAutomationFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestAutomation"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Automation"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"runs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"trigger"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VersionCreatedTrigger"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"version"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"statusMessage"}},{"kind":"Field","name":{"kind":"Name","value":"contextView"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"elapsed"}},{"kind":"Field","name":{"kind":"Name","value":"results"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"currentRevision"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"triggerDefinitions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"VersionCreatedTriggerDefinition"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"functions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"parameters"}},{"kind":"Field","name":{"kind":"Name","value":"release"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"versionTag"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"inputSchema"}},{"kind":"Field","name":{"kind":"Name","value":"commitId"}}]}}]}}]}}]}}]} as unknown as DocumentNode<TestAutomationFragment, unknown>;
@@ -9625,6 +9936,12 @@ export const CrossSyncProjectViewerResourcesDocument = {"kind":"Document","defin
export const CrossSyncDownloadableCommitViewerThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CrossSyncDownloadableCommitViewerThreads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"25"}}],"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalArchivedCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"DownloadbleCommentMetadata"}},{"kind":"Field","name":{"kind":"Name","value":"replies"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"DownloadbleCommentMetadata"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"DownloadbleCommentMetadata"},"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":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}}]}}]} as unknown as DocumentNode<CrossSyncDownloadableCommitViewerThreadsQuery, CrossSyncDownloadableCommitViewerThreadsQueryVariables>;
export const CrossSyncProjectMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CrossSyncProjectMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}},"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":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"100"}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<CrossSyncProjectMetadataQuery, CrossSyncProjectMetadataQueryVariables>;
export const CrossSyncClientTestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CrossSyncClientTest"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"_"}}]}}]} as unknown as DocumentNode<CrossSyncClientTestQuery, CrossSyncClientTestQueryVariables>;
export const CreateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSavedViewInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedViewMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<CreateSavedViewMutation, CreateSavedViewMutationVariables>;
export const CreateSavedViewGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSavedViewGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSavedViewGroupInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupViewsInput"}}},"defaultValue":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"savedViewMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedViewGroup"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}}]} as unknown as DocumentNode<CreateSavedViewGroupMutation, CreateSavedViewGroupMutationVariables>;
export const GetProjectSavedViewGroupsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectSavedViewGroups"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupsInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupViewsInput"}}},"defaultValue":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}]}}],"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":"savedViewGroups"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedViewGroup"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectSavedViewGroupsQuery, GetProjectSavedViewGroupsQueryVariables>;
export const GetProjectSavedViewGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectSavedViewGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"groupId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupViewsInput"}}},"defaultValue":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}]}}],"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":"savedViewGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"groupId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedViewGroup"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectSavedViewGroupQuery, GetProjectSavedViewGroupQueryVariables>;
export const GetProjectUngroupedViewGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectUngroupedViewGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetUngroupedViewGroupInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupViewsInput"}}},"defaultValue":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}]}}],"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":"ungroupedViewGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedViewGroup"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectUngroupedViewGroupQuery, GetProjectUngroupedViewGroupQueryVariables>;
export const GetProjectSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"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":"savedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<GetProjectSavedViewQuery, GetProjectSavedViewQueryVariables>;
export const CreateWorkspaceInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspaceInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<CreateWorkspaceInviteMutation, CreateWorkspaceInviteMutationVariables>;
export const BatchCreateWorkspaceInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"BatchCreateWorkspaceInvites"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"batchCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<BatchCreateWorkspaceInvitesMutation, BatchCreateWorkspaceInvitesMutationVariables>;
export const GetWorkspaceWithTeamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithTeam"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithTeamQuery, GetWorkspaceWithTeamQueryVariables>;
@@ -9653,8 +9970,8 @@ export const UpdateWorkspaceProjectRoleDocument = {"kind":"Document","definition
export const UpdateWorkspaceSeatTypeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceSeatType"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUpdateSeatTypeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSeatType"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"seatType"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<UpdateWorkspaceSeatTypeMutation, UpdateWorkspaceSeatTypeMutationVariables>;
export const GetProjectInvitableCollaboratorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectInvitableCollaborators"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"search"}},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"invitableCollaborators"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"search"},"value":{"kind":"Variable","name":{"kind":"Name","value":"search"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectInvitableCollaboratorsQuery, GetProjectInvitableCollaboratorsQueryVariables>;
export const GetUserWorkspaceAccessDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserWorkspaceAccess"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"seatType"}}]}}]}}]} as unknown as DocumentNode<GetUserWorkspaceAccessQuery, GetUserWorkspaceAccessQueryVariables>;
export const GetUserWorkspaceProjectsWithAccessChecksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserWorkspaceProjectsWithAccessChecks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"seatType"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}}]} as unknown as DocumentNode<GetUserWorkspaceProjectsWithAccessChecksQuery, GetUserWorkspaceProjectsWithAccessChecksQueryVariables>;
export const GetUserProjectsWithAccessChecksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserProjectsWithAccessChecks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}}]} as unknown as DocumentNode<GetUserProjectsWithAccessChecksQuery, GetUserProjectsWithAccessChecksQueryVariables>;
export const GetUserWorkspaceProjectsWithAccessChecksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserWorkspaceProjectsWithAccessChecks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"seatType"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}}]} as unknown as DocumentNode<GetUserWorkspaceProjectsWithAccessChecksQuery, GetUserWorkspaceProjectsWithAccessChecksQueryVariables>;
export const GetUserProjectsWithAccessChecksDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetUserProjectsWithAccessChecks"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UserProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectImplicitRoleCheck"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canRead"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadSettings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canReadWebhooks"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}},{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}}]} as unknown as DocumentNode<GetUserProjectsWithAccessChecksQuery, GetUserProjectsWithAccessChecksQueryVariables>;
export const CreateStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequestCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<CreateStreamAccessRequestMutation, CreateStreamAccessRequestMutationVariables>;
export const GetStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<GetStreamAccessRequestQuery, GetStreamAccessRequestQueryVariables>;
export const GetFullStreamAccessRequestDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFullStreamAccessRequest"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"streamAccessRequest"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"streamId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"}},{"kind":"Field","name":{"kind":"Name","value":"stream"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicStreamAccessRequestFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"StreamAccessRequest"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"requester"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"requesterId"}},{"kind":"Field","name":{"kind":"Name","value":"streamId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]} as unknown as DocumentNode<GetFullStreamAccessRequestQuery, GetFullStreamAccessRequestQueryVariables>;
@@ -10,8 +10,6 @@ import {
} from '@/modules/core/services/branch/retrieval'
import { getServerOrigin } from '@/modules/shared/helpers/envHelper'
import { last } from 'lodash-es'
import { getViewerResourceGroupsFactory } from '@/modules/core/services/commit/viewerResources'
import {
getPaginatedBranchCommitsFactory,
legacyGetPaginatedStreamCommitsFactory
@@ -24,6 +22,7 @@ import {
createBranchFactory,
deleteBranchByIdFactory,
getBranchByIdFactory,
getBranchesByIdsFactory,
getBranchLatestCommitsFactory,
getModelTreeItemsFactory,
getModelTreeItemsFilteredFactory,
@@ -60,6 +59,13 @@ import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
import { withOperationLogging } from '@/observability/domain/businessLogging'
import { getViewerResourceGroupsFactory } from '@/modules/viewer/services/viewerResources'
import { NotFoundError } from '@/modules/shared/errors'
import {
isModelResource,
resourceBuilder,
ViewerModelResource
} from '@speckle/shared/viewer/route'
export default {
User: {
@@ -164,7 +170,11 @@ export default {
}
)
},
async viewerResources(parent, { resourceIdString, loadedVersionsOnly }) {
async viewerResources(
parent,
{ resourceIdString, loadedVersionsOnly, savedViewId },
ctx
) {
const projectDB = await getProjectDbClient({ projectId: parent.id })
const getStreamObjects = getStreamObjectsFactory({ db: projectDB })
const getViewerResourceGroups = getViewerResourceGroupsFactory({
@@ -172,8 +182,45 @@ export default {
getBranchLatestCommits: getBranchLatestCommitsFactory({ db: projectDB }),
getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDB }),
getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDB }),
getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDB })
getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDB }),
getBranchesByIds: getBranchesByIdsFactory({ db: projectDB })
})
// Saved View: By default load already specified versions were available,
// otherwise load latest versions
if (savedViewId) {
const savedView = await ctx.loaders
.forRegion({ db: projectDB })
.savedViews.getSavedViews.load({ viewId: savedViewId, projectId: parent.id })
if (!savedView) {
throw new NotFoundError(
`Saved view with ID ${savedViewId} not found in project ${parent.id}`
)
}
const savedViewResources = resourceBuilder().addFromString(
savedView.resourceIds
)
const baseResources = resourceBuilder().addFromString(resourceIdString)
const finalSavedViewResources = savedViewResources.map((r) => {
if (!isModelResource(r) || !r.versionId) {
return r
}
const matchingBaseResource = baseResources
.filter(isModelResource)
.find((r2) => {
return r2.modelId === r.modelId
})
return new ViewerModelResource(r.modelId, matchingBaseResource?.versionId)
})
resourceIdString = resourceBuilder()
.addResources(finalSavedViewResources)
.toString()
}
return await getViewerResourceGroups({
projectId: parent.id,
resourceIdString,
@@ -20,6 +20,9 @@ export default {
User: {
permissions: () => ({})
},
PermissionCheckResult: {
errorMessage: (parent) => (parent.authorized ? undefined : parent.message)
},
ProjectPermissionChecks: {
canCreateModel: async (parent, _args, ctx) => {
const canCreateModel = await ctx.authPolicies.project.model.canCreate({
+1 -1
View File
@@ -1,4 +1,4 @@
import { graphSchema } from '@/modules'
import { graphSchema } from '@/modules/index'
const schema = await graphSchema()
export default schema
+26 -2
View File
@@ -2,7 +2,7 @@ import { BadRequestError } from '@/modules/shared/errors'
import { isGraphQLError } from '@/modules/shared/helpers/graphqlHelper'
import type { ApolloServerOptions, BaseContext } from '@apollo/server'
import { ensureError } from '@speckle/shared'
import { omit } from 'lodash-es'
import { get, isArray, isBoolean, isNumber, isString, omit } from 'lodash-es'
import VError from 'verror'
import { ZodError } from 'zod'
import { fromZodError } from 'zod-validation-error'
@@ -13,6 +13,19 @@ import { fromZodError } from 'zod-validation-error'
*/
const VERROR_TRASH_PROPS = ['jse_shortmsg', 'jse_cause', 'jse_info']
/**
* Add pino-pretty like formatting
*/
const pinoPretty = (log: object, msg: string) =>
msg.replace(/{([^{}]+)}/g, (match: string, p1: string) => {
const val = get(log, p1)
if (val === undefined) return match
const formattedValue =
isString(val) || isNumber(val) || isBoolean(val) ? val : JSON.stringify(val)
return formattedValue as string
})
/**
* Builds apollo server error formatter
*/
@@ -64,10 +77,21 @@ export function buildErrorFormatter(params: {
? VError.fullStack(realError)
: ensureError(realError).stack
} else {
delete extensions.exception.stacktrace
delete extensions.stacktrace
}
}
// Fix error message to work w/ pino templating
writableFormattedError.message = pinoPretty(
extensions,
writableFormattedError.message
)
if (extensions.stacktrace && isArray(extensions.stacktrace)) {
extensions.stacktrace = extensions.stacktrace.map((stack: string) =>
pinoPretty(extensions, stack)
)
}
return {
message: writableFormattedError.message,
locations: writableFormattedError.locations,
@@ -21,6 +21,7 @@ import type {
UserRecord
} from '@/modules/core/helpers/types'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import type { GraphqlPermissionCheckResult } from '@speckle/shared/authz'
/**
* The types of objects we return in resolvers often don't have the exact type as the object in the schema.
@@ -157,3 +158,5 @@ export type VersionPermissionChecksGraphQLReturn = {
}
export type EmbedTokenGraphQLReturn = EmbedApiTokenWithMetadata
export type PermissionCheckResultGraphQLReturn = GraphqlPermissionCheckResult
@@ -45,3 +45,14 @@ export const mapDbToGqlProjectVisibility = (
throwUncoveredError(visibility)
}
}
export const mapGqlToDbSortDirection = (direction: 'ASC' | 'DESC'): 'asc' | 'desc' => {
switch (direction) {
case 'ASC':
return 'asc'
case 'DESC':
return 'desc'
default:
throwUncoveredError(direction)
}
}
@@ -1,40 +1,23 @@
import type {
GetCommentsResources,
GetViewerResourceGroups,
GetViewerResourceItemsUngrouped,
GetViewerResourcesForComment,
GetViewerResourcesForComments,
GetViewerResourcesFromLegacyIdentifiers
} from '@/modules/comments/domain/operations'
import type {
GetBranchLatestCommits,
GetStreamBranchesByName
} from '@/modules/core/domain/branches/operations'
import type {
GetAllBranchCommits,
GetCommitsAndTheirBranchIds,
GetSpecificBranchCommits
} from '@/modules/core/domain/commits/operations'
import type { GetStreamObjects } from '@/modules/core/domain/objects/operations'
import type { GetCommitsAndTheirBranchIds } from '@/modules/core/domain/commits/operations'
import type {
ResourceIdentifier,
ResourceIdentifierInput,
ViewerResourceGroup,
ViewerResourceItem,
ViewerUpdateTrackingTarget
ViewerResourceItem
} from '@/modules/core/graph/generated/graphql'
import { ResourceType } from '@/modules/core/graph/generated/graphql'
import type { CommitRecord } from '@/modules/core/helpers/types'
import type { Optional } from '@speckle/shared'
import type { GetObjectResourceGroupsDeps } from '@/modules/viewer/services/viewerResources'
import {
getObjectResourceGroupsFactory,
isResourceItemEqual
} from '@/modules/viewer/services/viewerResources'
import { SpeckleViewer } from '@speckle/shared'
import { flatten, keyBy, reduce, uniq, uniqWith } from 'lodash-es'
function isResourceItemEqual(a: ViewerResourceItem, b: ViewerResourceItem) {
if (a.modelId !== b.modelId) return false
if (a.objectId !== b.objectId) return false
if (a.versionId !== b.versionId) return false
return true
}
import { reduce, uniqWith } from 'lodash-es'
function isResourceIdentifierEqual(
a: ResourceIdentifier | ResourceIdentifierInput,
@@ -45,328 +28,6 @@ function isResourceIdentifierEqual(
return true
}
type GetObjectResourceGroupsDeps = {
getStreamObjects: GetStreamObjects
}
const getObjectResourceGroupsFactory =
(deps: GetObjectResourceGroupsDeps) =>
async (
projectId: string,
resources: SpeckleViewer.ViewerRoute.ViewerObjectResource[]
) => {
const objects = keyBy(
await deps.getStreamObjects(
projectId,
resources.map((r) => r.objectId)
),
'id'
)
const results: ViewerResourceGroup[] = []
for (const objectResource of resources) {
if (!objects[objectResource.objectId]) continue
results.push({
identifier: objectResource.toString(),
items: [{ modelId: null, versionId: null, objectId: objectResource.objectId }]
})
}
return results
}
type GetVersionResourceGroupsIncludingAllVersionsFactoryDeps = {
getStreamBranchesByName: GetStreamBranchesByName
getAllBranchCommits: GetAllBranchCommits
}
const getVersionResourceGroupsIncludingAllVersionsFactory =
(deps: GetVersionResourceGroupsIncludingAllVersionsFactoryDeps) =>
async (
projectId: string,
params: {
modelResources?: SpeckleViewer.ViewerRoute.ViewerModelResource[]
folderResources?: SpeckleViewer.ViewerRoute.ViewerModelFolderResource[]
}
) => {
// by default we pull all versions of all relevant branches, but if loadedVersionsOnly is set, we only pull
// specifically requested versions (if version isn't set in identifier, then latest version)
const { modelResources = [], folderResources = [] } = params
const results: ViewerResourceGroup[] = []
const foldersModels = await deps.getStreamBranchesByName(
projectId,
folderResources.map((r) => r.folderName),
{ startsWithName: true }
)
const allBranchIds = [
...foldersModels.map((m) => m.id),
...modelResources.map((m) => m.modelId)
]
// get all versions of all referenced branches
const branchCommits = await deps.getAllBranchCommits({ branchIds: allBranchIds })
for (const folderResource of folderResources) {
const prefix = folderResource.folderName
const folderModels = foldersModels.filter((m) =>
m.name.toLowerCase().startsWith(prefix)
)
if (!folderModels.length) continue
const items: ViewerResourceItem[] = []
for (const folderModel of folderModels) {
const modelVersions = branchCommits[folderModel.id]
if (!modelVersions?.length) continue
for (const modelVersion of modelVersions) {
items.push({
modelId: folderModel.id,
versionId: modelVersion.id,
objectId: modelVersion.referencedObject
})
}
}
results.push({
identifier: folderResource.toString(),
items
})
}
for (const modelResource of modelResources) {
const modelVersions = branchCommits[modelResource.modelId] || []
const items: ViewerResourceItem[] = []
for (const modelVersion of modelVersions) {
items.push({
modelId: modelResource.modelId,
versionId: modelVersion.id,
objectId: modelVersion.referencedObject
})
}
results.push({
identifier: modelResource.toString(),
items
})
}
return results
}
type GetVersionResourceGroupsLoadedVersionsOnlyDeps = {
getStreamBranchesByName: GetStreamBranchesByName
getSpecificBranchCommits: GetSpecificBranchCommits
getBranchLatestCommits: GetBranchLatestCommits
}
const getVersionResourceGroupsLoadedVersionsOnlyFactory =
(deps: GetVersionResourceGroupsLoadedVersionsOnlyDeps) =>
async (
projectId: string,
params: {
modelResources?: SpeckleViewer.ViewerRoute.ViewerModelResource[]
folderResources?: SpeckleViewer.ViewerRoute.ViewerModelFolderResource[]
}
) => {
// by default we pull all versions of all relevant branches, but if loadedVersionsOnly is set, we only pull
// specifically requested versions (if version isn't set in identifier, then latest version)
const { modelResources = [], folderResources = [] } = params
const results: ViewerResourceGroup[] = []
const foldersModels = await deps.getStreamBranchesByName(
projectId,
folderResources.map((r) => r.folderName),
{ startsWithName: true }
)
const specificVersionPairs = modelResources
.filter(
(
r
): r is SpeckleViewer.ViewerRoute.ViewerModelResource & { versionId: string } =>
!!r.versionId
)
.map((r) => ({ branchId: r.modelId, commitId: r.versionId }))
const latestVersionModelIds = uniq([
...modelResources.filter((r) => !r.versionId).map((r) => r.modelId),
...foldersModels.map((m) => m.id)
])
const [specificVersions, latestVersions] = await Promise.all([
deps.getSpecificBranchCommits(specificVersionPairs),
deps.getBranchLatestCommits(latestVersionModelIds)
])
const modelLatestVersions = keyBy(latestVersions, 'branchId')
for (const folderResource of folderResources) {
const prefix = folderResource.folderName
const folderModels = foldersModels.filter((m) =>
m.name.toLowerCase().startsWith(prefix)
)
if (!folderModels.length) continue
const items: ViewerResourceItem[] = []
for (const folderModel of folderModels) {
const latestVersion = modelLatestVersions[folderModel.id]
if (!latestVersion) continue
items.push({
modelId: folderModel.id,
versionId: latestVersion.id,
objectId: latestVersion.referencedObject
})
}
results.push({
identifier: folderResource.toString(),
items
})
}
for (const modelResource of modelResources) {
let item: Optional<CommitRecord & { branchId: string }> = undefined
if (modelResource.versionId) {
item = specificVersions.find(
(v) =>
v.branchId === modelResource.modelId && v.id === modelResource.versionId
)
} else {
item = modelLatestVersions[modelResource.modelId]
}
if (!item) continue
results.push({
identifier: modelResource.toString(),
items: [
{
modelId: item.branchId,
versionId: item.id,
objectId: item.referencedObject
}
]
})
}
return results
}
type GetAllModelsResourceGroupDeps = {
getBranchLatestCommits: GetBranchLatestCommits
}
const getAllModelsResourceGroupFactory =
(deps: GetAllModelsResourceGroupDeps) =>
async (projectId: string): Promise<ViewerResourceGroup> => {
const allBranchCommits = await deps.getBranchLatestCommits(undefined, projectId)
return {
identifier: 'all',
items: allBranchCommits.map(
(c): ViewerResourceItem => ({
modelId: c.branchId,
versionId: c.id,
objectId: c.referencedObject
})
)
}
}
type GetVersionResourceGroupsDeps = GetAllModelsResourceGroupDeps &
GetVersionResourceGroupsLoadedVersionsOnlyDeps &
GetVersionResourceGroupsIncludingAllVersionsFactoryDeps
/**
* Version resources can be resolved 2 ways:
* * Default - Specific version IDs referenced in identifiers are ignored and the identifiers always
* refer to all versions of any referenced branch/branches of folders.
* * Loaded versions only - Identifiers only refer to specific version IDs referenced in resource
* identifiers, or if none are specified then only the latest version is referenced (e.g. in folder
* resources & model resources w/ an empty version ID)
*/
const getVersionResourceGroupsFactory =
(deps: GetVersionResourceGroupsDeps) =>
async (
projectId: string,
params: {
modelResources?: SpeckleViewer.ViewerRoute.ViewerModelResource[]
folderResources?: SpeckleViewer.ViewerRoute.ViewerModelFolderResource[]
allModelsResource?: SpeckleViewer.ViewerRoute.ViewerAllModelsResource
},
loadedVersionsOnly?: boolean
) => {
const allModelsGroup = params.allModelsResource
? await getAllModelsResourceGroupFactory(deps)(projectId)
: null
const groups = loadedVersionsOnly
? await getVersionResourceGroupsLoadedVersionsOnlyFactory(deps)(projectId, params)
: await getVersionResourceGroupsIncludingAllVersionsFactory(deps)(
projectId,
params
)
return [...(allModelsGroup ? [allModelsGroup] : []), ...groups]
}
/**
* Validate requested resource identifiers and build viewer resource groups & items with
* the metadata that the viewer needs to work with these
*/
export const getViewerResourceGroupsFactory =
(
deps: GetObjectResourceGroupsDeps & GetVersionResourceGroupsDeps
): GetViewerResourceGroups =>
async (target: ViewerUpdateTrackingTarget): Promise<ViewerResourceGroup[]> => {
const { resourceIdString, projectId, loadedVersionsOnly } = target
if (!resourceIdString?.trim().length) return []
const resources = SpeckleViewer.ViewerRoute.parseUrlParameters(resourceIdString)
const allModelsResource = resources.find(
SpeckleViewer.ViewerRoute.isAllModelsResource
)
const objectResources = resources.filter(SpeckleViewer.ViewerRoute.isObjectResource)
const modelResources = resources.filter(SpeckleViewer.ViewerRoute.isModelResource)
const folderResources = resources.filter(
SpeckleViewer.ViewerRoute.isModelFolderResource
)
const results: ViewerResourceGroup[] = flatten(
await Promise.all([
getObjectResourceGroupsFactory(deps)(projectId, objectResources),
getVersionResourceGroupsFactory(deps)(
projectId,
{ modelResources, folderResources, allModelsResource },
loadedVersionsOnly || false
)
])
)
return results
}
export const getViewerResourceItemsUngroupedFactory =
(deps: {
getViewerResourceGroups: GetViewerResourceGroups
}): GetViewerResourceItemsUngrouped =>
async (target: ViewerUpdateTrackingTarget): Promise<ViewerResourceItem[]> => {
const { resourceIdString } = target
if (!resourceIdString?.trim().length) return []
let results: ViewerResourceItem[] = []
const groups = await deps.getViewerResourceGroups(target)
for (const group of groups) {
results = results.concat(group.items)
}
return uniqWith(results, isResourceItemEqual)
}
export const getViewerResourcesFromLegacyIdentifiersFactory =
(
deps: {
@@ -450,28 +111,3 @@ export const getViewerResourcesForCommentFactory =
async (projectId: string, commentId: string): Promise<ViewerResourceItem[]> => {
return await getViewerResourcesForCommentsFactory(deps)(projectId, [commentId])
}
/**
* Whether any of the resource items match
*/
export function doViewerResourcesFit(
requestedResources: ViewerResourceItem[],
incomingResources: ViewerResourceItem[]
) {
return incomingResources.some((ir) =>
requestedResources.some((rr) => isResourceItemEqual(ir, rr))
)
}
export function viewerResourcesToString(resources: ViewerResourceItem[]): string {
const builder = SpeckleViewer.ViewerRoute.resourceBuilder()
for (const resource of resources) {
if (resource.modelId && resource.versionId) {
builder.addModel(resource.modelId, resource.versionId)
} else {
builder.addObject(resource.objectId)
}
}
return builder.toString()
}
@@ -17,6 +17,7 @@ import {
import {
createBranchFactory,
getBranchByIdFactory,
getBranchesByIdsFactory,
getBranchLatestCommitsFactory,
getStreamBranchByNameFactory,
getStreamBranchesByNameFactory,
@@ -53,8 +54,6 @@ import { getFirstAdminFactory, getUserFactory } from '@/modules/core/repositorie
import { createBranchAndNotifyFactory } from '@/modules/core/services/branch/management'
import { createCommitByBranchIdFactory } from '@/modules/core/services/commit/management'
import {
getViewerResourceGroupsFactory,
getViewerResourceItemsUngroupedFactory,
getViewerResourcesForCommentFactory,
getViewerResourcesForCommentsFactory,
getViewerResourcesFromLegacyIdentifiersFactory
@@ -69,6 +68,10 @@ import { ensureOnboardingProjectFactory } from '@/modules/cross-server-sync/serv
import { downloadProjectFactory } from '@/modules/cross-server-sync/services/project'
import type { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { getEventBus } from '@/modules/shared/services/eventBus'
import {
getViewerResourceGroupsFactory,
getViewerResourceItemsUngroupedFactory
} from '@/modules/viewer/services/viewerResources'
const crossServerSyncModule: SpeckleModule = {
init() {
@@ -96,7 +99,8 @@ const crossServerSyncModule: SpeckleModule = {
getBranchLatestCommits: getBranchLatestCommitsFactory({ db }),
getStreamBranchesByName: getStreamBranchesByNameFactory({ db }),
getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db }),
getAllBranchCommits: getAllBranchCommitsFactory({ db })
getAllBranchCommits: getAllBranchCommitsFactory({ db }),
getBranchesByIds: getBranchesByIdsFactory({ db })
})
})
const getViewerResourcesFromLegacyIdentifiers =
@@ -81,14 +81,15 @@ const dataLoadersDefinition = defineRequestDataloaders(
const workspacePlans = await getWorkspacePlansByWorkspaceId({
workspaceIds: workspaceIds.slice()
})
const featureFlags = getFeatureFlags()
return workspaceIds.map((workspaceId) => {
const plan = workspacePlans[workspaceId]
if (!plan) return null
const config = {
...WorkspacePaidPlanConfigs,
...WorkspaceUnpaidPlanConfigs
...WorkspacePaidPlanConfigs({ featureFlags }),
...WorkspaceUnpaidPlanConfigs({ featureFlags })
}
return config[plan.name]?.limits || null
})
@@ -3,6 +3,7 @@ import type {
CanWorkspaceAccessFeature,
WorkspaceFeatureAccessFunction
} from '@/modules/gatekeeper/domain/operations'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import { throwUncoveredError, workspacePlanHasAccessToFeature } from '@speckle/shared'
export const canWorkspaceAccessFeatureFactory =
@@ -27,7 +28,8 @@ export const canWorkspaceAccessFeatureFactory =
return workspacePlanHasAccessToFeature({
plan: workspacePlan.name,
feature: workspaceFeature
feature: workspaceFeature,
featureFlags: getFeatureFlags()
})
}
+2 -1
View File
@@ -110,7 +110,8 @@ const getEnabledModuleNames = () => {
'webhooks',
'workspacesCore',
'gatekeeperCore',
'multiregion'
'multiregion',
'viewer'
]
if (FF_AUTOMATE_MODULE_ENABLED) moduleNames.push('automate')
@@ -535,3 +535,6 @@ export const getPreviewServiceMaxQueueBackpressure = (): number => {
export const emailVerificationTimeoutMinutes = (): number => {
return getIntFromEnv('EMAIL_VERIFICATION_TIMEOUT_MINUTES', '5')
}
export const areSavedViewsEnabled = (): boolean =>
getFeatureFlags().FF_SAVED_VIEWS_ENABLED
@@ -0,0 +1,13 @@
import type { ViewerUpdateTrackingTarget } from '@/modules/core/graph/generated/graphql'
import type {
ViewerResourceGroup,
ViewerResourceItem
} from '@/modules/viewer/domain/types/resources'
export type GetViewerResourceGroups = (
target: ViewerUpdateTrackingTarget & { allowEmptyModels?: boolean }
) => Promise<ViewerResourceGroup[]>
export type GetViewerResourceItemsUngrouped = (
target: ViewerUpdateTrackingTarget
) => Promise<ViewerResourceItem[]>
@@ -0,0 +1,168 @@
import type { Collection } from '@/modules/shared/helpers/dbHelper'
import type {
SavedView,
SavedViewGroup,
SavedViewVisibility
} from '@/modules/viewer/domain/types/savedViews'
import type { MaybeNullOrUndefined, NullableKeysToOptional } from '@speckle/shared'
import type { SerializedViewerState } from '@speckle/shared/viewer/state'
import type { Exact, SetOptional } from 'type-fest'
// REPO OPERATIONS:
export type StoreSavedView = <
View extends Exact<
SetOptional<NullableKeysToOptional<SavedView>, 'id' | 'createdAt' | 'updatedAt'>,
View
>
>(params: {
view: View
}) => Promise<SavedView>
export type StoreSavedViewGroup = <
Group extends Exact<
SetOptional<
NullableKeysToOptional<SavedViewGroup>,
'id' | 'createdAt' | 'updatedAt'
>,
Group
>
>(params: {
group: Group
}) => Promise<SavedViewGroup>
export type GetStoredViewCount = (params: { projectId: string }) => Promise<number>
export type GetProjectSavedViewGroupsBaseParams = {
/**
* Falsy means - anonymous user (so no onlyAuthored filtering)
*/
userId?: MaybeNullOrUndefined<string>
projectId: string
resourceIdString: string
onlyAuthored?: MaybeNullOrUndefined<boolean>
search?: MaybeNullOrUndefined<string>
}
export type GetProjectSavedViewGroupsPageParams =
GetProjectSavedViewGroupsBaseParams & {
limit?: MaybeNullOrUndefined<number>
cursor?: MaybeNullOrUndefined<string>
}
export type GetProjectSavedViewGroupsPageItems = (
params: GetProjectSavedViewGroupsPageParams
) => Promise<Omit<Collection<SavedViewGroup>, 'totalCount'>>
export type GetProjectSavedViewGroupsTotalCount = (
params: GetProjectSavedViewGroupsBaseParams
) => Promise<number>
export type GetGroupSavedViewsBaseParams = {
/**
* Falsy means - anonymous user (so no onlyAuthored filtering)
*/
userId?: MaybeNullOrUndefined<string>
projectId: string
groupResourceIdString: string
/**
* Null means a group w/ null id, undefined means - dont filter by group id at all
*/
groupId: MaybeNullOrUndefined<string>
onlyAuthored?: MaybeNullOrUndefined<boolean>
search?: MaybeNullOrUndefined<string>
}
export type GetGroupSavedViewsPageParams = GetGroupSavedViewsBaseParams & {
limit?: MaybeNullOrUndefined<number>
cursor?: MaybeNullOrUndefined<string>
sortDirection?: MaybeNullOrUndefined<'asc' | 'desc'>
sortBy?: MaybeNullOrUndefined<'createdAt' | 'name' | 'updatedAt'>
}
export type GetGroupSavedViewsTotalCount = (
params: GetGroupSavedViewsBaseParams
) => Promise<number>
export type GetGroupSavedViewsPageItems = (
params: GetGroupSavedViewsPageParams
) => Promise<Omit<Collection<SavedView>, 'totalCount'>>
export type GetSavedViewGroup = (params: {
id: string
/**
* If undefined, skip project ID check
*/
projectId: string | undefined
}) => Promise<SavedViewGroup | undefined>
export type GetUngroupedSavedViewsGroup = (params: {
projectId: string
resourceIdString: string
}) => SavedViewGroup
export type RecalculateGroupResourceIds = (params: {
groupId: string
}) => Promise<SavedViewGroup | undefined>
/**
* Get saved groups by IDs and their project IDs
*/
export type GetSavedViewGroups = (params: {
groupIds: Array<{ groupId: string; projectId: string }>
}) => Promise<{
[groupId: string]: SavedViewGroup | undefined
}>
export type GetSavedViews = (params: {
viewIds: Array<{ viewId: string; projectId: string }>
}) => Promise<{
[viewId: string]: SavedView | undefined
}>
// SERVICE OPERATIONS:
export type CreateSavedViewParams = {
input: {
projectId: string
resourceIdString: string
groupId?: MaybeNullOrUndefined<string>
name?: MaybeNullOrUndefined<string>
description?: MaybeNullOrUndefined<string>
/**
* SerializedViewerState that will be validated/formatted before saving.
*/
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
viewerState: unknown | SerializedViewerState
/**
* Base64 encoded screenshot of the view.
*/
screenshot: string
isHomeView?: MaybeNullOrUndefined<boolean>
visibility?: MaybeNullOrUndefined<SavedViewVisibility>
}
authorId: string
}
export type CreateSavedView = (params: CreateSavedViewParams) => Promise<SavedView>
export type CreateSavedViewGroupParams = {
input: {
projectId: string
resourceIdString: string
groupName: string
}
authorId: string
}
export type CreateSavedViewGroup = (
params: CreateSavedViewGroupParams
) => Promise<SavedViewGroup>
export type GetProjectSavedViewGroups = (
params: GetProjectSavedViewGroupsPageParams
) => Promise<Collection<SavedViewGroup>>
export type GetGroupSavedViews = (
params: GetGroupSavedViewsPageParams
) => Promise<Collection<SavedView>>
@@ -0,0 +1,16 @@
import type { Nullable } from '@speckle/shared'
export type ViewerResourceItem = {
/** Null if resource represents an object */
modelId?: Nullable<string>
objectId: string
/** Null if resource represents an object */
versionId?: Nullable<string>
}
export type ViewerResourceGroup = {
/** Resource identifier used to refer to a collection of resource items */
identifier: string
/** Viewer resources that the identifier refers to */
items: Array<ViewerResourceItem>
}
@@ -0,0 +1,62 @@
import type { Nullable, StringEnumValues } from '@speckle/shared'
import { StringEnum } from '@speckle/shared'
import type { VersionedSerializedViewerState } from '@speckle/shared/viewer/state'
export const SavedViewVisibility = StringEnum(['public', 'authorOnly'])
export type SavedViewVisibility = StringEnumValues<typeof SavedViewVisibility>
export type SavedView = {
id: string
name: string
description: Nullable<string>
projectId: string
/**
* Null only if the author deleted their account
*/
authorId: Nullable<string>
groupId: Nullable<string>
/**
* Fully specific/concrete (w/ version Ids) resource ids used to create this view.
*/
resourceIds: string[]
/**
* More abstract resource ids, w/o specific versions, used to group views together. Largely
* only exists because PGSQL can't simply truncate resourceIds in a query in realtime, and we use
* this to find views for groups.
*/
groupResourceIds: string[]
isHomeView: boolean
visibility: SavedViewVisibility
viewerState: VersionedSerializedViewerState
screenshot: string
position: number
createdAt: Date
updatedAt: Date
}
export type SavedViewGroup = {
/**
* Globally unique identifier. If this is the default/unsorted group, the ID will be a static string and it represents a group
* that doesn't actually exist in the database.
*/
id: string
/**
* Null only if the author deleted their account or if default/unsorted group
*/
authorId: Nullable<string>
/**
* Project that the group belongs to
*/
projectId: string
/**
* Resource (model) ids associated w/ this group. This is kept in sync w/ all of the resourceIds for the views in this group too.
* Groups need resourceIds independent of views, because you have to be able to retrieve empty groups too.
*/
resourceIds: string[]
/**
* Null means default/unsorted group
*/
name: Nullable<string>
createdAt: Date
updatedAt: Date
}
@@ -0,0 +1,19 @@
import { BaseError } from '@/modules/shared/errors'
export class SavedViewCreationValidationError extends BaseError {
static code = 'SAVED_VIEW_CREATION_VALIDATION_ERROR'
static defaultMessage = 'Saved view creation failed due to a validation error'
static statusCode = 400
}
export class SavedViewGroupCreationValidationError extends BaseError {
static code = 'SAVED_VIEW_GROUP_CREATION_VALIDATION_ERROR'
static defaultMessage = 'Saved view group creation failed due to a validation error'
static statusCode = 400
}
export class SavedViewInvalidResourceTargetError extends BaseError {
static code = 'SAVED_VIEW_INVALID_RESOURCE_TARGET_ERROR'
static defaultMessage = 'Invalid resource ids specified'
static statusCode = 400
}
@@ -0,0 +1,61 @@
import { defineRequestDataloaders } from '@/modules/shared/helpers/graphqlHelper'
import type {
SavedView,
SavedViewGroup
} from '@/modules/viewer/domain/types/savedViews'
import {
getSavedViewGroupsFactory,
getSavedViewsFactory
} from '@/modules/viewer/repositories/savedViews'
import type { Nullable } from '@speckle/shared'
declare module '@/modules/core/loaders' {
interface ModularizedDataLoaders extends ReturnType<typeof dataLoadersDefinition> {}
}
const dataLoadersDefinition = defineRequestDataloaders(
({ createLoader, deps: { db } }) => {
const getSavedViewGroups = getSavedViewGroupsFactory({ db })
const getSavedViews = getSavedViewsFactory({ db })
return {
savedViews: {
/**
* Get a saved view group by ID. Can also handle unpersisted ungrouped groups, just make sure
* you use their encoded IDs.
*/
getSavedViewGroup: createLoader<
{ groupId: string; projectId: string },
Nullable<SavedViewGroup>,
string
>(
async (ids) => {
const groups = await getSavedViewGroups({ groupIds: ids.slice() })
return ids.map(({ groupId }) => groups[groupId] || null)
},
{
cacheKeyFn: ({ groupId, projectId }) => `${groupId}-${projectId}`
}
),
/**
* Get saved views by their IDs
*/
getSavedViews: createLoader<
{ viewId: string; projectId: string },
Nullable<SavedView>,
string
>(
async (ids) => {
const views = await getSavedViews({ viewIds: ids.slice() })
return ids.map(({ viewId }) => views[viewId] || null)
},
{
cacheKeyFn: ({ viewId, projectId }) => `${viewId}-${projectId}`
}
)
}
}
}
)
export default dataLoadersDefinition
@@ -0,0 +1,292 @@
import { db } from '@/db/knex'
import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types'
import type { Resolvers } from '@/modules/core/graph/generated/graphql'
import { mapGqlToDbSortDirection } from '@/modules/core/helpers/project'
import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token'
import {
getBranchesByIdsFactory,
getBranchLatestCommitsFactory,
getStreamBranchesByNameFactory
} from '@/modules/core/repositories/branches'
import {
getAllBranchCommitsFactory,
getSpecificBranchCommitsFactory
} from '@/modules/core/repositories/commits'
import { getStreamObjectsFactory } from '@/modules/core/repositories/objects'
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import { LogicError, NotFoundError, NotImplementedError } from '@/modules/shared/errors'
import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper'
import { buildDefaultGroupId } from '@/modules/viewer/helpers/savedViews'
import {
getGroupSavedViewsPageItemsFactory,
getGroupSavedViewsTotalCountFactory,
getProjectSavedViewGroupsPageItemsFactory,
getProjectSavedViewGroupsTotalCountFactory,
getSavedViewGroupFactory,
getStoredViewCountFactory,
getUngroupedSavedViewsGroupFactory,
recalculateGroupResourceIdsFactory,
storeSavedViewFactory,
storeSavedViewGroupFactory
} from '@/modules/viewer/repositories/savedViews'
import {
createSavedViewFactory,
createSavedViewGroupFactory,
getGroupSavedViewsFactory,
getProjectSavedViewGroupsFactory
} from '@/modules/viewer/services/savedViewsManagement'
import { getViewerResourceGroupsFactory } from '@/modules/viewer/services/viewerResources'
import { Authz } from '@speckle/shared'
import { parseResourceFromString, resourceBuilder } from '@speckle/shared/viewer/route'
import { formatSerializedViewerState } from '@speckle/shared/viewer/state'
import type { Knex } from 'knex'
import { ungroupedScenesGroupTitle } from '@speckle/shared/saved-views'
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
const buildGetViewerResourceGroups = (params: { projectDb: Knex }) => {
const { projectDb } = params
return getViewerResourceGroupsFactory({
getStreamObjects: getStreamObjectsFactory({ db: projectDb }),
getBranchLatestCommits: getBranchLatestCommitsFactory({ db: projectDb }),
getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDb }),
getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDb }),
getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb }),
getBranchesByIds: getBranchesByIdsFactory({ db: projectDb })
})
}
const resolvers: Resolvers = {
Project: {
async savedViewGroups(parent, args, ctx) {
const { input } = args
const getProjectSavedViewGroups = getProjectSavedViewGroupsFactory({
getProjectSavedViewGroupsPageItems: getProjectSavedViewGroupsPageItemsFactory({
db
}),
getProjectSavedViewGroupsTotalCount: getProjectSavedViewGroupsTotalCountFactory(
{ db }
)
})
return await getProjectSavedViewGroups({
projectId: parent.id,
resourceIdString: input.resourceIdString,
userId: ctx.userId,
onlyAuthored: input.onlyAuthored,
search: input.search,
limit: input.limit,
cursor: input.cursor
})
},
async savedViewGroup(parent, args, ctx) {
const projectDb = await getProjectDbClient({ projectId: parent.id })
const group = await ctx.loaders
.forRegion({ db: projectDb })
.savedViews.getSavedViewGroup.load({
groupId: args.id,
projectId: parent.id
})
if (!group) {
throw new NotFoundError(
`Saved view group with ID ${args.id} not found in project ${parent.id}`
)
}
return group
},
ungroupedViewGroup: async (parent, args) => {
const getDefaultGroup = getUngroupedSavedViewsGroupFactory()
const group = getDefaultGroup({
projectId: parent.id,
resourceIdString: args.input.resourceIdString
})
return group
},
savedView: async (parent, args, ctx) => {
const projectDb = await getProjectDbClient({ projectId: parent.id })
const view = await ctx.loaders
.forRegion({ db: projectDb })
.savedViews.getSavedViews.load({
viewId: args.id,
projectId: parent.id
})
if (!view) {
throw new NotFoundError(
`Saved view with ID ${args.id} not found in project ${parent.id}`
)
}
return view
}
},
SavedView: {
async author(parent, _args, ctx) {
return parent.authorId
? await ctx.loaders.users.getUser.load(parent.authorId)
: null
},
resourceIdString(parent) {
const resourceIds = parent.resourceIds
return resourceBuilder().addFromString(resourceIds.join(',')).toString()
},
viewerState(parent) {
return formatSerializedViewerState(parent.viewerState.state)
},
group: async (parent, _args, ctx) => {
const groupId =
parent.groupId ||
buildDefaultGroupId({
resourceIds: parent.resourceIds,
projectId: parent.projectId
})
const projectDb = await getProjectDbClient({ projectId: parent.projectId })
const group = await ctx.loaders
.forRegion({ db: projectDb })
.savedViews.getSavedViewGroup.load({
groupId,
projectId: parent.projectId
})
if (!group) {
throw new LogicError('Unexpectedly could not resolve a view group')
}
return group
}
},
SavedViewGroup: {
title: (parent) => parent.name || ungroupedScenesGroupTitle,
isUngroupedViewsGroup: (parent) => parent.name === null,
groupId: (parent) => (parent.name ? parent.id : null),
async views(parent, args, ctx) {
const { input } = args
const getGroupSavedViews = getGroupSavedViewsFactory({
getGroupSavedViewsPageItems: getGroupSavedViewsPageItemsFactory({ db }),
getGroupSavedViewsTotalCount: getGroupSavedViewsTotalCountFactory({ db })
})
const allowedSortBy = <const>['createdAt', 'name', 'updatedAt']
const sortBy = input.sortBy
? allowedSortBy.find((s) => s === input.sortBy)
: undefined
return await getGroupSavedViews({
projectId: parent.projectId,
groupResourceIdString: resourceBuilder()
.addResources(parent.resourceIds.map(parseResourceFromString))
.toString(),
userId: ctx.userId,
groupId: parent.name ? parent.id : null,
onlyAuthored: input.onlyAuthored,
search: input.search,
limit: input.limit,
cursor: input.cursor,
sortDirection: input.sortDirection
? mapGqlToDbSortDirection(input.sortDirection)
: undefined,
sortBy
})
}
},
ProjectMutations: {
savedViewMutations: () => ({})
},
SavedViewMutations: {
createView: async (_parent, args, ctx) => {
const projectId = args.input.projectId
throwIfResourceAccessNotAllowed({
resourceId: projectId,
resourceType: TokenResourceIdentifierType.Project,
resourceAccessRules: ctx.resourceAccessRules
})
const canCreate = await ctx.authPolicies.project.savedViews.canCreate({
userId: ctx.userId,
projectId
})
throwIfAuthNotOk(canCreate)
const projectDb = await getProjectDbClient({ projectId })
const createSavedView = createSavedViewFactory({
getViewerResourceGroups: buildGetViewerResourceGroups({ projectDb }),
getStoredViewCount: getStoredViewCountFactory({ db: projectDb }),
storeSavedView: storeSavedViewFactory({ db: projectDb }),
getSavedViewGroup: getSavedViewGroupFactory({ db: projectDb }),
recalculateGroupResourceIds: recalculateGroupResourceIdsFactory({
db: projectDb
})
})
return await createSavedView({ input: args.input, authorId: ctx.userId! })
},
createGroup: async (_parent, args, ctx) => {
const projectId = args.input.projectId
throwIfResourceAccessNotAllowed({
resourceId: projectId,
resourceType: TokenResourceIdentifierType.Project,
resourceAccessRules: ctx.resourceAccessRules
})
const canCreate = await ctx.authPolicies.project.savedViews.canCreate({
userId: ctx.userId,
projectId
})
throwIfAuthNotOk(canCreate)
const projectDb = await getProjectDbClient({ projectId })
const createSavedViewGroup = createSavedViewGroupFactory({
storeSavedViewGroup: storeSavedViewGroupFactory({ db: projectDb }),
getViewerResourceGroups: buildGetViewerResourceGroups({ projectDb })
})
return await createSavedViewGroup({
input: args.input,
authorId: ctx.userId!
})
}
},
ProjectPermissionChecks: {
canCreateSavedView: async (parent, _args, ctx) => {
const projectId = parent.projectId
const canCreate = await ctx.authPolicies.project.savedViews.canCreate({
userId: ctx.userId,
projectId
})
return Authz.toGraphqlResult(canCreate)
}
}
}
const disabledMessage = 'Saved views are disabled on this server'
const disabledResolvers: Resolvers = {
Project: {
savedViewGroups: () => {
throw new NotImplementedError(disabledMessage)
},
savedViewGroup: () => {
throw new NotImplementedError(disabledMessage)
},
ungroupedViewGroup: () => {
throw new NotImplementedError(disabledMessage)
},
savedView: () => {
throw new NotImplementedError(disabledMessage)
}
},
ProjectMutations: {
savedViewMutations: () => {
throw new NotImplementedError(disabledMessage)
}
},
ProjectPermissionChecks: {
canCreateSavedView: () => {
return {
authorized: false,
message: disabledMessage,
code: 'SAVED_VIEWS_DISABLED',
payload: null
}
}
}
}
export default getFeatureFlags().FF_SAVED_VIEWS_ENABLED ? resolvers : disabledResolvers
@@ -0,0 +1,7 @@
import type {
SavedView,
SavedViewGroup
} from '@/modules/viewer/domain/types/savedViews'
export type SavedViewGraphQLReturn = SavedView
export type SavedViewGroupGraphQLReturn = SavedViewGroup
@@ -0,0 +1,71 @@
import { base64Decode, base64Encode } from '@/modules/shared/helpers/cryptoHelper'
import type { Nullable } from '@speckle/shared'
import {
isModelResource,
isObjectResource,
resourceBuilder
} from '@speckle/shared/viewer/route'
import { isObjectLike } from 'lodash-es'
export type DefaultGroupMetadata = {
resourceIds: string[]
projectId: string
name: 'Default Group'
}
export const buildDefaultGroupId = (params: {
resourceIds: string[]
projectId: string
}) => {
const payload: DefaultGroupMetadata = {
resourceIds: formatResourceIdsForGroup(params.resourceIds),
projectId: params.projectId,
name: 'Default Group'
}
const str = JSON.stringify(payload)
return 'default-' + base64Encode(str)
}
export const decodeDefaultGroupId = (id: string): Nullable<DefaultGroupMetadata> => {
try {
if (!id.startsWith('default-')) return null
const json = base64Decode(id.replace('default-', ''))
const obj = JSON.parse(json)
if (
!isObjectLike(obj) ||
!obj.resourceIds ||
!obj.projectId ||
obj.name !== 'Default Group'
) {
throw new Error('Invalid saved view group ID format')
}
return obj as Nullable<DefaultGroupMetadata>
} catch {
// Suppress - not the default group ID
return null
}
}
/**
* Converts a resourceId string into a more abstract format used by groups that disregards
* specific versions of models and objects.
*/
export const formatResourceIdsForGroup = (resourceIdString: string | string[]) => {
resourceIdString = Array.isArray(resourceIdString)
? resourceIdString.join(',')
: resourceIdString
return resourceBuilder()
.addFromString(resourceIdString)
.forEach((r) => {
if (isModelResource(r)) {
// not interested in the specific version ids originally used
r.versionId = undefined
}
})
.filter((r) => {
// filter out any resources that are not ViewerModelResource or ViewerObjectResource
return isModelResource(r) || isObjectResource(r)
})
.map((r) => r.toString())
}
+12
View File
@@ -0,0 +1,12 @@
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
import type { SpeckleModule } from '@/modules/shared/helpers/typeHelper'
import { viewerLogger } from '@/observability/logging'
const viewerModule: SpeckleModule = {
init: async () => {
if (!getFeatureFlags().FF_SAVED_VIEWS_ENABLED) return
viewerLogger.info('🤩 Initializing viewer module...')
}
}
export default viewerModule
@@ -0,0 +1,82 @@
import type { Knex } from 'knex'
const viewsTable = 'saved_views'
const groupsTable = 'saved_view_groups'
// TODO: Validate indexing strategy based on queries
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable(groupsTable, (table) => {
table.string('id').notNullable().primary()
table.string('name').notNullable()
table
.string('projectId')
.notNullable()
.references('id')
.inTable('streams')
.onDelete('CASCADE')
table
.string('authorId')
.nullable()
.references('id')
.inTable('users')
.onDelete('SET NULL') // If the author is deleted, we keep the view but lose the author reference
table.specificType('resourceIds', 'varchar(255)[]').notNullable().defaultTo('{}')
table
.timestamp('createdAt', { precision: 3, useTz: true })
.defaultTo(knex.fn.now())
.notNullable()
table
.timestamp('updatedAt', { precision: 3, useTz: true })
.defaultTo(knex.fn.now())
.notNullable()
})
await knex.schema.createTable(viewsTable, (table) => {
table.string('id').notNullable().primary()
table.string('name').notNullable()
table.text('description').nullable()
table
.string('projectId')
.notNullable()
.references('id')
.inTable('streams')
.onDelete('CASCADE')
table
.string('authorId')
.nullable()
.references('id')
.inTable('users')
.onDelete('SET NULL') // If the author is deleted, we keep the view but lose the author reference
table
.string('groupId')
.nullable()
.references('id')
.inTable(groupsTable)
.onDelete('SET NULL') // If group deleted, ungroup
table.specificType('resourceIds', 'varchar(255)[]').notNullable().defaultTo('{}')
table
.specificType('groupResourceIds', 'varchar(255)[]')
.notNullable()
.defaultTo('{}')
table.boolean('isHomeView').notNullable().defaultTo(false)
table.string('visibility').defaultTo('public').notNullable() // public, authorOnly
table.jsonb('viewerState').notNullable() // SerializedViewerState
table.text('screenshot').notNullable() // Base64 encoded screenshot
table.double('position').defaultTo(0).notNullable() // Used for manual positioning in the UI
table
.timestamp('createdAt', { precision: 3, useTz: true })
.defaultTo(knex.fn.now())
.notNullable()
table
.timestamp('updatedAt', { precision: 3, useTz: true })
.defaultTo(knex.fn.now())
.notNullable()
})
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(viewsTable)
await knex.schema.dropTableIfExists(groupsTable)
}
@@ -0,0 +1,485 @@
import { buildTableHelper } from '@/modules/core/dbSchema'
import { compositeCursorTools } from '@/modules/shared/helpers/dbHelper'
import type {
GetGroupSavedViewsBaseParams,
GetGroupSavedViewsPageItems,
GetGroupSavedViewsTotalCount,
GetProjectSavedViewGroupsBaseParams,
GetProjectSavedViewGroupsPageItems,
GetProjectSavedViewGroupsTotalCount,
GetSavedViewGroups,
GetSavedViewGroup,
GetStoredViewCount,
GetUngroupedSavedViewsGroup,
RecalculateGroupResourceIds,
StoreSavedView,
StoreSavedViewGroup,
GetSavedViews
} from '@/modules/viewer/domain/operations/savedViews'
import {
SavedViewVisibility,
type SavedView,
type SavedViewGroup
} from '@/modules/viewer/domain/types/savedViews'
import type { DefaultGroupMetadata } from '@/modules/viewer/helpers/savedViews'
import {
buildDefaultGroupId,
decodeDefaultGroupId,
formatResourceIdsForGroup
} from '@/modules/viewer/helpers/savedViews'
import { resourceBuilder } from '@speckle/shared/viewer/route'
import cryptoRandomString from 'crypto-random-string'
import dayjs from 'dayjs'
import { type Knex } from 'knex'
import { clamp, isUndefined } from 'lodash-es'
const SavedViews = buildTableHelper('saved_views', [
'id',
'name',
'description',
'projectId',
'authorId',
'groupId',
'resourceIds',
'groupResourceIds',
'isHomeView',
'visibility',
'viewerState',
'screenshot',
'position',
'createdAt',
'updatedAt'
])
const SavedViewGroups = buildTableHelper('saved_view_groups', [
'id',
'authorId',
'projectId',
'resourceIds',
'name',
'createdAt',
'updatedAt'
])
const savedGroupCursorUtils = () =>
compositeCursorTools({
schema: SavedViewGroups,
cols: ['updatedAt', 'id']
})
const generateId = () => cryptoRandomString({ length: 10 })
const buildDefaultGroup = (params: {
resourceIds: string[]
projectId: string
}): SavedViewGroup => {
const { resourceIds, projectId } = params
return {
id: buildDefaultGroupId({ projectId, resourceIds }),
authorId: null,
projectId,
resourceIds,
name: null,
createdAt: dayjs(0).toDate(),
updatedAt: dayjs(0).toDate()
}
}
const tables = {
savedViews: (db: Knex) => db<SavedView>(SavedViews.name),
savedViewGroups: (db: Knex) => db<SavedViewGroup>(SavedViewGroups.name)
}
export const storeSavedViewFactory =
(deps: { db: Knex }): StoreSavedView =>
async ({ view }) => {
const [insertedItem] = await tables.savedViews(deps.db).insert(
{
id: generateId(),
...view
},
'*'
)
return insertedItem
}
export const storeSavedViewGroupFactory =
(deps: { db: Knex }): StoreSavedViewGroup =>
async ({ group }) => {
const [insertedItem] = await tables.savedViewGroups(deps.db).insert(
{
id: generateId(),
...group
},
'*'
)
return insertedItem
}
export const getStoredViewCountFactory =
(deps: { db: Knex }): GetStoredViewCount =>
async ({ projectId }) => {
const [count] = await tables.savedViews(deps.db).where({ projectId }).count()
return parseInt(count.count + '')
}
const getProjectSavedViewGroupsBaseQueryFactory =
(deps: { db: Knex }) => async (params: GetProjectSavedViewGroupsBaseParams) => {
const { projectId, resourceIdString, search, userId } = params
const onlyAuthored = params.onlyAuthored && userId
const isFiltering = search || onlyAuthored
const resourceIds = formatResourceIdsForGroup(resourceIdString)
/**
* When looking for default group items, the group doesn't exist, so we have to apply
* the same filters to the views table instead
*/
const applyFilters = (query: Knex.QueryBuilder, mode: 'view' | 'group') => {
const isGroupQuery = mode === 'group'
const isViewQuery = mode === 'view'
if (isViewQuery) {
// empty groupId - we're looking for ungrouped views only
query.andWhere({
[SavedViews.col.groupId]: null
})
}
// group's or view's authorId
query.andWhere({
[isGroupQuery ? SavedViewGroups.col.projectId : SavedViews.col.projectId]:
projectId
})
// group's or view's resourceIds
if (resourceIds.length) {
// Col contains at least one of the resources
query.andWhereRaw('?? && ?', [
isGroupQuery
? SavedViewGroups.col.resourceIds
: SavedViews.col.groupResourceIds,
resourceIds
])
} else {
// Make query exit early - no resources
query.andWhereRaw('false')
}
// checking authored only on views (in case of groups, they're joined on)
if (onlyAuthored) {
query.andWhere({ [SavedViews.col.authorId]: userId })
} else if (mode === 'view') {
query.andWhere((w1) => {
w1.andWhere(SavedViews.col.visibility, SavedViewVisibility.public)
if (userId) {
w1.orWhere(SavedViews.col.authorId, userId)
}
})
}
// checking search only on views
if (search) {
query.andWhere(SavedViews.col.name, 'ilike', `%${search}%`)
}
return query
}
const q = tables
.savedViewGroups(deps.db)
.select<SavedViewGroup[]>(SavedViewGroups.cols)
if (isFiltering) {
q.innerJoin(SavedViews.name, SavedViews.col.groupId, SavedViewGroups.col.id)
}
applyFilters(q, 'group')
// Group by groupId
q.groupBy(SavedViewGroups.col.id)
/**
* Check if default group should be shown
*/
const ungroupedViewFound = await applyFilters(
tables.savedViews(deps.db),
'view'
).first()
const includeDefaultGroup = Boolean(ungroupedViewFound)
return { q, resourceIds, isFiltering, includeDefaultGroup }
}
export const getProjectSavedViewGroupsTotalCountFactory =
(deps: { db: Knex }): GetProjectSavedViewGroupsTotalCount =>
async (params) => {
const { q, includeDefaultGroup } = await getProjectSavedViewGroupsBaseQueryFactory(
deps
)(params)
const countQ = deps.db.count<{ count: string }[]>().from(q.as('sq1'))
const [count] = await countQ
const numberCount = parseInt(count.count + '') + (includeDefaultGroup ? 1 : 0)
return numberCount
}
export const getProjectSavedViewGroupsPageItemsFactory =
(deps: { db: Knex }): GetProjectSavedViewGroupsPageItems =>
async (params) => {
const { projectId } = params
const { q, resourceIds, includeDefaultGroup } =
await getProjectSavedViewGroupsBaseQueryFactory(deps)(params)
const { applyCursorSortAndFilter, resolveNewCursor } = savedGroupCursorUtils()
const limit = clamp(params.limit ?? 10, 0, 100)
q.limit(limit)
// Apply cursor filter and sort
applyCursorSortAndFilter({
query: q,
cursor: params.cursor
})
const items: SavedViewGroup[] = await q
// If first page and allowed, add the default/unsorted group
if (!params.cursor && includeDefaultGroup) {
const defaultGroup: SavedViewGroup = buildDefaultGroup({
resourceIds,
projectId
})
// Before we add the group, we need to potentially pop the last item
// if the limit was reached
if (items.length >= limit) {
items.pop()
}
items.unshift(defaultGroup)
}
const newCursor = resolveNewCursor(items)
return {
items,
cursor: newCursor
}
}
const getGroupSavedViewsBaseQueryFactory =
(deps: { db: Knex }) => (params: GetGroupSavedViewsBaseParams) => {
const { projectId, groupResourceIdString, groupId, search, userId } = params
const onlyAuthored = params.onlyAuthored && userId
const q = tables
.savedViews(deps.db)
.where({ [SavedViews.col.projectId]: projectId })
const groupResourceIds = formatResourceIdsForGroup(groupResourceIdString)
if (!groupResourceIds.length && !groupId) {
// If no resources and no groupId, exit early
q.whereRaw('false')
}
// Set group filter
if (!isUndefined(groupId)) {
q.where({ [SavedViews.col.groupId]: groupId })
}
// If no groupId, filter by resourceIds
if (groupResourceIds.length && !groupId) {
q.whereRaw('?? && ?', [SavedViews.col.groupResourceIds, groupResourceIds])
}
if (onlyAuthored) {
q.where({ [SavedViews.col.authorId]: userId })
} else {
q.andWhere((w1) => {
w1.andWhere(SavedViews.col.visibility, SavedViewVisibility.public)
if (userId) {
w1.orWhere(SavedViews.col.authorId, userId)
}
})
}
if (search) {
q.where(SavedViews.col.name, 'ilike', `%${search}%`)
}
return q
}
export const getGroupSavedViewsTotalCountFactory =
(deps: { db: Knex }): GetGroupSavedViewsTotalCount =>
async (params) => {
const q = getGroupSavedViewsBaseQueryFactory(deps)(params)
const countQ = deps.db.count<{ count: string }[]>().from(q.as('sq1'))
const [count] = await countQ
return parseInt(count.count + '')
}
export const getGroupSavedViewsPageItemsFactory =
(deps: { db: Knex }): GetGroupSavedViewsPageItems =>
async (params) => {
const sortByCol = params.sortBy || 'updatedAt'
const sortDir = params.sortDirection || 'desc'
const q = getGroupSavedViewsBaseQueryFactory(deps)(params)
const { applyCursorSortAndFilter, resolveNewCursor } = compositeCursorTools({
schema: SavedViews,
cols: [sortByCol, 'id']
})
const limit = clamp(params.limit ?? 10, 0, 100)
q.limit(limit)
// Apply cursor filter and sort
applyCursorSortAndFilter({ query: q, cursor: params.cursor, sort: sortDir })
const items = await q
const newCursor = resolveNewCursor(items)
return {
items,
cursor: newCursor
}
}
export const getSavedViewGroupFactory =
(deps: { db: Knex }): GetSavedViewGroup =>
async ({ id, projectId }) => {
// Check if default group ID
const defaultGroupMetadata = decodeDefaultGroupId(id)
if (defaultGroupMetadata) {
if (projectId && defaultGroupMetadata.projectId !== projectId) {
return undefined
}
return buildDefaultGroup({
resourceIds: defaultGroupMetadata.resourceIds,
projectId: defaultGroupMetadata.projectId
})
}
const q = tables.savedViewGroups(deps.db).where({
[SavedViewGroups.col.id]: id
})
if (projectId) {
q.andWhere({ [SavedViewGroups.col.projectId]: projectId })
}
const group = await q.first()
return group
}
export const getUngroupedSavedViewsGroupFactory =
(): GetUngroupedSavedViewsGroup =>
({ projectId, resourceIdString }) =>
buildDefaultGroup({
resourceIds: resourceBuilder()
.addFromString(resourceIdString)
.map((r) => r.toString()),
projectId
})
export const recalculateGroupResourceIdsFactory =
(deps: { db: Knex }): RecalculateGroupResourceIds =>
async ({ groupId }) => {
const RawSavedViews = SavedViews.with({ quoted: true, withCustomTablePrefix: 'v' })
const RawSavedViewGroups = SavedViewGroups.with({ quoted: true })
const q = tables
.savedViewGroups(deps.db)
.where({ [SavedViewGroups.col.id]: groupId })
.update(
{
// Recalculate the groups resourceIds based on the views in the group
[SavedViewGroups.withoutTablePrefix.col.resourceIds]: deps.db.raw(
`(
SELECT ARRAY(
SELECT DISTINCT unnest
FROM ${RawSavedViews.name},
unnest(${RawSavedViews.col.resourceIds}) AS unnest
WHERE ${RawSavedViews.col.groupId} = ${RawSavedViewGroups.col.id}
)
)`
)
},
'*'
)
const results = await q
return results.at(0)
}
/**
* Get saved groups by IDs. Can handle unpersisted ungrouped groups too.
*/
export const getSavedViewGroupsFactory =
(deps: { db: Knex }): GetSavedViewGroups =>
async ({ groupIds }) => {
if (!groupIds.length) {
return {}
}
const defaultGroupsMetadata: { [groupId: string]: DefaultGroupMetadata } = {}
const persistedGroupIds: Array<{ groupId: string; projectId: string }> = []
for (const { groupId, projectId } of groupIds) {
const defaultGroupMetadata = decodeDefaultGroupId(groupId)
if (defaultGroupMetadata) {
if (defaultGroupMetadata.projectId === projectId) {
defaultGroupsMetadata[groupId] = defaultGroupMetadata
}
} else {
persistedGroupIds.push({ groupId, projectId })
}
}
let persistedGroups: SavedViewGroup[] = []
if (persistedGroupIds.length) {
const q = tables.savedViewGroups(deps.db).whereIn(
[SavedViewGroups.col.id, SavedViewGroups.col.projectId],
persistedGroupIds.map((g) => [g.groupId, g.projectId])
)
persistedGroups = await q
}
const groupsMap: { [groupId: string]: SavedViewGroup | undefined } = {}
for (const { groupId, projectId } of groupIds) {
const defaultGroupMetadata = defaultGroupsMetadata[groupId]
if (defaultGroupMetadata) {
groupsMap[groupId] = buildDefaultGroup({
resourceIds: defaultGroupMetadata.resourceIds,
projectId: defaultGroupMetadata.projectId
})
} else {
groupsMap[groupId] =
persistedGroups.find((g) => g.id === groupId && g.projectId === projectId) ||
undefined
}
}
return groupsMap
}
export const getSavedViewsFactory =
(deps: { db: Knex }): GetSavedViews =>
async ({ viewIds }) => {
if (!viewIds.length) {
return {}
}
const q = tables.savedViews(deps.db).whereIn(
[SavedViews.col.id, SavedViews.col.projectId],
viewIds.map((v) => [v.viewId, v.projectId])
)
const views = await q
const viewsMap: { [viewId: string]: SavedView | undefined } = {}
for (const view of views) {
viewsMap[view.id] = view
}
return viewsMap
}
@@ -0,0 +1,290 @@
import type {
CreateSavedView,
CreateSavedViewGroup,
GetGroupSavedViews,
GetGroupSavedViewsPageItems,
GetGroupSavedViewsTotalCount,
GetProjectSavedViewGroups,
GetProjectSavedViewGroupsPageItems,
GetProjectSavedViewGroupsTotalCount,
GetSavedViewGroup,
GetStoredViewCount,
RecalculateGroupResourceIds,
StoreSavedView,
StoreSavedViewGroup
} from '@/modules/viewer/domain/operations/savedViews'
import { SavedViewVisibility } from '@/modules/viewer/domain/types/savedViews'
import {
SavedViewCreationValidationError,
SavedViewGroupCreationValidationError,
SavedViewInvalidResourceTargetError
} from '@/modules/viewer/errors/savedViews'
import { resourceBuilder } from '@speckle/shared/viewer/route'
import { inputToVersionedState } from '@speckle/shared/viewer/state'
import { isValidBase64Image } from '@speckle/shared/images/base64'
import type { GetViewerResourceGroups } from '@/modules/viewer/domain/operations/resources'
import { formatResourceIdsForGroup } from '@/modules/viewer/helpers/savedViews'
/**
* Validates an incoming resourceIdString against the resources in the project and returns the validated list (as a builder)
*/
const validateProjectResourceIdStringFactory =
(deps: { getViewerResourceGroups: GetViewerResourceGroups }) =>
async (params: {
resourceIdString: string
projectId: string
errorMetadata: Record<string, unknown>
}) => {
const { resourceIdString, errorMetadata, projectId } = params
// Validate resourceIdString - it should only point to valid resources belonging to the project
const resourceIds = resourceBuilder().addFromString(resourceIdString)
if (!resourceIds.length) {
throw new SavedViewInvalidResourceTargetError(
"No valid resources referenced in 'resourceIdString'",
{
info: errorMetadata
}
)
}
const resourceGroups = await deps.getViewerResourceGroups({
projectId,
loadedVersionsOnly: true,
resourceIdString: resourceIds.toString(),
allowEmptyModels: true
})
// Check if any of the resources could not be found
const failingResources = resourceIds.clone().filter((rId) => {
const resourceGroup = resourceGroups.find(
(rg) => rg.identifier === rId.toString()
)
if (!resourceGroup) return true
return false
})
if (failingResources.length) {
throw new SavedViewInvalidResourceTargetError(
'One or more resources could not be found in the project: {resourceIdString}',
{
info: {
...errorMetadata,
resourceIdString: failingResources.toString()
}
}
)
}
return resourceIds
}
export const createSavedViewFactory =
(deps: {
getViewerResourceGroups: GetViewerResourceGroups
getStoredViewCount: GetStoredViewCount
storeSavedView: StoreSavedView
getSavedViewGroup: GetSavedViewGroup
recalculateGroupResourceIds: RecalculateGroupResourceIds
}): CreateSavedView =>
async ({ input, authorId }) => {
const { resourceIdString, projectId } = input
const visibility = input.visibility || SavedViewVisibility.public
const position = 0 // TODO: Resolve based on existing views
const groupId = input.groupId?.trim() || null
const description = input.description?.trim() || null
const isHomeView = input.isHomeView || false
// Validate resourceIdString - it should only point to valid resources belonging to the project
const resourceIds = await validateProjectResourceIdStringFactory(deps)({
resourceIdString,
projectId,
errorMetadata: {
input,
authorId
}
})
const screenshot = input.screenshot.trim()
if (!isValidBase64Image(screenshot)) {
throw new SavedViewCreationValidationError(
'Invalid screenshot provided. Must be a valid base64 encoded image.',
{
info: {
input,
authorId
}
}
)
}
const state = inputToVersionedState(input.viewerState)
if (!state) {
throw new SavedViewCreationValidationError(
'Invalid viewer state provided. Must be a valid SerializedViewerState.',
{
info: {
input,
authorId
}
}
)
}
// Validate state match
if (state.state.resources.request.resourceIdString !== input.resourceIdString) {
throw new SavedViewCreationValidationError(
'Viewer state does not match the provided resourceIdString.',
{
info: {
input,
authorId
}
}
)
}
if (state.state.projectId !== projectId) {
throw new SavedViewCreationValidationError(
'Viewer state projectId does not match the provided projectId.',
{
info: {
input,
authorId
}
}
)
}
// Validate groupId - group is a valid and accessible group in the project
if (groupId) {
const group = await deps.getSavedViewGroup({
id: groupId,
projectId
})
if (!group) {
throw new SavedViewCreationValidationError(
'Provided groupId does not exist in the project.',
{
info: {
input,
authorId
}
}
)
}
}
// Auto-generate name, if one not set
let name = input.name?.trim()
if (!name?.length) {
const viewCount = await deps.getStoredViewCount({ projectId })
name = `Scene - ${String(viewCount + 1).padStart(3, '0')}`
}
const concreteResourceIds = resourceIds.toResources().map((r) => r.toString())
const ret = await deps.storeSavedView({
view: {
projectId,
resourceIds: concreteResourceIds,
groupResourceIds: formatResourceIdsForGroup(concreteResourceIds),
groupId,
name,
description,
viewerState: state,
screenshot,
visibility,
position,
authorId,
isHomeView
}
})
// If grouped view, recalculate its resourceIds
if (groupId) {
await deps.recalculateGroupResourceIds({ groupId })
}
return ret
}
export const createSavedViewGroupFactory =
(deps: {
storeSavedViewGroup: StoreSavedViewGroup
getViewerResourceGroups: GetViewerResourceGroups
}): CreateSavedViewGroup =>
async ({ input, authorId }) => {
const { projectId, resourceIdString } = input
const groupName = input.groupName.trim()
if (groupName.length < 1 || groupName.length > 255) {
throw new SavedViewGroupCreationValidationError(
'Group name must be between 1 and 255 characters long',
{
info: {
input,
authorId
}
}
)
}
// Validate resourceIdString - it should only point to valid resources belonging to the project
const resourceIds = await validateProjectResourceIdStringFactory(deps)({
resourceIdString,
projectId,
errorMetadata: {
input,
authorId
}
})
// Insert
const group = await deps.storeSavedViewGroup({
group: {
projectId,
resourceIds: resourceIds.toResources().map((r) => r.toString()),
name: groupName,
authorId
}
})
return group
}
export const getProjectSavedViewGroupsFactory =
(deps: {
getProjectSavedViewGroupsPageItems: GetProjectSavedViewGroupsPageItems
getProjectSavedViewGroupsTotalCount: GetProjectSavedViewGroupsTotalCount
}): GetProjectSavedViewGroups =>
async (params) => {
const noItemsNeeded = params.limit === 0
const [totalCount, pageItems] = await Promise.all([
deps.getProjectSavedViewGroupsTotalCount(params),
noItemsNeeded
? Promise.resolve({ items: [], cursor: null })
: deps.getProjectSavedViewGroupsPageItems(params)
])
return {
totalCount,
...pageItems
}
}
export const getGroupSavedViewsFactory =
(deps: {
getGroupSavedViewsPageItems: GetGroupSavedViewsPageItems
getGroupSavedViewsTotalCount: GetGroupSavedViewsTotalCount
}): GetGroupSavedViews =>
async (params) => {
const noItemsNeeded = params.limit === 0
const [totalCount, pageItems] = await Promise.all([
deps.getGroupSavedViewsTotalCount(params),
noItemsNeeded
? Promise.resolve({ items: [], cursor: null })
: deps.getGroupSavedViewsPageItems(params)
])
return {
totalCount,
...pageItems
}
}
@@ -0,0 +1,413 @@
import type {
GetBranchesByIds,
GetBranchLatestCommits,
GetStreamBranchesByName
} from '@/modules/core/domain/branches/operations'
import type {
GetAllBranchCommits,
GetSpecificBranchCommits
} from '@/modules/core/domain/commits/operations'
import type { GetStreamObjects } from '@/modules/core/domain/objects/operations'
import type {
ViewerResourceGroup,
ViewerResourceItem,
ViewerUpdateTrackingTarget
} from '@/modules/core/graph/generated/graphql'
import type { CommitRecord } from '@/modules/core/helpers/types'
import type {
GetViewerResourceGroups,
GetViewerResourceItemsUngrouped
} from '@/modules/viewer/domain/operations/resources'
import type { Optional } from '@speckle/shared'
import { SpeckleViewer } from '@speckle/shared'
import type { ViewerModelResource } from '@speckle/shared/viewer/route'
import { flatten, keyBy, uniq, uniqWith } from 'lodash-es'
export function isResourceItemEqual(a: ViewerResourceItem, b: ViewerResourceItem) {
if (a.modelId !== b.modelId) return false
if (a.objectId !== b.objectId) return false
if (a.versionId !== b.versionId) return false
return true
}
export type GetObjectResourceGroupsDeps = {
getStreamObjects: GetStreamObjects
}
export const getObjectResourceGroupsFactory =
(deps: GetObjectResourceGroupsDeps) =>
async (
projectId: string,
resources: SpeckleViewer.ViewerRoute.ViewerObjectResource[]
) => {
const objects = keyBy(
await deps.getStreamObjects(
projectId,
resources.map((r) => r.objectId)
),
'id'
)
const results: ViewerResourceGroup[] = []
for (const objectResource of resources) {
if (!objects[objectResource.objectId]) continue
results.push({
identifier: objectResource.toString(),
items: [{ modelId: null, versionId: null, objectId: objectResource.objectId }]
})
}
return results
}
type GetVersionResourceGroupsIncludingAllVersionsFactoryDeps = {
getStreamBranchesByName: GetStreamBranchesByName
getAllBranchCommits: GetAllBranchCommits
}
const getVersionResourceGroupsIncludingAllVersionsFactory =
(deps: GetVersionResourceGroupsIncludingAllVersionsFactoryDeps) =>
async (
projectId: string,
params: {
modelResources?: SpeckleViewer.ViewerRoute.ViewerModelResource[]
folderResources?: SpeckleViewer.ViewerRoute.ViewerModelFolderResource[]
}
) => {
// by default we pull all versions of all relevant branches, but if loadedVersionsOnly is set, we only pull
// specifically requested versions (if version isn't set in identifier, then latest version)
const { modelResources = [], folderResources = [] } = params
const results: ViewerResourceGroup[] = []
const foldersModels = await deps.getStreamBranchesByName(
projectId,
folderResources.map((r) => r.folderName),
{ startsWithName: true }
)
const allBranchIds = [
...foldersModels.map((m) => m.id),
...modelResources.map((m) => m.modelId)
]
// get all versions of all referenced branches
const branchCommits = await deps.getAllBranchCommits({ branchIds: allBranchIds })
for (const folderResource of folderResources) {
const prefix = folderResource.folderName
const folderModels = foldersModels.filter((m) =>
m.name.toLowerCase().startsWith(prefix)
)
if (!folderModels.length) continue
const items: ViewerResourceItem[] = []
for (const folderModel of folderModels) {
const modelVersions = branchCommits[folderModel.id]
if (!modelVersions?.length) continue
for (const modelVersion of modelVersions) {
items.push({
modelId: folderModel.id,
versionId: modelVersion.id,
objectId: modelVersion.referencedObject
})
}
}
results.push({
identifier: folderResource.toString(),
items
})
}
for (const modelResource of modelResources) {
const modelVersions = branchCommits[modelResource.modelId] || []
const items: ViewerResourceItem[] = []
for (const modelVersion of modelVersions) {
items.push({
modelId: modelResource.modelId,
versionId: modelVersion.id,
objectId: modelVersion.referencedObject
})
}
results.push({
identifier: modelResource.toString(),
items
})
}
return results
}
type GetVersionResourceGroupsLoadedVersionsOnlyDeps = {
getStreamBranchesByName: GetStreamBranchesByName
getBranchesByIds: GetBranchesByIds
getSpecificBranchCommits: GetSpecificBranchCommits
getBranchLatestCommits: GetBranchLatestCommits
}
const getVersionResourceGroupsLoadedVersionsOnlyFactory =
(deps: GetVersionResourceGroupsLoadedVersionsOnlyDeps) =>
async (
projectId: string,
params: {
modelResources?: SpeckleViewer.ViewerRoute.ViewerModelResource[]
folderResources?: SpeckleViewer.ViewerRoute.ViewerModelFolderResource[]
allowEmptyModels?: boolean
}
) => {
// by default we pull all versions of all relevant branches, but if loadedVersionsOnly is set, we only pull
// specifically requested versions (if version isn't set in identifier, then latest version)
const { modelResources = [], folderResources = [], allowEmptyModels } = params
const results: ViewerResourceGroup[] = []
const foldersModels = await deps.getStreamBranchesByName(
projectId,
folderResources.map((r) => r.folderName),
{ startsWithName: true }
)
const specificVersionPairs = modelResources
.filter(
(
r
): r is SpeckleViewer.ViewerRoute.ViewerModelResource & { versionId: string } =>
!!r.versionId
)
.map((r) => ({ branchId: r.modelId, commitId: r.versionId }))
const latestVersionModelIds = uniq([
...modelResources.filter((r) => !r.versionId).map((r) => r.modelId),
...foldersModels.map((m) => m.id)
])
const [specificVersions, latestVersions] = await Promise.all([
deps.getSpecificBranchCommits(specificVersionPairs),
deps.getBranchLatestCommits(latestVersionModelIds)
])
const modelLatestVersions = keyBy(latestVersions, 'branchId')
for (const folderResource of folderResources) {
const prefix = folderResource.folderName
const folderModels = foldersModels.filter((m) =>
m.name.toLowerCase().startsWith(prefix)
)
if (!folderModels.length) continue
const items: ViewerResourceItem[] = []
for (const folderModel of folderModels) {
const latestVersion = modelLatestVersions[folderModel.id]
if (!latestVersion) continue
items.push({
modelId: folderModel.id,
versionId: latestVersion.id,
objectId: latestVersion.referencedObject
})
}
results.push({
identifier: folderResource.toString(),
items
})
}
const emptyModels: ViewerModelResource[] = []
for (const modelResource of modelResources) {
let item: Optional<CommitRecord & { branchId: string }> = undefined
if (modelResource.versionId) {
item = specificVersions.find(
(v) =>
v.branchId === modelResource.modelId && v.id === modelResource.versionId
)
} else {
item = modelLatestVersions[modelResource.modelId]
}
if (!item) {
if (allowEmptyModels && !modelResource.versionId) {
emptyModels.push(modelResource)
}
continue
}
results.push({
identifier: modelResource.toString(),
items: [
{
modelId: item.branchId,
versionId: item.id,
objectId: item.referencedObject
}
]
})
}
// Validate that empty model resources are actually real models
if (emptyModels.length && allowEmptyModels) {
const emptyModelRecords = await deps.getBranchesByIds(
emptyModels.map((r) => r.modelId),
{ streamId: projectId }
)
const emptyModelIds = new Set(emptyModelRecords.map((m) => m.id))
for (const emptyModelId of emptyModelIds) {
results.push({
identifier: emptyModelId,
items: []
})
}
}
return results
}
type GetAllModelsResourceGroupDeps = {
getBranchLatestCommits: GetBranchLatestCommits
}
const getAllModelsResourceGroupFactory =
(deps: GetAllModelsResourceGroupDeps) =>
async (projectId: string): Promise<ViewerResourceGroup> => {
const allBranchCommits = await deps.getBranchLatestCommits(undefined, projectId)
return {
identifier: 'all',
items: allBranchCommits.map(
(c): ViewerResourceItem => ({
modelId: c.branchId,
versionId: c.id,
objectId: c.referencedObject
})
)
}
}
type GetVersionResourceGroupsDeps = GetAllModelsResourceGroupDeps &
GetVersionResourceGroupsLoadedVersionsOnlyDeps &
GetVersionResourceGroupsIncludingAllVersionsFactoryDeps
/**
* Version resources can be resolved 2 ways:
* * Default - Specific version IDs referenced in identifiers are ignored and the identifiers always
* refer to all versions of any referenced branch/branches of folders.
* * Loaded versions only - Identifiers only refer to specific version IDs referenced in resource
* identifiers, or if none are specified then only the latest version is referenced (e.g. in folder
* resources & model resources w/ an empty version ID)
*/
const getVersionResourceGroupsFactory =
(deps: GetVersionResourceGroupsDeps) =>
async (
projectId: string,
params: {
modelResources?: SpeckleViewer.ViewerRoute.ViewerModelResource[]
folderResources?: SpeckleViewer.ViewerRoute.ViewerModelFolderResource[]
allModelsResource?: SpeckleViewer.ViewerRoute.ViewerAllModelsResource
loadedVersionsOnly?: boolean
allowEmptyModels?: boolean
}
) => {
const allModelsGroup = params.allModelsResource
? await getAllModelsResourceGroupFactory(deps)(projectId)
: null
const groups = params.loadedVersionsOnly
? await getVersionResourceGroupsLoadedVersionsOnlyFactory(deps)(projectId, params)
: await getVersionResourceGroupsIncludingAllVersionsFactory(deps)(
projectId,
params
)
return [...(allModelsGroup ? [allModelsGroup] : []), ...groups]
}
/**
* Validate requested resource identifiers and build viewer resource groups & items with
* the metadata that the viewer needs to work with these
*/
export const getViewerResourceGroupsFactory =
(
deps: GetObjectResourceGroupsDeps & GetVersionResourceGroupsDeps
): GetViewerResourceGroups =>
async (
target: ViewerUpdateTrackingTarget & {
/**
* By default this only returns groups w/ resources in them. W/ this flag set, it will also
* return valid model groups that have no resources in them
*/
allowEmptyModels?: boolean
}
): Promise<ViewerResourceGroup[]> => {
const { resourceIdString, projectId, loadedVersionsOnly, allowEmptyModels } = target
if (!resourceIdString?.trim().length) return []
const resources = SpeckleViewer.ViewerRoute.parseUrlParameters(resourceIdString)
const allModelsResource = resources.find(
SpeckleViewer.ViewerRoute.isAllModelsResource
)
const objectResources = resources.filter(SpeckleViewer.ViewerRoute.isObjectResource)
const modelResources = resources.filter(SpeckleViewer.ViewerRoute.isModelResource)
const folderResources = resources.filter(
SpeckleViewer.ViewerRoute.isModelFolderResource
)
const results: ViewerResourceGroup[] = flatten(
await Promise.all([
getObjectResourceGroupsFactory(deps)(projectId, objectResources),
getVersionResourceGroupsFactory(deps)(projectId, {
modelResources,
folderResources,
allModelsResource,
loadedVersionsOnly: loadedVersionsOnly || false,
allowEmptyModels
})
])
)
return results
}
export const getViewerResourceItemsUngroupedFactory =
(deps: {
getViewerResourceGroups: GetViewerResourceGroups
}): GetViewerResourceItemsUngrouped =>
async (target: ViewerUpdateTrackingTarget): Promise<ViewerResourceItem[]> => {
const { resourceIdString } = target
if (!resourceIdString?.trim().length) return []
let results: ViewerResourceItem[] = []
const groups = await deps.getViewerResourceGroups(target)
for (const group of groups) {
results = results.concat(group.items)
}
return uniqWith(results, isResourceItemEqual)
}
/**
* Whether any of the resource items match
*/
export function doViewerResourcesFit(
requestedResources: ViewerResourceItem[],
incomingResources: ViewerResourceItem[]
) {
return incomingResources.some((ir) =>
requestedResources.some((rr) => isResourceItemEqual(ir, rr))
)
}
export function viewerResourcesToString(resources: ViewerResourceItem[]): string {
const builder = SpeckleViewer.ViewerRoute.resourceBuilder()
for (const resource of resources) {
if (resource.modelId && resource.versionId) {
builder.addModel(resource.modelId, resource.versionId)
} else {
builder.addObject(resource.objectId)
}
}
return builder.toString()
}
@@ -0,0 +1,135 @@
import gql from 'graphql-tag'
const basicSavedViewFragment = gql`
fragment BasicSavedView on SavedView {
id
name
description
author {
id
}
groupId
group {
id
title
isUngroupedViewsGroup
}
createdAt
updatedAt
resourceIdString
resourceIds
isHomeView
visibility
viewerState
screenshot
position
projectId
}
`
const basicSavedViewGroupFragment = gql`
fragment BasicSavedViewGroup on SavedViewGroup {
id
projectId
resourceIds
title
isUngroupedViewsGroup
views(input: $viewsInput) {
totalCount
cursor
items {
...BasicSavedView
}
}
}
`
export const createSavedViewMutation = gql`
mutation CreateSavedView($input: CreateSavedViewInput!) {
projectMutations {
savedViewMutations {
createView(input: $input) {
...BasicSavedView
}
}
}
}
${basicSavedViewFragment}
`
export const createSavedGroupMutation = gql`
mutation CreateSavedViewGroup(
$input: CreateSavedViewGroupInput!
$viewsInput: SavedViewGroupViewsInput! = { limit: 10 }
) {
projectMutations {
savedViewMutations {
createGroup(input: $input) {
...BasicSavedViewGroup
}
}
}
}
`
export const getProjectSavedViewGroupsQuery = gql`
query GetProjectSavedViewGroups(
$projectId: String!
$input: SavedViewGroupsInput!
$viewsInput: SavedViewGroupViewsInput! = { limit: 10 }
) {
project(id: $projectId) {
savedViewGroups(input: $input) {
totalCount
cursor
items {
...BasicSavedViewGroup
}
}
}
}
${basicSavedViewGroupFragment}
`
export const getProjectSavedViewGroupQuery = gql`
query GetProjectSavedViewGroup(
$projectId: String!
$groupId: ID!
$viewsInput: SavedViewGroupViewsInput! = { limit: 10 }
) {
project(id: $projectId) {
savedViewGroup(id: $groupId) {
...BasicSavedViewGroup
}
}
}
${basicSavedViewGroupFragment}
`
export const getProjectUngroupedViewGroupQuery = gql`
query GetProjectUngroupedViewGroup(
$projectId: String!
$input: GetUngroupedViewGroupInput!
$viewsInput: SavedViewGroupViewsInput! = { limit: 10 }
) {
project(id: $projectId) {
ungroupedViewGroup(input: $input) {
...BasicSavedViewGroup
}
}
}
`
export const getProjectSavedViewQuery = gql`
query GetProjectSavedView($projectId: String!, $viewId: ID!) {
project(id: $projectId) {
savedView(id: $viewId) {
...BasicSavedView
}
}
}
${basicSavedViewFragment}
`
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,355 @@
import { db } from '@/db/knex'
import {
getBranchesByIdsFactory,
getBranchLatestCommitsFactory,
getStreamBranchesByNameFactory
} from '@/modules/core/repositories/branches'
import {
getAllBranchCommitsFactory,
getSpecificBranchCommitsFactory
} from '@/modules/core/repositories/commits'
import { getStreamObjectsFactory } from '@/modules/core/repositories/objects'
import { buildBasicTestProject } from '@/modules/core/tests/helpers/creation'
import {
doViewerResourcesFit,
getViewerResourceGroupsFactory,
isResourceItemEqual,
viewerResourcesToString
} from '@/modules/viewer/services/viewerResources'
import { itEach } from '@/test/assertionHelper'
import type { BasicTestUser } from '@/test/authHelper'
import { buildBasicTestUser, createTestUser } from '@/test/authHelper'
import {
createTestBranch,
type BasicTestBranch
} from '@/test/speckle-helpers/branchHelper'
import type { BasicTestCommit } from '@/test/speckle-helpers/commitHelper'
import { createTestCommit } from '@/test/speckle-helpers/commitHelper'
import type { BasicTestStream } from '@/test/speckle-helpers/streamHelper'
import { createTestStream } from '@/test/speckle-helpers/streamHelper'
import {
resourceBuilder,
ViewerAllModelsResource,
ViewerModelResource,
ViewerObjectResource
} from '@speckle/shared/viewer/route'
import { expect } from 'chai'
import { times } from 'lodash-es'
describe('Viewer Resources Collection Service', () => {
describe('getViewerResourceGroupsFactory', () => {
let me: BasicTestUser
let myProject: BasicTestStream
let myModels: BasicTestBranch[]
let myVersions: {
[modelId: string]: BasicTestCommit[]
}
const buildSUT = () =>
getViewerResourceGroupsFactory({
getStreamObjects: getStreamObjectsFactory({ db }),
getBranchLatestCommits: getBranchLatestCommitsFactory({ db }),
getStreamBranchesByName: getStreamBranchesByNameFactory({ db }),
getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db }),
getAllBranchCommits: getAllBranchCommitsFactory({ db }),
getBranchesByIds: getBranchesByIdsFactory({ db })
})
const allVersions = (): BasicTestCommit[] => {
return Object.values(myVersions).flat()
}
before(async () => {
me = await createTestUser(buildBasicTestUser())
myProject = await createTestStream(buildBasicTestProject(), me)
// Add 3 models
myModels = await Promise.all(
times(3, (i) =>
createTestBranch({
branch: {
name: `Model ${i + 1}`,
description: `Description for model ${i + 1}`,
streamId: myProject.id,
authorId: me.id,
id: ''
},
stream: myProject,
owner: me
})
)
)
// Add 3 versions to each model
const dateGen = (i: number) => new Date(Date.now() - i * 1000)
myVersions = {}
await Promise.all(
myModels.map(async (model) => {
myVersions[model.id] = await Promise.all(
times(3, (i) =>
createTestCommit({
streamId: myProject.id,
authorId: me.id,
message: `Version ${i + 1} for model ${model.name}`,
createdAt: dateGen(i),
id: '',
objectId: '',
branchId: model.id
})
)
)
})
)
})
itEach(
['all', 'specific', 'latest'],
(type) => `successfully resolves model groups (gets ${type} versions for each)`,
async (type) => {
const sut = buildSUT()
const resourceIds = resourceBuilder().addResources(
myModels.map(
(m) =>
new ViewerModelResource(
m.id,
type === 'specific' ? myVersions[m.id].at(-1)?.id : undefined
)
)
)
const result = await sut({
projectId: myProject.id,
resourceIdString: resourceIds.toString().toString(),
loadedVersionsOnly: type !== 'all'
})
expect(result.length).to.equal(myModels.length)
for (const group of result) {
const model = myModels.find((m) => group.identifier.startsWith(m.id))
expect(model).to.be.ok
const versions = myVersions[model!.id]
expect(group.items).to.have.length(type === 'all' ? 3 : 1)
if (type === 'all') {
for (const item of group.items) {
const version = versions.find((v) => v.id === item.versionId)
expect(version).to.exist
expect(item.modelId).to.include(model!.id)
expect(item.objectId).to.equal(version?.objectId)
}
} else {
let versionToCompareTo: BasicTestCommit
if (type === 'specific') {
const latestVersion = versions.at(-1) // we targeted the last one
expect(latestVersion).to.be.ok
versionToCompareTo = latestVersion!
} else {
const latestVersion = versions
.sort((a, b) => b.createdAt!.getTime() - a.createdAt!.getTime())
.at(0)
expect(latestVersion).to.be.ok
versionToCompareTo = latestVersion!
}
expect(group.items.length).to.equal(1) // one item per version
const item = group.items[0]
expect(item.modelId).to.equal(model!.id)
expect(item.objectId).to.equal(versionToCompareTo.objectId)
expect(item.versionId).to.equal(versionToCompareTo.id)
}
}
}
)
it('return empty array on empty resourceIdString', async () => {
const sut = buildSUT()
const result = await sut({
projectId: myProject.id,
resourceIdString: ''
})
expect(result).to.have.length(0)
})
it('successfully returns objectId based groups', async () => {
const sut = buildSUT()
const versions = allVersions()
const resourceIds = resourceBuilder().addResources(
versions.map((v) => new ViewerObjectResource(v.objectId))
)
const result = await sut({
projectId: myProject.id,
resourceIdString: resourceIds.toString()
})
expect(result.length).to.equal(versions.length)
for (const group of result) {
const version = versions.find((v) => v.objectId === group.identifier)
expect(version).to.be.ok
expect(group.identifier).to.equal(version!.objectId)
expect(group.items.length).to.equal(1) // one item per version
const item = group.items[0]
expect(item.objectId).to.equal(version!.objectId)
expect(item.versionId).to.not.be.ok
expect(item.modelId).to.not.be.ok
}
})
it('successfully resolves all group (each models latest version)', async () => {
const sut = buildSUT()
const resourceIds = resourceBuilder().addResources([
new ViewerAllModelsResource()
])
const result = await sut({
projectId: myProject.id,
resourceIdString: resourceIds.toString()
})
expect(result.length).to.equal(1)
const group = result[0]
expect(group.identifier).to.equal('all')
expect(group.items.length).to.equal(myModels.length)
for (const item of group.items) {
const model = myModels.find((m) => m.id === item.modelId)
expect(model).to.be.ok
expect(item.modelId).to.equal(model!.id)
// Sort versions by createdAt, descending
const latestVersion = myVersions[model!.id]
.slice()
.sort((a, b) => b.createdAt!.getTime() - a.createdAt!.getTime())
.at(0)
expect(latestVersion).to.be.ok
expect(item.objectId).to.equal(latestVersion!.objectId)
expect(item.versionId).to.equal(latestVersion!.id)
}
})
})
describe('isResourceItemEqual', () => {
it('returns true for identical ViewerResourceItems', () => {
const itemA = { modelId: 'model1', objectId: 'obj1', versionId: 'ver1' }
const itemB = { modelId: 'model1', objectId: 'obj1', versionId: 'ver1' }
expect(isResourceItemEqual(itemA, itemB)).to.be.true
})
it('returns false if modelId differs', () => {
const itemA = { modelId: 'model1', objectId: 'obj1', versionId: 'ver1' }
const itemB = { modelId: 'model2', objectId: 'obj1', versionId: 'ver1' }
expect(isResourceItemEqual(itemA, itemB)).to.be.false
})
it('returns false if objectId differs', () => {
const itemA = { modelId: 'model1', objectId: 'obj1', versionId: 'ver1' }
const itemB = { modelId: 'model1', objectId: 'obj2', versionId: 'ver1' }
expect(isResourceItemEqual(itemA, itemB)).to.be.false
})
it('returns false if versionId differs', () => {
const itemA = { modelId: 'model1', objectId: 'obj1', versionId: 'ver1' }
const itemB = { modelId: 'model1', objectId: 'obj1', versionId: 'ver2' }
expect(isResourceItemEqual(itemA, itemB)).to.be.false
})
})
describe('doViewerResourcesFit', () => {
it('returns true if any incoming resource matches any requested resource', () => {
const requested = [
{ modelId: 'model1', objectId: 'obj1', versionId: 'ver1' },
{ modelId: 'model2', objectId: 'obj2', versionId: 'ver2' }
]
const incoming = [
{ modelId: 'model3', objectId: 'obj3', versionId: 'ver3' },
{ modelId: 'model2', objectId: 'obj2', versionId: 'ver2' }
]
expect(doViewerResourcesFit(requested, incoming)).to.be.true
})
it('returns false if no incoming resource matches any requested resource', () => {
const requested = [{ modelId: 'model1', objectId: 'obj1', versionId: 'ver1' }]
const incoming = [{ modelId: 'model2', objectId: 'obj2', versionId: 'ver2' }]
expect(doViewerResourcesFit(requested, incoming)).to.be.false
})
it('returns false if both arrays are empty', () => {
expect(doViewerResourcesFit([], [])).to.be.false
})
it('returns false if incomingResources is empty', () => {
const requested = [{ modelId: 'model1', objectId: 'obj1', versionId: 'ver1' }]
expect(doViewerResourcesFit(requested, [])).to.be.false
})
it('returns false if requestedResources is empty', () => {
const incoming = [{ modelId: 'model1', objectId: 'obj1', versionId: 'ver1' }]
expect(doViewerResourcesFit([], incoming)).to.be.false
})
it('returns true if multiple matches exist', () => {
const requested = [
{ modelId: 'model1', objectId: 'obj1', versionId: 'ver1' },
{ modelId: 'model2', objectId: 'obj2', versionId: 'ver2' }
]
const incoming = [
{ modelId: 'model1', objectId: 'obj1', versionId: 'ver1' },
{ modelId: 'model2', objectId: 'obj2', versionId: 'ver2' }
]
expect(doViewerResourcesFit(requested, incoming)).to.be.true
})
})
describe('viewerResourcesToString', () => {
it('returns correct string for model resources with modelId and versionId', () => {
const resources = [
{ modelId: 'model1', objectId: 'obj1', versionId: 'ver1' },
{ modelId: 'model2', objectId: 'obj2', versionId: 'ver2' }
]
// The builder should call addModel for each
const str = viewerResourcesToString(resources)
// Should contain both modelId/versionId pairs, and not just objectIds
expect(str).to.include('model1')
expect(str).to.include('ver1')
expect(str).to.include('model2')
expect(str).to.include('ver2')
})
it('returns correct string for object resources with only objectId', () => {
const resources = [
{ modelId: null, objectId: 'obj1', versionId: null },
{ modelId: undefined, objectId: 'obj2', versionId: undefined }
]
const str = viewerResourcesToString(resources)
expect(str).to.include('obj1')
expect(str).to.include('obj2')
// Should not contain "model" or "ver"
expect(str).to.not.include('model')
expect(str).to.not.include('ver')
})
it('returns correct string for mixed model and object resources', () => {
const resources = [
{ modelId: 'model1', objectId: 'obj1', versionId: 'ver1' },
{ modelId: null, objectId: 'obj2', versionId: null }
]
const str = viewerResourcesToString(resources)
expect(str).to.include('model1')
expect(str).to.include('ver1')
expect(str).to.include('obj2')
})
it('returns empty string for empty resources array', () => {
const str = viewerResourcesToString([])
expect(str).to.equal('')
})
})
})
@@ -169,7 +169,7 @@ export const createTestWorkspace = async (
regionKey?: string
addCreationState?: Pick<WorkspaceCreationState, 'completed' | 'state'>
}
) => {
): Promise<BasicTestWorkspace> => {
const {
domain,
addPlan = true,
@@ -185,7 +185,8 @@ export const createTestWorkspace = async (
// be created as if it was not assigned to a workspace, allowing tests to still work
// (Surely if you explicitly invoke createTestWorkspace with FFs off, you know what you're doing)
workspace.id = undefined as unknown as string
return
workspace.slug = undefined as unknown as string
return workspace as BasicTestWorkspace
}
const upsertWorkspacePlan = upsertWorkspacePlanFactory({ db })
@@ -362,6 +363,14 @@ export const createTestWorkspace = async (
workspaceInput: { domainBasedMembershipProtectionEnabled: true }
})
}
return {
...workspace,
...newWorkspace,
description: workspace.description || undefined,
logo: workspace.logo || undefined,
ownerId: owner.id
}
}
export const buildBasicTestWorkspace = (
@@ -10,6 +10,7 @@ export const fullPermissionCheckResultFragment = gql(`
code
message
payload
errorMessage
}
`)
+1
View File
@@ -37,6 +37,7 @@ export const emailLogger = extendLoggerComponent(logger, 'email')
export const taskSchedulerLogger = extendLoggerComponent(logger, 'task-scheduler')
export const cacheLogger = extendLoggerComponent(logger, 'cache')
export const previewLogger = extendLoggerComponent(logger, 'preview')
export const viewerLogger = extendLoggerComponent(logger, 'viewer')
export type Logger = typeof logger
export { extendLoggerComponent, Observability }
+8 -8
View File
@@ -13,7 +13,7 @@
},
"type": "module",
"engines": {
"node": "^22.6.0"
"node": "^22.17.1"
},
"scripts": {
"build": "tsc -p ./tsconfig.build.json",
@@ -24,25 +24,25 @@
"dev:js": "concurrently \"npm:build:watch\" \"npm:run:watch:js\" \"yarn gqlgen:watch\" -n tsc,server,gqlgen",
"build:clean": "rimraf ./dist && yarn build",
"dev:clean": "yarn build:clean && yarn dev",
"ts-mocha": "tsx ./bin/mocha",
"ts-gqlgen": "tsx ./bin/gqlgen",
"test": "cross-env NODE_ENV=test LOG_FILTER=test LOG_PRETTY=true TSX=true yarn ts-mocha",
"ts-mocha": "node --experimental-strip-types --experimental-transform-types --import ./esmLoader.js ./bin/mocha",
"ts-gqlgen": "tsx --import ./esmLoader.js ./bin/gqlgen",
"test": "cross-env TSX=true NODE_ENV=test LOG_FILTER=test LOG_PRETTY=true yarn ts-mocha",
"test:all-ff": "cross-env ENABLE_ALL_FFS=true yarn test",
"test:multiregion": "cross-env RUN_TESTS_IN_MULTIREGION_MODE=true FF_WORKSPACES_MODULE_ENABLED=true FF_WORKSPACES_MULTI_REGION_ENABLED=true yarn test --grep @multiregion",
"test:no-ff": "cross-env DISABLE_ALL_FFS=true yarn test",
"test:coverage": "cross-env NODE_ENV=test LOG_FILTER=test LOG_PRETTY=true c8 yarn ts-mocha",
"test:coverage": "cross-env NODE_ENV=test LOG_FILTER=test LOG_PRETTY=true c8 yarn test",
"test:report": "MOCHA_FILE=reports/test-results.xml yarn test:coverage -- --reporter mocha-multi --reporter-options spec=-,mocha-junit-reporter=reports/test-results.xml",
"lint": "yarn lint:tsc && yarn lint:eslint",
"lint:ci": "yarn lint:tsc",
"lint:tsc": "tsc --noEmit",
"lint:eslint": "eslint .",
"cli": "cross-env LOG_LEVEL=debug LOG_PRETTY=true NODE_ENV=development TSX=true tsx ./modules/cli/index.ts",
"cli:test": "cross-env LOG_LEVEL=debug LOG_PRETTY=true NODE_ENV=test TSX=true tsx ./modules/cli/index.ts",
"cli": "cross-env LOG_LEVEL=debug LOG_PRETTY=true NODE_ENV=development TSX=true tsx --import ./esmLoader.js ./modules/cli/index.ts",
"cli:test": "cross-env LOG_LEVEL=debug LOG_PRETTY=true NODE_ENV=test TSX=true tsx --import ./esmLoader.js ./modules/cli/index.ts",
"cli:test:multiregion": "cross-env RUN_TESTS_IN_MULTIREGION_MODE=true yarn cli:test",
"cli:download:commit": "cross-env LOG_PRETTY=true LOG_LEVEL=debug yarn cli download commit",
"cli:purge-test-dbs": "yarn cli:test db purge-test-dbs",
"migrate": "yarn cli db migrate",
"migrate:test": "cross-env NODE_ENV=test tsx ./modules/cli/index.js db migrate",
"migrate:test": "cross-env NODE_ENV=test tsx --import ./esmLoader.js ./modules/cli/index.js db migrate",
"gqlgen": "yarn ts-gqlgen --config codegen.ts",
"gqlgen:watch": "yarn gqlgen --watch",
"stripe:listen": "stripe listen --forward-to 127.0.0.1:3000/api/v1/billing/webhooks"

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