Merge branch 'main' into andrew/web-2567-update-members-and-guests-settings-pages

This commit is contained in:
andrewwallacespeckle
2025-03-21 13:02:01 +00:00
17 changed files with 107 additions and 84 deletions
@@ -1,32 +1,35 @@
<template>
<ProjectPageSettingsBlock title="Collaborators">
<template #introduction>
<p class="text-body-xs text-foreground">
Invite new collaborators and set permissions.
</p>
</template>
<template #top-buttons>
<FormButton :disabled="!canInvite" @click="toggleInviteDialog">Invite</FormButton>
</template>
<div class="flex flex-col mt-6">
<ProjectPageSettingsCollaboratorsRow
v-for="collaborator in collaboratorListItems"
:key="collaborator.id"
:can-edit="canEdit"
:collaborator="collaborator"
:loading="loading"
@cancel-invite="onCancelInvite"
@change-role="onCollaboratorRoleChange"
<div>
<div v-if="project">
<div class="flex justify-between space-x-2">
<div>
<h1 class="block text-heading-xl mb-2">Collaborators</h1>
<p class="text-body-xs text-foreground">
Invite new collaborators and set permissions.
</p>
</div>
<FormButton class="mt-1" :disabled="!canInvite" @click="toggleInviteDialog">
Invite
</FormButton>
</div>
<div class="flex flex-col mt-6">
<ProjectPageSettingsCollaboratorsRow
v-for="collaborator in collaboratorListItems"
:key="collaborator.id"
:can-edit="canEdit"
:collaborator="collaborator"
:loading="loading"
@cancel-invite="onCancelInvite"
@change-role="onCollaboratorRoleChange"
/>
</div>
<InviteDialogProject
v-if="project"
v-model:open="showInviteDialog"
:project="project"
/>
</div>
<InviteDialogProject
v-if="project"
v-model:open="showInviteDialog"
:project="project"
/>
</ProjectPageSettingsBlock>
</div>
</template>
<script setup lang="ts">
import type { Project } from '~~/lib/common/generated/gql/graphql'
@@ -116,7 +116,7 @@ import { isProject } from '~~/lib/server-management/helpers/utils'
import { useDebouncedTextInput, type LayoutMenuItem } from '@speckle/ui-components'
import { graphql } from '~/lib/common/generated/gql'
import { useRouter } from 'vue-router'
import { projectCollaboratorsRoute, projectRoute } from '~/lib/common/helpers/route'
import { projectRoute } from '~/lib/common/helpers/route'
graphql(`
fragment SettingsSharedProjects_Project on Project {
@@ -187,7 +187,7 @@ const onActionChosen = (
project: ProjectsDeleteDialog_ProjectFragment
) => {
if (actionItem.id === ActionTypes.EditMembers) {
router.push(projectCollaboratorsRoute(project.id))
router.push(projectRoute(project.id, 'collaborators'))
} else if (actionItem.id === ActionTypes.ViewProject) {
handleProjectClick(project.id)
} else if (actionItem.id === ActionTypes.DeleteProject) {
@@ -132,7 +132,7 @@ graphql(`
...SettingsWorkspacesMembersTableHeader_Workspace
...SettingsSharedDeleteUserDialog_Workspace
...SettingsWorkspacesMembersChangeRoleDialog_Workspace
team {
team(limit: 250) {
items {
id
...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator
@@ -134,7 +134,7 @@ graphql(`
...SettingsSharedDeleteUserDialog_Workspace
...SettingsWorkspacesMembersTableHeader_Workspace
...SettingsWorkspacesMembersChangeRoleDialog_Workspace
team {
team(limit: 250) {
items {
id
...SettingsWorkspacesMembersTable_WorkspaceCollaborator
@@ -77,6 +77,7 @@
v-if="!isWorkspaceGuest"
:workspace-info="workspaceInfo"
:is-workspace-admin="isWorkspaceAdmin"
:is-workspace-guest="isWorkspaceGuest"
@show-invite-dialog="$emit('show-invite-dialog')"
/>
</div>
@@ -5,7 +5,6 @@
:icon="iconName"
:icon-click="iconClick"
:icon-text="iconText"
:tag="workspaceInfo.team.totalCount.toString() || undefined"
no-hover
>
<div class="flex lg:flex-col items-center lg:items-start gap-y-3 pb-0 lg:pb-4 mt-1">
@@ -15,6 +14,11 @@
:users="team.map((teamMember) => teamMember.user)"
:max-avatars="isDesktop ? 5 : 3"
class="shrink-0"
:on-hidden-count-click="
() => {
navigateTo(settingsWorkspaceRoutes.members.route(workspaceInfo.slug))
}
"
/>
<div class="w-full flex items-center gap-x-2">
<button
@@ -66,6 +70,7 @@ const props = defineProps<{
workspaceInfo: WorkspaceTeam_WorkspaceFragment
collapsible?: boolean
isWorkspaceAdmin?: boolean
isWorkspaceGuest?: boolean
}>()
const breakpoints = useBreakpoints(TailwindBreakpoints)
@@ -74,19 +79,19 @@ const isDesktop = breakpoints.greaterOrEqual('lg')
const team = computed(() => props.workspaceInfo.team.items || [])
const iconName = computed(() => {
if (!props.isWorkspaceAdmin) return undefined
return 'edit'
if (props.isWorkspaceAdmin) return 'edit'
return 'view'
})
const iconClick = computed(() => {
if (!props.isWorkspaceAdmin) return undefined
if (props.isWorkspaceGuest) return undefined
return () =>
navigateTo(settingsWorkspaceRoutes.members.route(props.workspaceInfo.slug))
})
const iconText = computed(() => {
if (!props.isWorkspaceAdmin) return undefined
return 'Manage members'
if (props.isWorkspaceAdmin) return 'Manage members'
return 'View members'
})
const invitedTeamCount = computed(() => props.workspaceInfo?.invitedTeam?.length ?? 0)
@@ -80,7 +80,7 @@ export const settingsWorkspaceRoutes = {
export const projectRoute = (
id: string,
tab?: 'models' | 'discussions' | 'automations' | 'settings'
tab?: 'models' | 'discussions' | 'automations' | 'collaborators' | 'settings'
) => {
let res = `/projects/${id}`
if (tab && tab !== 'models') {
@@ -113,9 +113,6 @@ export const projectDiscussionsRoute = (projectId: string) => `/projects/${proje
export const projectSettingsRoute = (projectId: string) =>
`/projects/${projectId}/settings`
export const projectCollaboratorsRoute = (projectId: string) =>
`/projects/${projectId}/settings/collaborators`
export const projectWebhooksRoute = (projectId: string) =>
`/projects/${projectId}/settings/webhooks`
@@ -108,7 +108,7 @@ export const settingsWorkspacesMembersSearchQuery = graphql(`
query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {
workspaceBySlug(slug: $slug) {
id
team(filter: $filter) {
team(filter: $filter, limit: 250) {
items {
id
...SettingsWorkspacesMembersTable_WorkspaceCollaborator
@@ -39,7 +39,7 @@ export const workspaceTeamFragment = graphql(`
fragment WorkspaceTeam_Workspace on Workspace {
id
slug
team {
team(limit: 250) {
totalCount
items {
id
+5 -5
View File
@@ -170,31 +170,31 @@ export default defineNuxtConfig({
// Redirect old settings pages
'/server-management/projects': {
redirect: {
to: '/?settings=server/projects',
to: '/settings/server/projects',
statusCode: 301
}
},
'/server-management/active-users': {
redirect: {
to: '/?settings=server/active-users',
to: '/settings/server/active-users',
statusCode: 301
}
},
'/server-management/pending-invitations': {
redirect: {
to: '/?settings=server/pending-invitations',
to: '/settings/server/pending-invitations',
statusCode: 301
}
},
'/server-management': {
redirect: {
to: '/?settings=server/general',
to: '/settings/server/general',
statusCode: 301
}
},
'/profile': {
redirect: {
to: '/?settings/user/profile',
to: '/settings/user/profile',
statusCode: 301
}
},
@@ -25,7 +25,7 @@
</div>
<div class="flex flex-row gap-x-3">
<div v-tippy="collaboratorsTooltip">
<NuxtLink :to="hasRole ? projectCollaboratorsRoute(project.id) : ''">
<NuxtLink :to="hasRole ? projectRoute(project.id, 'collaborators') : ''">
<UserAvatarGroup
:users="teamUsers"
:max-count="2"
@@ -74,7 +74,6 @@ import { useGeneralProjectPageUpdateTracking } from '~~/lib/projects/composables
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'
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
import { EllipsisHorizontalIcon } from '@heroicons/vue/24/solid'
import { HorizontalDirection } from '~~/lib/common/composables/window'
@@ -227,6 +226,11 @@ const pageTabItems = computed((): LayoutPageTabItem[] => {
}
if (hasRole.value) {
items.push({
title: 'Collaborators',
id: 'collaborators'
})
items.push({
title: 'Settings',
id: 'settings'
@@ -248,6 +252,8 @@ const activePageTab = computed({
const path = router.currentRoute.value.path
if (/\/discussions\/?$/i.test(path)) return findTabById('discussions')
if (/\/automations\/?.*$/i.test(path)) return findTabById('automations')
if (/\/collaborators\/?/i.test(path) && hasRole.value)
return findTabById('collaborators')
if (/\/settings\/?/i.test(path) && hasRole.value) return findTabById('settings')
return findTabById('models')
},
@@ -263,6 +269,11 @@ const activePageTab = computed({
case 'automations':
router.push({ path: projectRoute(projectId.value, 'automations') })
break
case 'collaborators':
if (hasRole.value) {
router.push({ path: projectRoute(projectId.value, 'collaborators') })
}
break
case 'settings':
if (hasRole.value) {
router.push({ path: projectRoute(projectId.value, 'settings') })
@@ -0,0 +1,19 @@
<template>
<ProjectPageSettingsCollaborators />
</template>
<script setup lang="ts">
import type { ProjectPageProjectFragment } from '~~/lib/common/generated/gql/graphql'
const attrs = useAttrs() as {
project: ProjectPageProjectFragment
}
const projectName = computed(() =>
attrs.project.name.length ? attrs.project.name : ''
)
useHead({
title: `Collaborators | ${projectName.value}`
})
</script>
@@ -11,11 +11,7 @@
</template>
<script setup lang="ts">
import { LayoutTabsVertical, type LayoutPageTabItem } from '@speckle/ui-components'
import {
projectCollaboratorsRoute,
projectSettingsRoute,
projectWebhooksRoute
} from '~~/lib/common/helpers/route'
import { projectSettingsRoute, projectWebhooksRoute } from '~~/lib/common/helpers/route'
import { graphql } from '~~/lib/common/generated/gql'
import type { ProjectPageProjectFragment } from '~~/lib/common/generated/gql/graphql'
import { Roles } from '@speckle/shared'
@@ -51,10 +47,6 @@ const settingsTabItems = computed((): LayoutPageTabItem[] => [
title: 'General',
id: 'general'
},
{
title: 'Collaborators',
id: 'collaborators'
},
{
title: 'Webhooks',
id: 'webhooks',
@@ -68,15 +60,11 @@ const projectId = computed(() => route.params.id as string)
const activeSettingsPageTab = computed({
get: () => {
const path = route.path
if (path.includes('/settings/collaborators')) return settingsTabItems.value[1]
if (path.includes('/settings/webhooks')) return settingsTabItems.value[2]
return settingsTabItems.value[0]
},
set: (val: LayoutPageTabItem) => {
switch (val.id) {
case 'collaborators':
router.push(projectCollaboratorsRoute(projectId.value))
break
case 'webhooks':
router.push(projectWebhooksRoute(projectId.value))
break
@@ -1,3 +0,0 @@
<template>
<ProjectPageSettingsCollaborators />
</template>
@@ -46,6 +46,7 @@
@click="iconClick"
>
<Edit v-if="icon === 'edit'" class="h-4 w-4" />
<ChevronRightIcon v-else-if="icon === 'view'" class="h-4 w-4" />
<Plus v-else class="h-4 w-4" />
</button>
</div>
@@ -61,13 +62,14 @@ import Plus from '~~/src/components/global/icon/Plus.vue'
import Edit from '~~/src/components/global/icon/Edit.vue'
import ArrowFilled from '~~/src/components/global/icon/ArrowFilled.vue'
import CommonBadge from '~~/src/components/common/Badge.vue'
import { ChevronRightIcon } from '@heroicons/vue/24/outline'
defineProps<{
tag?: string
title?: string
collapsible?: boolean
collapsed?: boolean
icon?: 'add' | 'edit'
icon?: 'add' | 'edit' | 'view'
iconText?: string
iconClick?: () => void
noHover?: boolean
@@ -14,7 +14,13 @@
:hide-tooltip="hideTooltips"
/>
</div>
<UserAvatar v-if="totalHiddenCount" :size="size" class="select-none">
<UserAvatar
v-if="totalHiddenCount"
:size="size"
class="select-none"
:class="{ 'cursor-pointer': !!onHiddenCountClick }"
@click="onHiddenCountClick && onHiddenCountClick()"
>
+{{ totalHiddenCount }}
</UserAvatar>
</div>
@@ -35,6 +41,7 @@ const props = withDefaults(
maxCount?: number
hideTooltips?: boolean
maxAvatars?: number
onHiddenCountClick?: () => void
}>(),
{
users: () => [],
@@ -42,7 +49,8 @@ const props = withDefaults(
size: 'base',
maxCount: undefined,
hideTooltips: false,
maxAvatars: undefined
maxAvatars: undefined,
onHiddenCountClick: undefined
}
)
@@ -1078,15 +1078,11 @@ export class SmoothOrbitControls extends SpeckleControls {
protected onPointerDown = (event: PointerEvent) => {
if (this._options.orbitAroundCursor) {
const x =
((event.clientX - this._container.offsetLeft) / this._container.offsetWidth) *
2 -
1
/** Hope this is not slow */
const rect = this._container.getBoundingClientRect()
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1
const y = ((event.clientY - rect.top) / rect.height) * -2 + 1
const y =
((event.clientY - this._container.offsetTop) / this._container.offsetHeight) *
-2 +
1
const res = this.renderer.intersections.intersect(
this.renderer.scene,
this._targetCamera as PerspectiveCamera,
@@ -1242,14 +1238,10 @@ export class SmoothOrbitControls extends SpeckleControls {
}
protected onWheel = (event: WheelEvent) => {
const x =
((event.clientX - this._container.offsetLeft) / this._container.offsetWidth) * 2 -
1
const y =
((event.clientY - this._container.offsetTop) / this._container.offsetHeight) *
-2 +
1
/** Hope this is not slow */
const rect = this._container.getBoundingClientRect()
const x = ((event.clientX - rect.left) / rect.width) * 2 - 1
const y = ((event.clientY - rect.top) / rect.height) * -2 + 1
this.zoomControlCoord.set(x, y)