|
|
@ -1,7 +1,7 @@ |
|
|
|
<script lang="ts" setup> |
|
|
|
<script lang="ts" setup> |
|
|
|
import { ProjectRoles, type RoleLabels, WorkspaceUserRoles } from 'nocodb-sdk' |
|
|
|
import { ProjectRoles, type RoleLabels, WorkspaceUserRoles } from 'nocodb-sdk' |
|
|
|
|
|
|
|
|
|
|
|
import { extractEmail } from '~/helpers/parsers/parserHelpers' |
|
|
|
import { extractEmail } from '../../helpers/parsers/parserHelpers' |
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps<{ |
|
|
|
const props = defineProps<{ |
|
|
|
modelValue: boolean |
|
|
|
modelValue: boolean |
|
|
@ -12,6 +12,8 @@ const props = defineProps<{ |
|
|
|
}>() |
|
|
|
}>() |
|
|
|
const emit = defineEmits(['update:modelValue']) |
|
|
|
const emit = defineEmits(['update:modelValue']) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { baseRoles, workspaceRoles } = useRoles() |
|
|
|
|
|
|
|
|
|
|
|
const basesStore = useBases() |
|
|
|
const basesStore = useBases() |
|
|
|
|
|
|
|
|
|
|
|
const workspaceStore = useWorkspace() |
|
|
|
const workspaceStore = useWorkspace() |
|
|
@ -26,6 +28,10 @@ const orderedRoles = computed(() => { |
|
|
|
return props.type === 'base' ? ProjectRoles : WorkspaceUserRoles |
|
|
|
return props.type === 'base' ? ProjectRoles : WorkspaceUserRoles |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const userRoles = computed(() => { |
|
|
|
|
|
|
|
return props.type === 'base' ? baseRoles.value : workspaceRoles.value |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const inviteData = reactive({ |
|
|
|
const inviteData = reactive({ |
|
|
|
email: '', |
|
|
|
email: '', |
|
|
|
roles: orderedRoles.value.NO_ACCESS, |
|
|
|
roles: orderedRoles.value.NO_ACCESS, |
|
|
@ -47,6 +53,28 @@ const emailBadges = ref<Array<string>>([]) |
|
|
|
|
|
|
|
|
|
|
|
const allowedRoles = ref<[]>([]) |
|
|
|
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 = () => { |
|
|
|
const focusOnDiv = () => { |
|
|
|
focusRef.value?.focus() |
|
|
|
focusRef.value?.focus() |
|
|
|
isDivFocused.value = true |
|
|
|
isDivFocused.value = true |
|
|
@ -218,10 +246,9 @@ const onPaste = (e: ClipboardEvent) => { |
|
|
|
inviteData.email = '' |
|
|
|
inviteData.email = '' |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const workSpaces = ref<NcWorkspace[]>([]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const inviteCollaborator = async () => { |
|
|
|
const inviteCollaborator = async () => { |
|
|
|
try { |
|
|
|
try { |
|
|
|
|
|
|
|
isLoading.value = true |
|
|
|
const payloadData = singleEmailValue.value || emailBadges.value.join(',') |
|
|
|
const payloadData = singleEmailValue.value || emailBadges.value.join(',') |
|
|
|
if (!payloadData.includes(',')) { |
|
|
|
if (!payloadData.includes(',')) { |
|
|
|
const validationStatus = validateEmail(payloadData) |
|
|
|
const validationStatus = validateEmail(payloadData) |
|
|
@ -239,7 +266,7 @@ const inviteCollaborator = async () => { |
|
|
|
await inviteWsCollaborator(payloadData, inviteData.roles, props.workspaceId) |
|
|
|
await inviteWsCollaborator(payloadData, inviteData.roles, props.workspaceId) |
|
|
|
} else if (props.type === 'organization') { |
|
|
|
} else if (props.type === 'organization') { |
|
|
|
// TODO: Add support for Bulk Workspace Invite |
|
|
|
// 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) |
|
|
|
await inviteWsCollaborator(payloadData, inviteData.roles, workspace.id) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
@ -252,32 +279,17 @@ const inviteCollaborator = async () => { |
|
|
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
|
|
message.error(await extractSdkResponseErrorMsg(e)) |
|
|
|
} finally { |
|
|
|
} finally { |
|
|
|
singleEmailValue.value = '' |
|
|
|
singleEmailValue.value = '' |
|
|
|
|
|
|
|
isLoading.value = false |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const organizationStore = useOrganization() |
|
|
|
const isOrgSelectMenuOpen = ref(false) |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => { |
|
|
|
onMounted(async () => { |
|
|
|
if (props.type === 'organization') { |
|
|
|
if (props.type === 'organization') { |
|
|
|
await listWorkspaces() |
|
|
|
await listWorkspaces() |
|
|
|
} |
|
|
|
} |
|
|
|
}) |
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role as ProjectRoles | WorkspaceUserRoles) |
|
|
|
const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role as ProjectRoles | WorkspaceUserRoles) |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
@ -291,7 +303,7 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role |
|
|
|
@keydown.esc="dialogShow = false" |
|
|
|
@keydown.esc="dialogShow = false" |
|
|
|
> |
|
|
|
> |
|
|
|
<template #header> |
|
|
|
<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' |
|
|
|
type === 'organization' |
|
|
|
? $t('labels.addMembersToOrganization') |
|
|
|
? $t('labels.addMembersToOrganization') |
|
|
@ -331,6 +343,7 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role |
|
|
|
id="email" |
|
|
|
id="email" |
|
|
|
ref="focusRef" |
|
|
|
ref="focusRef" |
|
|
|
v-model="inviteData.email" |
|
|
|
v-model="inviteData.email" |
|
|
|
|
|
|
|
:disabled="isLoading" |
|
|
|
:placeholder="$t('activity.enterEmail')" |
|
|
|
:placeholder="$t('activity.enterEmail')" |
|
|
|
class="w-full min-w-36 outline-none px-2" |
|
|
|
class="w-full min-w-36 outline-none px-2" |
|
|
|
data-testid="email-input" |
|
|
|
data-testid="email-input" |
|
|
@ -354,23 +367,71 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role |
|
|
|
}}</span> |
|
|
|
}}</span> |
|
|
|
|
|
|
|
|
|
|
|
<template v-if="type === 'organization'"> |
|
|
|
<template v-if="type === 'organization'"> |
|
|
|
<NcSelect :placeholder="$t('labels.selectWorkspace')" size="middle" @change="addToList"> |
|
|
|
<NcDropdown v-model:visible="isOrgSelectMenuOpen"> |
|
|
|
<a-select-option v-for="workspace in workSpaceSelectList" :key="workspace.id" :value="workspace.id"> |
|
|
|
<NcButton class="!justify-between" full-width size="medium" type="secondary"> |
|
|
|
{{ workspace.title }} |
|
|
|
<div |
|
|
|
</a-select-option> |
|
|
|
:class="{ |
|
|
|
</NcSelect> |
|
|
|
'!text-gray-600': selectedWorkspaces.length > 0, |
|
|
|
|
|
|
|
}" |
|
|
|
<div class="flex flex-wrap gap-2"> |
|
|
|
class="flex text-gray-500 justify-between items-center w-full" |
|
|
|
<NcBadge v-for="workspace in workSpaces" :key="workspace.id"> |
|
|
|
> |
|
|
|
<div class="px-2 flex gap-2 items-center py-1"> |
|
|
|
<NcTooltip class="!max-w-130 truncate" show-on-truncate-only> |
|
|
|
<GeneralWorkspaceIcon :workspace="workspace" hide-label size="small" /> |
|
|
|
<span class=""> |
|
|
|
<span class="text-gray-600"> |
|
|
|
{{ |
|
|
|
{{ workspace.title }} |
|
|
|
selectedWorkspaces.length > 0 |
|
|
|
|
|
|
|
? selectedWorkspaces.map((w) => w.title).join(', ') |
|
|
|
|
|
|
|
: '-select workspaces to invite to-' |
|
|
|
|
|
|
|
}} |
|
|
|
</span> |
|
|
|
</span> |
|
|
|
<component :is="iconMap.close" class="w-3 h-3" @click="removeWorkspace(workspace.id)" /> |
|
|
|
<template #title> |
|
|
|
|
|
|
|
{{ |
|
|
|
|
|
|
|
selectedWorkspaces.length > 0 |
|
|
|
|
|
|
|
? selectedWorkspaces.map((w) => w.title).join(', ') |
|
|
|
|
|
|
|
: '-select workspaces to invite to-' |
|
|
|
|
|
|
|
}} |
|
|
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
</NcTooltip> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<component :is="iconMap.chevronDown" /> |
|
|
|
|
|
|
|
</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> |
|
|
|
</NcBadge> |
|
|
|
|
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
</template> |
|
|
|
|
|
|
|
/> |
|
|
|
|
|
|
|
</NcDropdown> |
|
|
|
</template> |
|
|
|
</template> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
@ -378,7 +439,8 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role |
|
|
|
<div class="flex gap-2"> |
|
|
|
<div class="flex gap-2"> |
|
|
|
<NcButton type="secondary" @click="dialogShow = false"> {{ $t('labels.cancel') }} </NcButton> |
|
|
|
<NcButton type="secondary" @click="dialogShow = false"> {{ $t('labels.cancel') }} </NcButton> |
|
|
|
<NcButton |
|
|
|
<NcButton |
|
|
|
:disabled="isInviteButtonDisabled || emailValidation.isError" |
|
|
|
:disabled="isInviteButtonDisabled || emailValidation.isError || isLoading" |
|
|
|
|
|
|
|
:loading="isLoading" |
|
|
|
size="medium" |
|
|
|
size="medium" |
|
|
|
type="primary" |
|
|
|
type="primary" |
|
|
|
class="nc-invite-btn" |
|
|
|
class="nc-invite-btn" |
|
|
|