Merge branch 'feature/initial-viewer-ui-updates' of https://github.com/specklesystems/speckle-server into feature/initial-viewer-ui-updates
This commit is contained in:
@@ -63,26 +63,26 @@
|
||||
@click="emit('next', modelValue)"
|
||||
/>
|
||||
</div>
|
||||
<FormButton
|
||||
v-show="isDragged"
|
||||
v-tippy="'Pop in'"
|
||||
:icon-left="ArrowTopRightOnSquareIcon"
|
||||
hide-text
|
||||
class="rotate-180"
|
||||
color="subtle"
|
||||
size="sm"
|
||||
@click="isDragged = false"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-x-0.5">
|
||||
<FormButton
|
||||
v-tippy="'Copy link'"
|
||||
:icon-left="LinkIcon"
|
||||
hide-text
|
||||
color="subtle"
|
||||
size="sm"
|
||||
@click="onCopyLink"
|
||||
/>
|
||||
<div class="cursor-pointer">
|
||||
<LayoutMenu
|
||||
v-model:open="showMenu"
|
||||
:menu-id="menuId"
|
||||
:items="actionsItems"
|
||||
mount-menu-on-body
|
||||
@click.stop.prevent
|
||||
@chosen="onActionChosen"
|
||||
>
|
||||
<FormButton
|
||||
hide-text
|
||||
size="sm"
|
||||
color="subtle"
|
||||
:icon-left="iconThreeDots"
|
||||
@click="showMenu = !showMenu"
|
||||
/>
|
||||
</LayoutMenu>
|
||||
</div>
|
||||
<FormButton
|
||||
v-tippy="modelValue.archived ? 'Unresolve' : 'Resolve'"
|
||||
:icon-left="IconCircleCheck"
|
||||
@@ -172,10 +172,8 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
LinkIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowLeftIcon,
|
||||
ArrowUpRightIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
@@ -202,6 +200,12 @@ import { useThreadUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { useEmbed } from '~/lib/viewer/composables/setup/embed'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { ConcreteComponent } from 'vue'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
|
||||
enum ActionTypes {
|
||||
CopyLink = 'copy-link',
|
||||
PopIn = 'pop-in'
|
||||
}
|
||||
|
||||
graphql(`
|
||||
fragment ViewerCommentThreadData on Comment {
|
||||
@@ -235,7 +239,6 @@ const { copy } = useClipboard()
|
||||
const { isLoggedIn } = useActiveUser()
|
||||
const archiveComment = useArchiveComment()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const { projectId } = useInjectedViewerState()
|
||||
const canReply = useCheckViewerCommentingAccess()
|
||||
const { disableTextSelection } = useDisableGlobalTextSelection()
|
||||
@@ -246,10 +249,13 @@ const { threadResourceStatus, hasClickedFullContext, goBack, handleContextClick
|
||||
useCommentContext()
|
||||
const { isOpenThread, open, closeAllThreads } = useThreadUtilities()
|
||||
const router = useRouter()
|
||||
const menuId = useId()
|
||||
|
||||
const showMenu = ref(false)
|
||||
const commentsContainer = ref(null as Nullable<HTMLElement>)
|
||||
const threadContainer = ref(null as Nullable<HTMLElement>)
|
||||
const threadActivator = ref(null as Nullable<HTMLElement>)
|
||||
const iconThreeDots = resolveComponent('IconThreeDots') as ConcreteComponent
|
||||
|
||||
onClickOutside(threadContainer, (event) => {
|
||||
const viewerElement = document.getElementById('viewer')
|
||||
@@ -382,6 +388,37 @@ const threadAuthors = computed(() => {
|
||||
return authors
|
||||
})
|
||||
|
||||
const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
title: 'Copy link',
|
||||
id: ActionTypes.CopyLink
|
||||
},
|
||||
{
|
||||
title: 'Pop in',
|
||||
id: ActionTypes.PopIn,
|
||||
disabled: !isDragged.value
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const canArchiveOrUnarchive = computed(
|
||||
() => props.modelValue.permissions.canArchive.authorized
|
||||
)
|
||||
|
||||
const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) => {
|
||||
const { item } = params
|
||||
|
||||
switch (item.id) {
|
||||
case ActionTypes.CopyLink:
|
||||
onCopyLink()
|
||||
break
|
||||
case ActionTypes.PopIn:
|
||||
isDragged.value = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const changeExpanded = async (newVal: boolean) => {
|
||||
if (newVal) {
|
||||
await open(props.modelValue.id)
|
||||
@@ -398,10 +435,6 @@ const changeExpanded = async (newVal: boolean) => {
|
||||
})
|
||||
}
|
||||
|
||||
const canArchiveOrUnarchive = computed(
|
||||
() => props.modelValue.permissions.canArchive.authorized
|
||||
)
|
||||
|
||||
const toggleCommentResolvedStatus = async () => {
|
||||
// Remove thread ID from URL when resolving
|
||||
if (!props.modelValue.archived) {
|
||||
|
||||
@@ -5,44 +5,26 @@
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="relative pr-2">
|
||||
<FormButton
|
||||
ref="settingsButtonRef"
|
||||
hide-text
|
||||
size="sm"
|
||||
color="subtle"
|
||||
:icon-left="settingsIcon"
|
||||
:class="showVisibilityOptions ? '!text-primary-focus !bg-info-lighter' : ''"
|
||||
@click="showVisibilityOptions = !showVisibilityOptions"
|
||||
/>
|
||||
|
||||
<ViewerLayoutPanel
|
||||
v-if="showVisibilityOptions"
|
||||
class="absolute right-2 top-full w-56 z-50"
|
||||
<LayoutMenu
|
||||
v-model:open="showVisibilityOptions"
|
||||
:menu-id="menuId"
|
||||
:items="actionsItems"
|
||||
:menu-position="HorizontalDirection.Right"
|
||||
mount-menu-on-body
|
||||
:custom-menu-items-classes="['!w-[270px]']"
|
||||
show-ticks
|
||||
@click.stop.prevent
|
||||
@chosen="onActionChosen"
|
||||
>
|
||||
<div class="p-1">
|
||||
<ViewerMenuItem
|
||||
label="Show in 3D model"
|
||||
:active="!hideBubbles"
|
||||
@click="hideBubbles = !hideBubbles"
|
||||
/>
|
||||
<ViewerMenuItem
|
||||
:label="`Show resolved (${
|
||||
commentThreadsMetadata?.totalArchivedCount || 0
|
||||
})`"
|
||||
:active="!!includeArchived"
|
||||
@click="includeArchived = includeArchived ? undefined : 'includeArchived'"
|
||||
/>
|
||||
<ViewerMenuItem
|
||||
label="Exclude threads from other versions"
|
||||
:active="!!loadedVersionsOnly"
|
||||
@click="
|
||||
loadedVersionsOnly = loadedVersionsOnly
|
||||
? undefined
|
||||
: 'loadedVersionsOnly'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</ViewerLayoutPanel>
|
||||
<FormButton
|
||||
hide-text
|
||||
size="sm"
|
||||
color="subtle"
|
||||
:icon-left="settingsIcon"
|
||||
:class="showVisibilityOptions ? '!text-primary-focus !bg-info-lighter' : ''"
|
||||
@click="showVisibilityOptions = !showVisibilityOptions"
|
||||
/>
|
||||
</LayoutMenu>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-col">
|
||||
@@ -77,6 +59,14 @@ import {
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useCheckViewerCommentingAccess } from '~~/lib/viewer/composables/commentManagement'
|
||||
import { useSelectionUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
import { HorizontalDirection } from '~~/lib/common/composables/window'
|
||||
|
||||
enum ActionTypes {
|
||||
HideBubbles = 'hide-bubbles',
|
||||
IncludeArchived = 'include-archived',
|
||||
LoadedVersionsOnly = 'loaded-versions-only'
|
||||
}
|
||||
|
||||
graphql(`
|
||||
fragment ViewerCommentsListItem on Comment {
|
||||
@@ -117,6 +107,7 @@ const {
|
||||
}
|
||||
} = useInjectedViewerInterfaceState()
|
||||
const canPostComment = useCheckViewerCommentingAccess()
|
||||
const menuId = useId()
|
||||
|
||||
const showVisibilityOptions = ref(false)
|
||||
const settingsIcon = resolveComponent('IconViewerSettings') as ConcreteComponent
|
||||
@@ -159,7 +150,43 @@ watch(includeArchived, (newVal) =>
|
||||
const { objectIds: selectedObjectIds } = useSelectionUtilities()
|
||||
|
||||
const hasSelectedObjects = computed(() => selectedObjectIds.value.size > 0)
|
||||
const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
title: 'Show in 3D model',
|
||||
id: ActionTypes.HideBubbles,
|
||||
active: !hideBubbles.value
|
||||
},
|
||||
{
|
||||
title: `Show resolved (${commentThreadsMetadata.value?.totalArchivedCount || 0})`,
|
||||
id: ActionTypes.IncludeArchived,
|
||||
active: !!includeArchived.value
|
||||
},
|
||||
{
|
||||
title: 'Exclude threads from other versions',
|
||||
id: ActionTypes.LoadedVersionsOnly,
|
||||
active: !!loadedVersionsOnly.value
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) => {
|
||||
const { item } = params
|
||||
|
||||
switch (item.id) {
|
||||
case ActionTypes.HideBubbles:
|
||||
hideBubbles.value = !hideBubbles.value
|
||||
break
|
||||
case ActionTypes.IncludeArchived:
|
||||
includeArchived.value = includeArchived.value ? undefined : 'includeArchived'
|
||||
break
|
||||
case ActionTypes.LoadedVersionsOnly:
|
||||
loadedVersionsOnly.value = loadedVersionsOnly.value
|
||||
? undefined
|
||||
: 'loadedVersionsOnly'
|
||||
break
|
||||
}
|
||||
}
|
||||
const onNewDiscussion = () => {
|
||||
if (!hasSelectedObjects.value) return
|
||||
newThreadEditor.value = true
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-x-2">
|
||||
<p>Selected objects</p>
|
||||
<p>Selected</p>
|
||||
<CommonBadge v-if="objects.length" rounded>
|
||||
{{ objects.length }}
|
||||
</CommonBadge>
|
||||
@@ -24,25 +24,33 @@
|
||||
<FormButton
|
||||
size="sm"
|
||||
color="subtle"
|
||||
:icon-left="isHidden ? EyeSlashIcon : EyeIcon"
|
||||
:icon-left="isHidden ? iconEyeClosed : iconEye"
|
||||
hide-text
|
||||
@click.stop="hideOrShowSelection"
|
||||
/>
|
||||
<FormButton
|
||||
size="sm"
|
||||
color="subtle"
|
||||
:icon-left="isIsolated ? FunnelIcon : FunnelIconOutline"
|
||||
:icon-left="isIsolated ? iconViewerUnisolate : iconViewerIsolate"
|
||||
hide-text
|
||||
@click.stop="isolateOrUnisolateSelection"
|
||||
/>
|
||||
<FormButton
|
||||
size="sm"
|
||||
color="subtle"
|
||||
:icon-left="ArrowTopRightOnSquareIcon"
|
||||
:to="selectionLink"
|
||||
target="_blank"
|
||||
hide-text
|
||||
/>
|
||||
<LayoutMenu
|
||||
v-model:open="showSubMenu"
|
||||
:menu-id="menuId"
|
||||
:items="actionsItems"
|
||||
:custom-menu-items-classes="['!w-48']"
|
||||
@click.stop.prevent
|
||||
@chosen="onActionChosen"
|
||||
>
|
||||
<FormButton
|
||||
hide-text
|
||||
size="sm"
|
||||
color="subtle"
|
||||
:icon-left="settingsIcon"
|
||||
@click="showSubMenu = !showSubMenu"
|
||||
/>
|
||||
</LayoutMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,13 +78,6 @@
|
||||
</ViewerCommentsPortalOrDiv>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
EyeSlashIcon,
|
||||
EyeIcon,
|
||||
FunnelIcon,
|
||||
ArrowTopRightOnSquareIcon
|
||||
} from '@heroicons/vue/24/solid'
|
||||
import { FunnelIcon as FunnelIconOutline } from '@heroicons/vue/24/outline'
|
||||
import { onKeyStroke, useBreakpoints } from '@vueuse/core'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { getTargetObjectIds } from '~~/lib/object-sidebar/helpers'
|
||||
@@ -87,6 +88,12 @@ import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { useIsSmallerOrEqualThanBreakpoint } from '~~/composables/browser'
|
||||
import { modelRoute } from '~/lib/common/helpers/route'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import type { ConcreteComponent } from 'vue'
|
||||
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
|
||||
|
||||
enum ActionTypes {
|
||||
OpenInNewTab = 'open-in-new-tab'
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
@@ -103,10 +110,18 @@ const { hideObjects, showObjects, isolateObjects, unIsolateObjects } =
|
||||
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
|
||||
const breakpoints = useBreakpoints(TailwindBreakpoints)
|
||||
const isGreaterThanSm = breakpoints.greater('sm')
|
||||
const menuId = useId()
|
||||
const mp = useMixpanel()
|
||||
|
||||
const itemCount = ref(20)
|
||||
const sidebarOpen = ref(false)
|
||||
const sidebarWidth = ref(280)
|
||||
const showSubMenu = ref(false)
|
||||
const iconViewerUnisolate = resolveComponent('IconViewerUnisolate') as ConcreteComponent
|
||||
const iconViewerIsolate = resolveComponent('IconViewerIsolate') as ConcreteComponent
|
||||
const iconEyeClosed = resolveComponent('IconEyeClosed') as ConcreteComponent
|
||||
const iconEye = resolveComponent('IconEye') as ConcreteComponent
|
||||
const settingsIcon = resolveComponent('IconThreeDots') as ConcreteComponent
|
||||
|
||||
const objectsUniqueByAppId = computed(() => {
|
||||
if (!diff.enabled.value) return objects.value
|
||||
@@ -145,12 +160,29 @@ const isIsolated = computed(() => {
|
||||
return containsAll(allTargetIds.value, isolatedObjects.value)
|
||||
})
|
||||
|
||||
const mp = useMixpanel()
|
||||
const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
title: 'Open selection in new tab',
|
||||
id: ActionTypes.OpenInNewTab
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
const selectionLink = computed(() => {
|
||||
return modelRoute(projectId.value, allTargetIds.value.join(','))
|
||||
})
|
||||
|
||||
const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) => {
|
||||
const { item } = params
|
||||
|
||||
switch (item.id) {
|
||||
case ActionTypes.OpenInNewTab:
|
||||
window.open(selectionLink.value, '_blank')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const hideOrShowSelection = () => {
|
||||
if (!isHidden.value) {
|
||||
hideObjects(allTargetIds.value)
|
||||
|
||||
@@ -29,6 +29,9 @@
|
||||
@click="chooseItem(item, $event)"
|
||||
>
|
||||
<Component :is="item.icon" v-if="item.icon" class="h-4 w-4" />
|
||||
<div v-if="showTicks" class="w-5 shrink-0">
|
||||
<IconCheck v-if="item.active" class="h-4 w-4 text-foreground-2" />
|
||||
</div>
|
||||
<slot name="item" :item="item">{{ item.title }}</slot>
|
||||
</button>
|
||||
</span>
|
||||
@@ -71,6 +74,8 @@ const props = defineProps<{
|
||||
*/
|
||||
menuPosition?: HorizontalDirection
|
||||
mountMenuOnBody?: boolean
|
||||
customMenuItemsClasses?: string[]
|
||||
showTicks?: boolean
|
||||
}>()
|
||||
|
||||
const menuItems = ref(null as Nullable<{ el: HTMLDivElement }>)
|
||||
@@ -131,6 +136,10 @@ const menuItemsClasses = computed(() => {
|
||||
'mt-1 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) {
|
||||
classParts.push(...props.customMenuItemsClasses)
|
||||
}
|
||||
|
||||
if (props.mountMenuOnBody) {
|
||||
classParts.push('fixed')
|
||||
} else {
|
||||
@@ -157,7 +166,7 @@ const buildButtonClassses = (params: {
|
||||
}) => {
|
||||
const { active, disabled, color } = params
|
||||
const classParts = [
|
||||
'group flex space-x-2 w-full items-center rounded-md px-2 py-1 text-body-xs'
|
||||
'group flex space-x-2 w-full items-center rounded-md px-2 py-1 text-body-xs text-left'
|
||||
]
|
||||
|
||||
if (active && !color) {
|
||||
|
||||
@@ -31,6 +31,7 @@ export type LayoutMenuItem<I extends string = string> = {
|
||||
disabled?: boolean
|
||||
disabledTooltip?: string
|
||||
color?: 'danger' | 'info'
|
||||
active?: boolean
|
||||
}
|
||||
|
||||
export type LayoutDialogButton = {
|
||||
|
||||
Reference in New Issue
Block a user