c771fc8553
* Update Nav Link Font weight. Nowrap badge * Plus spacing * Workspace page tidy up * Mobile testing * Responsive fixes * Fix clipping of avatar * Adjust workspace item pl * Square the plus button * Font overwrites for workspace items in settings * Remove unused props
259 lines
7.3 KiB
Vue
259 lines
7.3 KiB
Vue
<template>
|
|
<div>
|
|
<Portal to="primary-actions"></Portal>
|
|
<ProjectsDashboardHeader
|
|
:projects-invites="projectsPanelResult?.activeUser || undefined"
|
|
:workspaces-invites="workspacesResult?.activeUser || undefined"
|
|
/>
|
|
|
|
<div v-if="!showEmptyState" class="flex flex-col gap-4">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<Squares2X2Icon class="h-5 w-5" />
|
|
<h1 class="text-heading-lg">Projects</h1>
|
|
</div>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-2 sm:items-center justify-between">
|
|
<div class="flex flex-col sm:flex-row gap-2">
|
|
<FormTextInput
|
|
name="modelsearch"
|
|
:show-label="false"
|
|
placeholder="Search..."
|
|
:custom-icon="MagnifyingGlassIcon"
|
|
color="foundation"
|
|
wrapper-classes="grow md:grow-0 md:w-60"
|
|
:show-clear="!!search"
|
|
v-bind="bind"
|
|
v-on="on"
|
|
></FormTextInput>
|
|
<FormSelectProjectRoles
|
|
v-if="!showEmptyState"
|
|
v-model="selectedRoles"
|
|
class="md:w-56 grow md:grow-0"
|
|
fixed-height
|
|
/>
|
|
</div>
|
|
<FormButton v-if="!isGuest" @click="openNewProject = true">
|
|
New project
|
|
</FormButton>
|
|
</div>
|
|
</div>
|
|
<CommonLoadingBar :loading="showLoadingBar" class="my-2" />
|
|
<ProjectsDashboardEmptyState
|
|
v-if="showEmptyState"
|
|
:is-guest="isGuest"
|
|
@create-project="openNewProject = true"
|
|
/>
|
|
<template v-else-if="projects?.items?.length">
|
|
<ProjectsDashboardFilled :projects="projects" show-workspace-link />
|
|
<InfiniteLoading
|
|
:settings="{ identifier: infiniteLoaderId }"
|
|
@infinite="infiniteLoad"
|
|
/>
|
|
</template>
|
|
<CommonEmptySearchState v-else-if="!showLoadingBar" @clear-search="clearSearch" />
|
|
<ProjectsAddDialog v-model:open="openNewProject" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
useApolloClient,
|
|
useQuery,
|
|
useQueryLoading,
|
|
useSubscription
|
|
} from '@vue/apollo-composable'
|
|
import {
|
|
projectsDashboardQuery,
|
|
projectsDashboardWorkspaceQuery
|
|
} from '~~/lib/projects/graphql/queries'
|
|
import { graphql } from '~~/lib/common/generated/gql'
|
|
import {
|
|
getCacheId,
|
|
evictObjectFields,
|
|
modifyObjectFields
|
|
} from '~~/lib/common/helpers/graphql'
|
|
import type { User, UserProjectsArgs } from '~~/lib/common/generated/gql/graphql'
|
|
import { UserProjectsUpdatedMessageType } from '~~/lib/common/generated/gql/graphql'
|
|
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
|
|
import { projectRoute } from '~~/lib/common/helpers/route'
|
|
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
|
|
import type { Nullable, Optional, StreamRoles } from '@speckle/shared'
|
|
import { useDebouncedTextInput, type InfiniteLoaderState } from '@speckle/ui-components'
|
|
import { MagnifyingGlassIcon, Squares2X2Icon } from '@heroicons/vue/24/outline'
|
|
|
|
const logger = useLogger()
|
|
|
|
const infiniteLoaderId = ref('')
|
|
const cursor = ref(null as Nullable<string>)
|
|
const selectedRoles = ref(undefined as Optional<StreamRoles[]>)
|
|
const openNewProject = ref(false)
|
|
const showLoadingBar = ref(false)
|
|
const { activeUser, isGuest } = useActiveUser()
|
|
const { triggerNotification } = useGlobalToast()
|
|
const areQueriesLoading = useQueryLoading()
|
|
const apollo = useApolloClient().client
|
|
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
|
|
|
const {
|
|
on,
|
|
bind,
|
|
value: search
|
|
} = useDebouncedTextInput({
|
|
debouncedBy: 800
|
|
})
|
|
|
|
const {
|
|
result: projectsPanelResult,
|
|
fetchMore: fetchMoreProjects,
|
|
onResult: onProjectsResult,
|
|
variables: projectsVariables
|
|
} = useQuery(projectsDashboardQuery, () => ({
|
|
filter: {
|
|
search: (search.value || '').trim() || null,
|
|
onlyWithRoles: selectedRoles.value?.length ? selectedRoles.value : null
|
|
}
|
|
}))
|
|
|
|
const { result: workspacesResult } = useQuery(
|
|
projectsDashboardWorkspaceQuery,
|
|
undefined,
|
|
() => ({
|
|
enabled: isWorkspacesEnabled.value
|
|
})
|
|
)
|
|
|
|
onProjectsResult((res) => {
|
|
cursor.value = res.data?.activeUser?.projects.cursor || null
|
|
infiniteLoaderId.value = JSON.stringify(projectsVariables.value?.filter || {})
|
|
})
|
|
|
|
const { onResult: onUserProjectsUpdate } = useSubscription(
|
|
graphql(`
|
|
subscription OnUserProjectsUpdate {
|
|
userProjectsUpdated {
|
|
type
|
|
id
|
|
project {
|
|
...ProjectDashboardItem
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
)
|
|
|
|
const projects = computed(() => projectsPanelResult.value?.activeUser?.projects)
|
|
const showEmptyState = computed(() => {
|
|
const isFiltering =
|
|
projectsVariables.value?.filter?.onlyWithRoles?.length ||
|
|
projectsVariables.value?.filter?.search?.length
|
|
if (isFiltering) return false
|
|
|
|
return projects.value && !projects.value.items.length
|
|
})
|
|
|
|
const moreToLoad = computed(
|
|
() =>
|
|
(!projects.value || projects.value.items.length < projects.value.totalCount) &&
|
|
cursor.value
|
|
)
|
|
|
|
onUserProjectsUpdate((res) => {
|
|
const activeUserId = activeUser.value?.id
|
|
const event = res.data?.userProjectsUpdated
|
|
|
|
if (!event) return
|
|
if (!activeUserId) return
|
|
|
|
const isNewProject = event.type === UserProjectsUpdatedMessageType.Added
|
|
const incomingProject = event.project
|
|
const cache = apollo.cache
|
|
|
|
if (isNewProject && incomingProject) {
|
|
// Add to User.projects where possible
|
|
modifyObjectFields<UserProjectsArgs, User['projects']>(
|
|
cache,
|
|
getCacheId('User', activeUserId),
|
|
(fieldName, variables, value, { ref }) => {
|
|
if (fieldName !== 'projects') return
|
|
if (variables.filter?.search?.length) return
|
|
if (variables.filter?.onlyWithRoles?.length) {
|
|
const roles = variables.filter.onlyWithRoles
|
|
if (!roles.includes(incomingProject.role || '')) return
|
|
}
|
|
|
|
return {
|
|
...value,
|
|
items: [ref('Project', incomingProject.id), ...(value.items || [])],
|
|
totalCount: (value.totalCount || 0) + 1
|
|
}
|
|
}
|
|
)
|
|
|
|
// Elsewhere - just evict fields directly
|
|
evictObjectFields<UserProjectsArgs, User['projects']>(
|
|
cache,
|
|
getCacheId('User', activeUserId),
|
|
(fieldName, variables) => {
|
|
if (fieldName !== 'projects') return false
|
|
if (variables.filter?.search?.length) return true
|
|
|
|
return false
|
|
}
|
|
)
|
|
}
|
|
|
|
if (!isNewProject) {
|
|
// Evict old project from cache entirely to remove it from all searches
|
|
cache.evict({
|
|
id: getCacheId('Project', event.id)
|
|
})
|
|
}
|
|
|
|
// Emit toast notification
|
|
triggerNotification({
|
|
type: ToastNotificationType.Info,
|
|
title: isNewProject ? 'New project added' : 'A project has been removed',
|
|
cta:
|
|
isNewProject && incomingProject
|
|
? {
|
|
url: projectRoute(incomingProject.id),
|
|
title: 'View project'
|
|
}
|
|
: undefined
|
|
})
|
|
})
|
|
|
|
const infiniteLoad = async (state: InfiniteLoaderState) => {
|
|
if (!moreToLoad.value) return state.complete()
|
|
|
|
try {
|
|
await fetchMoreProjects({
|
|
variables: {
|
|
cursor: cursor.value
|
|
}
|
|
})
|
|
} catch (e) {
|
|
logger.error(e)
|
|
state.error()
|
|
return
|
|
}
|
|
|
|
state.loaded()
|
|
if (!moreToLoad.value) {
|
|
state.complete()
|
|
}
|
|
}
|
|
|
|
watch(search, (newVal) => {
|
|
if (newVal) showLoadingBar.value = true
|
|
else showLoadingBar.value = false
|
|
})
|
|
|
|
watch(areQueriesLoading, (newVal) => (showLoadingBar.value = newVal))
|
|
|
|
const clearSearch = () => {
|
|
search.value = ''
|
|
selectedRoles.value = []
|
|
}
|
|
</script>
|