Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: user management followup #8458

Merged
merged 8 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 101 additions & 39 deletions packages/nc-gui/components/dlg/InviteDlg.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ProjectRoles, type RoleLabels, WorkspaceUserRoles } from 'nocodb-sdk'

import { extractEmail } from '~/helpers/parsers/parserHelpers'
import { extractEmail } from '../../helpers/parsers/parserHelpers'

const props = defineProps<{
modelValue: boolean
Expand All @@ -12,6 +12,8 @@ const props = defineProps<{
}>()
const emit = defineEmits(['update:modelValue'])

const { baseRoles, workspaceRoles } = useRoles()

const basesStore = useBases()

const workspaceStore = useWorkspace()
Expand All @@ -26,6 +28,10 @@ const orderedRoles = computed(() => {
return props.type === 'base' ? ProjectRoles : WorkspaceUserRoles
})

const userRoles = computed(() => {
return props.type === 'base' ? baseRoles.value : workspaceRoles.value
})

const inviteData = reactive({
email: '',
roles: orderedRoles.value.NO_ACCESS,
Expand All @@ -47,6 +53,28 @@ const emailBadges = ref<Array<string>>([])

const allowedRoles = ref<[]>([])

const isLoading = ref(false)

const organizationStore = useOrganization()

const { listWorkspaces } = organizationStore

const { workspaces } = storeToRefs(organizationStore)

const searchQuery = ref('')

const workSpaceSelectList = computed<WorkspaceType[]>(() => {
return workspaces.value.filter((w: WorkspaceType) => w.title!.toLowerCase().includes(searchQuery.value.toLowerCase()))
})

const checked = reactive<{
[key: string]: boolean
}>({})

const selectedWorkspaces = computed<WorkspaceType[]>(() => {
return workSpaceSelectList.value.filter((ws: WorkspaceType) => checked[ws.id!])
})

const focusOnDiv = () => {
focusRef.value?.focus()
isDivFocused.value = true
Expand Down Expand Up @@ -218,10 +246,9 @@ const onPaste = (e: ClipboardEvent) => {
inviteData.email = ''
}

const workSpaces = ref<NcWorkspace[]>([])

const inviteCollaborator = async () => {
try {
isLoading.value = true
const payloadData = singleEmailValue.value || emailBadges.value.join(',')
if (!payloadData.includes(',')) {
const validationStatus = validateEmail(payloadData)
Expand All @@ -239,7 +266,7 @@ const inviteCollaborator = async () => {
await inviteWsCollaborator(payloadData, inviteData.roles, props.workspaceId)
} else if (props.type === 'organization') {
// TODO: Add support for Bulk Workspace Invite
for (const workspace of workSpaces.value) {
for (const workspace of selectedWorkspaces.value) {
await inviteWsCollaborator(payloadData, inviteData.roles, workspace.id)
}
}
Expand All @@ -252,32 +279,17 @@ const inviteCollaborator = async () => {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
singleEmailValue.value = ''
isLoading.value = false
}
}

const organizationStore = useOrganization()

const { listWorkspaces } = organizationStore

const { workspaces } = storeToRefs(organizationStore)

const workSpaceSelectList = computed(() => {
return workspaces.value.filter((w) => !workSpaces.value.find((ws) => ws.id === w.id))
})

const addToList = (workspaceId: string) => {
workSpaces.value.push(workspaces.value.find((w) => w.id === workspaceId)!)
}
const removeWorkspace = (workspaceId: string) => {
workSpaces.value = workSpaces.value.filter((w) => w.id !== workspaceId)
}
const isOrgSelectMenuOpen = ref(false)

onMounted(async () => {
if (props.type === 'organization') {
await listWorkspaces()
}
})

const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role as ProjectRoles | WorkspaceUserRoles)
</script>

Expand All @@ -291,7 +303,7 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row items-center gap-x-2">
<div class="flex flex-row text-2xl font-bold items-center gap-x-2">
{{
type === 'organization'
? $t('labels.addMembersToOrganization')
Expand Down Expand Up @@ -331,6 +343,7 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
id="email"
ref="focusRef"
v-model="inviteData.email"
:disabled="isLoading"
:placeholder="$t('activity.enterEmail')"
class="w-full min-w-36 outline-none px-2"
data-testid="email-input"
Expand All @@ -354,31 +367,80 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
}}</span>

<template v-if="type === 'organization'">
<NcSelect :placeholder="$t('labels.selectWorkspace')" size="middle" @change="addToList">
<a-select-option v-for="workspace in workSpaceSelectList" :key="workspace.id" :value="workspace.id">
{{ workspace.title }}
</a-select-option>
</NcSelect>

<div class="flex flex-wrap gap-2">
<NcBadge v-for="workspace in workSpaces" :key="workspace.id">
<div class="px-2 flex gap-2 items-center py-1">
<GeneralWorkspaceIcon :workspace="workspace" hide-label size="small" />
<span class="text-gray-600">
{{ workspace.title }}
</span>
<component :is="iconMap.close" class="w-3 h-3" @click="removeWorkspace(workspace.id)" />
<NcDropdown v-model:visible="isOrgSelectMenuOpen">
<NcButton class="!justify-between" full-width size="medium" type="secondary">
<div
:class="{
'!text-gray-600': selectedWorkspaces.length > 0,
}"
class="flex text-gray-500 justify-between items-center w-full"
>
<NcTooltip class="!max-w-130 truncate" show-on-truncate-only>
<span class="">
{{
selectedWorkspaces.length > 0
? selectedWorkspaces.map((w) => w.title).join(', ')
: '-select workspaces to invite to-'
}}
</span>
<template #title>
{{
selectedWorkspaces.length > 0
? selectedWorkspaces.map((w) => w.title).join(', ')
: '-select workspaces to invite to-'
}}
</template>
</NcTooltip>

<component :is="iconMap.chevronDown" />
</div>
</NcBadge>
</div>
</NcButton>
<template #overlay>
<div class="py-2">
<div class="mx-2">
<a-input
v-model:value="searchQuery"
:class="{
'!border-brand-500': searchQuery.length > 0,
}"
class="!rounded-lg !h-8 !ring-0 !placeholder:text-gray-500 !border-gray-200 !px-4"
data-testid="nc-ws-search"
placeholder="Search workspace"
>
<template #prefix>
<component :is="iconMap.search" class="h-4 w-4 mr-1 text-gray-500" />
</template>
</a-input>
</div>

<div class="flex flex-col max-h-64 overflow-y-auto nc-scrollbar-md mt-2">
<div
v-for="ws in workSpaceSelectList"
:key="ws.id"
class="px-4 cursor-pointer hover:bg-gray-100 rounded-lg h-9.5 py-2 w-full flex gap-2"
@click="checked[ws.id!] = !checked[ws.id!]"
>
<div class="flex gap-2 capitalize items-center">
<GeneralWorkspaceIcon :hide-label="true" :workspace="ws" size="small" />
{{ ws.title }}
</div>
<div class="flex-1" />
<NcCheckbox v-model:checked="checked[ws.id!]" size="large" />
</div>
</div>
</div>
</template>
/>
</NcDropdown>
</template>
</div>
</div>
<div class="flex mt-8 justify-end">
<div class="flex gap-2">
<NcButton type="secondary" @click="dialogShow = false"> {{ $t('labels.cancel') }} </NcButton>
<NcButton
:disabled="isInviteButtonDisabled || emailValidation.isError"
:disabled="isInviteButtonDisabled || emailValidation.isError || isLoading"
:loading="isLoading"
size="medium"
type="primary"
class="nc-invite-btn"
Expand Down
64 changes: 64 additions & 0 deletions packages/nc-gui/components/dlg/WorkspaceDelete.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<script lang="ts" setup>
const props = defineProps<{
visible: boolean
workspaceId: string
}>()

const emits = defineEmits(['update:visible'])
const visible = useVModel(props, 'visible', emits)

const workspaceStore = useWorkspace()

const { deleteWorkspace: _deleteWorkspace, loadWorkspaces, navigateToWorkspace, loadWorkspace } = workspaceStore

const { workspacesList, activeWorkspace } = storeToRefs(workspaceStore)

const { refreshCommandPalette } = useCommandPalette()

const workspace = computedAsync(async () => {
if (props.workspaceId) {
const ws = workspacesList.value.find((workspace) => workspace.id === props.workspaceId)
if (!ws) {
await loadWorkspace(props.workspaceId)

return workspacesList.value.find((workspace) => workspace.id === props.workspaceId)
}
}
return activeWorkspace.value ?? workspacesList.value[0]
})

const onDelete = async () => {
if (!workspace.value) return

try {
await _deleteWorkspace(workspace.value.id!)
await loadWorkspaces()

if (!workspacesList.value?.[0]?.id) {
return await navigateToWorkspace()
}

await navigateToWorkspace(workspacesList.value?.[0]?.id)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
refreshCommandPalette()
}
}
</script>

<template>
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.workspace')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="workspace" class="flex flex-row items-center py-2.25 px-2.75 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralIcon icon="workspace" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-2.25"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
>
{{ workspace.title }}
</div>
</div>
</template>
</GeneralDeleteModal>
</template>
38 changes: 34 additions & 4 deletions packages/nc-gui/components/project/AccessSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,27 @@ onMounted(async () => {
}
})

const selected = reactive<{
[key: number]: boolean
}>({})

const toggleSelectAll = (value: boolean) => {
filteredCollaborators.value.forEach((_, i) => {
selected[_.id] = value
})
}

// const isSomeSelected = computed(() => Object.values(selected).some((v) => v))

const selectAll = computed({
get: () =>
Object.values(selected).every((v) => v) &&
Object.keys(selected).length > 0 &&
Object.values(selected).length === filteredCollaborators.value.length,
set: (value) => {
toggleSelectAll(value)
},
})
watch(isInviteModalVisible, () => {
if (!isInviteModalVisible.value) {
loadCollaborators()
Expand All @@ -172,11 +193,12 @@ watch(currentBase, () => {
>
<div v-if="isAdminPanel" class="font-bold w-full !mb-5 text-2xl" data-rec="true">
<div class="flex items-center gap-3">
<!-- TODO: @DarkPhoenix2704 -->
<NuxtLink
:href="`/admin/${orgId}/bases`"
class="!hover:(text-black underline-gray-600) !text-black !underline-transparent ml-0.75 max-w-1/4"
class="!hover:(text-black underline-gray-600) flex items-center !text-black !underline-transparent ml-0.75 max-w-1/4"
>
<component :is="iconMap.arrowLeft" class="text-3xl" />

{{ $t('objects.projects') }}
</NuxtLink>

Expand Down Expand Up @@ -218,8 +240,10 @@ watch(currentBase, () => {
<a-empty :description="$t('title.noMembersFound')" />
</div>
<div v-else class="nc-collaborators-list mt-6 h-full">
<div class="flex flex-col rounded-lg overflow-hidden border-1 max-w-350 max-h-[calc(100%-8rem)]">
<div class="flex flex-col overflow-hidden max-w-350 max-h-[calc(100%-8rem)]">
<div class="flex flex-row bg-gray-50 min-h-12 items-center border-b-1">
<div class="py-3 px-6"><NcCheckbox v-model:checked="selectAll" /></div>

<LazyAccountHeaderWithSorter
class="users-email-grid"
:header="$t('objects.users')"
Expand All @@ -243,8 +267,14 @@ watch(currentBase, () => {
<div
v-for="(collab, i) of sortedCollaborators"
:key="i"
class="user-row flex flex-row border-b-1 py-1 min-h-14 items-center"
:class="{
'bg-[#F0F3FF]': selected[collab.id],
}"
class="user-row flex hover:bg-[#F0F3FF] flex-row border-b-1 py-1 min-h-14 items-center"
>
<div class="py-3 px-6">
<NcCheckbox v-model:checked="selected[collab.id]" />
</div>
<div class="flex gap-3 items-center users-email-grid">
<GeneralUserIcon size="base" :email="collab.email" />
<div class="flex flex-col">
Expand Down
Loading
Loading