Merge branch 'main' into feature/initial-viewer-ui-updates
This commit is contained in:
@@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user