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:
@@ -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>
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user