Files
speckle-server/packages/frontend-2/components/project/page/models/Actions.vue
T

273 lines
7.4 KiB
Vue

<!-- eslint-disable vuejs-accessibility/mouse-events-have-key-events -->
<template>
<div class="relative">
<LayoutMenu
v-model:open="showActionsMenu"
:menu-id="menuId"
:items="actionsItems"
:menu-position="menuPosition ? menuPosition : HorizontalDirection.Left"
:mount-menu-on-body="mountMenuOnBody"
@click.stop.prevent
@chosen="onActionChosen"
>
<FormButton
color="subtle"
hide-text
:icon-right="EllipsisHorizontalIcon"
class="!text-foreground-2"
@click="onButtonClick"
></FormButton>
</LayoutMenu>
<ProjectPageModelsCardEditDialog
v-model:open="isRenameDialogOpen"
:model="model"
:project-id="project.id"
@updated="$emit('model-updated')"
/>
<ProjectPageModelsCardDeleteDialog
v-model:open="isDeleteDialogOpen"
:model="model"
:project-id="project.id"
@deleted="$emit('model-updated')"
/>
<ProjectModelPageDialogEmbed
v-model:open="isEmbedDialogOpen"
:project="project"
:model-id="model.id"
/>
<ProjectPageModelsUploadsDialog
v-model:open="isUploadsDialogOpen"
:project-id="project.id"
:model-id="model.id"
/>
</div>
</template>
<script setup lang="ts">
import type { Nullable } from '@speckle/shared'
import type {
ProjectPageModelsActionsFragment,
ProjectPageModelsActions_ProjectFragment
} from '~~/lib/common/generated/gql/graphql'
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
import { useCopyModelLink } from '~~/lib/projects/composables/modelManagement'
import { EllipsisHorizontalIcon } from '@heroicons/vue/24/solid'
import { graphql } from '~~/lib/common/generated/gql'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { HorizontalDirection } from '~~/lib/common/composables/window'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { modelVersionsRoute } from '~/lib/common/helpers/route'
import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
graphql(`
fragment ProjectPageModelsActions on Model {
id
name
permissions {
canUpdate {
...FullPermissionCheckResult
}
canDelete {
...FullPermissionCheckResult
}
canCreateVersion {
...FullPermissionCheckResult
}
}
}
`)
graphql(`
fragment ProjectPageModelsActions_Project on Project {
id
workspace {
id
slug
}
...ProjectsModelPageEmbed_Project
}
`)
enum ActionTypes {
Rename = 'rename',
Delete = 'delete',
Share = 'share',
ViewVersions = 'view-versions',
UploadVersion = 'upload-version',
CopyId = 'copy-id',
Embed = 'embed',
ViewUploads = 'view-uploads'
}
const emit = defineEmits<{
(e: 'update:open', v: boolean): void
(e: 'model-updated'): void
(e: 'upload-version'): void
(e: 'embed'): void
}>()
const props = defineProps<{
open?: boolean
model: ProjectPageModelsActionsFragment
project: ProjectPageModelsActions_ProjectFragment
menuPosition?: HorizontalDirection
mountMenuOnBody?: boolean
}>()
const copyModelLink = useCopyModelLink()
const { copy } = useClipboard()
const menuId = useId()
const { isLoggedIn } = useActiveUser()
const router = useRouter()
const mp = useMixpanel()
const { statusIsCanceled } = useWorkspacePlan(props.project.workspace?.slug || '')
const showActionsMenu = ref(false)
const openDialog = ref(null as Nullable<ActionTypes>)
const canEdit = computed(() => props.model.permissions.canUpdate)
const canDelete = computed(() => props.model.permissions.canDelete)
const canCreateVersion = computed(() => props.model.permissions.canCreateVersion)
const uploadVersionDisabled = computed(() => {
if (canCreateVersion.value.code === 'WORKSPACES_NOT_AUTHORIZED_ERROR') {
return {
disabled: true,
tooltip: `Your project role doesn't allow creating new model versions`
}
}
if (statusIsCanceled.value) {
return {
disabled: true,
tooltip:
"The workspace's subscription is cancelled, so no new model versions can be created"
}
}
if (!canCreateVersion.value.authorized) {
return {
disabled: true,
tooltip: canCreateVersion.value.message || 'Insufficient permissions'
}
}
return {
disabled: false,
tooltip: ''
}
})
const actionsItems = computed<LayoutMenuItem[][]>(() => [
...(isLoggedIn.value
? [
[
{
title: 'Edit model...',
id: ActionTypes.Rename,
disabled: !canEdit.value.authorized,
disabledTooltip: canEdit.value.message || 'Insufficient permissions'
}
]
]
: []),
[
{
title: 'View versions',
id: ActionTypes.ViewVersions
},
{
title: 'View uploads',
id: ActionTypes.ViewUploads
},
...(isLoggedIn.value
? [
{
title: 'Upload new version...',
id: ActionTypes.UploadVersion,
disabled: uploadVersionDisabled.value.disabled,
disabledTooltip: uploadVersionDisabled.value.tooltip
}
]
: [])
],
[
{ title: 'Copy link', id: ActionTypes.Share },
{ title: 'Copy ID', id: ActionTypes.CopyId },
{ title: 'Embed model...', id: ActionTypes.Embed }
],
...(isLoggedIn.value
? [
[
{
title: 'Delete...',
id: ActionTypes.Delete,
// TODO:
disabled: !canDelete.value.authorized,
disabledTooltip: canDelete.value.message || 'Insufficient permissions'
}
]
]
: [])
])
const isRenameDialogOpen = computed({
get: () => openDialog.value === ActionTypes.Rename,
set: (isOpen) => (openDialog.value = isOpen ? ActionTypes.Rename : null)
})
const isDeleteDialogOpen = computed({
get: () => openDialog.value === ActionTypes.Delete,
set: (isOpen) => (openDialog.value = isOpen ? ActionTypes.Delete : null)
})
const isEmbedDialogOpen = computed({
get: () => openDialog.value === ActionTypes.Embed,
set: (isOpen) => (openDialog.value = isOpen ? ActionTypes.Embed : null)
})
const isUploadsDialogOpen = computed({
get: () => openDialog.value === ActionTypes.ViewUploads,
set: (isOpen) => (openDialog.value = isOpen ? ActionTypes.ViewUploads : null)
})
const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) => {
const { item } = params
switch (item.id) {
case ActionTypes.Rename:
case ActionTypes.Delete:
case ActionTypes.Embed:
case ActionTypes.ViewUploads:
openDialog.value = item.id
break
case ActionTypes.Share:
mp.track('Branch Action', { type: 'action', name: 'share' })
copyModelLink(props.project.id, props.model.id)
break
case ActionTypes.ViewVersions:
router.push(modelVersionsRoute(props.project.id, props.model.id))
break
case ActionTypes.UploadVersion:
emit('upload-version')
break
case ActionTypes.CopyId:
copy(props.model.id, { successMessage: 'Copied model ID to clipboard' })
break
}
}
const onButtonClick = () => {
showActionsMenu.value = !showActionsMenu.value
}
const showUploads = () => {
openDialog.value = ActionTypes.ViewUploads
}
// doing it this way with 2 watchers so that using the 'open' prop is optional
watch(showActionsMenu, (newVal) => emit('update:open', newVal))
watch(
() => props.open || false,
(newVal) => (showActionsMenu.value = newVal)
)
defineExpose({
showUploads
})
</script>