Files
speckle-server/packages/frontend-2/components/project/page/models/StructureItem.vue
T
andrewwallacespeckle 3ed4bd3f0d Remove @mouseleave (#2774)
2024-08-27 21:20:25 +01:00

418 lines
13 KiB
Vue

<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
<!-- eslint-disable vuejs-accessibility/mouse-events-have-key-events -->
<template>
<div class="space-y-4 relative">
<div
v-if="itemType !== StructureItemType.ModelWithOnlySubmodels"
class="group relative bg-foundation w-full p-2 flex flex-row rounded-md transition-all border border-outline-3 items-stretch"
>
<div class="flex items-center flex-grow order-2 sm:order-1 pl-2 sm:pl-4">
<!-- Name -->
<div
class="flex justify-between sm:justify-start gap-2 items-center w-full sm:w-auto"
>
<NuxtLink :to="modelLink || undefined">
<span class="text-heading text-foreground hover:text-primary">
{{ name }}
</span>
</NuxtLink>
<span
v-if="model"
class="opacity-100 sm:opacity-0 group-hover:opacity-100 transition"
>
<ProjectPageModelsActions
v-model:open="showActionsMenu"
:model="model"
:project="project"
:can-edit="canContribute"
@click.stop.prevent
@model-updated="$emit('model-updated')"
@upload-version="triggerVersionUpload"
/>
</span>
</div>
<!-- Empty model action -->
<NuxtLink
v-if="itemType === StructureItemType.EmptyModel"
:class="[
'cursor-pointer ml-2 text-xs text-foreground-2 flex items-center space-x-1',
'opacity-0 group-hover:opacity-100 transition duration-200',
'hover:text-primary p-1'
]"
@click.stop="$emit('create-submodel', model?.name || '')"
>
<PlusIcon class="w-3 h-3" />
submodel
</NuxtLink>
<!-- Spacer -->
<div class="flex-grow"></div>
<ProjectCardImportFileArea
v-if="!isPendingFileUpload(item)"
ref="importArea"
:project-id="project.id"
:model-name="item.fullName"
class="hidden"
/>
<div
v-if="
!isPendingFileUpload(item) &&
(pendingVersion || itemType === StructureItemType.EmptyModel)
"
class="flex items-center h-full"
>
<ProjectPendingFileImportStatus
v-if="pendingVersion"
:upload="pendingVersion"
type="subversion"
class="px-4 w-full"
/>
<ProjectCardImportFileArea
v-else
:project-id="project.id"
:model-name="item.fullName"
class="h-full w-full"
/>
</div>
<div v-else-if="hasVersions" class="hidden sm:flex items-center gap-x-2">
<div class="text-body-3xs text-foreground-2 text-right">
Updated
<span v-tippy="updatedAt.full">
{{ updatedAt.relative }}
</span>
</div>
<div class="space-x-2 flex flex-row pils">
<div class="text-body-xs text-foreground flex items-center space-x-1 pl-2">
<IconDiscussions class="w-4 h-4" />
<span>{{ model?.commentThreadCount.totalCount }}</span>
</div>
<div v-if="model?.automationsStatus">
<AutomateRunsTriggerStatus
:project-id="project.id"
:status="model.automationsStatus"
:model-id="model.id"
/>
</div>
<div class="flex gap-2 items-center">
<FormButton
v-if="!isPendingFileUpload(item) && model?.id"
rounded
size="sm"
class="gap-0.5"
color="subtle"
@click.stop="onVersionsClick"
>
<IconVersions class="h-4 w-4" />
{{ model?.versionCount.totalCount }}
</FormButton>
</div>
</div>
</div>
<ProjectPendingFileImportStatus
v-else-if="pendingModel && itemType === StructureItemType.PendingModel"
:upload="pendingModel"
class="text-foreground-2 text-sm flex flex-col items-center space-y-1 mr-4"
/>
</div>
<!-- Preview or icon section -->
<div
v-if="!isPendingFileUpload(item) && item.model?.previewUrl && !pendingVersion"
class="w-20 h-16"
>
<NuxtLink
:to="modelLink || ''"
class="h-full w-full block bg-foundation-page rounded-lg border border-outline-3 hover:border-outline-5"
>
<PreviewImage
v-if="item.model?.previewUrl"
:preview-url="item.model.previewUrl"
/>
</NuxtLink>
</div>
</div>
<!-- Doubling up for mixed items -->
<div
v-if="hasSubmodels"
class="border-l-2 border-primary-muted hover:border-primary transition rounded-md"
>
<button
class="group bg-foundation w-full py-1 pr-2 sm:pr-4 flex items-center rounded-md cursor-pointer hover:border-outline-5 transition-all border border-outline-3 border-l-0"
href="/test"
@click.stop="expanded = !expanded"
>
<!-- Icon -->
<div>
<div class="mx-2 flex items-center hover:text-primary text-foreground-2 h-14">
<ChevronDownIcon
:class="`w-4 h-4 transition ${expanded ? 'rotate-180' : ''}`"
/>
</div>
</div>
<!-- Name -->
<FolderIcon class="w-4 h-4 text-foreground" />
<div class="ml-2 text-heading text-foreground flex-grow text-left">
{{ name }}
</div>
<!-- Preview -->
<div class="flex flex-col items-end sm:flex-row sm:items-center gap-1 sm:gap-4">
<!-- Commented out so that we need to load less data, can be added back -->
<!-- <div
v-for="(child, index) in item.children"
:key="index"
:class="`w-16 h-16 ml-2`"
>
<div
class="w-full h-full rounded-md bg-primary-muted shadow flex items-center justify-center text-blue-500/50 text-xs"
>
{{ child?.name }}
</div>
</div> -->
<div class="text-body-2xs text-foreground-2">
Updated
<span v-tippy="updatedAt.full">
{{ updatedAt.relative }}
</span>
</div>
<FormButton
size="sm"
color="outline"
:to="viewAllUrl"
:disabled="!viewAllUrl"
@click.stop="trackFederateModels"
>
View all
</FormButton>
</div>
</button>
<!-- Children list -->
<div
v-if="hasChildren && expanded && !isPendingFileUpload(item)"
class="pl-8 mt-2 space-y-2"
>
<div v-if="childrenLoading" class="mr-8">
<CommonLoadingBar loading />
</div>
<template v-else>
<div v-for="child in children" :key="child.fullName" class="flex">
<div class="h-20 absolute -ml-8 flex items-center mt-0 mr-1 pl-1">
<ChevronDownIcon class="w-4 h-4 rotate-45 text-foreground-2" />
</div>
<ProjectPageModelsStructureItem
:item="child"
:project="project"
:can-contribute="canContribute"
class="flex-grow"
@model-updated="onModelUpdated"
@create-submodel="emit('create-submodel', $event)"
/>
</div>
</template>
<div v-if="canContribute" class="mr-8"></div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { modelVersionsRoute, modelRoute } from '~~/lib/common/helpers/route'
import { ChevronDownIcon, PlusIcon } from '@heroicons/vue/20/solid'
import { FolderIcon } from '@heroicons/vue/24/outline'
import type {
PendingFileUploadFragment,
ProjectPageModelsStructureItem_ProjectFragment,
SingleLevelModelTreeItemFragment
} from '~~/lib/common/generated/gql/graphql'
import { graphql } from '~~/lib/common/generated/gql'
import { useQuery } from '@vue/apollo-composable'
import { projectModelChildrenTreeQuery } from '~~/lib/projects/graphql/queries'
import { has } from 'lodash-es'
import type { Nullable } from '@speckle/shared'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useIsModelExpanded } from '~~/lib/projects/composables/models'
/**
* TODO: The template in this file is a complete mess, needs refactoring
*/
enum StructureItemType {
EmptyModel, // emptyModel
ModelWithOnlyVersions, // fullModel
ModelWithOnlySubmodels, // group
ModelWithVersionsAndSubmodels, // mixed
PendingModel
}
graphql(`
fragment ProjectPageModelsStructureItem_Project on Project {
id
...ProjectPageModelsActions_Project
}
`)
graphql(`
fragment SingleLevelModelTreeItem on ModelsTreeItem {
id
name
fullName
model {
...ProjectPageLatestItemsModelItem
}
hasChildren
updatedAt
}
`)
const isPendingFileUpload = (
i: SingleLevelModelTreeItemFragment | PendingFileUploadFragment
): i is PendingFileUploadFragment => has(i, 'uploadDate')
const emit = defineEmits<{
(e: 'model-updated'): void
(e: 'create-submodel', parentModelName: string): void
}>()
const props = defineProps<{
item: SingleLevelModelTreeItemFragment | PendingFileUploadFragment
project: ProjectPageModelsStructureItem_ProjectFragment
canContribute?: boolean
isSearchResult?: boolean
}>()
provide('projectId', props.project.id)
const router = useRouter()
const importArea = ref(
null as Nullable<{
triggerPicker: () => void
}>
)
const mp = useMixpanel()
const trackFederateModels = () =>
mp.track('Viewer Action', {
type: 'action',
name: 'federation',
action: 'view-all',
source: 'model grid item'
})
const showActionsMenu = ref(false)
const itemType = computed<StructureItemType>(() => {
if (isPendingFileUpload(props.item)) return StructureItemType.PendingModel
const item = props.item
if (item.model?.versionCount.totalCount) {
if (hasChildren.value) {
return StructureItemType.ModelWithVersionsAndSubmodels
} else {
return StructureItemType.ModelWithOnlyVersions
}
} else {
if (hasChildren.value) {
return StructureItemType.ModelWithOnlySubmodels
} else {
return StructureItemType.EmptyModel
}
}
})
const hasVersions = computed(() =>
[
StructureItemType.ModelWithOnlyVersions,
StructureItemType.ModelWithVersionsAndSubmodels
].includes(itemType.value)
)
const hasSubmodels = computed(() =>
[
StructureItemType.ModelWithOnlySubmodels,
StructureItemType.ModelWithVersionsAndSubmodels
].includes(itemType.value)
)
const name = computed(() => {
if (isPendingFileUpload(props.item)) return props.item.modelName
return props.isSearchResult ? props.item.fullName : props.item.name
})
const fullName = computed(() =>
isPendingFileUpload(props.item) ? props.item.modelName : props.item.fullName
)
const expanded = useIsModelExpanded({
fullName,
projectId: computed(() => props.project.id)
})
const model = computed(() =>
!isPendingFileUpload(props.item) ? props.item.model : null
)
const pendingModel = computed(() =>
isPendingFileUpload(props.item) ? props.item : null
)
const pendingVersion = computed(() => model.value?.pendingImportedVersions[0])
const hasChildren = computed(() =>
props.isSearchResult || isPendingFileUpload(props.item)
? false
: props.item.hasChildren
)
const updatedAt = computed(() => {
const date = isPendingFileUpload(props.item)
? props.item.convertedLastUpdate || props.item.uploadDate
: props.item.updatedAt
return {
full: formattedFullDate(date),
relative: formattedRelativeDate(date, { prefix: true })
}
})
const modelLink = computed(() => {
if (
isPendingFileUpload(props.item) ||
!props.item.model ||
props.item.model?.versionCount.totalCount === 0
)
return null
return modelRoute(props.project.id, props.item.model.id)
})
const viewAllUrl = computed(() => {
if (isPendingFileUpload(props.item)) return undefined
return modelRoute(props.project.id, `$${props.item.fullName}`)
})
const {
result: childrenResult,
refetch: refetchChildren,
loading: childrenLoading
} = useQuery(
projectModelChildrenTreeQuery,
() => ({
projectId: props.project.id,
parentName: isPendingFileUpload(props.item) ? '' : props.item.fullName
}),
() => ({
enabled: hasChildren.value && expanded.value && !isPendingFileUpload(props.item)
})
)
const children = computed(() => childrenResult.value?.project?.modelChildrenTree || [])
const onModelUpdated = () => {
emit('model-updated')
refetchChildren()
}
const triggerVersionUpload = () => {
importArea.value?.triggerPicker()
}
const onVersionsClick = () => {
if (model.value) {
router.push(modelVersionsRoute(props.project.id, model.value.id))
}
}
</script>