Browse Source

refactor/gui-v2-user-management-added

pull/2854/head
Muhammed Mustafa 2 years ago
parent
commit
161b080ca9
  1. 1
      packages/nc-gui-v2/components.d.ts
  2. 3
      packages/nc-gui-v2/components/dashboard/settings/SettingsModal.vue
  3. 247
      packages/nc-gui-v2/components/dashboard/settings/UserManagement.vue
  4. 209
      packages/nc-gui-v2/components/dashboard/settings/userManagement/ShareBase.vue
  5. 215
      packages/nc-gui-v2/components/dashboard/settings/userManagement/UsersModal.vue
  6. 7
      packages/nc-gui-v2/lib/enums.ts
  7. 38
      packages/nc-gui-v2/package-lock.json
  8. 2
      packages/nc-gui-v2/package.json
  9. 56
      packages/nc-gui-v2/utils/miscUtils.ts
  10. 4
      packages/nc-gui-v2/utils/urlUtils.ts
  11. 8
      packages/nc-gui-v2/utils/userUtils.ts

1
packages/nc-gui-v2/components.d.ts vendored

@ -19,6 +19,7 @@ declare module '@vue/runtime-core' {
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']

3
packages/nc-gui-v2/components/dashboard/settings/SettingsModal.vue

@ -4,6 +4,7 @@ import AuditTab from './AuditTab.vue'
import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import UserManagement from './UserManagement.vue'
import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple'
@ -39,7 +40,7 @@ const tabsInfo: TabGroup = {
subTabs: {
usersManagement: {
title: 'Users Management',
body: () => AuditTab,
body: () => UserManagement,
},
apiTokenManagement: {
title: 'API Token Management',

247
packages/nc-gui-v2/components/dashboard/settings/UserManagement.vue

@ -0,0 +1,247 @@
<script setup lang="ts">
import { useDebounce } from '@vueuse/core'
import UsersModal from './userManagement/UsersModal.vue'
import { projectRoleTagColors } from '~/utils/userUtils'
import MidAccountIcon from '~icons/mdi/account-outline'
import ReloadIcon from "~icons/mdi/reload"
import MdiEditIcon from '~icons/ic/round-edit'
import SearchIcon from '~icons/ic/round-search'
import { copyTextToClipboard } from '~/utils/miscUtils'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import EmailIcon from '~icons/eva/email-outline'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiContentCopyIcon from '~icons/mdi/content-copy'
import MdiEmailSendIcon from '~icons/mdi/email-arrow-right-outline'
import RolesIcon from '~icons/mdi/drama-masks'
import SendIcon from '~icons/material-symbols/send'
import { User } from '~~/lib/types'
import { useToast } from 'vue-toastification'
const toast = useToast()
const { $api, $e } = useNuxtApp()
const { project } = useProject()
let users = $ref<null | Array<User>>(null)
let selectedUser = $ref<null | User>(null)
let showUserModal = $ref(false)
let showUserDeleteModal = $ref(false)
let totalRows = $ref(0)
const currentPage = $ref(1)
const currentLimit = $ref(10)
const searchText = ref<string>('')
const debouncedSearchText = useDebounce(searchText, 300)
const loadUsers = async (page = currentPage, limit = currentLimit) => {
try {
console.log('loadUsers', page, limit)
if (!project.value?.id) return
const response = await $api.auth.projectUserList(project.value?.id, <any> {
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
query: searchText.value,
},
})
console.log('userData', response)
if (!response.users) return
totalRows = response.users.pageInfo.totalRows ?? 0
users = response.users.list as Array<User>
} catch (e) {
console.log(e)
}
}
const inviteUser = async (user) => {
try {
if (!project.value?.id) return
await $api.auth.projectUserAdd(project.id, user);
toast.success('Successfully added user to project');
await loadUsers();
} catch (e) {
toast.error(e.response.data.msg);
}
$e('a:user:add');
}
const deleteUser = async () => {
try {
if (!project.value?.id || !selectedUser?.id) return
await $api.auth.projectUserRemove(project.value.id, selectedUser.id);
toast.success('Successfully deleted user from project');
await loadUsers();
} catch (e) {
toast.error(e.response.data.msg);
}
$e('a:user:delete');
}
const onEdit = (user: User) => {
selectedUser = user
showUserModal = true
}
const onInvite = () => {
selectedUser = null
showUserModal = true
}
const onDelete = (user: User) => {
selectedUser = user
showUserDeleteModal = true
}
const resendInvite = async (user: User) => {
if (!project.value?.id ) return
try {
await $api.auth.projectUserResendInvite(project.value.id, user.id);
toast.success('Invite email sent successfully');
await loadUsers();
} catch (e) {
toast.error(e.response.data.msg);
}
$e('a:user:resend-invite');
}
const copyInviteUrl = (user: User) => {
if(!user.invite_token) return
const getInviteUrl = (token: string) => `${location.origin}${location.pathname}#/user/authentication/signup/${token}`;
copyTextToClipboard(getInviteUrl(user.invite_token) );
toast.success('Invite url copied to clipboard');
}
onMounted(async () => {
if (!users) {
await loadUsers()
}
})
watch(
() => debouncedSearchText.value,
() => loadUsers(),
)
</script>
<template>
<div class="flex flex-col w-full px-6">
<UsersModal :key="showUserModal" :show="showUserModal" :selected-user="selectedUser" @closed="showUserModal = false" />
<a-modal v-model:visible="showUserDeleteModal" :closable="false" width="28rem" centered :footer="null">
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">
This action will remove this user from this project
</div>
<div class="flex mt-6 justify-center space-x-2">
<a-button @click="showUserDeleteModal = false"> Cancel </a-button>
<a-button type="primary" danger @click="deleteUser"> Confirm </a-button>
</div>
</div>
</a-modal>
<div class="flex flex-row mb-4 mx-4 justify-between">
<div class="flex w-1/3" >
<a-input v-model:value="searchText" placeholder="Filter by email" >
<template #prefix>
<SearchIcon class="text-gray-400"/>
</template>
</a-input>
</div>
<div class="flex flex-row space-x-1">
<a-button size="middle" type="text" @click="loadUsers()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<ReloadIcon class="text-gray-500" />
<div class="text-gray-500">Reload</div>
</div>
</a-button>
<a-button size="middle" @click="onInvite">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MidAccountIcon />
<div>Invite Team</div>
</div>
</a-button>
</div>
</div>
<div class="px-6">
<div class="flex flex-row border-b-1 pb-2 px-2">
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EmailIcon class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">E-mail</div>
</div>
<div class="flex flex-row justify-center w-1/6 space-x-1 items-center pl-1">
<RolesIcon class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs">Role</div>
</div>
<div class="flex flex-row w-1/6 justify-end items-center pl-1">
<div class="text-gray-600 text-xs">Actions</div>
</div>
</div>
<div v-for="(user, index) in users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2">
<div class="flex w-4/6 flex-wrap">
{{ user.email }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
<div :class="`rounded-full px-2 py-1 bg-[${projectRoleTagColors[user.roles]}]`">
{{ user.roles }}
</div>
</div>
<div class="flex w-1/6 flex-wrap justify-end">
<a-button v-if="user.project_id" type="text" class="!rounded-md" @click="onEdit(user)">
<template #icon>
<MdiEditIcon height="1rem" class="flex mx-auto" />
</template>
</a-button>
<a-button type="text" v-if="!user.project_id" class="!rounded-md " @click="inviteUser(user)">
<template #icon>
<MdiPlusIcon height="1.1rem" class="flex mx-auto" />
</template>
</a-button>
<a-button v-else type="text" class="!rounded-md" @click="onDelete(user)">
<template #icon>
<MdiDeleteOutlineIcon height="1.1rem" class="flex mx-auto" />
</template>
</a-button>
<a-button type="text" class="!rounded-md" @click="resendInvite(user)">
<template #icon>
<MdiEmailSendIcon height="1.1rem" class="flex mx-auto" />
</template>
</a-button>
<a-button type="text" class="!rounded-md" @click="copyInviteUrl(user)">
<template #icon>
<MdiContentCopyIcon height="1.1rem" class="flex mx-auto" />
</template>
</a-button>
</div>
</div>
<a-pagination
hideOnSinglePage
v-model:current="currentPage"
class="mt-4"
:page-size="currentLimit"
:total="totalRows"
show-less-items
@change="loadUsers"
/>
</div>
</div>
</template>
<style scoped>
.users-table {
/* equally spaced columns in table */
table-layout: fixed;
width: 100%;
}
</style>

209
packages/nc-gui-v2/components/dashboard/settings/userManagement/ShareBase.vue

@ -0,0 +1,209 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import OpenInNewIcon from '~icons/mdi/open-in-new'
import { dashboardUrl } from '~/utils/urlUtils'
import MdiReload from '~icons/mdi/reload'
import DownIcon from '~icons/ic/round-keyboard-arrow-down'
import ContentCopyIcon from '~icons/mdi/content-copy'
import MdiXmlIcon from '~icons/mdi/xml'
import { copyTextToClipboard } from '~/utils/miscUtils'
const toast = useToast()
interface ShareBase {
uuid?: string
url?: string
role?: string
}
enum Role {
Owner = 'owner',
Editor = 'editor',
User = 'user',
Guest = 'guest',
Viewer = 'viewer',
}
const { $api, $e } = useNuxtApp()
let base = $ref<null | ShareBase>(null)
const showEditBaseDropdown = $ref(false)
const { project } = useProject()
const url = $computed(() => (base && base.uuid ? `${dashboardUrl()}#/nc/base/${base.uuid}` : null))
const loadBase = async () => {
try {
if (!project.value.id) return
const res = await $api.project.sharedBaseGet(project.value.id)
base = {
uuid: res.uuid,
url: res.url,
role: res.roles,
}
} catch (e) {
console.error(e)
toast.error('Something went wrong')
}
}
const createShareBase = async (role = Role.Viewer) => {
try {
if (!project.value.id) return
const res = await $api.project.sharedBaseUpdate(project.value.id, {
roles: role,
})
base = res || {}
base.role = role
} catch (e) {
console.error(e)
toast.error('Something went wrong')
}
$e('a:shared-base:enable', { role })
}
const disableSharedBase = async () => {
try {
if (!project.value.id) return
await $api.project.sharedBaseDisable(project.value.id)
base = {}
} catch (e) {
toast.error(e.message)
}
$e('a:shared-base:disable')
}
const recreate = async () => {
try {
if (!project.value.id) return
const sharedBase = await $api.project.sharedBaseCreate(project.value.id, {
roles: base?.role || 'viewer',
})
const newBase = sharedBase || {}
base = { ...newBase, role: base?.role }
} catch (e) {
toast.error(e.message)
}
$e('a:shared-base:recreate')
}
const copyUrl = async () => {
if (!url) return
copyTextToClipboard(url)
toast.success('Copied shareable base url to clipboard!')
$e('c:shared-base:copy-url')
}
const navigateToSharedBase = () => {
if (!url) return
window.open(url, '_blank')
$e('c:shared-base:open-url')
}
const generateEmbeddableIframe = () => {
if (!url) return
copyTextToClipboard(`<iframe
class="nc-embed"
src="${url}?embed"
frameborder="0"
width="100%"
height="700"
style="background: transparent; border: 1px solid #ddd"></iframe>`)
toast.success('Copied embeddable html code!')
$e('c:shared-base:copy-embed-frame')
}
onMounted(() => {
if (!base) {
loadBase()
}
})
</script>
<template>
<div class="flex flex-col">
<div class="flex flex-row items-center space-x-0.5 pl-1.5">
<OpenInNewIcon height="0.8rem" class="text-gray-500" />
<div class="text-gray-500 text-xs">Shared Base Link</div>
</div>
<div v-if="base?.uuid" class="flex flex-row mt-2 bg-red-50 py-4 mx-1 px-2 items-center rounded-sm">
<span class="text-xs overflow-x-hidden overflow-ellipsis text-gray-700">{{ url }}</span>
<a-divider class="flex" type="vertical" />
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="recreate">
<template #icon>
<MdiReload height="1rem" class="flex mx-auto text-gray-600" />
</template>
</a-button>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="copyUrl">
<template #icon>
<ContentCopyIcon height="1rem" class="flex mx-auto text-gray-600" />
</template>
</a-button>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="navigateToSharedBase">
<template #icon>
<OpenInNewIcon height="1rem" class="flex mx-auto text-gray-600" />
</template>
</a-button>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="generateEmbeddableIframe">
<template #icon>
<MdiXmlIcon height="1rem" class="flex mx-auto text-gray-600" />
</template>
</a-button>
</div>
<div class="flex text-xs text-gray-400 mt-2 justify-start ml-2">
This link can be used to signup by anyone under this project
</div>
<div class="mt-4 flex flex-row justify-between mx-1">
<a-dropdown v-model="showEditBaseDropdown" class="flex">
<a-button>
<div class="flex flex-row items-center space-x-2">
<div v-if="base?.uuid">Anyone with the link</div>
<div v-else>Disable shared base</div>
<DownIcon height="1rem" />
</div>
</a-button>
<template #overlay>
<a-menu>
<a-menu-item>
<div v-if="base?.uuid" @click="disableSharedBase">Disable shared base</div>
<div v-else @click="createShareBase(Role.Viewer)">Anyone with the link</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-select v-if="base?.uuid" v-model:value="base.role" class="flex">
<a-select-option
v-for="(role, index) in [Role.Editor, Role.Viewer]"
:key="index"
:value="role"
dropdown-class-name="capitalize"
@click="createShareBase(role)"
>
<div class="w-full px-2 capitalize">
{{ role }}
</div>
</a-select-option>
</a-select>
</div>
</div>
</template>
<style scoped></style>

215
packages/nc-gui-v2/components/dashboard/settings/userManagement/UsersModal.vue

@ -0,0 +1,215 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { Form } from 'ant-design-vue'
import ShareBase from './ShareBase.vue'
import SendIcon from '~icons/material-symbols/send-outline'
import CloseIcon from '~icons/material-symbols/close-rounded'
import MidAccountIcon from '~icons/mdi/account-outline'
import ContentCopyIcon from '~icons/mdi/content-copy'
import type { User } from '~~/lib/types'
import { ProjectRole } from '~/lib/enums'
import { projectRoleTagColors } from '~/utils/userUtils'
import { copyTextToClipboard } from '~/utils/miscUtils'
import { extractSdkResponseErrorMsg } from '~~/utils/errorUtils'
import { isEmail } from '~~/utils/validation'
const { show, selectedUser } = defineProps<Props>()
const emits = defineEmits(['closed'])
const toast = useToast()
interface Props {
show: boolean
selectedUser?: User
}
interface Users {
emails: string
role: ProjectRole
invitationToken?: string
}
const { project } = useProject()
const { $api, $e } = useNuxtApp()
const usersData = $ref<Users>({ emails: '', role: ProjectRole.Guest, invitationToken: undefined })
let isFirstRender = $ref(true)
const inviteToken = $ref(null)
const formRef = ref()
const useForm = Form.useForm
const validators = computed(() => {
return {
emails: [
{
validator: (rule: any, value: string, callback: (errMsg?: string) => void) => {
if (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 (isFirstRender && selectedUser) {
usersData.emails = selectedUser.email
usersData.role = selectedUser.roles
}
if (isFirstRender) isFirstRender = false
})
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: selectedUser.roles,
email: selectedUser.email,
project_id: project.value.id,
projectName: project.value.title,
})
} 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
}
toast.success('Successfully updated the user details')
} catch (e) {
toast.error(await extractSdkResponseErrorMsg(e))
}
}
const inviteUrl = $computed(() =>
inviteToken ? `${location.origin}${location.pathname}#/user/authentication/signup/${inviteToken}` : null,
)
const copyUrl = async () => {
if (!inviteUrl) return
copyTextToClipboard(inviteUrl)
toast.success('Copied shareable base url to clipboard!')
$e('c:shared-base:copy-url')
}
const clickInviteMore = () => {
$e('c:user:invite-more')
usersData.invitationToken = undefined
usersData.role = ProjectRole.Guest
usersData.emails = ''
}
</script>
<template>
<a-modal :footer="null" centered :visible="show" :closable="false" width="max(50vw, 44rem)" @cancel="emits('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" type="secondary" :level="5"> Share: {{ project.title }} </a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emits('closed')">
<template #icon>
<CloseIcon class="flex mx-auto" />
</template>
</a-button>
</div>
<div class="px-3">
<template v-if="usersData.invitationToken">
<div class="flex flex-col mt-1 border-b-1 pb-5">
<div class="flex flex-row items-center">
<MidAccountIcon height="1.1rem" class="text-gray-500" />
<div class="text-gray-500 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 w-full justify-between items-center">
<div class="flex pl-2 text-green-700">
{{ inviteUrl }} http://localhost:3001/nc/p_60c3wrrzv563zq#/nc/base/2d6f72d7-b16a-40f2-951c-00b1b90f3920
</div>
<a-button type="text" class="!rounded-md mr-1" @click="copyUrl">
<template #icon>
<ContentCopyIcon height="1rem" class="flex mx-auto text-green-700" />
</template>
</a-button>
</div>
</template>
</a-alert>
<div class="flex text-xs text-gray-400 mt-2 justify-start ml-2">
Looks like you have not configured mailer yet! Please copy above invite link and send it to
{{ usersData.invitationToken && usersData.emails }}.
</div>
<div class="flex flex-row justify-start mt-4 ml-1">
<a-button size="small" outlined @click="clickInviteMore">
<div class="flex flex-row justify-center items-center space-x-0.5">
<SendIcon height="0.8rem" class="flex mx-auto text-gray-600" />
<div class="text-xs text-gray-600">Invite more</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">
<MidAccountIcon height="1rem" class="text-gray-500" />
<div class="text-gray-500 text-xs ml-0.5 mt-0.5">{{ selectedUser ? 'Edit User' : 'Invite Team' }}</div>
</div>
<div class="border-1 py-3 px-4 rounded-md mt-1">
<a-form ref="formRef" :model="usersData" @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"
name="emails"
:rules="[{ required: true, message: 'Please input email' }]"
>
<div class="ml-1 mb-1 text-xs text-gray-500">Email:</div>
<a-input v-model:value="usersData.emails" placeholder="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">Select User Role:</div>
<a-select v-model:value="usersData.role">
<a-select-option v-for="(role, index) in Object.keys(projectRoleTagColors)" :key="index" :value="role">
<div class="flex flex-row h-full justify-start items-center">
<div :class="`px-2 py-1 flex rounded-full text-xs bg-[${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">Submit</a-button>
</div>
</a-form>
</div>
</div>
<div class="flex mt-4">
<ShareBase />
</div>
</div>
</div>
</a-modal>
</template>
<style scoped></style>

7
packages/nc-gui-v2/lib/enums.ts

@ -4,6 +4,13 @@ export enum Role {
User = 'user',
}
export enum ProjectRole {
Owner = 'owner',
Editor = 'editor',
User = 'user',
Guest = 'guest',
}
export enum ClientType {
MYSQL = 'mysql2',
MSSQL = 'mssql',

38
packages/nc-gui-v2/package-lock.json generated

@ -26,7 +26,9 @@
"devDependencies": {
"@antfu/eslint-config": "^0.25.2",
"@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7",
"@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25",
"@iconify-json/ri": "^1.1.3",
@ -10427,6 +10429,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/eva": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@iconify-json/eva/-/eva-1.1.2.tgz",
"integrity": "sha512-4n3sAzXYH4vX2ehi2+kMPP7VHM1TTZ2AKJyUrAogAdbEKWhHr891huFhOSZPRJ9F2/4D4Us8SjRxBHYyUvX80w==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/ic": {
"version": "1.1.7",
"dev": true,
@ -10435,6 +10446,15 @@
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/lucide": {
"version": "1.1.37",
"resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.1.37.tgz",
"integrity": "sha512-GfeDEy61ols35CYLfNxQWCIWdVTf/V0GI29D5IT74ToxcwqjtInQ6YVeNJCfKSGsTnnuyG+M6drd6YXLEfdjvQ==",
"dev": true,
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/material-symbols": {
"version": "1.1.8",
"dev": true,
@ -23062,6 +23082,15 @@
"@iconify/types": "*"
}
},
"@iconify-json/eva": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@iconify-json/eva/-/eva-1.1.2.tgz",
"integrity": "sha512-4n3sAzXYH4vX2ehi2+kMPP7VHM1TTZ2AKJyUrAogAdbEKWhHr891huFhOSZPRJ9F2/4D4Us8SjRxBHYyUvX80w==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify-json/ic": {
"version": "1.1.7",
"dev": true,
@ -23069,6 +23098,15 @@
"@iconify/types": "*"
}
},
"@iconify-json/lucide": {
"version": "1.1.37",
"resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.1.37.tgz",
"integrity": "sha512-GfeDEy61ols35CYLfNxQWCIWdVTf/V0GI29D5IT74ToxcwqjtInQ6YVeNJCfKSGsTnnuyG+M6drd6YXLEfdjvQ==",
"dev": true,
"requires": {
"@iconify/types": "*"
}
},
"@iconify-json/material-symbols": {
"version": "1.1.8",
"dev": true,

2
packages/nc-gui-v2/package.json

@ -32,7 +32,9 @@
"devDependencies": {
"@antfu/eslint-config": "^0.25.2",
"@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7",
"@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25",
"@iconify-json/ri": "^1.1.3",

56
packages/nc-gui-v2/utils/miscUtils.ts

@ -0,0 +1,56 @@
export const copyTextToClipboard = (text: string) => {
const textArea = document.createElement('textarea')
//
// *** This styling is an extra step which is likely not required. ***
//
// Why is it here? To ensure:
// 1. the element is able to have focus and selection.
// 2. if element was to flash render it has minimal visual impact.
// 3. less flakyness with selection and copying which **might** occur if
// the textarea element is not visible.
//
// The likelihood is the element won't even render, not even a
// flash, so some of these are just precautions. However in
// Internet Explorer the element is visible whilst the popup
// box asking the user for permission for the web page to
// copy to the clipboard.
//
// Place in top-left corner of screen regardless of scroll position.
textArea.style.position = 'fixed'
textArea.style.top = '0'
textArea.style.left = '0'
// Ensure it has a small width and height. Setting to 1px / 1em
// doesn't work as this gives a negative w/h on some browsers.
textArea.style.width = '2em'
textArea.style.height = '2em'
// We don't need padding, reducing the size if it does flash render.
textArea.style.padding = '0'
// Clean up any borders.
textArea.style.border = 'none'
textArea.style.outline = 'none'
textArea.style.boxShadow = 'none'
// Avoid flash of white box if rendered for any reason.
textArea.style.background = 'transparent'
textArea.addEventListener('focusin', (e) => e.stopPropagation())
textArea.value = text
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
document.execCommand('copy')
} catch (err) {
console.log('Oops, unable to copy')
}
document.body.removeChild(textArea)
}

4
packages/nc-gui-v2/utils/urlUtils.ts

@ -17,6 +17,10 @@ export const replaceUrlsWithLink = (text: string): boolean | string => {
return found && out
}
export const dashboardUrl = () => {
return `${location.origin}${location.pathname || ''}`
}
// ref : https://stackoverflow.com/a/5717133
export const isValidURL = (str: string) => {
const pattern =

8
packages/nc-gui-v2/utils/userUtils.ts

@ -0,0 +1,8 @@
import { ProjectRole } from '~/lib/enums'
export const projectRoleTagColors = {
[ProjectRole.Owner]: '#cfdffe',
[ProjectRole.Editor]: '#c2f5e8',
[ProjectRole.User]: '#4caf50',
[ProjectRole.Guest]: '#9e9e9e',
}
Loading…
Cancel
Save