Dim/fe2/view changes (#1608)
* Fixed an issue with curves doubling up on geometry and also not being selectable after the last filtering changes. Added the options to make lines transparent. Added lines to diffing * Points now are diff-able and support proper visual diff-ing. Visual diff filters are now chosen internally by the Differ. Fixed an issue with LineBatch and transparency * Implemented PLAIN visual diff mode, where all objects keep their original materil, but opacity is manipulated via the diff time. Added API member function to switch between the PLAIN and COLORED visual diff modes * feat(fe2): diffs wip * Diffing fixes for instances and blocks. Things seem to be working fine, but there are some caveats. Additionally, some older issues were fixed and diffing now works better on all the rest of the streams * feat(fe2): de-dupes diff results * feat(fe2): wip diffs * feat(fe2): diff transparency goes from 0 to 1 * feat(fe2): diff results display work * feat(fe2): diff results display work * feat(fe2): diff panel work * feat(fe2): diff work: various display changes, coloring toggle, selection logic, selection object display wip * feat(fe2): diff work: cleaned up old/new version, fixed minor bug in viewer diff time when swapping color mode * feat(fe2): diff work: implements custom selection logic and selection display for modified objects (they come in pairs now) * feat(fe2): diff minor fix in selected object display * feat(fe2): wip; trying to fix diff order to be consistent (ordered by date) * feat(fe2): wip, broken state right now * feat(fe2): fixes scrollbars in viewer * feat(fe2): fixes slider sync with diff time * feat(fe2): WIP syncs of diffs (threads, refreshes, etc.) * feat(fe2): diffing polish * speckle shared fix * speckle shared fix * more bugfixes * linter fixess * more CI fixes * fix viewerState serialization * more linting fixess * template fixes * moving tailwind classes to theme package * migrated away from diffString + simplified postSetup * moved diff new/old version resolution to use state.resources * cleanup * updating url threadId & diff command correctly * minor improvements to diff state --------- Co-authored-by: AlexandruPopovici <alexandrupopoviciioan@gmail.com> Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
This commit is contained in:
committed by
GitHub
parent
2b08bd5452
commit
7b037352df
@@ -5,7 +5,8 @@
|
||||
@tailwind utilities;
|
||||
|
||||
/**
|
||||
* Don't pollute this - it's going to be bundled in all pages!
|
||||
* Don't pollute this - it's going to be bundled in all pages! If it's a global style change to what can be
|
||||
considered the "speckle tailwind theme" then make this change in @speckle/tailwind-theme instead
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,11 +96,15 @@
|
||||
>
|
||||
<div v-show="activeControl === 'models'">
|
||||
<KeepAlive>
|
||||
<ViewerResourcesList
|
||||
class="pointer-events-auto"
|
||||
@loaded-more="scrollControlsToBottom"
|
||||
@close="activeControl = 'none'"
|
||||
/>
|
||||
<div>
|
||||
<ViewerResourcesList
|
||||
v-if="!enabled"
|
||||
class="pointer-events-auto"
|
||||
@loaded-more="scrollControlsToBottom"
|
||||
@close="activeControl = 'none'"
|
||||
/>
|
||||
<ViewerCompareChangesPanel v-else @close="activeControl = 'none'" />
|
||||
</div>
|
||||
</KeepAlive>
|
||||
</div>
|
||||
<div v-show="activeControl === 'explorer'">
|
||||
@@ -134,6 +138,7 @@ import {
|
||||
ModifierKeys,
|
||||
getKeyboardShortcutTitle
|
||||
} from '@speckle/ui-components'
|
||||
import { useInjectedViewerInterfaceState } from '~~/lib/viewer/composables/setup'
|
||||
|
||||
const {
|
||||
zoomExtentsOrSelection,
|
||||
@@ -146,6 +151,9 @@ type ActiveControl = 'none' | 'models' | 'explorer' | 'filters' | 'discussions'
|
||||
|
||||
const activeControl = ref<ActiveControl>('models')
|
||||
const scrollableControlsContainer = ref(null as Nullable<HTMLDivElement>)
|
||||
const {
|
||||
diff: { enabled }
|
||||
} = useInjectedViewerInterfaceState()
|
||||
|
||||
const modelsShortcut = ref(
|
||||
`Models (${getKeyboardShortcutTitle([ModifierKeys.AltOrOpt, 'm'])})`
|
||||
|
||||
@@ -37,12 +37,12 @@
|
||||
>
|
||||
<div
|
||||
ref="handle"
|
||||
class="p-1.5 cursor-move rounded-lg group hover:bg-blue-500/50"
|
||||
class="p-1.5 cursor-move rounded-lg group hover:bg-blue-500/50 transition"
|
||||
:class="{ 'is-dragging bg-blue-500/50': isDragging }"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'bg-white/80 dark:bg-neutral-800/90 backdrop-blur-sm shadow-md cursor-auto rounded-lg',
|
||||
'bg-white dark:bg-neutral-800 backdrop-blur-sm shadow-md cursor-auto rounded-lg',
|
||||
'group-hover:bg-foundation dark:group-hover:bg-neutral-800 group-[.is-dragging]:bg-foundation dark:group-[.is-dragging]:bg-neutral-800'
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
:class="`flex group justify-between pl-1 py-1 items-center text-foreground cursor-pointer w-full max-w-full overflow-hidden select-none rounded border-l-4 hover:bg-primary-muted hover:shadow-md transition-all ${
|
||||
isSelected ? 'border-primary bg-primary-muted' : 'border-transparent'
|
||||
}`"
|
||||
@click="setSelection()"
|
||||
@keypress="keyboardClick(setSelection)"
|
||||
>
|
||||
<div class="flex space-x-2 items-center truncate">
|
||||
<span :class="`w-3 h-3 rounded ${colorClasses}`"></span>
|
||||
<span class="truncate">
|
||||
{{ name }}
|
||||
</span>
|
||||
<span class="text-xs text-foreground-2">({{ objectIds.length }})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { keyboardClick } from '~~/lib/common/helpers/accessibility'
|
||||
import { useSelectionUtilities } from '~~/lib/viewer/composables/ui'
|
||||
|
||||
const {
|
||||
clearSelection,
|
||||
setSelectionFromObjectIds,
|
||||
objects: selectedObjects
|
||||
} = useSelectionUtilities()
|
||||
|
||||
const props = defineProps<{
|
||||
name: 'unchanged' | 'added' | 'removed' | 'modified'
|
||||
objectIds: string[]
|
||||
}>()
|
||||
|
||||
const colorClasses = computed(() => {
|
||||
switch (props.name) {
|
||||
case 'added':
|
||||
return 'bg-green-500'
|
||||
case 'removed':
|
||||
return 'bg-rose-500'
|
||||
case 'modified':
|
||||
return 'bg-yellow-500'
|
||||
case 'unchanged':
|
||||
default:
|
||||
return 'bg-neutral-500'
|
||||
}
|
||||
})
|
||||
|
||||
const isSelected = computed(() => {
|
||||
const selObjsIds = selectedObjects.value.map((o) => o.id as string)
|
||||
return selObjsIds.some((id: string) => props.objectIds.includes(id))
|
||||
})
|
||||
|
||||
const setSelection = () => {
|
||||
if (isSelected.value) return clearSelection()
|
||||
setSelectionFromObjectIds(props.objectIds)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div
|
||||
:class="`bg-foundation-2 shadow h-24 flex items-center justify-center flex-col rounded-md min-w-0 cursor-pointer
|
||||
border-b-4 hover:bg-primary-muted
|
||||
${isSelected ? 'border-primary bg-primary-muted' : 'border-transparent'}`"
|
||||
@click="setSelection()"
|
||||
@keypress="keyboardClick(setSelection)"
|
||||
>
|
||||
<div :class="`h2 font-bold truncate max-w-full ${color}`">
|
||||
{{ objectCount }}
|
||||
</div>
|
||||
<div>{{ name }}</div>
|
||||
<div class="text-xs text-foreground-2 px-1">{{ description }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useSelectionUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { keyboardClick } from '~~/lib/common/helpers/accessibility'
|
||||
|
||||
const {
|
||||
clearSelection,
|
||||
setSelectionFromObjectIds,
|
||||
objects: selectedObjects
|
||||
} = useSelectionUtilities()
|
||||
|
||||
const props = defineProps<{
|
||||
name: 'unchanged' | 'added' | 'removed' | 'modified'
|
||||
objectIds: string[]
|
||||
}>()
|
||||
|
||||
const color = computed(() => {
|
||||
switch (props.name) {
|
||||
case 'added':
|
||||
return 'text-green-500'
|
||||
case 'removed':
|
||||
return 'text-rose-500'
|
||||
case 'modified':
|
||||
return 'text-yellow-500'
|
||||
case 'unchanged':
|
||||
default:
|
||||
return 'text-neutral-500'
|
||||
}
|
||||
})
|
||||
|
||||
const isSelected = computed(() => {
|
||||
const selObjsIds = selectedObjects.value.map((o) => o.id as string)
|
||||
return selObjsIds.some((id: string) => props.objectIds.includes(id))
|
||||
})
|
||||
|
||||
const objectCount = computed(() => {
|
||||
if (props.name === 'modified') return props.objectIds.length / 2
|
||||
return props.objectIds.length
|
||||
})
|
||||
|
||||
const description = computed(() => {
|
||||
switch (props.name) {
|
||||
case 'added':
|
||||
return 'in new version'
|
||||
case 'removed':
|
||||
return 'from old version'
|
||||
case 'modified':
|
||||
return 'across both versions'
|
||||
default:
|
||||
return 'across both versions'
|
||||
}
|
||||
})
|
||||
|
||||
const setSelection = () => {
|
||||
if (isSelected.value) return clearSelection()
|
||||
setSelectionFromObjectIds(props.objectIds)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<ViewerLayoutPanel @close="$emit('close')">
|
||||
<template #actions>
|
||||
<FormButton size="xs" text :icon-left="ChevronLeftIcon" @click="clearDiff">
|
||||
Back
|
||||
</FormButton>
|
||||
</template>
|
||||
<div class="flex flex-col space-y-2 text-sm p-2">
|
||||
<div class="text-xs bg-blue-500/20 text-primary p-1 rounded">
|
||||
This is an experimental feature.
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<div class="grow w-1/2">
|
||||
<ViewerCompareChangesVersion
|
||||
v-if="diffState.newVersion.value"
|
||||
:version="diffState.newVersion.value"
|
||||
:is-newest="true"
|
||||
@click="localDiffTime = 0"
|
||||
/>
|
||||
</div>
|
||||
<div class="grow w-1/2">
|
||||
<ViewerCompareChangesVersion
|
||||
v-if="diffState.oldVersion.value"
|
||||
:version="diffState.oldVersion.value"
|
||||
:is-newest="false"
|
||||
@click="localDiffTime = 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow flex items-center space-x-2 py-2">
|
||||
<label for="diffTime" class="sr-only">Diff Time</label>
|
||||
<input
|
||||
id="diffTime"
|
||||
v-model="localDiffTime"
|
||||
class="h-2 w-full"
|
||||
type="range"
|
||||
name="diffTime"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-full px-1">
|
||||
<span class="text-xs text-left">Color objects by status</span>
|
||||
<FormButton
|
||||
size="xs"
|
||||
:outlined="diffState.mode.value !== VisualDiffMode.COLORED"
|
||||
@click="swapDiffMode()"
|
||||
>
|
||||
{{ diffState.mode.value === VisualDiffMode.COLORED ? 'ON' : 'OFF' }}
|
||||
</FormButton>
|
||||
</div>
|
||||
<!-- <div class="ml-1">Change summary:</div> -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<ViewerCompareChangesObjectGroup name="unchanged" :object-ids="unchangedIds" />
|
||||
<ViewerCompareChangesObjectGroup name="modified" :object-ids="modifiedIds" />
|
||||
<ViewerCompareChangesObjectGroup name="added" :object-ids="addedIds" />
|
||||
<ViewerCompareChangesObjectGroup name="removed" :object-ids="removedIds" />
|
||||
</div>
|
||||
</div>
|
||||
</ViewerLayoutPanel>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { ChevronLeftIcon } from '@heroicons/vue/24/solid'
|
||||
import { VisualDiffMode } from '@speckle/viewer'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { uniqBy } from 'lodash-es'
|
||||
import { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
|
||||
defineEmits<{
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const {
|
||||
ui: { diff: diffState },
|
||||
urlHashState: { diff }
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const localDiffTime = ref(diffState.time.value)
|
||||
|
||||
watch(
|
||||
localDiffTime,
|
||||
(newVal) => {
|
||||
diffState.time.value = newVal
|
||||
},
|
||||
{ immediate: false }
|
||||
)
|
||||
|
||||
watch(diffState.result, () => {
|
||||
localDiffTime.value = 0.5
|
||||
})
|
||||
|
||||
function swapDiffMode() {
|
||||
if (diffState.mode.value === VisualDiffMode.COLORED)
|
||||
return (diffState.mode.value = VisualDiffMode.PLAIN)
|
||||
|
||||
diffState.mode.value = VisualDiffMode.COLORED
|
||||
}
|
||||
|
||||
// NOTE: deduping will not be needed anymore
|
||||
const added = computed(() => {
|
||||
const mapped = diffState.result.value?.added.map(
|
||||
(node) => node.model.raw as SpeckleObject
|
||||
)
|
||||
return uniqBy(mapped, (node) => node.id)
|
||||
})
|
||||
const addedIds = computed(() => added.value.map((o) => o.id as string))
|
||||
|
||||
const removed = computed(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
const mapped = diffState.result.value?.removed.map(
|
||||
(node) => node.model.raw as SpeckleObject
|
||||
)
|
||||
return uniqBy(mapped, (node) => node.id)
|
||||
})
|
||||
const removedIds = computed(() => removed.value.map((o) => o.id as string))
|
||||
|
||||
const unchanged = computed(() => {
|
||||
const mapped = diffState.result.value?.unchanged.map(
|
||||
(node) => node.model.raw as SpeckleObject
|
||||
)
|
||||
return uniqBy(mapped, (node) => node.id)
|
||||
})
|
||||
const unchangedIds = computed(() => unchanged.value.map((o) => o.id as string))
|
||||
const modified = computed(() => {
|
||||
const mapped = diffState.result.value?.modified.map((tuple) => {
|
||||
return [tuple[0].model.raw as SpeckleObject, tuple[1].model.raw as SpeckleObject]
|
||||
})
|
||||
return uniqBy(mapped, (tuple) => tuple[0].id)
|
||||
})
|
||||
const modifiedIds = computed(() => {
|
||||
return [
|
||||
...modified.value.map((t) => t[0].id as string),
|
||||
...modified.value.map((t) => t[1].id as string)
|
||||
]
|
||||
})
|
||||
|
||||
const clearDiff = async () => {
|
||||
await diff.update(null)
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="shadow rounded-md p-1 flex flex-col justify-center cursor-pointer">
|
||||
<div class="h-20 w-full">
|
||||
<PreviewImage :preview-url="version.previewUrl" />
|
||||
</div>
|
||||
<div
|
||||
v-tippy="createdAt"
|
||||
class="bg-foundation-focus inline-block rounded-md px-2 text-xs font-bold truncate text-center py-1"
|
||||
>
|
||||
{{ timeAgoCreatedAt }}
|
||||
<br />
|
||||
{{ isNewest ? 'New' : 'Old' }} Version
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import { ViewerModelVersionCardItemFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
|
||||
const props = defineProps<{
|
||||
version: ViewerModelVersionCardItemFragment
|
||||
isNewest: boolean
|
||||
}>()
|
||||
|
||||
const timeAgoCreatedAt = computed(() => dayjs(props.version.createdAt).from(dayjs()))
|
||||
|
||||
const createdAt = computed(() => {
|
||||
return dayjs(props.version.createdAt).format('LLL')
|
||||
})
|
||||
</script>
|
||||
@@ -41,18 +41,17 @@
|
||||
</ViewerLayoutPanel>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { SpeckleViewer } from '@speckle/shared'
|
||||
import {
|
||||
useInjectedViewerLoadedResources,
|
||||
useInjectedViewerRequestedResources
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import { PlusIcon, CheckIcon, MinusIcon } from '@heroicons/vue/24/solid'
|
||||
import { SpeckleViewer } from '@speckle/shared'
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
const showRemove = ref(false)
|
||||
const { resourceItems, modelsAndVersionIds } = useInjectedViewerLoadedResources()
|
||||
|
||||
const { items } = useInjectedViewerRequestedResources()
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
:last="index === props.model.versions.totalCount - 1"
|
||||
:last-loaded="index === props.model.versions.items.length - 1"
|
||||
@change-version="handleVersionChange"
|
||||
@view-changes="handleViewChanges"
|
||||
/>
|
||||
<div class="mt-4 px-2 py-2">
|
||||
<FormButton
|
||||
@@ -92,21 +93,22 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import dayjs from 'dayjs'
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import {
|
||||
XMarkIcon,
|
||||
ArrowPathRoundedSquareIcon,
|
||||
ChevronUpIcon
|
||||
} from '@heroicons/vue/24/solid'
|
||||
import { ViewerLoadedResourcesQuery } from '~~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
ViewerLoadedResourcesQuery,
|
||||
ViewerModelVersionCardItemFragment
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { Get } from 'type-fest'
|
||||
import {
|
||||
useInjectedViewerLoadedResources,
|
||||
useInjectedViewerRequestedResources
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
|
||||
dayjs.extend(localizedFormat)
|
||||
import { useDiffUtilities } from '~~/lib/viewer/composables/ui'
|
||||
|
||||
type ModelItem = NonNullable<Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>>
|
||||
|
||||
@@ -122,6 +124,7 @@ const props = defineProps<{
|
||||
|
||||
const { switchModelToVersion } = useInjectedViewerRequestedResources()
|
||||
const { loadMoreVersions } = useInjectedViewerLoadedResources()
|
||||
const { diffModelVersions } = useDiffUtilities()
|
||||
|
||||
const showVersions = ref(false)
|
||||
|
||||
@@ -196,4 +199,9 @@ async function handleVersionChange(versionId: string) {
|
||||
const onLoadMore = async () => {
|
||||
await loadMoreVersions(props.model.id)
|
||||
}
|
||||
|
||||
async function handleViewChanges(version: ViewerModelVersionCardItemFragment) {
|
||||
if (!loadedVersion.value?.id) return
|
||||
await diffModelVersions(modelId.value, loadedVersion.value.id, version.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
:class="`bg-foundation group relative block w-full space-y-2 rounded-md pb-2 text-left transition ${
|
||||
clickable ? 'hover:bg-primary-muted' : 'cursor-default'
|
||||
}
|
||||
${!showTimeline ? 'bg-primary-muted' : ''}`"
|
||||
${!showTimeline ? 'bg-primary-muted' : ''}
|
||||
${isLoaded ? '' : ''}
|
||||
`"
|
||||
@click="handleClick"
|
||||
>
|
||||
<!-- Timeline left border -->
|
||||
<div
|
||||
v-if="showTimeline"
|
||||
:class="`absolute top-3 ml-[2px] h-[99%] w-1 border-dashed ${
|
||||
isLoaded ? 'border-primary border-r-2' : 'border-outline-3 border-r-2'
|
||||
:class="`absolute top-3 ml-[2px] h-[99%] w-1 ${
|
||||
isLoaded
|
||||
? 'border-primary border-r-4 border'
|
||||
: 'border-dashed border-outline-3 border-r-2'
|
||||
} group-hover:border-primary left-[7px] z-10 transition-all`"
|
||||
></div>
|
||||
<div
|
||||
@@ -34,6 +38,17 @@
|
||||
>
|
||||
<span>{{ isLatest ? 'Latest' : timeAgoCreatedAt }}</span>
|
||||
</div>
|
||||
<FormButton
|
||||
v-if="!isLoaded"
|
||||
v-tippy="'Shows a summary of added, deleted and changed elements.'"
|
||||
size="xs"
|
||||
text
|
||||
class="opacity-0 group-hover:opacity-100 transition"
|
||||
@click.stop="emit('viewChanges', props.version)"
|
||||
>
|
||||
View Changes
|
||||
</FormButton>
|
||||
<FormButton v-else size="xs" text disabled>Currently Viewing</FormButton>
|
||||
</div>
|
||||
<!-- Main stuff -->
|
||||
<div class="flex items-center space-x-1 pl-5">
|
||||
@@ -82,6 +97,7 @@ const props = withDefaults(
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'changeVersion', version: string): void
|
||||
(e: 'viewChanges', version: ViewerModelVersionCardItemFragment): void
|
||||
}>()
|
||||
|
||||
const isLoaded = computed(() => props.isLoadedVersion)
|
||||
@@ -95,6 +111,7 @@ const createdAt = computed(() => {
|
||||
})
|
||||
|
||||
function handleClick() {
|
||||
console.log('wot')
|
||||
if (props.clickable) emit('changeVersion', props.version.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
:class="`${
|
||||
isModifiedQuery.modified && root
|
||||
? 'outline outline-2 rounded py-1 px-1 outline-amber-500'
|
||||
: ''
|
||||
}`"
|
||||
>
|
||||
<div class="mb-1 flex items-center">
|
||||
<button
|
||||
class="hover:bg-primary-muted hover:text-primary flex h-full min-w-0 items-center space-x-1 rounded"
|
||||
@click="unfold = !unfold"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
:class="`h-3 w-3 transition ${!unfold ? '-rotate-90' : 'rotate-0'}`"
|
||||
:class="`h-3 w-3 transition ${headerClasses} ${
|
||||
!unfold ? '-rotate-90' : 'rotate-0'
|
||||
}`"
|
||||
/>
|
||||
<div class="truncate text-xs font-bold">
|
||||
<div :class="`truncate text-xs font-bold ${headerClasses}`">
|
||||
{{ title || headerAndSubheader.header }}
|
||||
<span
|
||||
v-if="(props.root || props.modifiedSibling) && isModifiedQuery.modified"
|
||||
>
|
||||
{{ isModifiedQuery.isNew ? '(New)' : '(Old)' }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -81,25 +94,100 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isModifiedQuery.modified && isModifiedQuery.pair && root" class="mt-2">
|
||||
<ViewerSelectionObject :object="isModifiedQuery.pair" :modified-sibling="true" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { ChevronDownIcon } from '@heroicons/vue/24/solid'
|
||||
import { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import { getHeaderAndSubheaderForSpeckleObject } from '~~/lib/object-sidebar/helpers'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
const {
|
||||
ui: {
|
||||
diff: { result, enabled: diffEnabled }
|
||||
}
|
||||
} = useInjectedViewerState()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
object: SpeckleObject
|
||||
root?: boolean
|
||||
title?: string
|
||||
unfold: boolean
|
||||
unfold?: boolean
|
||||
debug?: boolean
|
||||
modifiedSibling?: boolean
|
||||
}>(),
|
||||
{ debug: false, unfold: true }
|
||||
{ debug: false, unfold: false, root: false, modifiedSibling: false }
|
||||
)
|
||||
|
||||
const unfold = ref(props.unfold)
|
||||
|
||||
const isAdded = computed(() => {
|
||||
if (!diffEnabled.value) return false
|
||||
return (
|
||||
result.value?.added.findIndex(
|
||||
(o) => (o.model.raw as SpeckleObject).applicationId === props.object.applicationId
|
||||
) !== -1
|
||||
)
|
||||
})
|
||||
|
||||
const isRemoved = computed(() => {
|
||||
if (!diffEnabled.value) return false
|
||||
return (
|
||||
result.value?.removed.findIndex(
|
||||
(o) => (o.model.raw as SpeckleObject).applicationId === props.object.applicationId
|
||||
) !== -1
|
||||
)
|
||||
})
|
||||
|
||||
const isUnchanged = computed(() => {
|
||||
if (!diffEnabled.value) return false
|
||||
return (
|
||||
result.value?.unchanged.findIndex(
|
||||
(o) => (o.model.raw as SpeckleObject).applicationId === props.object.applicationId
|
||||
) !== -1
|
||||
)
|
||||
})
|
||||
|
||||
const isModifiedQuery = computed(() => {
|
||||
// if (props.modifiedSibling) return { modified: false } // prevent recursion?
|
||||
if (!diffEnabled.value) return { modified: false }
|
||||
const modifiedObjectPairs = result.value?.modified.map((pair) => {
|
||||
return [pair[0].model.raw as SpeckleObject, pair[1].model.raw as SpeckleObject]
|
||||
})
|
||||
if (!modifiedObjectPairs) return { modified: false }
|
||||
const obj = props.object
|
||||
const pairedItems = modifiedObjectPairs.find(
|
||||
(item) => item[0].id === obj.id || item[1].id === obj.id
|
||||
)
|
||||
if (!pairedItems) return { modified: false }
|
||||
const pair = pairedItems[0].id === obj.id ? pairedItems[1] : pairedItems[0]
|
||||
if (!pair) return { modified: false }
|
||||
return {
|
||||
modified: true,
|
||||
pair,
|
||||
isNew: pairedItems[0].id !== obj.id
|
||||
}
|
||||
})
|
||||
|
||||
const headerClasses = computed(() => {
|
||||
if (props.modifiedSibling) return 'text-amber-500'
|
||||
if (!props.root) return ''
|
||||
if (!diffEnabled.value) return ''
|
||||
if (!Object.keys(props.object).includes('applicationId')) return ''
|
||||
|
||||
if (isAdded.value) return 'text-green-500'
|
||||
|
||||
if (isRemoved.value) return 'text-red-500'
|
||||
|
||||
if (isUnchanged.value) return 'text-foreground'
|
||||
|
||||
return 'text-amber-500'
|
||||
})
|
||||
|
||||
const headerAndSubheader = computed(() => {
|
||||
return getHeaderAndSubheaderForSpeckleObject(props.object)
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
:key="(object.id as string)"
|
||||
:object="object"
|
||||
:unfold="false"
|
||||
:root="true"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="itemCount <= objects.length" class="mb-2">
|
||||
@@ -54,19 +55,29 @@ import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { getTargetObjectIds } from '~~/lib/object-sidebar/helpers'
|
||||
import { containsAll } from '~~/lib/common/helpers/utils'
|
||||
import { useFilterUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { uniqWith } from 'lodash-es'
|
||||
|
||||
const {
|
||||
viewer: {
|
||||
metadata: { filteringState }
|
||||
}
|
||||
},
|
||||
ui: { diff }
|
||||
} = useInjectedViewerState()
|
||||
const { objects, clearSelection } = useSelectionUtilities()
|
||||
const { hideObjects, showObjects, isolateObjects, unIsolateObjects } =
|
||||
useFilterUtilities()
|
||||
|
||||
const itemCount = ref(10)
|
||||
const itemCount = ref(42)
|
||||
|
||||
const objectsUniqueByAppId = computed(() => {
|
||||
if (!diff.enabled.value) return objects.value
|
||||
return uniqWith(objects.value, (a, b) => {
|
||||
return a.applicationId === b.applicationId
|
||||
})
|
||||
})
|
||||
|
||||
const objectsLimited = computed(() => {
|
||||
return objects.value.slice(0, itemCount.value)
|
||||
return objectsUniqueByAppId.value.slice(0, itemCount.value)
|
||||
})
|
||||
|
||||
const hiddenObjects = computed(() => filteringState.value?.hiddenObjects)
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface AsyncWritableComputedOptions<T> {
|
||||
set: (value: T) => MaybeAsync<void>
|
||||
initialState: T
|
||||
readOptions?: AsyncComputedOptions
|
||||
asyncRead?: boolean
|
||||
}
|
||||
|
||||
export type AsyncWritableComputedRef<T> = ComputedRef<T> & {
|
||||
@@ -15,16 +16,20 @@ export type AsyncWritableComputedRef<T> = ComputedRef<T> & {
|
||||
|
||||
/**
|
||||
* Allows async read/write from/to computed. Use `res.value` to read and `res.update` to write. If you only need
|
||||
* the computed to be read-only then use vueuse's `computedAsync`.
|
||||
* the computed to be read-only then use vueuse's `computedAsync`. If you only need async writes you can
|
||||
* disable async reads through the `asyncRead` param.
|
||||
* @param params
|
||||
*/
|
||||
export function writableAsyncComputed<T>(
|
||||
params: AsyncWritableComputedOptions<T>
|
||||
): AsyncWritableComputedRef<T> {
|
||||
const readValue = computedAsync(params.get, params.initialState, params.readOptions)
|
||||
const { get, initialState, readOptions, set, asyncRead = true } = params
|
||||
const readValue = asyncRead
|
||||
? computedAsync(get, initialState, readOptions)
|
||||
: computed(get)
|
||||
|
||||
const getter = computed(() => readValue.value) as AsyncWritableComputedRef<T>
|
||||
getter.update = params.set
|
||||
getter.update = set
|
||||
|
||||
return getter
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function useRouteHashState() {
|
||||
if (hash.length < 2 || !hash.startsWith('#')) return {}
|
||||
|
||||
const keyValuePairs = hash.substring(1).split('&')
|
||||
return reduce(
|
||||
const result = reduce(
|
||||
keyValuePairs,
|
||||
(result, item) => {
|
||||
const [key, value] = item.split('=')
|
||||
@@ -37,6 +37,7 @@ export function useRouteHashState() {
|
||||
},
|
||||
{} as Record<string, Nullable<string>>
|
||||
)
|
||||
return result
|
||||
},
|
||||
set: async (newVal) => {
|
||||
const hashString = serializeHashState(newVal)
|
||||
@@ -45,7 +46,8 @@ export function useRouteHashState() {
|
||||
hash: hashString
|
||||
})
|
||||
},
|
||||
initialState: {}
|
||||
initialState: {},
|
||||
asyncRead: false
|
||||
})
|
||||
|
||||
return { hashState }
|
||||
|
||||
@@ -128,8 +128,9 @@ const documents = {
|
||||
"\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": types.CreateCommentReplyDocument,
|
||||
"\n mutation ArchiveComment($commentId: String!, $archived: Boolean) {\n commentMutations {\n archive(commentId: $commentId, archived: $archived)\n }\n }\n": types.ArchiveCommentDocument,
|
||||
"\n query ProjectViewerResources($projectId: String!, $resourceUrlString: String!) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": types.ProjectViewerResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n": types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n": types.ViewerLoadedResourcesDocument,
|
||||
"\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": types.ViewerModelVersionsDocument,
|
||||
"\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": types.ViewerDiffVersionsDocument,
|
||||
"\n query ViewerLoadedThreads(\n $projectId: String!\n $filter: ProjectCommentsFilter!\n $cursor: String\n $limit: Int = 25\n ) {\n project(id: $projectId) {\n id\n commentThreads(filter: $filter, cursor: $cursor, limit: $limit) {\n totalCount\n totalArchivedCount\n items {\n ...ViewerCommentThread\n ...LinkableComment\n }\n }\n }\n }\n": types.ViewerLoadedThreadsDocument,
|
||||
"\n subscription OnViewerUserActivityBroadcasted(\n $target: ViewerUpdateTrackingTarget!\n $sessionId: String!\n ) {\n viewerUserActivityBroadcasted(target: $target, sessionId: $sessionId) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\n }\n }\n": types.OnViewerUserActivityBroadcastedDocument,
|
||||
"\n subscription OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\n }\n }\n }\n": types.OnViewerCommentsUpdatedDocument,
|
||||
@@ -616,11 +617,15 @@ export function graphql(source: "\n query ProjectViewerResources($projectId: St
|
||||
/**
|
||||
* 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 ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n"];
|
||||
export function graphql(source: "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n }\n }\n"): (typeof documents)["\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\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 ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\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 query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n"): (typeof documents)["\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
@@ -694,6 +694,8 @@ export type ModelMutationsUpdateArgs = {
|
||||
export type ModelVersionsFilter = {
|
||||
/** Make sure these specified versions are always loaded first */
|
||||
priorityIds?: InputMaybe<Array<Scalars['String']>>;
|
||||
/** Only return versions specified in `priorityIds` */
|
||||
priorityIdsOnly?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ModelsTreeItem = {
|
||||
@@ -3180,6 +3182,16 @@ export type ViewerModelVersionsQueryVariables = Exact<{
|
||||
|
||||
export type ViewerModelVersionsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, role?: string | null, model: { __typename?: 'Model', id: string, versions: { __typename?: 'VersionCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Version', id: string, message?: string | null, referencedObject: string, sourceApplication?: string | null, createdAt: string, previewUrl: string, authorUser?: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null } | null }> } } } };
|
||||
|
||||
export type ViewerDiffVersionsQueryVariables = Exact<{
|
||||
projectId: Scalars['String'];
|
||||
modelId: Scalars['String'];
|
||||
versionAId: Scalars['String'];
|
||||
versionBId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewerDiffVersionsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, model: { __typename?: 'Model', id: string, versionA: { __typename?: 'Version', id: string, message?: string | null, referencedObject: string, sourceApplication?: string | null, createdAt: string, previewUrl: string, authorUser?: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null } | null }, versionB: { __typename?: 'Version', id: string, message?: string | null, referencedObject: string, sourceApplication?: string | null, createdAt: string, previewUrl: string, authorUser?: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null } | null } } } };
|
||||
|
||||
export type ViewerLoadedThreadsQueryVariables = Exact<{
|
||||
projectId: Scalars['String'];
|
||||
filter: ProjectCommentsFilter;
|
||||
@@ -3333,8 +3345,9 @@ export const CreateCommentThreadDocument = {"kind":"Document","definitions":[{"k
|
||||
export const CreateCommentReplyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCommentReply"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateCommentReplyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reply"},"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":"ViewerCommentsReplyItem"}}]}}]}}]}},...ViewerCommentsReplyItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions]} as unknown as DocumentNode<CreateCommentReplyMutation, CreateCommentReplyMutationVariables>;
|
||||
export const ArchiveCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"archived"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"commentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"archived"},"value":{"kind":"Variable","name":{"kind":"Name","value":"archived"}}}]}]}}]}}]} as unknown as DocumentNode<ArchiveCommentMutation, ArchiveCommentMutationVariables>;
|
||||
export const ProjectViewerResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectViewerResources"},"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":"resourceUrlString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"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":"viewerResources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"resourceIdString"},"value":{"kind":"Variable","name":{"kind":"Name","value":"resourceUrlString"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"versionId"}},{"kind":"Field","name":{"kind":"Name","value":"objectId"}}]}}]}}]}}]}}]} as unknown as DocumentNode<ProjectViewerResourcesQuery, ProjectViewerResourcesQueryVariables>;
|
||||
export const ViewerLoadedResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedResources"},"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":"modelIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"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":"role"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","alias":{"kind":"Name","value":"loadedVersion"},"name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"priorityIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}}}]}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"1"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"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":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ModelPageProject"}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ProjectPageLatestItemsModelsFragmentDoc.definitions,...ModelPageProjectFragmentDoc.definitions]} as unknown as DocumentNode<ViewerLoadedResourcesQuery, ViewerLoadedResourcesQueryVariables>;
|
||||
export const ViewerLoadedResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedResources"},"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":"modelIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"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":"role"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","alias":{"kind":"Name","value":"loadedVersion"},"name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"priorityIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"priorityIdsOnly"},"value":{"kind":"BooleanValue","value":true}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"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":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ModelPageProject"}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ProjectPageLatestItemsModelsFragmentDoc.definitions,...ModelPageProjectFragmentDoc.definitions]} as unknown as DocumentNode<ViewerLoadedResourcesQuery, ViewerLoadedResourcesQueryVariables>;
|
||||
export const ViewerModelVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerModelVersions"},"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":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"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":"role"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"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":"ViewerModelVersionCardItem"}}]}}]}}]}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode<ViewerModelVersionsQuery, ViewerModelVersionsQueryVariables>;
|
||||
export const ViewerDiffVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerDiffVersions"},"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":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionAId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionBId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"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":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","alias":{"kind":"Name","value":"versionA"},"name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionAId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"versionB"},"name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionBId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode<ViewerDiffVersionsQuery, ViewerDiffVersionsQueryVariables>;
|
||||
export const ViewerLoadedThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedThreads"},"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":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"25"}}],"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":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalArchivedCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LinkableComment"}}]}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions,...LinkableCommentFragmentDoc.definitions]} as unknown as DocumentNode<ViewerLoadedThreadsQuery, ViewerLoadedThreadsQueryVariables>;
|
||||
export const OnViewerUserActivityBroadcastedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerUserActivityBroadcasted"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewerUserActivityBroadcasted"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}},{"kind":"Argument","name":{"kind":"Name","value":"sessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userName"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"sessionId"}}]}}]}},...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode<OnViewerUserActivityBroadcastedSubscription, OnViewerUserActivityBroadcastedSubscriptionVariables>;
|
||||
export const OnViewerCommentsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerCommentsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectCommentsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"comment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions]} as unknown as DocumentNode<OnViewerCommentsUpdatedSubscription, OnViewerCommentsUpdatedSubscriptionVariables>;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const KEYBOARD_CLICK_CHAR = 'Enter'
|
||||
|
||||
/**
|
||||
* Visible, non-interactive elements with click handlers must have at least one keyboard listener for accessibility.
|
||||
* You can wrap your click handler with this in @keypress, to run it when enter is pressed on the selected component
|
||||
* See more: https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility/blob/main/docs/click-events-have-key-events.md
|
||||
*/
|
||||
export function keyboardClick(cb: (e: KeyboardEvent) => void) {
|
||||
return (e: KeyboardEvent) => {
|
||||
if (e.code !== KEYBOARD_CLICK_CHAR) return
|
||||
cb(e)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export const projectRoute = (id: string) => `/projects/${id}`
|
||||
export const modelRoute = (
|
||||
projectId: string,
|
||||
resourceIdString: string,
|
||||
hashState?: Record<ViewerHashStateKeys, string>
|
||||
hashState?: Partial<Record<ViewerHashStateKeys, string>>
|
||||
) =>
|
||||
`/projects/${projectId}/models/${encodeURIComponent(resourceIdString)}${
|
||||
hashState ? serializeHashState(hashState) || '' : ''
|
||||
|
||||
@@ -175,7 +175,9 @@ export function useViewerCommentBubbles(
|
||||
|
||||
const commentThreads = ref({} as Record<string, CommentBubbleModel>)
|
||||
const openThread = computed(() =>
|
||||
Object.values(commentThreads.value).find((t) => t.isExpanded)
|
||||
Object.values(commentThreads.value).find(
|
||||
(t) => t.isExpanded && t.id === focusedThreadId.value
|
||||
)
|
||||
)
|
||||
|
||||
useSelectionEvents(
|
||||
|
||||
@@ -6,13 +6,14 @@ import { isNonNullable } from '~~/lib/common/helpers/utils'
|
||||
import { SpeckleViewer, TimeoutError } from '@speckle/shared'
|
||||
import { get } from 'lodash-es'
|
||||
import { Vector3, Box3 } from 'three'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { useDiffUtilities, useFilterUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { NumericPropertyInfo } from '@speckle/viewer'
|
||||
|
||||
type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState
|
||||
|
||||
export function useStateSerialization() {
|
||||
const state = useInjectedViewerState()
|
||||
const { serializeDiffCommand } = useDiffUtilities()
|
||||
|
||||
/**
|
||||
* We don't want to save a comment w/ implicit identifiers like ones that only have a model ID or a folder prefix, because
|
||||
@@ -77,6 +78,13 @@ export function useStateSerialization() {
|
||||
newThreadEditor: state.ui.threads.openThread.newThreadEditor.value
|
||||
}
|
||||
},
|
||||
diff: {
|
||||
command: state.urlHashState.diff.value
|
||||
? serializeDiffCommand(state.urlHashState.diff.value)
|
||||
: null,
|
||||
time: state.ui.diff.time.value,
|
||||
mode: state.ui.diff.mode.value
|
||||
},
|
||||
spotlightUserSessionId: state.ui.spotlightUserSessionId.value,
|
||||
filters: {
|
||||
isolatedObjectIds: state.ui.filters.isolatedObjectIds.value,
|
||||
@@ -126,7 +134,8 @@ export function useApplySerializedState() {
|
||||
sectionBox,
|
||||
highlightedObjectIds,
|
||||
explodeFactor,
|
||||
lightConfig
|
||||
lightConfig,
|
||||
diff
|
||||
},
|
||||
resources: {
|
||||
request: { resourceIdString }
|
||||
@@ -144,6 +153,7 @@ export function useApplySerializedState() {
|
||||
waitForAvailableFilter
|
||||
} = useFilterUtilities()
|
||||
const resetState = useResetUiState()
|
||||
const { diffModelVersions, deserializeDiffCommand, endDiff } = useDiffUtilities()
|
||||
|
||||
return async (state: SerializedViewerState, mode: StateApplyMode) => {
|
||||
if (mode === StateApplyMode.Reset) {
|
||||
@@ -241,6 +251,23 @@ export function useApplySerializedState() {
|
||||
await urlHashState.focusedThreadId.update(state.ui.threads.openThread.threadId)
|
||||
}
|
||||
|
||||
const command = state.ui.diff.command
|
||||
? deserializeDiffCommand(state.ui.diff.command)
|
||||
: null
|
||||
if (command && command.diffs.length) {
|
||||
diff.time.value = state.ui.diff.time
|
||||
diff.mode.value = state.ui.diff.mode
|
||||
|
||||
const instruction = command.diffs[0]
|
||||
await diffModelVersions(
|
||||
instruction.versionA.modelId,
|
||||
instruction.versionA.versionId,
|
||||
instruction.versionB.versionId
|
||||
)
|
||||
} else {
|
||||
await endDiff()
|
||||
}
|
||||
|
||||
explodeFactor.value = state.ui.explodeFactor
|
||||
lightConfig.value = {
|
||||
...lightConfig.value,
|
||||
|
||||
@@ -8,7 +8,9 @@ import {
|
||||
ViewerEvent,
|
||||
SunLightConfiguration,
|
||||
DefaultLightConfiguration,
|
||||
SpeckleView
|
||||
SpeckleView,
|
||||
DiffResult,
|
||||
VisualDiffMode
|
||||
} from '@speckle/viewer'
|
||||
import { MaybeRef } from '@vueuse/shared'
|
||||
import {
|
||||
@@ -19,7 +21,8 @@ import {
|
||||
ComputedRef,
|
||||
WritableComputedRef,
|
||||
Raw,
|
||||
Ref
|
||||
Ref,
|
||||
ShallowRef
|
||||
} from 'vue'
|
||||
import { useScopedState } from '~~/lib/common/composables/scopedState'
|
||||
import { Nullable, Optional, SpeckleViewer } from '@speckle/shared'
|
||||
@@ -37,7 +40,8 @@ import {
|
||||
ViewerLoadedThreadsQuery,
|
||||
ViewerResourceItem,
|
||||
ViewerLoadedThreadsQueryVariables,
|
||||
ProjectCommentsFilter
|
||||
ProjectCommentsFilter,
|
||||
ViewerModelVersionCardItemFragment
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { SetNonNullable, Get } from 'type-fest'
|
||||
import {
|
||||
@@ -55,11 +59,16 @@ import { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import { Box3, Vector3 } from 'three'
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { wrapRefWithTracking } from '~~/lib/common/helpers/debugging'
|
||||
import { useFilterUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import {
|
||||
AsyncWritableComputedRef,
|
||||
writableAsyncComputed
|
||||
} from '~~/lib/common/composables/async'
|
||||
import {
|
||||
DiffStateCommand,
|
||||
setupUiDiffState
|
||||
} from '~~/lib/viewer/composables/setup/diff'
|
||||
import { useDiffUtilities, useFilterUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { reduce } from 'lodash-es'
|
||||
|
||||
export type LoadedModel = NonNullable<
|
||||
Get<ViewerLoadedResourcesQuery, 'project.models.items[0]'>
|
||||
@@ -71,12 +80,6 @@ export type LoadedThreadsMetadata = NonNullable<
|
||||
|
||||
export type LoadedCommentThread = NonNullable<Get<LoadedThreadsMetadata, 'items[0]'>>
|
||||
|
||||
// export type FilterAction = (
|
||||
// objectIds: string[],
|
||||
// stateKey: string,
|
||||
// includeDescendants?: boolean
|
||||
// ) => Promise<void>
|
||||
|
||||
export type InjectableViewerState = Readonly<{
|
||||
/**
|
||||
* The project which we're opening in the viewer (all loaded models should belong to it)
|
||||
@@ -145,6 +148,9 @@ export type InjectableViewerState = Readonly<{
|
||||
* Helper for switching model to a specific version (or just latest)
|
||||
*/
|
||||
switchModelToVersion: (modelId: string, versionId?: string) => Promise<void>
|
||||
// addModelVersion: (modelId: string, versionId: string) => void
|
||||
// removeModelVersion: (modelId: string, versionId?: string) => void
|
||||
// setModelVersions: (newResources: ViewerResource[]) => void
|
||||
}
|
||||
/**
|
||||
* State of resolved, validated & de-duplicated resources that are loaded in the viewer. These
|
||||
@@ -165,6 +171,12 @@ export type InjectableViewerState = Readonly<{
|
||||
* Model GQL objects paired with their loaded version IDs
|
||||
*/
|
||||
modelsAndVersionIds: ComputedRef<Array<{ model: LoadedModel; versionId: string }>>
|
||||
/**
|
||||
* All available (retrieved from GQL) models and their versions
|
||||
*/
|
||||
availableModelsAndVersions: ComputedRef<
|
||||
Array<{ model: LoadedModel; versions: LoadedModel['versions']['items'] }>
|
||||
>
|
||||
/**
|
||||
* Detached objects (not models/versions)
|
||||
*/
|
||||
@@ -232,6 +244,14 @@ export type InjectableViewerState = Readonly<{
|
||||
target: Ref<Vector3>
|
||||
isOrthoProjection: Ref<boolean>
|
||||
}
|
||||
diff: {
|
||||
newVersion: ComputedRef<ViewerModelVersionCardItemFragment | undefined>
|
||||
oldVersion: ComputedRef<ViewerModelVersionCardItemFragment | undefined>
|
||||
time: Ref<number>
|
||||
mode: Ref<VisualDiffMode>
|
||||
result: ShallowRef<Optional<DiffResult>> //ComputedRef<Optional<DiffResult>>
|
||||
enabled: Ref<boolean>
|
||||
}
|
||||
sectionBox: Ref<Nullable<Box3>>
|
||||
highlightedObjectIds: Ref<string[]>
|
||||
lightConfig: Ref<SunLightConfiguration>
|
||||
@@ -244,6 +264,7 @@ export type InjectableViewerState = Readonly<{
|
||||
*/
|
||||
urlHashState: {
|
||||
focusedThreadId: AsyncWritableComputedRef<Nullable<string>>
|
||||
diff: AsyncWritableComputedRef<Nullable<DiffStateCommand>>
|
||||
}
|
||||
}>
|
||||
|
||||
@@ -394,7 +415,8 @@ function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest
|
||||
hash: route.hash
|
||||
})
|
||||
},
|
||||
initialState: []
|
||||
initialState: [],
|
||||
asyncRead: false
|
||||
})
|
||||
|
||||
// we could use getParam, but `createGetParamFromResources` does sorting and de-duplication AFAIK
|
||||
@@ -404,7 +426,8 @@ function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest
|
||||
const newResources = SpeckleViewer.ViewerRoute.parseUrlParameters(newVal)
|
||||
await resources.update(newResources)
|
||||
},
|
||||
initialState: ''
|
||||
initialState: '',
|
||||
asyncRead: false
|
||||
})
|
||||
|
||||
const threadFilters = ref({} as Omit<ProjectCommentsFilter, 'resourceIdString'>)
|
||||
@@ -527,8 +550,7 @@ function setupResponseResourceItems(
|
||||
...objectItems
|
||||
]
|
||||
|
||||
// Get rid of duplicates - only 1 resource per model & 1 resource per objectId
|
||||
// TODO: @dim here you can remove the restriction to only have 1 model
|
||||
// Get rid of duplicates - only 1 resource per objectId
|
||||
const encounteredModels = new Set<string>()
|
||||
const encounteredObjects = new Set<string>()
|
||||
const finalItems: ViewerResourceItem[] = []
|
||||
@@ -536,7 +558,8 @@ function setupResponseResourceItems(
|
||||
const modelId = item.modelId
|
||||
const objectId = item.objectId
|
||||
|
||||
if (modelId && encounteredModels.has(modelId)) continue
|
||||
// In case we want to go back to 1 resource per model:
|
||||
// if (modelId && encounteredModels.has(modelId)) continue
|
||||
if (encounteredObjects.has(objectId)) continue
|
||||
|
||||
finalItems.push(item)
|
||||
@@ -615,6 +638,24 @@ function setupResponseResourceData(
|
||||
.filter((o): o is SetNonNullable<typeof o, 'model'> => !!(o.versionId && o.model))
|
||||
)
|
||||
|
||||
const availableModelsAndVersions = computed(() => {
|
||||
const modelItems = models.value
|
||||
return reduce(
|
||||
modelItems,
|
||||
(res, entry) => {
|
||||
res.push({
|
||||
model: entry,
|
||||
versions: [...entry.loadedVersion.items, ...entry.versions.items]
|
||||
})
|
||||
return res
|
||||
},
|
||||
[] as Array<{
|
||||
model: (typeof modelItems)[0]
|
||||
versions: (typeof modelItems)[0]['versions']['items']
|
||||
}>
|
||||
)
|
||||
})
|
||||
|
||||
onViewerLoadedResourcesError((err) => {
|
||||
globalError.value = createError({
|
||||
statusCode: 500,
|
||||
@@ -698,6 +739,7 @@ function setupResponseResourceData(
|
||||
commentThreads,
|
||||
commentThreadsMetadata,
|
||||
modelsAndVersionIds,
|
||||
availableModelsAndVersions,
|
||||
project,
|
||||
resourceQueryVariables: computed(() => viewerLoadedResourcesVariables.value),
|
||||
threadsQueryVariables: computed(() => threadsQueryVariables.value),
|
||||
@@ -768,6 +810,11 @@ function setupInterfaceState(
|
||||
const newThreadEditor = ref(false)
|
||||
const hideBubbles = ref(false)
|
||||
|
||||
/**
|
||||
* Diffing
|
||||
*/
|
||||
const diffState = setupUiDiffState(state)
|
||||
|
||||
const position = ref(new Vector3())
|
||||
const target = ref(new Vector3())
|
||||
const isOrthoProjection = ref(false as boolean)
|
||||
@@ -775,6 +822,9 @@ function setupInterfaceState(
|
||||
return {
|
||||
...state,
|
||||
ui: {
|
||||
diff: {
|
||||
...diffState
|
||||
},
|
||||
selection,
|
||||
lightConfig,
|
||||
explodeFactor,
|
||||
@@ -881,6 +931,7 @@ export function useResetUiState() {
|
||||
ui: { threads, camera, sectionBox, highlightedObjectIds, lightConfig }
|
||||
} = useInjectedViewerState()
|
||||
const { resetFilters } = useFilterUtilities()
|
||||
const { endDiff } = useDiffUtilities()
|
||||
|
||||
return async () => {
|
||||
await threads.closeAllThreads()
|
||||
@@ -889,5 +940,6 @@ export function useResetUiState() {
|
||||
highlightedObjectIds.value = []
|
||||
lightConfig.value = { ...DefaultLightConfiguration }
|
||||
resetFilters()
|
||||
endDiff()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Nullable, Optional, SpeckleViewer } from '@speckle/shared'
|
||||
import { DiffResult, VisualDiffMode } from '@speckle/viewer'
|
||||
import { ViewerModelVersionCardItemFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
InitialStateWithUrlHashState,
|
||||
InjectableViewerState
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import {
|
||||
isObjectLike,
|
||||
has,
|
||||
get,
|
||||
isArray,
|
||||
differenceBy,
|
||||
sortBy,
|
||||
isString
|
||||
} from 'lodash-es'
|
||||
|
||||
export function setupUiDiffState(
|
||||
state: InitialStateWithUrlHashState
|
||||
): InjectableViewerState['ui']['diff'] {
|
||||
const {
|
||||
urlHashState: { diff },
|
||||
resources: {
|
||||
response: { availableModelsAndVersions }
|
||||
}
|
||||
} = state
|
||||
const result = shallowRef(undefined as Optional<DiffResult>)
|
||||
const time = ref(0.5)
|
||||
const mode = ref<VisualDiffMode>(VisualDiffMode.COLORED)
|
||||
|
||||
// TODO: Only single diff for now
|
||||
const getVersion = (type: keyof DiffInstruction) => {
|
||||
const instruction = diff.value?.diffs[0]
|
||||
if (!instruction) return undefined
|
||||
|
||||
const model = availableModelsAndVersions.value.find(
|
||||
(m) => m.model.id === instruction[type].modelId
|
||||
)
|
||||
if (!model) return undefined
|
||||
return model.versions.find((v) => v.id === instruction[type].versionId)
|
||||
}
|
||||
|
||||
const versionA = computed(
|
||||
(): Optional<ViewerModelVersionCardItemFragment> => getVersion('versionA')
|
||||
)
|
||||
const versionB = computed(
|
||||
(): Optional<ViewerModelVersionCardItemFragment> => getVersion('versionB')
|
||||
)
|
||||
|
||||
const sortedActiveVersions = computed(() => {
|
||||
if (!versionA.value || !versionB.value) return null
|
||||
const sorted = sortBy([versionA.value, versionB.value], (v) =>
|
||||
new Date(v.createdAt).getTime()
|
||||
)
|
||||
|
||||
return {
|
||||
oldVersion: sorted[0],
|
||||
newVersion: sorted[1]
|
||||
}
|
||||
})
|
||||
|
||||
const enabled = computed(() => !!(diff.value && sortedActiveVersions.value))
|
||||
|
||||
return {
|
||||
newVersion: computed(() => sortedActiveVersions.value?.newVersion),
|
||||
oldVersion: computed(() => sortedActiveVersions.value?.oldVersion),
|
||||
time,
|
||||
mode,
|
||||
enabled,
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
export type DiffInstruction = {
|
||||
versionA: SpeckleViewer.ViewerRoute.ViewerVersionResource
|
||||
versionB: SpeckleViewer.ViewerRoute.ViewerVersionResource
|
||||
}
|
||||
|
||||
export type DiffStateCommand = {
|
||||
diffs: DiffInstruction[]
|
||||
}
|
||||
|
||||
export function useDiffBuilderUtilities() {
|
||||
const serializeDiffCommand = (command: DiffStateCommand): string =>
|
||||
JSON.stringify(command)
|
||||
|
||||
const deserializeDiffCommand = (
|
||||
command: Nullable<string>
|
||||
): Nullable<DiffStateCommand> => {
|
||||
if (!command) return null
|
||||
|
||||
try {
|
||||
const deserializedCommand = JSON.parse(command) as unknown
|
||||
if (!isObjectLike(deserializedCommand)) throw new Error('Invalid structure')
|
||||
if (!has(deserializedCommand, 'diffs')) throw new Error('Invalid structure')
|
||||
|
||||
const diffs = get(deserializedCommand, 'diffs') as unknown
|
||||
if (!isArray(diffs)) throw new Error('Invalid structure')
|
||||
|
||||
const finalDiffs: DiffInstruction[] = []
|
||||
for (const diff of diffs) {
|
||||
const getResource = (
|
||||
val: unknown
|
||||
): Nullable<SpeckleViewer.ViewerRoute.ViewerVersionResource> => {
|
||||
const valString = isString(val) ? val : null
|
||||
if (!valString) return null
|
||||
|
||||
const [resource] = SpeckleViewer.ViewerRoute.parseUrlParameters(valString)
|
||||
if (!resource || !SpeckleViewer.ViewerRoute.isModelResource(resource))
|
||||
return null
|
||||
|
||||
const modelId = resource.modelId
|
||||
const versionId = resource.versionId
|
||||
if (!modelId || !versionId) return null
|
||||
|
||||
return new SpeckleViewer.ViewerRoute.ViewerVersionResource(modelId, versionId)
|
||||
}
|
||||
|
||||
const versionA = getResource(get(diff, 'versionA'))
|
||||
const versionB = getResource(get(diff, 'versionB'))
|
||||
if (!versionA || !versionB) continue
|
||||
|
||||
finalDiffs.push({
|
||||
versionB,
|
||||
versionA
|
||||
})
|
||||
}
|
||||
|
||||
if (!finalDiffs.length) throw new Error('No valid resource referneces found')
|
||||
return { diffs: finalDiffs }
|
||||
} catch (e) {
|
||||
console.warn('Diff command deserialization failed', e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const areDiffsEqual = (a: DiffStateCommand, b: DiffStateCommand): boolean => {
|
||||
const differentInstructions = differenceBy(
|
||||
a.diffs,
|
||||
b.diffs,
|
||||
(instruction) =>
|
||||
`${instruction.versionA.toString()}->${instruction.versionB.toString()}`
|
||||
)
|
||||
return differentInstructions.length < 1
|
||||
}
|
||||
|
||||
return {
|
||||
serializeDiffCommand,
|
||||
deserializeDiffCommand,
|
||||
areDiffsEqual
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import {
|
||||
PropertyInfo,
|
||||
StringPropertyInfo,
|
||||
SunLightConfiguration,
|
||||
ViewerEvent
|
||||
ViewerEvent,
|
||||
VisualDiffMode
|
||||
} from '@speckle/viewer'
|
||||
import { useAuthCookie } from '~~/lib/auth/composables/auth'
|
||||
import {
|
||||
@@ -44,6 +45,7 @@ import { Vector3 } from 'three'
|
||||
import { areVectorsLooselyEqual } from '~~/lib/viewer/helpers/three'
|
||||
import { Nullable } from '@speckle/shared'
|
||||
import { useCameraUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { watchTriggerable } from '@vueuse/core'
|
||||
|
||||
function useViewerIsBusyEventHandler() {
|
||||
const state = useInjectedViewerState()
|
||||
@@ -102,7 +104,9 @@ function useViewerObjectAutoLoading() {
|
||||
// Viewer initialized - load in all resources
|
||||
if (newIsInitialized && !oldIsInitialized) {
|
||||
const allObjectIds = getUniqueObjectIds(newResources)
|
||||
|
||||
await Promise.all(allObjectIds.map((i) => loadObject(i)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -301,11 +305,7 @@ function useViewerCameraIntegration() {
|
||||
// debouncing pos/target updates to avoid jitteriness + spotlight mode unnecessarily disabling
|
||||
useViewerCameraTracker(
|
||||
() => {
|
||||
const cameraManuallyChanged = loadCameraDataFromViewer()
|
||||
if (cameraManuallyChanged) {
|
||||
// Stop following TODO: Doesn't work very well
|
||||
// spotlightUserSessionId.value = null
|
||||
}
|
||||
loadCameraDataFromViewer()
|
||||
}
|
||||
// { debounceWait: 100 }
|
||||
)
|
||||
@@ -369,7 +369,6 @@ function useViewerCameraIntegration() {
|
||||
if ((!newVal && !oldVal) || (oldVal && areVectorsLooselyEqual(newVal, oldVal))) {
|
||||
return
|
||||
}
|
||||
|
||||
instance.setView({
|
||||
position: newVal,
|
||||
target: target.value
|
||||
@@ -479,14 +478,14 @@ function useViewerFiltersIntegration() {
|
||||
{ immediate: true, flush: 'sync' }
|
||||
)
|
||||
|
||||
const syncColorFilterToViewer = (
|
||||
const syncColorFilterToViewer = async (
|
||||
filter: Nullable<PropertyInfo>,
|
||||
isApplied: boolean
|
||||
) => {
|
||||
const targetFilter = filter || speckleTypeFilter.value
|
||||
|
||||
if (isApplied && targetFilter) instance.setColorFilter(targetFilter)
|
||||
if (!isApplied) instance.removeColorFilter()
|
||||
if (isApplied && targetFilter) await instance.setColorFilter(targetFilter)
|
||||
if (!isApplied) await instance.removeColorFilter()
|
||||
}
|
||||
|
||||
watch(
|
||||
@@ -495,19 +494,19 @@ function useViewerFiltersIntegration() {
|
||||
filters.propertyFilter.filter.value,
|
||||
filters.propertyFilter.isApplied.value
|
||||
],
|
||||
(newVal) => {
|
||||
async (newVal) => {
|
||||
const [filter, isApplied] = newVal
|
||||
syncColorFilterToViewer(filter, isApplied)
|
||||
await syncColorFilterToViewer(filter, isApplied)
|
||||
},
|
||||
{ immediate: true, flush: 'sync' }
|
||||
)
|
||||
|
||||
useOnViewerLoadComplete(
|
||||
() => {
|
||||
async () => {
|
||||
const targetFilter =
|
||||
filters.propertyFilter.filter.value || speckleTypeFilter.value
|
||||
const isApplied = filters.propertyFilter.isApplied.value
|
||||
syncColorFilterToViewer(targetFilter, isApplied)
|
||||
await syncColorFilterToViewer(targetFilter, isApplied)
|
||||
},
|
||||
{ initialOnly: true }
|
||||
)
|
||||
@@ -594,6 +593,109 @@ function useExplodeFactorIntegration() {
|
||||
)
|
||||
}
|
||||
|
||||
function useDiffingIntegration() {
|
||||
const state = useInjectedViewerState()
|
||||
const authCookie = useAuthCookie()
|
||||
const getObjectUrl = useGetObjectUrl()
|
||||
|
||||
const hasInitialLoadFired = ref(false)
|
||||
|
||||
const { trigger: triggerDiffCommandWatch } = watchTriggerable(
|
||||
() => <const>[state.ui.diff.oldVersion.value, state.ui.diff.newVersion.value],
|
||||
async (newVal, oldVal) => {
|
||||
if (!hasInitialLoadFired.value) return
|
||||
const [oldVersion, newVersion] = newVal
|
||||
const [oldOldVersion, oldNewVersion] = oldVal || [null, null]
|
||||
|
||||
const versionId = (version: typeof oldOldVersion) => version?.id || null
|
||||
const commandId = (
|
||||
oldVersion: typeof oldOldVersion,
|
||||
newVersion: typeof oldOldVersion
|
||||
) => {
|
||||
const oldId = versionId(oldVersion)
|
||||
const newId = versionId(newVersion)
|
||||
return oldId && newId ? `${oldId}->${newId}` : null
|
||||
}
|
||||
|
||||
const newCommand = commandId(oldVersion, newVersion)
|
||||
const oldCommand = commandId(oldOldVersion, oldNewVersion)
|
||||
|
||||
if ((newCommand && oldCommand === newCommand) || !!newCommand === !!oldCommand)
|
||||
return
|
||||
|
||||
if (!newCommand || oldVal) {
|
||||
await state.viewer.instance.undiff()
|
||||
if (!newCommand) return
|
||||
}
|
||||
|
||||
// values shouldn't be undefined cause commandId() generation succeeded
|
||||
const oldObjUrl = getObjectUrl(
|
||||
state.projectId.value,
|
||||
oldVersion?.referencedObject as string
|
||||
)
|
||||
const newObjUrl = getObjectUrl(
|
||||
state.projectId.value,
|
||||
newVersion?.referencedObject as string
|
||||
)
|
||||
|
||||
state.ui.diff.result.value = await state.viewer.instance.diff(
|
||||
oldObjUrl,
|
||||
newObjUrl,
|
||||
state.ui.diff.mode.value,
|
||||
authCookie.value
|
||||
)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// const preventWatchers = 0
|
||||
watch(state.ui.diff.result, (val) => {
|
||||
if (!val) return
|
||||
// reset visual diff time and mode on new diff result
|
||||
// sometimes the watcher won't fire even when the values are updated, because they're updated to
|
||||
// the same values that they were already. because of that we're manually & forcefully running
|
||||
// the relevant watchers when diffResult changes
|
||||
ignoreDiffModeUpdates(() => {
|
||||
ignoreDiffTimeUpdates(() => {
|
||||
state.ui.diff.time.value = 0.5
|
||||
state.ui.diff.mode.value = VisualDiffMode.COLORED
|
||||
|
||||
// this watcher also updates diffTime, so no need to invoke that separately
|
||||
triggerDiffModeWatch()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const { ignoreUpdates: ignoreDiffTimeUpdates } = watchTriggerable(
|
||||
state.ui.diff.time,
|
||||
(val) => {
|
||||
if (!hasInitialLoadFired.value) return
|
||||
if (!state.ui.diff.result.value) return
|
||||
|
||||
state.viewer.instance.setDiffTime(state.ui.diff.result.value, val)
|
||||
}
|
||||
)
|
||||
|
||||
const { trigger: triggerDiffModeWatch, ignoreUpdates: ignoreDiffModeUpdates } =
|
||||
watchTriggerable(state.ui.diff.mode, (val) => {
|
||||
if (!hasInitialLoadFired.value) return
|
||||
if (!state.ui.diff.result.value) return
|
||||
|
||||
state.viewer.instance.setVisualDiffMode(state.ui.diff.result.value, val)
|
||||
state.viewer.instance.setDiffTime(
|
||||
state.ui.diff.result.value,
|
||||
state.ui.diff.time.value
|
||||
) // hmm, why do i need to call diff time again? seems like a minor viewer bug
|
||||
})
|
||||
|
||||
useOnViewerLoadComplete(({ isInitial }) => {
|
||||
if (!isInitial) return
|
||||
hasInitialLoadFired.value = true
|
||||
|
||||
triggerDiffCommandWatch()
|
||||
})
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
function useDebugViewerEvents() {
|
||||
if (process.server) return
|
||||
@@ -624,6 +726,7 @@ export function useViewerPostSetup() {
|
||||
useViewerFiltersIntegration()
|
||||
useLightConfigIntegration()
|
||||
useExplodeFactorIntegration()
|
||||
useDiffingIntegration()
|
||||
|
||||
// test
|
||||
// useDebugViewerEvents()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import { Nullable } from '@speckle/shared'
|
||||
import { SelectionEvent } from '@speckle/viewer'
|
||||
import { SpeckleObject } from '~~/lib/common/helpers/sceneExplorer'
|
||||
import { useInjectedViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { useCameraUtilities, useSelectionUtilities } from '~~/lib/viewer/composables/ui'
|
||||
import { useSelectionEvents } from '~~/lib/viewer/composables/viewer'
|
||||
@@ -35,6 +37,30 @@ function useSelectOrZoomOnSelection() {
|
||||
|
||||
if (!firstVisibleSelectionHit) return clearSelection()
|
||||
addToSelection(firstVisibleSelectionHit.object)
|
||||
// Expands default viewer selection behaviour with a special case in diff mode.
|
||||
// In diff mode, if we select via a mouse click an object, and that object is
|
||||
// "modified", we want to select its pair as well.
|
||||
if (
|
||||
state.ui.diff.enabled.value &&
|
||||
state.ui.diff.result.value &&
|
||||
firstVisibleSelectionHit.object.applicationId
|
||||
) {
|
||||
const modifiedObjectPairs = state.ui.diff.result.value.modified
|
||||
const obj = firstVisibleSelectionHit.object
|
||||
const pairedItems = modifiedObjectPairs.find(
|
||||
(item) =>
|
||||
(item[0].model.raw as SpeckleObject).id === obj.id ||
|
||||
(item[1].model.raw as SpeckleObject).id === obj.id
|
||||
)
|
||||
if (!pairedItems) return
|
||||
|
||||
const pair =
|
||||
(pairedItems[0].model.raw as SpeckleObject).id === obj.id
|
||||
? (pairedItems[1].model.raw as SpeckleObject)
|
||||
: (pairedItems[0].model.raw as SpeckleObject)
|
||||
if (!pair) return
|
||||
addToSelection(pair)
|
||||
}
|
||||
},
|
||||
doubleClickCallback: (args, { firstVisibleSelectionHit }) => {
|
||||
if (!args) return zoom()
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { writableAsyncComputed } from '~~/lib/common/composables/async'
|
||||
import { useRouteHashState } from '~~/lib/common/composables/url'
|
||||
import type { InjectableViewerState } from '~~/lib/viewer/composables/setup'
|
||||
import { useDiffBuilderUtilities } from '~~/lib/viewer/composables/setup/diff'
|
||||
|
||||
export enum ViewerHashStateKeys {
|
||||
FocusedThreadId = 'threadId'
|
||||
FocusedThreadId = 'threadId',
|
||||
Diff = 'diff'
|
||||
}
|
||||
|
||||
export function setupUrlHashState(): InjectableViewerState['urlHashState'] {
|
||||
const { hashState } = useRouteHashState()
|
||||
const { deserializeDiffCommand, serializeDiffCommand } = useDiffBuilderUtilities()
|
||||
|
||||
const focusedThreadId = writableAsyncComputed({
|
||||
get: () => hashState.value[ViewerHashStateKeys.FocusedThreadId] || null,
|
||||
@@ -17,10 +20,26 @@ export function setupUrlHashState(): InjectableViewerState['urlHashState'] {
|
||||
[ViewerHashStateKeys.FocusedThreadId]: newVal
|
||||
})
|
||||
},
|
||||
initialState: null
|
||||
initialState: null,
|
||||
asyncRead: false
|
||||
})
|
||||
|
||||
const diff = writableAsyncComputed({
|
||||
get: () => {
|
||||
const urlValue = hashState.value[ViewerHashStateKeys.Diff]
|
||||
return deserializeDiffCommand(urlValue)
|
||||
},
|
||||
set: async (newVal) =>
|
||||
await hashState.update({
|
||||
...hashState.value,
|
||||
[ViewerHashStateKeys.Diff]: newVal ? serializeDiffCommand(newVal) : null
|
||||
}),
|
||||
initialState: null,
|
||||
asyncRead: false
|
||||
})
|
||||
|
||||
return {
|
||||
focusedThreadId
|
||||
focusedThreadId,
|
||||
diff
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { timeoutAt } from '@speckle/shared'
|
||||
import { SpeckleViewer, timeoutAt } from '@speckle/shared'
|
||||
import { PropertyInfo } from '@speckle/viewer'
|
||||
import { until } from '@vueuse/shared'
|
||||
import { difference, isString, uniq } from 'lodash-es'
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useInjectedViewerInterfaceState,
|
||||
useInjectedViewerState
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import { useDiffBuilderUtilities } from '~~/lib/viewer/composables/setup/diff'
|
||||
|
||||
export function useSectionBoxUtilities() {
|
||||
const { instance } = useInjectedViewer()
|
||||
@@ -271,3 +272,42 @@ export function useSelectionUtilities() {
|
||||
objects: selectedObjects
|
||||
}
|
||||
}
|
||||
|
||||
export function useDiffUtilities() {
|
||||
const state = useInjectedViewerState()
|
||||
const { serializeDiffCommand, deserializeDiffCommand, areDiffsEqual } =
|
||||
useDiffBuilderUtilities()
|
||||
|
||||
const endDiff = async () => {
|
||||
await state.urlHashState.diff.update(null)
|
||||
}
|
||||
|
||||
const diffModelVersions = async (
|
||||
modelId: string,
|
||||
versionA: string,
|
||||
versionB: string
|
||||
) => {
|
||||
await state.urlHashState.diff.update({
|
||||
diffs: [
|
||||
{
|
||||
versionA: new SpeckleViewer.ViewerRoute.ViewerVersionResource(
|
||||
modelId,
|
||||
versionA
|
||||
),
|
||||
versionB: new SpeckleViewer.ViewerRoute.ViewerVersionResource(
|
||||
modelId,
|
||||
versionB
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
serializeDiffCommand,
|
||||
deserializeDiffCommand,
|
||||
endDiff,
|
||||
diffModelVersions,
|
||||
areDiffsEqual
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
} from '~~/lib/viewer/composables/setup'
|
||||
import { SelectionEvent, ViewerEvent } from '@speckle/viewer'
|
||||
import { debounce, isArray, throttle } from 'lodash-es'
|
||||
import { until } from '@vueuse/core'
|
||||
import { MaybeAsync, Nullable, TimeoutError, timeoutAt } from '@speckle/shared'
|
||||
import { until } from '@vueuse/shared'
|
||||
import { Vector3 } from 'three'
|
||||
import { areVectorsLooselyEqual } from '~~/lib/viewer/helpers/three'
|
||||
|
||||
|
||||
@@ -35,7 +35,9 @@ export const viewerLoadedResourcesQuery = graphql(`
|
||||
id
|
||||
name
|
||||
updatedAt
|
||||
loadedVersion: versions(filter: { priorityIds: $versionIds }, limit: 1) {
|
||||
loadedVersion: versions(
|
||||
filter: { priorityIds: $versionIds, priorityIdsOnly: true }
|
||||
) {
|
||||
items {
|
||||
...ViewerModelVersionCardItem
|
||||
}
|
||||
@@ -82,6 +84,28 @@ export const viewerModelVersionsQuery = graphql(`
|
||||
}
|
||||
`)
|
||||
|
||||
export const viewerDiffVersionsQuery = graphql(`
|
||||
query ViewerDiffVersions(
|
||||
$projectId: String!
|
||||
$modelId: String!
|
||||
$versionAId: String!
|
||||
$versionBId: String!
|
||||
) {
|
||||
project(id: $projectId) {
|
||||
id
|
||||
model(id: $modelId) {
|
||||
id
|
||||
versionA: version(id: $versionAId) {
|
||||
...ViewerModelVersionCardItem
|
||||
}
|
||||
versionB: version(id: $versionBId) {
|
||||
...ViewerModelVersionCardItem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const viewerLoadedThreadsQuery = graphql(`
|
||||
query ViewerLoadedThreads(
|
||||
$projectId: String!
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<TourOnboarding />
|
||||
</div>
|
||||
<!-- Viewer host -->
|
||||
<div class="special-gradient absolute w-screen h-screen z-10">
|
||||
<div class="special-gradient absolute w-screen h-screen z-10 overflow-hidden">
|
||||
<ViewerBase />
|
||||
<Transition
|
||||
enter-from-class="opacity-0"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat'
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.extend(localizedFormat)
|
||||
})
|
||||
|
||||
@@ -239,6 +239,11 @@ input ModelVersionsFilter {
|
||||
Make sure these specified versions are always loaded first
|
||||
"""
|
||||
priorityIds: [String!]
|
||||
|
||||
"""
|
||||
Only return versions specified in `priorityIds`
|
||||
"""
|
||||
priorityIdsOnly: Boolean
|
||||
}
|
||||
|
||||
enum ProjectModelsUpdatedMessageType {
|
||||
|
||||
@@ -163,7 +163,12 @@ export async function convertLegacyDataToState(
|
||||
data.location.y as number,
|
||||
data.location.z as number
|
||||
]
|
||||
: null
|
||||
: null,
|
||||
diff: {
|
||||
command: null,
|
||||
mode: 1,
|
||||
time: 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
|
||||
@@ -706,6 +706,8 @@ export type ModelMutationsUpdateArgs = {
|
||||
export type ModelVersionsFilter = {
|
||||
/** Make sure these specified versions are always loaded first */
|
||||
priorityIds?: InputMaybe<Array<Scalars['String']>>;
|
||||
/** Only return versions specified in `priorityIds` */
|
||||
priorityIdsOnly?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ModelsTreeItem = {
|
||||
|
||||
@@ -48,7 +48,8 @@ export async function getPaginatedBranchCommits(
|
||||
// Load priority commits first
|
||||
let priorityCommitPromise: Optional<ReturnType<typeof getSpecificBranchCommits>> =
|
||||
undefined
|
||||
if (params.filter?.priorityIds && !params.cursor) {
|
||||
const loadPriorityIds = params.filter?.priorityIds && !params.cursor
|
||||
if (params.filter?.priorityIds && loadPriorityIds) {
|
||||
priorityCommitPromise = getSpecificBranchCommits(
|
||||
params.filter.priorityIds.map((i) => ({
|
||||
branchId: params.branchId,
|
||||
@@ -57,16 +58,23 @@ export async function getPaginatedBranchCommits(
|
||||
)
|
||||
}
|
||||
|
||||
const priorityIdsOnly = loadPriorityIds && params.filter?.priorityIdsOnly
|
||||
const [results, totalCount, priorityCommits] = await Promise.all([
|
||||
getPaginatedBranchCommitsDb({
|
||||
...params,
|
||||
filter: {
|
||||
...(params.filter || {}),
|
||||
// If we loaded priority commits first, exclude them from base results
|
||||
excludeIds: params.filter?.priorityIds || undefined
|
||||
}
|
||||
}),
|
||||
getBranchCommitsTotalCount(params),
|
||||
!priorityIdsOnly
|
||||
? getPaginatedBranchCommitsDb({
|
||||
...params,
|
||||
filter: {
|
||||
...(params.filter || {}),
|
||||
// If we loaded priority commits first, exclude them from base results
|
||||
excludeIds: params.filter?.priorityIds || undefined
|
||||
}
|
||||
})
|
||||
: { commits: [], cursor: null },
|
||||
!priorityIdsOnly
|
||||
? getBranchCommitsTotalCount(params)
|
||||
: (priorityCommitPromise || Promise.resolve([])).then(
|
||||
(commits) => commits.length
|
||||
),
|
||||
priorityCommitPromise || Promise.resolve([])
|
||||
])
|
||||
|
||||
|
||||
@@ -697,6 +697,8 @@ export type ModelMutationsUpdateArgs = {
|
||||
export type ModelVersionsFilter = {
|
||||
/** Make sure these specified versions are always loaded first */
|
||||
priorityIds?: InputMaybe<Array<Scalars['String']>>;
|
||||
/** Only return versions specified in `priorityIds` */
|
||||
priorityIdsOnly?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
|
||||
export type ModelsTreeItem = {
|
||||
|
||||
@@ -38,6 +38,19 @@ export class ViewerModelResource implements ViewerResource {
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewerVersionResource extends ViewerModelResource {
|
||||
public versionId: string
|
||||
|
||||
constructor(modelId: string, versionId: string) {
|
||||
super(modelId, versionId)
|
||||
this.versionId = versionId
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export class ViewerObjectResource implements ViewerResource {
|
||||
public type: ViewerResourceType
|
||||
public objectId: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { intersection, isObjectLike } from 'lodash'
|
||||
import { get, intersection, isObjectLike } from 'lodash'
|
||||
import { MaybeNullOrUndefined, Nullable } from '../../core/helpers/utilityTypes'
|
||||
|
||||
export const SERIALIZED_VIEWER_STATE_VERSION = 1.1
|
||||
export const SERIALIZED_VIEWER_STATE_VERSION = 1.2
|
||||
|
||||
export type SerializedViewerState = {
|
||||
projectId: string
|
||||
@@ -31,6 +31,11 @@ export type SerializedViewerState = {
|
||||
newThreadEditor: boolean
|
||||
}
|
||||
}
|
||||
diff: {
|
||||
command: Nullable<string>
|
||||
time: number
|
||||
mode: number
|
||||
}
|
||||
spotlightUserSessionId: Nullable<string>
|
||||
filters: {
|
||||
isolatedObjectIds: string[]
|
||||
@@ -99,5 +104,17 @@ export const formatSerializedViewerState = (
|
||||
state.ui.spotlightUserSessionId = null
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.1 -> v1.2
|
||||
* - ui.diff added
|
||||
*/
|
||||
if (!state.ui.diff || !get(state.ui.diff, 'command')) {
|
||||
state.ui.diff = {
|
||||
command: null,
|
||||
mode: 1,
|
||||
time: 0.5
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
@@ -122,6 +122,18 @@ export default plugin(function ({ addComponents, addBase }) {
|
||||
"[type='checkbox']:focus, [type='radio']:focus": {
|
||||
'@apply ring-offset-foundation': {}
|
||||
},
|
||||
"input[type='range']": {
|
||||
'@apply appearance-none bg-transparent': {}
|
||||
},
|
||||
"input[type='range']::-webkit-slider-runnable-track": {
|
||||
'@apply bg-black/25 rounded-full': {}
|
||||
},
|
||||
"input[type='range']::-moz-range-track": {
|
||||
'@apply bg-black/25 rounded-full': {}
|
||||
},
|
||||
"input[type='range']::-ms-track": {
|
||||
'@apply bg-black/25 rounded-full': {}
|
||||
},
|
||||
body: {
|
||||
'@apply font-sans': {}
|
||||
},
|
||||
|
||||
@@ -262,8 +262,8 @@ export class Differ {
|
||||
}
|
||||
|
||||
public setDiffTime(time: number) {
|
||||
const from = Math.min(Math.max(1 - time, 0.2), 1)
|
||||
const to = Math.min(Math.max(time, 0.2), 1)
|
||||
const from = Math.min(Math.max(1 - time, 0), 1)
|
||||
const to = Math.min(Math.max(time, 0), 1)
|
||||
|
||||
this.addedMaterials.forEach((mat) => {
|
||||
mat.opacity =
|
||||
|
||||
@@ -568,6 +568,10 @@ export class Viewer extends EventEmitter implements IViewer {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Alex, don't kill me over this one - it's making things in the FE much easier...
|
||||
// I know this probably screws up showing multiple diffs at the same time, but for the
|
||||
// time being it's probs a good compromise
|
||||
private dynamicallyLoadedDiffResources = [] as string[]
|
||||
public async diff(
|
||||
urlA: string,
|
||||
urlB: string,
|
||||
@@ -575,10 +579,16 @@ export class Viewer extends EventEmitter implements IViewer {
|
||||
authToken?: string
|
||||
): Promise<DiffResult> {
|
||||
const loadPromises = []
|
||||
if (!this.tree.findId(urlA))
|
||||
this.dynamicallyLoadedDiffResources = []
|
||||
|
||||
if (!this.tree.findId(urlA)) {
|
||||
loadPromises.push(this.loadObjectAsync(urlA, authToken, undefined, 1))
|
||||
if (!this.tree.findId(urlB))
|
||||
this.dynamicallyLoadedDiffResources.push(urlA)
|
||||
}
|
||||
if (!this.tree.findId(urlB)) {
|
||||
loadPromises.push(this.loadObjectAsync(urlB, authToken, undefined, 1))
|
||||
this.dynamicallyLoadedDiffResources.push(urlB)
|
||||
}
|
||||
await Promise.all(loadPromises)
|
||||
|
||||
const diffResult = await this.differ.diff(urlA, urlB)
|
||||
@@ -599,12 +609,20 @@ export class Viewer extends EventEmitter implements IViewer {
|
||||
return Promise.resolve(diffResult)
|
||||
}
|
||||
|
||||
public undiff() {
|
||||
public async undiff() {
|
||||
const pipelineOptions = this.speckleRenderer.pipelineOptions
|
||||
pipelineOptions.depthSide = DoubleSide
|
||||
this.speckleRenderer.pipelineOptions = pipelineOptions
|
||||
this.differ.resetMaterialGroups()
|
||||
this.filteringManager.removeUserMaterials()
|
||||
|
||||
const unloadPromises = []
|
||||
if (this.dynamicallyLoadedDiffResources.length !== 0) {
|
||||
for (const id of this.dynamicallyLoadedDiffResources)
|
||||
unloadPromises.push(this.unloadObject(id))
|
||||
}
|
||||
this.dynamicallyLoadedDiffResources = []
|
||||
await Promise.all(unloadPromises)
|
||||
}
|
||||
|
||||
public setDiffTime(diffResult: DiffResult, time: number) {
|
||||
|
||||
@@ -68,9 +68,7 @@ async function doWork() {
|
||||
(err, stdout, stderr) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return reject()
|
||||
}
|
||||
|
||||
if (stdout) {
|
||||
console.log(stdout)
|
||||
}
|
||||
@@ -79,9 +77,15 @@ async function doWork() {
|
||||
}
|
||||
}
|
||||
)
|
||||
proc.on('exit', () => {
|
||||
console.log(`...done [${Math.round(performance.now() - now)}ms]`)
|
||||
return resolve()
|
||||
proc.on('exit', (code) => {
|
||||
console.log(
|
||||
`...done w/ status ${code} [${Math.round(performance.now() - now)}ms]`
|
||||
)
|
||||
if (!code) {
|
||||
resolve()
|
||||
} else {
|
||||
reject(new Error('Failed with non-0 status code'))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,7 +94,17 @@
|
||||
"vue-syntactic-server.trace.server": "off",
|
||||
"svg.preview.background": "transparent",
|
||||
"editor.tabSize": 2,
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/bower_components": true,
|
||||
"**/*.code-search": true,
|
||||
"**/.nuxt": true,
|
||||
"**/.output": true
|
||||
},
|
||||
"[vue]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations.
|
||||
|
||||
Reference in New Issue
Block a user