Merge branch 'main' into feature/initial-viewer-ui-updates

This commit is contained in:
andrewwallacespeckle
2025-08-08 13:38:36 +01:00
30 changed files with 1640 additions and 241 deletions
@@ -0,0 +1,116 @@
<template>
<FormSelectBase
v-model="selectedValue"
:name="name || 'savedViewGroup'"
:label="label || 'Group'"
:label-id="labelId"
:button-id="buttonId"
mount-menu-on-body
:show-label="showLabel"
:fully-control-value="fullyControlValue"
:disabled="disabled"
:clearable="clearable"
:get-search-results="getSearchResults"
:allow-unset="allowUnset"
search
>
<template #nothing-selected>Select a group</template>
<template #something-selected="{ value }">
<div class="truncate text-foreground capitalize">
{{ isArrayValue(value) ? value.map((v) => v.title).join(', ') : value.title }}
</div>
</template>
<template #option="{ item }">
<div class="flex flex-col space-y-0.5">
<span class="truncate capitalize">{{ item.title }}</span>
</div>
</template>
</FormSelectBase>
</template>
<script setup lang="ts">
import { useFormSelectChildInternals } from '@speckle/ui-components'
import { useApolloClient } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import type { FormSelectSavedViewGroup_SavedViewGroupFragment } from '~/lib/common/generated/gql/graphql'
graphql(`
fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {
id
title
isUngroupedViewsGroup
}
`)
const searchItemsQuery = graphql(`
query FormSelectSavedViewGroup_SavedViewGroups(
$projectId: String!
$input: SavedViewGroupsInput!
) {
project(id: $projectId) {
id
savedViewGroups(input: $input) {
items {
id
...FormSelectSavedViewGroup_SavedViewGroup
}
totalCount
cursor
}
}
}
`)
type ItemType = FormSelectSavedViewGroup_SavedViewGroupFragment
type ValueType = ItemType | ItemType[] | undefined
const emit = defineEmits<{
(e: 'update:modelValue', v: ValueType): void
}>()
const props = withDefaults(
defineProps<{
projectId: string
resourceIdString: string
modelValue?: ValueType
fullyControlValue?: boolean
label?: string
disabled?: boolean
showLabel?: boolean
clearable?: boolean
allowUnset?: boolean
name?: string
}>(),
{
clearable: false,
allowUnset: false
}
)
const apollo = useApolloClient().client
const labelId = useId()
const buttonId = useId()
const { selectedValue, isArrayValue } = useFormSelectChildInternals<ItemType>({
props: toRefs(props),
emit
})
const getSearchResults = async (search: string): Promise<ItemType[]> => {
const res = await apollo
.query({
query: searchItemsQuery,
variables: {
projectId: props.projectId,
input: {
resourceIdString: props.resourceIdString,
search,
limit: 10
}
}
})
.catch(convertThrowIntoFetchResult)
const items = res.data?.project.savedViewGroups.items || []
return items
}
</script>
@@ -1,5 +1,4 @@
<template>
<!-- TODO: Add seat type filter to the query -->
<FormSelectBase
v-model="selectedValue"
:items="seatTypes"
@@ -53,7 +53,8 @@ const color = computed(() => {
const isSelected = computed(() => {
const selObjsIds = selectedObjects.value.map((o) => o.id as string)
return selObjsIds.some((id: string) => props.objectIds.includes(id))
const objectIdsSet = new Set(props.objectIds)
return selObjsIds.some((id: string) => objectIdsSet.has(id))
})
const objectCount = computed(() => {
@@ -101,8 +101,10 @@ const isSelected = computed(() => hasIntersection(objectIds.value, props.item.id
const availableTargetIds = computed(() => {
let targets = props.item.ids
if (isolatedObjectIds.value.length)
targets = props.item.ids.filter((id) => isolatedObjectIds.value.includes(id))
if (isolatedObjectIds.value.length) {
const isolatedSet = new Set(isolatedObjectIds.value)
targets = props.item.ids.filter((id) => isolatedSet.has(id))
}
return targets
})
@@ -121,7 +123,8 @@ const isHidden = computed(() => {
const isIsolated = computed(() => {
if (!isolatedObjectIds.value.length) return true
const ids = props.item.ids
return isolatedObjectIds.value.some((id) => ids.includes(id))
const isolatedSet = new Set(isolatedObjectIds.value)
return ids.some((id) => isolatedSet.has(id))
})
const color = computed(() => {
@@ -20,23 +20,37 @@
<div class="text-body-2xs text-foreground-3 truncate">
{{ view.author?.name }}
</div>
<LayoutMenu
v-model:open="showMenu"
:items="menuItems"
:menu-id="menuId"
mount-menu-on-body
@chosen="({ item: actionItem }) => onActionChosen(actionItem)"
>
<FormButton
size="sm"
color="subtle"
:icon-left="Ellipsis"
hide-text
name="viewActions"
class="shrink-0 opacity-0 group-hover:opacity-100"
@click="showMenu = !showMenu"
/>
</LayoutMenu>
<div class="flex items-center">
<LayoutMenu
v-model:open="showMenu"
:items="menuItems"
:menu-id="menuId"
mount-menu-on-body
@chosen="({ item: actionItem }) => onActionChosen(actionItem)"
>
<FormButton
size="sm"
color="subtle"
:icon-left="Ellipsis"
hide-text
name="viewActions"
class="shrink-0 opacity-0 group-hover:opacity-100"
@click="showMenu = !showMenu"
/>
</LayoutMenu>
<div v-tippy="canUpdate?.errorMessage">
<FormButton
size="sm"
color="subtle"
:icon-left="SquarePen"
hide-text
name="editView"
class="shrink-0 opacity-0 group-hover:opacity-100"
:disabled="!canUpdate?.authorized || isLoading"
@click="showEditDialog = !showEditDialog"
/>
</div>
</div>
</div>
<div class="w-full flex">
<div
@@ -47,13 +61,14 @@
</div>
</div>
</div>
<ViewerSavedViewsPanelViewEditDialog v-model:open="showEditDialog" :view="view" />
</div>
</template>
<script setup lang="ts">
import { StringEnum, throwUncoveredError, type StringEnumValues } from '@speckle/shared'
import type { LayoutMenuItem } from '@speckle/ui-components'
import { useMutationLoading } from '@vue/apollo-composable'
import { Ellipsis } from 'lucide-vue-next'
import { Ellipsis, SquarePen } from 'lucide-vue-next'
import { graphql } from '~/lib/common/generated/gql'
import type { ViewerSavedViewsPanelView_SavedViewFragment } from '~/lib/common/generated/gql/graphql'
import { useEventBus } from '~/lib/core/composables/eventBus'
@@ -80,6 +95,7 @@ graphql(`
}
}
...UseDeleteSavedView_SavedView
...ViewerSavedViewsPanelViewEditDialog_SavedView
}
`)
@@ -91,6 +107,7 @@ const eventBus = useEventBus()
const deleteView = useDeleteSavedView()
const isLoading = useMutationLoading()
const showEditDialog = ref(false)
const showMenu = ref(false)
const menuId = useId()
@@ -0,0 +1,169 @@
<template>
<LayoutDialog
v-model:open="open"
title="Edit view details"
max-width="sm"
:buttons="buttons"
:on-submit="onSubmit"
>
<div class="flex flex-col gap-4">
<FormTextInput
name="name"
label="View name"
show-label
color="foundation"
auto-focus
:rules="[isRequired, isStringOfLength({ maxLength: 255 })]"
/>
<FormTextArea
name="description"
label="Description"
show-label
color="foundation"
placeholder="Add a description..."
:rules="[isStringOfLength({ maxLength: 1000 })]"
/>
<FormSelectSavedViewGroup
name="group"
show-label
:project-id="projectId"
:resource-id-string="resourceIdString"
:rules="[isRequired]"
/>
<FormRadioGroup
:options="radioOptions"
size="sm"
name="visibility"
:rules="[isRequired]"
/>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { FormRadioGroupItem, LayoutDialogButton } from '@speckle/ui-components'
import { Globe, Lock } from 'lucide-vue-next'
import { useForm } from 'vee-validate'
import { graphql } from '~/lib/common/generated/gql'
import {
SavedViewVisibility,
type FormSelectSavedViewGroup_SavedViewGroupFragment,
type ViewerSavedViewsPanelViewEditDialog_SavedViewFragment
} from '~/lib/common/generated/gql/graphql'
import { isRequired, isStringOfLength } from '~/lib/common/helpers/validation'
import { useUpdateSavedView } from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
// TODO: Should we switch to resolvedResourceIdString everywhere?
// TODO: If search for 'Ungrouped' (ungrouped title), then return the ungrouped group too
graphql(`
fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {
id
name
description
visibility
group {
...FormSelectSavedViewGroup_SavedViewGroup
}
...UseUpdateSavedView_SavedView
}
`)
type FormType = {
name: string
description: string | null
visibility: SavedViewVisibility
group: FormSelectSavedViewGroup_SavedViewGroupFragment
}
const props = defineProps<{
view: ViewerSavedViewsPanelViewEditDialog_SavedViewFragment
}>()
const open = defineModel<boolean>('open', {
required: true
})
const { handleSubmit, setValues } = useForm<FormType>()
const {
projectId,
resources: {
request: { resourceIdString }
}
} = useInjectedViewerState()
const updateView = useUpdateSavedView()
const buttons = computed((): LayoutDialogButton[] => [
{
id: 'cancel',
text: 'Cancel',
props: {
color: 'outline'
},
onClick: () => {
open.value = false
}
},
{
id: 'save',
text: 'Save',
submit: true
}
])
const radioOptions = computed((): FormRadioGroupItem<SavedViewVisibility>[] => [
{
value: SavedViewVisibility.Public,
title: 'Public',
introduction: 'Visible to anyone with access to the model.',
icon: Globe
},
{
value: SavedViewVisibility.AuthorOnly,
title: 'Private',
introduction: 'Visible only to the view author.',
icon: Lock
}
])
const onSubmit = handleSubmit(async (values) => {
const name =
values.name.trim() && values.name.trim() !== props.view.name
? values.name.trim()
: null
const description =
values.description?.trim() !== (props.view.description || undefined)
? values.description?.trim() || null
: null
const visibility =
values.visibility !== props.view.visibility ? values.visibility : null
const groupId = values.group.id !== props.view.group.id ? values.group.id : null
const res = await updateView({
view: props.view,
input: {
name,
description,
visibility,
groupId,
id: props.view.id,
projectId: props.view.projectId
}
})
if (res?.id) {
open.value = false
}
})
watch(open, (newVal, oldVal) => {
if (newVal && !oldVal) {
// Reset form state when dialog opens
setValues({
name: props.view.name,
description: props.view.description,
visibility: props.view.visibility,
group: props.view.group
})
}
})
</script>
@@ -44,6 +44,8 @@ type Documents = {
"\n query DashboardSidebar {\n activeUser {\n id\n activeWorkspace {\n id\n role\n }\n }\n }\n": typeof types.DashboardSidebarDocument,
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": typeof types.FormSelectModels_ModelFragmentDoc,
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": typeof types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n": typeof types.FormSelectSavedViewGroup_SavedViewGroupFragmentDoc,
"\n query FormSelectSavedViewGroup_SavedViewGroups(\n $projectId: String!\n $input: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroups(input: $input) {\n items {\n id\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n totalCount\n cursor\n }\n }\n }\n": typeof types.FormSelectSavedViewGroup_SavedViewGroupsDocument,
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": typeof types.FormUsersSelectItemFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherWorkspaceListItem_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n": typeof types.HeaderWorkspaceSwitcherWorkspaceListItem_WorkspaceFragmentDoc,
"\n fragment WorkspaceSwitcherActiveWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n slug\n role\n }\n": typeof types.WorkspaceSwitcherActiveWorkspace_LimitedWorkspaceFragmentDoc,
@@ -165,9 +167,10 @@ type Documents = {
"\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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\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 ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n isUngroupedViewsGroup\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\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 fragment ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": typeof types.ViewerSavedViewsPanelViewsGroupInner_SavedViewGroupFragmentDoc,
@@ -405,6 +408,8 @@ type 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": typeof types.CreateSavedViewDocument,
"\n mutation DeleteSavedView($input: DeleteSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n deleteView(input: $input)\n }\n }\n }\n": typeof types.DeleteSavedViewDocument,
"\n fragment UseDeleteSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n": typeof types.UseDeleteSavedView_SavedViewFragmentDoc,
"\n mutation UpdateSavedView($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n": typeof types.UpdateSavedViewDocument,
"\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n": typeof types.UseUpdateSavedView_SavedViewFragmentDoc,
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n": typeof types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": typeof types.ViewerCommentThreadFragmentDoc,
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": typeof types.ViewerCommentsReplyItemFragmentDoc,
@@ -533,6 +538,8 @@ const documents: Documents = {
"\n query DashboardSidebar {\n activeUser {\n id\n activeWorkspace {\n id\n role\n }\n }\n }\n": types.DashboardSidebarDocument,
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": types.FormSelectModels_ModelFragmentDoc,
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n": types.FormSelectSavedViewGroup_SavedViewGroupFragmentDoc,
"\n query FormSelectSavedViewGroup_SavedViewGroups(\n $projectId: String!\n $input: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroups(input: $input) {\n items {\n id\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n totalCount\n cursor\n }\n }\n }\n": types.FormSelectSavedViewGroup_SavedViewGroupsDocument,
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherWorkspaceListItem_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n": types.HeaderWorkspaceSwitcherWorkspaceListItem_WorkspaceFragmentDoc,
"\n fragment WorkspaceSwitcherActiveWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n slug\n role\n }\n": types.WorkspaceSwitcherActiveWorkspace_LimitedWorkspaceFragmentDoc,
@@ -654,9 +661,10 @@ const documents: Documents = {
"\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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\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 ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n isUngroupedViewsGroup\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\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 fragment ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": types.ViewerSavedViewsPanelViewsGroupInner_SavedViewGroupFragmentDoc,
@@ -894,6 +902,8 @@ const documents: 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": types.CreateSavedViewDocument,
"\n mutation DeleteSavedView($input: DeleteSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n deleteView(input: $input)\n }\n }\n }\n": types.DeleteSavedViewDocument,
"\n fragment UseDeleteSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n": types.UseDeleteSavedView_SavedViewFragmentDoc,
"\n mutation UpdateSavedView($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n": types.UpdateSavedViewDocument,
"\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n": types.UseUpdateSavedView_SavedViewFragmentDoc,
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n": types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": types.ViewerCommentThreadFragmentDoc,
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": types.ViewerCommentsReplyItemFragmentDoc,
@@ -1126,6 +1136,14 @@ export function graphql(source: "\n fragment FormSelectModels_Model on Model {\
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n"): (typeof documents)["\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n"): (typeof documents)["\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query FormSelectSavedViewGroup_SavedViewGroups(\n $projectId: String!\n $input: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroups(input: $input) {\n items {\n id\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n totalCount\n cursor\n }\n }\n }\n"): (typeof documents)["\n query FormSelectSavedViewGroup_SavedViewGroups(\n $projectId: String!\n $input: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroups(input: $input) {\n items {\n id\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n totalCount\n cursor\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1613,7 +1631,7 @@ export function graphql(source: "\n fragment ViewerSavedViewsPanel_Project on P
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n }\n"];
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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1622,6 +1640,10 @@ export function graphql(source: "\n fragment ViewerSavedViewsPanelViews_Project
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n 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 ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2570,6 +2592,14 @@ export function graphql(source: "\n mutation DeleteSavedView($input: DeleteSave
* 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 UseDeleteSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n"): (typeof documents)["\n fragment UseDeleteSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation UpdateSavedView($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateSavedView($input: UpdateSavedViewInput!) {\n projectMutations {\n savedViewMutations {\n updateView(input: $input) {\n id\n ...ViewerSavedViewsPanelView_SavedView\n group {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n"): (typeof documents)["\n fragment UseUpdateSavedView_SavedView on SavedView {\n id\n projectId\n group {\n id\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -649,6 +649,10 @@ export const modifyObjectField = <
* Build a reference object for a specific object in the cache
*/
ref: typeof getObjectReference
/**
* Parse a reference object to get its type and id separately
*/
fromRef: typeof parseObjectReference
}
}) =>
| Optional<ModifyObjectFieldValue<Type, Field>>
@@ -744,7 +748,8 @@ export const modifyObjectField = <
get: getIfExists,
evict,
readField,
ref: getObjectReference
ref: getObjectReference,
fromRef: parseObjectReference
}
})
},
@@ -752,6 +757,72 @@ export const modifyObjectField = <
)
}
/**
* Iterate over object field versions (same field can have different arg versions). If you also
* want to make updates, use modifyObjectField instead.
*/
export const iterateObjectField = <
Type extends keyof AllObjectTypes,
Field extends keyof AllObjectTypes[Type]
>(
cache: ApolloCache<unknown>,
key: ApolloCacheObjectKey<Type>,
fieldName: Field,
predicate: (params: {
fieldName: string
variables: Field extends keyof AllObjectFieldArgTypes[Type]
? AllObjectFieldArgTypes[Type][Field]
: never
/**
* Value found in the cache. Read-only and should not be mutated directly. Use the
* createUpdatedValue() helper to build a new value with updated fields.
*/
value: ReadonlyDeep<ModifyObjectFieldValue<Type, Field>>
helpers: {
/**
* Get value from specific path, only if it exists in the cache value
*/
get: <Path extends Paths<ModifyObjectFieldValue<Type, Field>> & string>(
path: Path
) => Optional<Get<ModifyObjectFieldValue<Type, Field>, Path>>
/**
* Read field data from a Reference object
*/
readField: <
ReadFieldType extends keyof AllObjectTypes,
ReadFieldName extends keyof AllObjectTypes[ReadFieldType] & string
>(
ref: CacheObjectReference<ReadFieldType>,
fieldName: ReadFieldName
) => Optional<AllObjectTypes[ReadFieldType][ReadFieldName]>
/**
* Build a reference object for a specific object in the cache
*/
ref: typeof getObjectReference
/**
* Parse a reference object to get its type and id separately
*/
fromRef: typeof parseObjectReference
}
}) => void,
options?: Partial<{
debug: boolean
}>
) => {
modifyObjectField<Type, Field>(
cache,
key,
fieldName,
(params) => {
predicate(params)
},
{
...(options || {}),
autoEvictFiltered: false // no mutations here
}
)
}
export const hasErrorWith = (params: {
errors: readonly GraphQLFormattedError[] | undefined
codes?: Array<string | RegExp>
@@ -2,11 +2,16 @@ import { useMutation } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import type {
CreateSavedViewInput,
UseDeleteSavedView_SavedViewFragment
UpdateSavedViewInput,
UseDeleteSavedView_SavedViewFragment,
UseUpdateSavedView_SavedViewFragment
} from '~/lib/common/generated/gql/graphql'
import { parseObjectReference } from '~/lib/common/helpers/graphql'
import { useStateSerialization } from '~/lib/viewer/composables/serialization'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import {
onGroupViewRemovalCacheUpdates,
onNewGroupViewCacheUpdates
} from '~/lib/viewer/helpers/savedViews/cache'
const createSavedViewMutation = graphql(`
mutation CreateSavedView($input: CreateSavedViewInput!) {
@@ -62,38 +67,11 @@ export const useCreateSavedView = () => {
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 }
)
onNewGroupViewCacheUpdates(cache, {
viewId,
groupId,
projectId: projectId.value
})
}
}
).catch(convertThrowIntoFetchResult)
@@ -101,7 +79,7 @@ export const useCreateSavedView = () => {
const res = result?.data?.projectMutations.savedViewMutations.createView
if (res?.id) {
triggerNotification({
title: 'Saved View Created',
title: 'Saved view created',
type: ToastNotificationType.Success
})
} else {
@@ -140,12 +118,13 @@ graphql(`
export const useDeleteSavedView = () => {
const { mutate } = useMutation(deleteSavedViewMutation)
const { triggerNotification } = useGlobalToast()
const { isLoggedIn } = useActiveUser()
return async (params: { view: UseDeleteSavedView_SavedViewFragment }) => {
const { id, projectId } = params.view
const groupId = params.view.group.id
if (!id || !projectId) {
if (!id || !projectId || !isLoggedIn.value) {
return
}
@@ -157,64 +136,28 @@ export const useDeleteSavedView = () => {
}
},
{
update: (cache) => {
update: (cache, res) => {
if (!res.data?.projectMutations.savedViewMutations.deleteView) return
onGroupViewRemovalCacheUpdates(cache, {
viewId: id,
groupId,
projectId
})
// Remove the view from the cache
cache.evict({ id: getCacheId('SavedView', id) })
// Check if default/ungrouped group
const isDefaultGroup = groupId.startsWith('default-')
// If default group and its now empty - remove it as it doesn't exist otherwise
let shouldEvict
if (isDefaultGroup) {
let viewsRemain = false
modifyObjectField(
cache,
getCacheId('SavedViewGroup', groupId),
'views',
({ value }) => {
const otherItems = value?.items?.filter(
(item) => item.__ref !== getCacheId('SavedView', id)
)
if (otherItems?.length) {
viewsRemain = true
}
}
)
if (!viewsRemain) {
shouldEvict = true
}
}
// Remove default group, if its empty
if (shouldEvict) {
cache.evict({ id: getCacheId('SavedViewGroup', groupId) })
} else {
// Remove view from view lists (in groups)
// SavedViewGroup.views
modifyObjectField(
cache,
getCacheId('SavedViewGroup', groupId),
'views',
({ helpers: { createUpdatedValue } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (count) => count - 1)
update('items', (items) =>
items.filter((item) => parseObjectReference(item).id !== id)
)
})
},
{ autoEvictFiltered: true }
)
}
}
}
).catch(convertThrowIntoFetchResult)
const res = result?.data?.projectMutations.savedViewMutations.deleteView
if (!res) {
if (res) {
triggerNotification({
title: 'View deleted',
type: ToastNotificationType.Success
})
} else {
const err = getFirstGqlErrorMessage(result?.errors)
triggerNotification({
title: "Couldn't delete saved view",
@@ -226,3 +169,91 @@ export const useDeleteSavedView = () => {
return res
}
}
const updateSavedViewMutation = graphql(`
mutation UpdateSavedView($input: UpdateSavedViewInput!) {
projectMutations {
savedViewMutations {
updateView(input: $input) {
id
...ViewerSavedViewsPanelView_SavedView
group {
id
...ViewerSavedViewsPanelViewsGroup_SavedViewGroup
}
}
}
}
}
`)
graphql(`
fragment UseUpdateSavedView_SavedView on SavedView {
id
projectId
group {
id
}
}
`)
export const useUpdateSavedView = () => {
const { mutate } = useMutation(updateSavedViewMutation)
const { triggerNotification } = useGlobalToast()
const { isLoggedIn } = useActiveUser()
return async (params: {
view: UseUpdateSavedView_SavedViewFragment
input: UpdateSavedViewInput
}) => {
if (!isLoggedIn.value) return
const { input } = params
const oldGroupId = params.view.group.id
const result = await mutate(
{ input },
{
update: (cache, res) => {
const update = res.data?.projectMutations.savedViewMutations.updateView
if (!update) return
const newGroupId = update.group.id
const groupChanged = oldGroupId !== newGroupId
if (groupChanged) {
// Clean up old group
onGroupViewRemovalCacheUpdates(cache, {
viewId: params.view.id,
groupId: oldGroupId,
projectId: params.view.projectId
})
// Update new group
onNewGroupViewCacheUpdates(cache, {
viewId: update.id,
groupId: newGroupId,
projectId: params.view.projectId
})
}
}
}
).catch(convertThrowIntoFetchResult)
const res = result?.data?.projectMutations.savedViewMutations.updateView
if (res?.id) {
triggerNotification({
title: 'View updated',
type: ToastNotificationType.Success
})
} else {
const err = getFirstGqlErrorMessage(result?.errors)
triggerNotification({
title: "Couldn't update saved view",
description: err,
type: ToastNotificationType.Danger
})
}
return res
}
}
@@ -0,0 +1,138 @@
import type { ApolloCache } from '@apollo/client/cache'
/**
* Cache mutations for when a group gets a new view:
* - If new group, Project.savedViewGroups + 1
* - SavedViewGroup.views + 1
*/
export const onNewGroupViewCacheUpdates = (
cache: ApolloCache<unknown>,
params: {
/**
* The ID of the view being added
*/
viewId: string
/**
* The ID of the group the view is being added to
*/
groupId: string
projectId: string
}
) => {
const { viewId, groupId, projectId } = params
// Project.savedViewGroups + 1, if it is a new group
modifyObjectField(
cache,
getCacheId('Project', projectId),
'savedViewGroups',
({ helpers: { createUpdatedValue, ref, fromRef }, value }) => {
const isNewGroup = !value?.items?.some((group) => fromRef(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 }
)
}
/**
* Cache mutations for when a view is removed from a group:
* - If default group and it is now empty, remove it entirely - evict and remove from Project.savedViewGroups
* - Otherwise just: SavedViewGroup.views - 1
*/
export const onGroupViewRemovalCacheUpdates = (
cache: ApolloCache<unknown>,
params: {
/**
* The ID of the view being removed
*/
viewId: string
/**
* The ID of the group the view is being removed from
*/
groupId: string
projectId: string
}
) => {
const { viewId: id, groupId, projectId } = params
// Check if default/ungrouped group
const isDefaultGroup = groupId.startsWith('default-')
// If default group and its now empty - remove it as it doesn't exist otherwise
let shouldEvict
if (isDefaultGroup) {
let viewsRemain = false
iterateObjectField(
cache,
getCacheId('SavedViewGroup', groupId),
'views',
({ value, helpers: { fromRef } }) => {
const otherItems = value?.items?.filter((item) => fromRef(item).id !== id)
if (otherItems?.length) {
viewsRemain = true
}
}
)
if (!viewsRemain) {
shouldEvict = true
}
}
// Remove default group, if its empty
if (shouldEvict) {
// Project.savedViewGroups - 1
modifyObjectField(
cache,
getCacheId('Project', projectId),
'savedViewGroups',
({ helpers: { createUpdatedValue, fromRef } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (count) => count - 1)
update('items', (items) =>
items.filter((item) => fromRef(item).id !== groupId)
)
})
},
{ autoEvictFiltered: true }
)
// Evict entirely
cache.evict({ id: getCacheId('SavedViewGroup', groupId) })
} else {
// Remove view from view lists (in groups)
// SavedViewGroup.views - 1
modifyObjectField(
cache,
getCacheId('SavedViewGroup', groupId),
'views',
({ helpers: { createUpdatedValue, fromRef } }) => {
return createUpdatedValue(({ update }) => {
update('totalCount', (count) => count - 1)
update('items', (items) => items.filter((item) => fromRef(item).id !== id))
})
},
{ autoEvictFiltered: true }
)
}
}
+2
View File
@@ -6,6 +6,7 @@ import {
convertThrowIntoFetchResult,
getCacheId,
getFirstErrorMessage as getFirstGqlErrorMessage,
iterateObjectField,
modifyObjectField,
ROOT_MUTATION,
ROOT_QUERY,
@@ -32,6 +33,7 @@ export {
convertThrowIntoFetchResult,
getFirstGqlErrorMessage,
modifyObjectField,
iterateObjectField,
getCacheId,
checkIfIsInPlaceNavigation,
ROOT_QUERY,
@@ -170,10 +170,47 @@ input DeleteSavedViewInput {
projectId: ID!
}
input UpdateSavedViewInput {
id: ID!
projectId: ID!
"""
New resource targets, if necessary. Must be set together w/ viewerState & screenshot.
"""
resourceIdString: String
"""
New group id, if grouping necessary
"""
groupId: String
"""
New name for the view
"""
name: String
description: String
"""
SerializedViewerState. If omitted, comment won't render (correctly) inside the
viewer, but will still be retrievable through the API.
Must be set together w/ resourceIdString & screenshot.
"""
viewerState: JSONObject
"""
Encoded screenshot of the view.
"""
screenshot: String
"""
Optionally also set this as the home/default view for the target model
"""
isHomeView: Boolean
"""
Optionally change visibility of the view
"""
visibility: SavedViewVisibility
}
type SavedViewMutations {
createGroup(input: CreateSavedViewGroupInput!): SavedViewGroup!
createView(input: CreateSavedViewInput!): SavedView!
deleteView(input: DeleteSavedViewInput!): Boolean!
updateView(input: UpdateSavedViewInput!): SavedView!
}
extend type ProjectMutations {
@@ -4,7 +4,7 @@ import { StringEnum } from '@speckle/shared'
export const ImporterAutomateFunctions = {
svf2: {
functionId: '4665e0b3ba',
functionReleaseId: '4cda76f8a9'
functionReleaseId: 'ad5f3e7cfe'
}
}
@@ -5,7 +5,7 @@ import type {
import type { InsertableAutomationRun } from '@/modules/automate/repositories/automations'
import type { GetCommit } from '@/modules/core/domain/commits/operations'
import type { LegacyGetUser } from '@/modules/core/domain/users/operations'
import { CommitNotFoundError } from '@/modules/core/errors/commit'
import { logger } from '@/observability/logging'
import { throwUncoveredError } from '@speckle/shared'
export type AutomateTrackingDeps = {
@@ -28,7 +28,16 @@ export const getUserEmailFromAutomationRunFactory =
const version = await deps.getCommit(trigger.triggeringId, {
streamId: projectId
})
if (!version) throw new CommitNotFoundError("Version doesn't exist any more")
// TODO: This is an error when ACC is using the correct trigger types
if (!version) {
logger.warn(
{
versionId: trigger.triggeringId
},
'Version {versionId} not found for automation run.'
)
return userEmail
}
const userId = version.author
if (userId) {
const user = await deps.getUser(userId)
@@ -3516,6 +3516,7 @@ export type SavedViewMutations = {
createGroup: SavedViewGroup;
createView: SavedView;
deleteView: Scalars['Boolean']['output'];
updateView: SavedView;
};
@@ -3533,6 +3534,11 @@ export type SavedViewMutationsDeleteViewArgs = {
input: DeleteSavedViewInput;
};
export type SavedViewMutationsUpdateViewArgs = {
input: UpdateSavedViewInput;
};
export type SavedViewPermissionChecks = {
__typename?: 'SavedViewPermissionChecks';
canUpdate: PermissionCheckResult;
@@ -4356,6 +4362,30 @@ export type UpdateModelInput = {
projectId: Scalars['ID']['input'];
};
export type UpdateSavedViewInput = {
description?: InputMaybe<Scalars['String']['input']>;
/** New group id, if grouping necessary */
groupId?: InputMaybe<Scalars['String']['input']>;
id: Scalars['ID']['input'];
/** Optionally also set this as the home/default view for the target model */
isHomeView?: InputMaybe<Scalars['Boolean']['input']>;
/** New name for the view */
name?: InputMaybe<Scalars['String']['input']>;
projectId: Scalars['ID']['input'];
/** New resource targets, if necessary. Must be set together w/ viewerState & screenshot. */
resourceIdString?: InputMaybe<Scalars['String']['input']>;
/** Encoded screenshot of the view. */
screenshot?: InputMaybe<Scalars['String']['input']>;
/**
* SerializedViewerState. If omitted, comment won't render (correctly) inside the
* viewer, but will still be retrievable through the API.
* Must be set together w/ resourceIdString & screenshot.
*/
viewerState?: InputMaybe<Scalars['JSONObject']['input']>;
/** Optionally change visibility of the view */
visibility?: InputMaybe<SavedViewVisibility>;
};
export type UpdateServerRegionInput = {
description?: InputMaybe<Scalars['String']['input']>;
key: Scalars['String']['input'];
@@ -6043,6 +6073,7 @@ export type ResolversTypes = {
UpdateAccSyncItemInput: UpdateAccSyncItemInput;
UpdateAutomateFunctionInput: UpdateAutomateFunctionInput;
UpdateModelInput: UpdateModelInput;
UpdateSavedViewInput: UpdateSavedViewInput;
UpdateServerRegionInput: UpdateServerRegionInput;
UpdateVersionInput: UpdateVersionInput;
UpgradePlanInput: UpgradePlanInput;
@@ -6394,6 +6425,7 @@ export type ResolversParentTypes = {
UpdateAccSyncItemInput: UpdateAccSyncItemInput;
UpdateAutomateFunctionInput: UpdateAutomateFunctionInput;
UpdateModelInput: UpdateModelInput;
UpdateSavedViewInput: UpdateSavedViewInput;
UpdateServerRegionInput: UpdateServerRegionInput;
UpdateVersionInput: UpdateVersionInput;
UpgradePlanInput: UpgradePlanInput;
@@ -7718,6 +7750,7 @@ export type SavedViewMutationsResolvers<ContextType = GraphQLContext, ParentType
createGroup?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType, RequireFields<SavedViewMutationsCreateGroupArgs, 'input'>>;
createView?: Resolver<ResolversTypes['SavedView'], ParentType, ContextType, RequireFields<SavedViewMutationsCreateViewArgs, 'input'>>;
deleteView?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<SavedViewMutationsDeleteViewArgs, 'input'>>;
updateView?: Resolver<ResolversTypes['SavedView'], ParentType, ContextType, RequireFields<SavedViewMutationsUpdateViewArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
@@ -9059,6 +9092,13 @@ export type CanUpdateSavedViewQueryVariables = Exact<{
export type CanUpdateSavedViewQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, savedView: { __typename?: 'SavedView', id: string, permissions: { __typename?: 'SavedViewPermissionChecks', canUpdate: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null } } } } };
export type UpdateSavedViewMutationVariables = Exact<{
input: UpdateSavedViewInput;
}>;
export type UpdateSavedViewMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', savedViewMutations: { __typename?: 'SavedViewMutations', updateView: { __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 };
@@ -10189,6 +10229,7 @@ export const GetProjectSavedViewDocument = {"kind":"Document","definitions":[{"k
export const DeleteSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteSavedViewInput"}}}}],"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":"deleteView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<DeleteSavedViewMutation, DeleteSavedViewMutationVariables>;
export const CanCreateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CanCreateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canCreateSavedView"},"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<CanCreateSavedViewQuery, CanCreateSavedViewQueryVariables>;
export const CanUpdateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CanUpdateSavedView"},"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":"id"}},{"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":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canUpdate"},"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<CanUpdateSavedViewQuery, CanUpdateSavedViewQueryVariables>;
export const UpdateSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateSavedViewInput"}}}}],"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":"updateView"},"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<UpdateSavedViewMutation, UpdateSavedViewMutationVariables>;
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>;
+17 -1
View File
@@ -110,6 +110,21 @@ export async function buildRequestLoaders(
return regionLoaders.get(deps.db) as ModularizedDataLoaders
}
/**
* Do something for each region, including mainDb (e.g. clear loader in all regions). This only
* loops over initiated/cached regions, not all server registrated regions.
*/
const forEachCachedRegion = (
predicate: (params: { db: Knex; loaders: ModularizedDataLoaders }) => void
) => {
for (const [db, loaders] of [
...regionLoaders.entries(),
<const>[mainDb, mainDbLoaders]
]) {
predicate({ db, loaders })
}
}
/**
* Clear all request loaders across all regions
*/
@@ -130,7 +145,8 @@ export async function buildRequestLoaders(
return {
...mainDbLoaders,
clearAll,
forRegion
forRegion,
forEachCachedRegion
}
}
@@ -92,10 +92,7 @@ export type GetGroupSavedViewsPageItems = (
export type GetSavedViewGroup = (params: {
id: string
/**
* If undefined, skip project ID check
*/
projectId: string | undefined
projectId: string
}) => Promise<SavedViewGroup | undefined>
export type GetUngroupedSavedViewsGroup = (params: {
@@ -122,10 +119,23 @@ export type GetSavedViews = (params: {
[viewId: string]: SavedView | undefined
}>
export type GetSavedView = (params: {
id: string
projectId: string
}) => Promise<SavedView | undefined>
export type DeleteSavedViewRecord = (params: {
savedViewId: string
}) => Promise<boolean>
export type UpdateSavedViewRecord = <
Update extends Exact<Partial<SavedView>, Update>
>(params: {
id: string
projectId: string
update: Update
}) => Promise<SavedView | undefined>
/////////////////////
// SERVICE OPERATIONS:
/////////////////////
@@ -180,3 +190,21 @@ export type DeleteSavedView = (params: {
projectId: string
userId: string
}) => Promise<void>
export type UpdateSavedViewParams = {
id: string
projectId: string
groupId?: MaybeNullOrUndefined<string>
name?: MaybeNullOrUndefined<string>
description?: MaybeNullOrUndefined<string>
isHomeView?: MaybeNullOrUndefined<boolean>
visibility?: MaybeNullOrUndefined<SavedViewVisibility>
viewerState?: MaybeNullOrUndefined<unknown>
resourceIdString?: MaybeNullOrUndefined<string>
screenshot?: MaybeNullOrUndefined<string>
}
export type UpdateSavedView = (params: {
input: UpdateSavedViewParams
userId: string
}) => Promise<SavedView>
@@ -17,3 +17,9 @@ export class SavedViewInvalidResourceTargetError extends BaseError {
static defaultMessage = 'Invalid resource ids specified'
static statusCode = 400
}
export class SavedViewUpdateValidationError extends BaseError {
static code = 'SAVED_VIEW_UPDATE_VALIDATION_ERROR'
static defaultMessage = 'Saved view update failed due to a validation error'
static statusCode = 400
}
@@ -22,19 +22,20 @@ import {
getGroupSavedViewsTotalCountFactory,
getProjectSavedViewGroupsPageItemsFactory,
getProjectSavedViewGroupsTotalCountFactory,
getSavedViewGroupFactory,
getStoredViewCountFactory,
getUngroupedSavedViewsGroupFactory,
recalculateGroupResourceIdsFactory,
storeSavedViewFactory,
storeSavedViewGroupFactory
storeSavedViewGroupFactory,
updateSavedViewRecordFactory
} from '@/modules/viewer/repositories/savedViews'
import {
createSavedViewFactory,
createSavedViewGroupFactory,
deleteSavedViewFactory,
getGroupSavedViewsFactory,
getProjectSavedViewGroupsFactory
getProjectSavedViewGroupsFactory,
updateSavedViewFactory
} from '@/modules/viewer/services/savedViewsManagement'
import { getViewerResourceGroupsFactory } from '@/modules/viewer/services/viewerResources'
import { Authz } from '@speckle/shared'
@@ -43,6 +44,10 @@ 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'
import {
getSavedViewFactory,
getSavedViewGroupFactory
} from '@/modules/viewer/repositories/dataLoaders/savedViews'
const buildGetViewerResourceGroups = (params: { projectDb: Knex }) => {
const { projectDb } = params
@@ -219,7 +224,7 @@ const resolvers: Resolvers = {
getViewerResourceGroups: buildGetViewerResourceGroups({ projectDb }),
getStoredViewCount: getStoredViewCountFactory({ db: projectDb }),
storeSavedView: storeSavedViewFactory({ db: projectDb }),
getSavedViewGroup: getSavedViewGroupFactory({ db: projectDb }),
getSavedViewGroup: getSavedViewGroupFactory({ loaders: ctx.loaders }),
recalculateGroupResourceIds: recalculateGroupResourceIdsFactory({
db: projectDb
})
@@ -255,6 +260,54 @@ const resolvers: Resolvers = {
return true
},
updateView: async (_parent, args, ctx) => {
const projectId = args.input.projectId
const projectDb = await getProjectDbClient({ projectId })
throwIfResourceAccessNotAllowed({
resourceId: projectId,
resourceType: TokenResourceIdentifierType.Project,
resourceAccessRules: ctx.resourceAccessRules
})
const canUpdate = await ctx.authPolicies.project.savedViews.canUpdate({
userId: ctx.userId,
projectId,
savedViewId: args.input.id
})
throwIfAuthNotOk(canUpdate)
const updateSavedView = updateSavedViewFactory({
getViewerResourceGroups: buildGetViewerResourceGroups({ projectDb }),
getSavedView: getSavedViewFactory({ loaders: ctx.loaders }),
getSavedViewGroup: getSavedViewGroupFactory({ loaders: ctx.loaders }),
updateSavedViewRecord: updateSavedViewRecordFactory({
db: projectDb
})
})
const updatedView = await updateSavedView({
input: args.input,
userId: ctx.userId!
})
// update loader cache
ctx.loaders.forEachCachedRegion(({ loaders }) => {
loaders.savedViews.getSavedView.clear({
viewId: updatedView.id,
projectId: updatedView.projectId
})
loaders.savedViews.getSavedView.prime(
{
viewId: updatedView.id,
projectId: updatedView.projectId
},
updatedView
)
})
return updatedView
},
createGroup: async (_parent, args, ctx) => {
const projectId = args.input.projectId
throwIfResourceAccessNotAllowed({
@@ -1,5 +1,6 @@
import { base64Decode, base64Encode } from '@/modules/shared/helpers/cryptoHelper'
import type { Nullable } from '@speckle/shared'
import type { ViewerResourcesTarget } from '@speckle/shared/viewer/route'
import {
isModelResource,
isObjectResource,
@@ -50,13 +51,9 @@ export const decodeDefaultGroupId = (id: string): Nullable<DefaultGroupMetadata>
* 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
export const formatResourceIdsForGroup = (resources: ViewerResourcesTarget) => {
return resourceBuilder()
.addFromString(resourceIdString)
.addResources(resources)
.forEach((r) => {
if (isModelResource(r)) {
// not interested in the specific version ids originally used
@@ -0,0 +1,32 @@
import type { RequestDataLoaders } from '@/modules/core/loaders'
import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import type {
GetSavedView,
GetSavedViewGroup
} from '@/modules/viewer/domain/operations/savedViews'
export const getSavedViewFactory =
(deps: { loaders: RequestDataLoaders }): GetSavedView =>
async ({ id, projectId }) => {
const projectDb = await getProjectDbClient({ projectId })
return (
(await deps.loaders.forRegion({ db: projectDb }).savedViews.getSavedView.load({
viewId: id,
projectId
})) || undefined
)
}
export const getSavedViewGroupFactory =
(deps: { loaders: RequestDataLoaders }): GetSavedViewGroup =>
async ({ id, projectId }) => {
const projectDb = await getProjectDbClient({ projectId })
return (
(await deps.loaders
.forRegion({ db: projectDb })
.savedViews.getSavedViewGroup.load({
groupId: id,
projectId
})) || undefined
)
}
@@ -15,7 +15,8 @@ import type {
StoreSavedView,
StoreSavedViewGroup,
GetSavedViews,
DeleteSavedViewRecord
DeleteSavedViewRecord,
UpdateSavedViewRecord
} from '@/modules/viewer/domain/operations/savedViews'
import {
SavedViewVisibility,
@@ -504,3 +505,20 @@ export const deleteSavedViewRecordFactory =
// Otherwise, return true
return true
}
export const updateSavedViewRecordFactory =
(deps: { db: Knex }): UpdateSavedViewRecord =>
async (params) => {
const { id, projectId, update } = params
// Update the saved view
const [updatedView] = await tables
.savedViews(deps.db)
.where({
[SavedViews.col.id]: id,
[SavedViews.col.projectId]: projectId
})
.update(update, '*')
return updatedView || undefined
}
@@ -9,23 +9,32 @@ import type {
GetProjectSavedViewGroups,
GetProjectSavedViewGroupsPageItems,
GetProjectSavedViewGroupsTotalCount,
GetSavedView,
GetSavedViewGroup,
GetStoredViewCount,
RecalculateGroupResourceIds,
StoreSavedView,
StoreSavedViewGroup
StoreSavedViewGroup,
UpdateSavedView,
UpdateSavedViewRecord
} from '@/modules/viewer/domain/operations/savedViews'
import { SavedViewVisibility } from '@/modules/viewer/domain/types/savedViews'
import {
SavedViewCreationValidationError,
SavedViewGroupCreationValidationError,
SavedViewInvalidResourceTargetError
SavedViewInvalidResourceTargetError,
SavedViewUpdateValidationError
} from '@/modules/viewer/errors/savedViews'
import type { ResourceBuilder } from '@speckle/shared/viewer/route'
import { resourceBuilder } from '@speckle/shared/viewer/route'
import type { VersionedSerializedViewerState } from '@speckle/shared/viewer/state'
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'
import { omit } from 'lodash-es'
import type { DependenciesOf } from '@/modules/shared/helpers/factory'
import { removeNullOrUndefinedKeys } from '@speckle/shared'
/**
* Validates an incoming resourceIdString against the resources in the project and returns the validated list (as a builder)
@@ -80,6 +89,47 @@ const validateProjectResourceIdStringFactory =
return resourceIds
}
const validateViewerStateFactory =
() =>
(params: {
viewerState: unknown
projectId: string
resourceIds: ResourceBuilder
errorMetadata: Record<string, unknown>
}) => {
const { viewerState, projectId, resourceIds, errorMetadata } = params
const state = inputToVersionedState(viewerState)
if (!state) {
throw new SavedViewInvalidResourceTargetError(
'Invalid viewer state provided. Must be a valid SerializedViewerState.',
{
info: errorMetadata
}
)
}
// Validate state match
if (state.state.resources.request.resourceIdString !== resourceIds.toString()) {
throw new SavedViewInvalidResourceTargetError(
'Viewer state does not match the provided resourceIdString.',
{
info: errorMetadata
}
)
}
if (state.state.projectId !== projectId) {
throw new SavedViewInvalidResourceTargetError(
'Viewer state projectId does not match the provided projectId.',
{
info: errorMetadata
}
)
}
return state
}
export const createSavedViewFactory =
(deps: {
getViewerResourceGroups: GetViewerResourceGroups
@@ -119,42 +169,16 @@ export const createSavedViewFactory =
)
}
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 state
const state = validateViewerStateFactory()({
viewerState: input.viewerState,
projectId,
resourceIds,
errorMetadata: {
input,
authorId
}
})
// Validate groupId - group is a valid and accessible group in the project
if (groupId) {
@@ -180,6 +204,16 @@ export const createSavedViewFactory =
if (!name?.length) {
const viewCount = await deps.getStoredViewCount({ projectId })
name = `View - ${String(viewCount + 1).padStart(3, '0')}`
} else if (name.length > 255) {
throw new SavedViewCreationValidationError(
'View name must be between 1 and 255 characters long',
{
info: {
input,
authorId
}
}
)
}
const concreteResourceIds = resourceIds.toResources().map((r) => r.toString())
@@ -297,3 +331,159 @@ export const deleteSavedViewFactory =
const { id } = params
await deps.deleteSavedViewRecord({ savedViewId: id })
}
export const updateSavedViewFactory =
(
deps: {
getSavedView: GetSavedView
getSavedViewGroup: GetSavedViewGroup
updateSavedViewRecord: UpdateSavedViewRecord
} & DependenciesOf<typeof validateProjectResourceIdStringFactory>
): UpdateSavedView =>
async (params) => {
const { input, userId } = params
const { projectId, id } = input
// Check if view even exists
const view = await deps.getSavedView({
id: input.id,
projectId
})
if (!view) {
throw new SavedViewUpdateValidationError(
"The specified saved view doesn't exist",
{
info: {
input,
userId
}
}
)
}
// Validate that required fields are set
const hasResourceIdString = 'resourceIdString' in input && input.resourceIdString
const hasViewerState = 'viewerState' in input && input.viewerState
const hasScreenshot = 'screenshot' in input && input.screenshot
if (hasResourceIdString || hasViewerState) {
if (!hasResourceIdString || !hasViewerState || !hasScreenshot) {
throw new SavedViewUpdateValidationError(
'If the resourceIdString or viewerState are being updated, resourceIdString, viewerState and screenshot must all be submitted.',
{
info: {
input,
userId
}
}
)
}
}
// Check if there's any actual changes
const changes = removeNullOrUndefinedKeys(omit(input, ['id', 'projectId']))
if (Object.keys(changes).length === 0) {
throw new SavedViewUpdateValidationError('No changes submitted with the input.', {
info: {
input,
userId
}
})
}
// Validate updated resourceIds
let resourceIds: ResourceBuilder | undefined = undefined
if ('resourceIdString' in changes && changes.resourceIdString) {
const validate = validateProjectResourceIdStringFactory(deps)
resourceIds = await validate({
resourceIdString: changes.resourceIdString,
projectId: input.projectId,
errorMetadata: {
input,
userId
}
})
}
// Validate viewerState
let viewerState: VersionedSerializedViewerState | undefined = undefined
if ('viewerState' in changes && changes.viewerState) {
// Validate state
viewerState = validateViewerStateFactory()({
viewerState: changes.viewerState,
projectId,
resourceIds: resourceIds!, // ts not smart enough, we checked for this above
errorMetadata: {
input,
userId
}
})
}
// Validate groupId - group is a valid and accessible group in the project
if (changes.groupId) {
const group = await deps.getSavedViewGroup({
id: changes.groupId,
projectId
})
if (!group) {
throw new SavedViewUpdateValidationError(
'Provided groupId does not exist in the project.',
{
info: {
input,
userId
}
}
)
}
}
// Validate screenshot
if (changes.screenshot && !isValidBase64Image(changes.screenshot)) {
throw new SavedViewUpdateValidationError(
'Invalid screenshot provided. Must be a valid base64 encoded image.',
{
info: {
input,
userId
}
}
)
}
// Validate name
if (changes.name && changes.name.length > 255) {
throw new SavedViewUpdateValidationError(
'View name must be between 1 and 255 characters long',
{
info: {
input,
userId
}
}
)
}
const finalChanges = omit(changes, ['resourceIdString', 'viewerState'])
const updatedView = await deps.updateSavedViewRecord({
id,
projectId,
update: {
...finalChanges,
...(resourceIds
? {
resourceIds: resourceIds
? resourceIds.map((r) => r.toString())
: undefined,
groupResourceIds: formatResourceIdsForGroup(resourceIds)
}
: { resourceIdString: undefined }),
...(viewerState
? {
viewerState
}
: { viewerState: undefined })
}
})
return updatedView! // should exist, we checked before
}
@@ -178,3 +178,17 @@ export const canUpdateSavedViewQuery = gql`
}
}
`
export const updateSavedViewMutation = gql`
mutation UpdateSavedView($input: UpdateSavedViewInput!) {
projectMutations {
savedViewMutations {
updateView(input: $input) {
...BasicSavedView
}
}
}
}
${basicSavedViewFragment}
`
@@ -9,7 +9,9 @@ import type {
GetProjectSavedViewGroupQueryVariables,
GetProjectSavedViewGroupsQueryVariables,
GetProjectSavedViewQueryVariables,
GetProjectUngroupedViewGroupQueryVariables
GetProjectUngroupedViewGroupQueryVariables,
UpdateSavedViewInput,
UpdateSavedViewMutationVariables
} from '@/modules/core/graph/generated/graphql'
import {
CanCreateSavedViewDocument,
@@ -20,7 +22,8 @@ import {
GetProjectSavedViewDocument,
GetProjectSavedViewGroupDocument,
GetProjectSavedViewGroupsDocument,
GetProjectUngroupedViewGroupDocument
GetProjectUngroupedViewGroupDocument,
UpdateSavedViewDocument
} from '@/modules/core/graph/generated/graphql'
import {
buildBasicTestModel,
@@ -32,7 +35,8 @@ import { SavedViewVisibility } from '@/modules/viewer/domain/types/savedViews'
import {
SavedViewCreationValidationError,
SavedViewGroupCreationValidationError,
SavedViewInvalidResourceTargetError
SavedViewInvalidResourceTargetError,
SavedViewUpdateValidationError
} from '@/modules/viewer/errors/savedViews'
import type { BasicTestWorkspace } from '@/modules/workspaces/tests/helpers/creation'
import {
@@ -67,6 +71,8 @@ const { FF_WORKSPACES_MODULE_ENABLED, FF_SAVED_VIEWS_ENABLED } = getFeatureFlags
const fakeScreenshot =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PiQ2YQAAAABJRU5ErkJggg=='
const fakeScreenshot2 =
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAICAgICAgICAgICAgICAwUDAwMDAwYEBAMFBQYGBQYGBwcICQoJCQkJCQoMCgsMDAwMDAwP/2wBDAwMDAwQDBAgEBAgQEBAgMCAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgP/wAARCAABAAEDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAHEAP/EABQQAQAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAQUCf//EABQRAQAAAAAAAAAAAAAAAAAAAD/2gAIAQMBAT8BP//EABQRAQAAAAAAAAAAAAAAAAAAAD/2gAIAQIBAT8BP//Z'
const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerState>) =>
merge(
@@ -99,6 +105,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
let apollo: TestApolloServer
let me: BasicTestUser
let guest: BasicTestUser
let otherGuy: BasicTestUser
let myProject: BasicTestStream
let myProjectWorkspace: BasicTestWorkspace
let myLackingProjectWorkspace: BasicTestWorkspace
@@ -174,47 +181,67 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
options?: ExecuteOperationOptions
) => apollo.execute(CanUpdateSavedViewDocument, input, options)
const updateView = (
input: UpdateSavedViewMutationVariables,
options?: ExecuteOperationOptions
) => apollo.execute(UpdateSavedViewDocument, input, options)
const model1ResourceIds = () => ViewerRoute.resourceBuilder().addModel(myModel1.id)
const model2ResourceIds = () => ViewerRoute.resourceBuilder().addModel(myModel2.id)
before(async () => {
me = await createTestUser(buildBasicTestUser({ name: 'me' }))
guest = await createTestUser(buildBasicTestUser({ name: 'guest' }))
const userCreate = await Promise.all([
createTestUser(buildBasicTestUser({ name: 'me' })),
createTestUser(buildBasicTestUser({ name: 'guest' })),
createTestUser(buildBasicTestUser({ name: 'other-guy' }))
])
me = userCreate[0]
guest = userCreate[1]
otherGuy = userCreate[2]
myLackingProjectWorkspace = await createTestWorkspace(
buildBasicTestWorkspace(),
me,
{
const workspaceCreate = await Promise.all([
createTestWorkspace(buildBasicTestWorkspace(), me, {
addPlan: WorkspacePlans.Free
}
)
myLackingProject = await createTestStream(
buildBasicTestProject({
workspaceId: myLackingProjectWorkspace.id
}),
me
)
createTestWorkspace(buildBasicTestWorkspace(), me, {
addPlan: WorkspacePlans.Pro
})
])
myLackingProjectWorkspace = workspaceCreate[0]
myProjectWorkspace = workspaceCreate[1]
myProjectWorkspace = await createTestWorkspace(buildBasicTestWorkspace(), me, {
addPlan: WorkspacePlans.Pro
})
myProject = await createTestStream(
buildBasicTestProject({
workspaceId: myProjectWorkspace.id
const projectCreate = await Promise.all([
createTestStream(
buildBasicTestProject({
workspaceId: myLackingProjectWorkspace.id
}),
me
),
createTestStream(
buildBasicTestProject({
workspaceId: myProjectWorkspace.id
}),
me
)
])
myLackingProject = projectCreate[0]
myProject = projectCreate[1]
const modelCreate = await Promise.all([
createTestBranch({
branch: buildBasicTestModel(),
stream: myProject,
owner: me
}),
me
)
myModel1 = await createTestBranch({
branch: buildBasicTestModel(),
stream: myProject,
owner: me
})
myModel2 = await createTestBranch({
branch: buildBasicTestModel({ name: 'model-2' }),
stream: myProject,
owner: me
})
createTestBranch({
branch: buildBasicTestModel({ name: 'model-2' }),
stream: myProject,
owner: me
})
])
myModel1 = modelCreate[0]
myModel2 = modelCreate[1]
apollo = await testApolloServer({ authUserId: me.id })
// We only run a small subset of tests if the module is disabled, and we dont need this stuff:
@@ -606,7 +633,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
)
expect(res).to.haveGraphQLErrors({
code: SavedViewCreationValidationError.code
code: SavedViewInvalidResourceTargetError.code
})
expect(res.data?.projectMutations.savedViewMutations.createView).to.not.be.ok
})
@@ -633,7 +660,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
)
expect(res).to.haveGraphQLErrors({
code: SavedViewCreationValidationError.code
code: SavedViewInvalidResourceTargetError.code
})
expect(res.data?.projectMutations.savedViewMutations.createView).to.not.be.ok
})
@@ -651,7 +678,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
)
expect(res).to.haveGraphQLErrors({
code: SavedViewCreationValidationError.code
code: SavedViewInvalidResourceTargetError.code
})
expect(res.data?.projectMutations.savedViewMutations.createView).to.not.be.ok
})
@@ -686,6 +713,279 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
})
})
describe('updates', () => {
let updatablesProject: BasicTestStream
let models: BasicTestBranch[]
let testView: BasicSavedViewFragment
let optionalGroup: BasicSavedViewGroupFragment
before(async () => {
updatablesProject = await createTestStream(
buildBasicTestProject({
name: 'updatables-project',
workspaceId: myProjectWorkspace.id
}),
me
)
await addToStream(updatablesProject, otherGuy, Roles.Stream.Reviewer)
models = await Promise.all(
times(3, async (i) => {
return await createTestBranch({
branch: buildBasicTestModel({
name: `Model #${i}`
}),
stream: updatablesProject,
owner: me
})
})
)
optionalGroup = (
await createSavedViewGroup(
{
input: {
projectId: updatablesProject.id,
resourceIdString: models[0].id,
groupName: 'Test Recalculation Group'
}
},
{ assertNoErrors: true }
)
)?.data?.projectMutations.savedViewMutations.createGroup!
})
beforeEach(async () => {
const createRes = await createSavedView(
buildCreateInput({
projectId: updatablesProject.id,
resourceIdString: models[0].id,
overrides: { name: 'View to update' }
}),
{ assertNoErrors: true }
)
testView = createRes.data?.projectMutations.savedViewMutations.createView!
expect(testView).to.be.ok
})
afterEach(async () => {
await deleteView(
{
input: {
id: testView.id,
projectId: updatablesProject.id
}
},
{ assertNoErrors: true }
)
})
const buildValidResourcesUpdate = () => ({
resourceIdString: 'invalid-resource-id',
screenshot: fakeScreenshot,
viewerState: fakeViewerState({
projectId: updatablesProject.id,
resources: {
request: {
resourceIdString: models[0].id
}
}
})
})
it('successfully updates a saved view (name)', async () => {
const newName = 'Updated View Name'
const res = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
name: newName
}
})
expect(res).to.not.haveGraphQLErrors()
const updatedView = res.data?.projectMutations.savedViewMutations.updateView
expect(updatedView).to.be.ok
expect(updatedView!.id).to.equal(testView.id)
expect(updatedView!.name).to.equal(newName)
})
it('successfully updated everyting in a saved view', async () => {
const input: UpdateSavedViewInput = {
id: testView.id,
projectId: updatablesProject.id,
// NEW UPDATES
resourceIdString: models.at(-1)!.id,
groupId: optionalGroup.id,
name: 'Updated View Name',
description: 'Updated description :)',
viewerState: fakeViewerState({
projectId: updatablesProject.id,
resources: {
request: {
resourceIdString: models.at(-1)!.id
}
}
}),
screenshot: fakeScreenshot2,
isHomeView: true,
visibility: SavedViewVisibility.authorOnly
}
const res = await updateView({
input
})
expect(res).to.not.haveGraphQLErrors()
const updatedView = res.data?.projectMutations.savedViewMutations.updateView
expect(updatedView).to.be.ok
expect(updatedView!.id).to.equal(testView.id)
expect(updatedView!.name).to.equal(input.name)
expect(updatedView!.description).to.equal(input.description)
expect(updatedView!.groupId).to.equal(input.groupId)
expect(updatedView!.resourceIdString).to.equal(input.resourceIdString)
expect(updatedView!.viewerState).to.deep.equalInAnyOrder(input.viewerState)
expect(updatedView!.screenshot).to.equal(input.screenshot)
expect(updatedView!.isHomeView).to.equal(input.isHomeView)
expect(updatedView!.visibility).to.equal(input.visibility)
})
it('fails if user has no access to update the view', async () => {
const newName = 'Updated View Name'
const res = await updateView(
{
input: {
id: testView.id,
projectId: updatablesProject.id,
name: newName
}
},
{ authUserId: otherGuy.id }
)
expect(res).to.haveGraphQLErrors({ code: ForbiddenError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails if view does not exist', async () => {
const res = await updateView({
input: { id: 'non-existent-id', projectId: updatablesProject.id, name: 'x' }
})
expect(res).to.haveGraphQLErrors({ code: NotFoundError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails if no changes submitted', async () => {
const res = await updateView({
input: { id: testView.id, projectId: updatablesProject.id }
})
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails if updating resourceIdString/viewerState/screenshot with missing required fields', async () => {
// Only resourceIdString
let res = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
resourceIdString: models[0].id
}
})
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
// Only viewerState
res = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
viewerState: { a: 1 }
}
})
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
// Only screenshot
res = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
screenshot: 'invalid'
}
})
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails if groupId does not exist', async () => {
const res = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
groupId: 'non-existent-group-id',
name: 'x'
}
})
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails if screenshot is invalid', async () => {
const res = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
screenshot: 'not-base64',
name: 'x'
}
})
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails if name is too long', async () => {
const longName = 'x'.repeat(256)
const res = await updateView({
input: { id: testView.id, projectId: updatablesProject.id, name: longName }
})
expect(res).to.haveGraphQLErrors({ code: SavedViewUpdateValidationError.code })
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails updating resourceIdString, if its invalid', async () => {
const res = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
...buildValidResourcesUpdate(),
resourceIdString: 'invalid-resource-id'
}
})
expect(res).to.haveGraphQLErrors({
code: SavedViewInvalidResourceTargetError.code
})
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails updating viewerState, if its invalid', async () => {
const res = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
...buildValidResourcesUpdate(),
viewerState: { a: 1 } as unknown as ViewerState.SerializedViewerState // invalid state
}
})
expect(res).to.haveGraphQLErrors({
code: SavedViewInvalidResourceTargetError.code
})
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
})
describe('deletions', () => {
let deletablesProject: BasicTestStream
let models: BasicTestBranch[]
@@ -711,8 +1011,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
})
)
// add guest as reviewer
await addToStream(deletablesProject, guest, Roles.Stream.Reviewer, {
await addToStream(deletablesProject, otherGuy, Roles.Stream.Reviewer, {
owner: me
})
})
@@ -783,7 +1082,7 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
viewId: view.id
},
{
authUserId: guest.id
authUserId: otherGuy.id
}
)
+9 -7
View File
@@ -132,14 +132,14 @@ export const isModelFolderResource = (
r: ViewerResource
): r is ViewerModelFolderResource => r.type === ViewerResourceType.ModelFolder
type StringViewerResources = string | string[]
type ViewerResources =
type StringViewerResourcesTarget = string | string[]
export type ViewerResourcesTarget =
| ViewerResourceBuilder
| ViewerResource[]
| ViewerResource
| StringViewerResources
| StringViewerResourcesTarget
const toViewerResourceArray = (res: ViewerResources): ViewerResource[] => {
const toViewerResourceArray = (res: ViewerResourcesTarget): ViewerResource[] => {
if (res instanceof ViewerResourceBuilder) {
return res.toResources()
}
@@ -184,7 +184,7 @@ class ViewerResourceBuilder implements Iterable<ViewerResource> {
/**
* @deprecated Use 'addResources' or 'addNew' instead
*/
addFromString(stringResources: StringViewerResources) {
addFromString(stringResources: StringViewerResourcesTarget) {
const strings = Array.isArray(stringResources) ? stringResources : [stringResources]
for (const resourceIdString of strings) {
const resources = parseUrlParameters(resourceIdString.toLowerCase())
@@ -194,7 +194,7 @@ class ViewerResourceBuilder implements Iterable<ViewerResource> {
this.#order()
return this
}
addResources(res: ViewerResources) {
addResources(res: ViewerResourcesTarget) {
this.#resources.push(...toViewerResourceArray(res))
this.#order()
return this
@@ -204,7 +204,7 @@ class ViewerResourceBuilder implements Iterable<ViewerResource> {
* Only add those resources that are not already in the builder.
*/
addNew(
incoming: ViewerResources,
incoming: ViewerResourcesTarget,
options?: {
/**
* If true, will require exact version matches for model resources
@@ -290,3 +290,5 @@ class ViewerResourceBuilder implements Iterable<ViewerResource> {
export function resourceBuilder() {
return new ViewerResourceBuilder()
}
export type ResourceBuilder = ReturnType<typeof resourceBuilder>
@@ -18,6 +18,7 @@
: 'hover:border-outline-1'
]"
:disabled="disabled || option.disabled"
type="button"
@click="selectItem(option.value)"
>
<div
@@ -84,36 +85,46 @@
</div>
</div>
</div>
<div v-if="errorMessage" class="text-danger text-body-2xs mt-2">
{{ errorMessage }}
</div>
</div>
</template>
<script setup lang="ts" generic="Value extends string">
import { InformationCircleIcon } from '@heroicons/vue/24/outline'
import { type ConcreteComponent } from 'vue'
import { useField, type RuleExpression } from 'vee-validate'
import { computed } from 'vue'
import type { FormRadioGroupItem } from '~~/src/helpers/common/components'
type OptionType = {
value: Value
title: string
subtitle?: string
introduction?: string
icon?: ConcreteComponent
help?: string
disabled?: boolean
}
defineEmits<{
(e: 'update:modelValue', v: Value): void
}>()
const props = withDefaults(
defineProps<{
options: OptionType[]
name?: string
modelValue?: Value
options: FormRadioGroupItem<Value>[]
disabled?: boolean
isStacked?: boolean
size?: 'sm' | 'base'
rules?: RuleExpression<Value>
}>(),
{
size: 'base'
size: 'base',
name: 'formRadioGroup'
}
)
const selected = defineModel<Value>()
const { value, errorMessage } = useField<Value>(props.name, props.rules, {
initialValue: props.modelValue as Value
})
const selected = computed({
get: () => value.value,
set: (newVal: Value) => (value.value = newVal)
})
const selectItem = (value: Value) => {
selected.value = value
@@ -30,3 +30,13 @@ export type AlertAction = {
externalUrl?: boolean
disabled?: boolean
}
export type FormRadioGroupItem<V extends string = string> = {
value: V
title: string
subtitle?: string
introduction?: string
icon?: ConcreteComponent
help?: string
disabled?: boolean
}
+2
View File
@@ -112,6 +112,7 @@ export { useAvatarSizeClasses } from '~~/src/composables/user/avatar'
export type { UserAvatarSize } from '~~/src/composables/user/avatar'
import CommonProgressBar from '~~/src/components/common/ProgressBar.vue'
import FormRange from '~~/src/components/form/Range.vue'
import type { FormRadioGroupItem } from '~~/src/helpers/common/components'
export {
MissingFileExtensionError,
@@ -190,6 +191,7 @@ export {
FormRange
}
export type {
FormRadioGroupItem,
LayoutDialogButton,
LayoutHeaderButton,
ToastNotification,