Files
speckle-server/packages/frontend-2/components/settings/workspaces/members/MembersTable.vue
T
andrewwallacespeckle c89fe339ec refactor: fix pagination with stable resolveKey, use reactive default… (#4951)
* refactor: fix pagination with stable resolveKey, use reactive defaultRoles, and remove email permission check

* Changes from call

* More changes from call

* WIP fixing team composite cursor

* paginated items fix

* minor rename

* composite cursor tools improved

* fe undoing debugging stuff

* extra fixes

* invitable collabs fix

---------

Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
2025-06-19 10:28:31 +03:00

193 lines
5.9 KiB
Vue

<template>
<div>
<CommonAlert color="neutral" hide-icon class="mb-6 mt-2">
<template #description>
Workspace members can view all projects by default. If on a Viewer seat, they
can view projects on web and comment. If on an Editor seat, they can create new
projects in the workspace and fully contribute to other projects if given the
permission.
</template>
</CommonAlert>
<SettingsWorkspacesMembersTableHeader
v-model:search="search"
v-model:role="roleFilter"
v-model:seat-type="seatTypeFilter"
search-placeholder="Search members..."
:workspace="workspace"
show-role-filter
show-seat-filter
/>
<LayoutTable
class="mt-6 mb-12"
:columns="[
{ id: 'name', header: 'Name', classes: 'col-span-3' },
{ id: 'email', header: 'Email', classes: 'col-span-4' },
{ id: 'seat', header: 'Seat', classes: 'col-span-2' },
{ id: 'joined', header: 'Joined', classes: 'col-span-2' },
{
id: 'actions',
header: '',
classes: 'col-span-1 flex items-center justify-end'
}
]"
:items="members"
:loading="isVeryFirstLoading"
:empty-message="
search.length || seatTypeFilter || roleFilter
? 'No results'
: 'This workspace has no members'
"
>
<template #name="{ item }">
<div class="flex items-center gap-2">
<UserAvatar
hide-tooltip
:user="item.user"
light-style
class="bg-foundation"
no-bg
/>
<span class="truncate text-body-xs text-foreground">
{{ item.user.name }}
<span
v-if="item.id === activeUser?.id"
class="text-foreground-3 text-body-3xs"
>
(You)
</span>
</span>
<CommonBadge
v-if="item.role === Roles.Workspace.Admin"
rounded
color-classes="bg-highlight-3 text-foreground-2"
>
Admin
</CommonBadge>
<div
v-if="
item.user.workspaceDomainPolicyCompliant === false &&
item.role !== Roles.Workspace.Guest
"
v-tippy="
'This user does not comply with the domain policy set on this workspace'
"
>
<ExclamationCircleIcon class="text-danger w-5" />
</div>
</div>
</template>
<template #email="{ item }">
<div class="flex">
<span class="text-foreground-2 truncate">
{{ item.email }}
</span>
</div>
</template>
<template #seat="{ item }">
<SettingsWorkspacesMembersTableSeatType
:seat-type="item.seatType"
:role="Roles.Workspace.Member"
/>
</template>
<template #joined="{ item }">
<span class="text-foreground-2">{{ formattedFullDate(item.joinDate) }}</span>
</template>
<template #actions="{ item }">
<SettingsWorkspacesMembersActionsMenu
:target-user="item"
:workspace="workspace"
:initial-action="selectedAction[item.id]"
/>
</template>
</LayoutTable>
<InfiniteLoading
v-if="members?.length"
:settings="{ identifier }"
class="py-4"
@infinite="onInfiniteLoad"
/>
<SettingsWorkspacesMembersActionsProjectPermissionsDialog
v-model:open="showProjectPermissionsDialog"
:user="targetUser"
:workspace-id="workspace?.id || ''"
/>
</div>
</template>
<script setup lang="ts">
import { Roles, type Nullable, type WorkspaceRoles } from '@speckle/shared'
import {
settingsWorkspacesMembersSearchQuery,
settingsWorkspacesMembersTableQuery
} from '~~/lib/settings/graphql/queries'
import type {
WorkspaceSeatType,
SettingsWorkspacesMembersActionsMenu_UserFragment
} from '~~/lib/common/generated/gql/graphql'
import { graphql } from '~/lib/common/generated/gql'
import { ExclamationCircleIcon } from '@heroicons/vue/24/outline'
import type { WorkspaceUserActionTypes } from '~/lib/settings/helpers/types'
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
import { useQuery } from '@vue/apollo-composable'
graphql(`
fragment SettingsWorkspacesMembersTable_WorkspaceCollaborator on WorkspaceCollaborator {
id
email
...SettingsWorkspacesMembersActionsMenu_User
}
`)
const props = defineProps<{
workspaceSlug: string
}>()
const { activeUser } = useActiveUser()
const { result } = useQuery(settingsWorkspacesMembersTableQuery, () => ({
slug: props.workspaceSlug
}))
const selectedAction = ref<Record<string, WorkspaceUserActionTypes>>({})
const search = ref('')
const roleFilter = ref<WorkspaceRoles>()
const seatTypeFilter = ref<WorkspaceSeatType>()
const showProjectPermissionsDialog = ref(false)
const targetUser = ref<SettingsWorkspacesMembersActionsMenu_UserFragment | undefined>(
undefined
)
const defaultRoles = shallowRef([Roles.Workspace.Admin, Roles.Workspace.Member])
const {
identifier,
onInfiniteLoad,
query: { result: membersResult },
isVeryFirstLoading
} = usePaginatedQuery({
query: settingsWorkspacesMembersSearchQuery,
baseVariables: computed(() => ({
limit: 10,
slug: props.workspaceSlug,
filter: {
search: search.value,
roles: roleFilter.value ? [roleFilter.value] : defaultRoles.value,
seatType: seatTypeFilter.value
},
cursor: null as Nullable<string>
})),
resolveKey: (vars) => ({
slug: vars.slug,
filter: vars.filter
}),
resolveCurrentResult: (res) => res?.workspaceBySlug.team,
resolveNextPageVariables: (baseVars, cursor) => ({
...baseVars,
cursor
}),
resolveCursorFromVariables: (vars) => vars.cursor
})
const workspace = computed(() => result.value?.workspaceBySlug)
const members = computed(() => membersResult.value?.workspaceBySlug.team.items)
</script>