Browse Source

Merge branch 'develop' into chore/docker-build

pull/3273/head
Wing-Kam Wong 2 years ago
parent
commit
feef675385
  1. 4
      packages/nc-gui-v2/assets/style.scss
  2. 9
      packages/nc-gui-v2/components/dashboard/GithubStarButton.vue
  3. 23
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  4. 38
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  5. 7
      packages/nc-gui-v2/components/dashboard/settings/app-store/AppInstall.vue
  6. 26
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  7. 51
      packages/nc-gui-v2/components/dlg/TableRename.vue
  8. 85
      packages/nc-gui-v2/components/general/HelpAndSupport.vue
  9. 10
      packages/nc-gui-v2/components/general/ShareBaseButton.vue
  10. 55
      packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue
  11. 57
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  12. 75
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  13. 4
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  14. 4
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  15. 2
      packages/nc-gui-v2/composables/useProject.ts
  16. 15
      packages/nc-gui-v2/composables/useTable.ts
  17. 4
      packages/nc-gui-v2/github-star.shims.d.ts
  18. 4
      packages/nc-gui-v2/layouts/base.vue
  19. 403
      packages/nc-gui-v2/package-lock.json
  20. 2
      packages/nc-gui-v2/package.json
  21. 9
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  22. 244
      packages/nc-gui-v2/pages/index/index.vue
  23. 140
      packages/nc-gui-v2/pages/index/index/[id].vue
  24. 144
      packages/nc-gui-v2/pages/index/index/create-external.vue
  25. 131
      packages/nc-gui-v2/pages/index/index/create.vue
  26. 211
      packages/nc-gui-v2/pages/index/index/index.vue
  27. 17
      packages/nc-gui-v2/pages/index/user/index/index.vue
  28. 5
      packages/nc-gui-v2/pages/project/index.vue
  29. 83
      packages/nc-gui-v2/pages/project/index/[id].vue
  30. 78
      packages/nc-gui-v2/pages/project/index/create.vue
  31. 2
      packages/nc-gui-v2/pages/signin.vue
  32. 12
      packages/nc-gui-v2/windi.config.ts
  33. 3
      packages/nc-gui/components/ProjectTreeView.vue
  34. 5
      packages/nocodb-sdk/src/lib/Api.ts
  35. 81
      packages/nocodb/src/lib/meta/api/tableApis.ts
  36. 16
      packages/nocodb/src/lib/models/Model.ts
  37. 12
      packages/nocodb/src/lib/plugins/smtp/SMTP.ts
  38. 13
      packages/nocodb/src/lib/plugins/smtp/index.ts
  39. 2
      packages/nocodb/tests/pg-cy-quick/01-cy-quick.sql
  40. 6
      scripts/cypress/integration/common/1a_table_operations.js
  41. 3
      scripts/cypress/support/page_objects/mainPage.js
  42. 8
      scripts/sdk/swagger.json

4
packages/nc-gui-v2/assets/style.scss

@ -210,6 +210,10 @@ a {
}
}
.ant-dropdown-menu {
@apply !p-0 !rounded;
}
.ant-dropdown-menu-submenu-popup {
@apply scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !shadow !rounded;
}

9
packages/nc-gui-v2/components/dashboard/GithubStarButton.vue

@ -1,9 +0,0 @@
<script setup lang="ts">
import GithubButton from 'vue-github-button'
</script>
<template>
<GithubButton href="https://github.com/nocodb/nocodb" data-icon="octicon-star" data-show-count="true" data-size="large"
>Star</GithubButton
>
</template>

23
packages/nc-gui-v2/components/dashboard/TreeView.vue

@ -2,6 +2,7 @@
import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue'
import GithubButton from 'vue-github-button'
import {
computed,
inject,
@ -401,12 +402,22 @@ function openTableCreateDialog() {
<a-divider class="!my-0" />
<div class="flex items-start flex-col justify-start px-4 py-3 gap-2">
<GeneralShareBaseButton class="py-1 px-2 text-primary font-bold cursor-pointer select-none" />
<GeneralHelpAndSupport class="py-1 px-2 text-gray-500 cursor-pointer select-none" />
<DashboardGithubStarButton class="ml-2 py-1" />
<div class="flex items-start flex-col justify-start px-2 py-3 gap-2">
<GeneralShareBaseButton
class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent"
/>
<GeneralHelpAndSupport class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
<GithubButton
class="ml-2 py-1"
href="https://github.com/nocodb/nocodb"
data-icon="octicon-star"
data-show-count="true"
data-size="large"
>
Star
</GithubButton>
</div>
</div>
</template>

38
packages/nc-gui-v2/components/dashboard/settings/Modal.vue

@ -1,19 +1,17 @@
<script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue'
import { useI18n } from 'vue-i18n'
import AuditTab from './AuditTab.vue'
import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import Misc from './Misc.vue'
import { useI18n, useUIPermission, useVModel, watch } from '#imports'
import ApiTokenManagement from '~/components/tabs/auth/ApiTokenManagement.vue'
import UserManagement from '~/components/tabs/auth/UserManagement.vue'
import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple'
import NootbookOutline from '~icons/mdi/notebook-outline'
import { useUIPermission, useVModel, watch } from '#imports'
import MdiCloseIcon from '~icons/mdi/close'
interface Props {
modelValue: boolean
@ -140,22 +138,27 @@ watch(
>
<div class="flex flex-row justify-between w-full items-center mb-1">
<a-typography-title class="ml-4 select-none" type="secondary" :level="5">SETTINGS</a-typography-title>
<a-button type="text" class="!rounded-md border-none -mt-1.5 -mr-1" @click="vModel = false">
<template #icon>
<MdiCloseIcon class="cursor-pointer mt-1 nc-modal-close" />
<MdiClose class="cursor-pointer mt-1 nc-modal-close" />
</template>
</a-button>
</div>
<a-layout class="mt-3 modal-body flex">
<a-layout class="mt-3 h-[75vh] overflow-y-auto flex">
<!-- Side tabs -->
<a-layout-sider theme="light">
<a-menu v-model:selected-keys="selectedTabKeys" class="h-full" mode="inline" :open-keys="[]">
<a-menu-item v-for="(tab, key) of tabsInfo" :key="key">
<div class="flex flex-row items-center space-x-2">
<component :is="tab.icon" class="flex" />
<div class="flex select-none">
<a-layout-sider>
<a-menu v-model:selected-keys="selectedTabKeys" class="tabs-menu h-full" :open-keys="[]">
<a-menu-item
v-for="(tab, key) of tabsInfo"
:key="key"
class="group active:(!ring-0) hover:(!bg-primary !bg-opacity-25)"
>
<div class="flex items-center space-x-2">
<component :is="tab.icon" class="group-hover:text-accent" />
<div class="select-none">
{{ tab.title }}
</div>
</div>
@ -166,7 +169,7 @@ watch(
<!-- Sub Tabs -->
<a-layout-content class="h-auto px-4 scrollbar-thumb-gray-500">
<a-menu v-model:selectedKeys="selectedSubTabKeys" :open-keys="[]" mode="horizontal">
<a-menu-item v-for="(tab, key) of selectedTab.subTabs" :key="key" class="select-none">
<a-menu-item v-for="(tab, key) of selectedTab.subTabs" :key="key" class="active:(!ring-0) select-none">
{{ tab.title }}
</a-menu-item>
</a-menu>
@ -177,9 +180,10 @@ watch(
</a-modal>
</template>
<style scoped>
.modal-body {
@apply h-[75vh];
@apply overflow-y-auto;
<style lang="scss" scoped>
.tabs-menu {
:deep(.ant-menu-item-selected) {
@apply border-r-3 border-primary bg-primary !bg-opacity-25;
}
}
</style>

7
packages/nc-gui-v2/components/dashboard/settings/app-store/AppInstall.vue

@ -103,6 +103,13 @@ const readPluginDetails = async () => {
const emptyParsedInput = formDetails.array ? [{}] : {}
const parsedInput = res.input ? JSON.parse(res.input) : emptyParsedInput
// the type of 'secure' was XcType.SingleLineText in 0.0.1
// and it has been changed to XcType.Checkbox, since 0.0.2
// hence, change the text value to boolean here
if ('secure' in parsedInput && typeof parsedInput.secure === 'string') {
parsedInput.secure = !!parsedInput.secure
}
plugin = { ...res, formDetails, parsedInput }
pluginFormData = plugin.parsedInput
} catch (e) {

26
packages/nc-gui-v2/components/dlg/TableCreate.vue

@ -19,7 +19,7 @@ const inputEl = ref<HTMLInputElement>()
const { addTab } = useTabs()
const { loadTables } = useProject()
const { loadTables, isMysql, isMssql, isPg } = useProject()
const { table, createTable, generateUniqueTitle, tables, project } = useTable(async (table) => {
await loadTables()
@ -39,7 +39,29 @@ const validateDuplicateAlias = (v: string) => (tables.value || []).every((t) =>
const validators = computed(() => {
return {
title: [validateTableName, validateDuplicateAlias],
title: [
validateTableName,
validateDuplicateAlias,
{
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255
if (isMysql) {
tableNameLengthLimit = 64
} else if (isPg) {
tableNameLengthLimit = 63
} else if (isMssql) {
tableNameLengthLimit = 128
}
const projectPrefix = project?.value?.prefix || ''
if ((projectPrefix + value).length > tableNameLengthLimit) {
return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`))
}
resolve()
})
},
},
],
table_name: [validateTableName],
}
})

51
packages/nc-gui-v2/components/dlg/TableRename.vue

@ -24,8 +24,7 @@ const dialogShow = computed({
})
const { updateTab } = useTabs()
const { loadTables } = useProject()
const { tables } = useProject()
const { loadTables, tables, project, isMysql, isMssql, isPg } = useProject()
const inputEl = $ref<any>()
let loading = $ref(false)
@ -38,18 +37,39 @@ const validators = computed(() => {
title: [
validateTableName,
{
validator: (rule: any, value: any, callback: (errMsg?: string) => void) => {
if (/^\s+|\s+$/.test(value)) {
callback('Leading or trailing whitespace not allowed in table name')
}
if (
!(tables?.value || []).every(
(t) => t.id === tableMeta.id || t.table_name.toLowerCase() !== (value || '').toLowerCase(),
)
) {
callback('Duplicate table alias')
}
callback()
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
let tableNameLengthLimit = 255
if (isMysql) {
tableNameLengthLimit = 64
} else if (isPg) {
tableNameLengthLimit = 63
} else if (isMssql) {
tableNameLengthLimit = 128
}
const projectPrefix = project?.value?.prefix || ''
if ((projectPrefix + value).length > tableNameLengthLimit) {
return reject(new Error(`Table name exceeds ${tableNameLengthLimit} characters`))
}
resolve()
})
},
},
{
validator: (rule: any, value: any) => {
return new Promise<void>((resolve, reject) => {
if (/^\s+|\s+$/.test(value)) {
return reject(new Error('Leading or trailing whitespace not allowed in table name'))
}
if (
!(tables?.value || []).every(
(t) => t.id === tableMeta.id || t.table_name.toLowerCase() !== (value || '').toLowerCase(),
)
) {
return reject(new Error('Duplicate table alias'))
}
resolve()
})
},
},
],
@ -71,7 +91,8 @@ const renameTable = async () => {
loading = true
try {
await $api.dbTable.update(tableMeta?.id as string, {
title: formState.title,
project_id: tableMeta?.project_id,
table_name: formState.title,
})
dialogShow.value = false
loadTables()

85
packages/nc-gui-v2/components/general/HelpAndSupport.vue

@ -15,54 +15,53 @@ const openSwaggerLink = () => {
</script>
<template>
<div>
<div @click="showDrawer = true">
<div class="flex items-center space-x-1">
<MdiCommentTextOutline class="mr-1 nc-share-base" />
<!-- todo: i18n -->
<div>APIs & Support</div>
</div>
</div>
<div
class="flex items-center space-x-1 w-full cursor-pointer pl-3 py-1.5 hover:(text-primary bg-primary bg-opacity-5)"
@click="showDrawer = true"
>
<MdiCommentTextOutline class="mr-1 nc-share-base" />
<!-- todo: i18n -->
<div>APIs & Support</div>
</div>
<a-drawer
v-model:visible="showDrawer"
class="h-full relative"
placement="right"
size="small"
:closable="false"
:body-style="{ padding: '12px 24px 0 24px', background: '#fafafa' }"
>
<div class="flex flex-col w-full h-full p-4 pb-0">
<!-- todo: i18n -->
<a-typography-title :level="4" class="!mb-6 !text-gray-500">Help center</a-typography-title>
<a-drawer
v-model:visible="showDrawer"
class="h-full relative"
placement="right"
size="small"
:closable="false"
:body-style="{ padding: '12px 24px 0 24px', background: '#fafafa' }"
>
<div class="flex flex-col w-full h-full p-4 pb-0">
<!-- todo: i18n -->
<a-typography-title :level="4" class="!mb-6 !text-gray-500">Help center</a-typography-title>
<GeneralSocialCard show-swagger-link class="!w-full nc-social-card">
<template #before>
<a-list-item v-if="project">
<nuxt-link
v-t="['e:docs']"
class="text-primary !no-underline !text-current py-4 font-weight-medium"
target="_blank"
@click="openSwaggerLink"
>
<div class="ml-3 flex items-center text-sm">
<LogosSwagger />
<!-- todo: i18n -->
<span class="ml-3">{{ project.title }} : Swagger Documentation</span>
</div>
</nuxt-link>
</a-list-item>
</template>
</GeneralSocialCard>
<GeneralSocialCard class="!w-full nc-social-card">
<template #before>
<a-list-item v-if="project">
<nuxt-link
v-t="['e:docs']"
class="!no-underline !text-current py-4 font-semibold"
target="_blank"
@click="openSwaggerLink"
>
<div class="ml-3 flex items-center text-sm">
<LogosSwagger />
<!-- todo: i18n -->
<span class="ml-3">{{ project.title }} : Swagger Documentation</span>
</div>
</nuxt-link>
</a-list-item>
</template>
</GeneralSocialCard>
<div class="flex-1 my-2"></div>
<div class="flex-1 my-2"></div>
<GeneralSponsors class="!w-full" />
<GeneralSponsors class="!w-full" />
<div class="min-h-10 w-full" />
</div>
</a-drawer>
</div>
<div class="min-h-10 w-full" />
</div>
</a-drawer>
</template>
<style scoped lang="scss">

10
packages/nc-gui-v2/components/general/ShareBaseButton.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { useRoute } from '#imports'
import { useRoute, useUIPermission } from '#imports'
const route = useRoute()
@ -9,13 +9,13 @@ const { isUIAllowed } = useUIPermission()
</script>
<template>
<div class="flex items-center">
<div class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)">
<div
v-if="
isUIAllowed('newUser') &&
route.name !== 'index' &&
route.name !== 'project-index-create' &&
route.name !== 'project-index-create-external' &&
route.name !== 'index-index-create' &&
route.name !== 'index-index-create-external' &&
route.name !== 'index-user-index'
"
@click="showUserModal = true"
@ -23,9 +23,11 @@ const { isUIAllowed } = useUIPermission()
<div class="flex items-center space-x-1">
<MdiAccountPlusOutline class="mr-1 nc-share-base" />
<!-- todo: i18n <div>{{ $t('activity.share') }}</div> -->
<div>{{ $t('activity.inviteTeam') }}</div>
</div>
</div>
<TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
</div>
</template>

55
packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { computed, inject, ref, useSmartsheetStoreOrThrow } from '#imports'
import { ReloadViewDataHookInj } from '~/context'
import { ReloadViewDataHookInj, computed, inject, onClickOutside, ref, useSmartsheetStoreOrThrow } from '#imports'
const reloadData = inject(ReloadViewDataHookInj)!
@ -9,6 +8,10 @@ const { search, meta } = useSmartsheetStoreOrThrow()
// todo: where is this value supposed to come from? it's not in the store
const isDropdownOpen = ref(false)
const searchDropdown = ref(null)
onClickOutside(searchDropdown, () => (isDropdownOpen.value = false))
const columns = computed(() =>
meta.value?.columns?.map((c) => ({
value: c.id,
@ -22,21 +25,35 @@ function onPressEnter() {
</script>
<template>
<a-input v-model:value="search.query" size="small" class="max-w-[200px]" placeholder="Filter query" @press-enter="onPressEnter">
<template #addonBefore>
<div class="flex items-center relative" @click="isDropdownOpen = true">
<MdiMagnify class="text-grey" />
<MdiMenuDown class="text-grey" />
<a-select
v-model:value="search.field"
size="small"
:dropdown-match-select-width="false"
:options="columns"
class="!absolute top-0 left-0 w-full h-full z-10 !text-xs opacity-0"
>
</a-select>
</div>
</template>
</a-input>
<div class="flex flex-row border-1 rounded-sm">
<div
ref="searchDropdown"
class="flex items-center relative bg-gray-50 px-2 cursor-pointer border-r-1"
:class="{ '!bg-gray-100 ': isDropdownOpen }"
@click="isDropdownOpen = !isDropdownOpen"
>
<MdiMagnify class="text-grey" />
<MdiMenuDown class="text-grey" />
<a-select
v-model:value="search.field"
:open="isDropdownOpen"
size="small"
:dropdown-match-select-width="false"
:options="columns"
dropdown-class-name="!py-0 !rounded"
class="!absolute top-0 left-0 w-full h-full z-10 !text-xs opacity-0"
/>
</div>
<a-input
v-model:value="search.query"
size="small"
class="max-w-[200px]"
placeholder="Filter query"
:bordered="false"
@press-enter="onPressEnter"
>
<template #addonBefore> </template>
</a-input>
</div>
</template>

57
packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import { ViewTypes } from 'nocodb-sdk'
import { ref } from '#imports'
import { viewIcons } from '~/utils'
import { ref, useUIPermission, viewIcons } from '#imports'
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void
@ -13,8 +12,6 @@ const { isUIAllowed } = useUIPermission()
const isView = ref(false)
const showWebhookDrawer = ref(false)
function onOpenModal(type: ViewTypes, title = '') {
emits('openModal', { type, title })
}
@ -22,7 +19,6 @@ function onOpenModal(type: ViewTypes, title = '') {
<template>
<a-menu :selected-keys="[]" class="flex flex-col">
<div class="flex-1"></div>
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')">
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }}
@ -38,7 +34,7 @@ function onOpenModal(type: ViewTypes, title = '') {
{{ $t('msg.info.addView.grid') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<div class="nc-project-menu-item text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :style="{ color: viewIcons[ViewTypes.GRID].color }" />
<div>{{ $t('objects.viewType.grid') }}</div>
@ -50,17 +46,13 @@ function onOpenModal(type: ViewTypes, title = '') {
</a-tooltip>
</a-menu-item>
<a-menu-item
key="gallery"
class="group !flex !items-center !-my0 !h-[30px] nc-create-2-view"
@click="onOpenModal(ViewTypes.GALLERY)"
>
<a-menu-item key="gallery" class="group !flex !items-center !my-0 nc-create-2-view" @click="onOpenModal(ViewTypes.GALLERY)">
<a-tooltip :mouse-enter-delay="1" placement="left">
<template #title>
{{ $t('msg.info.addView.gallery') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<div class="nc-project-menu-item text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :style="{ color: viewIcons[ViewTypes.GALLERY].color }" />
<div>{{ $t('objects.viewType.gallery') }}</div>
@ -83,7 +75,7 @@ function onOpenModal(type: ViewTypes, title = '') {
{{ $t('msg.info.addView.form') }}
</template>
<div class="text-xs flex items-center h-full w-full gap-2">
<div class="nc-project-menu-item text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :style="{ color: viewIcons[ViewTypes.FORM].color }" />
<div>{{ $t('objects.viewType.form') }}</div>
@ -95,44 +87,7 @@ function onOpenModal(type: ViewTypes, title = '') {
</a-tooltip>
</a-menu-item>
<div class="w-full h-4"></div>
<div class="w-full h-4" />
</div>
<!--
todo: bring back later
<general-flipping-card class="my-4 lg:my-6 min-h-[100px]" :triggers="['click', { duration: 15000 }]">
<template #front>
<div class="flex h-full w-full gap-6 flex-col">
<general-social />
<div>
<a
v-t="['e:hiring']"
class="px-4 py-3 rounded border text-xs text-current"
href="https://angel.co/company/nocodb"
target="_blank"
@click.stop
>
🚀 We are Hiring! 🚀
</a>
</div>
</div>
</template>
<template #back>
&lt;!&ndash; todo: add project cost &ndash;&gt;
<a
href="https://github.com/sponsors/nocodb"
target="_blank"
class="group flex items-center gap-2 w-full mx-3 px-4 py-3 rounded-l border transform translate-x-4 hover:(translate-x-0 shadow-lg !opacity-100) transition duration-150 ease !text-xs text-current"
@click.stop
>
<mdi-cards-heart class="text-red-500" />
{{ $t('activity.sponsorUs') }}
</a>
</template>
</general-flipping-card> -->
<WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" />
</a-menu>
</template>

75
packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue

@ -1,14 +1,26 @@
<script lang="ts" setup>
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import type { ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Ref } from 'vue'
import Sortable from 'sortablejs'
import RenameableMenuItem from './RenameableMenuItem.vue'
import { inject, onMounted, ref, useApi, useRoute, useRouter, watch } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import { ActiveViewInj, ViewListInj } from '~/context'
import {
ActiveViewInj,
ViewListInj,
extractSdkResponseErrorMsg,
inject,
onMounted,
ref,
useApi,
useDialog,
useRoute,
useRouter,
watch,
} from '#imports'
import DlgViewDelete from '~/components/dlg/ViewDelete.vue'
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string }): void
@ -18,6 +30,12 @@ interface Emits {
const emits = defineEmits<Emits>()
const viewTypeAlias = {
[ViewTypes.GRID as any]: 'grid',
[ViewTypes.FORM as any]: 'form',
[ViewTypes.GALLERY as any]: 'gallery',
}
const activeView = inject(ActiveViewInj, ref())
const views = inject<Ref<any[]>>(ViewListInj, ref([]))
@ -34,11 +52,6 @@ const selected = ref<string[]>([])
/** dragging renamable view items */
let dragging = $ref(false)
let deleteModalVisible = $ref(false)
/** view to delete for modal */
let toDelete = $ref<Record<string, any> | undefined>()
const menuRef = $ref<typeof AntMenu>()
let isMarked = $ref<string | false>(false)
@ -158,25 +171,34 @@ async function onRename(view: ViewType) {
}
/** Open delete modal */
async function onDelete(view: Record<string, any>) {
toDelete = view
deleteModalVisible = true
}
function openDeleteDialog(view: Record<string, any>) {
const isOpen = ref(true)
const { close } = useDialog(DlgViewDelete, {
'modelValue': isOpen,
'view': view,
'onUpdate:modelValue': closeDialog,
'onDeleted': () => {
closeDialog()
emits('deleted')
// return to the default view
activeView.value = views.value[0]
},
})
function closeDialog() {
isOpen.value = false
/** View was deleted, trigger reload */
function onDeleted() {
emits('deleted')
toDelete = undefined
deleteModalVisible = false
// return to the default view
activeView.value = views.value[0]
close(1000)
}
}
</script>
<template>
<a-menu ref="menuRef" :class="{ dragging }" class="nc-views-menu flex-1" :selected-keys="selected">
<RenameableMenuItem
v-for="view of views"
v-for="(view, index) of views"
:id="view.id"
:key="view.id"
:view="view"
@ -184,17 +206,16 @@ function onDeleted() {
class="transition-all ease-in duration-300"
:class="{
'bg-gray-100': isMarked === view.id,
'active': route.params.viewTitle && route.params.viewTitle === view.title,
[`nc-view-item nc-${view.type}-view-item`]: true,
'active':
(route.params.viewTitle && route.params.viewTitle === view.title) || (route.params.viewTitle === '' && index === 0),
[`nc-view-item nc-${viewTypeAlias[view.type] || view.type}-view-item`]: true,
}"
@change-view="changeView"
@open-modal="$emit('openModal', $event)"
@delete="onDelete"
@delete="openDeleteDialog(view)"
@rename="onRename"
/>
</a-menu>
<dlg-view-delete v-model="deleteModalVisible" :view="toDelete" @deleted="onDeleted" />
</template>
<style lang="scss">
@ -217,7 +238,7 @@ function onDeleted() {
}
.ant-menu-item:not(.sortable-chosen) {
@apply color-transition hover:!bg-transparent;
@apply color-transition;
}
.sortable-chosen {

4
packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -146,14 +146,14 @@ function onStopEdit() {
<template>
<a-menu-item
class="select-none group !flex !items-center !my-0"
class="select-none group !flex !items-center !my-0 hover:(bg-primary !bg-opacity-5)"
@dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick()"
@click.stop="onClick"
>
<div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">
<div class="flex w-auto">
<MdiDrag
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 !cursor-move"
@click.stop.prevent
/>

4
packages/nc-gui-v2/components/smartsheet/sidebar/index.vue

@ -107,9 +107,7 @@ function onCreate(view: ViewType) {
<div v-if="isOpen" class="flex-1 flex flex-col min-h-0">
<MenuTop @open-modal="openModal" @deleted="loadViews" @sorted="loadViews" />
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="px-3">
<div class="!my-3 w-full border-b-1 border-dashed" />
</div>
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="!my-3 w-full border-b-1" />
<MenuBottom @open-modal="openModal" />
</div>

2
packages/nc-gui-v2/composables/useProject.ts

@ -20,6 +20,7 @@ export function useProject(projectId?: MaybeRef<string>) {
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
const isMssql = computed(() => projectBaseType === 'mssql')
const isPg = computed(() => projectBaseType === 'pg')
const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
@ -82,6 +83,7 @@ export function useProject(projectId?: MaybeRef<string>) {
loadProject,
loadTables,
isMysql,
isMssql,
isPg,
sqlUi,
isSharedBase,

15
packages/nc-gui-v2/composables/useTable.ts

@ -30,12 +30,15 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
return table.columns.includes(col.column_name)
})
const tableMeta = await $api.dbTable.create(project?.value?.id as string, {
...table,
columns,
})
onTableCreate?.(tableMeta)
try {
const tableMeta = await $api.dbTable.create(project?.value?.id as string, {
...table,
columns,
})
onTableCreate?.(tableMeta)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
watch(

4
packages/nc-gui-v2/github-star.shims.d.ts vendored

@ -0,0 +1,4 @@
declare module 'vue-github-button' {
import type { Component } from '@vue/runtime-core'
export default Component
}

4
packages/nc-gui-v2/layouts/base.vue

@ -40,7 +40,7 @@ hooks.hook('page:finish', () => {
class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg"
>
<div
v-if="route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'"
v-if="!route.params.projectType"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>
@ -59,7 +59,7 @@ hooks.hook('page:finish', () => {
<GeneralReleaseInfo />
<GeneralShareBaseButton v-if="!isSharedBase" />
<GeneralShareBaseButton v-if="!isSharedBase" class="pr-4 font-semibold" />
<a-tooltip placement="bottom" :mouse-enter-delay="1">
<template #title> Switch language</template>

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

@ -14,6 +14,7 @@
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jsep": "^1.3.6",
"json-schema-traverse": "^1.0.0",
"just-clone": "^6.1.1",
"jwt-decode": "^3.1.2",
"locale-codes": "^1.3.1",
@ -56,6 +57,7 @@
"@windicss/plugin-animations": "^1.0.9",
"@windicss/plugin-question-mark": "^0.1.1",
"@windicss/plugin-scrollbar": "^1.2.3",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"happy-dom": "^6.0.3",
@ -906,7 +908,6 @@
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
"integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
"dev": true,
"peer": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@ -927,7 +928,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -944,7 +944,6 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
"integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
"dev": true,
"peer": true,
"dependencies": {
"type-fest": "^0.20.2"
},
@ -959,15 +958,13 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schem-traverse/-/json-schem-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/@eslint/eslintrc/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
@ -976,11 +973,10 @@
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
"integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz",
"integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==",
"dev": true,
"peer": true,
"dependencies": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
@ -990,12 +986,21 @@
"node": ">=10.10.0"
}
},
"node_modules/@humanwhocodes/gitignore-to-minimatch": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz",
"integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==",
"dev": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schem-1.2.1.tgz",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/@iconify-json/bi": {
"version": "1.1.6",
@ -3744,8 +3749,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/array-includes": {
"version": "3.1.5",
@ -4140,7 +4144,6 @@
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=6"
}
@ -5265,8 +5268,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/deepmerge": {
"version": "4.2.2",
@ -5392,7 +5394,6 @@
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"peer": true,
"dependencies": {
"esutils": "^2.0.2"
},
@ -6224,14 +6225,14 @@
}
},
"node_modules/eslint": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz",
"integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==",
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.22.0.tgz",
"integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint/eslintrc": "^1.3.0",
"@humanwhocodes/config-array": "^0.9.2",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -6241,14 +6242,17 @@
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.3.2",
"espree": "^9.3.3",
"esquery": "^1.4.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"functional-red-black-tree": "^1.0.1",
"glob-parent": "^6.0.1",
"globals": "^13.15.0",
"globby": "^11.1.0",
"grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
@ -6767,7 +6771,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -6784,7 +6787,6 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"peer": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@ -6801,7 +6803,6 @@
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
@ -6814,7 +6815,6 @@
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
"dev": true,
"peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@ -6828,17 +6828,31 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=4.0"
}
},
"node_modules/eslint/node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"peer": true,
"dependencies": {
"is-glob": "^4.0.3"
},
@ -6851,7 +6865,6 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
"integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
"dev": true,
"peer": true,
"dependencies": {
"type-fest": "^0.20.2"
},
@ -6862,19 +6875,100 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"dependencies": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.2.9",
"ignore": "^5.2.0",
"merge2": "^1.4.1",
"slash": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/json-schem-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schem-traverse/-/json-schem-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"node_modules/eslint/node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"peer": true
"dependencies": {
"p-locate": "^5.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"dependencies": {
"yocto-queue": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint/node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/eslint/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/eslint/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=10"
},
@ -6883,17 +6977,20 @@
}
},
"node_modules/espree": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz",
"integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==",
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz",
"integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==",
"dev": true,
"dependencies": {
"acorn": "^8.7.1",
"acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esprima": {
@ -7122,8 +7219,7 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/fastq": {
"version": "1.13.0",
@ -7186,7 +7282,6 @@
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
"dev": true,
"peer": true,
"dependencies": {
"flat-cache": "^3.0.4"
},
@ -7306,7 +7401,6 @@
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
"dev": true,
"peer": true,
"dependencies": {
"flatted": "^3.1.0",
"rimraf": "^3.0.2"
@ -7319,8 +7413,7 @@
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz",
"integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.1",
@ -7763,6 +7856,12 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true
},
"node_modules/grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"dev": true
},
"node_modules/gzip-size": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz",
@ -8407,7 +8506,6 @@
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"peer": true,
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@ -8424,7 +8522,6 @@
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"peer": true,
"dependencies": {
"callsites": "^3.0.0"
},
@ -8437,7 +8534,6 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"peer": true,
"engines": {
"node": ">=4"
}
@ -8447,7 +8543,6 @@
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.8.19"
}
@ -9072,7 +9167,6 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"peer": true,
"dependencies": {
"argparse": "^2.0.1"
},
@ -9200,12 +9294,16 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/json5": {
"version": "2.2.1",
@ -9435,7 +9533,6 @@
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@ -9599,8 +9696,7 @@
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/lodash.pick": {
"version": "4.4.0",
@ -10803,7 +10899,6 @@
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
"dev": true,
"peer": true,
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@ -11762,7 +11857,6 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"peer": true,
"engines": {
"node": ">= 0.8.0"
}
@ -13201,7 +13295,6 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"peer": true,
"engines": {
"node": ">=8"
},
@ -13534,8 +13627,7 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/then-request": {
"version": "6.0.2",
@ -13762,7 +13854,6 @@
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"peer": true,
"dependencies": {
"prelude-ls": "^1.2.1"
},
@ -14166,8 +14257,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true,
"peer": true
"dev": true
},
"node_modules/validate-npm-package-license": {
"version": "3.0.4",
@ -14912,7 +15002,6 @@
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -15747,7 +15836,6 @@
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
"integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
"dev": true,
"peer": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@ -15765,7 +15853,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"peer": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -15778,7 +15865,6 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
"integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
"dev": true,
"peer": true,
"requires": {
"type-fest": "^0.20.2"
}
@ -15787,36 +15873,38 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schem-traverse/-/json-schem-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"peer": true
"dev": true
},
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"peer": true
"dev": true
}
}
},
"@humanwhocodes/config-array": {
"version": "0.9.5",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz",
"integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==",
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.4.tgz",
"integrity": "sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==",
"dev": true,
"peer": true,
"requires": {
"@humanwhocodes/object-schema": "^1.2.1",
"debug": "^4.1.1",
"minimatch": "^3.0.4"
}
},
"@humanwhocodes/gitignore-to-minimatch": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz",
"integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==",
"dev": true
},
"@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schem-1.2.1.tgz",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true,
"peer": true
"dev": true
},
"@iconify-json/bi": {
"version": "1.1.6",
@ -17937,8 +18025,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"peer": true
"dev": true
},
"array-includes": {
"version": "3.1.5",
@ -18218,8 +18305,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"peer": true
"dev": true
},
"camelcase": {
"version": "6.3.0",
@ -19083,8 +19169,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"peer": true
"dev": true
},
"deepmerge": {
"version": "4.2.2",
@ -19179,7 +19264,6 @@
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"peer": true,
"requires": {
"esutils": "^2.0.2"
}
@ -19710,14 +19794,14 @@
}
},
"eslint": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz",
"integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==",
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.22.0.tgz",
"integrity": "sha512-ci4t0sz6vSRKdmkOGmprBo6fmI4PrphDFMy5JEq/fNS0gQkJM3rLmrqcp8ipMcdobH3KtUP40KniAE9W19S4wA==",
"dev": true,
"peer": true,
"requires": {
"@eslint/eslintrc": "^1.3.0",
"@humanwhocodes/config-array": "^0.9.2",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@ -19727,14 +19811,17 @@
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.3.2",
"espree": "^9.3.3",
"esquery": "^1.4.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^6.0.1",
"find-up": "^5.0.0",
"functional-red-black-tree": "^1.0.1",
"glob-parent": "^6.0.1",
"globals": "^13.15.0",
"globby": "^11.1.0",
"grapheme-splitter": "^1.0.4",
"ignore": "^5.2.0",
"import-fresh": "^3.0.0",
"imurmurhash": "^0.1.4",
@ -19758,7 +19845,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"peer": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@ -19771,7 +19857,6 @@
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"peer": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@ -19781,15 +19866,13 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"peer": true
"dev": true
},
"eslint-scope": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
"integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
"dev": true,
"peer": true,
"requires": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
@ -19799,15 +19882,23 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
},
"find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"peer": true
"requires": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
}
},
"glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"peer": true,
"requires": {
"is-glob": "^4.0.3"
}
@ -19817,24 +19908,74 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz",
"integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==",
"dev": true,
"peer": true,
"requires": {
"type-fest": "^0.20.2"
}
},
"globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true,
"requires": {
"array-union": "^2.1.0",
"dir-glob": "^3.0.1",
"fast-glob": "^3.2.9",
"ignore": "^5.2.0",
"merge2": "^1.4.1",
"slash": "^3.0.0"
}
},
"json-schem-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schem-traverse/-/json-schem-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true
},
"locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"peer": true
"requires": {
"p-locate": "^5.0.0"
}
},
"p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"requires": {
"yocto-queue": "^0.1.0"
}
},
"p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"requires": {
"p-limit": "^3.0.2"
}
},
"path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"dev": true,
"peer": true
"dev": true
}
}
},
@ -20188,12 +20329,12 @@
"dev": true
},
"espree": {
"version": "9.3.2",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz",
"integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==",
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz",
"integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==",
"dev": true,
"requires": {
"acorn": "^8.7.1",
"acorn": "^8.8.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^3.3.0"
}
@ -20379,8 +20520,7 @@
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"peer": true
"dev": true
},
"fastq": {
"version": "1.13.0",
@ -20423,7 +20563,6 @@
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
"integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
"dev": true,
"peer": true,
"requires": {
"flat-cache": "^3.0.4"
}
@ -20521,7 +20660,6 @@
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz",
"integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==",
"dev": true,
"peer": true,
"requires": {
"flatted": "^3.1.0",
"rimraf": "^3.0.2"
@ -20531,8 +20669,7 @@
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz",
"integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==",
"dev": true,
"peer": true
"dev": true
},
"follow-redirects": {
"version": "1.15.1",
@ -20863,6 +21000,12 @@
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
"dev": true
},
"grapheme-splitter": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz",
"integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==",
"dev": true
},
"gzip-size": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz",
@ -21343,7 +21486,6 @@
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
"integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
"dev": true,
"peer": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@ -21354,7 +21496,6 @@
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"peer": true,
"requires": {
"callsites": "^3.0.0"
}
@ -21363,8 +21504,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"peer": true
"dev": true
}
}
},
@ -21372,8 +21512,7 @@
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"peer": true
"dev": true
},
"indent-string": {
"version": "4.0.0",
@ -21823,7 +21962,6 @@
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"peer": true,
"requires": {
"argparse": "^2.0.1"
}
@ -21921,12 +22059,16 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
},
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"peer": true
"dev": true
},
"json5": {
"version": "2.2.1",
@ -22111,7 +22253,6 @@
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"peer": true,
"requires": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
@ -22251,8 +22392,7 @@
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"peer": true
"dev": true
},
"lodash.pick": {
"version": "4.4.0",
@ -23210,7 +23350,6 @@
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz",
"integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==",
"dev": true,
"peer": true,
"requires": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
@ -23878,8 +24017,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"peer": true
"dev": true
},
"prettier": {
"version": "2.7.1",
@ -24972,8 +25110,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"peer": true
"dev": true
},
"strip-literal": {
"version": "0.4.0",
@ -25216,8 +25353,7 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
"dev": true,
"peer": true
"dev": true
},
"then-request": {
"version": "6.0.2",
@ -25407,7 +25543,6 @@
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"peer": true,
"requires": {
"prelude-ls": "^1.2.1"
}
@ -25673,8 +25808,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true,
"peer": true
"dev": true
},
"validate-npm-package-license": {
"version": "3.0.4",
@ -26223,8 +26357,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"dev": true,
"peer": true
"dev": true
},
"wrap-ansi": {
"version": "7.0.0",

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

@ -22,6 +22,7 @@
"dayjs": "^1.11.3",
"file-saver": "^2.0.5",
"jsep": "^1.3.6",
"json-schema-traverse": "^1.0.0",
"just-clone": "^6.1.1",
"jwt-decode": "^3.1.2",
"locale-codes": "^1.3.1",
@ -64,6 +65,7 @@
"@windicss/plugin-animations": "^1.0.9",
"@windicss/plugin-question-mark": "^0.1.1",
"@windicss/plugin-scrollbar": "^1.2.3",
"eslint": "^8.22.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"happy-dom": "^6.0.3",

9
packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue

@ -144,7 +144,7 @@ const onMenuClose = (visible: boolean) => {
</script>
<template>
<NuxtLayout id="content" class="flex">
<NuxtLayout id="content">
<template #sidebar>
<a-layout-sider
ref="sidebar"
@ -212,7 +212,7 @@ const onMenuClose = (visible: boolean) => {
</div>
<template #overlay>
<a-menu class="ml-6 !w-[300px] !text-sm !p-0 !rounded">
<a-menu class="!ml-1 !w-[300px] !text-sm">
<a-menu-item-group>
<template #title>
<div class="group select-none flex items-center gap-4 py-1">
@ -362,6 +362,8 @@ const onMenuClose = (visible: boolean) => {
</div>
</template>
<template #expandIcon></template>
<a-menu-item>
<div class="nc-project-menu-item group" @click.stop="openColorPicker('primary')">
<ClarityColorPickerSolid class="group-hover:text-accent" />
@ -403,9 +405,12 @@ const onMenuClose = (visible: boolean) => {
<DashboardTreeView v-show="isOpen" />
</a-layout-sider>
</template>
<div :key="$route.fullPath">
<dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" />
<NuxtPage />
<GeneralPreviewAs float />
</div>
</NuxtLayout>

244
packages/nc-gui-v2/pages/index/index.vue

@ -1,239 +1,37 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import {
computed,
definePageMeta,
extractSdkResponseErrorMsg,
navigateTo,
onMounted,
ref,
useApi,
useNuxtApp,
useSidebar,
useUIPermission,
} from '#imports'
import { useRoute } from '#imports'
definePageMeta({
title: 'title.myProject',
})
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
const { isUIAllowed } = useUIPermission()
useSidebar({ hasSidebar: true, isOpen: true })
const filterQuery = ref('')
const projects = ref<ProjectType[]>()
const loadProjects = async () => {
const response = await api.project.list({})
projects.value = response.list
}
const filteredProjects = computed(
() =>
projects.value?.filter(
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
) ?? [],
)
const deleteProject = (project: ProjectType) => {
$e('c:project:delete')
Modal.confirm({
title: `Do you want to delete '${project.title}' project?`,
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
try {
await api.project.delete(project.id as string)
$e('a:project:delete')
return projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e: any) {
return message.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
onMounted(() => {
loadProjects()
})
const route = useRoute()
</script>
<template>
<NuxtLayout>
<div class="flex flex-col md:flex-row flex-wrap gap-6 py-6 px-12">
<div class="hidden xl:(block)">
<GeneralSponsors />
<div
class="min-h-[calc(100vh_-_var(--header-height))] h-auto bg-primary bg-opacity-5 flex flex-col lg:flex-row flex-wrap gap-6 py-6 px-12 pt-65px"
>
<div class="flex-1 justify-end hidden xl:(flex)">
<div>
<GeneralSponsors />
</div>
</div>
<div class="min-w-2/4 flex-auto">
<a-card class="transition-all duration-300 ease-out !rounded-lg shadow">
<h1 class="flex items-center justify-center gap-2 leading-8 mb-8">
<!-- My Projects -->
<span class="text-4xl">{{ $t('title.myProject') }}</span>
<a-tooltip title="Reload projects">
<span
class="transition-all duration-200 h-full flex items-center group hover:ring active:(ring ring-accent) rounded-full mt-1"
:class="isLoading ? 'animate-spin ring ring-gray-200' : ''"
>
<MdiRefresh
v-t="['a:project:refresh']"
class="text-xl text-gray-500 group-hover:text-accent cursor-pointer"
:class="isLoading ? '!text-primary' : ''"
@click="loadProjects"
/>
</span>
</a-tooltip>
</h1>
<div class="order-1 flex mb-6">
<a-input-search
v-model:value="filterQuery"
class="max-w-[250px] nc-project-page-search rounded"
:placeholder="$t('activity.searchProject')"
/>
<div class="flex-1" />
<a-dropdown v-if="isUIAllowed('projectCreate', true)" :trigger="['click']">
<button class="nc-new-project-menu">
<div class="flex items-center w-full">
{{ $t('title.newProj') }}
<MdiMenuDown class="menu-icon" />
</div>
</button>
<template #overlay>
<a-menu class="!py-0 rounded">
<a-menu-item>
<div
v-t="['c:project:create:xcdb']"
class="nc-project-menu-item group"
@click="navigateTo('/project/create')"
>
<MdiPlusOutline class="group-hover:text-accent" />
<div>{{ $t('activity.createProject') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div
v-t="['c:project:create:extdb']"
class="nc-project-menu-item group"
@click="navigateTo('/project/create-external')"
>
<MdiDatabaseOutline class="group-hover:text-accent" />
<div v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div class="min-w-2/4 xl:max-w-2/4 w-full mx-auto">
<NuxtPage />
</div>
<TransitionGroup name="layout" mode="out-in">
<div v-if="isLoading">
<a-skeleton />
<div class="flex flex-1 justify-between gap-6 lg:block">
<template v-if="route.name === 'index-index'">
<TransitionGroup name="page" mode="out-in">
<div>
<GeneralSocialCard />
</div>
<a-table
v-else
:custom-row="
(record) => ({
onClick: () => {
$e('a:project:open')
navigateTo(`/nc/${record.id}`)
},
class: ['group'],
})
"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<div
class="capitalize color-transition group-hover:text-primary !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap"
>
{{ text }}
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex items-center gap-2">
<MdiEditOutline
v-t="['c:project:edit:rename']"
class="nc-action-btn"
@click.stop="navigateTo(`/project/${text}`)"
/>
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)" />
</div>
</template>
</a-table-column>
</a-table>
<div class="block mt-0 lg:(!mt-6) xl:hidden">
<GeneralSponsors />
</div>
</TransitionGroup>
</a-card>
</div>
<div class="flex gap-6 md:block">
<GeneralSocialCard />
<div class="block mt-0 md:(!mt-6) xl:hidden">
<GeneralSponsors />
</div>
</template>
</div>
</div>
</NuxtLayout>
</template>
<style scoped>
.nc-action-btn {
@apply text-gray-500 group-hover:text-accent active:(ring ring-accent) cursor-pointer p-2 w-[30px] h-[30px] hover:bg-gray-300/50 rounded-full;
}
.nc-new-project-menu {
@apply cursor-pointer z-1 relative color-transition rounded-md px-3 py-2 text-white;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-accent;
}
&:active::after {
@apply ring ring-accent;
}
}
:deep(.ant-table-cell) {
@apply py-1;
}
:deep(.ant-table-row) {
@apply cursor-pointer;
}
:deep(.ant-table) {
@apply min-h-[428px];
}
</style>

140
packages/nc-gui-v2/pages/index/index/[id].vue

@ -0,0 +1,140 @@
<script lang="ts" setup>
import type { Form } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import {
extractSdkResponseErrorMsg,
navigateTo,
nextTick,
onMounted,
projectTitleValidator,
reactive,
ref,
useApi,
useRoute,
useSidebar,
} from '#imports'
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: false })
const route = useRoute()
const nameValidationRules = [
{
required: true,
message: 'Project name is required',
},
projectTitleValidator,
]
const form = ref<typeof Form>()
const formState = reactive({
title: '',
})
const getProject = async () => {
try {
const result: ProjectType = await api.project.read(route.params.id as string)
formState.title = result.title as string
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const renameProject = async () => {
try {
await api.project.update(route.params.id as string, formState)
navigateTo(`/nc/${route.params.id}`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
// select and focus title field on load
onMounted(async () => {
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title.length)
input.focus()
}, 500)
})
})
await getProject()
</script>
<template>
<div
class="update-project bg-white relative flex-auto flex flex-col justify-center gap-2 p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<general-noco-icon class="color-transition hover:(ring ring-accent)" :class="[isLoading ? 'animated-bg-gradient' : '']" />
<div
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full bg-white cursor-pointer"
@click="navigateTo('/')"
>
<MdiChevronLeft class="text-black group-hover:(text-accent scale-110)" />
</div>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('activity.editProject') }}</h1>
<a-form
ref="form"
:model="formState"
name="basic"
layout="vertical"
class="lg:max-w-3/4 w-full !mx-auto"
no-style
autocomplete="off"
@finish="renameProject"
>
<a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules">
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item>
<div class="text-center">
<button type="submit" class="submit">
<span class="flex items-center gap-2">
<MaterialSymbolsRocketLaunchOutline />
{{ $t('general.edit') }}
</span>
</button>
</div>
</a-form>
</div>
</template>
<style lang="scss">
.update-project {
.ant-input-affix-wrapper,
.ant-input {
@apply !appearance-none my-1 border-1 border-solid rounded;
}
.submit {
@apply z-1 relative color-transition rounded p-3 text-white shadow-sm;
&::after {
@apply rounded absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-accent;
}
&:active::after {
@apply ring ring-accent;
}
}
}
</style>

144
packages/nc-gui-v2/pages/project/index/create-external.vue → packages/nc-gui-v2/pages/index/index/create-external.vue

@ -1,28 +1,36 @@
<script lang="ts" setup>
import { onMounted } from '@vue/runtime-core'
import { Form, Modal, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { computed, ref, useSidebar, watch } from '#imports'
import { navigateTo, useNuxtApp } from '#app'
import { ClientType } from '~/lib'
import type { ProjectCreateForm } from '~/utils'
import {
clientTypes,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
generateUniqueName,
getDefaultConnectionConfig,
getTestDatabaseName,
navigateTo,
nextTick,
onMounted,
projectTitleValidator,
readFile,
ref,
sslUsage,
} from '~/utils'
useApi,
useI18n,
useNuxtApp,
useSidebar,
watch,
} from '#imports'
import { ClientType } from '~/lib'
import type { ProjectCreateForm } from '~/utils'
const useForm = Form.useForm
const loading = ref(false)
const testSuccess = ref(false)
const { $api, $e } = useNuxtApp()
const { api, isLoading } = useApi()
const { $e } = useNuxtApp()
useSidebar({ hasSidebar: false })
@ -123,11 +131,13 @@ const createProject = async () => {
focusInvalidInput()
return
}
loading.value = true
try {
const connection = getConnectionConfig()
const config = { ...formState.dataSource, connection }
const result = await $api.project.create({
const result = await api.project.create({
title: formState.title,
bases: [
{
@ -139,12 +149,13 @@ const createProject = async () => {
],
external: true,
})
$e('a:project:create:extdb')
await navigateTo(`/nc/${result.id}`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
loading.value = false
}
const testConnection = async () => {
@ -154,19 +165,23 @@ const testConnection = async () => {
focusInvalidInput()
return
}
$e('a:project:create:extdb:test-connection', [])
try {
if (formState.dataSource.client === ClientType.SQLITE) {
testSuccess.value = true
} else {
const connection: any = getConnectionConfig()
connection.database = getTestDatabaseName(formState.dataSource)
const connection = getConnectionConfig()
connection.database = getTestDatabaseName(formState.dataSource)!
const testConnectionConfig = {
...formState.dataSource,
connection,
}
const result = await $api.utils.testConnection(testConnectionConfig)
const result = await api.utils.testConnection(testConnectionConfig)
if (result.code === 0) {
testSuccess.value = true
@ -183,11 +198,13 @@ const testConnection = async () => {
})
} else {
testSuccess.value = false
message.error(`${t('msg.error.dbConnectionFailed')} ${result.message}`)
}
}
} catch (e: any) {
testSuccess.value = false
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -213,26 +230,34 @@ onMounted(() => {
</script>
<template>
<a-card class="max-w-[600px] !mx-auto !mt-100px !mb-5 !shadow-md">
<GeneralNocoIcon />
<div
class="create-external bg-white relative flex flex-col justify-center gap-2 w-full p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<general-noco-icon class="color-transition hover:(ring ring-accent)" :class="[isLoading ? 'animated-bg-gradient' : '']" />
<div
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full bg-white cursor-pointer"
@click="navigateTo('/')"
>
<MdiChevronLeft class="text-black group-hover:(text-accent scale-110)" />
</div>
<h3 class="text-3xl text-center font-semibold mt-8 mb-4">{{ $t('activity.createProject') }}</h3>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('activity.createProject') }}</h1>
<a-form
ref="form"
:model="formState"
name="external-project-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
:wrapper-col="{ span: 18 }"
class="!pr-5"
>
<a-form-item :label="$t('placeholder.projName')" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" size="small" class="nc-extdb-proj-name" />
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item>
<a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']">
<a-select v-model:value="formState.dataSource.client" size="small" class="nc-extdb-db-type" @change="onClientChange">
<a-select v-model:value="formState.dataSource.client" class="nc-extdb-db-type" @change="onClientChange">
<a-select-option v-for="client in clientTypes" :key="client.value" :value="client.value"
>{{ client.text }}
</a-select-option>
@ -245,32 +270,28 @@ onMounted(() => {
:label="$t('labels.sqliteFile')"
v-bind="validateInfos['dataSource.connection.connection.filename']"
>
<a-input v-model:value="formState.dataSource.connection.connection.filename" size="small" />
<a-input v-model:value="formState.dataSource.connection.connection.filename" />
</a-form-item>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<a-input v-model:value="formState.dataSource.connection.host" size="small" class="nc-extdb-host-address" />
<a-input v-model:value="formState.dataSource.connection.host" class="nc-extdb-host-address" />
</a-form-item>
<!-- Port Number -->
<a-form-item :label="$t('labels.port')" v-bind="validateInfos['dataSource.connection.port']">
<a-input-number v-model:value="formState.dataSource.connection.port" class="!w-full nc-extdb-host-port" size="small" />
<a-input-number v-model:value="formState.dataSource.connection.port" class="!w-full nc-extdb-host-port" />
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<a-input v-model:value="formState.dataSource.connection.user" size="small" class="nc-extdb-host-user" />
<a-input v-model:value="formState.dataSource.connection.user" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')">
<a-input-password
v-model:value="formState.dataSource.connection.password"
size="small"
class="nc-extdb-host-password"
/>
<a-input-password v-model:value="formState.dataSource.connection.password" class="nc-extdb-host-password" />
</a-form-item>
<!-- Database -->
@ -279,24 +300,24 @@ onMounted(() => {
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
size="small"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item
v-if="[ClientType.MSSQL, ClientType.PG].includes(formState.dataSource.client) && formState.dataSource.searchPath"
:label="$t('labels.schemaName')"
v-bind="validateInfos['dataSource.searchPath.0']"
>
<a-input v-model:value="formState.dataSource.searchPath[0]" size="small" />
<a-input v-model:value="formState.dataSource.searchPath[0]" />
</a-form-item>
<a-collapse ghost expand-icon-position="right" class="mt-6">
<a-collapse ghost expand-icon-position="right" class="!mt-6">
<a-collapse-panel key="1" :header="$t('title.advancedParameters')">
<!-- todo: add in i18n -->
<a-form-item label="SSL mode">
<a-select v-model:value="formState.sslUse" size="small" @change="onClientChange">
<a-select v-model:value="formState.sslUse" @change="onClientChange">
<a-select-option v-for="opt in sslUsage" :key="opt" :value="opt">{{ opt }}</a-select-option>
</a-select>
</a-form-item>
@ -308,25 +329,29 @@ onMounted(() => {
<template #title>
<span>{{ $t('tooltip.clientCert') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="certFileInput.click()">
<a-button :disabled="!sslFilesRequired" class="shadow" @click="certFileInput.click()">
{{ $t('labels.clientCert') }}
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select .key file -->
<template #title>
<span>{{ $t('tooltip.clientKey') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="keyFileInput.click()">
<a-button :disabled="!sslFilesRequired" class="shadow" @click="keyFileInput.click()">
{{ $t('labels.clientKey') }}
</a-button>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select CA file -->
<template #title>
<span>{{ $t('tooltip.clientCA') }}</span>
</template>
<a-button :disabled="!sslFilesRequired" size="small" class="shadow" @click="caFileInput.click()">
<a-button :disabled="!sslFilesRequired" class="shadow" @click="caFileInput.click()">
{{ $t('labels.serverCA') }}
</a-button>
</a-tooltip>
@ -334,21 +359,25 @@ onMounted(() => {
</a-form-item>
<input ref="caFileInput" type="file" class="!hidden" @change="onFileSelect('ca', caFileInput)" />
<input ref="certFileInput" type="file" class="!hidden" @change="onFileSelect('cert', certFileInput)" />
<input ref="keyFileInput" type="file" class="!hidden" @change="onFileSelect('key', keyFileInput)" />
<a-form-item :label="$t('labels.inflection.tableName')">
<a-select v-model:value="formState.inflection.inflectionTable" size="small" @change="onClientChange">
<a-select v-model:value="formState.inflection.inflectionTable" @change="onClientChange">
<a-select-option v-for="type in inflectionTypes" :key="type" :value="type">{{ type }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('labels.inflection.columnName')">
<a-select v-model:value="formState.inflection.inflectionColumn" size="small" @change="onClientChange">
<a-select v-model:value="formState.inflection.inflectionColumn" @change="onClientChange">
<a-select-option v-for="type in inflectionTypes" :key="type" :value="type">{{ type }}</a-select-option>
</a-select>
</a-form-item>
<div class="flex justify-end">
<a-button size="small" class="!shadow-md" @click="configEditDlg = true">
<a-button class="!shadow-md" @click="configEditDlg = true">
<!-- Edit connection JSON -->
{{ $t('activity.editConnJson') }}
</a-button>
@ -357,11 +386,12 @@ onMounted(() => {
</a-collapse>
</template>
<a-form-item class="flex justify-center mt-5">
<a-form-item class="flex justify-center !mt-5">
<div class="flex justify-center gap-2">
<a-button type="primary" ghost class="nc-extdb-btn-test-connection" @click="testConnection">
{{ $t('activity.testDbConn') }}
</a-button>
<a-button type="primary" :disabled="!testSuccess" class="nc-extdb-btn-submit !shadow" @click="createProject">
Submit
</a-button>
@ -369,15 +399,17 @@ onMounted(() => {
</a-form-item>
</a-form>
<v-dialog v-model="configEditDlg">
<a-card>
<MonacoEditor v-if="configEditDlg" v-model="formState" class="h-[400px] w-[600px]" />
</a-card>
</v-dialog>
</a-card>
<!-- todo: needs replacement
<v-dialog v-model="configEditDlg">
<a-card>
<MonacoEditor v-if="configEditDlg" v-model="formState" class="h-[400px] w-[600px]" />
</a-card>
</v-dialog>
-->
</div>
</template>
<style scoped>
<style lang="scss" scoped>
:deep(.ant-collapse-header) {
@apply !pr-10 !-mt-4 text-right justify-end;
}
@ -398,7 +430,17 @@ onMounted(() => {
@apply !min-h-0;
}
:deep(.ant-card-head-title) {
@apply !text-3xl;
.create-external {
:deep(.ant-input-affix-wrapper),
:deep(.ant-input),
:deep(.ant-select) {
@apply !appearance-none border-1 border-solid rounded;
}
:deep(.ant-input-password) {
input {
@apply !border-none my-0;
}
}
}
</style>

131
packages/nc-gui-v2/pages/index/index/create.vue

@ -0,0 +1,131 @@
<script lang="ts" setup>
import type { Form } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import {
extractSdkResponseErrorMsg,
navigateTo,
nextTick,
onMounted,
projectTitleValidator,
reactive,
ref,
useApi,
useNuxtApp,
useSidebar,
} from '#imports'
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: false })
const nameValidationRules = [
{
required: true,
message: 'Project name is required',
},
projectTitleValidator,
]
const form = ref<typeof Form>()
const formState = reactive({
title: '',
})
const createProject = async () => {
$e('a:project:create:xcdb')
try {
const result = await api.project.create({
title: formState.title,
})
await navigateTo(`/nc/${result.id}`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
// select and focus title field on load
onMounted(async () => {
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title.length)
input.focus()
}, 500)
})
})
</script>
<template>
<div
class="create bg-white relative flex flex-col justify-center gap-2 w-full p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>
<general-noco-icon class="color-transition hover:(ring ring-accent)" :class="[isLoading ? 'animated-bg-gradient' : '']" />
<div
class="color-transition transform group absolute top-5 left-5 text-4xl rounded-full bg-white cursor-pointer"
@click="navigateTo('/')"
>
<MdiChevronLeft class="text-black group-hover:(text-accent scale-110)" />
</div>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('activity.createProject') }}</h1>
<a-form
ref="form"
:model="formState"
name="basic"
layout="vertical"
class="lg:max-w-3/4 w-full !mx-auto"
no-style
autocomplete="off"
@finish="createProject"
>
<a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules" class="m-10">
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item>
<div class="text-center">
<button class="submit" type="submit">
<span class="flex items-center gap-2">
<MaterialSymbolsRocketLaunchOutline />
{{ $t('general.create') }}
</span>
</button>
</div>
</a-form>
</div>
</template>
<style lang="scss">
.create {
.ant-input-affix-wrapper,
.ant-input {
@apply !appearance-none my-1 border-1 border-solid rounded;
}
.submit {
@apply z-1 relative color-transition rounded p-3 text-white shadow-sm;
&::after {
@apply rounded absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-accent;
}
&:active::after {
@apply ring ring-accent;
}
}
}
</style>

211
packages/nc-gui-v2/pages/index/index/index.vue

@ -0,0 +1,211 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import {
computed,
definePageMeta,
extractSdkResponseErrorMsg,
navigateTo,
ref,
useApi,
useNuxtApp,
useSidebar,
useUIPermission,
} from '#imports'
definePageMeta({
title: 'title.myProject',
})
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
const { isUIAllowed } = useUIPermission()
useSidebar({ hasSidebar: true, isOpen: true })
const filterQuery = ref('')
const projects = ref<ProjectType[]>()
const loadProjects = async () => {
const response = await api.project.list({})
projects.value = response.list
}
const filteredProjects = computed(
() =>
projects.value?.filter(
(project) => !filterQuery.value || project.title?.toLowerCase?.().includes(filterQuery.value.toLowerCase()),
) ?? [],
)
const deleteProject = (project: ProjectType) => {
$e('c:project:delete')
Modal.confirm({
title: `Do you want to delete '${project.title}' project?`,
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
async onOk() {
try {
await api.project.delete(project.id as string)
$e('a:project:delete')
projects.value?.splice(projects.value?.indexOf(project), 1)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
},
})
}
await loadProjects()
</script>
<template>
<div class="bg-white relative flex flex-col justify-center gap-2 w-full p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)">
<general-noco-icon class="color-transition hover:(ring ring-accent)" :class="[isLoading ? 'animated-bg-gradient' : '']" />
<h1 class="flex items-center justify-center gap-2 leading-8 mb-8 mt-4">
<!-- My Projects -->
<span class="text-4xl">{{ $t('title.myProject') }}</span>
<a-tooltip title="Reload projects">
<span
class="transition-all duration-200 h-full flex items-center group hover:ring active:(ring ring-accent) rounded-full mt-1"
:class="isLoading ? 'animate-spin ring ring-gray-200' : ''"
>
<MdiRefresh
v-t="['a:project:refresh']"
class="text-xl text-gray-500 group-hover:text-accent cursor-pointer"
:class="isLoading ? '!text-primary' : ''"
@click="loadProjects"
/>
</span>
</a-tooltip>
</h1>
<div class="flex mb-6">
<a-input-search
v-model:value="filterQuery"
class="max-w-[250px] nc-project-page-search rounded"
:placeholder="$t('activity.searchProject')"
/>
<div class="flex-1" />
<a-dropdown v-if="isUIAllowed('projectCreate', true)" :trigger="['click']">
<button class="nc-new-project-menu">
<span class="flex items-center w-full">
{{ $t('title.newProj') }}
<MdiMenuDown class="menu-icon" />
</span>
</button>
<template #overlay>
<a-menu class="!py-0 rounded">
<a-menu-item>
<div v-t="['c:project:create:xcdb']" class="nc-project-menu-item group" @click="navigateTo('/create')">
<MdiPlusOutline class="group-hover:text-accent" />
<div>{{ $t('activity.createProject') }}</div>
</div>
</a-menu-item>
<a-menu-item>
<div v-t="['c:project:create:extdb']" class="nc-project-menu-item group" @click="navigateTo('/create-external')">
<MdiDatabaseOutline class="group-hover:text-accent" />
<div v-html="$t('activity.createProjectExtended.extDB')" />
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<TransitionGroup name="layout" mode="out-in">
<div v-if="isLoading">
<a-skeleton />
</div>
<a-table
v-else
:custom-row="
(record) => ({
onClick: () => {
$e('a:project:open')
navigateTo(`/nc/${record.id}`)
},
class: ['group'],
})
"
:data-source="filteredProjects"
:pagination="{ position: ['bottomCenter'] }"
>
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text }">
<div
class="capitalize color-transition group-hover:text-primary !w-[400px] overflow-hidden overflow-ellipsis whitespace-nowrap"
>
{{ text }}
</div>
</template>
</a-table-column>
<!-- Actions -->
<a-table-column key="id" :title="$t('labels.actions')" data-index="id">
<template #default="{ text, record }">
<div class="flex items-center gap-2">
<MdiEditOutline v-t="['c:project:edit:rename']" class="nc-action-btn" @click.stop="navigateTo(`/${text}`)" />
<MdiDeleteOutline class="nc-action-btn" @click.stop="deleteProject(record)" />
</div>
</template>
</a-table-column>
</a-table>
</TransitionGroup>
</div>
</template>
<style scoped>
.nc-action-btn {
@apply text-gray-500 group-hover:text-accent active:(ring ring-accent) cursor-pointer p-2 w-[30px] h-[30px] hover:bg-gray-300/50 rounded-full;
}
.nc-new-project-menu {
@apply cursor-pointer z-1 relative color-transition rounded-md px-3 py-2 text-white;
&::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary bg-opacity-100;
content: '';
z-index: -1;
}
&:hover::after {
@apply transform scale-110 ring ring-accent;
}
&:active::after {
@apply ring ring-accent;
}
}
:deep(.ant-table-cell) {
@apply py-1;
}
:deep(.ant-table-row) {
@apply cursor-pointer;
}
:deep(.ant-table) {
@apply min-h-[428px];
}
</style>

17
packages/nc-gui-v2/pages/index/user/index/index.vue

@ -1,11 +1,6 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import { message } from 'ant-design-vue'
import { navigateTo } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils'
import { reactive, ref, useApi, useGlobal } from '#imports'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import MdiKeyChange from '~icons/mdi/key-change'
import { extractSdkResponseErrorMsg, navigateTo, reactive, ref, useApi, useGlobal, useI18n } from '#imports'
const { api } = useApi()
@ -81,7 +76,10 @@ const resetError = () => {
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div class="flex items-center gap-2 justify-center"><MaterialSymbolsWarning /> {{ error }}</div>
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
{{ error }}
</div>
</div>
</Transition>
@ -117,7 +115,10 @@ const resetError = () => {
<div class="flex flex-wrap gap-4 items-center mt-4 md:justify-between w-full">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiKeyChange /> {{ $t('activity.changePwd') }}</span>
<span class="flex items-center gap-2">
<MdiKeyChange />
{{ $t('activity.changePwd') }}
</span>
</button>
</div>
</div>

5
packages/nc-gui-v2/pages/project/index.vue

@ -1,5 +0,0 @@
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

83
packages/nc-gui-v2/pages/project/index/[id].vue

@ -1,83 +0,0 @@
<script lang="ts" setup>
import { onMounted } from '@vue/runtime-core'
import type { Form } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { ref } from 'vue'
import { message } from 'ant-design-vue'
import { navigateTo, useRoute } from '#app'
import { extractSdkResponseErrorMsg, projectTitleValidator } from '~/utils'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
import { nextTick, reactive, useSidebar } from '#imports'
const { api } = useApi()
useSidebar({ hasSidebar: false })
const route = useRoute()
const nameValidationRules = [
{
required: true,
message: 'Project name is required',
},
projectTitleValidator,
]
const formState = reactive({
title: '',
})
const getProject = async () => {
try {
const result: ProjectType = await api.project.read(route.params.id as string)
formState.title = result.title as string
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const renameProject = async () => {
try {
await api.project.update(route.params.id as string, formState)
navigateTo(`/nc/${route.params.id}`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const form = ref<typeof Form>()
// select and focus title field on load
onMounted(async () => {
await getProject()
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title.length)
input.focus()
}, 500)
})
})
</script>
<template>
<a-card class="w-[500px] !mx-auto !mt-100px shadow-md">
<h3 class="text-3xl text-center font-semibold mb-2">{{ $t('activity.editProject') }}</h3>
<a-form ref="form" :model="formState" name="basic" layout="vertical" autocomplete="off" @finish="renameProject">
<a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules" class="my-10 mx-10">
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item>
<a-form-item style="text-align: center" class="mt-2">
<a-button type="primary" html-type="submit" class="mx-auto flex justify-self-center">
<MaterialSymbolsRocketLaunchOutline class="mr-1" />
<span> {{ $t('general.edit') }} </span></a-button
>
</a-form-item>
</a-form>
</a-card>
</template>

78
packages/nc-gui-v2/pages/project/index/create.vue

@ -1,78 +0,0 @@
<script lang="ts" setup>
import { onMounted } from '@vue/runtime-core'
import type { Form } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { nextTick, reactive, ref, useApi, useSidebar } from '#imports'
import { navigateTo, useNuxtApp } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectTitleValidator } from '~/utils/validation'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
const { $e } = useNuxtApp()
const { api, isLoading } = useApi()
useSidebar({ hasSidebar: false })
const nameValidationRules = [
{
required: true,
message: 'Project name is required',
},
projectTitleValidator,
]
const formState = reactive({
title: '',
})
const createProject = async () => {
$e('a:project:create:xcdb')
try {
const result = await api.project.create({
title: formState.title,
})
await navigateTo(`/nc/${result.id}`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const form = ref<typeof Form>()
// select and focus title field on load
onMounted(async () => {
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title.length)
input.focus()
}, 500)
})
})
</script>
<template>
<a-card :loading="isLoading" class="w-[500px] !mx-auto !mt-100px shadow-md">
<GeneralNocoIcon />
<h3 class="text-3xl text-center font-semibold mt-8 mb-2">{{ $t('activity.createProject') }}</h3>
<a-form ref="form" :model="formState" name="basic" layout="vertical" autocomplete="off" @finish="createProject">
<a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules" class="my-10 mx-10">
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item>
<a-form-item style="text-align: center" class="mt-2">
<a-button type="primary" html-type="submit">
<div class="flex items-center">
<MaterialSymbolsRocketLaunchOutline class="mr-1" />
{{ $t('general.create') }}
</div>
</a-button>
</a-form-item>
</a-form>
</a-card>
</template>

2
packages/nc-gui-v2/pages/signin.vue

@ -80,7 +80,7 @@ function resetError() {
<template>
<NuxtLayout>
<div class="md:bg-primary bg-opacity-5 signin h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signup">
<div class="md:bg-primary bg-opacity-5 signin h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signin">
<div
class="bg-white mt-[60px] relative flex flex-col justify-center gap-2 w-full max-w-[500px] mx-auto p-8 md:(rounded-lg border-1 border-gray-200 shadow-xl)"
>

12
packages/nc-gui-v2/windi.config.ts

@ -57,6 +57,18 @@ export default defineConfig({
primary: 'rgba(var(--color-primary), var(--tw-text-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-text-opacity))',
},
borderColor: {
primary: 'rgba(var(--color-primary), var(--tw-border-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-border-opacity))',
},
backgroundColor: {
primary: 'rgba(var(--color-primary), var(--tw-bg-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-bg-opacity))',
},
ringColor: {
primary: 'rgba(var(--color-primary), var(--tw-ring-opacity))',
accent: 'rgba(var(--color-accent), var(--tw-ring-opacity))',
},
colors: {
...windiColors,
...themeColors,

3
packages/nc-gui/components/ProjectTreeView.vue

@ -1431,7 +1431,8 @@ export default {
let item = cookie;
try {
await this.$api.dbTable.update(item.id, {
title,
project_id: this.projectId,
table_name: title,
});
} catch (e) {
this.$toast.error(await this._extractSdkResponseErrorMsg(e)).goAway(3000);

5
packages/nocodb-sdk/src/lib/Api.ts

@ -121,6 +121,7 @@ export interface TableType {
columns?: ColumnType[];
columnsById?: object;
slug?: string;
project_id?: string;
}
export interface ViewType {
@ -169,7 +170,7 @@ export interface TableReqType {
deleted?: boolean;
order?: number;
mm?: boolean;
columns?: ColumnType[];
columns: ColumnType[];
}
export interface TableListType {
@ -1487,7 +1488,7 @@ export class Api<
*/
update: (
tableId: string,
data: { title?: string },
data: { table_name?: string; project_id?: string },
params: RequestParams = {}
) =>
this.request<any, any>({

81
packages/nocodb/src/lib/meta/api/tableApis.ts

@ -2,6 +2,7 @@ import { Request, Response, Router } from 'express';
import Model from '../../models/Model';
import { PagedResponseImpl } from '../helpers/PagedResponse';
import { Tele } from 'nc-help';
import DOMPurify from 'isomorphic-dompurify';
import {
AuditOperationSubTypes,
AuditOperationTypes,
@ -102,6 +103,8 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
}
}
req.body.table_name = DOMPurify.sanitize(req.body.table_name);
// validate table name
if (/^\s+|\s+$/.test(req.body.table_name)) {
NcError.badRequest(
@ -217,18 +220,86 @@ export async function tableCreate(req: Request<any, any, TableReqType>, res) {
export async function tableUpdate(req: Request<any, any>, res) {
const model = await Model.get(req.params.tableId);
const project = await Project.getWithInfo(req.body.project_id);
const base = project.bases[0];
if (!req.body.table_name) {
NcError.badRequest(
'Missing table name `table_name` property in request body'
);
}
if (project.prefix) {
if (!req.body.table_name.startsWith(project.prefix)) {
req.body.table_name = `${project.prefix}${req.body.table_name}`;
}
}
req.body.table_name = DOMPurify.sanitize(req.body.table_name);
// validate table name
if (/^\s+|\s+$/.test(req.body.table_name)) {
NcError.badRequest(
'Leading or trailing whitespace not allowed in table names'
);
}
if (
!(await Model.checkTitleAvailable({
table_name: req.body.table_name,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table name');
}
if (!req.body.title) {
req.body.title = getTableNameAlias(
req.body.table_name,
project.prefix,
base
);
}
if (
!(await Model.checkAliasAvailable({
title: req.body.title,
project_id: model.project_id,
base_id: model.base_id,
exclude_id: req.params.tableId,
project_id: project.id,
base_id: base.id,
}))
) {
NcError.badRequest('Duplicate table name');
NcError.badRequest('Duplicate table alias');
}
await Model.updateAlias(req.params.tableId, req.body.title);
const sqlMgr = await ProjectMgrv2.getSqlMgr(project);
const sqlClient = NcConnectionMgrv2.getSqlClient(base);
let tableNameLengthLimit = 255;
const sqlClientType = sqlClient.clientType;
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
tableNameLengthLimit = 64;
} else if (sqlClientType === 'pg') {
tableNameLengthLimit = 63;
} else if (sqlClientType === 'mssql') {
tableNameLengthLimit = 128;
}
if (req.body.table_name.length > tableNameLengthLimit) {
NcError.badRequest(`Table name exceeds ${tableNameLengthLimit} characters`);
}
await Model.updateAliasAndTableName(
req.params.tableId,
req.body.title,
req.body.table_name
);
await sqlMgr.sqlOpPlus(base, 'tableRename', {
...req.body,
tn: req.body.table_name,
tn_old: model.table_name,
});
Tele.emit('evt', { evt_type: 'table:updated' });

16
packages/nocodb/src/lib/models/Model.ts

@ -412,14 +412,25 @@ export default class Model implements TableType {
return insertObj;
}
static async updateAlias(tableId, title: string, ncMeta = Noco.ncMeta) {
if (!title) NcError.badRequest("Missing 'title' property in body");
static async updateAliasAndTableName(
tableId,
title: string,
table_name: string,
ncMeta = Noco.ncMeta
) {
if (!title) {
NcError.badRequest("Missing 'title' property in body");
}
if (!table_name) {
NcError.badRequest("Missing 'table_name' property in body");
}
// get existing cache
const key = `${CacheScope.MODEL}:${tableId}`;
const o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);
// update alias
if (o) {
o.title = title;
o.table_name = table_name;
// set cache
await NocoCache.set(key, o);
}
@ -430,6 +441,7 @@ export default class Model implements TableType {
MetaTable.MODELS,
{
title,
table_name,
},
tableId
);

12
packages/nocodb/src/lib/plugins/smtp/SMTP.ts

@ -14,11 +14,12 @@ export default class SMTP implements IEmailAdapter {
public async init(): Promise<any> {
const config = {
// from: this.input.from,
// options: {
host: this.input?.host,
port: parseInt(this.input?.port, 10),
secure: this.input?.secure === 'true',
secure:
typeof this.input?.secure === 'boolean'
? this.input?.secure
: this.input?.secure === 'true',
ignoreTLS:
typeof this.input?.ignoreTLS === 'boolean'
? this.input?.ignoreTLS
@ -27,8 +28,11 @@ export default class SMTP implements IEmailAdapter {
user: this.input?.username,
pass: this.input?.password,
},
// }
tls: {
rejectUnauthorized: this.input?.rejectUnauthorized,
},
};
this.transporter = nodemailer.createTransport(config);
}

13
packages/nocodb/src/lib/plugins/smtp/index.ts

@ -8,7 +8,7 @@ import SMTPPlugin from './SMTPPlugin';
const config: XcPluginConfig = {
builder: SMTPPlugin,
title: 'SMTP',
version: '0.0.1',
version: '0.0.2',
// icon: 'mdi-email-outline',
description: 'SMTP email client',
price: 'Free',
@ -42,8 +42,8 @@ const config: XcPluginConfig = {
key: 'secure',
label: 'Secure',
placeholder: 'Secure',
type: XcType.SingleLineText,
required: true,
type: XcType.Checkbox,
required: false,
},
{
key: 'ignoreTLS',
@ -52,6 +52,13 @@ const config: XcPluginConfig = {
type: XcType.Checkbox,
required: false,
},
{
key: 'rejectUnauthorized',
label: 'Reject Unauthorized',
placeholder: 'Reject Unauthorized',
type: XcType.Checkbox,
required: false,
},
{
key: 'username',
label: 'Username',

2
packages/nocodb/tests/pg-cy-quick/01-cy-quick.sql

@ -3308,7 +3308,7 @@ nc_iyhlectialukbv Vultr Object Storage Using Vultr Object Storage can give flexi
nc_lr8pcvg64g5bho OvhCloud Object Storage Upload your files to a space that you can access via HTTPS using the OpenStack Swift API, or the S3 API. f \N 0.0.1 \N install \N plugins/ovhCloud.png \N Storage Storage {"title":"Configure OvhCloud Object Storage","items":[{"key":"bucket","label":"Bucket Name","placeholder":"Bucket Name","type":"SingleLineText","required":true},{"key":"region","label":"Region","placeholder":"Region","type":"SingleLineText","required":true},{"key":"access_key","label":"Access Key","placeholder":"Access Key","type":"SingleLineText","required":true},{"key":"access_secret","label":"Access Secret","placeholder":"Access Secret","type":"Password","required":true}],"actions":[{"label":"Test","placeholder":"Test","key":"test","actionType":"TEST","type":"Button"},{"label":"Save","placeholder":"Save","key":"save","actionType":"SUBMIT","type":"Button"}],"msgOnInstall":"Successfully installed and attachment will be stored in OvhCloud Object Storage","msgOnUninstall":""} \N \N \N \N 2022-06-13 07:00:02.60416+00 2022-06-13 07:00:02.60416+00
nc_3f19m6v5iyudty Linode Object Storage S3-compatible Linode Object Storage makes it easy and more affordable to manage unstructured data such as content assets, as well as sophisticated and data-intensive storage challenges around artificial intelligence and machine learning. f \N 0.0.1 \N install \N plugins/linode.svg \N Storage Storage {"title":"Configure Linode Object Storage","items":[{"key":"bucket","label":"Bucket Name","placeholder":"Bucket Name","type":"SingleLineText","required":true},{"key":"region","label":"Region","placeholder":"Region","type":"SingleLineText","required":true},{"key":"access_key","label":"Access Key","placeholder":"Access Key","type":"SingleLineText","required":true},{"key":"access_secret","label":"Access Secret","placeholder":"Access Secret","type":"Password","required":true}],"actions":[{"label":"Test","placeholder":"Test","key":"test","actionType":"TEST","type":"Button"},{"label":"Save","placeholder":"Save","key":"save","actionType":"SUBMIT","type":"Button"}],"msgOnInstall":"Successfully installed and attachment will be stored in Linode Object Storage","msgOnUninstall":""} \N \N \N \N 2022-06-13 07:00:02.606391+00 2022-06-13 07:00:02.606391+00
nc_ikemr7ajwzfcr3 UpCloud Object Storage The perfect home for your data. Thanks to the S3-compatible programmable interface,\nyou have a host of options for existing tools and code implementations.\n f \N 0.0.1 \N install \N plugins/upcloud.png \N Storage Storage {"title":"Configure UpCloud Object Storage","items":[{"key":"bucket","label":"Bucket Name","placeholder":"Bucket Name","type":"SingleLineText","required":true},{"key":"endpoint","label":"Endpoint","placeholder":"Endpoint","type":"SingleLineText","required":true},{"key":"access_key","label":"Access Key","placeholder":"Access Key","type":"SingleLineText","required":true},{"key":"access_secret","label":"Access Secret","placeholder":"Access Secret","type":"Password","required":true}],"actions":[{"label":"Test","placeholder":"Test","key":"test","actionType":"TEST","type":"Button"},{"label":"Save","placeholder":"Save","key":"save","actionType":"SUBMIT","type":"Button"}],"msgOnInstall":"Successfully installed and attachment will be stored in UpCloud Object Storage","msgOnUninstall":""} \N \N \N \N 2022-06-13 07:00:02.6085+00 2022-06-13 07:00:02.6085+00
nc_tv5fgn17fgvcwh SMTP SMTP email client f \N 0.0.1 \N install \N \N \N Email Email {"title":"Configure Email SMTP","items":[{"key":"from","label":"From","placeholder":"eg: admin@run.com","type":"SingleLineText","required":true},{"key":"host","label":"Host","placeholder":"eg: smtp.run.com","type":"SingleLineText","required":true},{"key":"port","label":"Port","placeholder":"Port","type":"SingleLineText","required":true},{"key":"secure","label":"Secure","placeholder":"Secure","type":"SingleLineText","required":true},{"key":"ignoreTLS","label":"Ignore TLS","placeholder":"Ignore TLS","type":"Checkbox","required":false},{"key":"username","label":"Username","placeholder":"Username","type":"SingleLineText","required":false},{"key":"password","label":"Password","placeholder":"Password","type":"Password","required":false}],"actions":[{"label":"Test","key":"test","actionType":"TEST","type":"Button"},{"label":"Save","key":"save","actionType":"SUBMIT","type":"Button"}],"msgOnInstall":"Successfully installed and email notification will use SMTP configuration","msgOnUninstall":""} \N \N \N \N 2022-06-13 07:00:02.61079+00 2022-06-13 07:00:02.61079+00
nc_tv5fgn17fgvcwh SMTP SMTP email client f \N 0.0.1 \N install \N \N \N Email Email {"title":"Configure Email SMTP","items":[{"key":"from","label":"From","placeholder":"eg: admin@run.com","type":"SingleLineText","required":true},{"key":"host","label":"Host","placeholder":"eg: smtp.run.com","type":"SingleLineText","required":true},{"key":"port","label":"Port","placeholder":"Port","type":"SingleLineText","required":true},{"key":"secure","label":"Secure","placeholder":"Secure","type":"Checkbox","required":false},{"key":"ignoreTLS","label":"Ignore TLS","placeholder":"Ignore TLS","type":"Checkbox","required":false},{"key":"username","label":"Username","placeholder":"Username","type":"SingleLineText","required":false},{"key":"password","label":"Password","placeholder":"Password","type":"Password","required":false}],"actions":[{"label":"Test","key":"test","actionType":"TEST","type":"Button"},{"label":"Save","key":"save","actionType":"SUBMIT","type":"Button"}],"msgOnInstall":"Successfully installed and email notification will use SMTP configuration","msgOnUninstall":""} \N \N \N \N 2022-06-13 07:00:02.61079+00 2022-06-13 07:00:02.61079+00
nc_hcnsq71s0tr8um MailerSend MailerSend email client f \N 0.0.1 \N install \N plugins/mailersend.svg \N Email Email {"title":"Configure MailerSend","items":[{"key":"api_key","label":"API KEy","placeholder":"eg: ***************","type":"Password","required":true},{"key":"from","label":"From","placeholder":"eg: admin@run.com","type":"SingleLineText","required":true},{"key":"from_name","label":"From Name","placeholder":"eg: Adam","type":"SingleLineText","required":true}],"actions":[{"label":"Test","key":"test","actionType":"TEST","type":"Button"},{"label":"Save","key":"save","actionType":"SUBMIT","type":"Button"}],"msgOnInstall":"Successfully installed and email notification will use MailerSend configuration","msgOnUninstall":""} \N \N \N \N 2022-06-13 07:00:02.612874+00 2022-06-13 07:00:02.612874+00
nc_z2zas0qdy0cz7e Scaleway Object Storage Scaleway Object Storage is an S3-compatible object store from Scaleway Cloud Platform. f \N 0.0.1 \N install \N plugins/scaleway.png \N Storage Storage {"title":"Setup Scaleway","items":[{"key":"bucket","label":"Bucket name","placeholder":"Bucket name","type":"SingleLineText","required":true},{"key":"region","label":"Region of bucket","placeholder":"Region of bucket","type":"SingleLineText","required":true},{"key":"access_key","label":"Access Key","placeholder":"Access Key","type":"SingleLineText","required":true},{"key":"access_secret","label":"Access Secret","placeholder":"Access Secret","type":"Password","required":true}],"actions":[{"label":"Test","placeholder":"Test","key":"test","actionType":"TEST","type":"Button"},{"label":"Save","placeholder":"Save","key":"save","actionType":"SUBMIT","type":"Button"}],"msgOnInstall":"Successfully installed Scaleway Object Storage","msgOnUninstall":""} \N \N \N \N 2022-06-13 07:00:02.61482+00 2022-06-13 07:00:02.61482+00
nc_96vkc0jdyw7los SES Amazon Simple Email Service (SES) is a cost-effective, flexible, and scalable email service that enables developers to send mail from within any application. f \N 0.0.1 \N install \N plugins/aws.png \N Email Email {"title":"Configure Amazon Simple Email Service (SES)","items":[{"key":"from","label":"From","placeholder":"From","type":"SingleLineText","required":true},{"key":"region","label":"Region","placeholder":"Region","type":"SingleLineText","required":true},{"key":"access_key","label":"Access Key","placeholder":"Access Key","type":"SingleLineText","required":true},{"key":"access_secret","label":"Access Secret","placeholder":"Access Secret","type":"Password","required":true}],"actions":[{"label":"Test","placeholder":"Test","key":"test","actionType":"TEST","type":"Button"},{"label":"Save","placeholder":"Save","key":"save","actionType":"SUBMIT","type":"Button"}],"msgOnInstall":"Successfully installed and email notification will use Amazon SES","msgOnUninstall":""} \N \N \N \N 2022-06-13 07:00:02.617114+00 2022-06-13 07:00:02.617114+00

6
scripts/cypress/integration/common/1a_table_operations.js

@ -76,6 +76,9 @@ export const genTest = (apiType, dbType) => {
cy.closeTableTab("CityX");
// revert re-name operation to not impact rest of test suite
cy.renameTable("CityX", "City");
// 4. verify linked contents in other table
// 4a. Address table, has many field
cy.openTableTab("Address", 25);
@ -97,9 +100,6 @@ export const genTest = (apiType, dbType) => {
.contains("Kabul")
.should("exist");
cy.closeTableTab("Country");
// revert re-name operation to not impact rest of test suite
cy.renameTable("CityX", "City");
});
});
};

3
scripts/cypress/support/page_objects/mainPage.js

@ -222,7 +222,8 @@ export class _mainPage {
.click()
.type(host);
cy.getActiveModal().find('[placeholder="Port"]').click().type(port);
cy.getActiveModal().find('[placeholder="Secure"]').click().type(secure);
// TODO: in v2, it would be a button
// if (secure) cy.getActiveModal().find('[placeholder="Secure"]').click();
cy.getActiveModal().find("button").contains("Save").click();
cy.toastWait(
"Successfully installed and email notification will use SMTP configuration"

8
scripts/sdk/swagger.json

@ -1309,7 +1309,10 @@
"schema": {
"type": "object",
"properties": {
"title": {
"table_name": {
"type": "string"
},
"project_id": {
"type": "string"
}
}
@ -6050,6 +6053,9 @@
},
"slug": {
"type": "string"
},
"project_id": {
"type": "string"
}
},
"required": [

Loading…
Cancel
Save