feat: rename a group (#5228)
* add disclosure edit mode support * group update backend works * WIP group title edit * rename seems to work * fix menu overflow * remove comment * optimistic responser fix * rename validation * disclosure sync fix
This commit is contained in:
committed by
GitHub
parent
a3d18b7655
commit
be0155a95d
@@ -61,7 +61,7 @@
|
||||
<ViewerSavedViewsPanelConnectorViews
|
||||
v-if="selectedViewsType === ViewsType.Connector"
|
||||
/>
|
||||
<ViewerSavedViewsPanelViews
|
||||
<ViewerSavedViewsPanelGroups
|
||||
v-else
|
||||
v-model:selected-group-id="selectedGroupId"
|
||||
:views-type="selectedViewsType"
|
||||
|
||||
+22
-4
@@ -9,10 +9,13 @@
|
||||
v-for="group in groups"
|
||||
:key="group.id"
|
||||
:group="group"
|
||||
:is-selected="group.id === selectedGroupId"
|
||||
:is-selected="isGroupSelected(group)"
|
||||
:rename-mode="isGroupInRenameMode(group)"
|
||||
:only-authored="viewsType === ViewsType.My"
|
||||
@update:is-selected="(value) => (selectedGroupId = value ? group.id : null)"
|
||||
@update:rename-mode="(value) => (groupBeingRenamed = value ? group : undefined)"
|
||||
@delete-group="($event) => (groupBeingDeleted = $event)"
|
||||
@rename-group="($event) => (groupBeingRenamed = $event)"
|
||||
/>
|
||||
<InfiniteLoading
|
||||
v-if="groups.length"
|
||||
@@ -45,16 +48,18 @@ import { omit } from 'lodash-es'
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
UseUpdateSavedViewGroup_SavedViewGroupFragment,
|
||||
ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewEditDialog_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment,
|
||||
ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
import { ViewsType } from '~/lib/viewer/helpers/savedViews'
|
||||
|
||||
graphql(`
|
||||
fragment ViewerSavedViewsPanelViews_Project on Project {
|
||||
fragment ViewerSavedViewsPanelGroups_Project on Project {
|
||||
id
|
||||
savedViewGroups(input: $savedViewGroupsInput) {
|
||||
totalCount
|
||||
@@ -68,13 +73,13 @@ graphql(`
|
||||
`)
|
||||
|
||||
const paginableGroupsQuery = graphql(`
|
||||
query ViewerSavedViewsPanelViews_Groups(
|
||||
query ViewerSavedViewsPanelGroups_SavedViewGroups(
|
||||
$projectId: String!
|
||||
$savedViewGroupsInput: SavedViewGroupsInput!
|
||||
) {
|
||||
project(id: $projectId) {
|
||||
id
|
||||
...ViewerSavedViewsPanelViews_Project
|
||||
...ViewerSavedViewsPanelGroups_Project
|
||||
}
|
||||
}
|
||||
`)
|
||||
@@ -101,6 +106,7 @@ const viewBeingMoved = ref<ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment
|
||||
const viewBeingDeleted = ref<ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragment>()
|
||||
const groupBeingDeleted =
|
||||
ref<ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment>()
|
||||
const groupBeingRenamed = ref<UseUpdateSavedViewGroup_SavedViewGroupFragment>()
|
||||
|
||||
const {
|
||||
identifier,
|
||||
@@ -178,6 +184,18 @@ const showGroupDeleteDialog = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const isGroupInRenameMode = (
|
||||
group: ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment
|
||||
) => {
|
||||
return group.id === groupBeingRenamed.value?.id
|
||||
}
|
||||
|
||||
const isGroupSelected = (
|
||||
group: ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment
|
||||
) => {
|
||||
return group.id === selectedGroupId.value
|
||||
}
|
||||
|
||||
watch(
|
||||
groups,
|
||||
(newGroups) => {
|
||||
@@ -2,8 +2,10 @@
|
||||
<LayoutDisclosure
|
||||
v-if="!isUngroupedGroup"
|
||||
v-model:open="open"
|
||||
v-model:edit-title="renameMode"
|
||||
:title="group.title"
|
||||
lazy-load
|
||||
@update:title="onRename"
|
||||
>
|
||||
<ViewerSavedViewsPanelViewsGroupInner
|
||||
:group="group"
|
||||
@@ -60,18 +62,23 @@ import { useMutationLoading } from '@vue/apollo-composable'
|
||||
import { Ellipsis, Plus } from 'lucide-vue-next'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
UseUpdateSavedViewGroup_SavedViewGroupFragment,
|
||||
ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment,
|
||||
ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { useCreateSavedView } from '~/lib/viewer/composables/savedViews/management'
|
||||
import {
|
||||
useCreateSavedView,
|
||||
useUpdateSavedViewGroup
|
||||
} from '~/lib/viewer/composables/savedViews/management'
|
||||
|
||||
const MenuItems = StringEnum(['Delete'])
|
||||
const MenuItems = StringEnum(['Delete', 'Rename'])
|
||||
type MenuItems = StringEnumValues<typeof MenuItems>
|
||||
|
||||
graphql(`
|
||||
fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {
|
||||
id
|
||||
isUngroupedViewsGroup
|
||||
title
|
||||
permissions {
|
||||
canUpdate {
|
||||
...FullPermissionCheckResult
|
||||
@@ -79,6 +86,7 @@ graphql(`
|
||||
}
|
||||
...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup
|
||||
...ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup
|
||||
...UseUpdateSavedViewGroup_SavedViewGroup
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -100,6 +108,7 @@ const emit = defineEmits<{
|
||||
'delete-group': [
|
||||
group: ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment
|
||||
]
|
||||
'rename-group': [group: UseUpdateSavedViewGroup_SavedViewGroupFragment]
|
||||
}>()
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -108,9 +117,12 @@ const props = defineProps<{
|
||||
onlyAuthored?: boolean
|
||||
}>()
|
||||
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const isLoading = useMutationLoading()
|
||||
const createView = useCreateSavedView()
|
||||
const updateGroup = useUpdateSavedViewGroup()
|
||||
const isSelected = defineModel<boolean>('isSelected')
|
||||
const renameMode = defineModel<boolean>('renameMode')
|
||||
|
||||
const open = ref(false)
|
||||
const showMenu = ref(false)
|
||||
@@ -120,6 +132,14 @@ const isUngroupedGroup = computed(() => props.group.isUngroupedViewsGroup)
|
||||
const canUpdate = computed(() => props.group.permissions.canUpdate)
|
||||
|
||||
const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
|
||||
[
|
||||
{
|
||||
id: MenuItems.Rename,
|
||||
title: 'Rename',
|
||||
disabled: !canUpdate.value?.authorized || isLoading.value,
|
||||
disabledTooltip: canUpdate.value.errorMessage
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
id: MenuItems.Delete,
|
||||
@@ -135,6 +155,9 @@ const onActionChosen = async (item: LayoutMenuItem<MenuItems>) => {
|
||||
case MenuItems.Delete:
|
||||
emit('delete-group', props.group)
|
||||
break
|
||||
case MenuItems.Rename:
|
||||
emit('rename-group', props.group)
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(item.id)
|
||||
}
|
||||
@@ -147,6 +170,32 @@ const onAddGroupView = async () => {
|
||||
isSelected.value = true
|
||||
}
|
||||
|
||||
const onRename = async (newName: string) => {
|
||||
if (!newName.trim() || newName.length > 255) {
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Danger,
|
||||
title: 'Name must be between 1 and 255 characters long'
|
||||
})
|
||||
renameMode.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (props.group.title === newName) {
|
||||
renameMode.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const res = await updateGroup({
|
||||
group: props.group,
|
||||
update: {
|
||||
name: newName
|
||||
}
|
||||
})
|
||||
if (res?.id) {
|
||||
renameMode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isSelected.value,
|
||||
(isSelected) => {
|
||||
|
||||
@@ -167,13 +167,13 @@ 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 ViewerSavedViewsPanelGroups_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.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 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 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 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 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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_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 }\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,
|
||||
@@ -416,6 +416,8 @@ type Documents = {
|
||||
"\n mutation CreateSavedViewGroup($input: CreateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n createGroup(input: $input) {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n": typeof types.CreateSavedViewGroupDocument,
|
||||
"\n mutation DeleteSavedViewGroup($input: DeleteSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n deleteGroup(input: $input)\n }\n }\n }\n": typeof types.DeleteSavedViewGroupDocument,
|
||||
"\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 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,
|
||||
@@ -666,13 +668,13 @@ 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 ViewerSavedViewsPanelGroups_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.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 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 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 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 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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_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 }\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,
|
||||
@@ -915,6 +917,8 @@ const documents: Documents = {
|
||||
"\n mutation CreateSavedViewGroup($input: CreateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n createGroup(input: $input) {\n id\n ...ViewerSavedViewsPanelViewsGroup_SavedViewGroup\n }\n }\n }\n }\n": types.CreateSavedViewGroupDocument,
|
||||
"\n mutation DeleteSavedViewGroup($input: DeleteSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n deleteGroup(input: $input)\n }\n }\n }\n": types.DeleteSavedViewGroupDocument,
|
||||
"\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 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,
|
||||
@@ -1638,18 +1642,18 @@ export function graphql(source: "\n fragment ViewerResourcesWorkspaceLimitAlert
|
||||
* 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 ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\n ...FullPermissionCheckResult\n }\n }\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanel_Project on Project {\n id\n permissions {\n canCreateSavedView {\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.
|
||||
*/
|
||||
export function graphql(source: "\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 }\n"): (typeof documents)["\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 }\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 ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_Project\n }\n }\n"): (typeof documents)["\n query ViewerSavedViewsPanelGroups_SavedViewGroups(\n $projectId: String!\n $savedViewGroupsInput: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n ...ViewerSavedViewsPanelGroups_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 ViewerSavedViewsPanelView_SavedView on SavedView {\n id\n name\n description\n screenshot\n visibility\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 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"];
|
||||
/**
|
||||
* 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 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 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"];
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@@ -1665,7 +1669,7 @@ export function graphql(source: "\n fragment ViewerSavedViewsPanelViewMoveDialo
|
||||
/**
|
||||
* 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 permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroup\n }\n"): (typeof documents)["\n fragment ViewerSavedViewsPanelViewsGroup_SavedViewGroup on SavedViewGroup {\n id\n isUngroupedViewsGroup\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n }\n ...ViewerSavedViewsPanelViewsGroupInner_SavedViewGroup\n ...ViewerSavedViewsPanelViewsGroupDeleteDialog_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 }\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"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2634,6 +2638,14 @@ export function graphql(source: "\n mutation DeleteSavedViewGroup($input: Delet
|
||||
* 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 UseDeleteSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n groupId\n projectId\n isUngroupedViewsGroup\n }\n"): (typeof documents)["\n fragment UseDeleteSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n groupId\n projectId\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 mutation UpdateSavedViewGroup($input: UpdateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n updateGroup(input: $input) {\n id\n ...UseUpdateSavedViewGroup_SavedViewGroup\n }\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateSavedViewGroup($input: UpdateSavedViewGroupInput!) {\n projectMutations {\n savedViewMutations {\n updateGroup(input: $input) {\n id\n ...UseUpdateSavedViewGroup_SavedViewGroup\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 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.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,6 +8,12 @@ import { ApolloClients, provideApolloClient } from '@vue/apollo-composable'
|
||||
import {markRaw, toRaw} from 'vue'
|
||||
|
||||
export default defineNuxtPlugin(async (nuxt) => {
|
||||
// in dev mode, load better messages
|
||||
if (import.meta.dev) {
|
||||
const devSettings = await import('@apollo/client/dev')
|
||||
devSettings.loadDevMessages()
|
||||
}
|
||||
|
||||
// Load all configs
|
||||
const keyedConfigs = {};
|
||||
<% for (const key of Object.keys(options.configResolvers)) { %>
|
||||
|
||||
@@ -3,10 +3,13 @@ import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
CreateSavedViewGroupInput,
|
||||
CreateSavedViewInput,
|
||||
UpdateSavedViewGroupInput,
|
||||
UpdateSavedViewGroupMutationVariables,
|
||||
UpdateSavedViewInput,
|
||||
UseDeleteSavedView_SavedViewFragment,
|
||||
UseDeleteSavedViewGroup_SavedViewGroupFragment,
|
||||
UseUpdateSavedView_SavedViewFragment
|
||||
UseUpdateSavedView_SavedViewFragment,
|
||||
UseUpdateSavedViewGroup_SavedViewGroupFragment
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
import { useStateSerialization } from '~/lib/viewer/composables/serialization'
|
||||
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
||||
@@ -470,3 +473,86 @@ export const useDeleteSavedViewGroup = () => {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
const updateSavedViewGroupMutation = graphql(`
|
||||
mutation UpdateSavedViewGroup($input: UpdateSavedViewGroupInput!) {
|
||||
projectMutations {
|
||||
savedViewMutations {
|
||||
updateGroup(input: $input) {
|
||||
id
|
||||
...UseUpdateSavedViewGroup_SavedViewGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
graphql(`
|
||||
fragment UseUpdateSavedViewGroup_SavedViewGroup on SavedViewGroup {
|
||||
id
|
||||
projectId
|
||||
groupId
|
||||
title
|
||||
isUngroupedViewsGroup
|
||||
}
|
||||
`)
|
||||
|
||||
export const useUpdateSavedViewGroup = () => {
|
||||
const { mutate } = useMutation(updateSavedViewGroupMutation)
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
|
||||
return async (params: {
|
||||
group: UseUpdateSavedViewGroup_SavedViewGroupFragment
|
||||
update: Omit<UpdateSavedViewGroupInput, 'projectId' | 'groupId'>
|
||||
}) => {
|
||||
const { group, update } = params
|
||||
if (!isLoggedIn.value) return
|
||||
if (group.isUngroupedViewsGroup) return
|
||||
|
||||
const result = await mutate(
|
||||
{
|
||||
input: {
|
||||
projectId: group.projectId,
|
||||
groupId: group.id,
|
||||
...update
|
||||
}
|
||||
},
|
||||
{
|
||||
optimisticResponse(vars) {
|
||||
// apollo typing issue:
|
||||
const typedVars = vars as UpdateSavedViewGroupMutationVariables
|
||||
|
||||
// We want the name update to be immediate to avoid flashing content
|
||||
return {
|
||||
projectMutations: {
|
||||
savedViewMutations: {
|
||||
updateGroup: {
|
||||
...group,
|
||||
title: typedVars.input.name || group.title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
).catch(convertThrowIntoFetchResult)
|
||||
|
||||
const res = result?.data?.projectMutations.savedViewMutations.updateGroup
|
||||
if (res?.id) {
|
||||
triggerNotification({
|
||||
title: 'Group updated',
|
||||
type: ToastNotificationType.Success
|
||||
})
|
||||
} else {
|
||||
const err = getFirstGqlErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
title: "Couldn't update group",
|
||||
description: err,
|
||||
type: ToastNotificationType.Danger
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,9 +207,16 @@ input UpdateSavedViewInput {
|
||||
visibility: SavedViewVisibility
|
||||
}
|
||||
|
||||
input UpdateSavedViewGroupInput {
|
||||
projectId: ID!
|
||||
groupId: ID!
|
||||
name: String
|
||||
}
|
||||
|
||||
type SavedViewMutations {
|
||||
createGroup(input: CreateSavedViewGroupInput!): SavedViewGroup!
|
||||
deleteGroup(input: DeleteSavedViewGroupInput!): Boolean!
|
||||
updateGroup(input: UpdateSavedViewGroupInput!): SavedViewGroup!
|
||||
createView(input: CreateSavedViewInput!): SavedView!
|
||||
deleteView(input: DeleteSavedViewInput!): Boolean!
|
||||
updateView(input: UpdateSavedViewInput!): SavedView!
|
||||
|
||||
@@ -3537,6 +3537,7 @@ export type SavedViewMutations = {
|
||||
createView: SavedView;
|
||||
deleteGroup: Scalars['Boolean']['output'];
|
||||
deleteView: Scalars['Boolean']['output'];
|
||||
updateGroup: SavedViewGroup;
|
||||
updateView: SavedView;
|
||||
};
|
||||
|
||||
@@ -3561,6 +3562,11 @@ export type SavedViewMutationsDeleteViewArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type SavedViewMutationsUpdateGroupArgs = {
|
||||
input: UpdateSavedViewGroupInput;
|
||||
};
|
||||
|
||||
|
||||
export type SavedViewMutationsUpdateViewArgs = {
|
||||
input: UpdateSavedViewInput;
|
||||
};
|
||||
@@ -4396,6 +4402,12 @@ export type UpdateModelInput = {
|
||||
projectId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type UpdateSavedViewGroupInput = {
|
||||
groupId: Scalars['ID']['input'];
|
||||
name?: InputMaybe<Scalars['String']['input']>;
|
||||
projectId: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type UpdateSavedViewInput = {
|
||||
description?: InputMaybe<Scalars['String']['input']>;
|
||||
/** New group id, if grouping necessary */
|
||||
@@ -6110,6 +6122,7 @@ export type ResolversTypes = {
|
||||
UpdateAccSyncItemInput: UpdateAccSyncItemInput;
|
||||
UpdateAutomateFunctionInput: UpdateAutomateFunctionInput;
|
||||
UpdateModelInput: UpdateModelInput;
|
||||
UpdateSavedViewGroupInput: UpdateSavedViewGroupInput;
|
||||
UpdateSavedViewInput: UpdateSavedViewInput;
|
||||
UpdateServerRegionInput: UpdateServerRegionInput;
|
||||
UpdateVersionInput: UpdateVersionInput;
|
||||
@@ -6465,6 +6478,7 @@ export type ResolversParentTypes = {
|
||||
UpdateAccSyncItemInput: UpdateAccSyncItemInput;
|
||||
UpdateAutomateFunctionInput: UpdateAutomateFunctionInput;
|
||||
UpdateModelInput: UpdateModelInput;
|
||||
UpdateSavedViewGroupInput: UpdateSavedViewGroupInput;
|
||||
UpdateSavedViewInput: UpdateSavedViewInput;
|
||||
UpdateServerRegionInput: UpdateServerRegionInput;
|
||||
UpdateVersionInput: UpdateVersionInput;
|
||||
@@ -7798,6 +7812,7 @@ export type SavedViewMutationsResolvers<ContextType = GraphQLContext, ParentType
|
||||
createView?: Resolver<ResolversTypes['SavedView'], ParentType, ContextType, RequireFields<SavedViewMutationsCreateViewArgs, 'input'>>;
|
||||
deleteGroup?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<SavedViewMutationsDeleteGroupArgs, 'input'>>;
|
||||
deleteView?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<SavedViewMutationsDeleteViewArgs, 'input'>>;
|
||||
updateGroup?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType, RequireFields<SavedViewMutationsUpdateGroupArgs, 'input'>>;
|
||||
updateView?: Resolver<ResolversTypes['SavedView'], ParentType, ContextType, RequireFields<SavedViewMutationsUpdateViewArgs, 'input'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
@@ -9163,6 +9178,14 @@ export type CanUpdateSavedViewGroupQueryVariables = Exact<{
|
||||
|
||||
export type CanUpdateSavedViewGroupQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, savedViewGroup: { __typename?: 'SavedViewGroup', id: string, permissions: { __typename?: 'SavedViewGroupPermissionChecks', canUpdate: { __typename?: 'PermissionCheckResult', authorized: boolean, code: string, message: string, payload?: Record<string, unknown> | null } } } } };
|
||||
|
||||
export type UpdateSavedViewGroupMutationVariables = Exact<{
|
||||
input: UpdateSavedViewGroupInput;
|
||||
viewsInput?: SavedViewGroupViewsInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateSavedViewGroupMutation = { __typename?: 'Mutation', projectMutations: { __typename?: 'ProjectMutations', savedViewMutations: { __typename?: 'SavedViewMutations', updateGroup: { __typename?: 'SavedViewGroup', id: string, projectId: string, resourceIds: Array<string>, title: string, isUngroupedViewsGroup: boolean, views: { __typename?: 'SavedViewCollection', totalCount: number, cursor?: string | null, items: Array<{ __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 };
|
||||
@@ -10296,6 +10319,7 @@ export const CanUpdateSavedViewDocument = {"kind":"Document","definitions":[{"ki
|
||||
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 DeleteSavedViewGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteSavedViewGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteSavedViewGroupInput"}}}}],"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":"deleteGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]}}]} as unknown as DocumentNode<DeleteSavedViewGroupMutation, DeleteSavedViewGroupMutationVariables>;
|
||||
export const CanUpdateSavedViewGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CanUpdateSavedViewGroup"},"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":"groupId"}},"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":"savedViewGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"groupId"}}}],"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<CanUpdateSavedViewGroupQuery, CanUpdateSavedViewGroupQueryVariables>;
|
||||
export const UpdateSavedViewGroupDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateSavedViewGroup"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateSavedViewGroupInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupViewsInput"}}},"defaultValue":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}]}}],"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":"updateGroup"},"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":"BasicSavedViewGroup"}}]}}]}}]}}]}},{"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"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateSavedViewGroupMutation, UpdateSavedViewGroupMutationVariables>;
|
||||
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>;
|
||||
|
||||
@@ -143,6 +143,14 @@ export type DeleteSavedViewGroupRecord = (params: {
|
||||
projectId: string
|
||||
}) => Promise<boolean>
|
||||
|
||||
export type UpdateSavedViewGroupRecord = <
|
||||
Update extends Exact<Partial<SavedViewGroup>, Update>
|
||||
>(params: {
|
||||
groupId: string
|
||||
projectId: string
|
||||
update: Update
|
||||
}) => Promise<SavedViewGroup | undefined>
|
||||
|
||||
/////////////////////
|
||||
// SERVICE OPERATIONS:
|
||||
/////////////////////
|
||||
@@ -223,3 +231,12 @@ export type DeleteSavedViewGroup = (params: {
|
||||
}
|
||||
userId: string
|
||||
}) => Promise<boolean>
|
||||
|
||||
export type UpdateSavedViewGroup = (params: {
|
||||
input: {
|
||||
groupId: string
|
||||
projectId: string
|
||||
name?: MaybeNullOrUndefined<string>
|
||||
}
|
||||
userId: string
|
||||
}) => Promise<SavedViewGroup>
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
recalculateGroupResourceIdsFactory,
|
||||
storeSavedViewFactory,
|
||||
storeSavedViewGroupFactory,
|
||||
updateSavedViewGroupRecordFactory,
|
||||
updateSavedViewRecordFactory
|
||||
} from '@/modules/viewer/repositories/savedViews'
|
||||
import {
|
||||
@@ -38,7 +39,8 @@ import {
|
||||
deleteSavedViewGroupFactory,
|
||||
getGroupSavedViewsFactory,
|
||||
getProjectSavedViewGroupsFactory,
|
||||
updateSavedViewFactory
|
||||
updateSavedViewFactory,
|
||||
updateSavedViewGroupFactory
|
||||
} from '@/modules/viewer/services/savedViewsManagement'
|
||||
import { getViewerResourceGroupsFactory } from '@/modules/viewer/services/viewerResources'
|
||||
import { Authz } from '@speckle/shared'
|
||||
@@ -403,6 +405,34 @@ const resolvers: Resolvers = {
|
||||
})
|
||||
|
||||
return true
|
||||
},
|
||||
updateGroup: async (_parent, args, ctx) => {
|
||||
const projectId = args.input.projectId
|
||||
throwIfResourceAccessNotAllowed({
|
||||
resourceId: projectId,
|
||||
resourceType: TokenResourceIdentifierType.Project,
|
||||
resourceAccessRules: ctx.resourceAccessRules
|
||||
})
|
||||
|
||||
const canUpdate = await ctx.authPolicies.project.savedViews.canUpdateGroup({
|
||||
userId: ctx.userId,
|
||||
projectId,
|
||||
groupId: args.input.groupId
|
||||
})
|
||||
throwIfAuthNotOk(canUpdate)
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const updateSavedViewGroup = updateSavedViewGroupFactory({
|
||||
updateSavedViewGroupRecord: updateSavedViewGroupRecordFactory({
|
||||
db: projectDb
|
||||
}),
|
||||
getSavedViewGroup: getSavedViewGroupFactory({ loaders: ctx.loaders })
|
||||
})
|
||||
|
||||
return await updateSavedViewGroup({
|
||||
input: args.input,
|
||||
userId: ctx.userId!
|
||||
})
|
||||
}
|
||||
},
|
||||
ProjectPermissionChecks: {
|
||||
|
||||
@@ -19,7 +19,8 @@ import type {
|
||||
UpdateSavedViewRecord,
|
||||
GetSavedView,
|
||||
GetStoredViewGroupCount,
|
||||
DeleteSavedViewGroupRecord
|
||||
DeleteSavedViewGroupRecord,
|
||||
UpdateSavedViewGroupRecord
|
||||
} from '@/modules/viewer/domain/operations/savedViews'
|
||||
import {
|
||||
SavedViewVisibility,
|
||||
@@ -585,3 +586,20 @@ export const deleteSavedViewGroupRecordFactory =
|
||||
// Otherwise, return true
|
||||
return true
|
||||
}
|
||||
|
||||
export const updateSavedViewGroupRecordFactory =
|
||||
(deps: { db: Knex }): UpdateSavedViewGroupRecord =>
|
||||
async (params) => {
|
||||
const { groupId, projectId, update } = params
|
||||
|
||||
// Update the saved view group
|
||||
const [updatedGroup] = await tables
|
||||
.savedViewGroups(deps.db)
|
||||
.where({
|
||||
[SavedViewGroups.col.id]: groupId,
|
||||
[SavedViewGroups.col.projectId]: projectId
|
||||
})
|
||||
.update(update, '*')
|
||||
|
||||
return updatedGroup
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
StoreSavedView,
|
||||
StoreSavedViewGroup,
|
||||
UpdateSavedView,
|
||||
UpdateSavedViewGroup,
|
||||
UpdateSavedViewGroupRecord,
|
||||
UpdateSavedViewRecord
|
||||
} from '@/modules/viewer/domain/operations/savedViews'
|
||||
import { SavedViewVisibility } from '@/modules/viewer/domain/types/savedViews'
|
||||
@@ -422,14 +424,6 @@ export const updateSavedViewFactory =
|
||||
}
|
||||
: {})
|
||||
}
|
||||
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
|
||||
@@ -499,38 +493,50 @@ export const updateSavedViewFactory =
|
||||
}
|
||||
|
||||
// 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
|
||||
if (changes.name?.trim()) {
|
||||
if (changes.name.length > 255) {
|
||||
throw new SavedViewUpdateValidationError(
|
||||
'View name must be between 1 and 255 characters long',
|
||||
{
|
||||
info: {
|
||||
input,
|
||||
userId
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
delete changes['name']
|
||||
}
|
||||
|
||||
const finalChanges = omit(changes, ['resourceIdString', 'viewerState'])
|
||||
const update = {
|
||||
...finalChanges,
|
||||
...(resourceIds
|
||||
? {
|
||||
resourceIds: resourceIds ? resourceIds.map((r) => r.toString()) : undefined,
|
||||
groupResourceIds: formatResourceIdsForGroup(resourceIds)
|
||||
}
|
||||
: {}),
|
||||
...(viewerState
|
||||
? {
|
||||
viewerState
|
||||
}
|
||||
: {})
|
||||
}
|
||||
if (Object.keys(update).length === 0) {
|
||||
throw new SavedViewUpdateValidationError('No changes submitted with the input.', {
|
||||
info: {
|
||||
input,
|
||||
userId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
update
|
||||
})
|
||||
|
||||
if (updatedView?.groupId !== view.groupId) {
|
||||
@@ -573,3 +579,71 @@ export const deleteSavedViewGroupFactory =
|
||||
projectId
|
||||
})
|
||||
}
|
||||
|
||||
export const updateSavedViewGroupFactory =
|
||||
(deps: {
|
||||
updateSavedViewGroupRecord: UpdateSavedViewGroupRecord
|
||||
getSavedViewGroup: GetSavedViewGroup
|
||||
}): UpdateSavedViewGroup =>
|
||||
async ({ input, userId }) => {
|
||||
const { groupId, projectId } = input
|
||||
|
||||
if (isUngroupedGroup(groupId)) {
|
||||
throw new SavedViewGroupUpdateValidationError(
|
||||
'Cannot update ungrouped/default saved view group.'
|
||||
)
|
||||
}
|
||||
|
||||
const group = await deps.getSavedViewGroup({
|
||||
id: groupId,
|
||||
projectId
|
||||
})
|
||||
if (!group) {
|
||||
throw new SavedViewGroupUpdateValidationError('Group not found.', {
|
||||
info: {
|
||||
input,
|
||||
userId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const changes = removeNullOrUndefinedKeys(omit(input, ['groupId', 'projectId']))
|
||||
|
||||
// Validate name
|
||||
if (changes.name?.trim()) {
|
||||
if (changes.name.length > 255) {
|
||||
throw new SavedViewGroupUpdateValidationError(
|
||||
'View name must be between 1 and 255 characters long',
|
||||
{
|
||||
info: {
|
||||
input,
|
||||
userId
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
delete changes['name']
|
||||
}
|
||||
|
||||
if (Object.keys(changes).length === 0) {
|
||||
throw new SavedViewGroupUpdateValidationError(
|
||||
'No changes submitted with the input.',
|
||||
{
|
||||
info: {
|
||||
input,
|
||||
userId
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Update the saved view group
|
||||
const updatedGroup = await deps.updateSavedViewGroupRecord({
|
||||
groupId,
|
||||
projectId,
|
||||
update: changes
|
||||
})
|
||||
|
||||
return updatedGroup! // should exist, we checked before
|
||||
}
|
||||
|
||||
@@ -221,3 +221,20 @@ export const canUpdateSavedViewGroupQuery = gql`
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const updateSavedViewGroupMutation = gql`
|
||||
mutation UpdateSavedViewGroup(
|
||||
$input: UpdateSavedViewGroupInput!
|
||||
$viewsInput: SavedViewGroupViewsInput! = { limit: 10 }
|
||||
) {
|
||||
projectMutations {
|
||||
savedViewMutations {
|
||||
updateGroup(input: $input) {
|
||||
...BasicSavedViewGroup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${basicSavedViewGroupFragment}
|
||||
`
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
GetProjectSavedViewGroupsQueryVariables,
|
||||
GetProjectSavedViewQueryVariables,
|
||||
GetProjectUngroupedViewGroupQueryVariables,
|
||||
UpdateSavedViewGroupMutationVariables,
|
||||
UpdateSavedViewInput,
|
||||
UpdateSavedViewMutationVariables
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
@@ -27,7 +28,8 @@ import {
|
||||
GetProjectSavedViewGroupDocument,
|
||||
GetProjectSavedViewGroupsDocument,
|
||||
GetProjectUngroupedViewGroupDocument,
|
||||
UpdateSavedViewDocument
|
||||
UpdateSavedViewDocument,
|
||||
UpdateSavedViewGroupDocument
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import {
|
||||
buildBasicTestModel,
|
||||
@@ -39,6 +41,7 @@ import { SavedViewVisibility } from '@/modules/viewer/domain/types/savedViews'
|
||||
import {
|
||||
SavedViewCreationValidationError,
|
||||
SavedViewGroupCreationValidationError,
|
||||
SavedViewGroupUpdateValidationError,
|
||||
SavedViewInvalidResourceTargetError,
|
||||
SavedViewUpdateValidationError
|
||||
} from '@/modules/viewer/errors/savedViews'
|
||||
@@ -205,6 +208,35 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(CanUpdateSavedViewGroupDocument, input, options)
|
||||
|
||||
const updateSavedViewGroup = (
|
||||
input: UpdateSavedViewGroupMutationVariables,
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(UpdateSavedViewGroupDocument, input, options)
|
||||
|
||||
const getDefaultGroup = async (params: {
|
||||
projectId: string
|
||||
resourceIdString: string
|
||||
}) => {
|
||||
const { projectId, resourceIdString } = params
|
||||
|
||||
// Get default group id
|
||||
const groupsRes = await getProjectViewGroups(
|
||||
{
|
||||
projectId,
|
||||
input: {
|
||||
limit: 1,
|
||||
resourceIdString
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const defaultGroup = groupsRes.data?.project.savedViewGroups.items[0]
|
||||
expect(defaultGroup).to.be.ok
|
||||
expect(defaultGroup?.isUngroupedViewsGroup).to.be.true
|
||||
return defaultGroup!
|
||||
}
|
||||
|
||||
const model1ResourceIds = () => ViewerRoute.resourceBuilder().addModel(myModel1.id)
|
||||
|
||||
const model2ResourceIds = () => ViewerRoute.resourceBuilder().addModel(myModel2.id)
|
||||
@@ -919,23 +951,10 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
})
|
||||
|
||||
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
|
||||
expect(defaultGroup?.isUngroupedViewsGroup).to.be.true
|
||||
|
||||
// Update view to have that be the group
|
||||
const defaultGroup = await getDefaultGroup({
|
||||
projectId: updatablesProject.id,
|
||||
resourceIdString: models[0].id
|
||||
})
|
||||
const update = await updateView(
|
||||
{
|
||||
input: {
|
||||
@@ -953,6 +972,25 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
expect(updatedView?.group.id).to.equal(defaultGroup!.id)
|
||||
})
|
||||
|
||||
it('empty string name update gets ignored', async () => {
|
||||
const updatedname = ''
|
||||
|
||||
const res = await updateView({
|
||||
input: {
|
||||
id: testView.id,
|
||||
projectId: updatablesProject.id,
|
||||
name: updatedname
|
||||
}
|
||||
})
|
||||
|
||||
// should show empty changes update as we have nothing else to update
|
||||
expect(res).to.haveGraphQLErrors({
|
||||
code: SavedViewUpdateValidationError.code,
|
||||
message: 'No changes submitted with the input'
|
||||
})
|
||||
expect(res.data?.projectMutations.savedViewMutations.updateView.id).to.not.be.ok
|
||||
})
|
||||
|
||||
it('fails if user has no access to update the view', async () => {
|
||||
const newName = 'Updated View Name'
|
||||
|
||||
@@ -1086,6 +1124,144 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
})
|
||||
expect(res.data?.projectMutations.savedViewMutations.updateView).to.not.be.ok
|
||||
})
|
||||
|
||||
describe('to groups', () => {
|
||||
let updatableGroup: BasicSavedViewGroupFragment
|
||||
|
||||
beforeEach(async () => {
|
||||
const createRes = await createSavedViewGroup(
|
||||
{
|
||||
input: {
|
||||
projectId: updatablesProject.id,
|
||||
resourceIdString: models[0].id,
|
||||
groupName: 'Group to update'
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
const group = createRes.data?.projectMutations.savedViewMutations.createGroup!
|
||||
expect(group).to.be.ok
|
||||
updatableGroup = group
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await deleteSavedViewGroup({
|
||||
input: {
|
||||
groupId: updatableGroup.id,
|
||||
projectId: updatablesProject.id
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('successfully update the name', async () => {
|
||||
const updatedname = 'babababababababa123'
|
||||
|
||||
const res = await updateSavedViewGroup({
|
||||
input: {
|
||||
groupId: updatableGroup.id,
|
||||
projectId: updatableGroup.projectId,
|
||||
name: updatedname
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
|
||||
const group = res.data?.projectMutations.savedViewMutations.updateGroup
|
||||
expect(group?.id).to.be.ok
|
||||
expect(group?.title).to.equal(updatedname)
|
||||
})
|
||||
|
||||
it('fail invalid name length', async () => {
|
||||
const updatedname = 'a'.repeat(300)
|
||||
|
||||
const res = await updateSavedViewGroup({
|
||||
input: {
|
||||
groupId: updatableGroup.id,
|
||||
projectId: updatableGroup.projectId,
|
||||
name: updatedname
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors({
|
||||
code: SavedViewGroupUpdateValidationError.code
|
||||
})
|
||||
expect(res.data?.projectMutations.savedViewMutations.updateGroup.id).to.not.be
|
||||
.ok
|
||||
})
|
||||
|
||||
it('empty string name update gets ignored', async () => {
|
||||
const updatedname = ''
|
||||
|
||||
const res = await updateSavedViewGroup({
|
||||
input: {
|
||||
groupId: updatableGroup.id,
|
||||
projectId: updatableGroup.projectId,
|
||||
name: updatedname
|
||||
}
|
||||
})
|
||||
|
||||
// should show empty changes update as we have nothing else to update
|
||||
expect(res).to.haveGraphQLErrors({
|
||||
code: SavedViewGroupUpdateValidationError.code,
|
||||
message: 'No changes submitted with the input'
|
||||
})
|
||||
expect(res.data?.projectMutations.savedViewMutations.updateGroup.id).to.not.be
|
||||
.ok
|
||||
})
|
||||
|
||||
it('prevent updates to default/ungrouped groups', async () => {
|
||||
const defaultGroup = await getDefaultGroup({
|
||||
projectId: updatableGroup.projectId,
|
||||
resourceIdString: models[0].id
|
||||
})
|
||||
|
||||
const res = await updateSavedViewGroup({
|
||||
input: {
|
||||
groupId: defaultGroup.id,
|
||||
projectId: defaultGroup.projectId,
|
||||
name: 'New Group Name'
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors({
|
||||
code: BadRequestError.code,
|
||||
message: 'ungrouped group cannot be modified'
|
||||
})
|
||||
expect(res.data?.projectMutations.savedViewMutations.updateGroup.id).to.not.be
|
||||
.ok
|
||||
})
|
||||
|
||||
it('prevent updates to nonexistant groups', async () => {
|
||||
const res = await updateSavedViewGroup({
|
||||
input: {
|
||||
groupId: 'nonexistent-group-id',
|
||||
projectId: updatableGroup.projectId,
|
||||
name: 'New Group Name'
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors({
|
||||
code: NotFoundError.code
|
||||
})
|
||||
expect(res.data?.projectMutations.savedViewMutations.updateGroup.id).to.not.be
|
||||
.ok
|
||||
})
|
||||
|
||||
it('disallow empty changes being submitted', async () => {
|
||||
const res = await updateSavedViewGroup({
|
||||
input: {
|
||||
groupId: updatableGroup.id,
|
||||
projectId: updatableGroup.projectId
|
||||
}
|
||||
})
|
||||
|
||||
expect(res).to.haveGraphQLErrors({
|
||||
code: SavedViewGroupUpdateValidationError.code
|
||||
})
|
||||
expect(res.data?.projectMutations.savedViewMutations.updateGroup.id).to.not.be
|
||||
.ok
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('deletions', () => {
|
||||
@@ -1290,22 +1466,10 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
})
|
||||
|
||||
it('should fail to delete default group', async () => {
|
||||
// Get default group id
|
||||
const groupsRes = await getProjectViewGroups(
|
||||
{
|
||||
projectId: deletablesProject.id,
|
||||
input: {
|
||||
limit: 1,
|
||||
resourceIdString: models[0].id
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const defaultGroup = groupsRes.data?.project.savedViewGroups.items[0]
|
||||
expect(defaultGroup).to.be.ok
|
||||
expect(defaultGroup?.isUngroupedViewsGroup).to.be.true
|
||||
|
||||
const defaultGroup = await getDefaultGroup({
|
||||
projectId: deletablesProject.id,
|
||||
resourceIdString: models[0].id
|
||||
})
|
||||
const res = await deleteSavedViewGroup({
|
||||
input: {
|
||||
groupId: defaultGroup!.id,
|
||||
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
control: { type: 'select' }
|
||||
},
|
||||
color: {
|
||||
options: ['page', 'foundation', 'transparent'],
|
||||
options: ['page', 'foundation', 'transparent', 'fully-transparent'],
|
||||
control: { type: 'select' }
|
||||
},
|
||||
rules: {
|
||||
@@ -207,3 +207,19 @@ export const WithFoundationColor = mergeStories(Default, {
|
||||
name: generateRandomName('withFoundationColor')
|
||||
}
|
||||
})
|
||||
|
||||
export const WithFullyTransparentColor = mergeStories(Default, {
|
||||
render: (args) => ({
|
||||
components: { FormTextInput },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: `<div class="bg-foundation-page p-5">
|
||||
<form-text-input v-bind="args" @update:modelValue="args['update:modelValue']"/>
|
||||
</div>`
|
||||
}),
|
||||
args: {
|
||||
color: 'fully-transparent',
|
||||
name: generateRandomName('withFullyTransparentColor')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
:readonly="readOnly"
|
||||
role="textbox"
|
||||
v-bind="$attrs"
|
||||
:style="inputStyle"
|
||||
@change="$emit('change', { event: $event, value })"
|
||||
@input="$emit('input', { event: $event, value })"
|
||||
@focus="$emit('focus')"
|
||||
@@ -119,7 +120,7 @@
|
||||
import type { RuleExpression } from 'vee-validate'
|
||||
import { XMarkIcon } from '@heroicons/vue/20/solid'
|
||||
import { computed, ref, toRefs, useSlots } from 'vue'
|
||||
import type { PropType } from 'vue'
|
||||
import type { CSSProperties, PropType } from 'vue'
|
||||
import type { Nullable, Optional } from '@speckle/shared'
|
||||
import { useTextInputCore } from '~~/src/composables/form/textInput'
|
||||
import type { PropAnyComponent } from '~~/src/helpers/common/components'
|
||||
@@ -343,6 +344,16 @@ const {
|
||||
inputEl: inputElement
|
||||
})
|
||||
|
||||
const inputStyle = computed((): CSSProperties => {
|
||||
if (props.color !== 'fully-transparent') return {}
|
||||
|
||||
// In fully transparent mode, we want the input to fully blend in w/ parent styling
|
||||
const style: CSSProperties = {
|
||||
fontSize: 'inherit'
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
const leadingIconClasses = computed(() => {
|
||||
const classParts: string[] = ['h-4 w-4']
|
||||
|
||||
@@ -372,16 +383,21 @@ const iconClasses = computed((): string => {
|
||||
})
|
||||
|
||||
const sizeClasses = computed((): string => {
|
||||
// fully transparent should get sizing/coloring info from parent elements,
|
||||
// its supposed to fit into the existing style
|
||||
const ifNotFullyTransparent = (val: string) =>
|
||||
props.color === 'fully-transparent' ? '' : val
|
||||
|
||||
switch (props.size) {
|
||||
case 'sm':
|
||||
return 'h-6 text-body sm:text-body-sm'
|
||||
return `h-6 ${ifNotFullyTransparent('text-body sm:text-body-sm')}`
|
||||
case 'lg':
|
||||
return 'h-10 text-body sm:text-[13px]'
|
||||
return `h-10 ${ifNotFullyTransparent('text-body sm:text-[13px]')}`
|
||||
case 'xl':
|
||||
return 'h-14 text-body sm:text-sm'
|
||||
return `h-14 ${ifNotFullyTransparent('text-body sm:text-sm')}`
|
||||
case 'base':
|
||||
default:
|
||||
return 'h-8 text-body sm:text-body-sm'
|
||||
return `h-8 ${ifNotFullyTransparent('text-body sm:text-body-sm')}`
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -761,13 +761,13 @@ const finalItems = computed(() => {
|
||||
|
||||
const listboxOptionsClasses = computed(() => {
|
||||
const classParts = [
|
||||
'rounded-md bg-foundation py-1 label label--light border border-outline-3 shadow-md mt-1 '
|
||||
'rounded-md bg-foundation py-1 label label--light border border-outline-3 shadow-md'
|
||||
]
|
||||
|
||||
if (props.mountMenuOnBody) {
|
||||
classParts.push('fixed z-50')
|
||||
} else {
|
||||
classParts.push('absolute top-[100%] w-full z-40')
|
||||
classParts.push('absolute top-[100%] w-full z-40 mt-1')
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
|
||||
@@ -122,3 +122,31 @@ export const WithTitleActions: StoryObj = {
|
||||
...Default.args
|
||||
}
|
||||
}
|
||||
|
||||
export const WithEditableTitle: StoryObj = {
|
||||
render: (args) => ({
|
||||
components: { LayoutDisclosure, FormButton },
|
||||
setup() {
|
||||
const title = ref("Baby's first title")
|
||||
const editTitle = ref(false)
|
||||
|
||||
return { args, title, editTitle }
|
||||
},
|
||||
template: `
|
||||
<div class="flex flex-col gap-2">
|
||||
<LayoutDisclosure v-bind="args" v-model:edit-title="editTitle" v-model:title="title">
|
||||
<div class="flex flex-col text-foreground space-y-4">
|
||||
<div class="h4 font-semibold">Hello world!</div>
|
||||
<div>Lorem ipsum blah blah blah</div>
|
||||
</div>
|
||||
</LayoutDisclosure>
|
||||
<div>
|
||||
Saved/current title: {{ title }}
|
||||
</div>
|
||||
<FormButton @click="editTitle = true">Enable edit mode</FormButton>
|
||||
</div>`
|
||||
}),
|
||||
args: {
|
||||
...Default.args
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,18 @@
|
||||
<DisclosureButton :class="buttonClasses" @click="toggle">
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
<Component :is="icon" v-if="icon" class="h-5 w-5" />
|
||||
<span>{{ title }}</span>
|
||||
<span v-if="!editTitle">{{ title }}</span>
|
||||
<FormTextInput
|
||||
v-else
|
||||
v-bind="bind"
|
||||
name="disclosureTitle"
|
||||
color="fully-transparent"
|
||||
:input-classes="buttonTextClasses"
|
||||
:auto-focus="true"
|
||||
v-on="on"
|
||||
@click.stop
|
||||
@blur="onTitleInputBlur"
|
||||
/>
|
||||
<slot name="title-actions" />
|
||||
</div>
|
||||
<ChevronUpIcon :class="!open ? 'rotate-180 transform' : ''" class="h-5 w-5" />
|
||||
@@ -24,14 +35,14 @@ import {
|
||||
DisclosurePanel
|
||||
} from '@headlessui/vue'
|
||||
import { ChevronUpIcon } from '@heroicons/vue/24/solid'
|
||||
import { computed } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
import type { PropAnyComponent } from '~~/src/helpers/common/components'
|
||||
import { FormTextInput, useDebouncedTextInput } from '~~/src/lib'
|
||||
|
||||
type DisclosureColor = 'default' | 'danger' | 'success' | 'warning'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
title: string
|
||||
/**
|
||||
* HeadlessUI icon component to use
|
||||
*/
|
||||
@@ -41,42 +52,71 @@ const props = withDefaults(
|
||||
* Whether to lazy load the panel contents only upon opening
|
||||
*/
|
||||
lazyLoad?: boolean
|
||||
/**
|
||||
* If edit mode enabled - it will exit mode when user unfocuses
|
||||
*/
|
||||
exitEditModeOnBlur?: boolean
|
||||
}>(),
|
||||
{
|
||||
color: 'default'
|
||||
color: 'default',
|
||||
exitEditModeOnBlur: true
|
||||
}
|
||||
)
|
||||
|
||||
const editTitle = defineModel<boolean>('editTitle')
|
||||
const title = defineModel<string>('title')
|
||||
const open = defineModel<boolean>('open', {
|
||||
default: false
|
||||
})
|
||||
|
||||
const { on, bind, syncFromValue } = useDebouncedTextInput({
|
||||
disableDebouncedInput: true,
|
||||
model: title
|
||||
})
|
||||
|
||||
const buttonTextClasses = computed(() => {
|
||||
const classParts = ['font-medium']
|
||||
|
||||
switch (props.color) {
|
||||
case 'warning':
|
||||
classParts.push('text-warning')
|
||||
break
|
||||
case 'success':
|
||||
classParts.push('text-success')
|
||||
break
|
||||
case 'danger':
|
||||
classParts.push('text-danger')
|
||||
break
|
||||
case 'default':
|
||||
default:
|
||||
classParts.push('text-primary')
|
||||
break
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
})
|
||||
|
||||
const buttonClasses = computed(() => {
|
||||
const classParts = [
|
||||
'pr-3 h-10 w-full flex items-center justify-between border-l-2 px-2 rounded transition',
|
||||
'ring-1 font-medium',
|
||||
'group/disclosure'
|
||||
'ring-1',
|
||||
'group/disclosure',
|
||||
buttonTextClasses.value
|
||||
]
|
||||
|
||||
switch (props.color) {
|
||||
case 'warning':
|
||||
classParts.push(
|
||||
'border-warning text-warning ring-warning-lighter hover:ring-warning'
|
||||
)
|
||||
classParts.push('border-warning ring-warning-lighter hover:ring-warning')
|
||||
break
|
||||
case 'success':
|
||||
classParts.push(
|
||||
'border-success text-success ring-success-lighter hover:ring-success'
|
||||
)
|
||||
classParts.push('border-success ring-success-lighter hover:ring-success')
|
||||
break
|
||||
case 'danger':
|
||||
classParts.push('border-danger text-danger ring-danger-lighter hover:ring-danger')
|
||||
classParts.push('border-danger ring-danger-lighter hover:ring-danger')
|
||||
break
|
||||
case 'default':
|
||||
default:
|
||||
classParts.push(
|
||||
'border-primary text-primary ring-primary-muted hover:ring-primary'
|
||||
)
|
||||
classParts.push('border-primary ring-primary-muted hover:ring-primary')
|
||||
break
|
||||
}
|
||||
|
||||
@@ -108,4 +148,17 @@ const panelClasses = computed(() => {
|
||||
const toggle = () => {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
const onTitleInputBlur = () => {
|
||||
if (!props.exitEditModeOnBlur) return
|
||||
|
||||
editTitle.value = false
|
||||
}
|
||||
|
||||
watch(editTitle, (newVal, oldVal) => {
|
||||
// Reset input value on turning on edit mode
|
||||
if (newVal && !oldVal) {
|
||||
syncFromValue()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { EllipsisVerticalIcon, StarIcon } from '@heroicons/vue/24/solid'
|
||||
import { action } from '@storybook/addon-actions'
|
||||
import { computed, ref } from 'vue'
|
||||
import { HorizontalDirection } from '~~/src/lib'
|
||||
import { StringEnum, type StringEnumValues } from '@speckle/shared'
|
||||
import { includes } from 'lodash-es'
|
||||
|
||||
type StoryType = StoryObj<
|
||||
Record<string, unknown> & {
|
||||
@@ -142,30 +144,78 @@ export const WithResponsiveMenuDirection: StoryType = {
|
||||
render: (args, ctx) => ({
|
||||
components: { LayoutMenu, FormButton, EllipsisVerticalIcon },
|
||||
setup() {
|
||||
const location = ref<string | undefined>('left')
|
||||
const Location = StringEnum([
|
||||
'TopLeft',
|
||||
'TopCenter',
|
||||
'TopRight',
|
||||
'BottomLeft',
|
||||
'BottomCenter',
|
||||
'BottomRight'
|
||||
])
|
||||
type Location = StringEnumValues<typeof Location>
|
||||
|
||||
const location = ref<Location>(Location.TopLeft)
|
||||
const showMenu = ref(false)
|
||||
|
||||
const changeLocation = () => {
|
||||
if (location.value === 'left') {
|
||||
location.value = 'right'
|
||||
} else if (location.value === 'right') {
|
||||
location.value = undefined
|
||||
} else {
|
||||
location.value = 'left'
|
||||
switch (location.value) {
|
||||
case Location.TopLeft:
|
||||
location.value = Location.TopCenter
|
||||
break
|
||||
case Location.TopCenter:
|
||||
location.value = Location.TopRight
|
||||
break
|
||||
case Location.TopRight:
|
||||
location.value = Location.BottomLeft
|
||||
break
|
||||
case Location.BottomLeft:
|
||||
location.value = Location.BottomCenter
|
||||
break
|
||||
case Location.BottomCenter:
|
||||
location.value = Location.BottomRight
|
||||
break
|
||||
case Location.BottomRight:
|
||||
location.value = Location.TopLeft
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const wrapperClasses = computed(() => {
|
||||
const classParts: string[] = []
|
||||
|
||||
if (location.value === 'left') {
|
||||
// x axis
|
||||
const isLeft = includes([Location.TopLeft, Location.BottomLeft], location.value)
|
||||
const isRight = includes(
|
||||
[Location.TopRight, Location.BottomRight],
|
||||
location.value
|
||||
)
|
||||
const isCenter = includes(
|
||||
[Location.TopCenter, Location.BottomCenter],
|
||||
location.value
|
||||
)
|
||||
if (isLeft) {
|
||||
classParts.push('items-start')
|
||||
} else if (location.value === 'right') {
|
||||
} else if (isRight) {
|
||||
classParts.push('items-end')
|
||||
} else {
|
||||
} else if (isCenter) {
|
||||
classParts.push('items-center')
|
||||
}
|
||||
|
||||
// y axis
|
||||
const isTop = includes(
|
||||
[Location.TopLeft, Location.TopCenter, Location.TopRight],
|
||||
location.value
|
||||
)
|
||||
const isBottom = includes(
|
||||
[Location.BottomLeft, Location.BottomCenter, Location.BottomRight],
|
||||
location.value
|
||||
)
|
||||
if (isTop) {
|
||||
classParts.push('justify-start')
|
||||
} else if (isBottom) {
|
||||
classParts.push('justify-end')
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
})
|
||||
|
||||
@@ -179,23 +229,27 @@ export const WithResponsiveMenuDirection: StoryType = {
|
||||
wrapperClasses
|
||||
}
|
||||
},
|
||||
// -2rem for padding added by storybook
|
||||
template: `
|
||||
<div :class="['flex gap-2 flex-col', wrapperClasses]">
|
||||
<LayoutMenu
|
||||
v-bind="args"
|
||||
@click.stop.prevent
|
||||
v-model:open="showMenu"
|
||||
:items="longItems"
|
||||
@chosen="chosen"
|
||||
@update:open="args['update:open']"
|
||||
>
|
||||
<FormButton @click="showMenu = !showMenu">
|
||||
<EllipsisVerticalIcon class="w-4 h-4" />
|
||||
Open menu
|
||||
</FormButton>
|
||||
</LayoutMenu>
|
||||
<FormButton @click="changeLocation">Change location</FormButton>
|
||||
</div>`,
|
||||
<div class="flex gap-2 flex-col min-h-[calc(100vh-2rem)]">
|
||||
<FormButton @click="changeLocation">Change location</FormButton>
|
||||
<div :class="['flex gap-2 flex-col grow', wrapperClasses]">
|
||||
<LayoutMenu
|
||||
v-bind="args"
|
||||
@click.stop.prevent
|
||||
v-model:open="showMenu"
|
||||
:items="longItems"
|
||||
@chosen="chosen"
|
||||
@update:open="args['update:open']"
|
||||
>
|
||||
<FormButton @click="showMenu = !showMenu">
|
||||
<EllipsisVerticalIcon class="w-4 h-4" />
|
||||
Open menu
|
||||
</FormButton>
|
||||
</LayoutMenu>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
methods: {
|
||||
onOpenUpdate(val: boolean) {
|
||||
args['update:open'](val)
|
||||
|
||||
@@ -57,7 +57,7 @@ import {
|
||||
useResponsiveHorizontalDirectionCalculation
|
||||
} from '~~/src/composables/common/window'
|
||||
import type { LayoutMenuItem } from '~~/src/helpers/layout/components'
|
||||
import { useElementBounding, useEventListener } from '@vueuse/core'
|
||||
import { useElementBounding, useElementSize, useEventListener } from '@vueuse/core'
|
||||
import { useBodyMountedMenuPositioning } from '~~/src/composables/layout/menu'
|
||||
import { isNumber } from '#lodash'
|
||||
import IconCheck from '~~/src/components/global/icon/Check.vue'
|
||||
@@ -101,6 +101,8 @@ const menuButtonBounding = useElementBounding(menuButtonWrapper, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const menuItemsSize = useElementSize(computed(() => menuItems.value?.el || null))
|
||||
|
||||
const { direction: calculatedDirection } = useResponsiveHorizontalDirectionCalculation({
|
||||
el: computed(() => menuItems.value?.el || null),
|
||||
defaultDirection: props.menuPosition,
|
||||
@@ -124,7 +126,8 @@ const { menuStyle } = useBodyMountedMenuPositioning({
|
||||
default:
|
||||
return 176
|
||||
}
|
||||
})
|
||||
}),
|
||||
menuHeight: computed(() => menuItemsSize.height.value)
|
||||
})
|
||||
|
||||
const menuItemsStyles = computed(() => {
|
||||
@@ -141,7 +144,7 @@ const menuItemsStyles = computed(() => {
|
||||
|
||||
const menuItemsClasses = computed(() => {
|
||||
const classParts = [
|
||||
'mt-1 w-44 origin-top-right divide-y divide-outline-3 rounded-md bg-foundation shadow-lg border border-outline-2 z-50'
|
||||
'w-44 origin-top-right divide-y divide-outline-3 rounded-md bg-foundation shadow-lg border border-outline-2 z-50'
|
||||
]
|
||||
|
||||
if (props.customMenuItemsClasses) {
|
||||
@@ -151,7 +154,7 @@ const menuItemsClasses = computed(() => {
|
||||
if (props.mountMenuOnBody) {
|
||||
classParts.push('fixed')
|
||||
} else {
|
||||
classParts.push('absolute')
|
||||
classParts.push('absolute mt-1')
|
||||
|
||||
if (menuDirection.value === HorizontalDirection.Left) {
|
||||
classParts.push('right-0')
|
||||
|
||||
@@ -5,10 +5,18 @@ import { computed, onMounted, ref, unref, watch } from 'vue'
|
||||
import type { Ref, ToRefs } from 'vue'
|
||||
import type { MaybeNullOrUndefined, Nullable } from '@speckle/shared'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { debounce, isArray, isBoolean, isString, isUndefined, noop } from '#lodash'
|
||||
import {
|
||||
debounce,
|
||||
includes,
|
||||
isArray,
|
||||
isBoolean,
|
||||
isString,
|
||||
isUndefined,
|
||||
noop
|
||||
} from '#lodash'
|
||||
import type { LabelPosition } from './input'
|
||||
|
||||
export type InputColor = 'page' | 'foundation' | 'transparent'
|
||||
export type InputColor = 'page' | 'foundation' | 'transparent' | 'fully-transparent'
|
||||
|
||||
/**
|
||||
* Common setup for text input & textarea fields
|
||||
@@ -77,23 +85,31 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
})
|
||||
|
||||
const coreClasses = computed(() => {
|
||||
const classParts = [
|
||||
'block w-full text-foreground transition-all',
|
||||
coreInputClasses.value
|
||||
]
|
||||
const color = unref(props.color)
|
||||
const classParts = ['block w-full text-foreground', coreInputClasses.value]
|
||||
|
||||
if (color !== 'fully-transparent') {
|
||||
classParts.push('py-2 px-3')
|
||||
} else {
|
||||
classParts.push('p-0')
|
||||
}
|
||||
|
||||
if (hasError.value) {
|
||||
classParts.push('!border-danger')
|
||||
} else {
|
||||
classParts.push('border-0 focus:ring-2 focus:ring-outline-2')
|
||||
classParts.push('border-0')
|
||||
if (color !== 'fully-transparent') {
|
||||
classParts.push('transition-all focus:ring-2 focus:ring-outline-2')
|
||||
} else {
|
||||
classParts.push('focus:ring-0')
|
||||
}
|
||||
}
|
||||
|
||||
const color = unref(props.color)
|
||||
if (color === 'foundation') {
|
||||
classParts.push(
|
||||
'bg-foundation !border border-outline-2 hover:border-outline-5 focus-visible:border-outline-4 !ring-0 focus-visible:!outline-0'
|
||||
)
|
||||
} else if (color === 'transparent') {
|
||||
} else if (includes(['transparent', 'fully-transparent'], color)) {
|
||||
classParts.push('bg-transparent')
|
||||
} else {
|
||||
classParts.push('bg-foundation-page')
|
||||
@@ -127,6 +143,7 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
const helpTipId = computed(() =>
|
||||
hasHelpTip.value ? `${unref(props.name)}-${internalHelpTipId.value}` : undefined
|
||||
)
|
||||
|
||||
const helpTipClasses = computed((): string => {
|
||||
const classParts = ['text-body-2xs break-words']
|
||||
classParts.push(hasError.value ? 'text-danger' : 'text-foreground-2')
|
||||
@@ -135,6 +152,7 @@ export function useTextInputCore<V extends string | string[] = string>(params: {
|
||||
}
|
||||
return classParts.join(' ')
|
||||
})
|
||||
|
||||
const shouldShowClear = computed(() => {
|
||||
if (!unref(props.showClear)) return false
|
||||
return (value.value?.length || 0) > 0
|
||||
@@ -192,6 +210,13 @@ export function useDebouncedTextInput(params?: {
|
||||
*/
|
||||
debouncedBy?: number
|
||||
|
||||
/**
|
||||
* If enabled, value will only change on submit/enter, and just typing in values will never
|
||||
* register.
|
||||
* Default: false
|
||||
*/
|
||||
disableDebouncedInput?: boolean
|
||||
|
||||
/**
|
||||
* Optionally pass in the model ref that should be used as the source of truth
|
||||
*/
|
||||
@@ -217,15 +242,29 @@ export function useDebouncedTextInput(params?: {
|
||||
* Set to true if you want to see debug output for how events fire and are handled
|
||||
*/
|
||||
debug?: boolean | ((...logArgs: unknown[]) => void)
|
||||
|
||||
/**
|
||||
* Callback function that gets called when a new value is actually written to the model
|
||||
*/
|
||||
onWrite?: (val: string) => void
|
||||
}) {
|
||||
const { debouncedBy = 1000, isBasicHtmlInput = false, submitOnEnter } = params || {}
|
||||
const {
|
||||
debouncedBy = 1000,
|
||||
isBasicHtmlInput = false,
|
||||
submitOnEnter,
|
||||
disableDebouncedInput,
|
||||
onWrite
|
||||
} = params || {}
|
||||
const log = params?.debug
|
||||
? isBoolean(params.debug)
|
||||
? console.debug
|
||||
: params.debug
|
||||
: noop
|
||||
|
||||
// The actual source of truth holding the final value
|
||||
const value = params?.model || ref('')
|
||||
|
||||
// The internal model of the input
|
||||
const model = ref(value.value)
|
||||
|
||||
const getValue = (val: string | InputEvent | Event | FormInputChangeEvent) => {
|
||||
@@ -236,29 +275,39 @@ export function useDebouncedTextInput(params?: {
|
||||
return target?.value || ''
|
||||
}
|
||||
|
||||
const debouncedValueUpdate = debounce((val: string) => {
|
||||
/**
|
||||
* Persist changes to the core underlying source of truth that's available outwards
|
||||
*/
|
||||
const persistValue = (val: string) => {
|
||||
value.value = val
|
||||
log('Value updated: ' + val)
|
||||
}, debouncedBy)
|
||||
onWrite?.(val)
|
||||
}
|
||||
|
||||
const debouncedValueUpdate = disableDebouncedInput
|
||||
? undefined
|
||||
: debounce((val: string) => {
|
||||
persistValue(val)
|
||||
}, debouncedBy)
|
||||
|
||||
const inputEventName = isBasicHtmlInput ? 'input' : 'update:modelValue'
|
||||
const on = {
|
||||
[inputEventName]: (val: string | InputEvent) => {
|
||||
const newVal = getValue(val)
|
||||
model.value = newVal
|
||||
debouncedValueUpdate(newVal)
|
||||
debouncedValueUpdate?.(newVal)
|
||||
log(`Input event [${inputEventName}] triggered: ${newVal}`)
|
||||
},
|
||||
clear: () => {
|
||||
debouncedValueUpdate.cancel()
|
||||
debouncedValueUpdate?.cancel()
|
||||
model.value = ''
|
||||
value.value = ''
|
||||
persistValue('')
|
||||
log('Clear event')
|
||||
},
|
||||
change: (val: FormInputChangeEvent | Event) => {
|
||||
const newVal = getValue(val)
|
||||
debouncedValueUpdate.cancel()
|
||||
value.value = newVal
|
||||
debouncedValueUpdate?.cancel()
|
||||
persistValue(newVal)
|
||||
model.value = newVal
|
||||
log('Change event: ' + newVal)
|
||||
},
|
||||
@@ -297,9 +346,18 @@ export function useDebouncedTextInput(params?: {
|
||||
model.value = value.value
|
||||
})
|
||||
|
||||
const syncFromValue = () => {
|
||||
debouncedValueUpdate?.cancel()
|
||||
model.value = value.value
|
||||
}
|
||||
|
||||
return {
|
||||
on,
|
||||
bind,
|
||||
value
|
||||
value,
|
||||
/**
|
||||
* Force sync internal state from the source of truth
|
||||
*/
|
||||
syncFromValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { HorizontalDirection } from '~~/src/composables/common/window'
|
||||
* Simplifies correctly and responsively positioning (dropdown/right-click/etc) menus so that they open
|
||||
* to the correct direction, can change directions if there's not enough space or even go full screen
|
||||
* if there's no space in either direction.
|
||||
*
|
||||
* Also supports updating vertical position, incase the menu would clip w/ the bottom of the screen
|
||||
*/
|
||||
export const useBodyMountedMenuPositioning = (params: {
|
||||
/**
|
||||
@@ -22,6 +24,10 @@ export const useBodyMountedMenuPositioning = (params: {
|
||||
* that just uses the button width.
|
||||
*/
|
||||
menuWidth: ComputedRef<number | undefined>
|
||||
/**
|
||||
* Optionally also control target menu height.
|
||||
*/
|
||||
menuHeight?: ComputedRef<number | undefined>
|
||||
}) => {
|
||||
const menuStyle = computed(() => {
|
||||
const style: CSSProperties = {}
|
||||
@@ -31,6 +37,11 @@ export const useBodyMountedMenuPositioning = (params: {
|
||||
* 1.a. If menuWidth is bigger than screen width, use screen width
|
||||
* 1.b. If menuWidth is smaller than screen width, use menuWidth
|
||||
* 2. If 1.b. but menu is leaving screen bounds, make it open to other direction
|
||||
*
|
||||
* Also:
|
||||
* 1.a. If menuHeight is bigger than screen height, use screen height
|
||||
* 1.b. If menuHeight is smaller than screen height, use screenHeight
|
||||
* 2. If 1.b. but menu is leaving screen bounds, make it open to other direction (upwards)
|
||||
*/
|
||||
|
||||
const openToLeft = unref(params.menuOpenDirection) === HorizontalDirection.Left
|
||||
@@ -39,40 +50,57 @@ export const useBodyMountedMenuPositioning = (params: {
|
||||
const left = params.buttonBoundingBox.left.value
|
||||
const width = params.buttonBoundingBox.width.value
|
||||
const height = params.buttonBoundingBox.height.value
|
||||
const margin = 4 // how much space to leave in full-screen mode or between button and menu
|
||||
|
||||
let finalWidth = width
|
||||
let finalLeft = left
|
||||
let finalTop = top + height + margin
|
||||
|
||||
const menuWidth = unref(params.menuWidth)
|
||||
const menuHeight = unref(params?.menuHeight)
|
||||
|
||||
const viewportWidth = window.innerWidth
|
||||
const xMargin = 10 // how much space to leave in full-screen mode
|
||||
const viewportWithoutMargins = viewportWidth - xMargin * 2
|
||||
const viewportHeight = window.innerHeight
|
||||
|
||||
const viewportWidthWithoutMargins = viewportWidth - margin * 2
|
||||
const viewportHeightWithoutMargins = viewportHeight - margin * 2
|
||||
|
||||
if (!isUndefined(menuWidth)) {
|
||||
if (menuWidth > viewportWithoutMargins) {
|
||||
if (menuWidth > viewportWidthWithoutMargins) {
|
||||
// Menu too big: use full screen width
|
||||
finalWidth = viewportWithoutMargins
|
||||
finalLeft = xMargin
|
||||
finalWidth = viewportWidthWithoutMargins
|
||||
finalLeft = margin
|
||||
} else {
|
||||
// Open to right or left depending on available space
|
||||
finalWidth = menuWidth
|
||||
|
||||
if (openToLeft) {
|
||||
finalLeft = left + width - menuWidth
|
||||
if (finalLeft < xMargin) {
|
||||
finalLeft = xMargin
|
||||
if (finalLeft < margin) {
|
||||
finalLeft = margin
|
||||
}
|
||||
} else {
|
||||
if (left + menuWidth > viewportWithoutMargins) {
|
||||
finalLeft = Math.max(left + width - menuWidth, xMargin)
|
||||
if (left + menuWidth > viewportWidthWithoutMargins) {
|
||||
finalLeft = Math.max(left + width - menuWidth, margin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUndefined(menuHeight)) {
|
||||
if (menuHeight > viewportHeightWithoutMargins) {
|
||||
finalTop = margin
|
||||
} else {
|
||||
// By default opens downward, see if we need to move upward instead
|
||||
if (top + height + menuHeight > viewportHeightWithoutMargins) {
|
||||
finalTop = top - menuHeight - margin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
style.left = `${finalLeft}px`
|
||||
style.width = `${finalWidth}px`
|
||||
style.top = `${top + height}px`
|
||||
style.top = `${finalTop}px`
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user