Files
speckle-server/packages/frontend-2/components/workspace/moveProject/Manager.vue
T
Kristaps Fabians Geikins 596312ab0e feat(frontend): personal project limit disclaimers & prompts (#4822)
* 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>
2025-05-28 12:12:18 +03:00

312 lines
7.7 KiB
Vue

<template>
<LayoutDialog
v-model:open="open"
max-width="sm"
:title="step.title"
:fullscreen="isSmallDialog ? 'none' : 'mobile'"
:hide-title="isSmallDialog"
:hide-buttons="!([DialogStepId.project, DialogStepId.workspace] as string[]).includes(step.id)"
:is-transparent="isSmallDialog"
:hide-closer="preventClose"
:prevent-close-on-click-outside="preventClose"
>
<!-- Intro -->
<WorkspaceMoveProjectIntro
v-if="step.id === DialogStepId.intro"
:project="selectedProject"
:limit-type="limitType"
@cancel="onCancel"
@continue="goToNextStep"
/>
<!-- Project Selection -->
<WorkspaceMoveProjectSelectProject
v-if="step.id === DialogStepId.project"
:workspace="workspaceResult?.workspaceBySlug"
:project-permissions="projectResult?.project.permissions.canMoveToWorkspace"
:workspace-id="workspaceId"
@project-selected="onProjectSelected"
/>
<!-- Workspace Selection -->
<WorkspaceMoveProjectSelectWorkspace
v-if="selectedProject && step.id === DialogStepId.workspace"
:project="selectedProject"
:checker="(w) => w.permissions.canMoveProjectToWorkspace"
@workspace-selected="onWorkspaceSelected"
/>
<!-- Confirmation (v-show cause if it unmounts, we wont get the move-complete event) -->
<WorkspaceMoveProjectConfirm
v-if="selectedProject && selectedWorkspace"
v-show="step.id === DialogStepId.confirmation"
:project="selectedProject"
:workspace="selectedWorkspace"
@move-complete="onMoveComplete"
@back="onBack"
/>
<template #buttons>
<div class="-my-1 w-full flex justify-end">
<FormButton
v-if="step.id === DialogStepId.project && !preventClose"
color="outline"
@click="onCancel"
>
Cancel
</FormButton>
<FormButton
v-else-if="step.id === DialogStepId.workspace"
color="outline"
full-width
@click="navigateTo(workspaceCreateRoute)"
>
Create a new workspace
</FormButton>
</div>
</template>
</LayoutDialog>
</template>
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import { graphql } from '~~/lib/common/generated/gql'
import type {
WorkspaceMoveProjectManager_ProjectFragment,
WorkspaceMoveProjectSelectWorkspace_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import {
workspaceMoveProjectManagerProjectQuery,
workspaceMoveProjectManagerWorkspaceQuery
} from '~/lib/workspaces/graphql/queries'
import { workspaceCreateRoute } from '~/lib/common/helpers/route'
import { useMultiStepDialog } from '~/lib/common/composables/dialog'
import type { ViewerLimitsDialogType } from '~/lib/projects/helpers/limits'
const DialogStepId = {
intro: 'intro',
project: 'project',
workspace: 'workspace',
confirmation: 'confirmation'
} as const
type DialogStepId = (typeof DialogStepId)[keyof typeof DialogStepId]
graphql(`
fragment WorkspaceMoveProjectManager_ProjectBase on Project {
id
name
modelCount: models(limit: 0) {
totalCount
}
versions(limit: 0) {
totalCount
}
}
`)
graphql(`
fragment WorkspaceMoveProjectManager_Project on Project {
...WorkspaceMoveProjectManager_ProjectBase
permissions {
canMoveToWorkspace(workspaceId: $workspaceId) {
...FullPermissionCheckResult
}
}
workspace {
id
slug
permissions {
canMoveProjectToWorkspace(projectId: $projectId) {
...FullPermissionCheckResult
}
}
}
}
`)
graphql(`
fragment WorkspaceMoveProjectManager_Workspace on Workspace {
id
role
name
logo
slug
plan {
name
usage {
projectCount
modelCount
}
}
permissions {
canMoveProjectToWorkspace(projectId: $projectId) {
...FullPermissionCheckResult
}
}
projects {
totalCount
}
team {
items {
user {
id
name
avatar
}
}
}
...WorkspaceMoveProjectSelectWorkspace_Workspace
}
`)
const emit = defineEmits<{
done: []
}>()
const props = defineProps<{
projectId?: string
workspaceSlug?: string
workspaceId?: string
showIntro?: boolean
limitType?: ViewerLimitsDialogType
}>()
const open = defineModel<boolean>('open', { required: true })
// Internal state management
const selectedProject = ref<WorkspaceMoveProjectManager_ProjectFragment | null>(null)
const selectedWorkspace =
ref<WorkspaceMoveProjectSelectWorkspace_WorkspaceFragment | null>(null)
const { goToPreviousStep, step, goToNextStep, resetStep } =
useMultiStepDialog<DialogStepId>({
steps: computed(() => [
...(props.showIntro
? [
{
id: DialogStepId.intro,
title: 'Move your projects to a workspace'
}
]
: []),
{
id: DialogStepId.project,
title: 'Choose project to move'
},
{
id: DialogStepId.workspace,
title: 'Choose workspace'
},
{
id: DialogStepId.confirmation,
title: 'Confirm move'
}
]),
resolveNextStep: ({ reset }) => {
if (props.showIntro && reset) {
return DialogStepId.intro
}
if (!selectedProject.value) {
return DialogStepId.project
}
if (!selectedWorkspace.value) {
return DialogStepId.workspace
}
return DialogStepId.confirmation
},
resolvePreviousStep: () => {
if (props.workspaceSlug) {
return DialogStepId.project
} else {
return DialogStepId.workspace
}
}
})
// Fetch project data if provided
const { result: projectResult, onResult: onProjectResult } = useQuery(
workspaceMoveProjectManagerProjectQuery,
() => ({
projectId: props.projectId || '',
workspaceId: props.workspaceId
}),
() => ({
enabled: !!props.projectId
})
)
// Fetch workspace data if provided
const { result: workspaceResult, onResult: onWorkspaceResult } = useQuery(
workspaceMoveProjectManagerWorkspaceQuery,
() => ({
workspaceSlug: props.workspaceSlug || '',
projectId: props.projectId
}),
() => ({
enabled: !!props.workspaceSlug
})
)
const isSmallDialog = computed(() => step.value.id === DialogStepId.intro)
const preventClose = computed(() => !!props.limitType)
onProjectResult((res) => {
if (res.data?.project?.id !== selectedProject.value?.id) {
selectedProject.value = res.data.project
resetStep()
}
})
onWorkspaceResult((res) => {
if (res.data?.workspaceBySlug?.id !== selectedWorkspace.value?.id) {
resetStep()
}
})
watch(open, (newVal, oldVal) => {
if (newVal && !oldVal) {
if (workspaceResult.value?.workspaceBySlug) {
selectedWorkspace.value = workspaceResult.value.workspaceBySlug
}
if (projectResult.value?.project) {
selectedProject.value = projectResult.value.project
}
resetStep()
}
})
const onProjectSelected = (project: WorkspaceMoveProjectManager_ProjectFragment) => {
selectedProject.value = project
// If we already have a workspace (from props), go straight to confirmation
if (props.workspaceSlug && workspaceResult.value?.workspaceBySlug) {
selectedWorkspace.value = workspaceResult.value.workspaceBySlug
}
goToNextStep()
}
const onWorkspaceSelected = (
workspace: WorkspaceMoveProjectSelectWorkspace_WorkspaceFragment
) => {
selectedWorkspace.value = workspace
goToNextStep()
}
const onMoveComplete = () => {
emit('done')
selectedProject.value = null
selectedWorkspace.value = null
open.value = false
}
const onBack = () => {
goToPreviousStep()
}
const onCancel = () => {
open.value = false
selectedProject.value = null
selectedWorkspace.value = null
}
</script>