Feat: Add role label, version count, and update styling of project header (#2820)

This commit is contained in:
Mike
2024-08-30 12:44:51 +02:00
committed by GitHub
parent 80aa0aa20b
commit e62a68cee7
10 changed files with 166 additions and 141 deletions
@@ -1,9 +1,8 @@
<template>
<div class="flex flex-col gap-1">
<h2 class="text-heading-xl">{{ title }}</h2>
<p v-if="description" class="text-body-sm text-foreground-2">{{ description }}</p>
<p v-else class="text-body-sm text-foreground-2 italic select-none">
No description
<div class="flex flex-col">
<h2 class="text-heading">{{ title }}</h2>
<p class="text-body-sm text-foreground-2">
{{ description ? description : 'No description' }}
</p>
</div>
</template>
@@ -25,21 +25,23 @@
></HeaderNavLink>
</Portal>
<CommonTitleDescription :title="project.name" :description="project.description" />
<NuxtLink
v-if="project.workspace && isWorkspacesEnabled"
:to="workspaceRoute(project.workspace.id)"
class="pt-4 flex-1 flex items-center"
>
<WorkspaceAvatar
:logo="project.workspace.logo"
:default-logo-index="project.workspace.defaultLogoIndex"
size="sm"
<div class="flex gap-x-3">
<NuxtLink
v-if="project.workspace && isWorkspacesEnabled"
:to="workspaceRoute(project.workspace.id)"
>
<WorkspaceAvatar
:logo="project.workspace.logo"
:default-logo-index="project.workspace.defaultLogoIndex"
size="sm"
class="mt-0.5"
/>
</NuxtLink>
<CommonTitleDescription
:title="project.name"
:description="project.description"
/>
<p class="text-body-2xs text-foreground ml-2">
{{ project.workspace.name }}
</p>
</NuxtLink>
</div>
</div>
</template>
@@ -1,13 +0,0 @@
<template>
<div class="bg-foundation rounded-lg p-4 border border-outline-3">
<div class="flex flex-col">
<div class="text-foreground-2 mb-2">
<slot name="top" />
</div>
<div class="text-foreground">
<slot name="bottom" />
</div>
</div>
<slot />
</div>
</template>
@@ -1,39 +0,0 @@
<template>
<ProjectPageStatsBlock>
<template #top>
<div class="flex items-center">
<div class="flex items-center justify-between w-full">
<div class="flex items-center gap-1 flex-grow select-none">
<span class="text-heading-sm text-foreground">Collaborators</span>
</div>
<div class="text-body-xs flex items-center capitalize">
{{ project.role?.split(':').reverse()[0] }}
</div>
</div>
</div>
</template>
<template #bottom>
<div class="flex items-center justify-between mt-1">
<UserAvatarGroup :users="teamUsers" class="max-w-[104px]" />
<div v-if="canEdit">
<FormButton class="ml-2" :to="projectCollaboratorsRoute(project.id)">
Manage
</FormButton>
</div>
</div>
</template>
</ProjectPageStatsBlock>
</template>
<script setup lang="ts">
import { canEditProject } from '~~/lib/projects/helpers/permissions'
import type { ProjectPageTeamInternals_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
import { projectCollaboratorsRoute } from '~~/lib/common/helpers/route'
const props = defineProps<{
project: ProjectPageTeamInternals_ProjectFragment
}>()
const canEdit = computed(() => canEditProject(props.project))
const teamUsers = computed(() => props.project.team.map((t) => t.user))
</script>
@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col sm:gap-4 sm:flex-row justify-between sm:items-center">
<div class="flex gap-2 mb-3 mt-2">
<div class="flex items-center">
<div class="flex flex-col gap-4 sm:flex-row justify-between md:items-center">
<div class="flex gap-2 md:mb-3 md:mt-2">
<div class="flex items-center mr-2">
<WorkspaceAvatar
:logo="workspaceInfo.logo"
:default-logo-index="workspaceInfo.defaultLogoIndex"
@@ -9,45 +9,65 @@
/>
</div>
<div class="flex flex-col">
<h1 class="text-heading-lg">{{ workspaceInfo.name }}</h1>
<h1 class="text-heading">{{ workspaceInfo.name }}</h1>
<div class="text-body-xs text-foreground-2">
{{ workspaceInfo.description || 'No workspace description' }}
</div>
</div>
</div>
<div class="flex items-center gap-2">
<div
class="flex md:items-center gap-x-3 md:flex-row"
:class="[isWorkspaceAdmin ? 'flex-col' : 'flex-row items-cenetr']"
>
<div
class="text-body-3xs bg-foundation-2 text-foreground-2 rounded px-3 py-1 font-medium select-none whitespace-nowrap"
class="flex items-center gap-x-3 md:mb-0"
:class="[!isWorkspaceAdmin ? 'flex-1' : ' mb-3']"
>
{{ workspaceInfo.totalProjects.totalCount || 0 }} Project{{
workspaceInfo.totalProjects.totalCount === 1 ? '' : 's'
}}
<CommonBadge rounded :color-classes="'text-foreground-2 bg-primary-muted'">
{{ workspaceInfo.totalProjects.totalCount || 0 }} Project{{
workspaceInfo.totalProjects.totalCount === 1 ? '' : 's'
}}
</CommonBadge>
<CommonBadge rounded :color-classes="'text-foreground-2 bg-primary-muted'">
<span class="capitalize">
{{ workspaceInfo.role?.split(':').reverse()[0] }}
</span>
</CommonBadge>
</div>
<div class="flex items-center gap-x-3">
<div v-if="isWorkspaceAdmin" class="flex-1 md:flex-auto">
<WorkspacePageVersionCount
:versions-count="workspaceInfo.billing.versionsCount"
/>
</div>
<div class="flex items-center gap-x-3">
<UserAvatarGroup
:users="team.map((teamMember) => teamMember.user)"
class="max-w-[104px]"
/>
<FormButton
v-if="isWorkspaceAdmin"
color="outline"
@click="showInviteDialog = !showInviteDialog"
>
Invite
</FormButton>
<LayoutMenu
v-model:open="showActionsMenu"
:items="actionsItems"
:menu-position="HorizontalDirection.Left"
@click.stop.prevent
@chosen="onActionChosen"
>
<FormButton
color="subtle"
hide-text
:icon-right="EllipsisHorizontalIcon"
@click="showActionsMenu = !showActionsMenu"
/>
</LayoutMenu>
</div>
</div>
<UserAvatarGroup
:users="team.map((teamMember) => teamMember.user)"
class="max-w-[104px]"
/>
<FormButton
v-if="isWorkspaceAdmin"
color="outline"
@click="showInviteDialog = !showInviteDialog"
>
Invite
</FormButton>
<LayoutMenu
v-model:open="showActionsMenu"
:items="actionsItems"
:menu-position="HorizontalDirection.Left"
@click.stop.prevent
@chosen="onActionChosen"
>
<FormButton
color="subtle"
hide-text
:icon-right="EllipsisHorizontalIcon"
@click="showActionsMenu = !showActionsMenu"
/>
</LayoutMenu>
</div>
<WorkspaceInviteDialog
v-model:open="showInviteDialog"
@@ -82,6 +102,11 @@ graphql(`
totalProjects: projects {
totalCount
}
billing {
versionsCount {
...WorkspacePageVersionCount_WorkspaceVersionsCount
}
}
team {
items {
id
@@ -0,0 +1,31 @@
<template>
<div class="w-40 flex flex-col items-center md:mx-4">
<CommonProgressBar
class="mb-1"
:current-value="versionsCount.current"
:max-value="versionsCount.max"
/>
<div class="text-body-3xs text-foreground">
<span class="font-medium">
{{ versionsCount.current }}/{{ versionsCount.max }}
</span>
model versions used
</div>
</div>
</template>
<script lang="ts" setup>
import { graphql } from '~/lib/common/generated/gql'
import type { WorkspacePageVersionCount_WorkspaceVersionsCountFragment } from '~~/lib/common/generated/gql/graphql'
graphql(`
fragment WorkspacePageVersionCount_WorkspaceVersionsCount on WorkspaceVersionsCount {
current
max
}
`)
defineProps<{
versionsCount: WorkspacePageVersionCount_WorkspaceVersionsCountFragment
}>()
</script>
@@ -124,11 +124,12 @@ const documents = {
"\n fragment WorkspaceAvatar_Workspace on Workspace {\n id\n logo\n defaultLogoIndex\n }\n": types.WorkspaceAvatar_WorkspaceFragmentDoc,
"\n fragment WorkspaceInviteDialog_Workspace on Workspace {\n id\n team {\n items {\n id\n user {\n id\n }\n }\n }\n invitedTeam(filter: $invitesFilter) {\n title\n user {\n id\n }\n }\n }\n": types.WorkspaceInviteDialog_WorkspaceFragmentDoc,
"\n fragment WorkspaceProjectList_ProjectCollection on ProjectCollection {\n totalCount\n items {\n ...ProjectDashboardItem\n }\n cursor\n }\n": types.WorkspaceProjectList_ProjectCollectionFragmentDoc,
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
"\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n billing {\n versionsCount {\n ...WorkspacePageVersionCount_WorkspaceVersionsCount\n }\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n": types.WorkspaceHeader_WorkspaceFragmentDoc,
"\n fragment WorkspaceInviteBanner_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n workspaceId\n workspaceName\n token\n user {\n id\n }\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBanner_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment WorkspaceInviteBanners_User on User {\n discoverableWorkspaces {\n ...WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace\n }\n workspaceInvites {\n ...WorkspaceInviteBanner_PendingWorkspaceCollaborator\n }\n }\n": types.WorkspaceInviteBanners_UserFragmentDoc,
"\n fragment WorkspaceInviteBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n workspaceId\n workspaceName\n token\n user {\n id\n name\n ...LimitedUserAvatar\n }\n title\n email\n ...UseWorkspaceInviteManager_PendingWorkspaceCollaborator\n }\n": types.WorkspaceInviteBlock_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n": types.WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspaceFragmentDoc,
"\n fragment WorkspacePageVersionCount_WorkspaceVersionsCount on WorkspaceVersionsCount {\n current\n max\n }\n": types.WorkspacePageVersionCount_WorkspaceVersionsCountFragmentDoc,
"\n query ActiveUserMainMetadata {\n activeUser {\n id\n email\n company\n bio\n name\n role\n avatar\n isOnboardingFinished\n createdAt\n verified\n notificationPreferences\n versions(limit: 0) {\n totalCount\n }\n }\n }\n": types.ActiveUserMainMetadataDocument,
"\n mutation CreateOnboardingProject {\n projectMutations {\n createForOnboarding {\n ...ProjectPageProject\n ...ProjectDashboardItem\n }\n }\n }\n ": types.CreateOnboardingProjectDocument,
"\n mutation FinishOnboarding {\n activeUserMutations {\n finishOnboarding\n }\n }\n": types.FinishOnboardingDocument,
@@ -310,7 +311,7 @@ const documents = {
"\n fragment AutomateFunctionPage_AutomateFunction on AutomateFunction {\n id\n name\n description\n logo\n supportedSourceApps\n tags\n ...AutomateFunctionPageHeader_Function\n ...AutomateFunctionPageInfo_AutomateFunction\n ...AutomateAutomationCreateDialog_AutomateFunction\n creator {\n id\n }\n }\n": types.AutomateFunctionPage_AutomateFunctionFragmentDoc,
"\n query AutomateFunctionPage($functionId: ID!) {\n automateFunction(id: $functionId) {\n ...AutomateFunctionPage_AutomateFunction\n }\n }\n": types.AutomateFunctionPageDocument,
"\n query AutomateFunctionsPage($search: String, $cursor: String = null) {\n ...AutomateFunctionsPageItems_Query\n ...AutomateFunctionsPageHeader_Query\n }\n": types.AutomateFunctionsPageDocument,
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n }\n": types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n }\n": types.ProjectPageProjectFragmentDoc,
"\n fragment ProjectPageAutomationPage_Automation on Automation {\n id\n ...ProjectPageAutomationHeader_Automation\n ...ProjectPageAutomationFunctions_Automation\n ...ProjectPageAutomationRuns_Automation\n }\n": types.ProjectPageAutomationPage_AutomationFragmentDoc,
"\n fragment ProjectPageAutomationPage_Project on Project {\n id\n ...ProjectPageAutomationHeader_Project\n }\n": types.ProjectPageAutomationPage_ProjectFragmentDoc,
"\n fragment ProjectPageSettingsTab_Project on Project {\n id\n role\n }\n": types.ProjectPageSettingsTab_ProjectFragmentDoc,
@@ -777,7 +778,7 @@ export function graphql(source: "\n fragment WorkspaceProjectList_ProjectCollec
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n"): (typeof documents)["\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n"];
export function graphql(source: "\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n billing {\n versionsCount {\n ...WorkspacePageVersionCount_WorkspaceVersionsCount\n }\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n"): (typeof documents)["\n fragment WorkspaceHeader_Workspace on Workspace {\n ...WorkspaceAvatar_Workspace\n id\n role\n name\n logo\n description\n totalProjects: projects {\n totalCount\n }\n billing {\n versionsCount {\n ...WorkspacePageVersionCount_WorkspaceVersionsCount\n }\n }\n team {\n items {\n id\n user {\n id\n name\n ...LimitedUserAvatar\n }\n }\n }\n ...WorkspaceInviteDialog_Workspace\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -794,6 +795,10 @@ export function graphql(source: "\n fragment WorkspaceInviteBlock_PendingWorksp
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n }\n"): (typeof documents)["\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_DiscoverableWorkspace on DiscoverableWorkspace {\n id\n name\n description\n logo\n defaultLogoIndex\n }\n fragment WorkspaceInviteDiscoverableWorkspaceBanner_Workspace on Workspace {\n id\n name\n description\n createdAt\n updatedAt\n logo\n defaultLogoIndex\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\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 fragment WorkspacePageVersionCount_WorkspaceVersionsCount on WorkspaceVersionsCount {\n current\n max\n }\n"): (typeof documents)["\n fragment WorkspacePageVersionCount_WorkspaceVersionsCount on WorkspaceVersionsCount {\n current\n max\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1521,7 +1526,7 @@ export function graphql(source: "\n query AutomateFunctionsPage($search: String
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n }\n"];
export function graphql(source: "\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n }\n"): (typeof documents)["\n fragment ProjectPageProject on Project {\n id\n createdAt\n modelCount: models(limit: 0) {\n totalCount\n }\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectPageTeamInternals_Project\n ...ProjectPageProjectHeader\n ...ProjectPageTeamDialog\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
File diff suppressed because one or more lines are too long
@@ -8,10 +8,27 @@
@processed="onInviteAccepted"
/>
<div
class="flex flex-col md:flex-row md:justify-between md:items-start gap-8 mb-6 mt-4 md:my-6"
class="flex flex-col md:flex-row md:justify-between md:items-center gap-4 my-2"
>
<ProjectPageHeader :project="project" />
<ProjectPageTeamBlock :project="project" class="w-full md:w-72 shrink-0" />
<div class="flex gap-x-3 items-center">
<CommonBadge rounded :color-classes="'text-foreground-2 bg-primary-muted'">
{{ project.modelCount.totalCount || 0 }} Model{{
project.modelCount.totalCount === 1 ? '' : 's'
}}
</CommonBadge>
<CommonBadge rounded :color-classes="'text-foreground-2 bg-primary-muted'">
<span class="capitalize">{{ project.role?.split(':').reverse()[0] }}</span>
</CommonBadge>
<UserAvatarGroup :users="teamUsers" class="max-w-[104px]" />
<FormButton
v-if="canEdit"
color="outline"
:to="projectCollaboratorsRoute(project.id)"
>
Manage
</FormButton>
</div>
</div>
<LayoutTabsHorizontal v-model:active-item="activePageTab" :items="pageTabItems">
<NuxtPage :project="project" />
@@ -27,6 +44,8 @@ import { projectPageQuery } from '~~/lib/projects/graphql/queries'
import { useGeneralProjectPageUpdateTracking } from '~~/lib/projects/composables/projectPages'
import { LayoutTabsHorizontal, type LayoutPageTabItem } from '@speckle/ui-components'
import { projectRoute, projectWebhooksRoute } from '~/lib/common/helpers/route'
import { canEditProject } from '~~/lib/projects/helpers/permissions'
import { projectCollaboratorsRoute } from '~~/lib/common/helpers/route'
graphql(`
fragment ProjectPageProject on Project {
@@ -38,6 +57,7 @@ graphql(`
commentThreadCount: commentThreads(limit: 0) {
totalCount
}
...ProjectPageTeamInternals_Project
...ProjectPageProjectHeader
...ProjectPageTeamDialog
}
@@ -96,6 +116,8 @@ const projectName = computed(() =>
const modelCount = computed(() => project.value?.modelCount.totalCount)
const commentCount = computed(() => project.value?.commentThreadCount.totalCount)
const hasRole = computed(() => project.value?.role)
const canEdit = computed(() => (project.value ? canEditProject(project.value) : false))
const teamUsers = computed(() => project.value?.team.map((t) => t.user))
useHead({
title: projectName
@@ -1,8 +1,7 @@
<template>
<div class="relative w-full bg-foundation-2 rounded" :class="[heightClass]">
<div class="relative w-full bg-outline-3 rounded h-1.5">
<div
class="aboslute left-0 top-0 bg-success rounded"
:class="[heightClass]"
class="aboslute left-0 top-0 bg-success rounded h-1.5"
:style="{ width: `${percentage <= 100 ? percentage : 100}%` }"
/>
</div>
@@ -11,19 +10,10 @@
<script lang="ts" setup>
import { computed } from 'vue'
type ProgressBarSize = 'base'
const props = defineProps<{
currentValue: number
maxValue: number
}>()
const props = withDefaults(
defineProps<{
currentValue: number
maxValue: number
size: ProgressBarSize
}>(),
{
size: 'base'
}
)
const heightClass = computed(() => (props.size === 'base' ? 'h-1.5' : 'h-1'))
const percentage = computed(() => (props.currentValue / props.maxValue) * 100)
</script>