596312ab0e
* ProjectsAdd wrapper * WorkspaceMoveProject wrapper added * move wrapper finalized * passing through location * more cleanup * model add wrapper * permissions cleanup * add invite wrapper * vue-tippy bugfix * ViewerLimitsDialog prep * upgrade limit alert prep * limit alerts * movemanager fix * new add flow * slug update fix * add model flow * invites? * some extra fixes * move unmount fix? * more fixes * vue-tsc update * style: remove h-32 for smaller screens * vue-tsc parser fix * prep for new viewer limits dialog * updated viewer dialogs * comment variant cleanup * CR comments --------- Co-authored-by: michalspeckle <michal@speckle.systems>
269 lines
7.6 KiB
Vue
269 lines
7.6 KiB
Vue
<template>
|
|
<div>
|
|
<div class="flex flex-col-reverse md:justify-between md:flex-row md:gap-x-4">
|
|
<div class="relative w-full md:max-w-md mt-6 md:mt-0">
|
|
<FormTextInput
|
|
name="search"
|
|
:custom-icon="MagnifyingGlassIcon"
|
|
color="foundation"
|
|
search
|
|
placeholder="Search projects"
|
|
v-bind="bind"
|
|
v-on="on"
|
|
/>
|
|
</div>
|
|
<div v-tippy="createDisabledTooltip">
|
|
<FormButton :disabled="isCreateDisabled" @click="openNewProject = true">
|
|
Create
|
|
</FormButton>
|
|
</div>
|
|
</div>
|
|
|
|
<LayoutTable
|
|
class="mt-6"
|
|
:columns="[
|
|
{ id: 'name', header: 'Name', classes: 'col-span-3 truncate' },
|
|
{ id: 'created', header: 'Created', classes: 'col-span-2' },
|
|
{ id: 'modified', header: 'Modified', classes: 'col-span-2' },
|
|
{ id: 'models', header: 'Models', classes: 'col-span-1' },
|
|
{ id: 'versions', header: 'Versions', classes: 'col-span-1' },
|
|
{ id: 'contributors', header: 'Project members', classes: 'col-span-2 pr-8' },
|
|
{ id: 'actions', header: '', classes: 'absolute right-2 top-0.5' }
|
|
]"
|
|
:items="projects"
|
|
>
|
|
<template #name="{ item }">
|
|
<NuxtLink :to="projectRoute(item.id)">
|
|
{{ isProject(item) ? item.name : '' }}
|
|
</NuxtLink>
|
|
</template>
|
|
|
|
<template #created="{ item }">
|
|
<div class="text-xs">
|
|
{{ formattedFullDate(item.createdAt) }}
|
|
</div>
|
|
</template>
|
|
|
|
<template #modified="{ item }">
|
|
<div class="text-xs">
|
|
{{ formattedFullDate(item.updatedAt) }}
|
|
</div>
|
|
</template>
|
|
|
|
<template #models="{ item }">
|
|
<div class="text-xs">
|
|
{{ isProject(item) ? item.models.totalCount : '' }}
|
|
</div>
|
|
</template>
|
|
|
|
<template #versions="{ item }">
|
|
<div class="text-xs">
|
|
{{ isProject(item) ? item.versions.totalCount : '' }}
|
|
</div>
|
|
</template>
|
|
|
|
<template #contributors="{ item }">
|
|
<div v-if="isProject(item)">
|
|
<UserAvatarGroup :users="item.team.map((t) => t.user)" :max-count="3" />
|
|
</div>
|
|
</template>
|
|
|
|
<template #actions="{ item }">
|
|
<LayoutMenu
|
|
v-model:open="showActionsMenu[item.id]"
|
|
:items="actionItems[item.id]"
|
|
mount-menu-on-body
|
|
:menu-position="HorizontalDirection.Left"
|
|
:menu-id="menuId"
|
|
@chosen="({ item: actionItem }) => onActionChosen(actionItem, item)"
|
|
>
|
|
<FormButton
|
|
:color="showActionsMenu[item.id] ? 'outline' : 'subtle'"
|
|
hide-text
|
|
:icon-right="showActionsMenu[item.id] ? XMarkIcon : EllipsisHorizontalIcon"
|
|
@click.stop="toggleMenu(item.id)"
|
|
/>
|
|
</LayoutMenu>
|
|
</template>
|
|
</LayoutTable>
|
|
|
|
<ProjectsDeleteDialog
|
|
v-if="projectToModify"
|
|
v-model:open="showProjectDeleteDialog"
|
|
:project="projectToModify"
|
|
/>
|
|
|
|
<ProjectsAdd v-model:open="openNewProject" :workspace="workspace" />
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { HorizontalDirection } from '~~/lib/common/composables/window'
|
|
import type {
|
|
SettingsSharedProjects_ProjectFragment,
|
|
ProjectsDeleteDialog_ProjectFragment,
|
|
SettingsSharedProjects_WorkspaceFragment
|
|
} from '~~/lib/common/generated/gql/graphql'
|
|
import {
|
|
MagnifyingGlassIcon,
|
|
EllipsisHorizontalIcon,
|
|
XMarkIcon
|
|
} from '@heroicons/vue/24/outline'
|
|
import { isProject } from '~~/lib/server-management/helpers/utils'
|
|
import { useDebouncedTextInput, type LayoutMenuItem } from '@speckle/ui-components'
|
|
import { graphql } from '~/lib/common/generated/gql'
|
|
import { useRouter } from 'vue-router'
|
|
import { projectRoute } from '~/lib/common/helpers/route'
|
|
import { useCanCreatePersonalProject } from '~/lib/projects/composables/permissions'
|
|
import { useCanCreateWorkspaceProject } from '~/lib/workspaces/composables/projects/permissions'
|
|
import type { MaybeNullOrUndefined } from '@speckle/shared'
|
|
|
|
graphql(`
|
|
fragment SettingsSharedProjects_Project on Project {
|
|
...ProjectsDeleteDialog_Project
|
|
id
|
|
name
|
|
visibility
|
|
createdAt
|
|
updatedAt
|
|
models(limit: 0) {
|
|
totalCount
|
|
}
|
|
versions(limit: 0) {
|
|
totalCount
|
|
}
|
|
team {
|
|
id
|
|
user {
|
|
name
|
|
id
|
|
avatar
|
|
}
|
|
}
|
|
permissions {
|
|
canDelete {
|
|
...FullPermissionCheckResult
|
|
}
|
|
canReadSettings {
|
|
...FullPermissionCheckResult
|
|
}
|
|
canRead {
|
|
...FullPermissionCheckResult
|
|
}
|
|
}
|
|
}
|
|
`)
|
|
|
|
graphql(`
|
|
fragment SettingsSharedProjects_Workspace on Workspace {
|
|
id
|
|
...ProjectsAdd_Workspace
|
|
}
|
|
`)
|
|
|
|
const props = defineProps<{
|
|
workspaceId: MaybeNullOrUndefined<string>
|
|
projects: MaybeNullOrUndefined<SettingsSharedProjects_ProjectFragment[]>
|
|
workspace: MaybeNullOrUndefined<SettingsSharedProjects_WorkspaceFragment>
|
|
}>()
|
|
|
|
const { activeUser } = useActiveUser()
|
|
const canCreatePersonal = useCanCreatePersonalProject({
|
|
activeUser: computed(() => activeUser.value)
|
|
})
|
|
|
|
const canCreateWorkspace = useCanCreateWorkspaceProject({
|
|
workspace: computed(() => props.workspace)
|
|
})
|
|
|
|
const isCreateDisabled = computed(() => {
|
|
if (props.workspaceId) {
|
|
return !canCreateWorkspace.canClickCreate.value
|
|
}
|
|
|
|
return !canCreatePersonal.canClickCreate.value
|
|
})
|
|
const createDisabledTooltip = computed(() => {
|
|
if (props.workspaceId) {
|
|
return canCreateWorkspace.cantClickCreateReason.value
|
|
}
|
|
|
|
return canCreatePersonal.cantClickCreateReason.value
|
|
})
|
|
|
|
const search = defineModel<string>('search')
|
|
const { on, bind } = useDebouncedTextInput({ model: search })
|
|
const router = useRouter()
|
|
const menuId = useId()
|
|
|
|
const projectToModify = ref<ProjectsDeleteDialog_ProjectFragment | null>(null)
|
|
const showProjectDeleteDialog = ref(false)
|
|
const openNewProject = ref(false)
|
|
|
|
const openProjectDeleteDialog = (item: ProjectsDeleteDialog_ProjectFragment) => {
|
|
projectToModify.value = item
|
|
showProjectDeleteDialog.value = true
|
|
}
|
|
|
|
const handleProjectClick = (id: string) => {
|
|
router.push(projectRoute(id))
|
|
}
|
|
|
|
enum ActionTypes {
|
|
ViewProject = 'view-project',
|
|
EditMembers = 'edit-members',
|
|
DeleteProject = 'delete-project'
|
|
}
|
|
|
|
const showActionsMenu = ref<Record<string, boolean>>({})
|
|
|
|
const actionItems = computed((): { [projectId: string]: LayoutMenuItem[][] } =>
|
|
(props.projects || []).reduce((ret, project) => {
|
|
const canRead = project.permissions.canRead
|
|
const canDelete = project.permissions.canDelete
|
|
const canReadSettings = project.permissions.canReadSettings
|
|
|
|
ret[project.id] = [
|
|
[
|
|
{
|
|
title: 'View project',
|
|
id: ActionTypes.ViewProject,
|
|
disabled: !canRead?.authorized,
|
|
disabledTooltip: canRead?.message
|
|
},
|
|
{
|
|
title: 'Edit members',
|
|
id: ActionTypes.EditMembers,
|
|
disabled: !canReadSettings?.authorized,
|
|
disabledTooltip: canReadSettings?.message
|
|
},
|
|
{
|
|
title: 'Delete project...',
|
|
id: ActionTypes.DeleteProject,
|
|
disabled: !canDelete?.authorized,
|
|
disabledTooltip: canDelete?.message
|
|
}
|
|
]
|
|
]
|
|
return ret
|
|
}, {} as { [projectId: string]: LayoutMenuItem[][] })
|
|
)
|
|
|
|
const onActionChosen = (
|
|
actionItem: LayoutMenuItem,
|
|
project: ProjectsDeleteDialog_ProjectFragment
|
|
) => {
|
|
if (actionItem.id === ActionTypes.EditMembers) {
|
|
router.push(projectRoute(project.id, 'collaborators'))
|
|
} else if (actionItem.id === ActionTypes.ViewProject) {
|
|
handleProjectClick(project.id)
|
|
} else if (actionItem.id === ActionTypes.DeleteProject) {
|
|
openProjectDeleteDialog(project)
|
|
}
|
|
}
|
|
|
|
const toggleMenu = (itemId: string) => {
|
|
showActionsMenu.value[itemId] = !showActionsMenu.value[itemId]
|
|
}
|
|
</script>
|