mirror of
https://github.com/nocodb/nocodb.git
synced 2026-04-30 10:56:45 +00:00
chore: move nc-gui-v2 to nc-gui
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
<script setup lang="ts">
|
||||
import { Form, message } from 'ant-design-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ShareBase from './ShareBase.vue'
|
||||
import {
|
||||
computed,
|
||||
extractSdkResponseErrorMsg,
|
||||
isEmail,
|
||||
onMounted,
|
||||
projectRoles,
|
||||
ref,
|
||||
useClipboard,
|
||||
useDashboard,
|
||||
useNuxtApp,
|
||||
useProject,
|
||||
} from '#imports'
|
||||
import type { User } from '~/lib'
|
||||
import { ProjectRole } from '~/lib'
|
||||
|
||||
const { show, selectedUser } = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits(['closed', 'reload'])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
selectedUser?: User
|
||||
}
|
||||
|
||||
interface Users {
|
||||
emails?: string
|
||||
role: ProjectRole
|
||||
invitationToken?: string
|
||||
}
|
||||
|
||||
const { project } = useProject()
|
||||
const { $api, $e } = useNuxtApp()
|
||||
const { copy } = useClipboard()
|
||||
const { dashboardUrl } = $(useDashboard())
|
||||
|
||||
const usersData = $ref<Users>({ emails: undefined, role: ProjectRole.Viewer, invitationToken: undefined })
|
||||
const formRef = ref()
|
||||
|
||||
const useForm = Form.useForm
|
||||
const validators = computed(() => {
|
||||
return {
|
||||
emails: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: (errMsg?: string) => void) => {
|
||||
if (!value || value.length === 0) {
|
||||
callback('Email is required')
|
||||
return
|
||||
}
|
||||
const invalidEmails = (value || '').split(/\s*,\s*/).filter((e: string) => !isEmail(e))
|
||||
if (invalidEmails.length > 0) {
|
||||
callback(`${invalidEmails.length > 1 ? ' Invalid emails:' : 'Invalid email:'} ${invalidEmails.join(', ')} `)
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const { validateInfos } = useForm(usersData, validators)
|
||||
|
||||
onMounted(() => {
|
||||
if (!usersData.emails && selectedUser?.email) {
|
||||
usersData.emails = selectedUser.email
|
||||
usersData.role = selectedUser.roles
|
||||
}
|
||||
})
|
||||
|
||||
const saveUser = async () => {
|
||||
$e('a:user:invite', { role: usersData.role })
|
||||
|
||||
if (!project.value.id) return
|
||||
|
||||
await formRef.value?.validateFields()
|
||||
|
||||
try {
|
||||
if (selectedUser?.id) {
|
||||
await $api.auth.projectUserUpdate(project.value.id, selectedUser.id, {
|
||||
roles: usersData.role,
|
||||
email: selectedUser.email,
|
||||
project_id: project.value.id,
|
||||
projectName: project.value.title,
|
||||
})
|
||||
emit('closed')
|
||||
} else {
|
||||
const res = await $api.auth.projectUserAdd(project.value.id, {
|
||||
roles: usersData.role,
|
||||
email: usersData.emails,
|
||||
project_id: project.value.id,
|
||||
projectName: project.value.title,
|
||||
})
|
||||
usersData.invitationToken = res.invite_token
|
||||
}
|
||||
emit('reload')
|
||||
|
||||
// Successfully updated the user details
|
||||
message.success(t('msg.success.userDetailsUpdated'))
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
message.error(await extractSdkResponseErrorMsg(e))
|
||||
}
|
||||
}
|
||||
|
||||
const inviteUrl = $computed(() => (usersData.invitationToken ? `${dashboardUrl}#/signup/${usersData.invitationToken}` : null))
|
||||
|
||||
const copyUrl = async () => {
|
||||
if (!inviteUrl) return
|
||||
|
||||
await copy(inviteUrl)
|
||||
|
||||
// Copied shareable base url to clipboard!
|
||||
message.success(t('msg.success.shareableURLCopied'))
|
||||
|
||||
$e('c:shared-base:copy-url')
|
||||
}
|
||||
|
||||
const clickInviteMore = () => {
|
||||
$e('c:user:invite-more')
|
||||
usersData.invitationToken = undefined
|
||||
usersData.role = ProjectRole.Viewer
|
||||
usersData.emails = undefined
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal :footer="null" centered :visible="show" :closable="false" width="max(50vw, 44rem)" @cancel="emit('closed')">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full">
|
||||
<a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title>
|
||||
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
|
||||
<template #icon>
|
||||
<MaterialSymbolsCloseRounded class="flex mx-auto" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div class="px-2 mt-1.5">
|
||||
<template v-if="usersData.invitationToken">
|
||||
<div class="flex flex-col mt-1 border-b-1 pb-5">
|
||||
<div class="flex flex-row items-center pl-1.5 pb-1 h-[1.1rem]">
|
||||
<MdiAccountOutline />
|
||||
<div class="text-xs ml-0.5 mt-0.5">Copy Invite Token</div>
|
||||
</div>
|
||||
|
||||
<a-alert class="mt-1" type="success" show-icon>
|
||||
<template #message>
|
||||
<div class="flex flex-row justify-between items-center py-1">
|
||||
<div class="flex pl-2 text-green-700 text-xs">
|
||||
{{ inviteUrl }}
|
||||
</div>
|
||||
<a-button type="text" class="!rounded-md -mt-0.5" @click="copyUrl">
|
||||
<template #icon>
|
||||
<MdiContentCopy class="flex mx-auto text-green-700 h-[1rem]" />
|
||||
</template>
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-alert>
|
||||
|
||||
<div class="flex text-xs text-gray-500 mt-2 justify-start ml-2">
|
||||
{{ $t('msg.info.userInviteNoSMTP') }}
|
||||
{{ usersData.invitationToken && usersData.emails }}
|
||||
</div>
|
||||
<div class="flex flex-row justify-start mt-4 ml-2">
|
||||
<a-button size="small" outlined @click="clickInviteMore">
|
||||
<div class="flex flex-row justify-center items-center space-x-0.5">
|
||||
<MaterialSymbolsSendOutline class="flex mx-auto text-gray-600 h-[0.8rem]" />
|
||||
<div class="text-xs text-gray-600">{{ $t('activity.inviteMore') }}</div>
|
||||
</div>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex flex-col pb-4">
|
||||
<div class="flex flex-row items-center pl-2 pb-1 h-[1rem]">
|
||||
<MdiAccountOutline />
|
||||
<div class="text-xs ml-0.5 mt-0.5">{{ selectedUser ? $t('activity.editUser') : $t('activity.inviteTeam') }}</div>
|
||||
</div>
|
||||
<div class="border-1 py-3 px-4 rounded-md mt-1">
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:validate-on-rule-change="false"
|
||||
:model="usersData"
|
||||
validate-trigger="onBlur"
|
||||
@finish="saveUser"
|
||||
>
|
||||
<div class="flex flex-row space-x-4">
|
||||
<div class="flex flex-col w-3/4">
|
||||
<a-form-item
|
||||
v-bind="validateInfos.emails"
|
||||
validate-trigger="onBlur"
|
||||
name="emails"
|
||||
:rules="[{ required: true, message: 'Please input email' }]"
|
||||
>
|
||||
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('datatype.Email') }}:</div>
|
||||
<a-input
|
||||
v-model:value="usersData.emails"
|
||||
validate-trigger="onBlur"
|
||||
:placeholder="$t('labels.email')"
|
||||
:disabled="!!selectedUser"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="flex flex-col w-1/4">
|
||||
<a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]">
|
||||
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div>
|
||||
<a-select v-model:value="usersData.role" class="nc-user-roles">
|
||||
<a-select-option v-for="(role, index) in projectRoles" :key="index" :value="role" class="nc-role-option">
|
||||
<div class="flex flex-row h-full justify-start items-center">
|
||||
<div
|
||||
class="px-2 py-1 flex rounded-full text-xs"
|
||||
:style="{ backgroundColor: projectRoleTagColors[role] }"
|
||||
>
|
||||
{{ role }}
|
||||
</div>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-center">
|
||||
<a-button type="primary" html-type="submit">
|
||||
<div v-if="selectedUser">{{ $t('general.save') }}</div>
|
||||
<div v-else class="flex flex-row justify-center items-center space-x-1.5">
|
||||
<MaterialSymbolsSendOutline class="flex h-[0.8rem]" />
|
||||
<div>{{ $t('activity.invite') }}</div>
|
||||
</div>
|
||||
</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-4">
|
||||
<ShareBase />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user