feat(fe): workspace security settings - auto join
feat(fe): workspace security settings - auto join
This commit is contained in:
+50
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<LayoutDialog
|
||||
v-model:open="isOpen"
|
||||
title="Confirm change"
|
||||
max-width="xs"
|
||||
:buttons="dialogButtons"
|
||||
>
|
||||
<p class="text-body-xs text-foreground mb-2">
|
||||
This will allow users with verified domain emails to join automatically without
|
||||
admin approval.
|
||||
</p>
|
||||
<p class="text-body-xs text-foreground">Are you sure you want to enable this?</p>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm'): void
|
||||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const handleConfirm = () => {
|
||||
emit('confirm')
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Cancel',
|
||||
props: { color: 'outline' },
|
||||
onClick: handleCancel
|
||||
},
|
||||
{
|
||||
text: 'Confirm',
|
||||
props: {
|
||||
color: 'primary'
|
||||
},
|
||||
onClick: handleConfirm
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -11,7 +11,7 @@
|
||||
<div class="flex gap-3">
|
||||
<WorkspaceAvatar :name="name" :logo="logo" size="lg" />
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-between flex-1">
|
||||
<div class="flex flex-col items-start text-body-2xs text-foreground-2">
|
||||
<div class="flex flex-col items-start text-body-2xs text-foreground-2 gap-1">
|
||||
<h6 class="text-foreground text-body-sm font-medium">
|
||||
{{ name }}
|
||||
</h6>
|
||||
@@ -19,10 +19,16 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-y-2" @click.stop>
|
||||
<div class="flex flex-col gap-y-2 items-end" @click.stop>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="bannerText"
|
||||
class="mt-3 text-body-3xs text-center text-success-700 bg-success-50 px-2 py-1 w-full bg-foundation-page rounded-md border-outline-3 border"
|
||||
>
|
||||
{{ bannerText }}
|
||||
</div>
|
||||
</CommonCard>
|
||||
</template>
|
||||
|
||||
@@ -31,6 +37,7 @@ const props = defineProps<{
|
||||
logo: string
|
||||
name: string
|
||||
clickable?: boolean
|
||||
bannerText?: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<div class="flex flex-col items-center gap-2 w-full max-w-lg mx-auto">
|
||||
<h1 class="text-heading-xl text-foreground mb-2 font-normal mt-4">
|
||||
Join teammates
|
||||
Join your coworkers
|
||||
</h1>
|
||||
<p class="text-center text-body-sm text-foreground-2 mb-8">
|
||||
{{ description }}
|
||||
@@ -30,6 +30,8 @@
|
||||
:workspace="workspace"
|
||||
:request-status="workspace.requestStatus"
|
||||
location="workspace_join_page"
|
||||
@auto-joined="workspace.requestStatus = WorkspaceJoinRequestStatus.Approved"
|
||||
@request="workspace.requestStatus = WorkspaceJoinRequestStatus.Pending"
|
||||
/>
|
||||
<FormButton
|
||||
v-if="!showAllWorkspaces && discoverableWorkspacesAndJoinRequestsCount > 3"
|
||||
@@ -51,6 +53,7 @@
|
||||
Continue
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="!hasApprovedWorkspace"
|
||||
size="lg"
|
||||
full-width
|
||||
color="outline"
|
||||
@@ -58,6 +61,9 @@
|
||||
>
|
||||
Create a new workspace
|
||||
</FormButton>
|
||||
<FormButton v-else size="lg" full-width @click="navigateTo(homeRoute)">
|
||||
Continue to workspace
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="!hasDiscoverableJoinRequests && !isWorkspaceNewPlansEnabled"
|
||||
size="lg"
|
||||
@@ -76,6 +82,8 @@
|
||||
import { useAuthManager } from '~/lib/auth/composables/auth'
|
||||
import { workspaceCreateRoute, homeRoute } from '~~/lib/common/helpers/route'
|
||||
import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces'
|
||||
import type { DiscoverableWorkspace_LimitedWorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { WorkspaceJoinRequestStatus } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
const { logout } = useAuthManager()
|
||||
const isWorkspaceNewPlansEnabled = useWorkspaceNewPlansEnabled()
|
||||
@@ -86,12 +94,22 @@ const {
|
||||
hasDiscoverableJoinRequests
|
||||
} = useDiscoverableWorkspaces()
|
||||
|
||||
const hasApprovedWorkspace = computed(() =>
|
||||
localWorkspaces.value.some(
|
||||
(workspace) => workspace.requestStatus === WorkspaceJoinRequestStatus.Approved
|
||||
)
|
||||
)
|
||||
|
||||
const showAllWorkspaces = ref(false)
|
||||
|
||||
const localWorkspaces = ref<
|
||||
(DiscoverableWorkspace_LimitedWorkspaceFragment & { requestStatus: string | null })[]
|
||||
>([])
|
||||
|
||||
const workspacesToShow = computed(() => {
|
||||
return showAllWorkspaces.value
|
||||
? discoverableWorkspacesAndJoinRequests.value
|
||||
: discoverableWorkspacesAndJoinRequests.value.slice(0, 3)
|
||||
? localWorkspaces.value
|
||||
: localWorkspaces.value.slice(0, 3)
|
||||
})
|
||||
|
||||
const description = computed(() => {
|
||||
@@ -100,4 +118,15 @@ const description = computed(() => {
|
||||
}
|
||||
return 'We found workspaces that match your email domain'
|
||||
})
|
||||
|
||||
watch(
|
||||
discoverableWorkspacesAndJoinRequests,
|
||||
(newWorkspaces) => {
|
||||
// Only update if localWorkspaces is empty (initial load) or if we don't have any local modifications
|
||||
if (localWorkspaces.value.length === 0) {
|
||||
localWorkspaces.value = [...newWorkspaces]
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -2,22 +2,30 @@
|
||||
<WorkspaceCard
|
||||
:logo="workspace.logo ?? ''"
|
||||
:name="workspace.name"
|
||||
:class="requestStatus === 'pending' ? '' : 'bg-foundation'"
|
||||
:class="requestStatus === WorkspaceJoinRequestStatus.Pending ? '' : 'bg-foundation'"
|
||||
:banner-text="
|
||||
workspace.discoverabilityAutoJoinEnabled &&
|
||||
requestStatus !== WorkspaceJoinRequestStatus.Approved
|
||||
? 'You can join this workspace automatically. No admin approval needed.'
|
||||
: null
|
||||
"
|
||||
>
|
||||
<template #text>
|
||||
<div class="flex flex-col gap-y-1">
|
||||
<div v-if="workspace.description" class="text-body-2xs line-clamp-3">
|
||||
{{ workspace.description }}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<UserAvatarGroup
|
||||
v-if="members.length > 0 && requestStatus !== 'pending'"
|
||||
v-if="members.length > 0"
|
||||
:users="members"
|
||||
:max-count="5"
|
||||
size="base"
|
||||
/>
|
||||
<div class="text-body-3xs text-foreground-2">
|
||||
<span class="font-medium">Admins: </span>
|
||||
<span class="font-medium">
|
||||
{{ adminTeam.length === 1 ? 'Admin' : 'Admins' }}:
|
||||
</span>
|
||||
<span v-for="(admin, index) in adminTeam.slice(0, 3)" :key="admin.id">
|
||||
{{ admin.name
|
||||
}}{{ index < 2 && index < adminTeam.length - 1 ? ', ' : '' }}
|
||||
@@ -28,12 +36,30 @@
|
||||
</div>
|
||||
</template>
|
||||
<template #actions>
|
||||
<FormButton v-if="requestStatus" color="outline" size="sm" disabled>
|
||||
<FormButton
|
||||
v-if="requestStatus === WorkspaceJoinRequestStatus.Pending"
|
||||
color="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
>
|
||||
Join request sent
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-else-if="requestStatus === WorkspaceJoinRequestStatus.Approved"
|
||||
color="outline"
|
||||
size="sm"
|
||||
:icon-left="CheckIcon"
|
||||
disabled
|
||||
>
|
||||
Workspace joined
|
||||
</FormButton>
|
||||
<div v-else class="flex flex-col gap-2 sm:items-end">
|
||||
<FormButton color="outline" size="sm" @click="onRequest">
|
||||
Request to join
|
||||
{{
|
||||
workspace.discoverabilityAutoJoinEnabled
|
||||
? 'Join workspace'
|
||||
: 'Request to join'
|
||||
}}
|
||||
</FormButton>
|
||||
<FormButton
|
||||
v-if="showDismissButton"
|
||||
@@ -52,6 +78,8 @@
|
||||
import type { DiscoverableWorkspace_LimitedWorkspaceFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
import { CheckIcon } from '@heroicons/vue/20/solid'
|
||||
import { WorkspaceJoinRequestStatus } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
const props = defineProps<{
|
||||
workspace: DiscoverableWorkspace_LimitedWorkspaceFragment
|
||||
@@ -60,20 +88,41 @@ const props = defineProps<{
|
||||
requestStatus: string | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'auto-joined'): void
|
||||
(e: 'request'): void
|
||||
}>()
|
||||
|
||||
const { requestToJoinWorkspace, dismissDiscoverableWorkspace } =
|
||||
useDiscoverableWorkspaces()
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const adminTeam = computed(() => props.workspace.adminTeam?.map((t) => t.user) ?? [])
|
||||
const adminIds = computed(() => new Set(adminTeam.value.map((admin) => admin.id)))
|
||||
const members = computed(() =>
|
||||
(props.workspace.team?.items?.map((u) => u.user) ?? []).filter(
|
||||
(user) => !adminIds.value.has(user.id)
|
||||
)
|
||||
)
|
||||
const allMembers = computed(() => props.workspace.team?.items?.map((u) => u.user) ?? [])
|
||||
const members = computed(() => {
|
||||
// Only deduplicate if there's exactly one person total (admin who is also the only member)
|
||||
const totalUniqueUsers = new Set([
|
||||
...adminTeam.value.map((admin) => admin.id),
|
||||
...allMembers.value.map((member) => member.id)
|
||||
]).size
|
||||
|
||||
if (totalUniqueUsers === 1) {
|
||||
// Single user case: filter out admins from members to avoid duplication
|
||||
return allMembers.value.filter((user) => !adminIds.value.has(user.id))
|
||||
} else {
|
||||
// Multiple users: show all members including those who are also admins
|
||||
return allMembers.value
|
||||
}
|
||||
})
|
||||
|
||||
const onRequest = () => {
|
||||
requestToJoinWorkspace(props.workspace.id, props.location || 'discovery_card')
|
||||
requestToJoinWorkspace(props.workspace, props.location || 'discovery_card')
|
||||
if (props.workspace.discoverabilityAutoJoinEnabled) {
|
||||
emit('auto-joined')
|
||||
} else {
|
||||
emit('request')
|
||||
}
|
||||
}
|
||||
|
||||
const onDismiss = async () => {
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
:request-status="workspace.requestStatus"
|
||||
show-dismiss-button
|
||||
location="workspace_switcher"
|
||||
@auto-joined="workspace.requestStatus = WorkspaceJoinRequestStatus.Approved"
|
||||
@request="workspace.requestStatus = WorkspaceJoinRequestStatus.Pending"
|
||||
@go-to-workspace="open = false"
|
||||
/>
|
||||
<FormButton
|
||||
v-if="!showAllWorkspaces && discoverableWorkspacesAndJoinRequestsCount > 3"
|
||||
@@ -32,6 +35,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useDiscoverableWorkspaces } from '~/lib/workspaces/composables/discoverableWorkspaces'
|
||||
import { WorkspaceJoinRequestStatus } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
const {
|
||||
discoverableWorkspacesAndJoinRequests,
|
||||
@@ -40,14 +44,15 @@ const {
|
||||
} = useDiscoverableWorkspaces()
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const showAllWorkspaces = ref(false)
|
||||
const localWorkspaces = ref(discoverableWorkspacesAndJoinRequests.value)
|
||||
|
||||
const workspacesToShow = computed(() => {
|
||||
return showAllWorkspaces.value
|
||||
? discoverableWorkspacesAndJoinRequests.value
|
||||
: discoverableWorkspacesAndJoinRequests.value.slice(0, 3)
|
||||
? localWorkspaces.value
|
||||
: localWorkspaces.value.slice(0, 3)
|
||||
})
|
||||
|
||||
const dialogButtons = computed((): LayoutDialogButton[] => {
|
||||
return [
|
||||
{
|
||||
@@ -61,5 +66,8 @@ const dialogButtons = computed((): LayoutDialogButton[] => {
|
||||
|
||||
watch(open, () => {
|
||||
showAllWorkspaces.value = false
|
||||
if (!open.value) {
|
||||
localWorkspaces.value = discoverableWorkspacesAndJoinRequests.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -31,7 +31,7 @@ const invite = computed(() => ({
|
||||
|
||||
const handleRequest = async (accept: boolean) => {
|
||||
if (accept) {
|
||||
await requestToJoinWorkspace(props.workspace.id, 'discovery banner')
|
||||
await requestToJoinWorkspace(props.workspace, 'discovery banner')
|
||||
} else {
|
||||
await dismissDiscoverableWorkspace(props.workspace.id)
|
||||
mixpanel.track('Workspace Discovery Banner Dismissed', {
|
||||
|
||||
@@ -379,8 +379,8 @@ type Documents = {
|
||||
"\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 DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n }\n": typeof types.DiscoverableWorkspace_LimitedWorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceJoinRequests_LimitedWorkspaceJoinRequest on LimitedWorkspaceJoinRequest {\n id\n status\n workspace {\n id\n name\n logo\n slug\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n }\n }\n": typeof types.WorkspaceJoinRequests_LimitedWorkspaceJoinRequestFragmentDoc,
|
||||
"\n fragment DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n description\n slug\n discoverabilityAutoJoinEnabled\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n }\n": typeof types.DiscoverableWorkspace_LimitedWorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceJoinRequests_LimitedWorkspaceJoinRequest on LimitedWorkspaceJoinRequest {\n id\n status\n workspace {\n id\n name\n logo\n slug\n discoverabilityAutoJoinEnabled\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n }\n }\n": typeof types.WorkspaceJoinRequests_LimitedWorkspaceJoinRequestFragmentDoc,
|
||||
"\n fragment WorkspacePlanLimits_Workspace on Workspace {\n id\n slug\n plan {\n name\n }\n }\n": typeof types.WorkspacePlanLimits_WorkspaceFragmentDoc,
|
||||
"\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n workspaceSlug\n user {\n id\n }\n }\n": typeof types.UseWorkspaceInviteManager_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n slug\n plan {\n status\n createdAt\n name\n paymentMethod\n usage {\n projectCount\n modelCount\n }\n }\n seats {\n editors {\n assigned\n available\n }\n viewers {\n assigned\n available\n }\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n currency\n }\n }\n": typeof types.WorkspacesPlan_WorkspaceFragmentDoc,
|
||||
@@ -407,6 +407,7 @@ type Documents = {
|
||||
"\n mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n deny(input: $input)\n }\n }\n": typeof types.DenyWorkspaceJoinRequestDocument,
|
||||
"\n mutation RequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {\n workspaceMutations {\n requestToJoin(input: $input)\n }\n }\n": typeof types.RequestToJoinWorkspaceDocument,
|
||||
"\n mutation DismissDiscoverableWorkspace($input: WorkspaceDismissInput!) {\n workspaceMutations {\n dismiss(input: $input)\n }\n }\n": typeof types.DismissDiscoverableWorkspaceDocument,
|
||||
"\n mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityAutoJoinEnabled\n }\n }\n }\n": typeof types.WorkspaceUpdateAutoJoinMutationDocument,
|
||||
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n": typeof types.WorkspaceAccessCheckDocument,
|
||||
"\n query WorkspaceSidebar(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspaceSidebar_Workspace\n }\n }\n": typeof types.WorkspaceSidebarDocument,
|
||||
"\n query WorkspaceDashboard(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspaceDashboard_Workspace\n }\n }\n": typeof types.WorkspaceDashboardDocument,
|
||||
@@ -450,7 +451,7 @@ type Documents = {
|
||||
"\n fragment SettingsWorkspacesProjects_Workspace on Workspace {\n id\n name\n slug\n plan {\n name\n }\n role\n permissions {\n canCreateProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.SettingsWorkspacesProjects_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": typeof types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": typeof types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n": typeof types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n discoverabilityAutoJoinEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n": typeof types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
};
|
||||
const documents: Documents = {
|
||||
"\n fragment AuthLoginWithEmailBlock_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n email\n user {\n id\n }\n }\n": types.AuthLoginWithEmailBlock_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
@@ -818,8 +819,8 @@ const documents: Documents = {
|
||||
"\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 DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n }\n": types.DiscoverableWorkspace_LimitedWorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceJoinRequests_LimitedWorkspaceJoinRequest on LimitedWorkspaceJoinRequest {\n id\n status\n workspace {\n id\n name\n logo\n slug\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n }\n }\n": types.WorkspaceJoinRequests_LimitedWorkspaceJoinRequestFragmentDoc,
|
||||
"\n fragment DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n description\n slug\n discoverabilityAutoJoinEnabled\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n }\n": types.DiscoverableWorkspace_LimitedWorkspaceFragmentDoc,
|
||||
"\n fragment WorkspaceJoinRequests_LimitedWorkspaceJoinRequest on LimitedWorkspaceJoinRequest {\n id\n status\n workspace {\n id\n name\n logo\n slug\n discoverabilityAutoJoinEnabled\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n }\n }\n": types.WorkspaceJoinRequests_LimitedWorkspaceJoinRequestFragmentDoc,
|
||||
"\n fragment WorkspacePlanLimits_Workspace on Workspace {\n id\n slug\n plan {\n name\n }\n }\n": types.WorkspacePlanLimits_WorkspaceFragmentDoc,
|
||||
"\n fragment UseWorkspaceInviteManager_PendingWorkspaceCollaborator on PendingWorkspaceCollaborator {\n id\n token\n workspaceId\n workspaceSlug\n user {\n id\n }\n }\n": types.UseWorkspaceInviteManager_PendingWorkspaceCollaboratorFragmentDoc,
|
||||
"\n fragment WorkspacesPlan_Workspace on Workspace {\n id\n slug\n plan {\n status\n createdAt\n name\n paymentMethod\n usage {\n projectCount\n modelCount\n }\n }\n seats {\n editors {\n assigned\n available\n }\n viewers {\n assigned\n available\n }\n }\n subscription {\n billingInterval\n currentBillingCycleEnd\n currency\n }\n }\n": types.WorkspacesPlan_WorkspaceFragmentDoc,
|
||||
@@ -846,6 +847,7 @@ const documents: Documents = {
|
||||
"\n mutation DenyWorkspaceJoinRequest($input: DenyWorkspaceJoinRequestInput!) {\n workspaceJoinRequestMutations {\n deny(input: $input)\n }\n }\n": types.DenyWorkspaceJoinRequestDocument,
|
||||
"\n mutation RequestToJoinWorkspace($input: WorkspaceRequestToJoinInput!) {\n workspaceMutations {\n requestToJoin(input: $input)\n }\n }\n": types.RequestToJoinWorkspaceDocument,
|
||||
"\n mutation DismissDiscoverableWorkspace($input: WorkspaceDismissInput!) {\n workspaceMutations {\n dismiss(input: $input)\n }\n }\n": types.DismissDiscoverableWorkspaceDocument,
|
||||
"\n mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityAutoJoinEnabled\n }\n }\n }\n": types.WorkspaceUpdateAutoJoinMutationDocument,
|
||||
"\n query WorkspaceAccessCheck($slug: String!) {\n workspaceBySlug(slug: $slug) {\n id\n }\n }\n": types.WorkspaceAccessCheckDocument,
|
||||
"\n query WorkspaceSidebar(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspaceSidebar_Workspace\n }\n }\n": types.WorkspaceSidebarDocument,
|
||||
"\n query WorkspaceDashboard(\n $workspaceSlug: String!\n $invitesFilter: PendingWorkspaceCollaboratorsFilter\n ) {\n workspaceBySlug(slug: $workspaceSlug) {\n ...WorkspaceDashboard_Workspace\n }\n }\n": types.WorkspaceDashboardDocument,
|
||||
@@ -889,7 +891,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsWorkspacesProjects_Workspace on Workspace {\n id\n name\n slug\n plan {\n name\n }\n role\n permissions {\n canCreateProject {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.SettingsWorkspacesProjects_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_Workspace on Workspace {\n id\n role\n defaultRegion {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n hasAccessToMultiRegion: hasAccessToFeature(\n featureName: workspaceDataRegionSpecificity\n )\n hasProjects: projects(limit: 0) {\n totalCount\n }\n }\n": types.SettingsWorkspacesRegions_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesRegions_ServerInfo on ServerInfo {\n multiRegion {\n regions {\n id\n ...SettingsWorkspacesRegionsSelect_ServerRegionItem\n }\n }\n }\n": types.SettingsWorkspacesRegions_ServerInfoFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n discoverabilityAutoJoinEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n": types.SettingsWorkspacesSecurity_WorkspaceFragmentDoc,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2369,11 +2371,11 @@ export function graphql(source: "\n fragment ActiveWorkspace_Workspace on Works
|
||||
/**
|
||||
* 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 DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n }\n"): (typeof documents)["\n fragment DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n description\n slug\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n description\n slug\n discoverabilityAutoJoinEnabled\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n }\n"): (typeof documents)["\n fragment DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {\n id\n name\n logo\n description\n slug\n discoverabilityAutoJoinEnabled\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n adminTeam {\n user {\n id\n name\n avatar\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 WorkspaceJoinRequests_LimitedWorkspaceJoinRequest on LimitedWorkspaceJoinRequest {\n id\n status\n workspace {\n id\n name\n logo\n slug\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceJoinRequests_LimitedWorkspaceJoinRequest on LimitedWorkspaceJoinRequest {\n id\n status\n workspace {\n id\n name\n logo\n slug\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n }\n }\n"];
|
||||
export function graphql(source: "\n fragment WorkspaceJoinRequests_LimitedWorkspaceJoinRequest on LimitedWorkspaceJoinRequest {\n id\n status\n workspace {\n id\n name\n logo\n slug\n discoverabilityAutoJoinEnabled\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n }\n }\n"): (typeof documents)["\n fragment WorkspaceJoinRequests_LimitedWorkspaceJoinRequest on LimitedWorkspaceJoinRequest {\n id\n status\n workspace {\n id\n name\n logo\n slug\n discoverabilityAutoJoinEnabled\n adminTeam {\n user {\n id\n name\n avatar\n }\n }\n team {\n totalCount\n items {\n user {\n id\n name\n avatar\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2478,6 +2480,10 @@ export function graphql(source: "\n mutation RequestToJoinWorkspace($input: 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 mutation DismissDiscoverableWorkspace($input: WorkspaceDismissInput!) {\n workspaceMutations {\n dismiss(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DismissDiscoverableWorkspace($input: WorkspaceDismissInput!) {\n workspaceMutations {\n dismiss(input: $input)\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 mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityAutoJoinEnabled\n }\n }\n }\n"): (typeof documents)["\n mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {\n workspaceMutations {\n update(input: $input) {\n id\n discoverabilityAutoJoinEnabled\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2653,7 +2659,7 @@ export function graphql(source: "\n fragment SettingsWorkspacesRegions_ServerIn
|
||||
/**
|
||||
* 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 SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n"];
|
||||
export function graphql(source: "\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n discoverabilityAutoJoinEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecurity_Workspace on Workspace {\n id\n slug\n plan {\n name\n status\n }\n domains {\n id\n domain\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n ...SettingsWorkspacesSecuritySsoWrapper_Workspace\n domainBasedMembershipProtectionEnabled\n discoverabilityEnabled\n discoverabilityAutoJoinEnabled\n hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(\n featureName: domainBasedSecurityPolicies\n )\n }\n"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -92,7 +92,8 @@ export function useAddWorkspaceDomain() {
|
||||
discoverabilityEnabled,
|
||||
domainBasedMembershipProtectionEnabled,
|
||||
hasAccessToSSO,
|
||||
hasAccessToDomainBasedSecurityPolicies
|
||||
hasAccessToDomainBasedSecurityPolicies,
|
||||
discoverabilityAutoJoinEnabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
getFirstErrorMessage,
|
||||
getCacheId
|
||||
} from '~~/lib/common/helpers/graphql'
|
||||
import type { DiscoverableWorkspace_LimitedWorkspaceFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { activeUserWorkspaceExistenceCheckQuery } from '~/lib/auth/graphql/queries'
|
||||
|
||||
graphql(`
|
||||
fragment DiscoverableWorkspace_LimitedWorkspace on LimitedWorkspace {
|
||||
@@ -20,6 +22,7 @@ graphql(`
|
||||
logo
|
||||
description
|
||||
slug
|
||||
discoverabilityAutoJoinEnabled
|
||||
team {
|
||||
totalCount
|
||||
items {
|
||||
@@ -49,6 +52,7 @@ graphql(`
|
||||
name
|
||||
logo
|
||||
slug
|
||||
discoverabilityAutoJoinEnabled
|
||||
adminTeam {
|
||||
user {
|
||||
id
|
||||
@@ -140,30 +144,56 @@ export const useDiscoverableWorkspaces = () => {
|
||||
() => discoverableWorkspacesAndJoinRequests.value?.length || 0
|
||||
)
|
||||
|
||||
const requestToJoinWorkspace = async (workspaceId: string, location: string) => {
|
||||
const requestToJoinWorkspace = async (
|
||||
workspace: DiscoverableWorkspace_LimitedWorkspaceFragment,
|
||||
location: string
|
||||
) => {
|
||||
const activeUserId = activeUser.value?.id
|
||||
|
||||
if (!activeUserId) return
|
||||
|
||||
const result = await requestToJoin({
|
||||
input: { workspaceId }
|
||||
input: { workspaceId: workspace.id }
|
||||
}).catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (result?.data) {
|
||||
await refetch()
|
||||
|
||||
mixpanel.track('Workspace Join Request Sent', {
|
||||
workspaceId,
|
||||
location,
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspaceId
|
||||
apollo.query({
|
||||
query: activeUserWorkspaceExistenceCheckQuery,
|
||||
variables: {
|
||||
filter: { personalOnly: true }
|
||||
},
|
||||
fetchPolicy: 'network-only'
|
||||
})
|
||||
|
||||
triggerNotification({
|
||||
title: 'Request sent',
|
||||
description: 'Your request to join the workspace has been sent.',
|
||||
type: ToastNotificationType.Success
|
||||
})
|
||||
if (workspace.discoverabilityAutoJoinEnabled) {
|
||||
mixpanel.track('Workspace Auto Joined', {
|
||||
workspaceId: workspace.id,
|
||||
location,
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspace.id
|
||||
})
|
||||
|
||||
triggerNotification({
|
||||
title: 'Workspace joined',
|
||||
description: `You have joined ${workspace.name}.`,
|
||||
type: ToastNotificationType.Success
|
||||
})
|
||||
} else {
|
||||
mixpanel.track('Workspace Join Request Sent', {
|
||||
workspaceId: workspace.id,
|
||||
location,
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspace.id
|
||||
})
|
||||
|
||||
triggerNotification({
|
||||
title: 'Request sent',
|
||||
description: 'Your request to join the workspace has been sent.',
|
||||
type: ToastNotificationType.Success
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const errorMessage = getFirstErrorMessage(result?.errors)
|
||||
triggerNotification({
|
||||
|
||||
@@ -152,3 +152,14 @@ export const dismissDiscoverableWorkspaceMutation = graphql(`
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
export const workspaceUpdateAutoJoinMutation = graphql(`
|
||||
mutation WorkspaceUpdateAutoJoinMutation($input: WorkspaceUpdateInput!) {
|
||||
workspaceMutations {
|
||||
update(input: $input) {
|
||||
id
|
||||
discoverabilityAutoJoinEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
@@ -26,11 +26,7 @@
|
||||
class="border-x border-b first:border-t first:rounded-t-lg border-outline-2 last:rounded-b-lg p-6 py-4 flex items-center"
|
||||
>
|
||||
<p class="text-body-xs font-medium flex-1">@{{ domain.domain }}</p>
|
||||
<FormButton
|
||||
:disabled="workspaceDomains.length === 1 && isDomainProtectionEnabled"
|
||||
color="outline"
|
||||
@click="openRemoveDialog(domain)"
|
||||
>
|
||||
<FormButton color="outline" @click="openRemoveDialog(domain)">
|
||||
Delete
|
||||
</FormButton>
|
||||
</li>
|
||||
@@ -46,7 +42,7 @@
|
||||
<section class="mt-8">
|
||||
<div class="grid grid-cols-2 gap-x-6 items-center">
|
||||
<div class="flex flex-col gap-y-1">
|
||||
<p class="text-body-xs font-medium text-foreground">New domain</p>
|
||||
<p class="text-body-xs font-medium text-foreground">Add domain</p>
|
||||
<p class="text-body-2xs text-foreground-2 leading-5">
|
||||
Add a domain from a list of email domains for your active account.
|
||||
</p>
|
||||
@@ -73,6 +69,52 @@
|
||||
</section>
|
||||
<section class="flex flex-col space-y-3 mt-8">
|
||||
<div class="flex flex-col space-y-8">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1 flex-col pr-6 gap-y-1">
|
||||
<p class="text-body-xs font-medium text-foreground">
|
||||
Domain-based discoverability
|
||||
</p>
|
||||
<p class="text-body-2xs text-foreground-2 leading-5 max-w-md">
|
||||
Allow users with verified domain emails to find and request access to
|
||||
this workspace.
|
||||
</p>
|
||||
</div>
|
||||
<FormSwitch
|
||||
v-model="isDomainDiscoverabilityEnabled"
|
||||
v-tippy="
|
||||
!hasWorkspaceDomains
|
||||
? 'Your workspace must have at least one verified domain'
|
||||
: undefined
|
||||
"
|
||||
name="domain-discoverability"
|
||||
:disabled="!hasWorkspaceDomains"
|
||||
:show-label="false"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1 flex-col pr-6 gap-y-1">
|
||||
<p class="text-body-xs font-medium text-foreground">
|
||||
Join without admin approval
|
||||
</p>
|
||||
<p class="text-body-2xs text-foreground-2 leading-5 max-w-md">
|
||||
Allow users with verified domain emails to join immediately without
|
||||
admin approval.
|
||||
</p>
|
||||
</div>
|
||||
<FormSwitch
|
||||
v-model="isAutoJoinEnabled"
|
||||
v-tippy="
|
||||
!isDomainDiscoverabilityEnabled
|
||||
? 'Domain-based discoverability must be enabled'
|
||||
: undefined
|
||||
"
|
||||
name="auto-join"
|
||||
:disabled="!hasWorkspaceDomains || !isDomainDiscoverabilityEnabled"
|
||||
:show-label="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1 flex-col pr-6 gap-y-1">
|
||||
<div class="flex items-center">
|
||||
@@ -96,23 +138,6 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="flex-1 flex-col pr-6 gap-y-1">
|
||||
<p class="text-body-xs font-medium text-foreground">
|
||||
Domain-based discoverability
|
||||
</p>
|
||||
<p class="text-body-2xs text-foreground-2 leading-5 max-w-md">
|
||||
When enabled, users with a verified email address from your verified
|
||||
domain list will be able to request to join this workspace.
|
||||
</p>
|
||||
</div>
|
||||
<FormSwitch
|
||||
v-model="isDomainDiscoverabilityEnabled"
|
||||
name="domain-discoverability"
|
||||
:disabled="!hasWorkspaceDomains"
|
||||
:show-label="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -123,6 +148,12 @@
|
||||
:workspace-id="workspace?.id"
|
||||
:domain="removeDialogDomain"
|
||||
/>
|
||||
<SettingsWorkspacesSecurityConfirmJoinPolicyDialog
|
||||
v-if="showConfirmJoinPolicyDialog"
|
||||
v-model:open="showConfirmJoinPolicyDialog"
|
||||
@confirm="handleJoinPolicyConfirm"
|
||||
@cancel="pendingJoinPolicy = undefined"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -138,10 +169,16 @@ import { blockedDomains } from '@speckle/shared'
|
||||
import { useIsWorkspacesSsoEnabled } from '~/composables/globals'
|
||||
import {
|
||||
workspaceUpdateDomainProtectionMutation,
|
||||
workspaceUpdateDiscoverabilityMutation
|
||||
workspaceUpdateDiscoverabilityMutation,
|
||||
workspaceUpdateAutoJoinMutation
|
||||
} from '~/lib/workspaces/graphql/mutations'
|
||||
import { useVerifiedUserEmailDomains } from '~/lib/workspaces/composables/security'
|
||||
|
||||
enum JoinPolicy {
|
||||
AdminApproval = 'admin-approval',
|
||||
AutoJoin = 'auto-join'
|
||||
}
|
||||
|
||||
graphql(`
|
||||
fragment SettingsWorkspacesSecurity_Workspace on Workspace {
|
||||
id
|
||||
@@ -158,6 +195,7 @@ graphql(`
|
||||
...SettingsWorkspacesSecuritySsoWrapper_Workspace
|
||||
domainBasedMembershipProtectionEnabled
|
||||
discoverabilityEnabled
|
||||
discoverabilityAutoJoinEnabled
|
||||
hasAccessToDomainBasedSecurityPolicies: hasAccessToFeature(
|
||||
featureName: domainBasedSecurityPolicies
|
||||
)
|
||||
@@ -187,12 +225,16 @@ const { mutate: updateDomainProtection } = useMutation(
|
||||
const { mutate: updateDiscoverability } = useMutation(
|
||||
workspaceUpdateDiscoverabilityMutation
|
||||
)
|
||||
const { mutate: updateAutoJoin } = useMutation(workspaceUpdateAutoJoinMutation)
|
||||
const { triggerNotification } = useGlobalToast()
|
||||
|
||||
const selectedDomain = ref<string>()
|
||||
const showRemoveDomainDialog = ref(false)
|
||||
const removeDialogDomain =
|
||||
ref<SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomainFragment>()
|
||||
const blockedDomainItems: ShallowRef<string[]> = shallowRef(blockedDomains)
|
||||
const showConfirmJoinPolicyDialog = ref(false)
|
||||
const pendingJoinPolicy = ref<JoinPolicy>()
|
||||
|
||||
const { result } = useQuery(settingsWorkspacesSecurityQuery, {
|
||||
slug: slug.value
|
||||
@@ -257,10 +299,35 @@ const isDomainDiscoverabilityEnabled = computed({
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspace.value?.id
|
||||
})
|
||||
|
||||
// If turning off discoverability, also turn off auto-join
|
||||
if (!newVal && workspace.value.discoverabilityAutoJoinEnabled) {
|
||||
const autoJoinResult = await updateAutoJoin({
|
||||
input: {
|
||||
id: workspace.value.id,
|
||||
discoverabilityAutoJoinEnabled: false
|
||||
}
|
||||
}).catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (autoJoinResult?.data) {
|
||||
mixpanel.track('Workspace Join Policy Updated', {
|
||||
value: 'admin-approval',
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspace.value.id
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isAutoJoinEnabled = computed({
|
||||
get: () => workspace.value?.discoverabilityAutoJoinEnabled || false,
|
||||
set: (newVal) => {
|
||||
handleJoinPolicyUpdate(newVal ? JoinPolicy.AutoJoin : JoinPolicy.AdminApproval)
|
||||
}
|
||||
})
|
||||
|
||||
const switchDisabled = computed(() => {
|
||||
if (isDomainProtectionEnabled.value) return false
|
||||
if (!hasAccessToDomainBasedSecurityPolicies.value) return true
|
||||
@@ -308,11 +375,90 @@ const disabledItemPredicate = (item: string) => {
|
||||
return blockedDomainItems.value.includes(item)
|
||||
}
|
||||
|
||||
const handleJoinPolicyUpdate = async (newValue: JoinPolicy, confirmed = false) => {
|
||||
if (!workspace.value?.id) return
|
||||
|
||||
// If enabling auto-join and not yet confirmed, show confirmation dialog
|
||||
if (newValue === JoinPolicy.AutoJoin && !confirmed) {
|
||||
showConfirmJoinPolicyDialog.value = true
|
||||
pendingJoinPolicy.value = newValue
|
||||
return
|
||||
}
|
||||
|
||||
const isAutoJoinEnabled = newValue === JoinPolicy.AutoJoin
|
||||
|
||||
const result = await updateAutoJoin({
|
||||
input: {
|
||||
id: workspace.value.id,
|
||||
discoverabilityAutoJoinEnabled: isAutoJoinEnabled
|
||||
}
|
||||
}).catch(convertThrowIntoFetchResult)
|
||||
|
||||
if (result?.data) {
|
||||
// Reset dialog state if it was open
|
||||
if (showConfirmJoinPolicyDialog.value) {
|
||||
showConfirmJoinPolicyDialog.value = false
|
||||
pendingJoinPolicy.value = undefined
|
||||
}
|
||||
|
||||
const notificationConfig = isAutoJoinEnabled
|
||||
? {
|
||||
title: 'Join without admin approval enabled',
|
||||
description:
|
||||
'Users with a verified domain can now join without admin approval'
|
||||
}
|
||||
: {
|
||||
title: 'New user policy updated',
|
||||
description: 'Admin approval is now required for new users to join'
|
||||
}
|
||||
|
||||
triggerNotification({
|
||||
type: ToastNotificationType.Success,
|
||||
...notificationConfig
|
||||
})
|
||||
|
||||
mixpanel.track('Workspace Join Policy Updated', {
|
||||
value: isAutoJoinEnabled ? 'auto-join' : 'admin-approval',
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: workspace.value.id
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleJoinPolicyConfirm = async () => {
|
||||
if (!pendingJoinPolicy.value) return
|
||||
await handleJoinPolicyUpdate(pendingJoinPolicy.value, true)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => workspaceDomains.value,
|
||||
() => {
|
||||
if (!hasWorkspaceDomains.value) {
|
||||
isDomainDiscoverabilityEnabled.value = false
|
||||
() => workspaceDomains.value.length,
|
||||
async (newLength) => {
|
||||
// If last domain was removed, disable all domain features
|
||||
if (newLength === 0 && workspace.value?.id) {
|
||||
if (workspace.value.discoverabilityEnabled) {
|
||||
await updateDiscoverability({
|
||||
input: {
|
||||
id: workspace.value.id,
|
||||
discoverabilityEnabled: false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (workspace.value.discoverabilityAutoJoinEnabled) {
|
||||
await updateAutoJoin({
|
||||
input: {
|
||||
id: workspace.value.id,
|
||||
discoverabilityAutoJoinEnabled: false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (workspace.value.domainBasedMembershipProtectionEnabled) {
|
||||
await updateDomainProtection({
|
||||
input: {
|
||||
id: workspace.value.id,
|
||||
domainBasedMembershipProtectionEnabled: false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user