Feat: Allow selection of default workspace roles (#3034)
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
class="min-w-[150px]"
|
||||
:label-id="labelId"
|
||||
:button-id="buttonId"
|
||||
:disabled-item-tooltip="disabledItemsTooltip"
|
||||
:disabled-item-predicate="disabledItemPredicate"
|
||||
>
|
||||
<template #nothing-selected>
|
||||
{{ multiple ? 'Select roles' : 'Select role' }}
|
||||
@@ -21,7 +23,7 @@
|
||||
class="flex flex-wrap overflow-hidden space-x-0.5 h-6"
|
||||
>
|
||||
<div v-for="(item, i) in value" :key="item" class="text-foreground">
|
||||
{{ roleDisplayName(item) + (i < value.length - 1 ? ', ' : '') }}
|
||||
{{ roleSelectItems[item].title + (i < value.length - 1 ? ', ' : '') }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hiddenSelectedItemCount > 0" class="text-foreground-2 normal">
|
||||
@@ -31,13 +33,13 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="truncate text-foreground">
|
||||
{{ roleDisplayName(isArrayValue(value) ? value[0] : value) }}
|
||||
{{ roleSelectItems[isArrayValue(value) ? value[0] : value].title }}
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
<div class="flex items-center">
|
||||
<span class="truncate">{{ roleDisplayName(item) }}</span>
|
||||
<span class="truncate">{{ roleSelectItems[item].title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
@@ -45,8 +47,8 @@
|
||||
<script setup lang="ts">
|
||||
import { Roles } from '@speckle/shared'
|
||||
import type { StreamRoles, Nullable } from '@speckle/shared'
|
||||
import { capitalize } from 'lodash-es'
|
||||
import { useFormSelectChildInternals } from '~~/lib/form/composables/select'
|
||||
import { roleSelectItems } from '~~/lib/projects/helpers/components'
|
||||
|
||||
type ValueType = StreamRoles | StreamRoles[] | undefined
|
||||
|
||||
@@ -58,6 +60,8 @@ const props = defineProps<{
|
||||
multiple?: boolean
|
||||
modelValue?: ValueType
|
||||
clearable?: boolean
|
||||
disabledItems?: StreamRoles[]
|
||||
disabledItemsTooltip?: string
|
||||
}>()
|
||||
|
||||
const elementToWatchForChanges = ref(null as Nullable<HTMLElement>)
|
||||
@@ -72,6 +76,8 @@ const { selectedValue, isArrayValue, isMultiItemArrayValue, hiddenSelectedItemCo
|
||||
dynamicVisibility: { elementToWatchForChanges, itemContainer }
|
||||
})
|
||||
|
||||
const roleDisplayName = (role: StreamRoles) =>
|
||||
capitalize(Object.entries(Roles.Stream).find(([, val]) => val === role)?.[0] || role)
|
||||
const disabledItemPredicate = (item: StreamRoles) =>
|
||||
props.disabledItems && props.disabledItems.length > 0
|
||||
? props.disabledItems.includes(item)
|
||||
: false
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
:disabled-item-predicate="disabledItemPredicate"
|
||||
name="serverRoles"
|
||||
label="Role"
|
||||
show-label
|
||||
:show-label="showLabel"
|
||||
class="min-w-[110px]"
|
||||
:fully-control-value="fullyControlValue"
|
||||
:label-id="labelId"
|
||||
@@ -66,7 +66,8 @@ const props = defineProps({
|
||||
allowGuest: Boolean,
|
||||
allowAdmin: Boolean,
|
||||
allowArchived: Boolean,
|
||||
fullyControlValue: Boolean
|
||||
fullyControlValue: Boolean,
|
||||
showLabel: Boolean
|
||||
})
|
||||
|
||||
const elementToWatchForChanges = ref(null as Nullable<HTMLElement>)
|
||||
|
||||
@@ -121,6 +121,8 @@ graphql(`
|
||||
id
|
||||
workspaceId
|
||||
workspace {
|
||||
id
|
||||
defaultProjectRole
|
||||
team {
|
||||
items {
|
||||
role
|
||||
@@ -299,6 +301,16 @@ const disabledWorkspaceMemberRowMessage = (
|
||||
: undefined
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.project?.workspace?.defaultProjectRole,
|
||||
(newRole, oldRole) => {
|
||||
if (newRole && newRole !== oldRole) {
|
||||
role.value = newRole as StreamRoles
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(workspaceRole, (newRole, oldRole) => {
|
||||
if (newRole === oldRole) return
|
||||
|
||||
|
||||
@@ -48,6 +48,28 @@
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-6 border-outline-2" />
|
||||
<div class="flex flex-col sm:flex-row space-y-2 sm:space-x-8 items-center">
|
||||
<div class="flex flex-col w-full sm:w-6/12">
|
||||
<span class="text-body-xs font-medium text-foreground">
|
||||
Default project role
|
||||
</span>
|
||||
<span class="text-body-2xs text-foreground-2">
|
||||
Role workspace members get when added to the workspace and in newly created
|
||||
projects.
|
||||
</span>
|
||||
</div>
|
||||
<div class="w-full sm:w-6/12">
|
||||
<FormSelectProjectRoles
|
||||
v-model="defaultProjectRole"
|
||||
disabled-items-tooltip="Use project settings to assign a member as project owner"
|
||||
label="Project role"
|
||||
size="md"
|
||||
:disabled-items="[Roles.Stream.Owner]"
|
||||
@update:model-value="save()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="my-6 border-outline-2" />
|
||||
<div class="flex flex-col space-y-6">
|
||||
<SettingsSectionHeader title="Leave workspace" subheading />
|
||||
<CommonCard class="bg-foundation">
|
||||
@@ -92,7 +114,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useQuery, useMutation } from '@vue/apollo-composable'
|
||||
@@ -106,6 +127,7 @@ import {
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import { isRequired, isStringOfLength } from '~~/lib/common/helpers/validation'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { Roles, type StreamRoles } from '@speckle/shared'
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesGeneral_Workspace on Workspace {
|
||||
@@ -116,10 +138,11 @@ graphql(`
|
||||
description
|
||||
logo
|
||||
role
|
||||
defaultProjectRole
|
||||
}
|
||||
`)
|
||||
|
||||
type FormValues = { name: string; description: string }
|
||||
type FormValues = { name: string; description: string; defaultProjectRole: StreamRoles }
|
||||
|
||||
const props = defineProps<{
|
||||
workspaceId: string
|
||||
@@ -129,14 +152,18 @@ const mixpanel = useMixpanel()
|
||||
const { handleSubmit } = useForm<FormValues>()
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
const { mutate: updateMutation } = useMutation(settingsUpdateWorkspaceMutation)
|
||||
const { result: workspaceResult } = useQuery(settingsWorkspaceGeneralQuery, () => ({
|
||||
id: props.workspaceId
|
||||
}))
|
||||
const { result: workspaceResult, onResult } = useQuery(
|
||||
settingsWorkspaceGeneralQuery,
|
||||
() => ({
|
||||
id: props.workspaceId
|
||||
})
|
||||
)
|
||||
|
||||
const name = ref('')
|
||||
const description = ref('')
|
||||
const showDeleteDialog = ref(false)
|
||||
const showLeaveDialog = ref(false)
|
||||
const defaultProjectRole = ref<StreamRoles>()
|
||||
|
||||
const isAdmin = computed(
|
||||
() => workspaceResult.value?.workspace?.role === Roles.Workspace.Admin
|
||||
@@ -151,6 +178,8 @@ const save = handleSubmit(async () => {
|
||||
if (name.value !== workspaceResult.value.workspace.name) input.name = name.value
|
||||
if (description.value !== workspaceResult.value.workspace.description)
|
||||
input.description = description.value
|
||||
if (defaultProjectRole.value !== workspaceResult.value.workspace.defaultProjectRole)
|
||||
input.defaultProjectRole = defaultProjectRole.value
|
||||
|
||||
const result = await updateMutation({ input }).catch(convertThrowIntoFetchResult)
|
||||
|
||||
@@ -188,4 +217,10 @@ watch(
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
onResult((res) => {
|
||||
if (res.data) {
|
||||
defaultProjectRole.value = res.data.workspace.defaultProjectRole as StreamRoles
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -55,7 +55,7 @@ const documents = {
|
||||
"\n fragment ProjectsModelPageEmbed_Project on Project {\n id\n ...ProjectsPageTeamDialogManagePermissions_Project\n }\n": types.ProjectsModelPageEmbed_ProjectFragmentDoc,
|
||||
"\n fragment ProjectModelPageVersionsCardVersion on Version {\n id\n message\n authorUser {\n ...LimitedUserAvatar\n }\n createdAt\n previewUrl\n sourceApplication\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n ...ProjectModelPageDialogDeleteVersion\n ...ProjectModelPageDialogMoveToVersion\n automationsStatus {\n ...AutomateRunsTriggerStatus_TriggeredAutomationsStatus\n }\n }\n": types.ProjectModelPageVersionsCardVersionFragmentDoc,
|
||||
"\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n workspace {\n id\n name\n ...WorkspaceAvatar_Workspace\n }\n }\n": types.ProjectPageProjectHeaderFragmentDoc,
|
||||
"\n fragment ProjectPageInviteDialog_Project on Project {\n id\n workspaceId\n workspace {\n team {\n items {\n role\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n }\n ...ProjectPageTeamInternals_Project\n workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": types.ProjectPageInviteDialog_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageInviteDialog_Project on Project {\n id\n workspaceId\n workspace {\n id\n defaultProjectRole\n team {\n items {\n role\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n }\n ...ProjectPageTeamInternals_Project\n workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n": types.ProjectPageInviteDialog_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction on AutomationRevisionFunction {\n parameters\n release {\n id\n inputSchema\n function {\n id\n }\n }\n }\n": types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunctionFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationFunctionSettingsDialog_AutomationRevision on AutomationRevision {\n id\n triggerDefinitions {\n ... on VersionCreatedTriggerDefinition {\n type\n model {\n id\n ...CommonModelSelectorModel\n }\n }\n }\n }\n": types.ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFragmentDoc,
|
||||
"\n fragment ProjectPageAutomationFunctions_Automation on Automation {\n id\n currentRevision {\n id\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevision\n functions {\n release {\n id\n function {\n id\n ...AutomationsFunctionsCard_AutomateFunction\n releases(limit: 1) {\n items {\n id\n }\n }\n }\n }\n ...ProjectPageAutomationFunctionSettingsDialog_AutomationRevisionFunction\n }\n }\n }\n": types.ProjectPageAutomationFunctions_AutomationFragmentDoc,
|
||||
@@ -111,7 +111,7 @@ const documents = {
|
||||
"\n fragment SettingsUserProfileDetails_User on User {\n id\n name\n company\n ...UserProfileEditDialogAvatar_User\n }\n": types.SettingsUserProfileDetails_UserFragmentDoc,
|
||||
"\n fragment UserProfileEditDialogAvatar_User on User {\n id\n avatar\n ...ActiveUserAvatar\n }\n": types.UserProfileEditDialogAvatar_UserFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesBilling_Workspace on Workspace {\n billing {\n cost {\n subTotal\n total\n ...BillingSummary_WorkspaceCost\n }\n versionsCount {\n current\n max\n }\n }\n }\n": types.SettingsWorkspacesBilling_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n id\n name\n description\n logo\n role\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n id\n name\n description\n logo\n role\n defaultProjectRole\n }\n": types.SettingsWorkspacesGeneral_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspaceGeneralDeleteDialog_Workspace on Workspace {\n id\n name\n }\n": types.SettingsWorkspaceGeneralDeleteDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesGeneralEditAvatar_Workspace on Workspace {\n id\n logo\n name\n defaultLogoIndex\n }\n": types.SettingsWorkspacesGeneralEditAvatar_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesMembers_Workspace on Workspace {\n id\n role\n }\n": types.SettingsWorkspacesMembers_WorkspaceFragmentDoc,
|
||||
@@ -517,7 +517,7 @@ export function graphql(source: "\n fragment ProjectPageProjectHeader on Projec
|
||||
/**
|
||||
* 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 ProjectPageInviteDialog_Project on Project {\n id\n workspaceId\n workspace {\n team {\n items {\n role\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n }\n ...ProjectPageTeamInternals_Project\n workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageInviteDialog_Project on Project {\n id\n workspaceId\n workspace {\n team {\n items {\n role\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n }\n ...ProjectPageTeamInternals_Project\n workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment ProjectPageInviteDialog_Project on Project {\n id\n workspaceId\n workspace {\n id\n defaultProjectRole\n team {\n items {\n role\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n }\n ...ProjectPageTeamInternals_Project\n workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageInviteDialog_Project on Project {\n id\n workspaceId\n workspace {\n id\n defaultProjectRole\n team {\n items {\n role\n user {\n id\n name\n bio\n company\n avatar\n verified\n role\n }\n }\n }\n }\n ...ProjectPageTeamInternals_Project\n workspace {\n id\n domainBasedMembershipProtectionEnabled\n domains {\n domain\n id\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -741,7 +741,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesBilling_Workspac
|
||||
/**
|
||||
* 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 SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n id\n name\n description\n logo\n role\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n id\n name\n description\n logo\n role\n }\n"];
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n id\n name\n description\n logo\n role\n defaultProjectRole\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesGeneral_Workspace on Workspace {\n ...SettingsWorkspacesGeneralEditAvatar_Workspace\n ...SettingsWorkspaceGeneralDeleteDialog_Workspace\n id\n name\n description\n logo\n role\n defaultProjectRole\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
Reference in New Issue
Block a user