185806ec64
* init + new API query * embed dialog works * embed actions menu * embed label * WIP positioning fix: * cleanup * embed options finalized * prevent reset in embed mode * more embed UX improverments * tests fix
394 lines
11 KiB
Vue
394 lines
11 KiB
Vue
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
|
|
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
|
|
<template>
|
|
<div
|
|
v-keyboard-clickable
|
|
:class="[wrapperClasses, draggableClasses, draggableTargetClasses]"
|
|
:view-id="view.id"
|
|
draggable="true"
|
|
v-on="{ ...on, ...targetOn }"
|
|
@click="apply"
|
|
>
|
|
<div class="flex items-center shrink-0">
|
|
<div class="relative">
|
|
<img
|
|
:src="view.thumbnailUrl"
|
|
alt="View screenshot"
|
|
class="w-20 h-[60px] object-cover rounded border border-outline-3 bg-foundation-page cursor-pointer"
|
|
/>
|
|
<div
|
|
v-if="isHomeView && !isFederatedView"
|
|
class="absolute -top-1 -left-1 bg-orange-500 w-4 h-4 flex items-center justify-center rounded-[3px]"
|
|
>
|
|
<IconHome class="w-3 h-3" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col min-w-0 grow gap-y-0.5">
|
|
<div class="text-body-2xs font-medium text-foreground truncate grow-0">
|
|
{{ view.name }}
|
|
</div>
|
|
<div class="text-body-2xs text-foreground-3 truncate">
|
|
{{ view.author?.name }}
|
|
</div>
|
|
<div class="w-full flex items-center gap-1">
|
|
<User
|
|
v-if="isOnlyVisibleToMe"
|
|
v-tippy="getTooltipProps('Only visible to you')"
|
|
:size="12"
|
|
:stroke-width="1.5"
|
|
:absolute-stroke-width="true"
|
|
class="w-3 h-3 text-foreground-3 shrink-0"
|
|
/>
|
|
<div
|
|
v-tippy="{
|
|
content: formattedFullDate(view.updatedAt),
|
|
delay: [700, 100],
|
|
duration: [120, 150],
|
|
offset: [0, 2],
|
|
placement: 'right'
|
|
}"
|
|
class="text-body-2xs text-foreground-3 truncate pr-1.5"
|
|
>
|
|
{{ formattedRelativeDate(view.updatedAt, { capitalize: true }) }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="flex gap-0.5 items-center opacity-0 w-0 group-hover:opacity-100 group-hover:w-auto"
|
|
@click.stop
|
|
>
|
|
<LayoutMenu
|
|
v-model:open="showMenu"
|
|
:items="menuItems"
|
|
:menu-id="menuId"
|
|
mount-menu-on-body
|
|
show-ticks="right"
|
|
:size="230"
|
|
class="shrink-0"
|
|
@chosen="({ item: actionItem }) => onActionChosen(actionItem)"
|
|
>
|
|
<FormButton
|
|
size="sm"
|
|
color="subtle"
|
|
:icon-left="Ellipsis"
|
|
hide-text
|
|
name="viewActions"
|
|
class="shrink-0"
|
|
@click="showMenu = !showMenu"
|
|
/>
|
|
</LayoutMenu>
|
|
<div
|
|
v-tippy="
|
|
getTooltipProps(
|
|
canOpenEditDialog?.authorized
|
|
? 'Edit view'
|
|
: canOpenEditDialog?.errorMessage
|
|
)
|
|
"
|
|
class="shrink-0 opacity-0 group-hover:opacity-100"
|
|
>
|
|
<FormButton
|
|
size="sm"
|
|
color="subtle"
|
|
:icon-left="SquarePen"
|
|
hide-text
|
|
name="editView"
|
|
class="shrink-0"
|
|
:disabled="!canOpenEditDialog?.authorized"
|
|
@click="onEdit"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import {
|
|
StringEnum,
|
|
throwUncoveredError,
|
|
type Optional,
|
|
type StringEnumValues
|
|
} from '@speckle/shared'
|
|
import type { LayoutMenuItem } from '@speckle/ui-components'
|
|
import { useMutationLoading } from '@vue/apollo-composable'
|
|
import { difference } from 'lodash-es'
|
|
import { Ellipsis, SquarePen, User } from 'lucide-vue-next'
|
|
import { graphql } from '~/lib/common/generated/gql'
|
|
import {
|
|
SavedViewVisibility,
|
|
type ViewerSavedViewsPanelView_SavedViewFragment
|
|
} from '~/lib/common/generated/gql/graphql'
|
|
import { useMixpanel } from '~/lib/core/composables/mp'
|
|
import { useViewerSavedViewsUtils } from '~/lib/viewer/composables/savedViews/general'
|
|
import {
|
|
useCollectNewSavedViewViewerData,
|
|
useUpdateSavedView
|
|
} from '~/lib/viewer/composables/savedViews/management'
|
|
import {
|
|
useDraggableView,
|
|
useDraggableViewTargetView
|
|
} from '~/lib/viewer/composables/savedViews/ui'
|
|
import { useSavedViewValidationHelpers } from '~/lib/viewer/composables/savedViews/validation'
|
|
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
|
|
|
|
const MenuItems = StringEnum([
|
|
'Delete',
|
|
'LoadOriginalVersions',
|
|
'CopyLink',
|
|
'ChangeVisibility',
|
|
'ReplaceView',
|
|
'MoveToGroup',
|
|
'SetAsHomeView',
|
|
'Embed'
|
|
])
|
|
type MenuItems = StringEnumValues<typeof MenuItems>
|
|
|
|
const { getTooltipProps } = useSmartTooltipDelay()
|
|
|
|
graphql(`
|
|
fragment ViewerSavedViewsPanelView_SavedView on SavedView {
|
|
id
|
|
name
|
|
description
|
|
thumbnailUrl
|
|
visibility
|
|
isHomeView
|
|
resourceIds
|
|
author {
|
|
id
|
|
name
|
|
}
|
|
updatedAt
|
|
permissions {
|
|
canUpdate {
|
|
...FullPermissionCheckResult
|
|
}
|
|
}
|
|
...UseDeleteSavedView_SavedView
|
|
...UseUpdateSavedView_SavedView
|
|
...ViewerSavedViewsPanelViewEditDialog_SavedView
|
|
...UseSavedViewValidationHelpers_SavedView
|
|
...UseDraggableView_SavedView
|
|
}
|
|
`)
|
|
|
|
const props = defineProps<{
|
|
view: ViewerSavedViewsPanelView_SavedViewFragment
|
|
}>()
|
|
|
|
const {
|
|
resources: {
|
|
response: { savedView, isFederatedView, resourceItemsIds, project }
|
|
}
|
|
} = useInjectedViewerState()
|
|
const { collect } = useCollectNewSavedViewViewerData()
|
|
const updateView = useUpdateSavedView()
|
|
const isLoading = useMutationLoading()
|
|
const { copyLink, applyView } = useViewerSavedViewsUtils()
|
|
const eventBus = useEventBus()
|
|
const { formattedRelativeDate, formattedFullDate } = useDateFormatters()
|
|
const {
|
|
canUpdate,
|
|
isOnlyVisibleToMe,
|
|
canSetHomeView,
|
|
isHomeView,
|
|
canToggleVisibility,
|
|
canMove,
|
|
canOpenEditDialog,
|
|
canEmbed
|
|
} = useSavedViewValidationHelpers({
|
|
view: computed(() => props.view)
|
|
})
|
|
const { classes: draggableClasses, on } = useDraggableView({
|
|
view: computed(() => props.view)
|
|
})
|
|
const { classes: draggableTargetClasses, on: targetOn } = useDraggableViewTargetView({
|
|
view: computed(() => props.view)
|
|
})
|
|
|
|
const mp = useMixpanel()
|
|
|
|
const showMenu = ref(false)
|
|
const menuId = useId()
|
|
|
|
const isActive = computed(() => props.view.id === savedView.value?.id)
|
|
|
|
const isOriginalVersionAlreadyLoaded = computed(() => {
|
|
const viewResources = props.view.resourceIds
|
|
const currentlyLoadedResources = resourceItemsIds.value
|
|
return difference(viewResources, currentlyLoadedResources).length === 0
|
|
})
|
|
|
|
const canLoadOriginal = computed(
|
|
(): { authorized: boolean; message: Optional<string> } => {
|
|
if (isOriginalVersionAlreadyLoaded.value) {
|
|
return { authorized: false, message: 'Original version is already loaded' }
|
|
}
|
|
|
|
return { authorized: true, message: undefined }
|
|
}
|
|
)
|
|
|
|
const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
|
|
[
|
|
{
|
|
id: MenuItems.MoveToGroup,
|
|
title: 'Move to group',
|
|
disabled: !canMove.value?.authorized || isLoading.value,
|
|
disabledTooltip: canMove.value?.errorMessage
|
|
},
|
|
{
|
|
id: MenuItems.ReplaceView,
|
|
title: 'Replace view',
|
|
disabled: !canUpdate.value?.authorized || isLoading.value,
|
|
disabledTooltip: canUpdate.value?.errorMessage
|
|
},
|
|
{
|
|
id: MenuItems.CopyLink,
|
|
title: 'Copy link'
|
|
},
|
|
{
|
|
id: MenuItems.LoadOriginalVersions,
|
|
title: 'Load with original model version',
|
|
disabled: !canLoadOriginal.value.authorized || isLoading.value,
|
|
disabledTooltip: canLoadOriginal.value.message
|
|
}
|
|
],
|
|
[
|
|
{
|
|
id: MenuItems.SetAsHomeView,
|
|
title: 'Set as home view',
|
|
active: !!isHomeView.value,
|
|
disabled: !canSetHomeView.value.authorized,
|
|
disabledTooltip: canSetHomeView.value.message
|
|
},
|
|
{
|
|
id: MenuItems.ChangeVisibility,
|
|
title: isOnlyVisibleToMe.value ? 'Make view shared' : 'Make view private',
|
|
disabled: !canToggleVisibility.value.authorized,
|
|
disabledTooltip: canToggleVisibility.value.message
|
|
},
|
|
{
|
|
id: MenuItems.Embed,
|
|
title: 'Embed view',
|
|
disabled: !canEmbed.value?.authorized,
|
|
disabledTooltip: canEmbed.value?.errorMessage
|
|
}
|
|
],
|
|
[
|
|
{
|
|
id: MenuItems.Delete,
|
|
title: 'Delete view...',
|
|
disabled: !canUpdate.value?.authorized || isLoading.value,
|
|
disabledTooltip: canUpdate.value?.errorMessage
|
|
}
|
|
]
|
|
])
|
|
|
|
const wrapperClasses = computed(() => {
|
|
const classParts = [
|
|
'flex items-center gap-2 p-2 w-full group rounded-md cursor-pointer relative transition-all'
|
|
]
|
|
|
|
if (isActive.value) {
|
|
classParts.push('bg-highlight-2 hover:bg-highlight-3')
|
|
} else {
|
|
classParts.push('hover:bg-highlight-1')
|
|
}
|
|
|
|
return classParts.join(' ')
|
|
})
|
|
|
|
const onActionChosen = async (item: LayoutMenuItem<MenuItems>) => {
|
|
switch (item.id) {
|
|
case MenuItems.Delete:
|
|
eventBus.emit(ViewerEventBusKeys.MarkSavedViewForEdit, {
|
|
type: 'delete',
|
|
view: props.view
|
|
})
|
|
break
|
|
case MenuItems.CopyLink:
|
|
await copyLink({
|
|
settings: {
|
|
id: props.view.id
|
|
}
|
|
})
|
|
mp.track('Saved View Link Copied', {
|
|
viewId: props.view.id,
|
|
// eslint-disable-next-line camelcase
|
|
workspace_id: project.value?.workspaceId
|
|
})
|
|
break
|
|
case MenuItems.LoadOriginalVersions:
|
|
applyView({
|
|
id: props.view.id,
|
|
loadOriginal: true
|
|
})
|
|
mp.track('Saved View Original Version Loaded', {
|
|
viewId: props.view.id,
|
|
// eslint-disable-next-line camelcase
|
|
workspace_id: project.value?.workspaceId
|
|
})
|
|
break
|
|
case MenuItems.ChangeVisibility:
|
|
await updateView({
|
|
view: props.view,
|
|
input: {
|
|
id: props.view.id,
|
|
projectId: props.view.projectId,
|
|
visibility: isOnlyVisibleToMe.value
|
|
? SavedViewVisibility.Public
|
|
: SavedViewVisibility.AuthorOnly
|
|
}
|
|
})
|
|
break
|
|
case MenuItems.ReplaceView:
|
|
// Replace view w/ active one
|
|
await updateView({
|
|
view: props.view,
|
|
input: {
|
|
id: props.view.id,
|
|
...(await collect())
|
|
}
|
|
})
|
|
break
|
|
case MenuItems.MoveToGroup:
|
|
eventBus.emit(ViewerEventBusKeys.MarkSavedViewForEdit, {
|
|
type: 'move',
|
|
view: props.view
|
|
})
|
|
break
|
|
case MenuItems.SetAsHomeView:
|
|
await updateView({
|
|
view: props.view,
|
|
input: {
|
|
id: props.view.id,
|
|
projectId: props.view.projectId,
|
|
isHomeView: !isHomeView.value
|
|
}
|
|
})
|
|
break
|
|
case MenuItems.Embed:
|
|
eventBus.emit(ViewerEventBusKeys.MarkSavedViewForEmbed, {
|
|
view: props.view
|
|
})
|
|
break
|
|
default:
|
|
throwUncoveredError(item.id)
|
|
}
|
|
}
|
|
|
|
const apply = async () => {
|
|
applyView({
|
|
id: props.view.id
|
|
})
|
|
}
|
|
|
|
const onEdit = () => {
|
|
eventBus.emit(ViewerEventBusKeys.MarkSavedViewForEdit, {
|
|
type: 'edit',
|
|
view: props.view
|
|
})
|
|
}
|
|
</script>
|