Merge branch 'main' into iain/helm-chart-configurable-env-vars

This commit is contained in:
Iain Sproat
2025-04-08 07:38:45 +01:00
33 changed files with 688 additions and 492 deletions
@@ -2002,6 +2002,8 @@ export type Project = {
* real or fake (e.g., with a foo/bar model, it will be nested under foo even if such a model doesn't actually exist)
*/
modelsTree: ModelsTreeItemCollection;
/** Returns information about the potential effects of moving a project to a given workspace. */
moveToWorkspaceDryRun: ProjectMoveToWorkspaceDryRun;
name: Scalars['String']['output'];
object?: Maybe<Object>;
/** Pending project access requests */
@@ -2100,6 +2102,11 @@ export type ProjectModelsTreeArgs = {
};
export type ProjectMoveToWorkspaceDryRunArgs = {
workspaceId: Scalars['String']['input'];
};
export type ProjectObjectArgs = {
id: Scalars['String']['input'];
};
@@ -2416,6 +2423,17 @@ export enum ProjectModelsUpdatedMessageType {
Updated = 'UPDATED'
}
export type ProjectMoveToWorkspaceDryRun = {
__typename?: 'ProjectMoveToWorkspaceDryRun';
addedToWorkspace: Array<LimitedUser>;
addedToWorkspaceTotalCount: Scalars['Int']['output'];
};
export type ProjectMoveToWorkspaceDryRunAddedToWorkspaceArgs = {
limit?: InputMaybe<Scalars['Int']['input']>;
};
export type ProjectMutations = {
__typename?: 'ProjectMutations';
/** Access request related mutations */
@@ -4326,7 +4344,6 @@ export type Workspace = {
invitedTeam?: Maybe<Array<PendingWorkspaceCollaborator>>;
/** Logo image as base64-encoded string */
logo?: Maybe<Scalars['String']['output']>;
membersByRole?: Maybe<WorkspaceMembersByRole>;
name: Scalars['String']['output'];
permissions: WorkspacePermissionChecks;
plan?: Maybe<WorkspacePlan>;
@@ -4337,12 +4354,12 @@ export type Workspace = {
role?: Maybe<Scalars['String']['output']>;
/** Active user's seat type for this workspace. `null` if request is not authenticated, or the workspace is not explicitly shared with you. */
seatType?: Maybe<WorkspaceSeatType>;
seatsByType?: Maybe<WorkspaceSeatsByType>;
slug: Scalars['String']['output'];
/** Information about the workspace's SSO configuration and the current user's SSO session, if present */
sso?: Maybe<WorkspaceSso>;
subscription?: Maybe<WorkspaceSubscription>;
team: WorkspaceCollaboratorCollection;
teamByRole: WorkspaceTeamByRole;
updatedAt: Scalars['DateTime']['output'];
};
@@ -4434,6 +4451,8 @@ export type WorkspaceCollection = {
export type WorkspaceCreateInput = {
description?: InputMaybe<Scalars['String']['input']>;
/** Add this domain to the workspace as a verified domain and enable domain discoverability */
enableDomainDiscoverabilityForDomain?: InputMaybe<Scalars['String']['input']>;
/** Logo image as base64-encoded string */
logo?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
@@ -4583,13 +4602,6 @@ export enum WorkspaceJoinRequestStatus {
Pending = 'pending'
}
export type WorkspaceMembersByRole = {
__typename?: 'WorkspaceMembersByRole';
admins?: Maybe<WorkspaceRoleCollection>;
guests?: Maybe<WorkspaceRoleCollection>;
members?: Maybe<WorkspaceRoleCollection>;
};
export type WorkspaceMutations = {
__typename?: 'WorkspaceMutations';
addDomain: Workspace;
@@ -4893,18 +4905,25 @@ export type WorkspaceSubscription = {
updatedAt: Scalars['DateTime']['output'];
};
export type WorkspaceSubscriptionSeatCount = {
__typename?: 'WorkspaceSubscriptionSeatCount';
/** Total number of seats in use by workspace users */
assigned: Scalars['Int']['output'];
/** Total number of seats purchased and available in the current subscription cycle */
available: Scalars['Int']['output'];
};
export type WorkspaceSubscriptionSeats = {
__typename?: 'WorkspaceSubscriptionSeats';
/** Number assigned seats in the current billing cycle */
assigned: Scalars['Int']['output'];
/** @deprecated No longer supported */
guest: Scalars['Int']['output'];
/** @deprecated No longer supported */
plan: Scalars['Int']['output'];
/** Total number of seats purchased and available in the current subscription cycle */
totalCount: Scalars['Int']['output'];
/** Number of viewer seats currently assigned in the workspace */
viewersCount: Scalars['Int']['output'];
editors: WorkspaceSubscriptionSeatCount;
viewers: WorkspaceSubscriptionSeatCount;
};
export type WorkspaceTeamByRole = {
__typename?: 'WorkspaceTeamByRole';
admins?: Maybe<WorkspaceRoleCollection>;
guests?: Maybe<WorkspaceRoleCollection>;
members?: Maybe<WorkspaceRoleCollection>;
};
export type WorkspaceTeamFilter = {
@@ -117,7 +117,7 @@ const props = defineProps<{
}>()
const slots: SetupContext['slots'] = useSlots()
const { pricesNew } = useWorkspacePlanPrices()
const { prices } = useWorkspacePlanPrices()
const { upgradePlan, redirectToCheckout } = useBillingActions()
const isYearlyIntervalSelected = ref(props.yearlyIntervalSelected)
@@ -126,7 +126,7 @@ const planFeatures = computed(() => WorkspacePlanConfigs[props.plan].features)
const planPrice = computed(() => {
if (props.plan === WorkspacePlans.Team || props.plan === WorkspacePlans.Pro) {
return formatPrice(
pricesNew.value?.[props.plan]?.[WorkspacePlanBillingIntervals.Monthly]
prices.value?.[props.plan]?.[WorkspacePlanBillingIntervals.Monthly]
)
}
@@ -20,7 +20,7 @@
<template v-else>Bill</template>
</h3>
<p class="text-heading-lg text-foreground inline-block">
{{ totalCostFormatted }}
TODO
<span v-if="isPurchasablePlan">per {{ billingInterval }}</span>
</p>
<NuxtLink
@@ -61,7 +61,7 @@
<script setup lang="ts">
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
import { useBillingActions } from '~/lib/billing/composables/actions'
import type { MaybeNullOrUndefined } from '@speckle/shared'
import { type MaybeNullOrUndefined, WorkspacePlanStatuses } from '@speckle/shared'
import { formatName } from '~/lib/billing/helpers/plan'
defineProps<{
@@ -72,10 +72,12 @@ const { billingPortalRedirect } = useBillingActions()
const route = useRoute()
const slug = computed(() => (route.params.slug as string) || '')
const { plan, isPurchasablePlan, isActivePlan, totalCostFormatted, billingInterval } =
useWorkspacePlan(slug.value)
const { plan, isPurchasablePlan, billingInterval } = useWorkspacePlan(slug.value)
const showBillingPortalLink = computed(
() => isActivePlan.value && isPurchasablePlan.value
() =>
plan.value?.status === WorkspacePlanStatuses.Valid ||
plan.value?.status === WorkspacePlanStatuses.PaymentFailed ||
plan.value?.status === WorkspacePlanStatuses.CancelationScheduled
)
</script>
@@ -28,7 +28,6 @@ import {
import { useBillingActions } from '~/lib/billing/composables/actions'
import { startCase } from 'lodash'
import type { PaidWorkspacePlansOld } from '@speckle/shared'
import { Roles } from '@speckle/shared'
import { isPaidPlan } from '~/lib/billing/helpers/types'
import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices'
import { formatPrice } from '~/lib/billing/helpers/plan'
@@ -46,7 +45,7 @@ const { prices } = useWorkspacePlanPrices()
const seatPrice = computed(() => {
if (isPaidPlan(props.plan)) {
const planPrices = prices.value?.[props.plan]
const price = planPrices?.[props.billingInterval]?.[Roles.Workspace.Member]
const price = planPrices?.[props.billingInterval]
return formatPrice(price)
}
@@ -8,13 +8,17 @@
<div>
<h3 class="text-body-xs text-foreground-2 pb-2">Editor seats</h3>
<div class="flex items-center gap-x-2">
<p class="text-body-xs text-foreground font-medium leading-none">4</p>
<CommonBadge rounded>2 unused</CommonBadge>
<p class="text-body-xs text-foreground font-medium leading-none">
{{ seats?.editors.assigned }}
</p>
<CommonBadge rounded>{{ seats?.editors.available }} Unused</CommonBadge>
</div>
</div>
<div>
<h3 class="text-body-xs text-foreground-2 pb-2">Viewer seats</h3>
<p class="text-body-xs text-foreground font-medium leading-none">4</p>
<p class="text-body-xs text-foreground font-medium leading-none">
{{ seats?.viewers.assigned }}
</p>
</div>
</div>
<div class="flex xl:w-[34%] xl:justify-end">
@@ -32,25 +36,35 @@
>
<div>
<h3 class="text-body-xs text-foreground-2 pb-2">Projects</h3>
<p class="text-body-xs text-foreground font-medium pb-3 leading-none">
{{ formatUsageText(10, 100, 'project') }}
<template v-if="limits?.projectCount">
<p class="text-body-xs text-foreground font-medium pb-3 leading-none">
{{ formatUsageText(projectCount, limits.projectCount, 'project') }}
</p>
<CommonProgressBar
class="max-w-72 w-full"
:current-value="projectCount"
:max-value="limits.projectCount"
/>
</template>
<p v-else class="text-body-xs text-foreground font-medium leading-none">
{{ projectCount }}
</p>
<CommonProgressBar
class="max-w-72 w-full"
:current-value="10"
:max-value="100"
/>
</div>
<div>
<h3 class="text-body-xs text-foreground-2 pb-2">Models</h3>
<p class="text-body-xs text-foreground font-medium pb-3 leading-none">
{{ formatUsageText(10, 100, 'model') }}
<template v-if="limits?.modelCount">
<p class="text-body-xs text-foreground font-medium pb-3 leading-none">
{{ formatUsageText(modelCount, limits.modelCount, 'model') }}
</p>
<CommonProgressBar
class="max-w-72 w-full"
:current-value="modelCount"
:max-value="limits?.modelCount"
/>
</template>
<p v-else class="text-body-xs text-foreground font-medium leading-none">
{{ modelCount }}
</p>
<CommonProgressBar
class="max-w-72 w-full"
:current-value="10"
:max-value="100"
/>
</div>
</div>
<div class="flex xl:w-[34%] xl:justify-end">
@@ -67,11 +81,18 @@
<script lang="ts" setup>
import { settingsWorkspaceRoutes } from '~/lib/common/helpers/route'
import { useWorkspaceUsage } from '~/lib/workspaces/composables/usage'
import { useWorkspaceLimits } from '~/lib/workspaces/composables/limits'
import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
defineProps<{
const props = defineProps<{
slug: string
}>()
const { projectCount, modelCount } = useWorkspaceUsage(props.slug)
const { limits } = useWorkspaceLimits(props.slug)
const { seats } = useWorkspacePlan(props.slug)
const formatUsageText = (current: number, max: number, type: string) => {
return `${current} ${type}${current === 1 ? '' : 's'} used / ${max} included`
}
@@ -1,95 +0,0 @@
<template>
<LayoutDialog v-model:open="open" max-width="sm">
<template #header>Change project permissions</template>
<div class="text-foreground mb-8">
<div v-if="projectCount > 0" class="flex flex-col gap-4">
<p class="font-medium text-body-xs">
Projects {{ user?.user.name }} has access to:
</p>
<FormTextInput
v-bind="searchBind"
name="searchGuests"
color="foundation"
type="text"
size="lg"
:placeholder="`Search ${projectCount} project${
projectCount !== 1 ? 's' : ''
}...`"
class="px-3 py-2 border border-outline-3 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
v-on="searchOn"
/>
<div
class="flex flex-col divide-y divide-outline-3 rounded-md border border-outline-3"
>
<div
v-for="projectRole in filteredProjectRoles"
:key="projectRole.project.id"
class="flex items-center justify-between p-4"
>
<span class="text-body-sm">{{ projectRole.project.name }}</span>
<ProjectPageTeamPermissionSelect
:model-value="projectRole.role"
:disabled="false"
mount-menu-on-body
hide-owner
@update:model-value="
(newRole) => updateProjectRole(projectRole.project.id, newRole)
"
@delete="() => updateProjectRole(projectRole.project.id, null)"
/>
</div>
</div>
</div>
<div v-else>
This guest doesn't have access to any projects in this workspace.
</div>
</div>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { StreamRoles, MaybeNullOrUndefined } from '@speckle/shared'
import { useUpdateUserRole } from '~~/lib/projects/composables/projectManagement'
import type { WorkspaceCollaborator } from '~/lib/common/generated/gql/graphql'
import { useDebouncedTextInput } from '@speckle/ui-components'
const props = defineProps<{
user: WorkspaceCollaborator
workspaceId: MaybeNullOrUndefined<string>
}>()
const open = defineModel<boolean>('open', { required: true })
const loading = ref(false)
const searchTerm = ref('')
const { on: searchOn, bind: searchBind } = useDebouncedTextInput({
model: searchTerm,
debouncedBy: 300
})
const project = computed(() => ({ workspaceId: props.workspaceId }))
const filteredProjectRoles = computed(() => {
const roles = props.user?.projectRoles
if (!searchTerm.value) return roles || []
return (roles || []).filter((projectRole) =>
projectRole.project.name.toLowerCase().includes(searchTerm.value.toLowerCase())
)
})
const updateRole = useUpdateUserRole(project)
const updateProjectRole = async (projectId: string, newRole: StreamRoles | null) => {
if (!props.user) return
loading.value = true
await updateRole({
projectId,
userId: props.user.id,
role: newRole
})
loading.value = false
}
const projectCount = computed(() => props.user?.projectRoles?.length)
</script>
@@ -22,11 +22,12 @@
:columns="[
{ id: 'name', header: 'Name', classes: 'col-span-4' },
{ id: 'seat', header: 'Seat', classes: 'col-span-2' },
{ id: 'joined', header: 'Joined', classes: 'col-span-4' },
{ id: 'joined', header: 'Joined', classes: 'col-span-3' },
{ id: 'projects', header: 'Projects', classes: 'col-span-2' },
{
id: 'actions',
header: '',
classes: 'col-span-2 flex items-center justify-end'
classes: 'col-span-1 flex items-center justify-end'
}
]"
:items="guests"
@@ -62,79 +63,70 @@
{{ formattedFullDate(item.joinDate) }}
</span>
</template>
<template #projects="{ item }">
<FormButton
v-if="
item.projectRoles.length > 0 &&
isWorkspaceAdmin &&
item.role !== Roles.Workspace.Admin
"
color="subtle"
size="sm"
class="!font-normal !text-foreground-2 -ml-2"
@click="
() => {
targetUser = item
showProjectPermissionsDialog = true
}
"
>
{{ item.projectRoles.length }}
{{ item.projectRoles.length === 1 ? 'project' : 'projects' }}
</FormButton>
<div v-else class="text-foreground-2 max-w-max text-body-2xs select-none">
{{ item.projectRoles.length }}
{{ item.projectRoles.length === 1 ? 'project' : 'projects' }}
</div>
</template>
<template #actions="{ item }">
<SettingsWorkspacesMembersActionsMenu
v-if="isWorkspaceAdmin"
:target-user="{
...item.user,
role: item.role,
seatType: item.seatType,
joinDate: item.joinDate,
workspaceDomainPolicyCompliant: item.user.workspaceDomainPolicyCompliant
}"
:target-user="item"
:workspace="workspace"
/>
<span v-else />
</template>
</LayoutTable>
<SettingsWorkspacesMembersActionsProjectPermissionsDialog
v-model:open="showProjectPermissionsDialog"
:user="targetUser"
:workspace-id="workspace?.id || ''"
/>
</div>
</template>
<script setup lang="ts">
import {
WorkspaceSeatType,
type SettingsWorkspacesMembersGuestsTable_WorkspaceFragment
type SettingsWorkspacesMembersActionsMenu_UserFragment,
type SettingsWorkspacesMembersTable_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import { graphql } from '~/lib/common/generated/gql'
import { Roles, type MaybeNullOrUndefined } from '@speckle/shared'
import { settingsWorkspacesMembersSearchQuery } from '~~/lib/settings/graphql/queries'
import { useQuery } from '@vue/apollo-composable'
import { LearnMoreRolesSeatsUrl } from '~~/lib/common/helpers/route'
graphql(`
fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {
id
role
seatType
joinDate
user {
id
avatar
name
workspaceDomainPolicyCompliant(workspaceSlug: $slug)
}
projectRoles {
role
project {
id
name
}
}
}
`)
graphql(`
fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {
id
slug
name
...SettingsWorkspacesMembersTableHeader_Workspace
team(limit: 250) {
items {
id
...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator
}
}
}
`)
const props = defineProps<{
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersGuestsTable_WorkspaceFragment>
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersTable_WorkspaceFragment>
workspaceSlug: string
}>()
const search = ref('')
const seatTypeFilter = ref<WorkspaceSeatType>()
const showProjectPermissionsDialog = ref(false)
const targetUser = ref<SettingsWorkspacesMembersActionsMenu_UserFragment | undefined>(
undefined
)
const { result: searchResult, loading: searchResultLoading } = useQuery(
settingsWorkspacesMembersSearchQuery,
@@ -25,10 +25,11 @@
{ id: 'name', header: 'Name', classes: 'col-span-4' },
{ id: 'seat', header: 'Seat', classes: 'col-span-2' },
{ id: 'joined', header: 'Joined', classes: 'col-span-3' },
{ id: 'projects', header: 'Projects', classes: 'col-span-2' },
{
id: 'actions',
header: '',
classes: 'col-span-3 flex items-center justify-end'
classes: 'col-span-1 flex items-center justify-end'
}
]"
:items="members"
@@ -41,13 +42,13 @@
<div class="flex items-center gap-2">
<UserAvatar
hide-tooltip
:user="item"
:user="item.user"
light-style
class="bg-foundation"
no-bg
/>
<span class="truncate text-body-xs text-foreground">
{{ item.name }}
{{ item.user.name }}
<span
v-if="item.id === activeUser?.id"
class="text-foreground-3 text-body-3xs"
@@ -64,7 +65,7 @@
</CommonBadge>
<div
v-if="
item.workspaceDomainPolicyCompliant === false &&
item.user.workspaceDomainPolicyCompliant === false &&
item.role !== Roles.Workspace.Guest
"
v-tippy="
@@ -84,13 +85,44 @@
<template #joined="{ item }">
<span class="text-foreground-2">{{ formattedFullDate(item.joinDate) }}</span>
</template>
<template #projects="{ item }">
<FormButton
v-if="
item.projectRoles.length > 0 &&
isWorkspaceAdmin &&
item.role !== Roles.Workspace.Admin
"
color="subtle"
size="sm"
class="!font-normal !text-foreground-2 -ml-2"
@click="
() => {
targetUser = item
showProjectPermissionsDialog = true
}
"
>
{{ item.projectRoles.length }}
{{ item.projectRoles.length === 1 ? 'project' : 'projects' }}
</FormButton>
<div v-else class="text-foreground-2 max-w-max text-body-2xs select-none">
{{ item.projectRoles.length }}
{{ item.projectRoles.length === 1 ? 'project' : 'projects' }}
</div>
</template>
<template #actions="{ item }">
<SettingsWorkspacesMembersActionsMenu
:target-user="item"
:workspace="workspace"
:initial-action="selectedAction[item.id]"
/>
</template>
</LayoutTable>
<SettingsWorkspacesMembersActionsProjectPermissionsDialog
v-model:open="showProjectPermissionsDialog"
:user="targetUser"
:workspace-id="workspace?.id || ''"
/>
</div>
</template>
@@ -100,25 +132,23 @@ import { settingsWorkspacesMembersSearchQuery } from '~~/lib/settings/graphql/qu
import { useQuery } from '@vue/apollo-composable'
import {
WorkspaceSeatType,
type SettingsWorkspacesMembersTable_WorkspaceFragment
type SettingsWorkspacesMembersTable_WorkspaceFragment,
type SettingsWorkspacesMembersActionsMenu_UserFragment
} from '~~/lib/common/generated/gql/graphql'
import { graphql } from '~/lib/common/generated/gql'
import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
import { LearnMoreRolesSeatsUrl } from '~~/lib/common/helpers/route'
export type UserItem = (typeof members)['value'][0]
import type { WorkspaceUserActionTypes } from '~/lib/settings/helpers/types'
graphql(`
fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {
id
role
seatType
joinDate
user {
id
avatar
name
workspaceDomainPolicyCompliant(workspaceSlug: $slug)
projectRoles {
project {
id
}
}
...SettingsWorkspacesMembersActionsMenu_User
}
`)
@@ -145,6 +175,10 @@ const props = defineProps<{
const search = ref('')
const roleFilter = ref<WorkspaceRoles>()
const seatTypeFilter = ref<WorkspaceSeatType>()
const showProjectPermissionsDialog = ref(false)
const targetUser = ref<SettingsWorkspacesMembersActionsMenu_UserFragment | undefined>(
undefined
)
const { activeUser } = useActiveUser()
@@ -172,10 +206,9 @@ const members = computed(() => {
? searchResult.value?.workspaceBySlug?.team.items
: props.workspace?.team.items
return (memberArray || [])
.map(({ user, seatType, ...rest }) => ({
...user,
seatType: seatType || WorkspaceSeatType.Viewer,
...rest
.map((member) => ({
...member,
seatType: member.seatType || WorkspaceSeatType.Viewer
}))
.filter((user) => user.role !== Roles.Workspace.Guest)
})
@@ -185,4 +218,8 @@ const hasNoResults = computed(
(search.value.length || roleFilter.value || seatTypeFilter.value) &&
searchResult.value?.workspaceBySlug?.team.items.length === 0
)
const isWorkspaceAdmin = computed(() => props.workspace?.role === Roles.Workspace.Admin)
const selectedAction = ref<Record<string, WorkspaceUserActionTypes>>({})
</script>
@@ -14,18 +14,13 @@
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import { useWorkspaceUpdateRole } from '~/lib/workspaces/composables/management'
import type {
SettingsWorkspacesMembersGuestsTable_WorkspaceFragment,
SettingsWorkspacesMembersTable_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import type { SettingsWorkspacesMembersTable_WorkspaceFragment } from '~/lib/common/generated/gql/graphql'
import { useActiveUser } from '~/lib/auth/composables/activeUser'
import type { MaybeNullOrUndefined } from '@speckle/shared'
const props = defineProps<{
workspace: MaybeNullOrUndefined<
| SettingsWorkspacesMembersTable_WorkspaceFragment
| SettingsWorkspacesMembersGuestsTable_WorkspaceFragment
>
workspace: MaybeNullOrUndefined<SettingsWorkspacesMembersTable_WorkspaceFragment>
isOnlyAdmin: boolean
}>()
const emit = defineEmits<{
@@ -1,5 +1,5 @@
<template>
<div>
<div :key="computedKey">
<LayoutMenu
v-if="actionItems.length"
v-model:open="showMenu"
@@ -24,7 +24,8 @@
:workspace="workspace"
:new-role="newRole"
:is-active-user-target-user="isActiveUserTargetUser"
:is-domain-compliant="targetUser.workspaceDomainPolicyCompliant"
:is-only-admin="hasSingleAdmin"
:is-domain-compliant="targetUser.user.workspaceDomainPolicyCompliant"
@success="onDialogSuccess"
/>
@@ -58,6 +59,15 @@
v-if="dialogToShow.leaveWorkspace"
v-model:open="showDialog"
:workspace="workspace"
:is-only-admin="hasSingleAdmin"
@success="onDialogSuccess"
/>
<SettingsWorkspacesMembersActionsProjectPermissionsDialog
v-if="dialogToShow.projectPermissions"
v-model:open="showDialog"
:user="targetUser"
:workspace-id="workspace?.id || ''"
@success="onDialogSuccess"
/>
</div>
@@ -69,29 +79,49 @@ import { EllipsisHorizontalIcon, XMarkIcon } from '@heroicons/vue/24/outline'
import type { LayoutMenuItem } from '~~/lib/layout/helpers/components'
import { HorizontalDirection } from '~~/lib/common/composables/window'
import { WorkspaceUserActionTypes } from '~/lib/settings/helpers/types'
import type { UserItem } from '~/components/settings/workspaces/members/MembersTable.vue'
import { useSettingsMembersActions } from '~/lib/settings/composables/menu'
import type {
SettingsWorkspacesMembersGuestsTable_WorkspaceFragment,
SettingsWorkspacesMembersActionsMenu_UserFragment,
SettingsWorkspacesMembersTable_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import { useWorkspaceLastAdminCheck } from '~/lib/workspaces/composables/management'
import { graphql } from '~/lib/common/generated/gql'
graphql(`
fragment SettingsWorkspacesMembersActionsMenu_User on WorkspaceCollaborator {
id
role
seatType
joinDate
user {
id
name
avatar
workspaceDomainPolicyCompliant(workspaceSlug: $slug)
}
...SettingsWorkspacesMembersActionsProjectPermissionsDialog_User
}
`)
const props = defineProps<{
targetUser: UserItem
workspace?: MaybeNullOrUndefined<
| SettingsWorkspacesMembersTable_WorkspaceFragment
| SettingsWorkspacesMembersGuestsTable_WorkspaceFragment
>
targetUser: SettingsWorkspacesMembersActionsMenu_UserFragment
workspace?: MaybeNullOrUndefined<SettingsWorkspacesMembersTable_WorkspaceFragment>
}>()
const showMenu = ref(false)
const showDialog = ref(false)
const dialogType = ref<WorkspaceUserActionTypes>()
const { hasSingleAdmin } = useWorkspaceLastAdminCheck({
workspaceSlug: props.workspace?.slug || ''
})
const computedKey = computed(() => `${props.targetUser.id}-${props.targetUser.role}`)
const { actionItems, isActiveUserTargetUser } = useSettingsMembersActions({
workspaceRole: props.workspace?.role,
workspaceSlug: props.workspace?.slug,
targetUser: props.targetUser
workspaceRole: computed(() => props.workspace?.role),
workspaceSlug: computed(() => props.workspace?.slug),
targetUser: computed(() => props.targetUser)
})
const dialogToShow = computed(() => ({
@@ -106,7 +136,9 @@ const dialogToShow = computed(() => ({
dialogType.value === WorkspaceUserActionTypes.DowngradeEditor,
removeFromWorkspace:
dialogType.value === WorkspaceUserActionTypes.RemoveFromWorkspace,
leaveWorkspace: dialogType.value === WorkspaceUserActionTypes.LeaveWorkspace
leaveWorkspace: dialogType.value === WorkspaceUserActionTypes.LeaveWorkspace,
projectPermissions:
dialogType.value === WorkspaceUserActionTypes.UpdateProjectPermissions
}))
const newRole = computed(() => {
@@ -118,7 +150,8 @@ const newRole = computed(() => {
[WorkspaceUserActionTypes.UpgradeEditor]: undefined,
[WorkspaceUserActionTypes.DowngradeEditor]: undefined,
[WorkspaceUserActionTypes.RemoveFromWorkspace]: undefined,
[WorkspaceUserActionTypes.LeaveWorkspace]: undefined
[WorkspaceUserActionTypes.LeaveWorkspace]: undefined,
[WorkspaceUserActionTypes.UpdateProjectPermissions]: Roles.Workspace.Admin
}
return dialogType.value ? roleMap[dialogType.value] : undefined
})
@@ -0,0 +1,183 @@
<template>
<LayoutDialog v-model:open="open" max-width="sm">
<template #header>Manage project access</template>
<div class="text-foreground mb-8">
<div v-if="projectCount && projectCount > 0" class="flex flex-col gap-4">
<p class="font-medium text-body-xs">
Projects {{ user?.user.name }} has access to:
</p>
<FormTextInput
v-bind="searchBind"
name="searchGuests"
color="foundation"
type="text"
size="lg"
:placeholder="`Search ${projectCount} project${
projectCount !== 1 ? 's' : ''
}...`"
class="px-3 py-2 border border-outline-3 rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
v-on="searchOn"
/>
<CommonCard
class="border border-outline-3 bg-foundation-2 text-body-2xs !p-2 flex flex-col gap-2"
>
<div
v-for="projectRole in filteredProjectRoles"
:key="projectRole.project.id"
class="flex items-center relative"
>
<div class="text-body-xs flex-1 relative z-10 mr-40">
<NuxtLink
:to="projectRoute(projectRole.project.id)"
target="_blank"
class="group flex gap-1 items-center max-w-max border-b border-transparent hover:border-gray-300/90"
>
{{ projectRole.project.name }}
<ArrowTopRightOnSquareIcon
class="hidden group-hover:block w-3 h-3 opacity-60"
/>
</NuxtLink>
</div>
<div class="flex items-center gap-2 absolute right-0">
<ProjectPageTeamPermissionSelect
:model-value="projectRole.role"
:disabled="false"
mount-menu-on-body
hide-owner
show-remove
@update:model-value="
(newRole) => updateProjectRole(projectRole.project.id, newRole)
"
/>
<FormButton
color="outline"
size="sm"
@click="
() => {
projectToRemove = {
id: projectRole.project.id,
name: projectRole.project.name
}
showRemoveUserFromProjectConfirmationDialog = true
}
"
>
Remove
</FormButton>
</div>
</div>
</CommonCard>
</div>
<div v-else>This user doesn't have access to any projects in this workspace.</div>
</div>
<LayoutDialog
v-model:open="showRemoveUserFromProjectConfirmationDialog"
:buttons="dialogButtons"
max-width="xs"
>
<template #header>Remove user from project?</template>
<CommonCard class="!p-2 border border-outline-3 bg-foundation-2">
<div class="flex items-center gap-2">
<UserAvatar :user="user?.user" />
<div class="text-body-xs">
{{ user?.user.name }}
</div>
</div>
</CommonCard>
<div class="text-body-xs my-2">
Are you sure you want to remove this user from
<span class="font-medium">{{ projectToRemove?.name }}</span>
?
</div>
</LayoutDialog>
</LayoutDialog>
</template>
<script setup lang="ts">
import type { StreamRoles } from '@speckle/shared'
import { useUpdateUserRole } from '~~/lib/projects/composables/projectManagement'
import { useDebouncedTextInput, type LayoutDialogButton } from '@speckle/ui-components'
import { graphql } from '~/lib/common/generated/gql'
import type { SettingsWorkspacesMembersActionsMenu_UserFragment } from '~/lib/common/generated/gql/graphql'
import { projectRoute } from '~~/lib/common/helpers/route'
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/outline'
graphql(`
fragment SettingsWorkspacesMembersActionsProjectPermissionsDialog_User on WorkspaceCollaborator {
projectRoles {
project {
id
name
}
role
}
}
`)
const props = defineProps<{
user?: SettingsWorkspacesMembersActionsMenu_UserFragment
workspaceId: string
}>()
const open = defineModel<boolean>('open', { required: true })
const loading = ref(false)
const showRemoveUserFromProjectConfirmationDialog = ref(false)
const projectToRemove = ref<{ id: string; name: string } | null>(null)
const searchTerm = ref('')
const { on: searchOn, bind: searchBind } = useDebouncedTextInput({
model: searchTerm,
debouncedBy: 300
})
const project = computed(() => ({ workspaceId: props.workspaceId }))
const filteredProjectRoles = computed(() => {
const roles = props.user?.projectRoles
if (!searchTerm.value) return roles || []
return (roles || []).filter((projectRole) =>
projectRole.project.name.toLowerCase().includes(searchTerm.value.toLowerCase())
)
})
const updateRole = useUpdateUserRole(project)
const updateProjectRole = async (projectId: string, newRole: StreamRoles | null) => {
if (!props.user) return
loading.value = true
await updateRole({
projectId,
userId: props.user.id,
role: newRole
})
loading.value = false
}
const projectCount = computed(() => props.user?.projectRoles?.length)
const dialogButtons = computed((): LayoutDialogButton[] => [
{
text: 'Cancel',
props: { color: 'outline' },
onClick: () => {
showRemoveUserFromProjectConfirmationDialog.value = false
projectToRemove.value = null
}
},
{
text: 'Confirm',
onClick: () => {
if (projectToRemove.value?.id) {
updateProjectRole(projectToRemove.value.id, null)
if (projectCount.value === 0) {
open.value = false
}
showRemoveUserFromProjectConfirmationDialog.value = false
projectToRemove.value = null
}
}
}
])
</script>
@@ -6,12 +6,12 @@
<div class="flex flex-row gap-x-2 items-center">
<UserAvatar
hide-tooltip
:user="user"
:user="user.user"
light-style
class="bg-foundation"
no-bg
/>
{{ user.name }}
{{ user.user.name }}
</div>
</CommonCard>
@@ -26,20 +26,16 @@
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import type { UserItem } from '~/components/settings/workspaces/members/MembersTable.vue'
import { useWorkspaceUpdateRole } from '~/lib/workspaces/composables/management'
import type {
SettingsWorkspacesMembersGuestsTable_WorkspaceFragment,
SettingsWorkspacesMembersActionsMenu_UserFragment,
SettingsWorkspacesMembersTable_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
const props = defineProps<{
user: UserItem
workspace?: MaybeNullOrUndefined<
| SettingsWorkspacesMembersTable_WorkspaceFragment
| SettingsWorkspacesMembersGuestsTable_WorkspaceFragment
>
user: SettingsWorkspacesMembersActionsMenu_UserFragment
workspace?: MaybeNullOrUndefined<SettingsWorkspacesMembersTable_WorkspaceFragment>
}>()
const emit = defineEmits<{
@@ -3,11 +3,11 @@
<template #price>
<div v-if="isUpgrading" class="ml-auto flex items-center gap-1 font-medium">
<template v-if="hasAvailableSeat || isFreePlan">
<div class="line-through text-foreground-2">£{{ seatPrice }}/month</div>
<div class="line-through text-foreground-2">{{ seatPrice }}/month</div>
<div class="text-primary">Free</div>
</template>
<template v-else>
<div class="text-foreground text-primary">£{{ seatPrice }}/month</div>
<div class="text-primary">{{ seatPrice }}/month</div>
</template>
</div>
<div v-else class="ml-auto text-primary font-medium">Free</div>
@@ -24,7 +24,7 @@ const props = defineProps<{
isUnlimitedPlan: boolean
isGuest: boolean
hasAvailableSeat: boolean
seatPrice: number
seatPrice: string
}>()
const editorDescription = computed(() =>
@@ -6,12 +6,12 @@
<div class="flex flex-row gap-x-2 items-center">
<UserAvatar
hide-tooltip
:user="user"
:user="user.user"
light-style
class="bg-foundation"
no-bg
/>
{{ user.name }}
{{ user.user.name }}
</div>
</CommonCard>
@@ -36,20 +36,18 @@
:is-free-plan="isFreePlan"
:is-unlimited-plan="isUnlimitedPlan"
:is-guest="false"
:has-available-seat="editorSeats.hasSeatAvailable"
:seat-price="editorSeats.seatPrice"
:has-available-seat="hasAvailableEditorSeats"
:seat-price="editorSeatPriceFormatted"
/>
<p
v-if="needsEditorUpgrade && !editorSeats.hasSeatAvailable"
v-if="needsEditorUpgrade && !hasAvailableEditorSeats"
class="text-foreground-2 text-body-xs mt-4"
>
You have an unused Editor seat that is already paid for, so the change will
not incur any charges.
</p>
<p
v-if="
needsEditorUpgrade && !editorSeats.hasSeatAvailable && !isUnlimitedPlan
"
v-if="needsEditorUpgrade && !hasAvailableEditorSeats && !isUnlimitedPlan"
class="text-foreground-2 text-body-xs mt-4"
>
Note that the Editor seat is a paid seat type and this change will incur
@@ -79,7 +77,6 @@
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import type { UserItem } from '~/components/settings/workspaces/members/MembersTable.vue'
import { LearnMoreRolesSeatsUrl } from '~/lib/common/helpers/route'
import { Roles, SeatTypes } from '@speckle/shared'
import { WorkspaceRoleDescriptions } from '~/lib/settings/helpers/constants'
@@ -87,17 +84,14 @@ import { useWorkspaceUpdateRole } from '~/lib/workspaces/composables/management'
import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
import SeatTransitionCards from './SeatTransitionCards.vue'
import type {
SettingsWorkspacesMembersGuestsTable_WorkspaceFragment,
SettingsWorkspacesMembersActionsMenu_UserFragment,
SettingsWorkspacesMembersTable_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
const props = defineProps<{
user: UserItem
workspace?: MaybeNullOrUndefined<
| SettingsWorkspacesMembersTable_WorkspaceFragment
| SettingsWorkspacesMembersGuestsTable_WorkspaceFragment
>
user: SettingsWorkspacesMembersActionsMenu_UserFragment
workspace?: MaybeNullOrUndefined<SettingsWorkspacesMembersTable_WorkspaceFragment>
isActiveUserTargetUser: boolean
action?: 'make' | 'remove'
}>()
@@ -109,8 +103,13 @@ const emit = defineEmits<{
const open = defineModel<boolean>('open', { required: true })
const updateUserRole = useWorkspaceUpdateRole()
const { editorSeats, isFreePlan, isUnlimitedPlan, isPurchasablePlan } =
useWorkspacePlan(props.workspace?.slug || '')
const {
hasAvailableEditorSeats,
isFreePlan,
isUnlimitedPlan,
isPurchasablePlan,
editorSeatPriceFormatted
} = useWorkspacePlan(props.workspace?.slug || '')
const needsEditorUpgrade = computed(() => {
return props.action === 'make' && props.user.seatType === SeatTypes.Viewer
@@ -2,7 +2,7 @@
<LayoutDialog v-model:open="open" max-width="sm" :buttons="dialogButtons">
<template #header>{{ title }}</template>
<CommonAlert
v-if="props.isDomainCompliant === false"
v-if="props.user.user.workspaceDomainPolicyCompliant === false"
color="danger"
hide-icon
size="xs"
@@ -21,12 +21,12 @@
<div class="flex flex-row gap-x-2 items-center">
<UserAvatar
hide-tooltip
:user="user"
:user="user.user"
light-style
class="bg-foundation"
no-bg
/>
{{ user.name }}
{{ user.user.name }}
</div>
</CommonCard>
@@ -50,25 +50,20 @@
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import type { UserItem } from '~/components/settings/workspaces/members/MembersTable.vue'
import { LearnMoreRolesSeatsUrl } from '~/lib/common/helpers/route'
import { Roles } from '@speckle/shared'
import { WorkspaceRoleDescriptions } from '~/lib/settings/helpers/constants'
import { useWorkspaceUpdateRole } from '~/lib/workspaces/composables/management'
import type {
SettingsWorkspacesMembersGuestsTable_WorkspaceFragment,
SettingsWorkspacesMembersActionsMenu_UserFragment,
SettingsWorkspacesMembersTable_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import type { MaybeNullOrUndefined } from '@speckle/shared'
const props = defineProps<{
user: UserItem
user: SettingsWorkspacesMembersActionsMenu_UserFragment
newRole: MaybeNullOrUndefined<string>
isDomainCompliant?: MaybeNullOrUndefined<boolean>
workspace?: MaybeNullOrUndefined<
| SettingsWorkspacesMembersTable_WorkspaceFragment
| SettingsWorkspacesMembersGuestsTable_WorkspaceFragment
>
workspace?: MaybeNullOrUndefined<SettingsWorkspacesMembersTable_WorkspaceFragment>
}>()
const emit = defineEmits<{
@@ -93,7 +88,7 @@ const buttonText = computed(() => {
const mainMessage = computed(() => {
if (!props.newRole) return undefined
if (props.isDomainCompliant === false) return undefined
if (props.user.user.workspaceDomainPolicyCompliant === false) return undefined
if (props.newRole === Roles.Workspace.Member) {
return 'They will be able to access all projects.'
}
@@ -102,7 +97,7 @@ const mainMessage = computed(() => {
const roleInfo = computed(() => {
if (!props.newRole) return undefined
if (props.isDomainCompliant === false) return undefined
if (props.user.user.workspaceDomainPolicyCompliant === false) return undefined
return WorkspaceRoleDescriptions[
props.newRole as keyof typeof WorkspaceRoleDescriptions
]
@@ -133,7 +128,7 @@ const dialogButtons = computed((): LayoutDialogButton[] => [
color: 'primary'
},
onClick: handleConfirm,
disabled: props.isDomainCompliant === false
disabled: props.user.user.workspaceDomainPolicyCompliant === false
}
])
</script>
@@ -2,15 +2,15 @@
<LayoutDialog v-model:open="open" max-width="sm" :buttons="dialogButtons">
<template #header>{{ title }}</template>
<div class="flex flex-col mb-4">
<p class="text-body-sm mb-4">Confirm {{ user.name }}'s new seat.</p>
<p class="text-body-sm mb-4">Confirm {{ user.user.name }}'s new seat.</p>
<SeatTransitionCards
:is-upgrading="isUpgrading"
:is-free-plan="isFreePlan"
:is-unlimited-plan="isUnlimitedPlan"
:is-guest="user.role === Roles.Workspace.Guest"
:has-available-seat="editorSeats.hasSeatAvailable"
:seat-price="editorSeats.seatPrice"
:has-available-seat="hasAvailableEditorSeats"
:seat-price="editorSeatPriceFormatted"
/>
<p v-if="billingMessage" class="text-foreground-2 text-body-xs mt-4">
@@ -29,7 +29,6 @@
<script setup lang="ts">
import type { LayoutDialogButton } from '@speckle/ui-components'
import type { UserItem } from '~/components/settings/workspaces/members/MembersTable.vue'
import {
SeatTypes,
type WorkspaceSeatType,
@@ -40,17 +39,14 @@ import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
import { LearnMoreRolesSeatsUrl } from '~/lib/common/helpers/route'
import SeatTransitionCards from './SeatTransitionCards.vue'
import type {
SettingsWorkspacesMembersGuestsTable_WorkspaceFragment,
SettingsWorkspacesMembersActionsMenu_UserFragment,
SettingsWorkspacesMembersTable_WorkspaceFragment
} from '~/lib/common/generated/gql/graphql'
import { Roles } from '@speckle/shared'
const props = defineProps<{
user: UserItem
workspace?: MaybeNullOrUndefined<
| SettingsWorkspacesMembersTable_WorkspaceFragment
| SettingsWorkspacesMembersGuestsTable_WorkspaceFragment
>
user: SettingsWorkspacesMembersActionsMenu_UserFragment
workspace?: MaybeNullOrUndefined<SettingsWorkspacesMembersTable_WorkspaceFragment>
}>()
const emit = defineEmits<{
@@ -61,8 +57,8 @@ const open = defineModel<boolean>('open', { required: true })
const updateUserSeatType = useWorkspaceUpdateSeatType()
const {
editorSeats,
totalCostFormatted,
hasAvailableEditorSeats,
editorSeatPriceFormatted,
billingCycleEnd,
isPurchasablePlan,
isFreePlan,
@@ -76,9 +72,9 @@ const annualOrMonthly = computed(() => (intervalIsYearly.value ? 'year' : 'month
const billingMessage = computed(() => {
if (isFreePlan.value) return null
if (isUpgrading.value) {
return editorSeats.value.hasSeatAvailable
return hasAvailableEditorSeats.value
? 'You have an unused Editor seat that is already paid for, so the change will not incur any charges.'
: `This adds an extra Editor seat to your subscription, increasing your total billing to ${totalCostFormatted.value}/${annualOrMonthly.value}.`
: `This adds an extra Editor seat to your subscription, increasing your total billing by ${editorSeatPriceFormatted.value}/${annualOrMonthly.value}.`
} else {
return isPurchasablePlan.value
? `The Editor seat will still be paid for until your plan renews on ${billingCycleEnd.value}. You can freely reassign it to another person.`
@@ -1,10 +1,4 @@
import {
Roles,
WorkspaceGuestSeatType,
WorkspacePlanBillingIntervals,
type PaidWorkspacePlans,
type WorkspaceRoles
} from '@speckle/shared'
import { WorkspacePlanBillingIntervals, type PaidWorkspacePlans } from '@speckle/shared'
import { useQuery } from '@vue/apollo-composable'
import { graphql } from '~/lib/common/generated/gql'
@@ -29,17 +23,6 @@ const workspacePlanPricesQuery = graphql(`
`)
type WorkspacePlanPrices = {
[plan in PaidWorkspacePlans]: {
[interval in WorkspacePlanBillingIntervals]?: {
[role in WorkspaceRoles]: {
amount: number
currencySymbol: string
}
}
}
}
type WorkspacePlanPricesNew = {
[plan in PaidWorkspacePlans]: {
[interval in WorkspacePlanBillingIntervals]?: {
amount: number
@@ -58,39 +41,6 @@ export const useWorkspacePlanPrices = () => {
const base = result.value?.serverInfo?.workspaces?.planPrices
if (!base) return undefined
const guestSeatPrices = base.find((p) => p.id === 'guest')
return base.reduce((acc, price) => {
if (price.id === WorkspaceGuestSeatType) return acc
acc[price.id as keyof WorkspacePlanPrices] = {
...(price.monthly
? {
[WorkspacePlanBillingIntervals.Monthly]: {
[Roles.Workspace.Guest]: guestSeatPrices?.monthly || price.monthly,
[Roles.Workspace.Member]: price.monthly,
[Roles.Workspace.Admin]: price.monthly
}
}
: {}),
...(price.yearly
? {
[WorkspacePlanBillingIntervals.Yearly]: {
[Roles.Workspace.Guest]: guestSeatPrices?.yearly || price.yearly,
[Roles.Workspace.Member]: price.yearly,
[Roles.Workspace.Admin]: price.yearly
}
}
: {})
}
return acc
}, {} as WorkspacePlanPrices)
})
const pricesNew = computed(() => {
const base = result.value?.serverInfo?.workspaces?.planPrices
if (!base) return undefined
return Object.fromEntries(
base.map(({ id, monthly, yearly }) => [
id,
@@ -99,8 +49,8 @@ export const useWorkspacePlanPrices = () => {
...(yearly ? { [WorkspacePlanBillingIntervals.Yearly]: yearly } : {})
}
])
) as WorkspacePlanPricesNew
) as WorkspacePlanPrices
})
return { prices, pricesNew }
return { prices }
}
@@ -45,14 +45,14 @@ type Documents = {
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": typeof types.FormSelectModels_ModelFragmentDoc,
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": typeof types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": typeof types.FormUsersSelectItemFragmentDoc,
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc,
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": typeof types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
"\n fragment HeaderNavNotificationsWorkspaceInvite_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": typeof types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n ...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace\n }\n": typeof types.HeaderWorkspaceSwitcherActiveWorkspace_WorkspaceFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n": typeof types.HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n": typeof types.HeaderWorkspaceSwitcherWorkspaceList_UserFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n": typeof types.HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": typeof types.HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc,
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": typeof types.HeaderNavShare_ProjectFragmentDoc,
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": typeof types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
"\n fragment HeaderNavNotificationsWorkspaceInvite_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": typeof types.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n": typeof types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": typeof types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": typeof types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc,
@@ -117,14 +117,14 @@ type Documents = {
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n }\n": typeof types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n": typeof types.SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragmentDoc,
"\n fragment WorkspaceBillingPage_Workspace on Workspace {\n id\n role\n }\n": typeof types.WorkspaceBillingPage_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersGuestsTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n slug\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n inviteId\n role\n title\n updatedAt\n user {\n id\n ...LimitedUserAvatar\n }\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n }\n": typeof types.SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n invitedTeam {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n": typeof types.SettingsWorkspacesMembersInvitesTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {\n ...SettingsWorkspacesMembersTableHeader_Workspace\n id\n adminWorkspacesJoinRequests {\n totalCount\n items {\n ...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest\n id\n createdAt\n status\n user {\n id\n avatar\n name\n }\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersRequestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n }\n": typeof types.SettingsWorkspacesMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n projectRoles {\n project {\n id\n }\n }\n ...SettingsWorkspacesMembersActionsMenu_User\n }\n": typeof types.SettingsWorkspacesMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersTable_Workspace on Workspace {\n id\n slug\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersTable_WorkspaceCollaborator\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...InviteDialogWorkspace_Workspace\n }\n": typeof types.SettingsWorkspacesMembersTableHeader_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersActionsMenu_User on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n name\n avatar\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n ...SettingsWorkspacesMembersActionsProjectPermissionsDialog_User\n }\n": typeof types.SettingsWorkspacesMembersActionsMenu_UserFragmentDoc,
"\n fragment SettingsWorkspacesMembersActionsProjectPermissionsDialog_User on WorkspaceCollaborator {\n projectRoles {\n project {\n id\n name\n }\n role\n }\n }\n": typeof types.SettingsWorkspacesMembersActionsProjectPermissionsDialog_UserFragmentDoc,
"\n fragment SettingsWorkspacesRegionsSelect_ServerRegionItem on ServerRegionItem {\n id\n key\n name\n description\n }\n": typeof types.SettingsWorkspacesRegionsSelect_ServerRegionItemFragmentDoc,
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {\n id\n domain\n }\n": typeof types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragmentDoc,
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": typeof types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
@@ -310,7 +310,6 @@ type Documents = {
"\n query SettingsWorkspaceRegions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n serverInfo {\n ...SettingsWorkspacesRegions_ServerInfo\n }\n }\n": typeof types.SettingsWorkspaceRegionsDocument,
"\n query SettingsWorkspacesMembers($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n }\n }\n": typeof types.SettingsWorkspacesMembersDocument,
"\n query SettingsWorkspacesMembersTable($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersTable_Workspace\n }\n }\n": typeof types.SettingsWorkspacesMembersTableDocument,
"\n query SettingsWorkspacesMembersGuests($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n }\n }\n": typeof types.SettingsWorkspacesMembersGuestsDocument,
"\n query SettingsWorkspacesMembersInvites($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": typeof types.SettingsWorkspacesMembersInvitesDocument,
"\n query SettingsWorkspacesMembersRequests($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n": typeof types.SettingsWorkspacesMembersRequestsDocument,
"\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter, limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n": typeof types.SettingsWorkspacesMembersSearchDocument,
@@ -344,6 +343,7 @@ type Documents = {
"\n subscription OnViewerUserActivityBroadcasted(\n $target: ViewerUpdateTrackingTarget!\n $sessionId: String!\n ) {\n viewerUserActivityBroadcasted(target: $target, sessionId: $sessionId) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\n }\n }\n": typeof types.OnViewerUserActivityBroadcastedDocument,
"\n subscription OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\n }\n }\n }\n": typeof types.OnViewerCommentsUpdatedDocument,
"\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n": typeof types.LinkableCommentFragmentDoc,
"\n fragment ActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n }\n": typeof types.ActiveWorkspace_WorkspaceFragmentDoc,
"\n fragment DiscoverableList_Discoverable on User {\n discoverableWorkspaces {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n": typeof types.DiscoverableList_DiscoverableFragmentDoc,
"\n fragment DiscoverableList_Requests on User {\n workspaceJoinRequests {\n items {\n id\n status\n workspace {\n id\n name\n logo\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n }\n }\n": typeof types.DiscoverableList_RequestsFragmentDoc,
"\n fragment WorkspacePlanLimits_Workspace on Workspace {\n id\n plan {\n name\n }\n }\n": typeof types.WorkspacePlanLimits_WorkspaceFragmentDoc,
@@ -389,6 +389,7 @@ type Documents = {
"\n query DiscoverableWorkspaces {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n": typeof types.DiscoverableWorkspacesDocument,
"\n query DiscoverableWorkspacesRequests {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n": typeof types.DiscoverableWorkspacesRequestsDocument,
"\n query WorkspacePlan($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacesPlan_Workspace\n }\n }\n": typeof types.WorkspacePlanDocument,
"\n query activeWorkspace($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...ActiveWorkspace_Workspace\n }\n }\n": typeof types.ActiveWorkspaceDocument,
"\n query WorkspaceLastAdminCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceLastAdminCheck_Workspace\n }\n }\n": typeof types.WorkspaceLastAdminCheckDocument,
"\n query WorkspaceLimits($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacePlanLimits_Workspace\n }\n }\n": typeof types.WorkspaceLimitsDocument,
"\n query WorkspaceUsage($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceUsage_Workspace\n }\n }\n": typeof types.WorkspaceUsageDocument,
@@ -446,14 +447,14 @@ const documents: Documents = {
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": types.FormSelectModels_ModelFragmentDoc,
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc,
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc,
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
"\n fragment HeaderNavNotificationsWorkspaceInvite_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.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n ...HeaderWorkspaceSwitcherHeaderWorkspace_Workspace\n }\n": types.HeaderWorkspaceSwitcherActiveWorkspace_WorkspaceFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n creationState {\n completed\n }\n plan {\n name\n }\n }\n": types.HeaderWorkspaceSwitcherWorkspaceList_WorkspaceFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherWorkspaceList_User on User {\n id\n expiredSsoSessions {\n id\n ...HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace\n }\n workspaces {\n items {\n id\n ...HeaderWorkspaceSwitcherWorkspaceList_Workspace\n }\n }\n }\n": types.HeaderWorkspaceSwitcherWorkspaceList_UserFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspace on LimitedWorkspace {\n id\n slug\n name\n logo\n }\n": types.HeaderWorkspaceSwitcherHeaderExpiredSso_LimitedWorkspaceFragmentDoc,
"\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n": types.HeaderWorkspaceSwitcherHeaderWorkspace_WorkspaceFragmentDoc,
"\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n": types.HeaderNavShare_ProjectFragmentDoc,
"\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n": types.HeaderNavNotificationsProjectInvite_PendingStreamCollaboratorFragmentDoc,
"\n fragment HeaderNavNotificationsWorkspaceInvite_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.HeaderNavNotificationsWorkspaceInvite_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment InviteDialogWorkspace_Workspace on Workspace {\n id\n name\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n": types.InviteDialogWorkspace_WorkspaceFragmentDoc,
"\n fragment InviteDialogProject_Project on Project {\n id\n name\n ...InviteDialogProjectWorkspaceMembers_Project\n workspace {\n id\n name\n role\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": types.InviteDialogProject_ProjectFragmentDoc,
"\n fragment InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaborator on WorkspaceCollaborator {\n role\n id\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n": types.InviteDialogProjectWorkspaceMembersRow_WorkspaceCollaboratorFragmentDoc,
@@ -518,14 +519,14 @@ const documents: Documents = {
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n }\n": types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesGeneralEditSlugDialog_Workspace on Workspace {\n id\n name\n slug\n }\n": types.SettingsWorkspacesGeneralEditSlugDialog_WorkspaceFragmentDoc,
"\n fragment WorkspaceBillingPage_Workspace on Workspace {\n id\n role\n }\n": types.WorkspaceBillingPage_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n slug\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersGuestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n inviteId\n role\n title\n updatedAt\n user {\n id\n ...LimitedUserAvatar\n }\n invitedBy {\n id\n ...LimitedUserAvatar\n }\n }\n": types.SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersInvitesTable_Workspace on Workspace {\n id\n ...SettingsWorkspacesMembersTableHeader_Workspace\n invitedTeam {\n ...SettingsWorkspacesMembersInvitesTable_PendingWorkspaceCollaborator\n }\n }\n": types.SettingsWorkspacesMembersInvitesTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersRequestsTable_Workspace on Workspace {\n ...SettingsWorkspacesMembersTableHeader_Workspace\n id\n adminWorkspacesJoinRequests {\n totalCount\n items {\n ...WorkspaceJoinRequestApproveDialog_WorkspaceJoinRequest\n id\n createdAt\n status\n user {\n id\n avatar\n name\n }\n }\n }\n }\n": types.SettingsWorkspacesMembersRequestsTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n }\n": types.SettingsWorkspacesMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n projectRoles {\n project {\n id\n }\n }\n ...SettingsWorkspacesMembersActionsMenu_User\n }\n": types.SettingsWorkspacesMembersTable_WorkspaceCollaboratorFragmentDoc,
"\n fragment SettingsWorkspacesMembersTable_Workspace on Workspace {\n id\n slug\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersTable_WorkspaceCollaborator\n }\n }\n }\n": types.SettingsWorkspacesMembersTable_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...InviteDialogWorkspace_Workspace\n }\n": types.SettingsWorkspacesMembersTableHeader_WorkspaceFragmentDoc,
"\n fragment SettingsWorkspacesMembersActionsMenu_User on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n name\n avatar\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n ...SettingsWorkspacesMembersActionsProjectPermissionsDialog_User\n }\n": types.SettingsWorkspacesMembersActionsMenu_UserFragmentDoc,
"\n fragment SettingsWorkspacesMembersActionsProjectPermissionsDialog_User on WorkspaceCollaborator {\n projectRoles {\n project {\n id\n name\n }\n role\n }\n }\n": types.SettingsWorkspacesMembersActionsProjectPermissionsDialog_UserFragmentDoc,
"\n fragment SettingsWorkspacesRegionsSelect_ServerRegionItem on ServerRegionItem {\n id\n key\n name\n description\n }\n": types.SettingsWorkspacesRegionsSelect_ServerRegionItemFragmentDoc,
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain on WorkspaceDomain {\n id\n domain\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragmentDoc,
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
@@ -711,7 +712,6 @@ const documents: Documents = {
"\n query SettingsWorkspaceRegions($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n ...SettingsWorkspacesRegions_Workspace\n }\n serverInfo {\n ...SettingsWorkspacesRegions_ServerInfo\n }\n }\n": types.SettingsWorkspaceRegionsDocument,
"\n query SettingsWorkspacesMembers($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembers_Workspace\n }\n }\n": types.SettingsWorkspacesMembersDocument,
"\n query SettingsWorkspacesMembersTable($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersTableDocument,
"\n query SettingsWorkspacesMembersGuests($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersGuestsDocument,
"\n query SettingsWorkspacesMembersInvites($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersInvitesTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersInvitesDocument,
"\n query SettingsWorkspacesMembersRequests($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersRequestsTable_Workspace\n }\n }\n": types.SettingsWorkspacesMembersRequestsDocument,
"\n query SettingsWorkspacesMembersSearch($slug: String!, $filter: WorkspaceTeamFilter) {\n workspaceBySlug(slug: $slug) {\n id\n team(filter: $filter, limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersTable_WorkspaceCollaborator\n }\n }\n }\n }\n": types.SettingsWorkspacesMembersSearchDocument,
@@ -745,6 +745,7 @@ const documents: Documents = {
"\n subscription OnViewerUserActivityBroadcasted(\n $target: ViewerUpdateTrackingTarget!\n $sessionId: String!\n ) {\n viewerUserActivityBroadcasted(target: $target, sessionId: $sessionId) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\n }\n }\n": types.OnViewerUserActivityBroadcastedDocument,
"\n subscription OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\n }\n }\n }\n": types.OnViewerCommentsUpdatedDocument,
"\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n": types.LinkableCommentFragmentDoc,
"\n fragment ActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n }\n": types.ActiveWorkspace_WorkspaceFragmentDoc,
"\n fragment DiscoverableList_Discoverable on User {\n discoverableWorkspaces {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n": types.DiscoverableList_DiscoverableFragmentDoc,
"\n fragment DiscoverableList_Requests on User {\n workspaceJoinRequests {\n items {\n id\n status\n workspace {\n id\n name\n logo\n slug\n team {\n totalCount\n items {\n avatar\n }\n }\n }\n }\n }\n }\n": types.DiscoverableList_RequestsFragmentDoc,
"\n fragment WorkspacePlanLimits_Workspace on Workspace {\n id\n plan {\n name\n }\n }\n": types.WorkspacePlanLimits_WorkspaceFragmentDoc,
@@ -790,6 +791,7 @@ const documents: Documents = {
"\n query DiscoverableWorkspaces {\n activeUser {\n id\n ...DiscoverableList_Discoverable\n }\n }\n": types.DiscoverableWorkspacesDocument,
"\n query DiscoverableWorkspacesRequests {\n activeUser {\n id\n ...DiscoverableList_Requests\n }\n }\n": types.DiscoverableWorkspacesRequestsDocument,
"\n query WorkspacePlan($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacesPlan_Workspace\n }\n }\n": types.WorkspacePlanDocument,
"\n query activeWorkspace($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...ActiveWorkspace_Workspace\n }\n }\n": types.ActiveWorkspaceDocument,
"\n query WorkspaceLastAdminCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceLastAdminCheck_Workspace\n }\n }\n": types.WorkspaceLastAdminCheckDocument,
"\n query WorkspaceLimits($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacePlanLimits_Workspace\n }\n }\n": types.WorkspaceLimitsDocument,
"\n query WorkspaceUsage($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspaceUsage_Workspace\n }\n }\n": types.WorkspaceUsageDocument,
@@ -954,6 +956,18 @@ export function graphql(source: "\n fragment FormSelectProjects_Project on Proj
* 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 FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n"): (typeof documents)["\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\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 HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n"): (typeof documents)["\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\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 HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n"): (typeof documents)["\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\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 fragment HeaderNavNotificationsWorkspaceInvite_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"): (typeof documents)["\n fragment HeaderNavNotificationsWorkspaceInvite_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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -974,18 +988,6 @@ export function graphql(source: "\n fragment HeaderWorkspaceSwitcherHeaderExpir
* 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 HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\n }\n }\n"): (typeof documents)["\n fragment HeaderWorkspaceSwitcherHeaderWorkspace_Workspace on Workspace {\n ...InviteDialogWorkspace_Workspace\n id\n name\n logo\n role\n plan {\n name\n }\n team {\n totalCount\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 fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\n }\n"): (typeof documents)["\n fragment HeaderNavShare_Project on Project {\n id\n visibility\n ...ProjectsModelPageEmbed_Project\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 HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\n }\n }\n"): (typeof documents)["\n fragment HeaderNavNotificationsProjectInvite_PendingStreamCollaborator on PendingStreamCollaborator {\n id\n invitedBy {\n ...LimitedUserAvatar\n }\n projectId\n projectName\n token\n user {\n id\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 fragment HeaderNavNotificationsWorkspaceInvite_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"): (typeof documents)["\n fragment HeaderNavNotificationsWorkspaceInvite_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"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1242,14 +1244,6 @@ export function graphql(source: "\n fragment SettingsWorkspacesGeneralEditSlugD
* 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 WorkspaceBillingPage_Workspace on Workspace {\n id\n role\n }\n"): (typeof documents)["\n fragment WorkspaceBillingPage_Workspace on Workspace {\n id\n role\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 SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n projectRoles {\n role\n project {\n id\n name\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n projectRoles {\n role\n project {\n id\n name\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 fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n slug\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersGuestsTable_Workspace on Workspace {\n id\n slug\n name\n ...SettingsWorkspacesMembersTableHeader_Workspace\n team(limit: 250) {\n items {\n id\n ...SettingsWorkspacesMembersGuestsTable_WorkspaceCollaborator\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1265,7 +1259,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembersRequestsT
/**
* 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 SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n avatar\n name\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n }\n"];
export function graphql(source: "\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n projectRoles {\n project {\n id\n }\n }\n ...SettingsWorkspacesMembersActionsMenu_User\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {\n id\n projectRoles {\n project {\n id\n }\n }\n ...SettingsWorkspacesMembersActionsMenu_User\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -1274,6 +1268,14 @@ export function graphql(source: "\n fragment SettingsWorkspacesMembersTable_Wor
* 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 SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...InviteDialogWorkspace_Workspace\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersTableHeader_Workspace on Workspace {\n id\n role\n ...InviteDialogWorkspace_Workspace\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 SettingsWorkspacesMembersActionsMenu_User on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n name\n avatar\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n ...SettingsWorkspacesMembersActionsProjectPermissionsDialog_User\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersActionsMenu_User on WorkspaceCollaborator {\n id\n role\n seatType\n joinDate\n user {\n id\n name\n avatar\n workspaceDomainPolicyCompliant(workspaceSlug: $slug)\n }\n ...SettingsWorkspacesMembersActionsProjectPermissionsDialog_User\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 SettingsWorkspacesMembersActionsProjectPermissionsDialog_User on WorkspaceCollaborator {\n projectRoles {\n project {\n id\n name\n }\n role\n }\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesMembersActionsProjectPermissionsDialog_User on WorkspaceCollaborator {\n projectRoles {\n project {\n id\n name\n }\n role\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2014,10 +2016,6 @@ export function graphql(source: "\n query SettingsWorkspacesMembers($slug: Stri
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query SettingsWorkspacesMembersTable($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersTable_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesMembersTable($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersTable_Workspace\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 query SettingsWorkspacesMembersGuests($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n }\n }\n"): (typeof documents)["\n query SettingsWorkspacesMembersGuests($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...SettingsWorkspacesMembersGuestsTable_Workspace\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2150,6 +2148,10 @@ export function graphql(source: "\n subscription OnViewerCommentsUpdated($targe
* 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 LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n"): (typeof documents)["\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\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 fragment ActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n }\n"): (typeof documents)["\n fragment ActiveWorkspace_Workspace on Workspace {\n id\n name\n logo\n role\n slug\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -2330,6 +2332,10 @@ export function graphql(source: "\n query DiscoverableWorkspacesRequests {\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 query WorkspacePlan($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacesPlan_Workspace\n }\n }\n"): (typeof documents)["\n query WorkspacePlan($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...WorkspacesPlan_Workspace\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 query activeWorkspace($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...ActiveWorkspace_Workspace\n }\n }\n"): (typeof documents)["\n query activeWorkspace($slug: String!) {\n workspaceBySlug(slug: $slug) {\n ...ActiveWorkspace_Workspace\n }\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
@@ -5,7 +5,6 @@ import {
} from '~/lib/settings/helpers/types'
import { useIsMultipleEmailsEnabled, useActiveUser } from '~/composables/globals'
import { Roles, SeatTypes, type MaybeNullOrUndefined } from '@speckle/shared'
import type { UserItem } from '~/components/settings/workspaces/members/MembersTable.vue'
import { useIsMultiregionEnabled } from '~/lib/multiregion/composables/main'
import { graphql } from '~/lib/common/generated/gql'
import {
@@ -14,6 +13,7 @@ import {
settingsServerRoutes
} from '~/lib/common/helpers/route'
import type { LayoutMenuItem } from '@speckle/ui-components'
import type { SettingsWorkspacesMembersActionsMenu_UserFragment } from '~/lib/common/generated/gql/graphql'
import { useWorkspaceLastAdminCheck } from '~/lib/workspaces/composables/management'
graphql(`
@@ -143,24 +143,24 @@ export const useSettingsMenuState = () =>
}))
export const useSettingsMembersActions = (params: {
workspaceRole?: MaybeNullOrUndefined<string>
workspaceSlug?: MaybeNullOrUndefined<string>
targetUser: UserItem
workspaceRole: ComputedRef<MaybeNullOrUndefined<string>>
workspaceSlug: ComputedRef<MaybeNullOrUndefined<string>>
targetUser: ComputedRef<SettingsWorkspacesMembersActionsMenu_UserFragment>
}) => {
const { activeUser } = useActiveUser()
const { hasSingleAdmin } = useWorkspaceLastAdminCheck({
workspaceSlug: params.workspaceSlug || ''
workspaceSlug: params.workspaceSlug.value || ''
})
const targetUserRole = computed(() => {
return params.targetUser.role
return params.targetUser.value.role
})
const targetUserSeatType = computed(() => params.targetUser.seatType)
const targetUserSeatType = computed(() => params.targetUser.value.seatType)
const isActiveUserWorkspaceAdmin = computed(
() => params.workspaceRole === Roles.Workspace.Admin
() => params.workspaceRole.value === Roles.Workspace.Admin
)
const isOnlyAdmin = computed(
@@ -168,7 +168,7 @@ export const useSettingsMembersActions = (params: {
)
const isActiveUserTargetUser = computed(
() => activeUser.value?.id === params.targetUser.id
() => activeUser.value?.id === params.targetUser.value.id
)
const canModifyUser = computed(
@@ -203,6 +203,8 @@ export const useSettingsMembersActions = (params: {
const showLeaveWorkspace = computed(() => isActiveUserTargetUser.value)
const showUpdateProjectPermissions = computed(() => canModifyUser.value)
const actionItems = computed(() => {
const mainItems: LayoutMenuItem[] = []
const footerItems: LayoutMenuItem[] = []
@@ -241,6 +243,14 @@ export const useSettingsMembersActions = (params: {
disabledTooltip: 'Admins must be on an Editor seat'
})
}
if (showUpdateProjectPermissions.value) {
mainItems.push({
title: 'Manage project access...',
id: WorkspaceUserActionTypes.UpdateProjectPermissions,
disabled: params.targetUser.value.projectRoles.length === 0,
disabledTooltip: 'User is not in any projects'
})
}
if (showRemoveAdmin.value) {
footerItems.push({
@@ -61,14 +61,6 @@ export const settingsWorkspacesMembersTableQuery = graphql(`
}
`)
export const settingsWorkspacesMembersGuestsQuery = graphql(`
query SettingsWorkspacesMembersGuests($slug: String!) {
workspaceBySlug(slug: $slug) {
...SettingsWorkspacesMembersGuestsTable_Workspace
}
}
`)
export const settingsWorkspacesMembersInvitesQuery = graphql(`
query SettingsWorkspacesMembersInvites($slug: String!) {
workspaceBySlug(slug: $slug) {
@@ -24,7 +24,8 @@ export enum WorkspaceUserActionTypes {
MakeGuest = 'make-guest',
MakeMember = 'make-member',
UpgradeEditor = 'upgrade-editor',
DowngradeEditor = 'downgrade-editor'
DowngradeEditor = 'downgrade-editor',
UpdateProjectPermissions = 'update-project-permissions'
}
export type WorkspaceUserUpdateShowOptions = {
@@ -0,0 +1,31 @@
import { graphql } from '~/lib/common/generated/gql/gql'
import { useQuery } from '@vue/apollo-composable'
import { activeWorkspaceQuery } from '~/lib/workspaces/graphql/queries'
graphql(`
fragment ActiveWorkspace_Workspace on Workspace {
id
name
logo
role
slug
}
`)
export const useActiveWorkspace = (slug: string) => {
const { result } = useQuery(
activeWorkspaceQuery,
() => ({
slug
}),
() => ({
enabled: !!slug
})
)
const activeWorkspace = computed(() => result.value?.workspaceBySlug)
return {
activeWorkspace
}
}
@@ -42,6 +42,7 @@ export const useWorkspaceLimits = (slug: string) => {
limits.value.modelCount ? limits.value.modelCount - modelCount.value : 0
)
// TODO; move to permissions
const canAddProject = computed(() => {
// Unlimited
if (limits.value.projectCount === null) return true
@@ -49,6 +50,7 @@ export const useWorkspaceLimits = (slug: string) => {
return projectCount.value + 1 <= limits.value.projectCount
})
// TODO; move to permissions
const canAddModels = (additionalModels?: number) => {
// Unlimited
if (limits.value.modelCount === null) return true
@@ -4,12 +4,16 @@ import { useQuery } from '@vue/apollo-composable'
import {
isNewWorkspacePlan,
PaidWorkspacePlansNew,
UnpaidWorkspacePlans
UnpaidWorkspacePlans,
WorkspacePlans,
WorkspacePlanBillingIntervals
} from '@speckle/shared'
import {
WorkspacePlanStatuses,
BillingInterval
} from '~/lib/common/generated/gql/graphql'
import { useWorkspacePlanPrices } from '~/lib/billing/composables/prices'
import { formatPrice } from '~/lib/billing/helpers/plan'
graphql(`
fragment WorkspacesPlan_Workspace on Workspace {
@@ -43,6 +47,7 @@ graphql(`
export const useWorkspacePlan = (slug: string) => {
const isBillingIntegrationEnabled = useIsBillingIntegrationEnabled()
const { prices } = useWorkspacePlanPrices()
const { result } = useQuery(
workspacePlanQuery,
@@ -57,81 +62,58 @@ export const useWorkspacePlan = (slug: string) => {
const subscription = computed(() => result.value?.workspaceBySlug?.subscription)
const plan = computed(() => result.value?.workspaceBySlug?.plan)
// Plan type information
const isNewPlan = computed(() =>
isNewWorkspacePlan(result.value?.workspaceBySlug?.plan?.name)
)
const statusIsExpired = computed(
() => plan.value?.status === WorkspacePlanStatuses.Expired
const isFreePlan = computed(() => plan.value?.name === UnpaidWorkspacePlans.Free)
const isUnlimitedPlan = computed(
() => plan.value?.name === UnpaidWorkspacePlans.Unlimited
)
const statusIsCanceled = computed(
() => plan.value?.status === WorkspacePlanStatuses.Canceled
)
const statusIsCancelationScheduled = computed(
() => plan.value?.status === WorkspacePlanStatuses.CancelationScheduled
)
const isPurchasablePlan = computed(() =>
Object.values(PaidWorkspacePlansNew).includes(
plan.value?.name as PaidWorkspacePlansNew
)
)
const isActivePlan = computed(
() =>
plan.value?.status === WorkspacePlanStatuses.Valid ||
plan.value?.status === WorkspacePlanStatuses.PaymentFailed ||
plan.value?.status === WorkspacePlanStatuses.CancelationScheduled
// Plan status information
const statusIsExpired = computed(
() => plan.value?.status === WorkspacePlanStatuses.Expired
)
const statusIsCanceled = computed(
() => plan.value?.status === WorkspacePlanStatuses.Canceled
)
const statusIsCancelationScheduled = computed(
() => plan.value?.status === WorkspacePlanStatuses.CancelationScheduled
)
const isFreePlan = computed(() => plan.value?.name === UnpaidWorkspacePlans.Free)
// Billing cycle information
const billingInterval = computed(() => subscription.value?.billingInterval)
const intervalIsYearly = computed(
() => billingInterval.value === BillingInterval.Yearly
)
const billingCycleEnd = computed(() => subscription.value?.currentBillingCycleEnd)
// TODO: Replace with value from API call, this a placeholder value
const editorSeatPrice = 15
const totalCost = computed(() => {
return isPurchasablePlan.value
? intervalIsYearly.value
? editorSeatPrice * 12
: editorSeatPrice
: 0
})
// TODO: Replace with value from BE once ready
const totalCostFormatted = computed(() => {
return isPurchasablePlan.value
? `£${totalCost.value}`
: isFreePlan.value
? 'Free'
: 'Not applicable'
})
const editorSeats = computed(() => {
const seats = subscription.value?.seats
if (!seats)
return { limit: 0, used: 0, hasSeatAvailable: false, seatPrice: editorSeatPrice }
return {
limit: seats.editors.available,
used: seats.editors.assigned,
hasSeatAvailable: seats.editors.available > seats.editors.assigned,
seatPrice: editorSeatPrice
}
})
const isUnlimitedPlan = computed(
() => plan.value?.name === UnpaidWorkspacePlans.Unlimited
// Seat information
const seats = computed(() => subscription.value?.seats)
const hasAvailableEditorSeats = computed(() =>
seats.value?.editors.available && seats.value?.editors.available > 0 ? true : false
)
const editorSeatPriceFormatted = computed(() => {
if (
plan.value?.name === WorkspacePlans.Team ||
plan.value?.name === WorkspacePlans.Business
) {
return formatPrice(
prices.value?.[plan.value?.name]?.[WorkspacePlanBillingIntervals.Monthly]
)
}
return formatPrice({
amount: 0,
currencySymbol: '£'
})
})
return {
plan,
@@ -139,15 +121,15 @@ export const useWorkspacePlan = (slug: string) => {
statusIsExpired,
statusIsCanceled,
isPurchasablePlan,
isActivePlan,
isFreePlan,
billingInterval,
intervalIsYearly,
billingCycleEnd,
totalCostFormatted,
statusIsCancelationScheduled,
subscription,
editorSeats,
seats,
hasAvailableEditorSeats,
editorSeatPriceFormatted,
isUnlimitedPlan
}
}
@@ -150,6 +150,14 @@ export const workspacePlanQuery = graphql(`
}
`)
export const activeWorkspaceQuery = graphql(`
query activeWorkspace($slug: String!) {
workspaceBySlug(slug: $slug) {
...ActiveWorkspace_Workspace
}
}
`)
export const workspaceLastAdminCheckQuery = graphql(`
query WorkspaceLastAdminCheck($slug: String!) {
workspaceBySlug(slug: $slug) {
@@ -4,12 +4,12 @@
<script setup lang="ts">
import { useQuery } from '@vue/apollo-composable'
import { settingsWorkspacesMembersGuestsQuery } from '~/lib/settings/graphql/queries'
import { settingsWorkspacesMembersTableQuery } from '~/lib/settings/graphql/queries'
const route = useRoute()
const slug = computed(() => (route.params.slug as string) || '')
const { result } = useQuery(settingsWorkspacesMembersGuestsQuery, () => ({
const { result } = useQuery(settingsWorkspacesMembersTableQuery, () => ({
slug: slug.value
}))
+4 -1
View File
@@ -64,7 +64,10 @@ RUN apt-get update && \
fonts-thai-tlwg=1:0.7.3-1 \
fonts-kacst=2.01+mry-15 \
fonts-freefont-ttf=20120503-10 \
libxss1=1:1.2.3-1 && \
libxss1=1:1.2.3-1 \
# libegl1 & libxext6 are required for hardware accelarated rendering to work (vulkan support)
libegl1=1.6.0-1 \
libxext6=2:1.3.4-1+b1 && \
# Clean up
apt-get clean && \
rm -rf /var/lib/apt/lists/*
+4 -2
View File
@@ -10,7 +10,8 @@ export const {
CHROMIUM_EXECUTABLE_PATH,
USER_DATA_DIR,
LOG_LEVEL,
LOG_PRETTY
LOG_PRETTY,
GPU_ENABLED
} = parseEnv(process.env, {
REDIS_URL: z.string().url(),
HOST: z.string().default('127.0.0.1'), //safely default to localhost in case the env var is not set
@@ -20,5 +21,6 @@ export const {
CHROMIUM_EXECUTABLE_PATH: z.string(),
USER_DATA_DIR: z.string(),
LOG_LEVEL: z.string().default('info'),
LOG_PRETTY: z.boolean().default(false)
LOG_PRETTY: z.boolean().default(false),
GPU_ENABLED: z.boolean().default(false)
})
+16 -2
View File
@@ -7,7 +7,8 @@ import {
CHROMIUM_EXECUTABLE_PATH,
PREVIEWS_HEADED,
USER_DATA_DIR,
PREVIEW_TIMEOUT
PREVIEW_TIMEOUT,
GPU_ENABLED
} from '@/config.js'
import Bull from 'bull'
import { logger } from '@/logging.js'
@@ -65,6 +66,14 @@ let jobDoneCallback: Bull.DoneCallback | undefined = undefined
const server = app.listen(port, host, async () => {
logger.info({ port }, '📡 Started Preview Service server, listening on {port}')
const gpuWithVulkanArgs = [
'--headless=new',
'--use-angle=vulkan',
'--enable-features=Vulkan',
'--disable-vulkan-surface',
'--enable-unsafe-webgpu'
]
const launchBrowser = async (): Promise<Browser> => {
logger.debug('Starting browser')
return await puppeteer.launch({
@@ -73,7 +82,12 @@ const server = app.listen(port, host, async () => {
userDataDir: USER_DATA_DIR,
// we trust the web content that is running, so can disable the sandbox
// disabling the sandbox allows us to run the docker image without linux kernel privileges
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
...(GPU_ENABLED ? gpuWithVulkanArgs : [])
],
protocolTimeout: PREVIEW_TIMEOUT
})
}
@@ -99,6 +99,11 @@ spec:
value: {{ .Values.preview_service.puppeteer.timeoutMilliseconds | quote }}
{{- end }}
{{- if .Values.preview_service.gpu.enabled }}
- name: GPU_ENABLED
value: {{ .Values.preview_service.gpu.enabled | quote }}
{{- end }}
{{- with .Values.preview_service.additionalEnvVars }}
{{- toYaml . | nindent 10 }}
{{- end }}
@@ -1873,6 +1873,16 @@
"description": "Allows using a dedicated redis url for the preview service job queue",
"default": false
},
"gpu": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean",
"description": "If enabled, the Preview Service will be deployed with GPU support. Assumes Vulkan driver is available on the host node.",
"default": false
}
}
},
"replicas": {
"type": "number",
"description": "The number of instances of the Preview Service pod to be deployed within the cluster.",
+5
View File
@@ -1127,6 +1127,11 @@ preview_service:
##
dedicatedPreviewsQueue: false
gpu:
## @param preview_service.gpu.enabled If enabled, the Preview Service will be deployed with GPU support. Assumes Vulkan driver is available on the host node.
##
enabled: false
## @param preview_service.replicas The number of instances of the Preview Service pod to be deployed within the cluster.
##
replicas: 1