Skip to content

Commit

Permalink
feat(fe2): Frontend should show a pre-filtered list of users for work…
Browse files Browse the repository at this point in the history
…space members (#2845)

* Show prefiltered list for members in workspace with project ownership rights

* Fix: Added dropdown for project role

* Fix typo

---------

Co-authored-by: Mike Tasset <[email protected]>
  • Loading branch information
andrewwallacespeckle and Mikehrn authored Sep 2, 2024
1 parent 76ba079 commit 8787a5c
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 58 deletions.
3 changes: 2 additions & 1 deletion packages/frontend-2/components/form/select/ProjectRoles.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
v-model="selectedValue"
:items="Object.values(Roles.Stream)"
:multiple="multiple"
clearable
:clearable="clearable"
name="projectRoles"
label="Project roles"
class="min-w-[150px]"
Expand Down Expand Up @@ -57,6 +57,7 @@ const emit = defineEmits<{
const props = defineProps<{
multiple?: boolean
modelValue?: ValueType
clearable?: boolean
}>()
const elementToWatchForChanges = ref(null as Nullable<HTMLElement>)
Expand Down
179 changes: 128 additions & 51 deletions packages/frontend-2/components/project/page/InviteDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,65 +2,94 @@
<LayoutDialog v-model:open="isOpen" max-width="md" :buttons="dialogButtons">
<template #header>Invite to project</template>
<div class="flex flex-col gap-4 my-2">
<FormSelectWorkspaceRoles
v-if="project?.workspaceId"
v-model="workspaceRole"
show-label
label="Workspace role"
size="lg"
help="If target user does not have a role in the parent workspace, they will be assigned this role."
:allow-unset="false"
/>
<FormTextInput
v-model="search"
name="search"
size="lg"
placeholder="Search by email or username..."
:disabled="disabled"
:help="disabled ? 'You must be the project owner to invite users' : ''"
input-classes="pr-[85px] text-sm"
color="foundation"
label="Add people"
show-label
>
<template #input-right>
<div
class="absolute inset-y-0 right-0 flex items-center pr-2"
:class="disabled ? 'pointer-events-none' : ''"
>
<ProjectPageTeamPermissionSelect v-model="role" hide-remove />
</div>
</template>
</FormTextInput>
<div
v-if="hasTargets"
class="flex flex-col border bg-foundation border-primary-muted rounded-md"
>
<template v-if="searchUsers.length">
<ProjectPageTeamDialogInviteUserServerUserRow
v-for="user in searchUsers"
:key="user.id"
:user="user"
<div v-if="!isWorkspaceMemberAndProjectOwner" class="flex flex-col gap-4">
<FormSelectWorkspaceRoles
v-if="project?.workspaceId"
v-model="workspaceRole"
show-label
label="Workspace role"
size="lg"
help="If target user does not have a role in the parent workspace, they will be assigned this role."
:allow-unset="false"
/>
<FormTextInput
v-model="search"
name="search"
size="lg"
placeholder="Search by email or username..."
:disabled="disabled"
:help="disabled ? 'You must be the project owner to invite users' : ''"
input-classes="pr-[85px] text-sm"
color="foundation"
label="Add people"
show-label
>
<template #input-right>
<div
class="absolute inset-y-0 right-0 flex items-center pr-2"
:class="disabled ? 'pointer-events-none' : ''"
>
<ProjectPageTeamPermissionSelect v-model="role" hide-remove />
</div>
</template>
</FormTextInput>

<div
v-if="hasTargets"
class="flex flex-col border bg-foundation border-primary-muted rounded-md"
>
<template v-if="searchUsers.length">
<ProjectPageTeamDialogInviteUserServerUserRow
v-for="user in searchUsers"
:key="user.id"
:user="user"
:stream-role="role"
:disabled="loading"
@invite-user="($event) => onInviteUser($event.user)"
/>
</template>
<ProjectPageTeamDialogInviteUserEmailsRow
v-else-if="selectedEmails?.length"
:selected-emails="selectedEmails"
:stream-role="role"
:disabled="loading"
:target-workspace-role="workspaceRole"
@invite-user="($event) => onInviteUser($event.user)"
:is-guest-mode="isGuestMode"
:unmatching-domain-policy="unmatchingDomainPolicy"
class="p-2"
@invite-emails="($event) => onInviteUser($event.emails, $event.serverRole)"
/>
</template>
<ProjectPageTeamDialogInviteUserEmailsRow
v-else-if="selectedEmails?.length"
:selected-emails="selectedEmails"
:stream-role="role"
:disabled="loading"
:is-guest-mode="isGuestMode"
:unmatching-domain-policy="unmatchingDomainPolicy"
class="p-2"
@invite-emails="($event) => onInviteUser($event.emails, $event.serverRole)"
</div>
</div>
<div v-else class="flex flex-col gap-4">
<FormSelectProjectRoles
v-model="role"
show-label
label="Project role"
size="lg"
/>
<div>
<div class="text-body-xs font-medium mb-1">Add users from workspace</div>
<div
v-if="filteredInviteMembers.length"
class="flex flex-col border bg-foundation border-primary-muted rounded-md"
>
<ProjectPageTeamDialogInviteUserServerUserRow
v-for="user in filteredInviteMembers"
:key="user.id"
:user="user"
:stream-role="role"
:disabled="loading"
:target-workspace-role="workspaceRole"
@invite-user="($event) => onInviteUser($event.user)"
/>
</div>
<p v-else class="text-sm text-gray-500 mt-4">No available users found.</p>
</div>
</div>
</div>
</LayoutDialog>
</template>

<script setup lang="ts">
import { Roles } from '@speckle/shared'
import type { ServerRoles, StreamRoles, WorkspaceRoles } from '@speckle/shared'
Expand All @@ -85,6 +114,22 @@ graphql(`
fragment ProjectPageInviteDialog_Project on Project {
id
workspaceId
workspace {
team {
items {
role
user {
id
name
bio
company
avatar
verified
role
}
}
}
}
...ProjectPageTeamInternals_Project
workspace {
id
Expand Down Expand Up @@ -112,13 +157,33 @@ const projectId = computed(() => props.projectId as string)
const projectData = computed(() => props.project)
const { collaboratorListItems } = useTeamInternals(projectData)
const workspaceMembers = computed(() => {
return props.project?.workspace?.team?.items || []
})
const filteredInviteMembers = computed(() => {
const currentProjectMemberIds = new Set(
collaboratorListItems.value.map((item) => item.user?.id)
)
return workspaceMembers.value
.filter(
(member) =>
member.user && member.user.id && !currentProjectMemberIds.has(member.user.id)
)
.map((member) => member.user)
})
const loading = ref(false)
const search = ref('')
const role = ref<StreamRoles>(Roles.Stream.Contributor)
const workspaceRole = ref<WorkspaceRoles>(Roles.Workspace.Guest)
const { isGuestMode } = useServerInfo()
const createInvite = useInviteUserToProject()
const { activeUser } = useActiveUser()
const {
users: searchUsers,
emails: selectedEmails,
Expand All @@ -133,6 +198,18 @@ const {
workspaceId: props.project?.workspaceId
})
const isWorkspaceMemberAndProjectOwner = computed(() => {
const userIsWorkspaceMember =
workspaceMembers.value.some(
(item) =>
item.user?.id === activeUser.value?.id && item.role === Roles.Workspace.Member
) ?? false
const userIsProjectOwner = projectData.value?.role === Roles.Stream.Owner
return userIsWorkspaceMember && userIsProjectOwner
})
const dialogButtons = computed<LayoutDialogButton[]>(() => [
{
text: 'Cancel',
Expand Down
1 change: 1 addition & 0 deletions packages/frontend-2/components/projects/Dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
v-model="selectedRoles"
class="md:w-56 grow md:grow-0"
fixed-height
clearable
/>
</div>
<FormButton v-if="!isGuest" @click="openNewProject = true">
Expand Down
4 changes: 2 additions & 2 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 ...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 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 @@ -503,7 +503,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 ...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 ...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 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"];
/**
* 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 8787a5c

Please sign in to comment.