Support drag and drop for moving views into groups (#5353)

* Initiale drag and drop moving to group

* Update some styling

* Reenable ungrouped groups

* Support moving to ungrouped from dropdown

* Remove unneeded comments

* undo unnecessary groupId changes

* group cleanup

* clean up drag logic

---------

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
This commit is contained in:
Benjamin Ottensten
2025-09-02 14:09:38 +02:00
committed by GitHub
parent c641ce63e3
commit 4fc1aa6d40
11 changed files with 329 additions and 92 deletions
@@ -1,7 +1,14 @@
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<div v-keyboard-clickable :class="wrapperClasses" :view-id="view.id" @click="apply">
<div
v-keyboard-clickable
:class="[wrapperClasses, draggableClasses]"
:view-id="view.id"
draggable="true"
v-on="on"
@click="apply"
>
<div class="flex items-center shrink-0">
<div class="relative">
<img
@@ -112,6 +119,7 @@ import {
useCollectNewSavedViewViewerData,
useUpdateSavedView
} from '~/lib/viewer/composables/savedViews/management'
import { useDraggableView } from '~/lib/viewer/composables/savedViews/ui'
import { useSavedViewValidationHelpers } from '~/lib/viewer/composables/savedViews/validation'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
@@ -151,6 +159,7 @@ graphql(`
...UseUpdateSavedView_SavedView
...ViewerSavedViewsPanelViewEditDialog_SavedView
...UseSavedViewValidationHelpers_SavedView
...UseDraggableView_SavedView
}
`)
@@ -178,6 +187,9 @@ const {
} = useSavedViewValidationHelpers({
view: computed(() => props.view)
})
const { classes: draggableClasses, on } = useDraggableView({
view: computed(() => props.view)
})
const showMenu = ref(false)
const menuId = useId()
@@ -252,7 +264,7 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
const wrapperClasses = computed(() => {
const classParts = [
'flex items-center gap-2 p-1.5 w-full group rounded-md cursor-pointer'
'flex items-center gap-2 p-1.5 w-full group rounded-md cursor-pointer relative transition-all'
]
if (isActive.value) {
@@ -1,7 +1,7 @@
<template>
<LayoutDialog
v-model:open="open"
title="Edit view details"
title="Edit view"
max-width="sm"
:buttons="buttons"
:on-submit="onSubmit"
@@ -9,7 +9,7 @@
<div class="flex flex-col gap-4">
<FormTextInput
name="name"
label="View name"
label="Name"
show-label
color="foundation"
auto-focus
@@ -1,62 +1,66 @@
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<template>
<LayoutDisclosure
v-if="!isUngroupedGroup"
v-model:open="open"
v-model:edit-title="renameMode"
color="subtle"
:title="group.title"
lazy-load
@update:title="onRename"
>
<div :class="dropZoneClasses" v-on="on">
<LayoutDisclosure
v-if="!isUngroupedGroup"
v-model:open="open"
v-model:edit-title="renameMode"
color="subtle"
:title="group.title"
lazy-load
@update:title="onRename"
>
<ViewerSavedViewsPanelViewsGroupInner
:group="group"
:search="search"
:views-type="viewsType"
/>
<template #title-actions>
<div
class="flex gap-0.5 items-center opacity-0 group-hover/disclosure:opacity-100"
@click.stop
>
<LayoutMenu
v-if="!isUngroupedGroup"
v-model:open="showMenu"
:items="menuItems"
:menu-id="menuId"
mount-menu-on-body
show-ticks="right"
@chosen="({ item: actionItem }) => onActionChosen(actionItem)"
>
<FormButton
name="viewActions"
size="sm"
color="subtle"
:icon-left="Ellipsis"
hide-text
@click="showMenu = !showMenu"
/>
</LayoutMenu>
<div v-tippy="canCreateView?.errorMessage">
<FormButton
v-tippy="getTooltipProps('Create view')"
size="sm"
color="subtle"
:icon-left="Plus"
hide-text
name="addGroupView"
:disabled="!canCreateView.authorized || isLoading"
@click="onAddGroupView"
/>
</div>
</div>
</template>
</LayoutDisclosure>
<ViewerSavedViewsPanelViewsGroupInner
v-else
class="mb-[1px]"
:group="group"
:search="search"
:views-type="viewsType"
/>
<template #title-actions>
<div
class="flex gap-0.5 items-center opacity-0 group-hover/disclosure:opacity-100"
@click.stop
>
<LayoutMenu
v-model:open="showMenu"
:items="menuItems"
:menu-id="menuId"
mount-menu-on-body
show-ticks="right"
@chosen="({ item: actionItem }) => onActionChosen(actionItem)"
>
<FormButton
name="viewActions"
size="sm"
color="subtle"
:icon-left="Ellipsis"
hide-text
@click="showMenu = !showMenu"
/>
</LayoutMenu>
<div v-tippy="canCreateView?.errorMessage">
<FormButton
v-tippy="getTooltipProps('Create view in group')"
size="sm"
color="subtle"
:icon-left="Plus"
hide-text
name="addGroupView"
:disabled="!canCreateView.authorized || isLoading"
@click="onAddGroupView"
/>
</div>
</div>
</template>
</LayoutDisclosure>
<ViewerSavedViewsPanelViewsGroupInner
v-else
class="mb-[1px]"
:group="group"
:search="search"
:views-type="viewsType"
/>
</div>
</template>
<script setup lang="ts">
import { StringEnum, throwUncoveredError, type StringEnumValues } from '@speckle/shared'
@@ -70,11 +74,13 @@ import type {
ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment,
ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment
} from '~/lib/common/generated/gql/graphql'
import { ToastNotificationType } from '~/lib/common/composables/toast'
import {
useCreateSavedView,
useUpdateSavedViewGroup
} from '~/lib/viewer/composables/savedViews/management'
import type { ViewsType } from '~/lib/viewer/helpers/savedViews'
import { useDraggableViewTargetGroup } from '~/lib/viewer/composables/savedViews/ui'
const { getTooltipProps } = useSmartTooltipDelay()
@@ -105,6 +111,7 @@ graphql(`
...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup
...ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup
...UseUpdateSavedViewGroup_SavedViewGroup
...UseDraggableViewTargetGroup_SavedViewGroup
}
`)
@@ -140,8 +147,17 @@ const { triggerNotification } = useGlobalToast()
const isLoading = useMutationLoading()
const createView = useCreateSavedView()
const updateGroup = useUpdateSavedViewGroup()
const renameMode = defineModel<boolean>('renameMode')
const { on, classes: dropZoneClasses } = useDraggableViewTargetGroup({
group: computed(() => props.group),
onMoved: () => {
// Auto-open the group if it was closed
if (!open.value) {
open.value = true
}
}
})
const renameMode = defineModel<boolean>('renameMode')
const open = defineModel<boolean>('open')
const showMenu = ref(false)
const menuId = useId()
@@ -1,5 +1,5 @@
<template>
<div>
<div class="mb-1.5">
<div v-if="isVeryFirstLoading" class="flex justify-center">
<CommonLoadingIcon class="m-4" />
</div>
@@ -175,12 +175,12 @@ 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 ...UseSavedViewValidationHelpers_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 ...UseDraggableView_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 ...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,
"\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 ...UseDraggableViewTargetGroup_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 ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup on SavedViewGroup {\n id\n title\n ...UseDeleteSavedViewGroup_SavedViewGroup\n }\n": typeof types.ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": typeof types.ViewerSavedViewsPanelViewsGroupInner_SavedViewGroupFragmentDoc,
@@ -433,6 +433,8 @@ 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 UseDraggableView_SavedView on SavedView {\n id\n projectId\n name\n group {\n id\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseUpdateSavedView_SavedView\n }\n": typeof types.UseDraggableView_SavedViewFragmentDoc,
"\n fragment UseDraggableViewTargetGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": typeof types.UseDraggableViewTargetGroup_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,
@@ -693,12 +695,12 @@ 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 ...UseSavedViewValidationHelpers_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 ...UseDraggableView_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 ...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,
"\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 ...UseDraggableViewTargetGroup_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 ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup on SavedViewGroup {\n id\n title\n ...UseDeleteSavedViewGroup_SavedViewGroup\n }\n": types.ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragmentDoc,
"\n fragment ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": types.ViewerSavedViewsPanelViewsGroupInner_SavedViewGroupFragmentDoc,
@@ -951,6 +953,8 @@ 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 UseDraggableView_SavedView on SavedView {\n id\n projectId\n name\n group {\n id\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseUpdateSavedView_SavedView\n }\n": types.UseDraggableView_SavedViewFragmentDoc,
"\n fragment UseDraggableViewTargetGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": types.UseDraggableViewTargetGroup_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,
@@ -1711,7 +1715,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 ...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"];
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 ...UseDraggableView_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 ...UseDraggableView_SavedView\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1731,7 +1735,7 @@ export function graphql(source: "\n fragment ViewerSavedViewsPanelViewsGroup_Pr
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n isUngroupedViewsGroup\n title\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n"): (typeof documents)["\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"];
export function graphql(source: "\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 ...UseDraggableViewTargetGroup_SavedViewGroup\n }\n"): (typeof documents)["\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 ...UseDraggableViewTargetGroup_SavedViewGroup\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2740,6 +2744,14 @@ 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 UseDraggableView_SavedView on SavedView {\n id\n projectId\n name\n group {\n id\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...UseUpdateSavedView_SavedView\n }\n"): (typeof documents)["\n fragment UseDraggableView_SavedView on SavedView {\n id\n projectId\n name\n group {\n id\n }\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\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 UseDraggableViewTargetGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n"): (typeof documents)["\n fragment UseDraggableViewTargetGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -236,10 +236,18 @@ export const useUpdateSavedView = () => {
const { triggerNotification } = useGlobalToast()
const { isLoggedIn } = useActiveUser()
return async (params: {
view: UseUpdateSavedView_SavedViewFragment
input: UpdateSavedViewInput
}) => {
return async (
params: {
view: UseUpdateSavedView_SavedViewFragment
input: UpdateSavedViewInput
},
options?: Partial<{
/**
* Whether to skip toast notifications
*/
skipToast: boolean
}>
) => {
if (!isLoggedIn.value) return
const { input } = params
@@ -313,18 +321,20 @@ export const useUpdateSavedView = () => {
).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 view",
description: err,
type: ToastNotificationType.Danger
})
if (!options?.skipToast) {
if (res?.id) {
triggerNotification({
title: 'View updated',
type: ToastNotificationType.Success
})
} else {
const err = getFirstGqlErrorMessage(result?.errors)
triggerNotification({
title: "Couldn't update view",
description: err,
type: ToastNotificationType.Danger
})
}
}
return res
@@ -0,0 +1,168 @@
import { useMutationLoading } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
import type {
UseDraggableView_SavedViewFragment,
UseDraggableViewTargetGroup_SavedViewGroupFragment
} from '~/lib/common/generated/gql/graphql'
import { ensureError, safeParse } from '@speckle/shared'
import { has, isObjectLike } from 'lodash-es'
import { useUpdateSavedView } from '~/lib/viewer/composables/savedViews/management'
const isDraggableView = (view: unknown): view is UseDraggableView_SavedViewFragment =>
isObjectLike(view) && has(view, 'id') && has(view, 'permissions.canUpdate')
graphql(`
fragment UseDraggableView_SavedView on SavedView {
id
projectId
name
group {
id
}
permissions {
canUpdate {
...FullPermissionCheckResult
}
}
...UseUpdateSavedView_SavedView
}
`)
export const useDraggableView = (params: {
view: Ref<UseDraggableView_SavedViewFragment>
}) => {
const isDragging = ref(false)
const isLoading = useMutationLoading()
const classes = computed(() => {
const classParts: string[] = ['draggable-view']
if (isDragging.value) {
classParts.push('opacity-50 scale-95')
}
return classParts.join(' ')
})
const vOn = {
dragstart: (event: DragEvent) => {
if (!event.dataTransfer) return
if (!params.view.value.permissions.canUpdate.authorized || isLoading.value) {
event.preventDefault()
return
}
isDragging.value = true
event.dataTransfer.setData('application/json', JSON.stringify(params.view.value))
event.dataTransfer.effectAllowed = 'move'
const imageTarget =
(event.target as HTMLElement).closest('.draggable-view') ||
(event.target as HTMLElement)
event.dataTransfer.setDragImage(imageTarget, 0, 0)
},
dragend: () => {
isDragging.value = false
}
}
return {
classes,
on: vOn
}
}
graphql(`
fragment UseDraggableViewTargetGroup_SavedViewGroup on SavedViewGroup {
id
title
}
`)
export const useDraggableViewTargetGroup = (params: {
group: Ref<UseDraggableViewTargetGroup_SavedViewGroupFragment>
onMoved?: () => void
}) => {
const isDragOver = ref(false)
const dragCounter = ref(0)
const { triggerNotification } = useGlobalToast()
const updateView = useUpdateSavedView()
const vOn = {
dragover: (event: DragEvent) => {
if (!event.dataTransfer) return
event.preventDefault()
event.dataTransfer.dropEffect = 'move'
},
drop: async (event: DragEvent) => {
if (!event.dataTransfer) return
event.preventDefault()
isDragOver.value = false
dragCounter.value = 0
try {
const data = event.dataTransfer.getData('application/json')
const view = safeParse(data, isDraggableView)
if (!view || view.group.id === params.group.value.id) {
return
}
const success = await updateView({
view,
input: {
id: view.id,
projectId: view.projectId,
groupId: params.group.value.id
}
})
if (success) {
triggerNotification({
type: ToastNotificationType.Success,
title: `Moved "${view.name}" to "${params.group.value.title}"`
})
params.onMoved?.()
} else {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to move view'
})
}
} catch (e) {
triggerNotification({
type: ToastNotificationType.Danger,
title: 'Failed to move view',
description: ensureError(e).message
})
}
},
dragenter: (event: DragEvent) => {
event.preventDefault()
dragCounter.value++
isDragOver.value = true
},
dragleave: () => {
dragCounter.value--
if (dragCounter.value === 0) {
isDragOver.value = false
}
}
}
const classes = computed(() => {
const classParts: string[] = ['draggable-view-target']
if (isDragOver.value) {
classParts.push('rounded-md ring-2 ring-primary ring-opacity-50 bg-primary/5')
}
return classParts.join(' ')
})
return {
on: vOn,
classes
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ const config = {
`./plugins/**/*.{js,ts}`,
'./stories/**/*.{js,ts,vue,mdx}',
'./app.vue',
'./lib/**/composables/*.{js,ts}',
'./lib/**/composables/**/*.{js,ts}',
...themeEntries(),
...uiLibEntries()
],
@@ -0,0 +1,12 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const safeParse = <Result = any>(
json: string,
guard?: (data: unknown) => data is Result
): Result | null => {
try {
const parsed = JSON.parse(json) as Result
return guard ? (guard(parsed) ? parsed : null) : parsed
} catch {
return null
}
}
+1
View File
@@ -11,3 +11,4 @@ export * from './helpers/os.js'
export * from './helpers/optimization.js'
export * from './helpers/debugging.js'
export * from './helpers/url.js'
export * from './helpers/encoding.js'