Skip to content

Commit

Permalink
Feat: Allow selection of default workspace roles (#3034)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikehrn authored Sep 19, 2024
1 parent 9ff58d9 commit c5e79ea
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 27 deletions.
18 changes: 12 additions & 6 deletions packages/frontend-2/components/form/select/ProjectRoles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand All @@ -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">
Expand All @@ -31,22 +33,22 @@
</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>
</template>
<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
Expand All @@ -58,6 +60,8 @@ const props = defineProps<{
multiple?: boolean
modelValue?: ValueType
clearable?: boolean
disabledItems?: StreamRoles[]
disabledItemsTooltip?: string
}>()
const elementToWatchForChanges = ref(null as Nullable<HTMLElement>)
Expand All @@ -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>
5 changes: 3 additions & 2 deletions packages/frontend-2/components/form/select/ServerRoles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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>)
Expand Down
12 changes: 12 additions & 0 deletions packages/frontend-2/components/project/page/InviteDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ graphql(`
id
workspaceId
workspace {
id
defaultProjectRole
team {
items {
role
Expand Down Expand Up @@ -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
Expand Down
45 changes: 40 additions & 5 deletions packages/frontend-2/components/settings/workspaces/General.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -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'
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -188,4 +217,10 @@ watch(
},
{ deep: true, immediate: true }
)
onResult((res) => {
if (res.data) {
defaultProjectRole.value = res.data.workspace.defaultProjectRole as StreamRoles
}
})
</script>
8 changes: 4 additions & 4 deletions packages/frontend-2/lib/common/generated/gql/gql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading

0 comments on commit c5e79ea

Please sign in to comment.