Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(workspaces): create workspace button #2645

Merged
merged 17 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion packages/frontend-2/components/dashboard/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@
</template>
</LayoutSidebarMenuGroupItem>
</NuxtLink>
<LayoutSidebarMenuGroupItem
v-if="isUserAdmin"
label="Add workspace"
@click="showWorkspaceCreateDialog = true"
>
<template #icon>
<PlusIcon class="h-4 w-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</LayoutSidebarMenuGroup>

<LayoutSidebarMenuGroup title="Resources">
Expand Down Expand Up @@ -130,6 +139,8 @@
</LayoutSidebar>
</div>
</template>

<WorkspaceCreateDialog v-model:open="showWorkspaceCreateDialog" />
</div>
</template>
<script setup lang="ts">
Expand All @@ -140,7 +151,8 @@ import {
GlobeAltIcon,
ClockIcon,
Squares2X2Icon,
HomeIcon
HomeIcon,
PlusIcon
} from '@heroicons/vue/24/outline'
import {
FormButton,
Expand All @@ -158,12 +170,15 @@ import {
connectorsPageUrl
} from '~/lib/common/helpers/route'
import { useRoute } from 'vue-router'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'

const { isLoggedIn } = useActiveUser()
const isWorkspacesEnabled = useIsWorkspacesEnabled()
const route = useRoute()
const { activeUser: user } = useActiveUser()

const isOpenMobile = ref(false)
const showWorkspaceCreateDialog = ref(false)

const { result: workspaceResult } = useQuery(settingsSidebarQuery, null, {
enabled: isWorkspacesEnabled.value
Expand All @@ -173,6 +188,7 @@ const isActive = (...routes: string[]): boolean => {
return routes.some((routeTo) => route.path === routeTo)
}

const isUserAdmin = computed(() => user.value?.role === 'server:admin')
const workspacesItems = computed(() =>
workspaceResult.value?.activeUser
? workspaceResult.value.activeUser.workspaces.items.map((workspace) => ({
Expand Down
21 changes: 15 additions & 6 deletions packages/frontend-2/components/settings/Dialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,7 @@
@click="targetMenuItem = `${key}`"
/>
</LayoutSidebarMenuGroup>
<LayoutSidebarMenuGroup
v-if="isWorkspacesEnabled && hasWorkspaceItems"
title="Workspace settings"
>
<LayoutSidebarMenuGroup v-if="isWorkspacesEnabled" title="Workspace settings">
<template #title-icon>
<ServerStackIcon class="h-5 w-5" />
</template>
Expand All @@ -50,6 +47,7 @@
:key="key"
:title="workspaceItem.name"
collapsible
:collapsed="true"
>
<template #title-icon>
<WorkspaceAvatar
Expand Down Expand Up @@ -81,6 +79,15 @@
"
/>
</LayoutSidebarMenuGroup>
<LayoutSidebarMenuGroupItem
v-if="isAdmin"
label="Add workspace"
@click="showWorkspaceCreateDialog = true"
>
<template #icon>
<PlusIcon class="h-4 w-4 text-foreground-2" />
</template>
</LayoutSidebarMenuGroupItem>
</LayoutSidebarMenuGroup>
</LayoutSidebarMenu>
</LayoutSidebar>
Expand All @@ -96,6 +103,8 @@
@close="isOpen = false"
/>
</div>

<WorkspaceCreateDialog v-model:open="showWorkspaceCreateDialog" />
</LayoutDialog>
</template>

Expand All @@ -106,7 +115,7 @@ import { useQuery } from '@vue/apollo-composable'
import { settingsSidebarQuery } from '~/lib/settings/graphql/queries'
import { useBreakpoints } from '@vueuse/core'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
import { UserIcon, ServerStackIcon } from '@heroicons/vue/24/outline'
import { UserIcon, ServerStackIcon, PlusIcon } from '@heroicons/vue/24/outline'
import { useActiveUser } from '~/lib/auth/composables/activeUser'
import { useSettingsMenu } from '~/lib/settings/composables/menu'
import {
Expand Down Expand Up @@ -144,11 +153,11 @@ const { result: workspaceResult } = useQuery(settingsSidebarQuery, null, {

const isMobile = breakpoints.smaller('md')
const targetWorkspaceId = ref<string | null>(null)
const showWorkspaceCreateDialog = ref(false)

const workspaceItems = computed(
() => workspaceResult.value?.activeUser?.workspaces.items ?? []
)
const hasWorkspaceItems = computed(() => workspaceItems.value.length > 0)
const isAdmin = computed(() => user.value?.role === Roles.Server.Admin)
const selectedMenuItem = computed((): SettingsMenuItem | null => {
const categories = [
Expand Down
13 changes: 3 additions & 10 deletions packages/frontend-2/components/settings/workspaces/General.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,8 @@
color="foundation"
label="Name"
name="name"
placeholder="Example Ltd."
placeholder="Workspace name"
show-label
:disabled="!isAdmin"
:rules="[isRequired, isStringOfLength({ maxLength: 512 })]"
validate-on-value-update
@change="save()"
Expand All @@ -30,9 +29,8 @@
color="foundation"
label="Description"
name="description"
placeholder="This is your workspace"
placeholder="Workspace description"
show-label
:disabled="!isAdmin"
:rules="[isStringOfLength({ maxLength: 512 })]"
@change="save()"
/>
Expand All @@ -48,7 +46,7 @@
ask you to type in your workspace name and press the delete button.
</div>
<div>
<FormButton :disabled="!isAdmin" color="danger" @click="openDeleteDialog">
<FormButton color="danger" @click="openDeleteDialog">
Delete workspace
</FormButton>
</div>
Expand All @@ -65,8 +63,6 @@
<script setup lang="ts">
import { graphql } from '~~/lib/common/generated/gql'
import { useForm } from 'vee-validate'
import { useActiveUser } from '~/lib/auth/composables/activeUser'
import { Roles } from '@speckle/shared'
import { useQuery, useMutation } from '@vue/apollo-composable'
import { settingsUpdateWorkspaceMutation } from '~/lib/settings/graphql/mutations'
import { settingsWorkspaceGeneralQuery } from '~/lib/settings/graphql/queries'
Expand Down Expand Up @@ -95,7 +91,6 @@ const props = defineProps<{
workspaceId: string
}>()

const { activeUser: user } = useActiveUser()
const { handleSubmit } = useForm<FormValues>()
const { triggerNotification } = useGlobalToast()
const { mutate: updateMutation } = useMutation(settingsUpdateWorkspaceMutation)
Expand All @@ -107,8 +102,6 @@ const name = ref('')
const description = ref('')
const showDeleteDialog = ref(false)

const isAdmin = computed(() => user.value?.role === Roles.Server.Admin)

const save = handleSubmit(async () => {
if (!workspaceResult.value?.workspace) return

Expand Down
80 changes: 80 additions & 0 deletions packages/frontend-2/components/workspace/CreateDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<template>
<LayoutDialog
v-model:open="isOpen"
max-width="sm"
hide-closer
:buttons="dialogButtons"
title="Create workspace"
>
<div class="flex flex-col gap-4">
<FormTextInput
v-model:model-value="workspaceName"
name="name"
label="Name"
placeholder="Workspace name"
color="foundation"
:rules="[isRequired, isStringOfLength({ maxLength: 512 })]"
show-label
show-required
/>
<FormTextInput
v-model:model-value="workspaceDescription"
name="description"
label="Description"
placeholder="Workspace description"
:rules="[isStringOfLength({ maxLength: 512 })]"
color="foundation"
show-label
/>
</div>
</LayoutDialog>
</template>

<script setup lang="ts">
import { useForm } from 'vee-validate'
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useCreateWorkspace } from '~/lib/workspaces/composables/management'
import { useWorkspacesAvatar } from '~/lib/workspaces/composables/avatar'
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'

type FormValues = { name: string; description: string }

const isOpen = defineModel<boolean>('open', { required: true })

const createWorkspace = useCreateWorkspace()
const { generateDefaultLogoIndex } = useWorkspacesAvatar()
const { handleSubmit } = useForm<FormValues>()

const workspaceName = ref<string>('')
const workspaceDescription = ref<string>('')

const dialogButtons = computed((): LayoutDialogButton[] => [
{
text: 'Cancel',
props: { color: 'outline', fullWidth: true },
onClick: () => {
isOpen.value = false
}
},
{
text: 'Create',
props: {
fullWidth: true,
color: 'primary'
},
onClick: handleCreateWorkspace
}
])

const handleCreateWorkspace = handleSubmit(async () => {
const newWorkspace = await createWorkspace({
name: workspaceName.value,
description: workspaceDescription.value,
defaultLogoIndex: generateDefaultLogoIndex()
})

if (newWorkspace) {
isOpen.value = false
}
})
</script>
12 changes: 7 additions & 5 deletions packages/frontend-2/components/workspace/header/Header.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
<template>
<div class="flex flex-col sm:flex-row justify-between sm:items-center">
<div class="flex gap-2 mb-3 mt-2">
<WorkspaceAvatar
:logo="workspaceInfo.logo"
:default-logo-index="workspaceInfo.defaultLogoIndex"
size="sm"
/>
<div class="flex items-center">
<WorkspaceAvatar
:logo="workspaceInfo.logo"
:default-logo-index="workspaceInfo.defaultLogoIndex"
size="lg"
/>
</div>
<div class="flex flex-col">
<h1 class="text-heading-lg">{{ workspaceInfo.name }}</h1>
<div class="text-body-xs text-foreground-2">
Expand Down
5 changes: 5 additions & 0 deletions packages/frontend-2/lib/common/generated/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ const documents = {
"\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n user {\n id\n }\n }\n": types.UseWorkspaceInviteManager_PendingWorkspaceCollaboratorFragmentDoc,
"\n mutation UpdateRole($input: WorkspaceRoleUpdateInput!) {\n workspaceMutations {\n updateRole(input: $input) {\n id\n team {\n id\n role\n }\n }\n }\n }\n": types.UpdateRoleDocument,
"\n mutation InviteToWorkspace(\n $workspaceId: String!\n $input: [WorkspaceInviteCreateInput!]!\n ) {\n workspaceMutations {\n invites {\n batchCreate(workspaceId: $workspaceId, input: $input) {\n id\n invitedTeam {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n }\n }\n }\n": types.InviteToWorkspaceDocument,
"\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n }\n }\n }\n": types.CreateWorkspaceDocument,
"\n mutation ProcessWorkspaceInvite($input: WorkspaceInviteUseInput!) {\n workspaceMutations {\n invites {\n use(input: $input)\n }\n }\n }\n": types.ProcessWorkspaceInviteDocument,
"\n query WorkspaceAccessCheck($id: String!) {\n workspace(id: $id) {\n id\n }\n }\n": types.WorkspaceAccessCheckDocument,
"\n query WorkspacePageQuery(\n $workspaceId: String!\n $filter: WorkspaceProjectsFilter\n $cursor: String\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspace(id: $workspaceId) {\n id\n ...WorkspaceHeader_Workspace\n projects(filter: $filter, cursor: $cursor, limit: 10) {\n ...WorkspaceProjectList_ProjectCollection\n }\n }\n }\n": types.WorkspacePageQueryDocument,
Expand Down Expand Up @@ -1391,6 +1392,10 @@ export function graphql(source: "\n mutation UpdateRole($input: WorkspaceRoleUp
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation InviteToWorkspace(\n $workspaceId: String!\n $input: [WorkspaceInviteCreateInput!]!\n ) {\n workspaceMutations {\n invites {\n batchCreate(workspaceId: $workspaceId, input: $input) {\n id\n invitedTeam {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n }\n }\n }\n"): (typeof documents)["\n mutation InviteToWorkspace(\n $workspaceId: String!\n $input: [WorkspaceInviteCreateInput!]!\n ) {\n workspaceMutations {\n invites {\n batchCreate(workspaceId: $workspaceId, input: $input) {\n id\n invitedTeam {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n }\n }\n }\n"): (typeof documents)["\n mutation CreateWorkspace($input: WorkspaceCreateInput!) {\n workspaceMutations {\n create(input: $input) {\n id\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
Expand Down
Loading