Browse Source

feat: gui

pull/7833/head
DarkPhoenix2704 8 months ago
parent
commit
6125c2fca3
  1. 12
      packages/nc-gui/components/nc/Modal.vue
  2. 20
      packages/nc-gui/components/project/AccessSettings.vue
  3. 296
      packages/nc-gui/components/project/ShareBaseDlg.vue
  4. 2
      packages/nc-gui/components/roles/Selector.vue
  5. 2
      packages/nc-gui/components/workspace/InviteSection.vue
  6. 4
      packages/nc-gui/lang/en.json
  7. 1
      packages/nocodb/src/schema/swagger.json

12
packages/nc-gui/components/nc/Modal.vue

@ -6,19 +6,21 @@ const props = withDefaults(
size?: 'small' | 'medium' | 'large'
destroyOnClose?: boolean
maskClosable?: boolean
showSeparator?: boolean
wrapClassName?: string
}>(),
{
size: 'medium',
destroyOnClose: true,
maskClosable: true,
showSeparator: true,
wrapClassName: '',
},
)
const emits = defineEmits(['update:visible'])
const { width: propWidth, destroyOnClose, maskClosable, wrapClassName: _wrapClassName } = props
const { width: propWidth, destroyOnClose, maskClosable, wrapClassName: _wrapClassName, showSeparator } = props
const { isMobileMode } = useGlobal()
@ -98,7 +100,13 @@ const slots = useSlots()
maxHeight: height,
}"
>
<div v-if="slots.header" class="flex pb-2 mb-2 text-lg font-medium border-b-1 border-gray-100">
<div
v-if="slots.header"
:class="{
'border-b-1 border-gray-100': showSeparator,
}"
class="flex pb-2 mb-2 text-lg font-medium"
>
<slot name="header" />
</div>

20
packages/nc-gui/components/project/AccessSettings.vue

@ -9,8 +9,8 @@ import {
timeAgo,
} from 'nocodb-sdk'
import type { Roles, WorkspaceUserRoles } from 'nocodb-sdk'
import { isEeUI, storeToRefs, useUserSorts } from '#imports'
import type { User } from '#imports'
import { isEeUI, storeToRefs, useUserSorts } from '#imports'
const basesStore = useBases()
const { getBaseUsers, createProjectUser, updateProjectUser, removeProjectUser } = basesStore
@ -22,6 +22,8 @@ const { sorts, sortDirection, loadSorts, saveOrUpdate, handleGetSortedData } = u
const isSuper = computed(() => orgRoles.value?.[OrgUserRoles.SUPER_ADMIN])
const isInviteModalVisible = ref(false)
interface Collaborators {
id: string
email: string
@ -132,20 +134,34 @@ onMounted(async () => {
isLoading.value = false
}
})
watch(isInviteModalVisible, () => {
if (!isInviteModalVisible.value) {
loadCollaborators()
}
})
</script>
<template>
<div class="nc-collaborator-table-container mt-4 nc-access-settings-view h-[calc(100vh-8rem)]">
<LazyProjectShareBaseDlg v-model:model-value="isInviteModalVisible" />
<div v-if="isLoading" class="nc-collaborators-list items-center justify-center">
<GeneralLoader size="xlarge" />
</div>
<template v-else>
<div class="w-full flex flex-row justify-between items-baseline mt-6.5 mb-2 pr-0.25">
<div class="w-full flex flex-row justify-between items-baseline max-w-350 mt-6.5 mb-2 pr-0.25">
<a-input v-model:value="userSearchText" class="!max-w-90 !rounded-md" :placeholder="$t('title.searchMembers')">
<template #prefix>
<PhMagnifyingGlassBold class="!h-3.5 text-gray-500" />
</template>
</a-input>
<NcButton size="small" @click="isInviteModalVisible = true">
<div class="flex gap-1">
<component :is="iconMap.plus" class="w-4 h-4" />
{{ $t('activity.addMembers') }}
</div>
</NcButton>
</div>
<div v-if="isSearching" class="nc-collaborators-list items-center justify-center">

296
packages/nc-gui/components/project/ShareBaseDlg.vue

@ -0,0 +1,296 @@
<script setup lang="ts">
import { OrderedProjectRoles, ProjectRoles } from 'nocodb-sdk'
import type { User } from '#imports'
const props = defineProps<{
modelValue: boolean
baseId: string
}>()
const emit = defineEmits(['update:modelValue'])
const dialogShow = useVModel(props, 'modelValue', emit)
const inviteData = reactive({
email: '',
roles: ProjectRoles.NO_ACCESS,
})
const { baseRoles } = useRoles()
const basesStore = useBases()
const { activeProjectId } = storeToRefs(basesStore)
const { createProjectUser } = basesStore
const divRef = ref<HTMLDivElement>()
const focusRef = ref<HTMLInputElement>()
const isDivFocused = ref(false)
const emailValidation = reactive({
isError: true,
message: '',
})
const allowedRoles = ref<ProjectRoles[]>([])
onMounted(async () => {
try {
const currentRoleIndex = OrderedProjectRoles.findIndex(
(role) => baseRoles.value && Object.keys(baseRoles.value).includes(role),
)
if (currentRoleIndex !== -1) {
allowedRoles.value = OrderedProjectRoles.slice(currentRoleIndex + 1).filter((r) => r)
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
})
const singleEmailValue = ref('')
const emailBadges = ref<Array<string>>([])
const insertOrUpdateString = (str: string) => {
// Check if the string already exists in the array
const index = emailBadges.value.indexOf(str)
if (index !== -1) {
// If the string exists, remove it
emailBadges.value.splice(index, 1)
}
// Add the new string to the array
emailBadges.value.push(str)
}
const emailInputValidation = (input: string): boolean => {
if (!input.length) {
emailValidation.isError = true
emailValidation.message = 'Email should not be empty'
return false
}
if (!validateEmail(input.trim())) {
emailValidation.isError = true
emailValidation.message = 'Invalid Email'
return false
}
return true
}
const isInvitButtonDiabled = computed(() => {
if (!emailBadges.value.length && !singleEmailValue.value.length) {
return true
}
if (emailBadges.value.length && inviteData.email) {
return true
}
})
watch(inviteData, (newVal) => {
// when user only want to enter a single email
// we dont convert that as badge
const isSingleEmailValid = validateEmail(newVal.email)
if (isSingleEmailValid && !emailBadges.value.length) {
singleEmailValue.value = newVal.email
emailValidation.isError = false
return
}
singleEmailValue.value = ''
// when user enters multiple emails comma sepearted or space sepearted
const isNewEmail = newVal.email.charAt(newVal.email.length - 1) === ',' || newVal.email.charAt(newVal.email.length - 1) === ' '
if (isNewEmail && newVal.email.trim().length) {
const emailToAdd = newVal.email.split(',')[0].trim() || newVal.email.split(' ')[0].trim()
if (!validateEmail(emailToAdd)) {
emailValidation.isError = true
emailValidation.message = 'Invalid Email'
return
}
/**
if email is already enterd we delete the already
existing email and add new one
**/
if (emailBadges.value.includes(emailToAdd)) {
insertOrUpdateString(emailToAdd)
inviteData.email = ''
return
}
emailBadges.value.push(emailToAdd)
inviteData.email = ''
singleEmailValue.value = ''
}
if (!newVal.email.length && emailValidation.isError) {
emailValidation.isError = false
}
})
const handleEnter = () => {
const isEmailIsValid = emailInputValidation(inviteData.email)
if (!isEmailIsValid) return
inviteData.email += ' '
emailValidation.isError = false
emailValidation.message = ''
}
const focusOnDiv = () => {
focusRef.value?.focus()
isDivFocused.value = true
}
// remove one email per backspace
onKeyStroke('Backspace', () => {
if (isDivFocused.value && inviteData.email.length < 1) {
emailBadges.value.pop()
}
})
watch(dialogShow, (newVal) => {
if (newVal) {
setTimeout(() => {
focusOnDiv()
}, 100)
}
})
// when bulk email is pasted
const onPaste = (e: ClipboardEvent) => {
const pastedText = e.clipboardData?.getData('text')
const inputArray = pastedText?.split(',') || pastedText?.split(' ')
// if data is pasted to a already existing text in input
// we add existingInput + pasted data
if (inputArray?.length === 1 && inviteData.email.length) {
inputArray[0] = inviteData.email += inputArray[0]
}
inputArray?.forEach((el) => {
const isEmailIsValid = emailInputValidation(el)
if (!isEmailIsValid) return
/**
if email is already enterd we delete the already
existing email and add new one
**/
if (emailBadges.value.includes(el)) {
insertOrUpdateString(el)
return
}
emailBadges.value.push(el)
inviteData.email = ''
})
inviteData.email = ''
}
const inviteProjectCollaborator = async () => {
try {
const payloadData = singleEmailValue.value || emailBadges.value.join(',')
if (!payloadData.includes(',')) {
const validationStatus = validateEmail(payloadData)
if (!validationStatus) {
emailValidation.isError = true
emailValidation.message = 'invalid email'
}
}
await createProjectUser(activeProjectId.value!, {
email: payloadData,
roles: inviteData.roles,
} as unknown as User)
message.success('Invitation sent successfully')
inviteData.email = ''
emailBadges.value = []
dialogShow.value = false
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
singleEmailValue.value = ''
}
}
</script>
<template>
<NcModal
v-model:visible="dialogShow"
:show-separator="false"
:header="$t('activity.createTable')"
size="medium"
@keydown.esc="dialogShow = false"
>
<template #header>
<div class="flex flex-row items-center gap-x-2">
{{ $t('activity.addMember') }}
</div>
</template>
<div class="flex items-center justify-between gap-3 mt-2">
<div class="flex w-full flex-col">
<div class="flex justify-between gap-3 w-full">
<div
ref="divRef"
class="flex items-center border-1 gap-1 w-full overflow-x-scroll nc-scrollbar-x-md items-center h-10 rounded-lg !min-w-96"
tabindex="0"
:class="{
'border-primary/100': isDivFocused,
'p-1': emailBadges?.length > 1,
}"
@click="focusOnDiv"
@blur="isDivFocused = false"
>
<span
v-for="(email, index) in emailBadges"
:key="email"
class="border-1 text-gray-800 bg-gray-100 rounded-md flex items-center px-2 py-1"
>
{{ email }}
<component
:is="iconMap.close"
class="ml-0.5 hover:cursor-pointer mt-0.5 w-4 h-4"
@click="emailBadges.splice(index, 1)"
/>
</span>
<input
id="email"
ref="focusRef"
v-model="inviteData.email"
:placeholder="$t('activity.enterEmail')"
class="w-full min-w-36 outline-none px-2"
data-testid="email-input"
@keyup.enter="handleEnter"
@blur="isDivFocused = false"
@paste.prevent="onPaste"
/>
</div>
<RolesSelector
size="lg"
class="nc-invite-role-selector"
:role="inviteData.roles"
:roles="allowedRoles"
:on-role-change="(role: ProjectRoles) => (inviteData.roles = role)"
:description="false"
/>
</div>
<span v-if="emailValidation.isError && emailValidation.message" class="ml-2 text-red-500 text-[10px] mt-1.5">{{
emailValidation.message
}}</span>
</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
type="primary"
size="medium"
:disabled="isInvitButtonDiabled || emailValidation.isError"
@click="inviteProjectCollaborator"
>
{{ $t('activity.inviteToBase') }}
</NcButton>
</div>
</div>
</NcModal>
</template>

2
packages/nc-gui/components/roles/Selector.vue

@ -11,7 +11,7 @@ const props = withDefaults(
description?: boolean
inherit?: string
onRoleChange: (role: keyof typeof RoleLabels) => void
size: 'sm' | 'md'
size: 'sm' | 'md' | 'lg'
}>(),
{
description: true,

2
packages/nc-gui/components/workspace/InviteSection.vue

@ -6,7 +6,7 @@ import { validateEmail } from '~/utils/validation'
const inviteData = reactive({
email: '',
roles: WorkspaceUserRoles.VIEWER,
roles: WorkspaceUserRoles.NO_ACCESS,
})
const focusRef = ref<HTMLInputElement>()

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

@ -701,6 +701,10 @@
"clearSelection": "Clear selection"
},
"activity": {
"addMembers": "Add Members",
"enterEmail": "Enter email addresses",
"inviteToBase": "Invite to Base",
"addMember": "Add Member to Base",
"noRange": "Calendar view requires a date range",
"goToToday": "Go to Today",
"toggleSidebar": "Toggle Sidebar",

1
packages/nocodb/src/schema/swagger.json

@ -22601,7 +22601,6 @@
"type": "object",
"properties": {
"email": {
"format": "email",
"type": "string",
"description": "Base User Email"
},

Loading…
Cancel
Save