Feat: personal projects messaging (#7)

* Remember the personal projects choose

* Remove unused workspace selector

* Messaging about personal projects

* Pushy on move personal project into workspace
This commit is contained in:
Oğuzhan Koral
2025-05-16 18:18:25 +03:00
committed by GitHub
parent 82c95aab58
commit 21a4bd4076
4 changed files with 155 additions and 118 deletions
+18 -5
View File
@@ -18,18 +18,24 @@
</div>
</div>
<div class="opacity-0 group-hover:opacity-100 transition flex">
<button
v-tippy="'Open project in browser'"
class="hover:text-primary flex items-center space-x-2 p-2"
<div
:class="
isPersonalProject ? '' : 'opacity-0 group-hover:opacity-100 transition flex'
"
>
<button
v-tippy="projectNavigatorTippy"
class="hover:text-primary flex items-center space-x-2 p-2 relative animate-pulse"
>
<div class="relative w-4 h-4">
<ArrowTopRightOnSquareIcon
class="w-4"
class="w-4 h-4"
@click.stop="
$openUrl(projectUrl),
trackEvent('DUI3 Action', { name: 'Project View' }, project.accountId)
"
/>
</div>
</button>
</div>
</button>
@@ -130,6 +136,13 @@ const projectAccount = computed(() =>
accountStore.accountWithFallback(props.project.accountId, props.project.serverUrl)
)
const isPersonalProject = computed(() => !projectDetails.value?.workspace)
const projectNavigatorTippy = computed(() =>
isPersonalProject.value
? 'Move personal project into a workspace'
: 'Open project in browser'
)
const clientId = projectAccount.value.accountInfo.id
const { result: projectDetailsResult, refetch: refetchProjectDetails } = useQuery(
@@ -0,0 +1,99 @@
<template>
<div>
<div
class="px-3 py-1 rounded-md shadow transition overflow-hidden bg-foundation border-foundation-2 hover:shadow-md border-1 group"
>
<div class="flex flex-col sm:flex-row sm:gap-2 text-foreground">
<div class="flex flex-col gap-4">
<div class="text-body-xs">
<h1
class="mb-1 text-sm font-semibold w-full inline-block py-1 bg-clip-text"
>
Move your projects to a workspace
</h1>
<p class="mb-2">
<span class="text-sm"></span>
<span class="text-xs">
We are making workspaces the default way to work in Speckle.
</span>
</p>
<p class="mb-1">
<span class="text-sm"></span>
<span class="text-xs">
Introducing
<FormButton
text
link
external
size="sm"
class="font-semibold"
@click="$openUrl(`https://www.speckle.systems/pricing`)"
>
new pricing
</FormButton>
including limits to the free plan.
</span>
</p>
<FormButton
text
color="primary"
size="sm"
:class="showMore ? `mb-1` : ``"
:icon-right="showMore ? ChevronUpIcon : ChevronDownIcon"
@click="showMore = !showMore"
>
{{ showMore ? 'Show less' : 'Show timeline' }}
</FormButton>
<div v-show="showMore">
<hr />
<h3 class="font-medium text-warning-darker my-1">By June 1st 2025</h3>
<p class="text-xs mb-1">Move your projects to a workspace to:</p>
<ul class="list-disc list-inside pl-2 mb-2">
<li>
<span class="text-xs font-medium">
Create new projects and models
</span>
<span class="text-xs">
(will be disabled for personal projects; existing projects and
models stay editable)
</span>
</li>
<li>
<span class="text-xs font-medium">
Invite new project collaborators
</span>
<span class="text-xs">
(new invites will be unavailable for personal projects)
</span>
</li>
<li>
<span class="text-xs font-medium">
Preserve version and comment history
</span>
<span class="text-xs">
(history is reduced to 7 days for personal projects)
</span>
</li>
</ul>
<h3 class="font-medium text-warning-darker">By Janury 1st 2026</h3>
<span class="text-xs mb-1">
All projects will be archived if not moved into a workspace. Don't
worry, we'll give you plenty of reminders before then.
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ChevronUpIcon, ChevronDownIcon } from '@heroicons/vue/20/solid'
const { $openUrl } = useNuxtApp()
const showMore = ref(false)
</script>
+31 -16
View File
@@ -6,6 +6,7 @@
class="flex items-center space-x-2 bg-foundation -mx-3 -mt-2 px-3 py-2 shadow-sm border-b"
>
<div class="flex-grow min-w-0">
<!-- NO WORKSPACE YET -->
<div v-if="workspaces.length === 0">
<FormButton
full-width
@@ -50,16 +51,6 @@
/>
</div>
</div>
<!-- we can message to user about the non-workspace scenario -->
<!-- <div v-if="workspaces && workspaces.length === 0">
<CommonAlert size="xs" :color="'warning'">
<template #description>
You are listing legacy personal projects which will be deprecated end of
2025. We suggest you to move your personal projects into a workspace before
then.
</template>
</CommonAlert>
</div> -->
<div class="space-y-2">
<div class="flex items-center space-x-1 justify-between">
<FormTextInput
@@ -110,6 +101,16 @@
</div>
</div>
</div>
<div v-if="isPersonalProjectsAsWorkspace">
<!-- <CommonAlert size="xs" :color="'warning'">
<template #description>
You are listing legacy personal projects which will be deprecated end of
2025. We suggest you to move your personal projects into a workspace
before then.
</template>
</CommonAlert> -->
<WizardPersonalProjectsWarning />
</div>
<CommonLoadingBar v-if="loading" loading />
</div>
<div class="grid grid-cols-1 gap-2 relative z-0">
@@ -255,15 +256,30 @@ const activeWorkspace = computed(() => {
return previouslySelectedWorkspace
}
}
// fallback to activeWorkspace query result
return activeWorkspaceResult.value?.activeUser
const activeWorkspace = activeWorkspaceResult.value?.activeUser
?.activeWorkspace as WorkspaceListWorkspaceItemFragment
// fallback to activeWorkspace query result
if (activeWorkspace) {
return activeWorkspace
}
// if activeWorkspace is null will mean that it is personal projects - this fallback wont be the case soon
return {
id: 'personalProject',
name: 'Personal Projects'
} as WorkspaceListWorkspaceItemFragment
})
const selectedWorkspace = ref<WorkspaceListWorkspaceItemFragment | undefined>(
activeWorkspace.value
)
const isPersonalProjectsAsWorkspace = computed(
() => selectedWorkspace.value?.id === 'personalProject'
)
watch(
workspaces,
(newItems) => {
@@ -332,12 +348,11 @@ const {
limit: 10, // stupid hack, increased it since we do manual filter to be able to see more project, see below TODO note, once we have `personalOnly` filter, decrease back to 10
filter: {
search: (searchText.value || '').trim() || null,
workspaceId:
selectedWorkspace.value?.id === 'personalProject'
workspaceId: isPersonalProjectsAsWorkspace.value
? null
: selectedWorkspace.value?.id,
includeImplicitAccess: true,
personalOnly: selectedWorkspace.value?.id === 'personalProject'
personalOnly: isPersonalProjectsAsWorkspace.value
}
}),
() => ({
@@ -349,7 +364,7 @@ const {
)
const projects = computed(() =>
selectedWorkspace.value?.id === 'personalProject' // TODO: we need to replace this logic with `personalOnly` filter when it is implemented into app.speckle.systems
isPersonalProjectsAsWorkspace.value // TODO: we need to replace this logic with `personalOnly` filter when it is implemented into app.speckle.systems
? projectsResult.value?.activeUser?.projects.items.filter(
(i) => i.workspaceId === null
)
-90
View File
@@ -1,90 +0,0 @@
<template>
<div>
<div class="text-body-2xs mb-2 ml-1">Project workspace</div>
<FormSelectBase
key="name"
v-model="selectedWorkspace"
clearable
label="Workspaces"
placeholder="Nothing selected"
name="Workspaces"
:items="workspaces"
:disabled-item-predicate="userCantCreateWorkspace"
mount-menu-on-body
>
<template #something-selected="{ value }">
<span>{{ isArray(value) ? value[0].name : value.name }}</span>
</template>
<template #option="{ item }">
<div
v-tippy="{
content: item.readOnly
? 'This workspace is read-only.'
: item.role === 'workspace:guest'
? 'You do not have write access on this workspace.'
: undefined,
disabled: !(item.readOnly || item.role === 'workspace:guest')
}"
class="flex items-center"
>
<span class="truncate">{{ item.name }}</span>
</div>
</template>
</FormSelectBase>
<div
v-if="selectedWorkspace"
class="text-body-sm caption rounded p-2 bg-blue-500/10 my-2"
>
Project will be created in the selected workspace.
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useQuery } from '@vue/apollo-composable'
import { workspacesListQuery } from '~/lib/graphql/mutationsAndQueries'
import type { WorkspaceListWorkspaceItemFragment } from '~/lib/common/generated/gql/graphql'
import { storeToRefs } from 'pinia'
import { useAccountStore } from '~/store/accounts'
import { isArray } from 'lodash-es'
const emit = defineEmits<{
(
e: 'update:selectedWorkspace',
value: WorkspaceListWorkspaceItemFragment | undefined
): void
}>()
const accountStore = useAccountStore()
const { activeAccount } = storeToRefs(accountStore)
const accountId = computed(() => activeAccount.value.accountInfo.id)
const searchText = ref<string>()
const { result: workspacesResult } = useQuery(
workspacesListQuery,
() => ({
limit: 5,
filter: {
search: (searchText.value || '').trim() || null
}
}),
() => ({
clientId: accountId.value,
debounce: 500,
fetchPolicy: 'network-only'
})
)
const workspaces = computed(() => workspacesResult.value?.activeUser?.workspaces.items)
const selectedWorkspace = ref<WorkspaceListWorkspaceItemFragment>()
watch(selectedWorkspace, (newVal) => {
emit('update:selectedWorkspace', newVal)
})
// Utility function to check if the user cannot create a workspace
const userCantCreateWorkspace = (item: WorkspaceListWorkspaceItemFragment) =>
(!!item?.role && item.role === 'workspace:guest') || !!item.readOnly
</script>