fix: prevent making a home view personal (#5334)

This commit is contained in:
Kristaps Fabians Geikins
2025-08-28 14:32:35 +03:00
committed by GitHub
parent dbb3c4a374
commit e7ed024d52
9 changed files with 227 additions and 75 deletions
@@ -108,6 +108,7 @@ import {
useCollectNewSavedViewViewerData,
useUpdateSavedView
} from '~/lib/viewer/composables/savedViews/management'
import { useSavedViewValidationHelpers } from '~/lib/viewer/composables/savedViews/validation'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
const MenuItems = StringEnum([
@@ -143,6 +144,7 @@ graphql(`
...UseDeleteSavedView_SavedView
...UseUpdateSavedView_SavedView
...ViewerSavedViewsPanelViewEditDialog_SavedView
...UseSavedViewValidationHelpers_SavedView
}
`)
@@ -161,15 +163,19 @@ const isLoading = useMutationLoading()
const { copyLink, applyView } = useViewerSavedViewsUtils()
const eventBus = useEventBus()
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
const {
canUpdate,
isOnlyVisibleToMe,
canSetHomeView,
isHomeView,
canToggleVisibility
} = useSavedViewValidationHelpers({
view: computed(() => props.view)
})
const showMenu = ref(false)
const menuId = useId()
const canUpdate = computed(() => props.view.permissions.canUpdate)
const isOnlyVisibleToMe = computed(
() => props.view.visibility === SavedViewVisibility.AuthorOnly
)
const isHomeView = computed(() => props.view.isHomeView)
const isActive = computed(() => props.view.id === savedView.value?.id)
const isOriginalVersionAlreadyLoaded = computed(() => {
@@ -188,29 +194,6 @@ const canLoadOriginal = computed(
}
)
const canSetHomeView = computed(
(): { authorized: boolean; message: Optional<string> } => {
if (!canUpdate.value?.authorized || isLoading.value) {
return { authorized: false, message: canUpdate.value.errorMessage || undefined }
}
if (isFederatedView.value) {
return {
authorized: false,
message: "Home view settings can't be updated while in a federated view"
}
}
if (isOnlyVisibleToMe.value) {
return {
authorized: false,
message: 'A view must be shared to be set as home view'
}
}
return { authorized: true, message: undefined }
}
)
const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
[
{
@@ -223,13 +206,13 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
id: MenuItems.ReplaceView,
title: 'Replace view',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
disabledTooltip: canUpdate.value?.errorMessage
},
{
id: MenuItems.MoveToGroup,
title: 'Move to group',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
disabledTooltip: canUpdate.value?.errorMessage
},
{
id: MenuItems.CopyLink,
@@ -247,8 +230,8 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
{
id: MenuItems.ChangeVisibility,
title: isOnlyVisibleToMe.value ? 'Make view shared' : 'Make view private',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
disabled: !canToggleVisibility.value.authorized,
disabledTooltip: canToggleVisibility.value.message
}
],
[
@@ -256,7 +239,7 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
id: MenuItems.Delete,
title: 'Delete',
disabled: !canUpdate.value?.authorized || isLoading.value,
disabledTooltip: canUpdate.value.errorMessage
disabledTooltip: canUpdate.value?.errorMessage
}
]
])
@@ -31,28 +31,28 @@
:rules="[isRequired]"
/>
<FormRadioGroup
:options="radioOptions"
:options="visibilityOptions"
size="sm"
name="visibility"
:rules="[isRequired]"
:rules="[isRequired, validateVisibility]"
/>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { FormRadioGroupItem, LayoutDialogButton } from '@speckle/ui-components'
import { Globe, Lock } from 'lucide-vue-next'
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useForm } from 'vee-validate'
import { graphql } from '~/lib/common/generated/gql'
import {
SavedViewVisibility,
type FormSelectSavedViewGroup_SavedViewGroupFragment,
type ViewerSavedViewsPanelViewEditDialog_SavedViewFragment
} from '~/lib/common/generated/gql/graphql'
import { isRequired, isStringOfLength } from '~/lib/common/helpers/validation'
import { useUpdateSavedView } from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { isUndefined } from 'lodash-es'
import { useSavedViewValidationHelpers } from '~/lib/viewer/composables/savedViews/validation'
import type {
FormSelectSavedViewGroup_SavedViewGroupFragment,
SavedViewVisibility,
ViewerSavedViewsPanelViewEditDialog_SavedViewFragment
} from '~/lib/common/generated/gql/graphql'
graphql(`
fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {
@@ -64,6 +64,7 @@ graphql(`
...FormSelectSavedViewGroup_SavedViewGroup
}
...UseUpdateSavedView_SavedView
...UseSavedViewValidationHelpers_SavedView
}
`)
@@ -89,6 +90,9 @@ const {
}
} = useInjectedViewerState()
const updateView = useUpdateSavedView()
const { validateVisibility, visibilityOptions } = useSavedViewValidationHelpers({
view: computed(() => props.view)
})
const buttons = computed((): LayoutDialogButton[] => [
{
@@ -108,21 +112,6 @@ const buttons = computed((): LayoutDialogButton[] => [
}
])
const radioOptions = computed((): FormRadioGroupItem<SavedViewVisibility>[] => [
{
value: SavedViewVisibility.Public,
title: 'Shared',
introduction: 'Visible to anyone with access to the model.',
icon: Globe
},
{
value: SavedViewVisibility.AuthorOnly,
title: 'Private',
introduction: 'Visible only to the view author.',
icon: Lock
}
])
const onSubmit = handleSubmit(async (values) => {
if (!props.view) return
@@ -169,9 +169,9 @@ type Documents = {
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n": typeof types.ViewerSavedViewsPanel_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelGroups_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n ...ViewerSavedViewsPanelViewsGroup_Project\n }\n": typeof types.ViewerSavedViewsPanelGroups_ProjectFragmentDoc,
"\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n": typeof types.ViewerSavedViewsPanelGroups_SavedViewGroupsDocument,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": typeof types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewDeleteDialog_SavedView on SavedView {\n id\n name\n ...UseDeleteSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewMoveDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ViewerSavedViewsPanelViewsGroup_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n isUngroupedViewsGroup\n title\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n": typeof types.ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragmentDoc,
@@ -421,6 +421,7 @@ type Documents = {
"\n fragment UseDeleteSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n groupId\n projectId\n isUngroupedViewsGroup\n }\n": typeof types.UseDeleteSavedViewGroup_SavedViewGroupFragmentDoc,
"\n mutation UpdateSavedViewGroup($input: UpdateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n updateGroup(input: $input) {\n id\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n }\n }\n }\n": typeof types.UpdateSavedViewGroupDocument,
"\n fragment UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n projectId\n groupId\n title\n isUngroupedViewsGroup\n }\n": typeof types.UseUpdateSavedViewGroup_SavedViewGroupFragmentDoc,
"\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseSavedViewValidationHelpers_SavedViewFragmentDoc,
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n": typeof types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": typeof types.ViewerCommentThreadFragmentDoc,
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": typeof types.ViewerCommentsReplyItemFragmentDoc,
@@ -673,9 +674,9 @@ const documents: Documents = {
"\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n workspace {\n id\n seatType\n planSupportsSavedViews: hasAccessToFeature(featureName: savedViews)\n }\n }\n": types.ViewerSavedViewsPanel_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelGroups_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n ...ViewerSavedViewsPanelViewsGroup_Project\n }\n": types.ViewerSavedViewsPanelGroups_ProjectFragmentDoc,
"\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n": types.ViewerSavedViewsPanelGroups_SavedViewGroupsDocument,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": types.ViewerSavedViewsPanelView_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewDeleteDialog_SavedView on SavedView {\n id\n name\n ...UseDeleteSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n": types.ViewerSavedViewsPanelViewEditDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewMoveDialog_SavedViewFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ViewerSavedViewsPanelViewsGroup_ProjectFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n isUngroupedViewsGroup\n title\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n": types.ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragmentDoc,
@@ -925,6 +926,7 @@ const documents: Documents = {
"\n fragment UseDeleteSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n groupId\n projectId\n isUngroupedViewsGroup\n }\n": types.UseDeleteSavedViewGroup_SavedViewGroupFragmentDoc,
"\n mutation UpdateSavedViewGroup($input: UpdateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n updateGroup(input: $input) {\n id\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n }\n }\n }\n": types.UpdateSavedViewGroupDocument,
"\n fragment UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n projectId\n groupId\n title\n isUngroupedViewsGroup\n }\n": types.UseUpdateSavedViewGroup_SavedViewGroupFragmentDoc,
"\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseSavedViewValidationHelpers_SavedViewFragmentDoc,
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n": types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": types.ViewerCommentThreadFragmentDoc,
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": types.ViewerCommentsReplyItemFragmentDoc,
@@ -1659,7 +1661,7 @@ export function graphql(source: "\n query ViewerSavedViewsPanelGroups_SavedView
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n }\n"];
export function graphql(source: "\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\n isHomeView\n resourceIds\n author {\n id\n name\n }\n updatedAt\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseDeleteSavedView_SavedView\n ...UseUpdateSavedView_SavedView\n ...ViewerSavedViewsPanelViewEditDialog_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1667,7 +1669,7 @@ export function graphql(source: "\n fragment ViewerSavedViewsPanelViewDeleteDia
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n"];
export function graphql(source: "\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {\n id\n name\n description\n visibility\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n ...UseSavedViewValidationHelpers_SavedView\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2664,6 +2666,10 @@ export function graphql(source: "\n mutation UpdateSavedViewGroup($input: Updat
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n projectId\n groupId\n title\n isUngroupedViewsGroup\n }\n"): (typeof documents)["\n fragment UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n projectId\n groupId\n title\n isUngroupedViewsGroup\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -0,0 +1,134 @@
import type { MaybeNullOrUndefined, Optional } from '@speckle/shared'
import type { GenericValidateFunction } from 'vee-validate'
import { graphql } from '~/lib/common/generated/gql/gql'
import {
SavedViewVisibility,
type UseSavedViewValidationHelpers_SavedViewFragment
} from '~/lib/common/generated/gql/graphql'
import { Globe, Lock } from 'lucide-vue-next'
import type { FormRadioGroupItem } from '@speckle/ui-components'
import { useMutationLoading } from '@vue/apollo-composable'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
graphql(`
fragment UseSavedViewValidationHelpers_SavedView on SavedView {
id
isHomeView
visibility
permissions {
canUpdate {
...FullPermissionCheckResult
}
}
}
`)
export const useSavedViewValidationHelpers = (params: {
view: ComputedRef<
MaybeNullOrUndefined<UseSavedViewValidationHelpers_SavedViewFragment>
>
}) => {
const homeViewPrivateError = 'A home view must be shared'
const isLoading = useMutationLoading()
const {
resources: {
response: { isFederatedView }
}
} = useInjectedViewerState()
const canUpdate = computed(() => params.view.value?.permissions.canUpdate)
const isOnlyVisibleToMe = computed(
() => params.view.value?.visibility === SavedViewVisibility.AuthorOnly
)
const isHomeView = computed(() => params.view.value?.isHomeView)
/**
* Visibility options for visibility radio group
*/
const visibilityOptions = computed((): FormRadioGroupItem<SavedViewVisibility>[] => [
{
value: SavedViewVisibility.Public,
title: 'Shared',
introduction: 'Visible to anyone with access to the model.',
icon: Globe
},
{
value: SavedViewVisibility.AuthorOnly,
title: 'Private',
introduction: 'Visible only to the view author.',
icon: Lock,
...(params.view.value?.isHomeView
? {
disabled: true,
help: homeViewPrivateError
}
: {})
}
])
const canSetHomeView = computed(
(): { authorized: boolean; message: Optional<string> } => {
if (!canUpdate.value?.authorized || isLoading.value) {
return {
authorized: false,
message: canUpdate.value?.errorMessage || undefined
}
}
if (isFederatedView.value) {
return {
authorized: false,
message: "Home view settings can't be updated while in a federated view"
}
}
if (isOnlyVisibleToMe.value) {
return {
authorized: false,
message: 'A view must be shared to be set as home view'
}
}
return { authorized: true, message: undefined }
}
)
const canToggleVisibility = computed(() => {
if (!canUpdate.value?.authorized || isLoading.value) {
return {
authorized: false,
message: canUpdate.value?.errorMessage || undefined
}
}
if (isHomeView.value && !isOnlyVisibleToMe.value) {
return {
authorized: false,
message: homeViewPrivateError
}
}
return { authorized: true, message: undefined }
})
/**
* Vee-validate rule for visibility checks
*/
const validateVisibility: GenericValidateFunction<SavedViewVisibility> = (value) => {
if (!params.view.value) return true
if (!params.view.value.isHomeView) return true
return value === SavedViewVisibility.AuthorOnly ? homeViewPrivateError : true
}
return {
validateVisibility,
visibilityOptions,
canUpdate,
isOnlyVisibleToMe,
canSetHomeView,
isHomeView,
canToggleVisibility
}
}
@@ -46,7 +46,7 @@ import { formatResourceIdsForGroup } from '@/modules/viewer/helpers/savedViews'
import { difference, isUndefined, omit } from 'lodash-es'
import type { DependenciesOf } from '@/modules/shared/helpers/factory'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { removeNullOrUndefinedKeys } from '@speckle/shared'
import { removeNullOrUndefinedKeys, firstDefinedValue } from '@speckle/shared'
import { isUngroupedGroup } from '@speckle/shared/saved-views'
import { NotFoundError } from '@/modules/shared/errors'
@@ -580,8 +580,8 @@ export const updateSavedViewFactory =
// Validate home view settings
const { homeViewModel } = validateHomeViewSettingsFactory()({
isHomeView: changes.isHomeView,
visibility: changes.visibility || view.visibility,
isHomeView: firstDefinedValue(changes.isHomeView, view.isHomeView),
visibility: firstDefinedValue(changes.visibility, view.visibility),
errorMetadata: {
input,
userId
@@ -1223,6 +1223,32 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails if updating already home view to be private view', async () => {
await updateView(
{
input: {
id: testView.id,
projectId: updatablesProject.id,
isHomeView: true
}
},
{ assertNoErrors: true }
)
const res2 = await updateView({
input: {
id: testView.id,
projectId: updatablesProject.id,
visibility: SavedViewVisibility.authorOnly
}
})
expect(res2).to.haveGraphQLErrors({
code: SavedViewInvalidHomeViewSettingsError.code
})
expect(res2.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
})
it('fails if updating view to be a federated home view', async () => {
const resourceIdString = resourceBuilder()
.addModel(models.at(-1)!.id)
@@ -186,3 +186,15 @@ export const StringEnum = <T extends string>(args: T[]) => {
export type StringEnumValues<T extends Record<string, string>> = {
[K in keyof T]: T[K] extends string ? T[K] : never
}[keyof T]
/**
* Get first non-undefined/null value, or undefined if none found
*/
export const firstDefinedValue = <T>(
...args: (T | undefined | null)[]
): T | undefined => {
for (const arg of args) {
if (!isNullOrUndefined(arg)) return arg
}
return undefined
}
-1
View File
@@ -111,7 +111,6 @@
"Prorotation"
],
"typescript.tsserver.maxTsServerMemory": 8192,
"typescript.disableAutomaticTypeAcquisition": true,
"tailwindCSS.experimental.configFile": {
"packages/frontend-2/tailwind.config.cjs": "packages/frontend-2/**"
},