Browse Source

fix: user management followup (#8458)

* fix(nocodb): image upload

* fix(nc-gui): invite workspace error

* feat: bug fixes(wip)

* fix(nocodb): incorrect members list

* fix(nc-gui): update row size

* fix(nc-gui): some more changes

* fix(nc-gui): show image

* chore: cleanup
pull/8475/head
Anbarasu 2 months ago committed by GitHub
parent
commit
6c4d0482e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 140
      packages/nc-gui/components/dlg/InviteDlg.vue
  2. 64
      packages/nc-gui/components/dlg/WorkspaceDelete.vue
  3. 38
      packages/nc-gui/components/project/AccessSettings.vue
  4. 37
      packages/nc-gui/components/workspace/CollaboratorsList.vue
  5. 4
      packages/nc-gui/components/workspace/View.vue
  6. 9
      packages/nc-gui/composables/useUserSorts.ts
  7. 1
      packages/nc-gui/lang/en.json
  8. 2
      packages/nc-gui/lib/types.ts

140
packages/nc-gui/components/dlg/InviteDlg.vue

@ -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
@ -12,6 +12,8 @@ const props = defineProps<{
}>()
const emit = defineEmits(['update:modelValue'])
const { baseRoles, workspaceRoles } = useRoles()
const basesStore = useBases()
const workspaceStore = useWorkspace()
@ -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,
@ -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
@ -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)
@ -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)
}
}
@ -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>
@ -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')
@ -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"
@ -354,23 +367,71 @@ 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>
@ -378,7 +439,8 @@ const onRoleChange = (role: keyof typeof RoleLabels) => (inviteData.roles = role
<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"

64
packages/nc-gui/components/dlg/WorkspaceDelete.vue

@ -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
packages/nc-gui/components/project/AccessSettings.vue

@ -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()
@ -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>
@ -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')"
@ -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">

37
packages/nc-gui/components/workspace/CollaboratorsList.vue

@ -9,12 +9,20 @@ const { workspaceRoles, loadRoles } = useRoles()
const workspaceStore = useWorkspace()
const { removeCollaborator, updateCollaborator: _updateCollaborator } = workspaceStore
const { removeCollaborator, updateCollaborator: _updateCollaborator, loadWorkspace } = workspaceStore
const { collaborators, activeWorkspace: _activeWorkspace, workspaces } = storeToRefs(workspaceStore)
const { collaborators, activeWorkspace, workspacesList } = storeToRefs(workspaceStore)
const currentWorkspace = computed(() => {
return props.workspaceId ? workspaces.value.get(props.workspaceId) : _activeWorkspace.value
const currentWorkspace = 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 { sorts, loadSorts, handleGetSortedData, toggleSort } = useUserSorts('Workspace')
@ -34,8 +42,8 @@ const filterCollaborators = computed(() => {
return collaborators.value.filter(
(collab) =>
collab.display_name.toLowerCase().includes(userSearchText.value.toLowerCase()) ||
collab.email.toLowerCase().includes(userSearchText.value.toLowerCase()),
collab.display_name?.toLowerCase().includes(userSearchText.value.toLowerCase()) ||
collab.email?.toLowerCase().includes(userSearchText.value.toLowerCase()),
)
})
@ -64,8 +72,10 @@ const selectAll = computed({
})
const updateCollaborator = async (collab: any, roles: WorkspaceUserRoles) => {
if (!currentWorkspace.value || !currentWorkspace.value.id) return
console.log(WorkspaceUserRoles.OWNER)
try {
await _updateCollaborator(collab.id, roles, currentWorkspace.value?.id)
await _updateCollaborator(collab.id, roles, currentWorkspace.value.id)
message.success('Successfully updated user role')
collaborators.value?.forEach((collaborator) => {
@ -93,7 +103,7 @@ onMounted(async () => {
</script>
<template>
<DlgInviteDlg v-model:model-value="inviteDlg" :workspace-id="currentWorkspace?.id" type="workspace" />
<DlgInviteDlg v-if="currentWorkspace" v-model:model-value="inviteDlg" :workspace-id="currentWorkspace?.id" type="workspace" />
<div class="nc-collaborator-table-container mt-4 h-[calc(100vh-10rem)] max-w-350">
<div class="w-full flex justify-between mt-6.5 mb-2">
<a-input v-model:value="userSearchText" class="!max-w-90 !rounded-md mr-4" placeholder="Search members">
@ -138,7 +148,10 @@ onMounted(async () => {
<div
v-for="(collab, i) of sortedCollaborators"
:key="i"
class="user-row flex hover:bg-gray-50 flex-row last:border-b-0 border-b-1 py-1 min-h-14 items-center"
:class="{
'bg-[#F0F3FF]': selected[i],
}"
class="user-row flex hover:bg-[#F0F3FF] flex-row last:border-b-0 border-b-1 py-1 min-h-14 items-center"
>
<div class="py-3 px-6">
<NcCheckbox v-model:checked="selected[i]" />
@ -149,7 +162,7 @@ onMounted(async () => {
<div class="flex flex-col">
<div class="flex gap-3">
<span class="text-gray-800 capitalize font-semibold">
{{ collab.display_name || collab.email.slice(0, collab.email.indexOf('@')) }}
{{ collab.display_name || collab?.email?.slice(0, collab.email.indexOf('@')) }}
</span>
</div>
<span class="text-xs text-gray-600">
@ -159,10 +172,10 @@ onMounted(async () => {
</div>
<div class="w-full flex-1 px-6 py-3">
<div class="w-[30px]">
<template v-if="accessibleRoles.includes(collab.roles)">
<template v-if="accessibleRoles.includes(collab.roles as WorkspaceUserRoles)">
<RolesSelector
:description="false"
:on-role-change="(role) => updateCollaborator(collab, role)"
:on-role-change="(role) => updateCollaborator(collab, role as WorkspaceUserRoles)"
:role="collab.roles"
:roles="accessibleRoles"
class="cursor-pointer"

4
packages/nc-gui/components/workspace/View.vue

@ -77,8 +77,10 @@ onMounted(() => {
<div class="flex items-center gap-3">
<NuxtLink
:href="`/admin/${orgId}/workspaces`"
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('labels.workspaces') }}
</NuxtLink>

9
packages/nc-gui/composables/useUserSorts.ts

@ -138,6 +138,15 @@ export function useUserSorts(roleType: 'Workspace' | 'Org' | 'Project' | 'Organi
return b[sortsConfig.field]?.localeCompare(a[sortsConfig.field])
}
}
case 'baseCount':
case 'workspaceCount':
case 'memberCount': {
if (sortsConfig.direction === 'asc') {
return a[sortsConfig.field] - b[sortsConfig.field]
} else {
return b[sortsConfig.field] - a[sortsConfig.field]
}
}
}
return 0

1
packages/nc-gui/lang/en.json

@ -449,6 +449,7 @@
},
"labels": {
"today": "Today",
"workspace": "Workspace",
"txt": "TXT Record value",
"transferOwnership": "Transfer Ownership",
"recentActivity": "Recent Activity",

2
packages/nc-gui/lib/types.ts

@ -204,7 +204,7 @@ interface SidebarTableNode extends TableType {
}
interface UsersSortType {
field?: 'email' | 'roles' | 'title' | 'id'
field?: 'email' | 'roles' | 'title' | 'id' | 'memberCount' | 'baseCount' | 'workspaceCount'
direction?: 'asc' | 'desc'
}

Loading…
Cancel
Save