feat: move view into group dialog (#5211)
* looking into dialog premature closing * fixed group select * rover update * tests
This commit is contained in:
committed by
GitHub
parent
011320880e
commit
ea5dadbdb2
@@ -49,7 +49,7 @@
|
||||
name="editView"
|
||||
class="shrink-0 opacity-0 group-hover:opacity-100"
|
||||
:disabled="!canUpdate?.authorized || isLoading"
|
||||
@click="showEditDialog = !showEditDialog"
|
||||
@click="onEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -63,7 +63,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ViewerSavedViewsPanelViewEditDialog v-model:open="showEditDialog" :view="view" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -88,7 +87,8 @@ const MenuItems = StringEnum([
|
||||
'LoadOriginalVersions',
|
||||
'CopyLink',
|
||||
'ChangeVisibility',
|
||||
'ReplaceView'
|
||||
'ReplaceView',
|
||||
'MoveToGroup'
|
||||
])
|
||||
type MenuItems = StringEnumValues<typeof MenuItems>
|
||||
|
||||
@@ -124,8 +124,8 @@ const deleteView = useDeleteSavedView()
|
||||
const updateView = useUpdateSavedView()
|
||||
const isLoading = useMutationLoading()
|
||||
const { copyLink, applyView } = useViewerSavedViewsUtils()
|
||||
const eventBus = useEventBus()
|
||||
|
||||
const showEditDialog = ref(false)
|
||||
const showMenu = ref(false)
|
||||
const menuId = useId()
|
||||
|
||||
@@ -141,7 +141,15 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
|
||||
},
|
||||
{
|
||||
id: MenuItems.ReplaceView,
|
||||
title: 'Update view'
|
||||
title: 'Update view',
|
||||
disabled: !canUpdate.value?.authorized || isLoading.value,
|
||||
disabledTooltip: canUpdate.value.errorMessage
|
||||
},
|
||||
{
|
||||
id: MenuItems.MoveToGroup,
|
||||
title: 'Move to group',
|
||||
disabled: !canUpdate.value?.authorized || isLoading.value,
|
||||
disabledTooltip: canUpdate.value.errorMessage
|
||||
},
|
||||
{
|
||||
id: MenuItems.CopyLink,
|
||||
@@ -152,7 +160,9 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
|
||||
{
|
||||
id: MenuItems.ChangeVisibility,
|
||||
title: 'Only visible to me',
|
||||
active: !!isOnlyVisibleToMe.value
|
||||
active: !!isOnlyVisibleToMe.value,
|
||||
disabled: !canUpdate.value?.authorized || isLoading.value,
|
||||
disabledTooltip: canUpdate.value.errorMessage
|
||||
}
|
||||
],
|
||||
[
|
||||
@@ -205,6 +215,12 @@ const onActionChosen = async (item: LayoutMenuItem<MenuItems>) => {
|
||||
}
|
||||
})
|
||||
break
|
||||
case MenuItems.MoveToGroup:
|
||||
eventBus.emit(ViewerEventBusKeys.MarkSavedViewForEdit, {
|
||||
type: 'move',
|
||||
view: props.view
|
||||
})
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(item.id)
|
||||
}
|
||||
@@ -215,4 +231,11 @@ const apply = async () => {
|
||||
id: props.view.id
|
||||
})
|
||||
}
|
||||
|
||||
const onEdit = () => {
|
||||
eventBus.emit(ViewerEventBusKeys.MarkSavedViewForEdit, {
|
||||
type: 'edit',
|
||||
view: props.view
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,6 +18,15 @@
|
||||
hide-when-complete
|
||||
@infinite="onInfiniteLoad"
|
||||
/>
|
||||
<ViewerSavedViewsPanelViewEditDialog
|
||||
v-model:open="showEditDialog"
|
||||
:view="viewBeingEdited"
|
||||
/>
|
||||
<ViewerSavedViewsPanelViewMoveDialog
|
||||
v-model:open="showMoveDialog"
|
||||
:view="viewBeingMoved"
|
||||
@success="onMoveSuccess"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,6 +34,10 @@
|
||||
import { omit } from 'lodash-es'
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
ViewerSavedViewsPanelViewEditDialog_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
import { ViewsType } from '~/lib/viewer/helpers/savedViews'
|
||||
|
||||
@@ -68,8 +81,11 @@ const {
|
||||
request: { resourceIdString }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
const eventBus = useEventBus()
|
||||
|
||||
const search = ref('')
|
||||
const viewBeingEdited = ref<ViewerSavedViewsPanelViewEditDialog_SavedViewFragment>()
|
||||
const viewBeingMoved = ref<ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment>()
|
||||
|
||||
const {
|
||||
identifier,
|
||||
@@ -111,6 +127,24 @@ const groups = computed(() => {
|
||||
return result.value?.project.savedViewGroups.items || []
|
||||
})
|
||||
|
||||
const showEditDialog = computed({
|
||||
get: () => !!viewBeingEdited.value,
|
||||
set: (value) => {
|
||||
if (!value) {
|
||||
viewBeingEdited.value = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const showMoveDialog = computed({
|
||||
get: () => !!viewBeingMoved.value,
|
||||
set: (value) => {
|
||||
if (!value) {
|
||||
viewBeingMoved.value = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
groups,
|
||||
(newGroups) => {
|
||||
@@ -120,4 +154,16 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
eventBus.on(ViewerEventBusKeys.MarkSavedViewForEdit, ({ type, view }) => {
|
||||
if (type === 'edit') {
|
||||
viewBeingEdited.value = view
|
||||
} else if (type === 'move') {
|
||||
viewBeingMoved.value = view
|
||||
}
|
||||
})
|
||||
|
||||
const onMoveSuccess = (groupId: string) => {
|
||||
selectedGroupId.value = groupId
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -77,7 +77,7 @@ type FormType = {
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
view: ViewerSavedViewsPanelViewEditDialog_SavedViewFragment
|
||||
view: ViewerSavedViewsPanelViewEditDialog_SavedViewFragment | undefined
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', {
|
||||
@@ -126,6 +126,8 @@ const radioOptions = computed((): FormRadioGroupItem<SavedViewVisibility>[] => [
|
||||
])
|
||||
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
if (!props.view) return
|
||||
|
||||
const name =
|
||||
values.name.trim() && values.name.trim() !== props.view.name
|
||||
? values.name.trim()
|
||||
@@ -156,6 +158,8 @@ const onSubmit = handleSubmit(async (values) => {
|
||||
})
|
||||
|
||||
watch(open, (newVal, oldVal) => {
|
||||
if (!props.view) return
|
||||
|
||||
if (newVal && !oldVal) {
|
||||
// Reset form state when dialog opens
|
||||
setValues({
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="open"
|
||||
title="Move to group"
|
||||
max-width="sm"
|
||||
:buttons="buttons"
|
||||
:on-submit="onSubmit"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<FormSelectSavedViewGroup
|
||||
name="group"
|
||||
label="Select group"
|
||||
show-label
|
||||
:project-id="projectId"
|
||||
:resource-id-string="resourceIdString"
|
||||
:rules="[isRequired]"
|
||||
/>
|
||||
</div>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
FormSelectSavedViewGroup_SavedViewGroupFragment,
|
||||
ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { isRequired } from '~/lib/common/helpers/validation'
|
||||
import { useUpdateSavedView } from '~/lib/viewer/composables/savedViews/management'
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
|
||||
graphql(`
|
||||
fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {
|
||||
id
|
||||
group {
|
||||
...FormSelectSavedViewGroup_SavedViewGroup
|
||||
}
|
||||
...UseUpdateSavedView_SavedView
|
||||
}
|
||||
`)
|
||||
|
||||
type FormType = {
|
||||
group: FormSelectSavedViewGroup_SavedViewGroupFragment
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: [groupId: string]
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
view: ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment | undefined
|
||||
}>()
|
||||
|
||||
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 onSubmit = handleSubmit(async (values) => {
|
||||
if (!props.view) return
|
||||
const groupId = values.group.id !== props.view.group.id ? values.group.id : null
|
||||
if (!groupId) return
|
||||
|
||||
const res = await updateView({
|
||||
view: props.view,
|
||||
input: {
|
||||
id: props.view.id,
|
||||
projectId: props.view.projectId,
|
||||
groupId
|
||||
}
|
||||
})
|
||||
|
||||
if (res?.id) {
|
||||
emit('success', groupId)
|
||||
open.value = false
|
||||
}
|
||||
})
|
||||
|
||||
watch(open, (newVal, oldVal) => {
|
||||
if (!props.view) return
|
||||
|
||||
if (newVal && !oldVal) {
|
||||
// Reset form state when dialog opens
|
||||
setValues({
|
||||
group: props.view.group
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useActiveUser } from '~/lib/auth/composables/activeUser'
|
||||
import { usePageQueryStandardFetchPolicy } from '~/lib/common/composables/graphql'
|
||||
import { useGlobalToast } from '~/lib/common/composables/toast'
|
||||
import { useEventBus } from '~/lib/core/composables/eventBus'
|
||||
|
||||
export const useIsAccModuleEnabled = () => {
|
||||
const {
|
||||
@@ -83,4 +84,4 @@ export const useIsRhinoFileImporterEnabled = () => {
|
||||
return ref(FF_RHINO_FILE_IMPORTER_ENABLED)
|
||||
}
|
||||
|
||||
export { useGlobalToast, useActiveUser, usePageQueryStandardFetchPolicy }
|
||||
export { useGlobalToast, useActiveUser, usePageQueryStandardFetchPolicy, useEventBus }
|
||||
|
||||
@@ -171,6 +171,7 @@ type Documents = {
|
||||
"\n fragment ViewerSavedViewsPanelViews_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n": 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 ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": typeof types.ViewerSavedViewsPanelViewMoveDialog_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,
|
||||
@@ -665,6 +666,7 @@ const documents: Documents = {
|
||||
"\n fragment ViewerSavedViewsPanelViews_Project on Project {\n id\n savedViewGroups(input: $savedViewGroupsInput) {\n totalCount\n cursor\n items {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n": 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 ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n": types.ViewerSavedViewsPanelViewMoveDialog_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,
|
||||
@@ -1644,6 +1646,10 @@ export function graphql(source: "\n query ViewerSavedViewsPanelViews_Groups(\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.
|
||||
*/
|
||||
export function graphql(source: "\n fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\n group {\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n ...UseUpdateSavedView_SavedView\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewMoveDialog_SavedView on SavedView {\n id\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.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -41,7 +41,7 @@ export const useViewerSavedViewsUtils = () => {
|
||||
const applyView = (settings: SavedViewUrlSettings) => {
|
||||
// Force update, even if the view id is already set
|
||||
// (in case this is a frustration click w/ the state not applying)
|
||||
eventBus.emit(ViewerEventBusKeys.UpdateSavedView, settings)
|
||||
eventBus.emit(ViewerEventBusKeys.ApplySavedView, settings)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
onGroupViewRemovalCacheUpdates,
|
||||
onNewGroupViewCacheUpdates
|
||||
} from '~/lib/viewer/helpers/savedViews/cache'
|
||||
import { isUngroupedGroup } from '@speckle/shared/saved-views'
|
||||
|
||||
const createSavedViewMutation = graphql(`
|
||||
mutation CreateSavedView($input: CreateSavedViewInput!) {
|
||||
@@ -320,7 +321,7 @@ export const useCreateSavedViewGroup = () => {
|
||||
|
||||
// default comes first, then new group
|
||||
const defaultIdx = newItems.findIndex((i) =>
|
||||
fromRef(i).id.startsWith('default-')
|
||||
isUngroupedGroup(fromRef(i).id)
|
||||
)
|
||||
|
||||
newItems.splice(defaultIdx + 1, 0, ref('SavedViewGroup', group.id))
|
||||
|
||||
@@ -1003,7 +1003,7 @@ const useViewerSavedViewSetup = () => {
|
||||
}
|
||||
|
||||
// Allow force update
|
||||
on(ViewerEventBusKeys.UpdateSavedView, async (settings) => {
|
||||
on(ViewerEventBusKeys.ApplySavedView, async (settings) => {
|
||||
await update({ settings })
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
import type { ViewerSavedViewEventBusPayloads } from '~/lib/viewer/helpers/savedViews'
|
||||
import type {
|
||||
ViewerSavedViewsPanelViewEditDialog_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import type { SavedViewUrlSettings } from '~/lib/viewer/helpers/savedViews'
|
||||
|
||||
export enum ViewerEventBusKeys {
|
||||
UpdateSavedView = 'aaa'
|
||||
ApplySavedView = 'UpdateSavedView',
|
||||
MarkSavedViewForEdit = 'MarkSavedViewForEdit'
|
||||
}
|
||||
|
||||
export type ViewerSavedViewEventBusPayloads = {
|
||||
[ViewerEventBusKeys.ApplySavedView]: SavedViewUrlSettings
|
||||
[ViewerEventBusKeys.MarkSavedViewForEdit]:
|
||||
| {
|
||||
type: 'edit'
|
||||
view: ViewerSavedViewsPanelViewEditDialog_SavedViewFragment
|
||||
}
|
||||
| { type: 'move'; view: ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment }
|
||||
}
|
||||
|
||||
// Add mappings between event keys and expected payloads here
|
||||
export type ViewerEventBusKeyPayloadMap = {
|
||||
[ViewerEventBusKeys.UpdateSavedView]: ViewerSavedViewEventBusPayloads[ViewerEventBusKeys.UpdateSavedView]
|
||||
[ViewerEventBusKeys.ApplySavedView]: ViewerSavedViewEventBusPayloads[ViewerEventBusKeys.ApplySavedView]
|
||||
[ViewerEventBusKeys.MarkSavedViewForEdit]: ViewerSavedViewEventBusPayloads[ViewerEventBusKeys.MarkSavedViewForEdit]
|
||||
} & { [k in ViewerEventBusKeys]: unknown } & Record<string, unknown>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { StringEnumValues } from '@speckle/shared'
|
||||
import { isObjectLike, isString } from 'lodash-es'
|
||||
import type { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus'
|
||||
|
||||
export const ViewsType = {
|
||||
All: 'all-views',
|
||||
@@ -23,10 +22,6 @@ export type SavedViewUrlSettings = {
|
||||
loadOriginal?: boolean
|
||||
}
|
||||
|
||||
export type ViewerSavedViewEventBusPayloads = {
|
||||
[ViewerEventBusKeys.UpdateSavedView]: SavedViewUrlSettings
|
||||
}
|
||||
|
||||
export const parseSavedViewUrlSettings = (
|
||||
settingsString: string | null
|
||||
): SavedViewUrlSettings | null => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ApolloCache } from '@apollo/client/cache'
|
||||
import { isUngroupedGroup } from '@speckle/shared/saved-views'
|
||||
|
||||
/**
|
||||
* Cache mutations for when a group gets a new view:
|
||||
@@ -75,7 +76,7 @@ export const onGroupViewRemovalCacheUpdates = (
|
||||
const { viewId: id, groupId, projectId } = params
|
||||
|
||||
// Check if default/ungrouped group
|
||||
const isDefaultGroup = groupId.startsWith('default-')
|
||||
const isDefaultGroup = isUngroupedGroup(groupId)
|
||||
|
||||
// If default group and its now empty - remove it as it doesn't exist otherwise
|
||||
let shouldEvict
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ROOT_SUBSCRIPTION
|
||||
} from '~/lib/common/helpers/graphql'
|
||||
import { checkIfIsInPlaceNavigation } from '~/lib/common/helpers/navigation'
|
||||
import { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus'
|
||||
|
||||
/**
|
||||
* Debugging helper to ensure variables are available in debugging scope
|
||||
@@ -38,5 +39,6 @@ export {
|
||||
checkIfIsInPlaceNavigation,
|
||||
ROOT_QUERY,
|
||||
ROOT_MUTATION,
|
||||
ROOT_SUBSCRIPTION
|
||||
ROOT_SUBSCRIPTION,
|
||||
ViewerEventBusKeys
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { base64Decode, base64Encode } from '@/modules/shared/helpers/cryptoHelper'
|
||||
import type { Nullable } from '@speckle/shared'
|
||||
import { isUngroupedGroup } from '@speckle/shared/saved-views'
|
||||
import type { ViewerResourcesTarget } from '@speckle/shared/viewer/route'
|
||||
import {
|
||||
isModelResource,
|
||||
@@ -29,7 +30,7 @@ export const buildDefaultGroupId = (params: {
|
||||
|
||||
export const decodeDefaultGroupId = (id: string): Nullable<DefaultGroupMetadata> => {
|
||||
try {
|
||||
if (!id.startsWith('default-')) return null
|
||||
if (!isUngroupedGroup(id)) return null
|
||||
const json = base64Decode(id.replace('default-', ''))
|
||||
const obj = JSON.parse(json)
|
||||
if (
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
decodeDefaultGroupId,
|
||||
formatResourceIdsForGroup
|
||||
} from '@/modules/viewer/helpers/savedViews'
|
||||
import { isUngroupedGroup } from '@speckle/shared/saved-views'
|
||||
import { resourceBuilder } from '@speckle/shared/viewer/route'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -249,7 +250,7 @@ export const getProjectSavedViewGroupsPageItemsFactory =
|
||||
|
||||
// Adjust cursor, in case it points to non-existant default group
|
||||
let cursor = decode(params.cursor)
|
||||
if (cursor?.id.startsWith('default-')) {
|
||||
if (cursor?.id && isUngroupedGroup(cursor.id)) {
|
||||
// Default appears first, so just unset the cursor to get the real first item
|
||||
cursor = null
|
||||
}
|
||||
|
||||
@@ -33,9 +33,10 @@ 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 { isUndefined, omit } from 'lodash-es'
|
||||
import type { DependenciesOf } from '@/modules/shared/helpers/factory'
|
||||
import { removeNullOrUndefinedKeys } from '@speckle/shared'
|
||||
import { isUngroupedGroup } from '@speckle/shared/saved-views'
|
||||
|
||||
/**
|
||||
* Validates an incoming resourceIdString against the resources in the project and returns the validated list (as a builder)
|
||||
@@ -388,7 +389,14 @@ export const updateSavedViewFactory =
|
||||
}
|
||||
|
||||
// Check if there's any actual changes
|
||||
const changes = removeNullOrUndefinedKeys(omit(input, ['id', 'projectId']))
|
||||
const changes = {
|
||||
...removeNullOrUndefinedKeys(omit(input, ['id', 'projectId'])),
|
||||
...(!isUndefined(input.groupId)
|
||||
? {
|
||||
groupId: input.groupId // we want to allow null, which means - no group
|
||||
}
|
||||
: {})
|
||||
}
|
||||
if (Object.keys(changes).length === 0) {
|
||||
throw new SavedViewUpdateValidationError('No changes submitted with the input.', {
|
||||
info: {
|
||||
@@ -429,20 +437,26 @@ export const updateSavedViewFactory =
|
||||
|
||||
// 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
|
||||
// Check if default group (actually means - null group)
|
||||
const isDefaultGroup = changes.groupId && isUngroupedGroup(changes.groupId)
|
||||
if (isDefaultGroup) {
|
||||
changes.groupId = null
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -186,6 +186,11 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(UpdateSavedViewDocument, input, options)
|
||||
|
||||
const getProjectViewGroups = (
|
||||
input: GetProjectSavedViewGroupsQueryVariables,
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(GetProjectSavedViewGroupsDocument, input, options)
|
||||
|
||||
const model1ResourceIds = () => ViewerRoute.resourceBuilder().addModel(myModel1.id)
|
||||
|
||||
const model2ResourceIds = () => ViewerRoute.resourceBuilder().addModel(myModel2.id)
|
||||
@@ -866,6 +871,73 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
expect(updatedView!.visibility).to.equal(input.visibility)
|
||||
})
|
||||
|
||||
it('successfully sets and unsets a group', async () => {
|
||||
const newGroupId = optionalGroup.id
|
||||
|
||||
const res = await updateView({
|
||||
input: {
|
||||
id: testView.id,
|
||||
projectId: updatablesProject.id,
|
||||
groupId: newGroupId
|
||||
}
|
||||
})
|
||||
|
||||
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!.groupId).to.equal(newGroupId)
|
||||
|
||||
// Unset group
|
||||
const res2 = await updateView({
|
||||
input: {
|
||||
id: testView.id,
|
||||
projectId: updatablesProject.id,
|
||||
groupId: null
|
||||
}
|
||||
})
|
||||
|
||||
expect(res2).to.not.haveGraphQLErrors()
|
||||
const updatedView2 = res2.data?.projectMutations.savedViewMutations.updateView
|
||||
expect(updatedView2).to.be.ok
|
||||
expect(updatedView2!.id).to.equal(testView.id)
|
||||
expect(updatedView2!.groupId).to.be.null
|
||||
})
|
||||
|
||||
it('allow setting default group as group, which actually sets it to null', async () => {
|
||||
// Get default group id
|
||||
const groupsRes = await getProjectViewGroups(
|
||||
{
|
||||
projectId: updatablesProject.id,
|
||||
input: {
|
||||
limit: 1,
|
||||
resourceIdString: models[0].id
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const defaultGroup = groupsRes.data?.project.savedViewGroups.items[0]
|
||||
expect(defaultGroup).to.be.ok
|
||||
|
||||
// Update view to have that be the group
|
||||
const update = await updateView(
|
||||
{
|
||||
input: {
|
||||
id: testView.id,
|
||||
projectId: updatablesProject.id,
|
||||
groupId: defaultGroup!.id
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const updatedView = update.data?.projectMutations.savedViewMutations.updateView
|
||||
expect(updatedView?.id).to.be.ok
|
||||
expect(updatedView?.groupId).to.be.null
|
||||
expect(updatedView?.group.id).to.equal(defaultGroup!.id)
|
||||
})
|
||||
|
||||
it('fails if user has no access to update the view', async () => {
|
||||
const newName = 'Updated View Name'
|
||||
|
||||
@@ -1130,11 +1202,6 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
modelIds.map((id) => new ViewerRoute.ViewerModelResource(id))
|
||||
)
|
||||
|
||||
const getProjectViewGroups = (
|
||||
input: GetProjectSavedViewGroupsQueryVariables,
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(GetProjectSavedViewGroupsDocument, input, options)
|
||||
|
||||
before(async () => {
|
||||
otherReader = await createTestUser(buildBasicTestUser({ name: 'other-reader' }))
|
||||
await assignToWorkspace(
|
||||
@@ -1317,22 +1384,42 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
expect(defaultGroupsFound).to.equal(1) // only 1 default group
|
||||
})
|
||||
|
||||
// it('should return different groups in same project, if resource string differs', async () => {
|
||||
// const res = await getProjectViewGroups({
|
||||
// projectId: readTestProject.id,
|
||||
// input: {
|
||||
// limit: GROUP_COUNT, // all in 1 page
|
||||
// resourceIdString: 'ayy'
|
||||
// }
|
||||
// })
|
||||
it('should support default groups cursor', async () => {
|
||||
const res1 = await getProjectViewGroups(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 1, // only get default group
|
||||
resourceIdString: getAllReadModelResourceIds().toString()
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
// expect(res).to.not.haveGraphQLErrors()
|
||||
// const data = res.data?.project.savedViewGroups
|
||||
// expect(data).to.be.ok
|
||||
// expect(data!.totalCount).to.equal(1) // only default group returned
|
||||
// expect(data!.items.length).to.equal(1)
|
||||
// expect(data!.cursor).to.not.be.null
|
||||
// })
|
||||
const group = res1.data?.project.savedViewGroups.items[0]
|
||||
const cursor = res1.data?.project.savedViewGroups.cursor
|
||||
|
||||
expect(group).to.be.ok
|
||||
expect(group!.isUngroupedViewsGroup).to.be.true
|
||||
expect(cursor).to.be.ok
|
||||
|
||||
const res2 = await getProjectViewGroups(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 1, // get first real item
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
cursor
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const group2 = res2.data?.project.savedViewGroups.items[0]
|
||||
|
||||
expect(group2).to.be.ok
|
||||
expect(group2!.isUngroupedViewsGroup).to.be.false
|
||||
})
|
||||
|
||||
it('should respect search filter and filter out groups w/ views that dont have the search string in their name', async () => {
|
||||
const res = await getProjectViewGroups({
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apollo/rover": "^0.23.0",
|
||||
"@apollo/rover": "^0.35.0",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@graphql-codegen/cli": "^5.0.7",
|
||||
"@graphql-codegen/typed-document-node": "^5.1.2",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isUngroupedGroup } from './values.js'
|
||||
|
||||
describe('isUngroupedGroup', () => {
|
||||
it('should return true for ungrouped groups', () => {
|
||||
expect(isUngroupedGroup('default-group')).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for grouped groups', () => {
|
||||
expect(isUngroupedGroup('custom-group')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -2,3 +2,5 @@
|
||||
* Title used for the default 'Ungrouped Scenes' group in the saved views panel.
|
||||
*/
|
||||
export const ungroupedScenesGroupTitle = 'Ungrouped'
|
||||
|
||||
export const isUngroupedGroup = (groupId: string) => groupId.startsWith('default-')
|
||||
|
||||
@@ -135,18 +135,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@apollo/rover@npm:^0.23.0":
|
||||
version: 0.23.0
|
||||
resolution: "@apollo/rover@npm:0.23.0"
|
||||
"@apollo/rover@npm:^0.35.0":
|
||||
version: 0.35.0
|
||||
resolution: "@apollo/rover@npm:0.35.0"
|
||||
dependencies:
|
||||
axios: "npm:^1.6.5"
|
||||
axios-proxy-builder: "npm:^0.1.1"
|
||||
console.table: "npm:^0.10.0"
|
||||
detect-libc: "npm:^2.0.0"
|
||||
tar: "npm:^6.2.0"
|
||||
tar: "npm:^7.0.0"
|
||||
bin:
|
||||
rover: run.js
|
||||
checksum: 10/945f723c0a076503daf6622225653db761c3a0e2e80ee11f58b5f8cdc1cfe92eeaa732a5488881967b9123c49a92589800d3586ef61355767de16012be0af34a
|
||||
checksum: 10/d3f87f65e2fe9f57360315a5f8cb57c00fd9eb7a3c6154d9aea2644cc023c882c60caa92a189db9008695223927e76d6941d760ca740619dae3bf811ee446616
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -16278,7 +16278,7 @@ __metadata:
|
||||
resolution: "@speckle/server@workspace:packages/server"
|
||||
dependencies:
|
||||
"@apollo/client": "npm:^3.7.0"
|
||||
"@apollo/rover": "npm:^0.23.0"
|
||||
"@apollo/rover": "npm:^0.35.0"
|
||||
"@apollo/server": "npm:^4.11.0"
|
||||
"@aws-sdk/client-s3": "npm:^3.276.0"
|
||||
"@aws-sdk/lib-storage": "npm:^3.100.0"
|
||||
@@ -47036,7 +47036,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^7.4.0":
|
||||
"tar@npm:^7.0.0, tar@npm:^7.4.0":
|
||||
version: 7.4.3
|
||||
resolution: "tar@npm:7.4.3"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user