Browse Source

chore(gui-v2): rebase & resolve conflicts

pull/3087/head
Wing-Kam Wong 2 years ago
parent
commit
51a48bde5e
  1. 45
      packages/nc-gui-v2/assets/style-v2.scss
  2. 12
      packages/nc-gui-v2/components.d.ts
  3. 1
      packages/nc-gui-v2/components/cell/DateTimePicker.vue
  4. 9
      packages/nc-gui-v2/components/cell/attachment/index.vue
  5. 6
      packages/nc-gui-v2/components/cell/attachment/utils.ts
  6. 40
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  7. 16
      packages/nc-gui-v2/components/dashboard/settings/AppStore.vue
  8. 10
      packages/nc-gui-v2/components/dashboard/settings/Metadata.vue
  9. 10
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue
  10. 22
      packages/nc-gui-v2/components/dashboard/settings/app-store/AppInstall.vue
  11. 27
      packages/nc-gui-v2/components/dlg/AirtableImport.vue
  12. 37
      packages/nc-gui-v2/components/dlg/QuickImport.vue
  13. 12
      packages/nc-gui-v2/components/dlg/TableRename.vue
  14. 10
      packages/nc-gui-v2/components/dlg/ViewCreate.vue
  15. 12
      packages/nc-gui-v2/components/dlg/ViewDelete.vue
  16. 16
      packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue
  17. 4
      packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue
  18. 4
      packages/nc-gui-v2/components/smartsheet-column/CurrencyOptions.vue
  19. 2
      packages/nc-gui-v2/components/smartsheet-column/DateOptions.vue
  20. 2
      packages/nc-gui-v2/components/smartsheet-column/DurationOptions.vue
  21. 26
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  22. 22
      packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  23. 16
      packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue
  24. 6
      packages/nc-gui-v2/components/smartsheet-column/PercentOptions.vue
  25. 8
      packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue
  26. 18
      packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue
  27. 2
      packages/nc-gui-v2/components/smartsheet-column/SpecificDBTypeOptions.vue
  28. 28
      packages/nc-gui-v2/components/smartsheet-header/Cell.vue
  29. 38
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  30. 27
      packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue
  31. 6
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  32. 5
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  33. 16
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  34. 18
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  35. 10
      packages/nc-gui-v2/components/smartsheet-toolbar/SharedViewList.vue
  36. 4
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  37. 53
      packages/nc-gui-v2/components/smartsheet/Form.vue
  38. 8
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  39. 10
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  40. 6
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  41. 2
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  42. 12
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  43. 7
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  44. 18
      packages/nc-gui-v2/components/smartsheet/sidebar/menu/ApiSnippet.vue
  45. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue
  46. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  47. 14
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue
  48. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/Reload.vue
  49. 5
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  50. 40
      packages/nc-gui-v2/components/tabs/Auth.vue
  51. 24
      packages/nc-gui-v2/components/tabs/auth/ApiTokenManagement.vue
  52. 148
      packages/nc-gui-v2/components/tabs/auth/UserManagement.vue
  53. 26
      packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue
  54. 56
      packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue
  55. 51
      packages/nc-gui-v2/components/template/Editor.vue
  56. 2
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  57. 8
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  58. 3
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  59. 18
      packages/nc-gui-v2/components/webhook/Editor.vue
  60. 14
      packages/nc-gui-v2/components/webhook/List.vue
  61. 10
      packages/nc-gui-v2/components/webhook/Test.vue
  62. 15
      packages/nc-gui-v2/composables/useApi/index.ts
  63. 18
      packages/nc-gui-v2/composables/useColumnCreateStore.ts
  64. 9
      packages/nc-gui-v2/composables/useExpandedFormStore.ts
  65. 9
      packages/nc-gui-v2/composables/useGlobal/actions.ts
  66. 12
      packages/nc-gui-v2/composables/useGlobal/types.ts
  67. 27
      packages/nc-gui-v2/composables/useLTARStore.ts
  68. 7
      packages/nc-gui-v2/composables/useSmartsheetRowStore.ts
  69. 18
      packages/nc-gui-v2/composables/useTable.ts
  70. 43
      packages/nc-gui-v2/composables/useViewData.ts
  71. 38
      packages/nc-gui-v2/layouts/base.vue
  72. 2
      packages/nc-gui-v2/lib/types.ts
  73. 73
      packages/nc-gui-v2/pages/forgot-password.vue
  74. 6
      packages/nc-gui-v2/pages/index/index.vue
  75. 11
      packages/nc-gui-v2/pages/index/user/index/index.vue
  76. 4
      packages/nc-gui-v2/pages/nc/[projectId]/index.vue
  77. 35
      packages/nc-gui-v2/pages/nc/[projectId]/index/index.vue
  78. 10
      packages/nc-gui-v2/pages/project/index/[id].vue
  79. 24
      packages/nc-gui-v2/pages/project/index/create-external.vue
  80. 10
      packages/nc-gui-v2/pages/project/index/create.vue
  81. 6
      packages/nc-gui-v2/pages/projects/index.vue
  82. 90
      packages/nc-gui-v2/pages/signin.vue
  83. 143
      packages/nc-gui-v2/pages/signup.vue
  84. 206
      packages/nc-gui-v2/pages/signup/[[token]].vue
  85. 5
      packages/nocodb/src/lib/meta/api/projectApis.ts

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

@ -108,3 +108,48 @@ html {
.ant-modal-wrap {
@apply !scrollbar-thin-dull;
}
.animated-bg-gradient {
background: linear-gradient(122deg, #6f3381, #81c7d4, #fedfe1, #9ee59e);
background-size: 800% 800%;
-webkit-animation: gradient 4s ease infinite;
-moz-animation: gradient 4s ease infinite;
animation: gradient 4s ease infinite;
}
@-webkit-keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}
@-moz-keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}
@keyframes gradient {
0% {
background-position: 0% 22%
}
50% {
background-position: 100% 79%
}
100% {
background-position: 0% 22%
}
}

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

@ -65,16 +65,25 @@ declare module '@vue/runtime-core' {
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['default']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['default']
MaterialSymbolsChevronLeftRounded: typeof import('~icons/material-symbols/chevron-left-rounded')['default']
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
MdiAccountIcon: typeof import('~icons/mdi/account-icon')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
@ -98,7 +107,9 @@ declare module '@vue/runtime-core' {
MdiDownload: typeof import('~icons/mdi/download')['default']
MdiDrag: typeof import('~icons/mdi/drag')['default']
MdiDragVertical: typeof import('~icons/mdi/drag-vertical')['default']
MdiDramaMasks: typeof import('~icons/mdi/drama-masks')['default']
MdiEmail: typeof import('~icons/mdi/email')['default']
MdiEmailArrowRightOutline: typeof import('~icons/mdi/email-arrow-right-outline')['default']
MdiExitToApp: typeof import('~icons/mdi/exit-to-app')['default']
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
@ -112,6 +123,7 @@ declare module '@vue/runtime-core' {
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
MdiLogin: typeof import('~icons/mdi/login')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']

1
packages/nc-gui-v2/components/cell/DateTimePicker.vue

@ -69,6 +69,7 @@ watch(
:dropdown-class-name="randomClass"
:open="readOnlyMode ? false : open"
@click="open = !open"
@ok="open = !open"
>
<template #suffixIcon></template>
</a-date-picker>

9
packages/nc-gui-v2/components/cell/attachment/index.vue

@ -83,9 +83,9 @@ onKeyDown('Escape', () => {
<template #title> Click or drop a file into cell </template>
<div class="flex items-center gap-2">
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" />
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120) text-gray-500 text-[10px]" />
<div v-if="!visibleItems.length" class="group-hover:text-primary">Add file(s)</div>
<div v-if="!visibleItems.length" class="group-hover:text-primary text-gray-500 text-xs">Add file(s)</div>
</div>
</a-tooltip>
</div>
@ -131,7 +131,10 @@ onKeyDown('Escape', () => {
<a-tooltip v-else placement="bottom">
<template #title> View attachments </template>
<MdiArrowExpand class="select-none transform group-hover:(text-pink-500 scale-120)" @click.stop="modalVisible = true" />
<MdiArrowExpand
class="select-none transform group-hover:(text-pink-500 scale-120) text-[10px] text-gray-500"
@click.stop="modalVisible = true"
/>
</a-tooltip>
</div>
</template>

6
packages/nc-gui-v2/components/cell/attachment/utils.ts

@ -1,4 +1,4 @@
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import FileSaver from 'file-saver'
import { computed, inject, ref, useApi, useFileDialog, useInjectionState, useProject, watch } from '#imports'
import { ColumnInj, EditModeInj, MetaInj, ReadonlyInj } from '~/context'
@ -88,9 +88,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
newAttachments.push(...data)
} catch (e: any) {
notification.error({
message: e.message || 'Some internal error occurred',
})
message.error(e.message || 'Some internal error occurred')
}
}

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

@ -1,15 +1,14 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue'
import { useNuxtApp, useRoute } from '#app'
import { computed, useProject, useTable, useTabs, watchEffect } from '#imports'
import { TabType } from '~/composables'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuDown from '~icons/mdi/chevron-down'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiDrag from '~icons/mdi/drag-vertical'
import MdiPlus from '~icons/mdi/plus-circle-outline'
const { addTab } = useTabs()
@ -141,13 +140,15 @@ const activeTable = computed(() => {
<template>
<div class="nc-treeview-container flex flex-col">
<div class="px-6 py-[11.75px] border-b-1">
<a-input-search
<div class="px-6 py-[8.75px] border-b-1 nc-filter-input">
<div class="flex items-center bg-gray-50 rounded relative">
<a-input
v-model:value="filterQuery"
size="small"
class="nc-filter-input"
class="nc-filter-input !bg-transparent"
:placeholder="$t('placeholder.searchProjectTree')"
/>
<MdiSearch class="nc-filter-input-icon text-gray-400 mx-3 absolute right-[-4px] top-[7px]" />
</div>
</div>
<a-dropdown :trigger="['contextmenu']">
@ -155,7 +156,6 @@ const activeTable = computed(() => {
<div
style="direction: ltr"
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@click="showTableList = !showTableList"
@contextmenu="setMenuContext('main')"
>
<span class="flex-grow text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
@ -163,7 +163,6 @@ const activeTable = computed(() => {
<template v-if="tables?.length"> ({{ tables.length }}) </template>
</span>
<MdiPlus
v-if="isUIAllowed('treeview-add-button')"
v-t="['c:table:create:navdraw']"
@ -177,11 +176,7 @@ const activeTable = computed(() => {
/>
</div>
<div style="direction: ltr" class="flex-1">
<div
v-if="tables.length"
class="transition-height duration-200 overflow-hidden"
:class="{ 'h-100': showTableList, 'h-0': !showTableList }"
>
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden">
<div :key="key" ref="menuRef" class="border-none sortable-list">
<div
v-for="table of tables"
@ -242,6 +237,14 @@ const activeTable = computed(() => {
</div>
</div>
</div>
<a-card v-else class="mt-4 mx-4 !bg-gray-50">
<div class="flex flex-col align-center">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<a-button type="primary" @click.stop="tableCreateDlg = true">{{ $t('tooltip.addTable') }}</a-button>
</div>
</a-card>
</div>
</div>
@ -279,7 +282,7 @@ const activeTable = computed(() => {
</div>
</template>
<style scoped>
<style scoped lang="scss">
.nc-treeview-container {
@apply h-[calc(100vh_-_var(--header-height))];
}
@ -306,7 +309,7 @@ const activeTable = computed(() => {
@apply !pointer-events-none;
}
&.dragging {
& .dragging {
.nc-icon {
@apply !hidden;
}
@ -335,6 +338,7 @@ const activeTable = computed(() => {
.nc-tree-item.active {
@apply !text-primary font-weight-bold after:(!opacity-20);
svg {
@apply !text-primary;
}
@ -343,4 +347,10 @@ const activeTable = computed(() => {
.nc-tree-item:hover {
@apply !text-grey after:(!opacity-5);
}
:deep(.nc-filter-input) {
.ant-input {
@apply pr-6 !border-0;
}
}
</style>

16
packages/nc-gui-v2/components/dashboard/settings/AppStore.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import AppInstall from './app-store/AppInstall.vue'
import MdiEditIcon from '~icons/ic/round-edit'
import MdiCloseCircleIcon from '~icons/mdi/close-circle-outline'
@ -23,9 +23,7 @@ const fetchPluginApps = async () => {
parsedInput: p.input && JSON.parse(p.input),
}))
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -35,15 +33,11 @@ const resetPlugin = async () => {
input: undefined,
active: false,
})
notification.success({
message: 'Plugin uninstalled successfully',
})
message.success('Plugin uninstalled successfully')
showPluginUninstallModal = false
await fetchPluginApps()
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:appstore:reset', { app: pluginApp.title })
@ -116,7 +110,7 @@ onMounted(async () => {
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<a-button v-else size="small" outlined @click="showInstallPluginModal(app)">
<a-button v-else size="small" outlined type="primary" ghost @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlusIcon />
Install

10
packages/nc-gui-v2/components/dashboard/settings/Metadata.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import MdiDatabaseSync from '~icons/mdi/database-sync'
@ -38,14 +38,10 @@ async function syncMetaDiff() {
isLoading = true
await $api.project.metaDiffSync(project.value.id)
notification.info({
message: 'Table metadata recreated successfully',
})
message.info('Table metadata recreated successfully')
await loadMetaDiff()
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading = false
}

10
packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, viewIcons } from '~/utils'
import { computed, h, useNuxtApp, useProject } from '#imports'
@ -47,13 +47,9 @@ async function saveUIAcl() {
project.value.id,
tables.filter((t) => t.edited),
)
notification.success({
message: 'Updated UI ACL for tables successfully',
})
message.success('Updated UI ACL for tables successfully')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:proj-meta:ui-acl')
}

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { PluginType } from 'nocodb-sdk'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import CloseIcon from '~icons/material-symbols/close-rounded'
@ -53,13 +53,9 @@ const saveSettings = async () => {
})
emits('saved')
notification.success({
message: plugin?.formDetails.msgOnInstall || 'Plugin settings saved successfully',
})
message.success(plugin?.formDetails.msgOnInstall || 'Plugin settings saved successfully')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
} finally {
loadingAction = null
}
@ -77,18 +73,12 @@ const testSettings = async () => {
})
if (res) {
notification.success({
message: 'Successfully tested plugin settings',
})
message.success('Successfully tested plugin settings')
} else {
notification.info({
message: 'Invalid credentials',
})
message.info('Invalid credentials')
}
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
} finally {
loadingAction = null
}

27
packages/nc-gui-v2/components/dlg/AirtableImport.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import io from 'socket.io-client'
import type { Socket } from 'socket.io-client'
import { Form, notification } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import type { Card as AntCard } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, fieldRequiredValidator } from '~/utils'
import MdiCloseCircleOutlineIcon from '~icons/mdi/close-circle-outline'
@ -103,9 +103,7 @@ async function createOrUpdate() {
syncSource.value = data
}
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -156,9 +154,7 @@ async function sync() {
},
})
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -228,7 +224,13 @@ onBeforeUnmount(() => {
</script>
<template>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" :mask-closable="false" @keydown.esc="dialogShow = false">
<a-modal
v-model:visible="dialogShow"
width="max(30vw, 600px)"
:mask-closable="false"
class="pa-2"
@keydown.esc="dialogShow = false"
>
<template #footer>
<div v-if="step === 1">
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
@ -243,15 +245,14 @@ onBeforeUnmount(() => {
</a-button>
</div>
</template>
<a-typography-title class="ml-5 mt-5" type="secondary" :level="5">QUICK IMPORT - AIRTABLE</a-typography-title>
<span class="ml-5 mt-5 prose-xl font-weight-bold" type="secondary" :level="5">QUICK IMPORT - AIRTABLE</span>
<div class="ml-5 mr-5">
<a-divider />
<div v-if="step === 1">
<div class="mb-4">
<span class="prose-xl font-bold mr-3">Credentials</span>
<span class="mr-3 pt-2 text-gray-500 text-xs">Credentials</span>
<a
href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials"
class="prose-sm underline text-grey"
class="prose-sm underline text-grey text-xs"
target="_blank"
>Where to find this?
</a>
@ -273,7 +274,7 @@ onBeforeUnmount(() => {
size="large"
/>
</a-form-item>
<span class="prose-xl font-bold self-center my-4">Advanced Settings</span>
<span class="prose-lg self-center my-4 text-gray-500">Advanced Settings</span>
<a-divider class="mt-2 mb-5" />
<div class="mt-0 my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncData">Import Data</a-checkbox>

37
packages/nc-gui-v2/components/dlg/QuickImport.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Form, notification } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk'
import type { UploadChangeParam } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
@ -132,9 +132,7 @@ async function handlePreImport() {
await validate()
await parseAndExtractData(importState.url, '')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
} else if (activeKey.value === 'jsonEditorTab') {
await parseAndExtractData(JSON.stringify(importState.jsonEditor), '')
@ -147,9 +145,7 @@ async function handleImport() {
loading.value = true
await templateEditorRef.value.importTemplate()
} catch (e: any) {
return notification.error({
message: await extractSdkResponseErrorMsg(e),
})
return message.error(await extractSdkResponseErrorMsg(e))
} finally {
loading.value = false
}
@ -163,9 +159,7 @@ async function parseAndExtractData(val: any, name: string) {
importColumns.value = []
const templateGenerator: any = getAdapter(name, val)
if (!templateGenerator) {
notification.error({
message: 'Template Generator cannot be found!',
})
message.error('Template Generator cannot be found!')
return
}
await templateGenerator.init()
@ -176,18 +170,13 @@ async function parseAndExtractData(val: any, name: string) {
if (importOnly) importColumns.value = templateGenerator.getColumns()
templateEditorModal.value = true
} catch (e: any) {
console.log(e)
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
function rejectDrop(fileList: any[]) {
fileList.map((file) => {
return notification.error({
message: `Failed to upload file ${file.name}`,
})
return message.error(`Failed to upload file ${file.name}`)
})
}
@ -204,13 +193,9 @@ function handleChange(info: UploadChangeParam) {
reader.readAsArrayBuffer(info.file.originFileObj)
}
if (status === 'done') {
notification.success({
message: `Uploaded file ${info.file.name} successfully`,
})
message.success(`Uploaded file ${info.file.name} successfully`)
} else if (status === 'error') {
notification.error({
message: `Failed to upload file ${info.file.name}`,
})
message.error(`Failed to upload file ${info.file.name}`)
}
}
@ -250,7 +235,7 @@ function getAdapter(name: string, val: any) {
<template>
<a-modal v-model:visible="dialogShow" :width="modalWidth" :mask-closable="false" @keydown.esc="dialogShow = false">
<a-typography-title class="ml-5 mt-5 mb-5" type="secondary" :level="5">{{ importMeta.header }}</a-typography-title>
<span class="prose-xl font-weight-bold ml-5 mt-5 mb-5" type="secondary" :level="5">{{ importMeta.header }}</span>
<template #footer>
<a-button v-if="templateEditorModal" key="back" @click="templateEditorModal = false">Back</a-button>
<a-button v-else key="cancel" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
@ -275,7 +260,7 @@ function getAdapter(name: string, val: any) {
$t('activity.import')
}}</a-button>
</template>
<div class="ml-5 mr-5">
<div class="ml-5 mr-5 mt-5">
<TemplateEditor
v-if="templateEditorModal"
ref="templateEditorRef"
@ -345,7 +330,7 @@ function getAdapter(name: string, val: any) {
<div v-if="!templateEditorModal" class="ml-5 mr-5">
<a-divider />
<div class="mb-4">
<span class="prose-xl font-bold">Advanced Settings</span>
<span class="prose-lg">Advanced Settings</span>
<a-form-item class="mt-4 mb-2" :label="t('msg.info.footMsg')" v-bind="validateInfos.maxRowsToParse">
<a-input-number v-model:value="importState.parserConfig.maxRowsToParse" :min="1" :max="50000" />
</a-form-item>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import { Form, notification } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk'
import { useProject, useTabs } from '#imports'
import { extractSdkResponseErrorMsg, validateTableName } from '~/utils'
@ -76,15 +76,11 @@ const renameTable = async () => {
dialogShow.value = false
loadTables()
updateTab({ id: tableMeta?.id }, { title: formState.title })
notification.success({
message: 'Table renamed successfully',
})
message.success('Table renamed successfully')
$e('a:table:rename')
dialogShow.value = false
} catch (e) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
loading = false
}

10
packages/nc-gui-v2/components/dlg/ViewCreate.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ComponentPublicInstance } from '@vue/runtime-core'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Form as AntForm } from 'ant-design-vue'
import { capitalize, inject } from '@vue/runtime-core'
import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
@ -125,16 +125,12 @@ async function onSubmit() {
}
if (data) {
notification.success({
message: 'View created successfully',
})
message.success('View created successfully')
emits('created', data)
}
} catch (e: any) {
notification.error({
message: e.message,
})
message.error(e.message)
}
vModel.value = false

12
packages/nc-gui-v2/components/dlg/ViewDelete.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg } from '~/utils'
import { onKeyStroke, useApi, useNuxtApp, useVModel } from '#imports'
@ -34,15 +34,9 @@ async function onDelete() {
try {
await api.dbView.delete(props.view.id)
notification.success({
message: 'View deleted successfully',
duration: 3,
})
message.success('View deleted successfully')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
duration: 3,
})
message.error(await extractSdkResponseErrorMsg(e))
}
emits('deleted')

16
packages/nc-gui-v2/components/smartsheet-column/AdvancedOptions.vue

@ -16,13 +16,12 @@ formState.value.au = !!formState.value.au
</script>
<template>
<div class="p-4 border-[2px] radius-1 border-grey w-full">
<div class="p-4 border-[2px] radius-1 border-grey w-full flex flex-col gap-2">
<div class="flex justify-space-between">
<a-form-item label="NN">
<a-checkbox
v-model:checked="formState.rqd"
:disabled="formState.pk || !sqlUi.columnEditable(formState)"
size="small"
class="nc-column-checkbox-NN"
@change="onAlter"
/>
@ -31,7 +30,6 @@ formState.value.au = !!formState.value.au
<a-checkbox
v-model:checked="formState.pk"
:disabled="!sqlUi.columnEditable(formState)"
size="small"
class="nc-column-checkbox-PK"
@change="onAlter"
/>
@ -40,7 +38,6 @@ formState.value.au = !!formState.value.au
<a-checkbox
v-model:checked="formState.ai"
:disabled="sqlUi.colPropUNDisabled(formState) || !sqlUi.columnEditable(formState)"
size="small"
class="nc-column-checkbox-AI"
@change="onAlter"
/>
@ -50,18 +47,18 @@ formState.value.au = !!formState.value.au
:disabled="sqlUi.colPropUNDisabled(formState) || !sqlUi.columnEditable(formState)"
@change="onAlter"
>
<a-checkbox v-model:checked="formState.un" size="small" class="nc-column-checkbox-UN" />
<a-checkbox v-model:checked="formState.un" class="nc-column-checkbox-UN" />
</a-form-item>
<a-form-item
label="AU"
:disabled="sqlUi.colPropAuDisabled(formState) || !sqlUi.columnEditable(formState)"
@change="onAlter"
>
<a-checkbox v-model:checked="formState.au" size="small" class="nc-column-checkbox-AU" />
<a-checkbox v-model:checked="formState.au" class="nc-column-checkbox-AU" />
</a-form-item>
</div>
<a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt">
<a-select v-model:value="formState.dt" size="small" @change="onDataTypeChange">
<a-select v-model:value="formState.dt" @change="onDataTypeChange">
<a-select-option v-for="type in dataTypes" :key="type" :value="type">
{{ type }}
</a-select-option>
@ -71,15 +68,14 @@ formState.value.au = !!formState.value.au
<a-input
v-model:value="formState.dtxp"
:disabled="sqlUi.getDefaultLengthIsDisabled(formState.dt) || !sqlUi.columnEditable(formState)"
size="small"
@input="onAlter"
/>
</a-form-item>
<a-form-item v-if="sqlUi.showScale(formState)" label="Scale">
<a-input v-model="formState.dtxs" :disabled="!sqlUi.columnEditable(formState)" size="small" @input="onAlter" />
<a-input v-model="formState.dtxs" :disabled="!sqlUi.columnEditable(formState)" @input="onAlter" />
</a-form-item>
<a-form-item :label="$t('placeholder.defaultValue')">
<a-textarea v-model:value="formState.cdf" size="small" auto-size @input="onAlter(2, true)" />
<a-textarea v-model:value="formState.cdf" auto-size @input="onAlter(2, true)" />
<span class="text-gray-400 text-xs">{{ sqlUi.getDefaultValueForDatatype(formState.dt) }}</span>
</a-form-item>
</div>

4
packages/nc-gui-v2/components/smartsheet-column/CheckboxOptions.vue

@ -73,8 +73,9 @@ watch(
<a-row>
<a-col :span="24">
<a-form-item label="Icon">
<a-select v-model:value="formState.meta.iconIdx" size="small" class="w-52">
<a-select v-model:value="formState.meta.iconIdx" class="w-52">
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<div class="flex items-center">
<component
:is="getMdiIcon(icon.checked)"
class="mx-1"
@ -88,6 +89,7 @@ watch(
color: formState.meta.color,
}"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>

4
packages/nc-gui-v2/components/smartsheet-column/CurrencyOptions.vue

@ -67,12 +67,11 @@ formState.value.meta = {
</script>
<template>
<a-row>
<a-row gutter="8">
<a-col :span="12">
<a-form-item v-bind="validateInfos['meta.currency_locale']" label="Currency Locale">
<a-select
v-model:value="formState.meta.currency_locale"
size="small"
class="w-52"
show-search
:filter-option="filterOption"
@ -91,7 +90,6 @@ formState.value.meta = {
class="w-52"
show-search
:filter-option="filterOption"
size="small"
:disabled="isMoney && isPg"
>
<a-select-option v-for="(currencyCode, i) of currencyList" :key="i" :value="currencyCode">

2
packages/nc-gui-v2/components/smartsheet-column/DateOptions.vue

@ -12,7 +12,7 @@ if (!formState.meta?.date_format) {
<template>
<a-form-item label="Date Format">
<a-select v-model:value="formState.meta.date_format" size="small">
<a-select v-model:value="formState.meta.date_format">
<a-select-option v-for="(format, i) of dateFormats" :key="i" :value="format">
<div class="flex flex-row items-center">
<div class="text-xs">

2
packages/nc-gui-v2/components/smartsheet-column/DurationOptions.vue

@ -25,7 +25,7 @@ formState.value.meta = {
</a-col>
<a-col :span="24">
<a-form-item label="Duration Format">
<a-select v-model:value="formState.meta.duration" size="small" class="w-52">
<a-select v-model:value="formState.meta.duration" class="w-52">
<a-select-option v-for="(duration, i) of durationOptionList" :key="i" :value="duration.id">
{{ duration.title }}
</a-select-option>

26
packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue

@ -105,32 +105,26 @@ if (!formState.value?.column_name) {
</script>
<template>
<div class="min-w-[350px] w-max max-h-[95vh] bg-white shadow p-4 overflow-auto" @click.stop>
<div class="min-w-[400px] max-h-[95vh] bg-white shadow p-6 overflow-auto" @click.stop>
<a-form v-model="formState" name="column-create-or-edit" layout="vertical">
<div class="flex flex-col gap-2">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
<a-input ref="antInput" v-model:value="formState.title" size="small" class="nc-column-name-input" @input="onAlter(8)" />
<a-input ref="antInput" v-model:value="formState.title" class="nc-column-name-input" @input="onAlter(8)" />
</a-form-item>
<a-form-item
v-if="!(editColumnDropdown && !!onlyNameUpdateOnEditColumns.find((col) => col === formState.uidt))"
:label="$t('labels.columnType')"
>
<a-select
v-model:value="formState.uidt"
show-search
size="small"
class="nc-column-type-input"
@change="onUidtOrIdTypeChange"
>
<a-select v-model:value="formState.uidt" show-search class="nc-column-type-input" @change="onUidtOrIdTypeChange">
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="flex gap-1 align-center text-xs">
<div class="flex gap-1 align-center">
<component :is="opt.icon" class="text-grey" />
{{ opt.name }}
</div>
</a-select-option>
</a-select>
</a-form-item>
<SmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" ref="formulaOptionsRef" />
<SmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" />
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" />
@ -145,17 +139,17 @@ if (!formState.value?.column_name) {
<SmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnPercentOptions v-if="formState.uidt === UITypes.Percent" />
<SmartsheetColumnSelectOptions v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect" />
</div>
<div
v-if="!isVirtualCol(formState.uidt)"
class="text-xs cursor-pointer text-grey nc-more-options my-2 flex align-center gap-1 justify-end"
class="text-xs cursor-pointer text-grey nc-more-options mb-1 mt-4 flex align-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions"
>
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
<div class="overflow-hidden" :class="advancedOptions ? 'h-min' : 'h-0'">
<div class="overflow-hidden" :class="advancedOptions ? 'h-min mb-2' : 'h-0'">
<a-checkbox
v-if="formState.meta && columnToValidate.includes(formState.uidt)"
v-model:checked="formState.meta.validate"
@ -169,11 +163,11 @@ if (!formState.value?.column_name) {
</div>
<a-form-item>
<div class="flex justify-end gap-1 mt-4">
<a-button html-type="button" size="small" @click="onCancel">
<a-button html-type="button" @click="onCancel">
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button html-type="submit" type="primary" size="small" @click="onSubmit">
<a-button html-type="submit" type="primary" @click="onSubmit">
<!-- Save -->
{{ $t('general.save') }}
</a-button>

22
packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue

@ -41,7 +41,7 @@ const refTables = $computed(() => {
<template>
<div class="w-full flex flex-col mb-2 mt-4">
<div class="border-2 p-4">
<div class="border-2 p-6">
<a-form-item v-bind="validateInfos.type">
<a-radio-group v-model:value="formState.type" name="type" v-bind="validateInfos.type">
<a-radio value="hm">Has Many</a-radio>
@ -49,7 +49,7 @@ const refTables = $computed(() => {
</a-radio-group>
</a-form-item>
<a-form-item class="flex w-full pb-2 mt-4" :label="$t('labels.childTable')" v-bind="validateInfos.childId">
<a-select v-model:value="formState.childId" size="small" @change="onDataTypeChange">
<a-select v-model:value="formState.childId" @change="onDataTypeChange">
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.id">
{{ table.title }}
</a-select-option>
@ -65,29 +65,17 @@ const refTables = $computed(() => {
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
<div v-if="advancedOptions" class="flex flex-col p-4 border-2 mt-2">
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select
v-model:value="formState.onUpdate"
:disabled="formState.virtual"
name="onUpdate"
size="small"
@change="onDataTypeChange"
>
<a-select v-model:value="formState.onUpdate" :disabled="formState.virtual" name="onUpdate" @change="onDataTypeChange">
<a-select-option v-for="(option, index) in onUpdateDeleteOptions" :key="index" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.onDelete')">
<a-select
v-model:value="formState.onDelete"
:disabled="formState.virtual"
name="onDelete"
size="small"
@change="onDataTypeChange"
>
<a-select v-model:value="formState.onDelete" :disabled="formState.virtual" name="onDelete" @change="onDataTypeChange">
<a-select-option v-for="(option, index) in onUpdateDeleteOptions" :key="index" :value="option">
{{ option }}
</a-select-option>

16
packages/nc-gui-v2/components/smartsheet-column/LookupOptions.vue

@ -52,15 +52,10 @@ const columns = $computed(() => {
</script>
<template>
<div class="p-4 w-full flex flex-col border-2 mb-2 mt-4">
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.childTable')" v-bind="validateInfos.fk_relation_column_id">
<a-select
v-model:value="formState.fk_relation_column_id"
size="small"
dropdown-class-name="!w-64"
@change="onDataTypeChange"
>
<a-select v-model:value="formState.fk_relation_column_id" dropdown-class-name="!w-64" @change="onDataTypeChange">
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
@ -72,12 +67,7 @@ const columns = $computed(() => {
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id">
<a-select
v-model:value="formState.fk_lookup_column_id"
name="fk_lookup_column_id"
size="small"
@change="onDataTypeChange"
>
<a-select v-model:value="formState.fk_lookup_column_id" name="fk_lookup_column_id" @change="onDataTypeChange">
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
{{ column.title }}
</a-select-option>

6
packages/nc-gui-v2/components/smartsheet-column/PercentOptions.vue

@ -11,10 +11,10 @@ if (!formState.meta?.default) formState.meta.default = null
</script>
<template>
<div class="flex flex-col mt-2">
<div class="flex flex-col mt-2 gap-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" label="Precision">
<a-select v-model:value="formState.meta.precision" size="small">
<a-select v-model:value="formState.meta.precision">
<a-select-option v-for="(precision, i) of precisions" :key="i" :value="precision.id">
<div class="flex flex-row items-center">
<div class="text-xs">
@ -25,7 +25,7 @@ if (!formState.meta?.default) formState.meta.default = null
</a-select>
</a-form-item>
<a-form-item label="Default Number (%)">
<a-input v-model:value="formState.meta.default" size="small" name="default" type="number" />
<a-input v-model:value="formState.meta.default" name="default" type="number" />
</a-form-item>
</div>
<div class="flex flex-row mt-2">

8
packages/nc-gui-v2/components/smartsheet-column/RatingOptions.vue

@ -64,11 +64,12 @@ watch(
</script>
<template>
<a-row>
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="Icon">
<a-select v-model:value="formState.meta.iconIdx" size="small" class="w-52">
<a-select v-model:value="formState.meta.iconIdx" class="w-52">
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<div class="flex items-center">
<component
:is="getMdiIcon(icon.full)"
class="mx-1"
@ -82,13 +83,14 @@ watch(
color: formState.meta.color,
}"
/>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="Max">
<a-select v-model:value="formState.meta.max" class="w-52" size="small">
<a-select v-model:value="formState.meta.max" class="w-52">
<a-select-option v-for="(v, i) in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" :key="i" :value="v">
{{ v }}
</a-select-option>

18
packages/nc-gui-v2/components/smartsheet-column/RollupOptions.vue

@ -65,15 +65,10 @@ const columns = $computed(() => {
</script>
<template>
<div class="p-4 w-full flex flex-col border-2 mb-2 mt-4">
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.childTable')" v-bind="validateInfos.fk_relation_column_id">
<a-select
v-model:value="formState.fk_relation_column_id"
size="small"
dropdown-class-name="!w-64"
@change="onDataTypeChange"
>
<a-select v-model:value="formState.fk_relation_column_id" dropdown-class-name="!w-64" @change="onDataTypeChange">
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
@ -85,12 +80,7 @@ const columns = $computed(() => {
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_rollup_column_id">
<a-select
v-model:value="formState.fk_rollup_column_id"
name="fk_rollup_column_id"
size="small"
@change="onDataTypeChange"
>
<a-select v-model:value="formState.fk_rollup_column_id" name="fk_rollup_column_id" @change="onDataTypeChange">
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
{{ column.title }}
</a-select-option>
@ -98,7 +88,7 @@ const columns = $computed(() => {
</a-form-item>
</div>
<a-form-item label="Aggregate function" v-bind="validateInfos.rollup_function">
<a-select v-model:value="formState.rollup_function" size="small" @change="onDataTypeChange">
<a-select v-model:value="formState.rollup_function" @change="onDataTypeChange">
<a-select-option v-for="(func, index) of aggrFunctionsList" :key="index" :value="func.value">
{{ func.text }}
</a-select-option>

2
packages/nc-gui-v2/components/smartsheet-column/SpecificDBTypeOptions.vue

@ -1,3 +1,3 @@
<template>
<SmartsheetColumnAdvancedOptions class="mt-4" />
<SmartsheetColumnAdvancedOptions class="mt-4 mb-2" />
</template>

28
packages/nc-gui-v2/components/smartsheet-header/Cell.vue

@ -19,6 +19,14 @@ const { isUIAllowed } = useUIPermission()
provide(ColumnInj, column)
const editColumnDropdown = ref(false)
function onVisibleChange() {
// only allow to close the EditOrAdd component
// by clicking cancel button
editColumnDropdown.value = true
}
// instantiate column update store
useProvideColumnCreateStore(meta as Ref<TableType>, column)
</script>
@ -31,8 +39,26 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<template v-if="!hideMenu">
<div class="flex-1" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" @edit="editColumnDropdown = true" />
</template>
<a-dropdown
v-model:visible="editColumnDropdown"
:trigger="['click']"
placement="bottomRight"
@visible-change="onVisibleChange"
>
<div />
<template #overlay>
<SmartsheetColumnEditOrAdd
class="w-full"
:edit-column-dropdown="editColumnDropdown"
@click.stop
@keydown.stop
@cancel="editColumnDropdown = false"
/>
</template>
</a-dropdown>
</div>
</template>

38
packages/nc-gui-v2/components/smartsheet-header/Menu.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Modal, notification } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import { inject } from 'vue'
import { useI18n } from 'vue-i18n'
import { useNuxtApp } from '#app'
@ -12,8 +12,7 @@ import MdiDeleteIcon from '~icons/mdi/delete-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
const { virtual = false } = defineProps<{ virtual?: boolean }>()
const editColumnDropdown = ref(false)
const emit = defineEmits(['edit'])
const column = inject(ColumnInj)
@ -36,9 +35,7 @@ const deleteColumn = () =>
await $api.dbTableColumn.delete(column?.value?.id as string)
getMeta(meta?.value?.id as string, true)
} catch (e) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
},
})
@ -47,40 +44,19 @@ const setAsPrimaryValue = async () => {
try {
await $api.dbTableColumn.primaryColumnSet(column?.value?.id as string)
getMeta(meta?.value?.id as string, true)
notification.success({
message: 'Successfully updated as primary column',
})
message.success('Successfully updated as primary column')
} catch (e) {
notification.error({
message: 'Failed to update primary column',
})
message.error('Failed to update primary column')
}
}
function onVisibleChange() {
// only allow to close the EditOrAdd component
// by clicking cancel button
editColumnDropdown.value = true
}
</script>
<template>
<a-dropdown v-model:visible="editColumnDropdown" :trigger="['click']" @visible-change="onVisibleChange">
<span />
<template #overlay>
<SmartsheetColumnEditOrAdd
:edit-column-dropdown="editColumnDropdown"
@click.stop
@keydown.stop
@cancel="editColumnDropdown = false"
/>
</template>
</a-dropdown>
<a-dropdown :trigger="['hover']">
<a-dropdown placement="bottomRight" :trigger="['hover']">
<MdiMenuDownIcon class="text-grey nc-ui-dt-dropdown" />
<template #overlay>
<a-menu class="shadow bg-white">
<a-menu-item @click="editColumnDropdown = true">
<a-menu-item @click="emit('edit')">
<div class="nc-column-edit nc-header-menu-item">
<MdiEditIcon class="text-primary" />
<!-- Edit -->

27
packages/nc-gui-v2/components/smartsheet-header/VirtualCell.vue

@ -11,6 +11,8 @@ const column = toRef(props, 'column')
const hideMenu = toRef(props, 'hideMenu')
const editColumnDropdown = ref(false)
provide(ColumnInj, column)
const { metas } = useMetas()
@ -83,6 +85,12 @@ const tooltipMsg = computed(() => {
return ''
})
function onVisibleChange() {
// only allow to close the EditOrAdd component
// by clicking cancel button
editColumnDropdown.value = true
}
useProvideColumnCreateStore(meta as Ref<TableType>, column)
</script>
@ -107,9 +115,26 @@ useProvideColumnCreateStore(meta as Ref<TableType>, column)
<!-- </v-tooltip> -->
<template v-if="!hideMenu">
<v-spacer />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
</template>
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" />
<a-dropdown
v-model:visible="editColumnDropdown"
:trigger="['click']"
placement="bottomRight"
@visible-change="onVisibleChange"
>
<div />
<template #overlay>
<SmartsheetColumnEditOrAdd
class="w-full"
:edit-column-dropdown="editColumnDropdown"
@click.stop
@keydown.stop
@cancel="editColumnDropdown = false"
/>
</template>
</a-dropdown>
</div>
</template>

6
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue

@ -112,7 +112,7 @@ defineExpose({
<template>
<div
class="p-6 menu-filter-dropdown bg-gray-50"
class="p-6 menu-filter-dropdown bg-gray-50 !border"
:class="{ 'shadow-xl min-w-[430px] max-w-[630px] max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }"
>
<div v-if="filters && filters.length" class="nc-filter-grid mb-2" @click.stop>
@ -248,7 +248,7 @@ defineExpose({
</div>
<div class="flex gap-2 mb-2 mt-4">
<a-button class="elevation-0 text-capitalize" @click.stop="addFilter">
<a-button class="elevation-0 text-capitalize" type="primary" ghost @click.stop="addFilter">
<div class="flex align-center gap-1">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<MdiAddIcon />
@ -272,7 +272,7 @@ defineExpose({
<style scoped>
.nc-filter-grid {
display: grid;
grid-template-columns: 18px 70px auto auto auto;
grid-template-columns: 18px 75px auto auto auto;
@apply gap-[12px]
align-items: center;
}

5
packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue

@ -79,7 +79,10 @@ const onMove = (event: { moved: { newIndex: number } }) => {
</a-button>
</div>
<template #overlay>
<div class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto" @click.stop>
<div
class="p-3 min-w-[280px] bg-gray-50 shadow-lg nc-table-toolbar-menu max-h-[max(80vh,500px)] overflow-auto !border"
@click.stop
>
<div class="p-1" @click.stop>
<a-input v-model:value="filterQuery" size="small" :placeholder="$t('placeholder.searchFields')" />
</div>

16
packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue

@ -2,7 +2,7 @@
import * as XLSX from 'xlsx'
import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
import { ActiveViewInj, MetaInj } from '~/context'
@ -98,19 +98,13 @@ const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
}
offset = +headers['nc-export-offset']
if (offset > -1) {
notification.info({
message: 'Downloading more files',
})
message.info('Downloading more files')
} else {
notification.success({
message: 'Successfully exported all table data',
})
message.success('Successfully exported all table data')
}
}
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
@ -127,7 +121,7 @@ const exportFile = async (exportType: ExportTypes.EXCEL | ExportTypes.CSV) => {
</div>
</a-button>
<template #overlay>
<div class="bg-white shadow-lg">
<div class="bg-white shadow-lg !border">
<div>
<div v-t="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadIcon />

18
packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue

@ -2,7 +2,7 @@
import { useClipboard } from '@vueuse/core'
import { ViewTypes } from 'nocodb-sdk'
import { computed } from 'vue'
import { message, notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
@ -72,13 +72,9 @@ async function saveAllowCSVDownload() {
await $api.dbViewShare.update(shared.value.id, {
meta,
} as any)
notification.success({
message: 'Successfully updated',
})
message.success('Successfully updated')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
if (allowCSVDownload?.value) {
$e('a:view:share:enable-csv-download')
@ -92,13 +88,9 @@ const saveShareLinkPassword = async () => {
await $api.dbViewShare.update(shared.value.id, {
password: shared.value.password,
})
notification.success({
message: 'Successfully updated',
})
message.success('Successfully updated')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:view:share:enable-pwd')

10
packages/nc-gui-v2/components/smartsheet-toolbar/SharedViewList.vue

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { useClipboard } from '@vueuse/core'
import { ViewTypes } from 'nocodb-sdk'
import { message, notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { onMounted, useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import MdiVisibilityOnIcon from '~icons/mdi/visibility'
@ -80,14 +80,10 @@ const copyLink = (view: SharedViewType) => {
const deleteLink = async (id: string) => {
try {
await $api.dbViewShare.delete(id)
notification.success({
message: 'Deleted shared view successfully',
})
message.success('Deleted shared view successfully')
await loadSharedViewsList()
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>

4
packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue

@ -46,7 +46,7 @@ watch(
</a-button>
</div>
<template #overlay>
<div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto">
<div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border">
<div v-if="sorts?.length" class="sort-grid mb-2" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i">
<!-- <v-icon :key="`${i}icon`" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> mdi-close-box </v-icon> -->
@ -84,7 +84,7 @@ watch(
<!-- </v-select> -->
</template>
</div>
<a-button class="text-capitalize mb-1 mt-4" @click.stop="addSort">
<a-button class="text-capitalize mb-1 mt-4" type="primary" ghost @click.stop="addSort">
<div class="flex gap-1 align-center">
<MdiAddIcon />
<!-- Add Sort Option -->

53
packages/nc-gui-v2/components/smartsheet/Form.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import { RelationTypes, UITypes, getSystemColumns, isVirtualCol } from 'nocodb-sdk'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Permission } from '~/composables/useUIPermission/rolePermissions'
import { computed, inject, onClickOutside, useDebounceFn } from '#imports'
import { ActiveViewInj, IsFormInj, MetaInj } from '~/context'
@ -65,10 +65,7 @@ const activeRow = ref('')
function updateView() {
if ((formViewData.value?.subheading?.length || 0) > 255) {
notification.error({
message: 'Data too long for Form Description',
duration: 3,
})
message.error('Data too long for Form Description')
return
}
@ -79,12 +76,7 @@ async function submitForm() {
try {
await formRef.value?.validateFields()
} catch (e: any) {
e.errorFields.map((f: Record<string, any>) =>
notification.error({
message: f.errors.join(','),
duration: 3,
}),
)
e.errorFields.map((f: Record<string, any>) => message.error(f.errors.join(',')))
return
}
@ -154,10 +146,7 @@ function onMove(event: any) {
function hideColumn(idx: number) {
if (isDbRequired(localColumns.value[idx]) || localColumns.value[idx].required) {
notification.info({
message: "Required field can't be moved",
duration: 3,
})
message.info("Required field can't be moved")
return
}
@ -203,10 +192,7 @@ async function checkSMTPStatus() {
const emailPluginActive = await $api.plugin.status('SMTP')
if (!emailPluginActive) {
emailMe.value = false
notification.info({
message: 'Please activate SMTP plugin in App store for enabling email notification',
duration: 3,
})
message.info('Please activate SMTP plugin in App store for enabling email notification')
}
}
}
@ -277,10 +263,7 @@ const updateColMeta = useDebounceFn(async (col: Record<string, any>) => {
try {
$api.dbView.formColumnUpdate(col.id, col)
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
duration: 3,
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
}, 250)
@ -333,7 +316,7 @@ onMounted(async () => {
New form will be loaded after {{ secondsRemain }} seconds
</div>
<div v-if="formViewData.submit_another_form" class="text-center mt-4">
<a-button type="primary" size="large" @click="submitted = false"> Submit Another Form </a-button>
<a-button type="primary" size="large" @click="submitted = false"> Submit Another Form</a-button>
</div>
</div>
</a-col>
@ -423,9 +406,9 @@ onMounted(async () => {
</a-col>
<a-col v-if="formViewData" :span="isEditable ? 16 : 24" class="h-full overflow-auto scrollbar-thin-primary">
<div class="h-[200px]">
<a-card class="h-full !bg-[#dbdad7] ma-0 rounded-b-0">
<a-card class="h-full !bg-[#dbdad7] ma-0 rounded-b-0 pa-8">
<a-form ref="formRef" :model="formState">
<a-card class="rounded ma-10">
<a-card class="rounded ma-6 pb-10 px-15">
<!-- Header -->
<a-form-item class="ma-0 gap-0 pa-0">
<a-input
@ -512,6 +495,7 @@ onMounted(async () => {
<a-form-item class="my-0 w-1/2">
<a-input
v-model:value="element.label"
size="small"
class="form-meta-input !bg-[#dbdbdb]"
:placeholder="$t('msg.info.formInput')"
@change="updateColMeta(element)"
@ -521,14 +505,15 @@ onMounted(async () => {
<a-form-item class="mt-2 mb-0 w-1/2">
<a-input
v-model:value="element.description"
class="form-meta-input !bg-[#dbdbdb]"
size="small"
class="form-meta-input !bg-[#dbdbdb] text-sm"
:placeholder="$t('msg.info.formHelpText')"
@change="updateColMeta(element)"
/>
</a-form-item>
<div class="items-center flex">
<span class="text-sm text-gray-500 mr-2">{{ $t('general.required') }}</span>
<a-switch v-model:checked="element.required" class="my-2" @change="updateColMeta(element)" />
<a-switch v-model:checked="element.required" size="small" class="my-2" @change="updateColMeta(element)" />
</div>
</div>
<span class="text-gray-500">{{ element.description }}</span>
@ -543,16 +528,17 @@ onMounted(async () => {
</div>
</template>
</Draggable>
</a-card>
</a-form>
<div class="justify-center flex mt-5">
<div class="justify-center flex mt-10">
<a-button class="flex items-center gap-2 !bg-primary text-white rounded" size="large" @click="submitForm">
<!-- Submit -->
{{ $t('general.submit') }}
</a-button>
</div>
</a-card>
</a-form>
<div class="mx-10 px-10">
<!-- After form is submitted -->
<div class="text-gray-500 mt-4 mb-2">
{{ $t('msg.info.afterFormSubmitted') }}
@ -568,6 +554,7 @@ onMounted(async () => {
<a-switch
v-model:checked="formViewData.submit_another_form"
v-t="[`a:form-view:submit-another-form`]"
size="small"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.submitAnotherForm') }}</span>
@ -578,18 +565,20 @@ onMounted(async () => {
<a-switch
v-model:checked="formViewData.show_blank_form"
v-t="[`a:form-view:show-blank-form`]"
size="small"
@change="updateView"
/>
<span class="ml-4">{{ $t('msg.info.showBlankForm') }}</span>
</div>
<div class="my-4">
<a-switch v-model:checked="emailMe" v-t="[`a:form-view:email-me`]" @change="onEmailChange" />
<a-switch v-model:checked="emailMe" v-t="[`a:form-view:email-me`]" size="small" @change="onEmailChange" />
<!-- Email me at <email> -->
<span class="ml-4">
{{ $t('msg.info.emailForm') }} <span class="text-bold text-gray-600">{{ state.user.value?.email }}</span>
</span>
</div>
</div>
</div>
</a-card>
</div>
</a-col>

8
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -488,10 +488,10 @@ const expandForm = (row: Row, state: Record<string, any>) => {
table,
td,
th {
border-right: 1px solid #7f828b33 !important;
border-left: 1px solid #7f828b33 !important;
border-bottom: 1px solid #7f828b33 !important;
border-top: 1px solid #7f828b33 !important;
border-right: 1px solid #f0f0f0 !important;
border-left: 1px solid #f0f0f0 !important;
border-bottom: 1px solid #f0f0f0 !important;
border-top: 1px solid #f0f0f0 !important;
border-collapse: collapse;
}

10
packages/nc-gui-v2/components/smartsheet/Toolbar.vue

@ -5,7 +5,7 @@ const { isGrid, isForm } = useSmartsheetStoreOrThrow()
</script>
<template>
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[48px] px-2" style="z-index: 7">
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[48px] px-2 border-b" style="z-index: 7">
<SmartsheetToolbarFieldsMenu v-if="isGrid" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid" />
@ -15,10 +15,8 @@ const { isGrid, isForm } = useSmartsheetStoreOrThrow()
<SmartsheetToolbarShareView v-if="isForm || isGrid" />
<SmartsheetToolbarMoreActions v-if="isGrid" />
<SmartsheetToolbarSearchData v-if="isGrid" class="shrink ml-2" />
<div class="flex-1" />
<SmartsheetToolbarSearchData v-if="isGrid" class="shrink mr-2" />
</div>
</template>
@ -26,4 +24,8 @@ const { isGrid, isForm } = useSmartsheetStoreOrThrow()
:deep(.nc-toolbar-btn) {
@apply border-0 !text-xs font-semibold px-2;
}
.nc-table-toolbar {
border-color: #f0f0f0 !important;
}
</style>

6
packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue

@ -35,7 +35,7 @@ const iconColor = '#1890ff'
</script>
<template>
<div class="flex p-2 align-center gap-2">
<div class="flex p-2 align-center gap-2 p-4">
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0">
<mdi-table-arrow-right :style="{ color: iconColor }" />
@ -55,11 +55,11 @@ const iconColor = '#1890ff'
class="cursor-pointer select-none"
@click="commentsDrawer = !commentsDrawer"
/>
<a-button size="small" class="!text" @click="emit('cancel')">
<a-button class="!text" @click="emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button size="small" :disabled="!isUIAllowed('tableRowUpdate')" type="primary" @click="save">
<a-button :disabled="!isUIAllowed('tableRowUpdate')" type="primary" @click="save">
<!-- Save Row -->
{{ $t('activity.saveRow') }}
</a-button>

2
packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue

@ -86,7 +86,7 @@ const isExpanded = useVModel(props, 'modelValue', emits)
<div class="flex h-full nc-form-wrapper items-stretch">
<div class="flex-grow overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto">
<div v-for="col in fields" :key="col.title" class="mt-2">
<div v-for="col in fields" :key="col.title" class="mt-2 py-2">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />

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

@ -2,7 +2,7 @@
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Ref } from 'vue'
import Sortable from 'sortablejs'
import RenameableMenuItem from './RenameableMenuItem.vue'
@ -147,15 +147,9 @@ async function onRename(view: ViewType) {
order: String(view.order),
})
notification.success({
message: 'View renamed successfully',
duration: 3,
})
message.success('View renamed successfully')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
duration: 3,
})
message.error(await extractSdkResponseErrorMsg(e))
}
}

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import type { ViewTypes } from 'nocodb-sdk'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { viewIcons } from '~/utils'
import { onKeyStroke, useDebounceFn, useNuxtApp, useVModel } from '#imports'
@ -104,10 +104,7 @@ async function onRename() {
const isValid = props.onValidate(vModel.value)
if (isValid !== true) {
notification.error({
message: isValid,
duration: 2,
})
message.error(isValid)
onCancel()
return

18
packages/nc-gui-v2/components/smartsheet/sidebar/menu/ApiSnippet.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import HTTPSnippet from 'httpsnippet'
import { useClipboard } from '@vueuse/core'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { ActiveViewInj, MetaInj } from '~/context'
const props = defineProps<Props>()
@ -110,7 +110,7 @@ api.dbViewRow.list(
const onCopyToClipboard = () => {
copy(code)
notification.info({ message: 'Copied to clipboard' })
message.info('Copied to clipboard')
}
const afterVisibleChange = (visible: boolean) => {
@ -128,8 +128,8 @@ const afterVisibleChange = (visible: boolean) => {
:closable="false"
@after-visible-change="afterVisibleChange"
>
<div class="flex flex-col w-full h-full">
<a-typography-title :level="4">Code Snippet</a-typography-title>
<div class="flex flex-col w-full h-full p-4">
<a-typography-title :level="4" class="pb-1">Code Snippet</a-typography-title>
<a-tabs v-model:activeKey="selectedLangName" class="!h-full">
<a-tab-pane v-for="item in langs" :key="item.name" class="!h-full">
<template #tab>
@ -146,6 +146,11 @@ const afterVisibleChange = (visible: boolean) => {
:disable-deep-compare="true"
/>
<div class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase">
<a-select v-if="activeLang" v-model:value="selectedClient" style="width: 6rem">
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client">
{{ client }}
</a-select-option>
</a-select>
<a-button
v-t="[
'c:snippet:copy',
@ -155,11 +160,6 @@ const afterVisibleChange = (visible: boolean) => {
@click="onCopyToClipboard"
>Copy to clipboard</a-button
>
<a-select v-if="activeLang" v-model:value="selectedClient" style="width: 6rem">
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client">
{{ client }}
</a-select-option>
</a-select>
</div>
<div class="absolute bottom-4 flex flex-row justify-center w-[95%]">

2
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue

@ -12,7 +12,7 @@ const sidebarOpen = inject(RightSidebarInj, ref(true))
<template #title> {{ $t('activity.addRow') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group">
<MdiPlusOutline class="group-hover:(!text-white)" @click="emits('addRow')" />
<MdiPlusOutline class="cursor-pointer group-hover:(!text-white)" @click="emits('addRow')" />
</div>
</a-tooltip>
</template>

2
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/DeleteTable.vue

@ -14,7 +14,7 @@ const sidebarOpen = inject(RightSidebarInj, ref(true))
<template #title> {{ $t('activity.deleteTable') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-red-500 group">
<MdiDeleteOutline class="group-hover:(!text-white)" @click="deleteTable(meta)" />
<MdiDeleteOutline class="cursor-pointer group-hover:(!text-white)" @click="deleteTable(meta)" />
</div>
</a-tooltip>
</template>

14
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { computed, useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
@ -20,9 +20,7 @@ async function changeLockType(type: LockType) {
$e('a:grid:lockmenu', { lockType: type })
if (type === 'personal') {
return notification.info({
message: 'Coming soon',
})
return message.info('Coming soon')
}
try {
;(view.value as any).lock_type = type
@ -30,13 +28,9 @@ async function changeLockType(type: LockType) {
lock_type: type,
})
notification.success({
message: `Successfully Switched to ${type} view`,
})
message.success(`Successfully Switched to ${type} view`)
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}

2
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/Reload.vue

@ -14,7 +14,7 @@ const onClick = () => reloadHook.trigger()
<template #title> {{ $t('general.reload') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-green-500 group">
<MdiReload class="group-hover:(!text-white)" @click="onClick" />
<MdiReload class="cursor-pointer group-hover:(!text-white)" @click="onClick" />
</div>
</a-tooltip>
</template>

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

@ -1,6 +1,5 @@
<script lang="ts" setup>
import AddRow from './AddRow.vue'
import DeleteTable from './DeleteTable.vue'
import LockMenu from './LockMenu.vue'
import Reload from './Reload.vue'
</script>
@ -19,10 +18,6 @@ import Reload from './Reload.vue'
<AddRow />
<div class="dot" />
<DeleteTable />
<slot name="end" />
</div>
</template>

40
packages/nc-gui-v2/components/tabs/Auth.vue

@ -2,42 +2,48 @@
import UserManagement from './auth/UserManagement.vue'
import ApiTokenManagement from './auth/ApiTokenManagement.vue'
interface TabGroup {
[key: string]: {
interface Tab {
title: string
body: any
}
}
const tabsInfo: TabGroup = {
usersManagement: {
const tabsInfo: Tab[] = [
{
title: 'Users Management',
body: () => UserManagement,
},
apiTokenManagement: {
{
title: 'API Token Management',
body: () => ApiTokenManagement,
},
}
]
const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
// const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
const selectedTabKeys = $ref<string[]>([firstKeyOfObject(tabsInfo)])
const selectedTab = $computed(() => tabsInfo[selectedTabKeys[0]])
const selectedTabKey = $ref(0)
const selectedTab = $computed(() => tabsInfo[selectedTabKey])
</script>
<template>
<div class="mt-2">
<a-menu v-model:selectedKeys="selectedTabKeys" :open-keys="[]" mode="horizontal">
<a-menu-item v-for="(tab, key) of tabsInfo" :key="key" class="select-none">
<div class="text-xs pb-2.5">
<div>
<a-tabs v-model:active-key="selectedTabKey" :open-keys="[]" mode="horizontal" class="nc-auth-tabs mx-6">
<a-tabs-tab-pane v-for="(tab, key) of tabsInfo" :key="key" class="select-none">
<template #tab>
<span>
{{ tab.title }}
</div>
</a-menu-item>
</a-menu>
</span>
</template>
</a-tabs-tab-pane>
</a-tabs>
<div class="mx-4 py-6 mt-2">
<component :is="selectedTab.body()" />
</div>
</div>
</template>
<style scoped>
:deep(.nc-auth-tabs .ant-tabs-nav::before) {
@apply !border-none;
}
</style>

24
packages/nc-gui-v2/components/tabs/auth/ApiTokenManagement.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ApiTokenType } from 'nocodb-sdk'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { useClipboard } from '@vueuse/core'
import KebabIcon from '~icons/ic/baseline-more-vert'
import MdiPlusIcon from '~icons/mdi/plus'
@ -45,9 +45,7 @@ const copyToken = (token: string | undefined) => {
if (!token) return
copy(token)
notification.info({
message: 'Copied to clipboard',
})
message.info('Copied to clipboard')
$e('c:api-token:copy')
}
@ -58,15 +56,11 @@ const generateToken = async () => {
await $api.apiToken.create(project.id, selectedTokenData)
showNewTokenModal = false
notification.success({
message: 'Token generated successfullyd',
})
message.success('Token generated successfully')
selectedTokenData = {}
await loadApiTokens()
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:api-token:generate')
@ -78,15 +72,11 @@ const deleteToken = async () => {
await $api.apiToken.delete(project.id, selectedTokenData.token)
notification.success({
message: 'Token deleted successfully',
})
message.success('Token deleted successfully')
await loadApiTokens()
showDeleteTokenModal = false
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:api-token:delete')
@ -139,7 +129,7 @@ onMounted(() => {
<div class="text-gray-500">{{ $t('general.reload') }}</div>
</div>
</a-button>
<a-button size="middle" @click="openNewTokenModal">
<a-button size="middle" type="primary" ghost @click="openNewTokenModal">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MdiPlusIcon />
<div>Add New Token</div>

148
packages/nc-gui-v2/components/tabs/auth/UserManagement.vue

@ -1,38 +1,50 @@
<script setup lang="ts">
import { useClipboard, watchDebounced } from '@vueuse/core'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import UsersModal from './user-management/UsersModal.vue'
import FeedbackForm from './user-management/FeedbackForm.vue'
import KebabIcon from '~icons/ic/baseline-more-vert'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { projectRoleTagColors } from '~/utils/userUtils'
import MidAccountIcon from '~icons/mdi/account-outline'
import ReloadIcon from '~icons/mdi/reload'
import MdiEditIcon from '~icons/ic/round-edit'
import SearchIcon from '~icons/ic/round-search'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import EmailIcon from '~icons/eva/email-outline'
import MdiPlusIcon from '~icons/mdi/plus'
import MdiContentCopyIcon from '~icons/mdi/content-copy'
import MdiEmailSendIcon from '~icons/mdi/email-arrow-right-outline'
import RolesIcon from '~icons/mdi/drama-masks'
import type { User } from '~/lib/types'
const { $api, $e } = useNuxtApp()
import {
extractSdkResponseErrorMsg,
onMounted,
projectRoleTagColors,
ref,
useApi,
useClipboard,
useDashboard,
useNuxtApp,
useProject,
useUIPermission,
watchDebounced,
} from '#imports'
import type { User } from '~/lib'
const { $e } = useNuxtApp()
const { api } = useApi()
const { project } = useProject()
const { copy } = useClipboard()
const { isUIAllowed } = useUIPermission()
const { dashboardUrl } = $(useDashboard())
let users = $ref<null | User[]>(null)
let selectedUser = $ref<null | User>(null)
let showUserModal = $ref(false)
let showUserDeleteModal = $ref(false)
let isLoading = $ref(false)
let totalRows = $ref(0)
const currentPage = $ref(1)
const currentLimit = $ref(10)
const searchText = ref<string>('')
const loadUsers = async (page = currentPage, limit = currentLimit) => {
@ -40,21 +52,20 @@ const loadUsers = async (page = currentPage, limit = currentLimit) => {
if (!project.value?.id) return
// TODO: Types of api is not correct
const response: any = await $api.auth.projectUserList(project.value?.id, {
const response: any = await api.auth.projectUserList(project.value?.id, {
query: {
limit,
offset: searchText.value.length === 0 ? (page - 1) * limit : 0,
query: searchText.value,
},
})
} as any)
if (!response.users) return
totalRows = response.users.pageInfo.totalRows ?? 0
users = response.users.list as User[]
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -62,15 +73,12 @@ const inviteUser = async (user: User) => {
try {
if (!project.value?.id) return
await $api.auth.projectUserAdd(project.value.id, user)
notification.success({
message: 'Successfully added user to project',
})
await api.auth.projectUserAdd(project.value.id, user)
message.success('Successfully added user to project')
await loadUsers()
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:user:add')
@ -80,16 +88,15 @@ const deleteUser = async () => {
try {
if (!project.value?.id || !selectedUser?.id) return
await $api.auth.projectUserRemove(project.value.id, selectedUser.id)
notification.success({
message: 'Successfully deleted user from project',
})
await api.auth.projectUserRemove(project.value.id, selectedUser.id)
message.success('Successfully deleted user from project')
await loadUsers()
showUserDeleteModal = false
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
} finally {
showUserDeleteModal = false
}
@ -116,15 +123,12 @@ const resendInvite = async (user: User) => {
if (!project.value?.id) return
try {
await $api.auth.projectUserResendInvite(project.value.id, user.id)
notification.success({
message: 'Invite email sent successfully',
})
await api.auth.projectUserResendInvite(project.value.id, user.id, null)
message.success('Invite email sent successfully')
await loadUsers()
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:user:resend-invite')
@ -133,22 +137,16 @@ const resendInvite = async (user: User) => {
const copyInviteUrl = (user: User) => {
if (!user.invite_token) return
const getInviteUrl = (token: string) => `${dashboardUrl}/user/authentication/signup/${token}`
copy(`${dashboardUrl}/signup/${user.invite_token}`)
copy(getInviteUrl(user.invite_token))
notification.success({
message: 'Invite url copied to clipboard',
})
message.success('Invite url copied to clipboard')
}
onMounted(async () => {
onMounted(() => {
if (!users) {
isLoading = true
try {
await loadUsers()
} finally {
isLoading = false
}
loadUsers().finally(() => (isLoading = false))
}
})
@ -178,11 +176,11 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div>
</div>
</a-modal>
<div class="flex flex-row mb-4 mx-4 justify-between">
<div class="flex flex-row mb-4 mx-4 justify-between pb-2">
<div class="flex w-1/3">
<a-input v-model:value="searchText" placeholder="Filter by email">
<template #prefix>
<SearchIcon class="text-gray-400" />
<IcRoundSearch class="text-gray-400" />
</template>
</a-input>
</div>
@ -190,13 +188,13 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="flex flex-row space-x-1">
<a-button size="middle" type="text" @click="loadUsers()">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<ReloadIcon class="text-gray-500" />
<MdiReload class="text-gray-500" />
<div class="text-gray-500">Reload</div>
</div>
</a-button>
<a-button v-if="isUIAllowed('newUser')" size="middle" @click="onInvite">
<a-button v-if="isUIAllowed('newUser')" size="middle" type="primary" ghost @click="onInvite">
<div class="flex flex-row justify-center items-center caption capitalize space-x-1">
<MidAccountIcon />
<MdiAccountPlusOutline class="mr-1" />
<div>{{ $t('activity.inviteTeam') }}</div>
</div>
</a-button>
@ -205,12 +203,12 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="px-5">
<div class="flex flex-row border-b-1 pb-2 px-2">
<div class="flex flex-row w-4/6 space-x-1 items-center pl-1">
<EmailIcon class="flex text-gray-500 -mt-0.5" />
<EvaEmailOutline class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs space-x-1">{{ $t('labels.email') }}</div>
</div>
<div class="flex flex-row justify-center w-1/6 space-x-1 items-center pl-1">
<RolesIcon class="flex text-gray-500 -mt-0.5" />
<MdiDramaMasks class="flex text-gray-500 -mt-0.5" />
<div class="text-gray-600 text-xs">{{ $t('objects.role') }}</div>
</div>
@ -219,12 +217,13 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div>
</div>
<div v-for="(user, index) in users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2">
<div v-for="(user, index) of users" :key="index" class="flex flex-row items-center border-b-1 py-2 px-2">
<div class="flex w-4/6 flex-wrap">
{{ user.email }}
</div>
<div class="flex w-1/6 justify-center flex-wrap ml-4">
<div class="rounded-full px-2 py-1" :style="{ backgroundColor: projectRoleTagColors[user.roles as String] }">
<div class="rounded-full px-2 py-1" :style="{ backgroundColor: projectRoleTagColors[user.roles] }">
{{ user.roles }}
</div>
</div>
@ -235,7 +234,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</template>
<a-button type="text" class="!rounded-md" @click="onEdit(user)">
<template #icon>
<MdiEditIcon class="flex mx-auto h-[1rem] text-gray-500" />
<IcRoundEdit class="flex mx-auto h-[1rem] text-gray-500" />
</template>
</a-button>
</a-tooltip>
@ -245,7 +244,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</template>
<a-button type="text" class="!rounded-md" @click="inviteUser(user)">
<template #icon>
<MdiPlusIcon class="flex mx-auto h-[1.1rem] text-gray-500" />
<MdiPlus class="flex mx-auto h-[1.1rem] text-gray-500" />
</template>
</a-button>
</a-tooltip>
@ -256,7 +255,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</template>
<a-button type="text" class="!rounded-md" @click="onDelete(user)">
<template #icon>
<MdiDeleteOutlineIcon class="flex mx-auto h-[1.1rem] text-gray-500" />
<MdiDeleteOutline class="flex mx-auto h-[1.1rem] text-gray-500" />
</template>
</a-button>
</a-tooltip>
@ -265,7 +264,7 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<div class="flex flex-row items-center">
<a-button type="text" class="!px-0">
<div class="flex flex-row items-center h-[1.2rem]">
<KebabIcon />
<IcBaselineMoreVert />
</div>
</a-button>
</div>
@ -273,13 +272,13 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
<a-menu>
<a-menu-item>
<div class="flex flex-row items-center py-1" @click="resendInvite(user)">
<MdiEmailSendIcon class="flex h-[1rem] text-gray-500" />
<MdiEmailArrowRightOutline class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">Resend invite email</div>
</div>
</a-menu-item>
<a-menu-item>
<div class="flex flex-row items-center py-1" @click="copyInviteUrl(user)">
<MdiContentCopyIcon class="flex h-[1rem] text-gray-500" />
<MdiContentCopy class="flex h-[1rem] text-gray-500" />
<div class="text-xs pl-2">{{ $t('activity.copyInviteURL') }}</div>
</div>
</a-menu-item>
@ -301,12 +300,3 @@ watchDebounced(searchText, () => loadUsers(), { debounce: 300, maxWait: 600 })
</div>
</div>
</template>
<style scoped>
.users-table {
/* equally spaced columns in table */
table-layout: fixed;
width: 100%;
}
</style>

26
packages/nc-gui-v2/components/tabs/auth/user-management/ShareBase.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { onMounted, useClipboard, useNuxtApp, useProject } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
@ -39,9 +39,7 @@ const loadBase = async () => {
role: res.roles,
}
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -56,9 +54,7 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
base = res ?? {}
base!.role = role
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:shared-base:enable', { role })
@ -71,9 +67,7 @@ const disableSharedBase = async () => {
await $api.project.sharedBaseDisable(project.value.id)
base = null
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:shared-base:disable')
@ -91,9 +85,7 @@ const recreate = async () => {
base = { ...newBase, role: base?.role }
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:shared-base:recreate')
@ -104,9 +96,7 @@ const copyUrl = async () => {
await copy(url)
notification.success({
message: 'Copied shareable base url to clipboard!',
})
message.success('Copied shareable base url to clipboard!')
$e('c:shared-base:copy-url')
}
@ -130,9 +120,7 @@ width="100%"
height="700"
style="background: transparent; border: 1px solid #ddd"></iframe>`)
notification.success({
message: 'Copied embeddable html code!',
})
message.success('Copied embeddable html code!')
$e('c:shared-base:copy-embed-frame')
}

56
packages/nc-gui-v2/components/tabs/auth/user-management/UsersModal.vue

@ -1,14 +1,20 @@
<script setup lang="ts">
import { Form, notification } from 'ant-design-vue'
import { useClipboard } from '@vueuse/core'
import { Form, message } from 'ant-design-vue'
import ShareBase from './ShareBase.vue'
import SendIcon from '~icons/material-symbols/send-outline'
import CloseIcon from '~icons/material-symbols/close-rounded'
import MidAccountIcon from '~icons/mdi/account-outline'
import ContentCopyIcon from '~icons/mdi/content-copy'
import type { User } from '~/lib/types'
import { ProjectRole } from '~/lib/enums'
import { extractSdkResponseErrorMsg, isEmail, projectRoleTagColors, projectRoles } from '~/utils'
import {
computed,
extractSdkResponseErrorMsg,
isEmail,
onMounted,
projectRoles,
ref,
useClipboard,
useDashboard,
useNuxtApp,
useProject,
} from '#imports'
import type { User } from '~/lib'
import { ProjectRole } from '~/lib'
interface Props {
show: boolean
@ -22,6 +28,7 @@ interface Users {
}
const { show, selectedUser } = defineProps<Props>()
const emit = defineEmits(['closed', 'reload'])
const { project } = useProject()
@ -89,28 +96,21 @@ const saveUser = async () => {
})
usersData.invitationToken = res.invite_token
}
notification.success({
message: 'Successfully updated the user details',
})
message.success('Successfully updated the user details')
} catch (e: any) {
console.error(e)
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
const inviteUrl = $computed(() =>
usersData.invitationToken ? `${dashboardUrl}/user/authentication/signup/${usersData.invitationToken}` : null,
)
const inviteUrl = $computed(() => (usersData.invitationToken ? `${dashboardUrl}/signup/${usersData.invitationToken}` : null))
const copyUrl = async () => {
if (!inviteUrl) return
copy(inviteUrl)
notification.success({
message: 'Copied shareable base url to clipboard!',
})
await copy(inviteUrl)
message.success('Copied shareable base url to clipboard!')
$e('c:shared-base:copy-url')
}
@ -130,7 +130,7 @@ const clickInviteMore = () => {
<a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<template #icon>
<CloseIcon class="flex mx-auto" />
<MaterialSymbolsCloseRounded class="flex mx-auto" />
</template>
</a-button>
</div>
@ -139,7 +139,7 @@ const clickInviteMore = () => {
<template v-if="usersData.invitationToken">
<div class="flex flex-col mt-1 border-b-1 pb-5">
<div class="flex flex-row items-center pl-1.5 pb-1 h-[1.1rem]">
<MidAccountIcon />
<MdiAccountOutline />
<div class="text-xs ml-0.5 mt-0.5">Copy Invite Token</div>
</div>
@ -151,7 +151,7 @@ const clickInviteMore = () => {
</div>
<a-button type="text" class="!rounded-md -mt-0.5" @click="copyUrl">
<template #icon>
<ContentCopyIcon class="flex mx-auto text-green-700 h-[1rem]" />
<MdiContentCopy class="flex mx-auto text-green-700 h-[1rem]" />
</template>
</a-button>
</div>
@ -165,7 +165,7 @@ const clickInviteMore = () => {
<div class="flex flex-row justify-start mt-4 ml-2">
<a-button size="small" outlined @click="clickInviteMore">
<div class="flex flex-row justify-center items-center space-x-0.5">
<SendIcon class="flex mx-auto text-gray-600 h-[0.8rem]" />
<MaterialSymbolsSendOutline class="flex mx-auto text-gray-600 h-[0.8rem]" />
<div class="text-xs text-gray-600">{{ $t('activity.inviteMore') }}</div>
</div>
</a-button>
@ -174,7 +174,7 @@ const clickInviteMore = () => {
</template>
<div v-else class="flex flex-col pb-4">
<div class="flex flex-row items-center pl-2 pb-1 h-[1rem]">
<MidAccountIcon />
<MdiAccountOutline />
<div class="text-xs ml-0.5 mt-0.5">{{ selectedUser ? 'Edit User' : 'Invite Team' }}</div>
</div>
<div class="border-1 py-3 px-4 rounded-md mt-1">
@ -224,7 +224,7 @@ const clickInviteMore = () => {
<a-button type="primary" html-type="submit">
<div v-if="selectedUser">{{ $t('general.save') }}</div>
<div v-else class="flex flex-row justify-center items-center space-x-1.5">
<SendIcon class="flex h-[0.8rem]" />
<MaterialSymbolsSendOutline class="flex h-[0.8rem]" />
<div>{{ $t('activity.invite') }}</div>
</div>
</a-button>

51
packages/nc-gui-v2/components/template/Editor.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { Form, notification } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import { srcDestMappingColumns, tableColumns } from './utils'
import { computed, onMounted } from '#imports'
import { extractSdkResponseErrorMsg, fieldRequiredValidator, getUIDTIcon } from '~/utils'
@ -181,10 +181,7 @@ function missingRequiredColumnsValidation() {
(c.pk ? !c.ai && !c.cdf : !c.cdf && c.rqd) && !srcDestMapping.value.some((r) => r.destCn === c.title),
)
if (missingRequiredColumns.length) {
notification.error({
message: `Following columns are required : ${missingRequiredColumns.map((c) => c.title).join(', ')}`,
duration: 3,
})
message.error(`Following columns are required : ${missingRequiredColumns.map((c) => c.title).join(', ')}`)
return false
}
return true
@ -192,10 +189,7 @@ function missingRequiredColumnsValidation() {
function atLeastOneEnabledValidation() {
if (srcDestMapping.value.filter((v) => v.enabled === true).length === 0) {
notification.error({
message: 'At least one column has to be selected',
duration: 3,
})
message.error('At least one column has to be selected')
return false
}
return true
@ -210,18 +204,12 @@ function fieldsValidation(record: Record<string, any>) {
const tableName = meta?.value.title || ''
if (!record.destCn) {
notification.error({
message: `Cannot find the destination column for ${record.srcCn}`,
duration: 3,
})
message.error(`Cannot find the destination column for ${record.srcCn}`)
return false
}
if (srcDestMapping.value.filter((v) => v.destCn === record.destCn).length > 1) {
notification.error({
message: 'Duplicate mapping found, please remove one of the mapping',
duration: 3,
})
message.error('Duplicate mapping found, please remove one of the mapping')
return false
}
@ -234,10 +222,7 @@ function fieldsValidation(record: Record<string, any>) {
.slice(0, maxRowsToParse)
.some((r: Record<string, any>) => r[record.srcCn] === null || r[record.srcCn] === undefined || r[record.srcCn] === '')
) {
notification.error({
message: 'null value violates not-null constraint',
duration: 3,
})
message.error('null value violates not-null constraint')
}
}
@ -250,10 +235,7 @@ function fieldsValidation(record: Record<string, any>) {
(r: Record<string, any>) => r[record.sourceCn] !== null && r[record.srcCn] !== undefined && isNaN(+r[record.srcCn]),
)
) {
notification.error({
message: 'Source data contains some invalid numbers',
duration: 3,
})
message.error('Source data contains some invalid numbers')
return false
}
break
@ -280,10 +262,7 @@ function fieldsValidation(record: Record<string, any>) {
return false
})
) {
notification.error({
message: 'Source data contains some invalid boolean values',
duration: 3,
})
message.error('Source data contains some invalid boolean values')
return false
}
break
@ -340,15 +319,9 @@ async function importTemplate() {
// reload table
reloadHook.trigger()
notification.success({
message: 'Successfully imported table data',
duration: 3,
})
message.success('Successfully imported table data')
} catch (e: any) {
notification.error({
message: e.message,
duration: 3,
})
message.error(e.message)
} finally {
isImporting.value = false
}
@ -439,9 +412,7 @@ async function importTemplate() {
type: 'table',
})
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isImporting.value = false
}

2
packages/nc-gui-v2/components/virtual-cell/HasMany.vue

@ -65,7 +65,7 @@ const unlinkRef = async (rec: Record<string, any>) => {
<template v-if="!isForm">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="ch" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true"
>more...
</span>

8
packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Modal } from 'ant-design-vue'
import { Empty, Modal } from 'ant-design-vue'
import type { ColumnType } from 'nocodb-sdk'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel, watch } from '#imports'
import { ColumnInj, IsFormInj } from '~/context'
@ -64,9 +64,9 @@ const expandedFormRow = ref()
<MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<a-button type="primary" class="!text-xs" size="small" @click="emit('attachRecord')">
<a-button type="primary" ghost class="!text-xs" size="small" @click="emit('attachRecord')">
<div class="flex align-center gap-1">
<MdiLinkVariantRemove class="text-xs text-white" @click="unlinkRow(row)" />
<MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" />
Link to '{{ meta.title }}'
</div>
</a-button>
@ -113,7 +113,7 @@ const expandedFormRow = ref()
show-less-items
/>
</template>
<a-empty v-else class="my-10" />
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
</div>
<SmartsheetExpandedForm

3
packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue

@ -1,6 +1,7 @@
<script lang="ts" setup>
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { Empty } from 'ant-design-vue'
import { computed, useLTARStoreOrThrow, useSmartsheetRowStoreOrThrow, useVModel } from '#imports'
import { ColumnInj } from '~/context'
@ -115,7 +116,7 @@ const newRowState = computed(() => {
show-less-items
/>
</template>
<a-empty v-else class="my-10" />
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<SmartsheetExpandedForm
v-if="expandedFormDlg"

18
packages/nc-gui-v2/components/webhook/Editor.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Form, notification } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import { MetaInj } from '~/context'
import { extractSdkResponseErrorMsg, fieldRequiredValidator } from '~/utils'
import { inject, reactive, useApi, useNuxtApp } from '#imports'
@ -302,9 +302,7 @@ async function loadPluginList() {
hook.eventOperation = `${hook.event} ${hook.operation}`
}
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -313,9 +311,7 @@ async function saveHooks() {
try {
await validate()
} catch (_: any) {
notification.error({
message: 'Invalid Form',
})
message.error('Invalid Form')
loading.value = false
@ -353,13 +349,9 @@ async function saveHooks() {
// });
// }
notification.success({
message: 'Webhook details updated successfully',
})
message.success('Webhook details updated successfully')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
} finally {
loading.value = false
}

14
packages/nc-gui-v2/components/webhook/List.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { MetaInj } from '~/context'
import { inject, onMounted, ref, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
@ -20,9 +20,7 @@ async function loadHooksList() {
return hook
})
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -34,16 +32,12 @@ async function deleteHook(item: Record<string, any>, index: number) {
} else {
hooks.value.splice(index, 1)
}
notification.success({
message: 'Hook deleted successfully',
})
message.success('Hook deleted successfully')
if (!hooks.value.length) {
hooks.value = []
}
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
$e('a:webhook:delete')

10
packages/nc-gui-v2/components/webhook/Test.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { onMounted } from '@vue/runtime-core'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { MetaInj } from '~/context'
import { extractSdkResponseErrorMsg } from '~/utils'
@ -39,13 +39,9 @@ async function testWebhook() {
payload: sampleData.value,
})
notification.success({
message: 'Webhook tested successfully',
})
message.success('Webhook tested successfully')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}

15
packages/nc-gui-v2/composables/useApi/index.ts

@ -1,9 +1,9 @@
import type { AxiosError, AxiosResponse } from 'axios'
import { Api } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { addAxiosInterceptors } from './interceptors'
import type { CreateApiOptions, UseApiProps, UseApiReturn } from './types'
import { createEventHook, ref, unref, useCounter, useGlobal, useNuxtApp } from '#imports'
import { addAxiosInterceptors } from './interceptors'
import { createEventHook, ref, unref, useCounter, useNuxtApp } from '#imports'
export function createApiInstance<SecurityDataType = any>(options: CreateApiOptions = {}): Api<SecurityDataType> {
return addAxiosInterceptors(
@ -33,8 +33,6 @@ export function useApi<Data = any, RequestConfig = any>({
apiOptions,
axiosConfig,
}: UseApiProps<Data> = {}): UseApiReturn<Data, RequestConfig> {
const state = useGlobal()
/**
* Local state of running requests, do not confuse with global state of running requests
* This state is only counting requests made by this instance of `useApi` and not by other instances.
@ -54,11 +52,10 @@ export function useApi<Data = any, RequestConfig = any>({
const responseHook = createEventHook<AxiosResponse<Data, RequestConfig>>()
/** global api instance */
const $api = useNuxtApp().$api
const nuxtApp = useNuxtApp()
/** api instance - with interceptors for token refresh already bound */
const api = useGlobalInstance && !!$api ? $api : createApiInstance(apiOptions)
const api = useGlobalInstance && !!nuxtApp.$api ? nuxtApp.$api : createApiInstance(apiOptions)
/** set loading to true and increment local and global request counter */
function onRequestStart() {
@ -68,7 +65,7 @@ export function useApi<Data = any, RequestConfig = any>({
inc()
/** global count */
state.runningRequests.inc()
nuxtApp.$state.runningRequests.inc()
}
/** decrement local and global request counter and check if we can stop loading */
@ -76,7 +73,7 @@ export function useApi<Data = any, RequestConfig = any>({
/** local count */
dec()
/** global count */
state.runningRequests.dec()
nuxtApp.$state.runningRequests.dec()
/** try to stop loading */
stopLoading()

18
packages/nc-gui-v2/composables/useColumnCreateStore.ts

@ -1,5 +1,5 @@
import { createInjectionState } from '@vueuse/core'
import { Form, notification } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
@ -177,9 +177,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
console.log(formState, validators)
if (!(await validate())) return
} catch (e) {
notification.error({
message: 'Form validation failed',
})
message.error('Form validation failed')
return
}
@ -188,9 +186,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
// formState.value.title = formState.value.column_name
if (column?.value) {
await $api.dbTableColumn.update(column?.value?.id as string, formState.value)
notification.success({
message: 'Column updated',
})
message.success('Column updated')
} else {
// todo : set additional meta for auto generated string id
if (formState.value.uidt === UITypes.ID) {
@ -208,15 +204,11 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
getMeta(formState.value.childId, true).then(() => {})
}
notification.success({
message: 'Column created',
})
message.success('Column created')
}
onSuccess?.()
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}

9
packages/nc-gui-v2/composables/useExpandedFormStore.ts

@ -1,7 +1,7 @@
import { UITypes } from 'nocodb-sdk'
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { message, notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import dayjs from 'dayjs'
import { useApi, useInjectionState, useProject, useProvideSmartsheetRowStore } from '#imports'
import { NOCO } from '~/lib'
@ -161,14 +161,11 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
// this.$emit('input', this.localState);
// this.$emit('update:isNew', false);
notification.success({
message: `${primaryValue.value || 'Row'} updated successfully.`,
// position: 'bottom-right',
})
message.success(`${primaryValue.value || 'Row'} updated successfully.`)
changedColumns.value = new Set()
} catch (e: any) {
notification.error({ message: `Failed to update row`, description: await extractSdkResponseErrorMsg(e) })
message.error(`Failed to update row: ${await extractSdkResponseErrorMsg(e)}`)
}
$e('a:row-expand:add')
return data

9
packages/nc-gui-v2/composables/useGlobal/actions.ts

@ -1,4 +1,4 @@
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Actions, State } from './types'
import { useNuxtApp } from '#imports'
@ -40,12 +40,7 @@ export function useGlobalActions(state: State): Actions {
}
})
.catch((err) => {
notification.error({
// todo: add translation
message: err.message || 'You have been signed out.',
})
console.error(err)
message.error(err.message || 'You have been signed out.')
signOut()
})
}

12
packages/nc-gui-v2/composables/useGlobal/types.ts

@ -13,6 +13,18 @@ export interface FeedbackForm {
export interface AppInfo {
ncSiteUrl: string
authType: 'jwt' | 'masterKey' | 'none'
connectToExternalDB: boolean
defaultLimit: number
firstUser: boolean
githubAuthEnabled: boolean
googleAuthEnabled: boolean
ncMin: boolean
oneClick: boolean
projectHasAdmin: boolean
teleEnabled: boolean
type: string
version: string
}
export interface StoredState {

27
packages/nc-gui-v2/composables/useLTARStore.ts

@ -1,6 +1,6 @@
import type { ColumnType, LinkToAnotherRecordType, PaginatedType, TableType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { Modal, notification } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import { useInjectionState, useMetas, useProject } from '#imports'
import { NOCO } from '~/lib'
import type { Row } from '~/composables'
@ -104,10 +104,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
)
}
} catch (e: any) {
notification.error({
message: 'Failed to load list',
description: await extractSdkResponseErrorMsg(e),
})
message.error(`Failed to load list: ${await extractSdkResponseErrorMsg(e)}`)
}
}
@ -129,10 +126,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
},
)
} catch (e: any) {
notification.error({
message: 'Failed to load children list',
description: await extractSdkResponseErrorMsg(e),
})
message.error(`Failed to load children list: ${await extractSdkResponseErrorMsg(e)}`)
}
}
@ -147,10 +141,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
reloadData?.()
await loadChildrenList()
} catch (e: any) {
notification.error({
message: 'Delete failed',
description: await extractSdkResponseErrorMsg(e),
})
message.error(`Delete failed: ${await extractSdkResponseErrorMsg(e)}`)
}
},
})
@ -182,10 +173,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
getRelatedTableRowId(row) as string,
)
} catch (e: any) {
notification.error({
message: 'Unlink failed',
description: await extractSdkResponseErrorMsg(e),
})
message.error(`Unlink failed: ${await extractSdkResponseErrorMsg(e)}`)
}
reloadData?.()
// todo: reload table data and children list
@ -221,10 +209,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
)
await loadChildrenList()
} catch (e: any) {
notification.error({
message: 'Linking failed',
description: await extractSdkResponseErrorMsg(e),
})
message.error(`Linking failed: ${await extractSdkResponseErrorMsg(e)}`)
}
// todo: reload table data and child list

7
packages/nc-gui-v2/composables/useSmartsheetRowStore.ts

@ -1,4 +1,4 @@
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { UITypes } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType, RelationTypes, TableType } from 'nocodb-sdk'
import type { Ref } from 'vue'
@ -52,10 +52,7 @@ const [useProvideSmartsheetRowStore, useSmartsheetRowStore] = useInjectionState(
relatedRowId,
)
} catch (e: any) {
notification.error({
message: 'Linking failed',
description: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}

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

@ -1,4 +1,4 @@
import { Modal, notification } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { useProject } from './useProject'
@ -58,7 +58,7 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
$e('c:table:delete')
// 'Click Submit to Delete The table'
Modal.confirm({
title: `Click Yes to Delete The table : ${table.title}`,
title: `Do you want to delete the table : ${table.title}`,
okText: 'Yes',
okType: 'danger',
cancelText: 'No',
@ -76,13 +76,13 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
return `${i + 1}. ${c.title} is a LinkToAnotherRecord of ${(refMeta && refMeta.title) || c.title}`
}),
)
notification.info({
message: h('div', {
message.info(
h('div', {
innerHTML: `<div style="padding:10px 4px">Unable to delete tables because of the following.
<br><br>${refColMsgs.join('<br>')}<br><br>
Delete them & try again</div>`,
}),
})
)
return
}
@ -97,14 +97,10 @@ export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
await loadTables()
removeMeta(table.id as string)
notification.info({
message: `Deleted table ${table.title} successfully`,
})
message.info(`Deleted table ${table.title} successfully`)
$e('a:table:delete')
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
},
})

43
packages/nc-gui-v2/composables/useViewData.ts

@ -1,6 +1,6 @@
import type { Api, ColumnType, FormType, GalleryType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue'
import { notification } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { useProject } from '#imports'
import { NOCO } from '~/lib'
@ -135,10 +135,7 @@ export function useViewData(
})
syncCount()
} catch (error: any) {
notification.error({
message: 'Row insert failed',
description: await extractSdkResponseErrorMsg(error),
})
message.error(await extractSdkResponseErrorMsg(error))
}
}
@ -179,11 +176,8 @@ export function useViewData(
})
.then(() => {})
*/
} catch (error: any) {
notification.error({
message: 'Row update failed',
description: await extractSdkResponseErrorMsg(error),
})
} catch (e: any) {
message.error(`Row update failed ${await extractSdkResponseErrorMsg(e)}`)
}
}
@ -221,14 +215,13 @@ export function useViewData(
)
if (res.message) {
notification.info({
message: 'Row delete failed',
description: h('div', {
message.info(
`Row delete failed: ${h('div', {
innerHTML: `<div style="padding:10px 4px">Unable to delete row with ID ${id} because of the following:
<br><br>${res.message.join('<br>')}<br><br>
Clear the data first & try again</div>`,
}),
})
})}`,
)
return false
}
return true
@ -251,10 +244,7 @@ export function useViewData(
formattedData.value.splice(rowIndex, 1)
syncCount()
} catch (e: any) {
notification.error({
message: 'Failed to delete row',
description: await extractSdkResponseErrorMsg(e),
})
message.error(`Failed to delete row: ${await extractSdkResponseErrorMsg(e)}`)
}
}
@ -279,10 +269,7 @@ export function useViewData(
}
formattedData.value.splice(row, 1)
} catch (e: any) {
return notification.error({
message: 'Failed to delete row',
description: await extractSdkResponseErrorMsg(e),
})
return message.error(`Failed to delete row: ${await extractSdkResponseErrorMsg(e)}`)
}
}
syncCount()
@ -316,10 +303,7 @@ export function useViewData(
}))
.sort((a: Record<string, any>, b: Record<string, any>) => a.order - b.order) as Record<string, any>
} catch (e: any) {
return notification.error({
message: 'Failed to set form data',
description: await extractSdkResponseErrorMsg(e),
})
return message.error(`Failed to set form data: ${await extractSdkResponseErrorMsg(e)}`)
}
}
@ -328,10 +312,7 @@ export function useViewData(
if (!viewMeta?.value?.id || !view) return
await $api.dbView.formUpdate(viewMeta.value.id, view)
} catch (e: any) {
return notification.error({
message: 'Failed to update form view',
description: await extractSdkResponseErrorMsg(e),
})
return message.error(`Failed to update form view: ${await extractSdkResponseErrorMsg(e)}`)
}
}

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

@ -19,9 +19,12 @@ const logout = () => {
<div id="nc-sidebar-left" />
<a-layout class="!flex-col">
<a-layout-header class="flex !bg-primary items-center text-white pl-1 pr-4 shadow-lg">
<Transition name="layout">
<a-layout-header v-if="signedIn" class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg">
<div
v-if="route.name === 'index'"
v-if="
route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'
"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105"
@click="navigateTo('/')"
>
@ -76,8 +79,19 @@ const logout = () => {
</a-dropdown>
</template>
</a-layout-header>
</Transition>
<div class="w-full overflow-hidden" style="height: calc(100% - var(--header-height))">
<a-tooltip>
<template #title> Switch language </template>
<Transition name="layout">
<div v-if="!signedIn" class="nc-lang-btn">
<GeneralLanguage />
</div>
</Transition>
</a-tooltip>
<div class="w-full h-full overflow-hidden">
<slot />
</div>
</a-layout>
@ -92,4 +106,22 @@ const logout = () => {
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
.nc-lang-btn {
@apply color-transition flex items-center justify-center fixed bottom-10 right-10 z-99 w-12 h-12 rounded-full shadow-md shadow-gray-500 p-2 !bg-primary text-white active:(ring ring-pink-500) hover:(ring ring-pink-500);
&::after {
@apply rounded-full 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-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}
</style>

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

@ -6,6 +6,8 @@ export interface User {
firstname: string | null
lastname: string | null
roles: Roles
invite_token?: string
project_id?: string
}
export type Roles = Record<Role, boolean>

73
packages/nc-gui-v2/pages/forgot-password.vue

@ -1,14 +1,7 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { definePageMeta } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { useNuxtApp } from '#app'
import { isEmail } from '~/utils/validation'
import MdiLogin from '~icons/mdi/login'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import ClaritySuccessLine from '~icons/clarity/success-line'
import { definePageMeta, extractSdkResponseErrorMsg, isEmail, reactive, ref, useApi, useI18n } from '#imports'
const { $api } = $(useNuxtApp())
const { api, isLoading } = useApi()
const { t } = useI18n()
@ -18,6 +11,7 @@ definePageMeta({
})
let error = $ref<string | null>(null)
let success = $ref(false)
const formValidator = ref()
@ -43,13 +37,14 @@ const formRules = {
],
}
const resetPassword = async () => {
const valid = formValidator.value.validate()
if (!valid) return
async function resetPassword() {
if (!formValidator.value.validate()) return
resetError()
error = null
try {
await $api.auth.passwordForgot(form)
await api.auth.passwordForgot(form)
success = true
} catch (e: any) {
// todo: errors should not expose what was wrong (i.e. do not show "Password is wrong" messages)
@ -57,10 +52,8 @@ const resetPassword = async () => {
}
}
const resetError = () => {
if (error) {
error = null
}
function resetError() {
if (error) error = null
}
</script>
@ -70,34 +63,41 @@ const resetError = () => {
ref="formValidator"
layout="vertical"
:model="form"
class="forgot-password h-full min-h-[600px] flex justify-center items-center"
class="bg-primary/5 forgot-password h-full min-h-[600px] flex justify-center items-center"
@finish="resetPassword"
>
<div class="h-full w-full flex flex-col flex-wrap justify-center items-center">
<div
class="color-transition bg-white dark:(!bg-gray-900 !text-white) 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)"
>
<general-noco-icon />
<general-noco-icon
class="color-transition hover:(ring ring-pink-500)"
:class="[isLoading ? 'animated-bg-gradient' : '']"
/>
<div class="self-center flex flex-col justify-center items-center text-center gap-4">
<div class="self-center flex flex-col justify-center items-center text-center gap-2">
<h1 class="prose-2xl font-bold my-4 w-full">{{ $t('title.resetPassword') }}</h1>
<template v-if="!success">
<p class="prose-sm">{{ $t('msg.info.passwordRecovery.message_1') }}</p>
<p class="prose-sm mb-4">{{ $t('msg.info.passwordRecovery.message_2') }}</p>
<div class="prose-sm">{{ $t('msg.info.passwordRecovery.message_1') }}</div>
<div class="prose-sm mb-4">{{ $t('msg.info.passwordRecovery.message_2') }}</div>
</template>
<template v-else>
<p class="prose-sm text-success flex items-center leading-8 gap-2">
<div class="prose-sm text-success flex items-center leading-8 gap-2">
{{ $t('msg.info.passwordRecovery.success') }} <ClaritySuccessLine />
</p>
</div>
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</template>
</div>
<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 v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div style="flex: 0 0 auto" class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
@ -105,10 +105,11 @@ const resetError = () => {
<a-input v-model:value="form.email" size="large" :placeholder="$t('labels.email')" @focus="resetError" />
</a-form-item>
<div class="self-center flex flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full">
<div class="self-center flex flex-col gap-4 items-center justify-center w-full">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiLogin /> {{ $t('activity.sendEmail') }}</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
@ -128,7 +129,21 @@ const resetError = () => {
}
.submit {
@apply ml-1 border border-gray-300 rounded-lg p-4 bg-gray-100/50 text-white bg-primary hover:bg-primary/75 dark:(!bg-secondary/75 hover:!bg-secondary/50);
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
&::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-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}
}
</style>

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

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Modal, notification } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { navigateTo } from '#app'
import { computed, onMounted, ref, useApi, useNuxtApp, useSidebar } from '#imports'
@ -48,9 +48,7 @@ const deleteProject = (project: ProjectType) => {
$e('a:project:delete')
return projects.value?.splice(projects.value.indexOf(project), 1)
} catch (e: any) {
return notification.error({
message: await extractSdkResponseErrorMsg(e),
})
return message.error(await extractSdkResponseErrorMsg(e))
}
},
})

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

@ -1,8 +1,9 @@
<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 } from '#imports'
import { reactive, ref, useApi, useGlobal } from '#imports'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import MdiKeyChange from '~icons/mdi/key-change'
@ -10,6 +11,8 @@ const { api } = useApi()
const { t } = useI18n()
const { signOut } = useGlobal()
const formValidator = ref()
let error = $ref<string | null>(null)
@ -51,11 +54,13 @@ const passwordChange = async () => {
error = null
try {
const { msg } = await api.auth.passwordChange({
await api.auth.passwordChange({
currentPassword: form.currentPassword,
newPassword: form.password,
})
message.success(msg)
message.success('Password changed successfully. Please login again.')
signOut()
navigateTo('/signin')
} catch (e: any) {
error = await extractSdkResponseErrorMsg(e)
}

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

@ -56,7 +56,7 @@ await loadTables()
collapsible
theme="light"
>
<div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 gap-2">
<div style="height: var(--header-height)" class="flex items-center !bg-primary text-white px-1 pl-5 gap-2">
<div
v-if="isOpen"
class="w-[40px] min-w-[40px] transition-all duration-200 p-1 cursor-pointer transform hover:scale-105"
@ -65,7 +65,7 @@ await loadTables()
<img alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<a-dropdown v-model:visible="dropdownOpen" :trigger="['click']">
<a-dropdown v-model:visible="dropdownOpen" :trigger="['click']" placement="bottom">
<div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']"

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

@ -46,7 +46,7 @@ const icon = (tab: TabItem) => {
<div class="h-full w-full nc-container pt-[9px]">
<div class="h-full w-full flex flex-col">
<div>
<a-tabs v-model:activeKey="activeTabIndex" type="editable-card" @edit="closeTab">
<a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs" type="editable-card" @edit="closeTab">
<a-tab-pane v-for="(tab, i) in tabs" :key="i">
<template #tab>
<div class="flex align-center gap-2">
@ -60,7 +60,7 @@ const icon = (tab: TabItem) => {
<a-menu v-if="isUIAllowed('addOrImport')" v-model:selectedKeys="currentMenu" class="border-0" mode="horizontal">
<a-sub-menu key="addORImport">
<template #title>
<div class="text-sm flex items-center gap-2">
<div class="text-sm flex items-center gap-2 pt-[8px] pb-3">
<MdiPlusIcon />
Add / Import
</div>
@ -154,14 +154,29 @@ const icon = (tab: TabItem) => {
</div>
</template>
<style scoped>
<style scoped lang="scss">
.nc-container {
height: calc(100% - var(--header-height));
flex: 1 1 100%;
}
:deep(.ant-tabs-nav) {
:deep(.nc-root-tabs) {
& > .ant-tabs-nav {
@apply !mb-0;
& > .ant-tabs-nav-wrap > .ant-tabs-nav-list {
& > .ant-tabs-nav-add {
@apply !hidden;
}
& > .ant-tabs-tab-active {
@apply font-weight-medium;
}
& > .ant-tabs-tab:not(.ant-tabs-tab-active) {
@apply bg-gray-100;
}
}
}
}
:deep(.ant-menu-item-selected) {
@ -173,16 +188,4 @@ const icon = (tab: TabItem) => {
:deep(.ant-menu-submenu::after) {
@apply !border-none;
}
:deep(.ant-tabs-nav-add) {
@apply !hidden;
}
:deep(.ant-tabs-tab-active) {
@apply font-weight-medium;
}
:deep(.ant-tabs-tab:not(.ant-tabs-tab-active)) {
@apply bg-gray-100;
}
</style>

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

@ -3,7 +3,7 @@ import { onMounted } from '@vue/runtime-core'
import type { Form } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { ref } from 'vue'
import { notification } from 'ant-design-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'
@ -32,9 +32,7 @@ const getProject = async () => {
const result: ProjectType = await api.project.read(route.params.id as string)
formState.title = result.title as string
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -44,9 +42,7 @@ const renameProject = async () => {
navigateTo(`/nc/${route.params.id}`)
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import { onMounted } from '@vue/runtime-core'
import { Form, Modal, notification } from 'ant-design-vue'
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'
@ -142,9 +142,7 @@ const createProject = async () => {
$e('a:project:create:extdb')
await navigateTo(`/nc/${result.id}`)
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
loading.value = false
}
@ -185,16 +183,12 @@ const testConnection = async () => {
})
} else {
testSuccess.value = false
notification.error({
message: `${t('msg.error.dbConnectionFailed')} ${result.message}`,
})
message.error(`${t('msg.error.dbConnectionFailed')} ${result.message}`)
}
}
} catch (e: any) {
testSuccess.value = false
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -219,11 +213,11 @@ onMounted(() => {
</script>
<template>
<a-card
class="max-w-[600px] mx-auto !mt-15 !mb-5 !shadow-md"
:title="$t('activity.createProject')"
:head-style="{ textAlign: 'center', fontWeight: '700' }"
>
<a-card class="max-w-[600px] mx-auto !mt-100px !mb-5 !shadow-md">
<GeneralNocoIcon />
<h3 class="text-3xl text-center font-semibold mt-8 mb-4">{{ $t('activity.createProject') }}</h3>
<a-form
ref="form"
:model="formState"

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

@ -1,7 +1,7 @@
<script lang="ts" setup>
import { onMounted } from '@vue/runtime-core'
import type { Form } from 'ant-design-vue'
import { notification } 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'
@ -35,9 +35,7 @@ const createProject = async () => {
await navigateTo(`/nc/${result.id}`)
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
}
@ -58,7 +56,9 @@ onMounted(async () => {
<template>
<a-card :loading="isLoading" class="w-[500px] mx-auto !mt-100px shadow-md">
<h3 class="text-3xl text-center font-semibold mb-2">{{ $t('activity.createProject') }}</h3>
<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">

6
packages/nc-gui-v2/pages/projects/index.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { Modal, notification } from 'ant-design-vue'
import { Modal, message } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { navigateTo } from '#app'
import { extractSdkResponseErrorMsg } from '~/utils'
@ -49,9 +49,7 @@ const deleteProject = (project: ProjectType) => {
await $api.project.delete(project.id as string)
projects.splice(projects.indexOf(project), 1)
} catch (e: any) {
notification.error({
message: await extractSdkResponseErrorMsg(e),
})
message.error(await extractSdkResponseErrorMsg(e))
}
},
})

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

@ -1,14 +1,21 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import type { RuleObject } from 'ant-design-vue/es/form'
import { definePageMeta, useSidebar } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { navigateTo, useNuxtApp } from '#app'
import { isEmail } from '~/utils/validation'
import MdiLogin from '~icons/mdi/login'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
const { $api, $state } = $(useNuxtApp())
import {
definePageMeta,
extractSdkResponseErrorMsg,
isEmail,
navigateTo,
reactive,
ref,
useApi,
useGlobal,
useI18n,
useSidebar,
} from '#imports'
const { signIn: _signIn } = useGlobal()
const { api, isLoading } = useApi()
const { t } = useI18n()
@ -49,25 +56,25 @@ const formRules: Record<string, RuleObject[]> = {
],
}
const signIn = async () => {
const valid = formValidator.value.validate()
if (!valid) return
async function signIn() {
if (!formValidator.value.validate()) return
resetError()
error = null
try {
const { token } = await $api.auth.signin(form)
$state.signIn(token!)
api.auth
.signin(form)
.then(async ({ token }) => {
_signIn(token!)
await navigateTo('/')
} catch (e: any) {
})
.catch(async (err) => {
// todo: errors should not expose what was wrong (i.e. do not show "Password is wrong" messages)
error = await extractSdkResponseErrorMsg(e)
}
error = await extractSdkResponseErrorMsg(err)
})
}
const resetError = () => {
if (error) {
error = null
}
function resetError() {
if (error) error = null
}
</script>
@ -77,20 +84,26 @@ const resetError = () => {
ref="formValidator"
:model="form"
layout="vertical"
class="signin h-[calc(100%_+_90px)] min-h-[600px] flex justify-center items-center nc-form-signin"
class="bg-primary/5 signin h-full min-h-[600px] flex justify-center items-center nc-form-signin"
@finish="signIn"
>
<div class="h-full w-full flex flex-col flex-wrap items-center pt-[100px]">
<div
class="bg-white dark:(!bg-gray-900 !text-white) 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)"
>
<general-noco-icon />
<general-noco-icon
class="!rounded-full color-transition hover:(ring ring-pink-500)"
:class="[isLoading ? 'animated-bg-gradient' : '']"
/>
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signIn') }}</h1>
<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 v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div style="flex: 0 0 auto" class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
@ -108,18 +121,17 @@ const resetError = () => {
/>
</a-form-item>
<div class="hidden md:block self-end mx-8">
<div class="hidden md:block self-end">
<nuxt-link class="prose-sm" to="/forgot-password">
{{ $t('msg.info.signUp.forgotPassword') }}
</nuxt-link>
</div>
<div
class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full"
>
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4 justify-center">
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MdiLogin /> {{ $t('general.signIn') }}</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.dontHaveAccount') }}
<nuxt-link to="/signup">{{ $t('general.signUp') }}</nuxt-link>
@ -155,7 +167,21 @@ const resetError = () => {
}
.submit {
@apply ml-1 border border-gray-300 rounded-lg p-4 bg-gray-100/50 text-white bg-primary hover:bg-primary/75 dark:(!bg-secondary/75 hover:!bg-secondary/50);
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
&::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-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}
}
</style>

143
packages/nc-gui-v2/pages/signup.vue

@ -1,143 +0,0 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { navigateTo } from '#app'
import { isEmail } from '~/utils/validation'
import MaterialSymbolsWarning from '~icons/material-symbols/warning'
import MaterialSymbolsRocketLaunchOutline from '~icons/material-symbols/rocket-launch-outline'
const { $api, $state } = useNuxtApp()
const { t } = useI18n()
definePageMeta({
requiresAuth: false,
})
const formValidator = ref()
let error = $ref<string | null>(null)
const form = reactive({
email: '',
password: '',
})
const formRules = {
email: [
// E-mail is required
{ required: true, message: t('msg.error.signUpRules.emailReqd') },
// E-mail must be valid format
{
validator: (_: unknown, v: string) => {
return new Promise((resolve, reject) => {
if (isEmail(v)) return resolve(true)
reject(new Error(t('msg.error.signUpRules.emailInvalid')))
})
},
message: t('msg.error.signUpRules.emailInvalid'),
},
],
password: [
// Password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
{ min: 8, message: t('msg.error.signUpRules.passwdLength') },
],
}
const signUp = async () => {
const valid = formValidator.value.validate()
if (!valid) return
error = null
try {
const { token } = await $api.auth.signup(form)
$state.signIn(token!)
await navigateTo('/')
} catch (e: any) {
error = await extractSdkResponseErrorMsg(e)
}
}
const resetError = () => {
if (error) {
error = null
}
}
</script>
<template>
<NuxtLayout>
<a-form
ref="formValidator"
:model="form"
layout="vertical"
class="signup h-[calc(100%_+_90px)] min-h-[600px] flex justify-center items-center nc-form-signup"
@finish="signUp"
>
<div class="h-full w-full flex flex-col flex-wrap pt-[100px]">
<div
class="bg-white dark:(!bg-gray-900 !text-white) 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)"
>
<general-noco-icon />
<h1 class="prose-2xl font-bold self-center my-4">{{ $t('general.signUp') }}</h1>
<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>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input v-model:value="form.email" size="large" :placeholder="$t('labels.email')" @focus="resetError" />
</a-form-item>
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('labels.password')"
@focus="resetError"
/>
</a-form-item>
<div
class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center w-full"
>
<button class="submit" type="submit">
<span class="flex items-center gap-2"><MaterialSymbolsRocketLaunchOutline /> {{ $t('general.signUp') }}</span>
</button>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</div>
</div>
</a-form>
</NuxtLayout>
</template>
<style lang="scss">
.signup {
.ant-input-affix-wrapper,
.ant-input {
@apply dark:(bg-gray-700 !text-white) !appearance-none my-1 border-1 border-solid border-primary/50 rounded;
}
.password {
input {
@apply !border-none;
}
.ant-input-password-icon {
@apply dark:!text-white;
}
}
.submit {
@apply ml-1 border border-gray-300 rounded-lg p-4 bg-gray-100/50 text-white bg-primary hover:bg-primary/75 dark:(!bg-secondary/75 hover:!bg-secondary/50);
}
}
</style>

206
packages/nc-gui-v2/pages/signup/[[token]].vue

@ -0,0 +1,206 @@
<script setup lang="ts">
import {
definePageMeta,
extractSdkResponseErrorMsg,
isEmail,
navigateTo,
reactive,
ref,
useApi,
useGlobal,
useI18n,
useRoute,
} from '#imports'
definePageMeta({
requiresAuth: false,
})
const route = useRoute()
const { appInfo, signIn } = useGlobal()
const { api, isLoading } = useApi()
const { t } = useI18n()
const formValidator = ref()
let error = $ref<string | null>(null)
const subscribe = ref(false)
const form = reactive({
email: '',
password: '',
})
const formRules = {
email: [
// E-mail is required
{ required: true, message: t('msg.error.signUpRules.emailReqd') },
// E-mail must be valid format
{
validator: (_: unknown, v: string) => {
return new Promise((resolve, reject) => {
if (isEmail(v)) return resolve(true)
reject(new Error(t('msg.error.signUpRules.emailInvalid')))
})
},
message: t('msg.error.signUpRules.emailInvalid'),
},
],
password: [
// Password is required
{ required: true, message: t('msg.error.signUpRules.passwdRequired') },
{ min: 8, message: t('msg.error.signUpRules.passwdLength') },
],
}
async function signUp() {
if (!formValidator.value.validate()) return
resetError()
const data: any = {
...form,
token: route.params.token,
}
if (subscribe.value) {
data.ignore_subscribe = !subscribe.value
}
api.auth
.signup(data)
.then(async ({ token }) => {
signIn(token!)
await navigateTo('/')
})
.catch(async (err) => {
error = await extractSdkResponseErrorMsg(err)
})
}
function resetError() {
if (error) error = null
}
</script>
<template>
<NuxtLayout>
<div class="bg-primary/5 signup h-full min-h-[600px] flex flex-col justify-center items-center nc-form-signup">
<div
class="bg-white dark:(!bg-gray-900 !text-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)"
>
<general-noco-icon
class="color-transition hover:(ring ring-pink-500)"
:class="[isLoading ? 'animated-bg-gradient' : '']"
/>
<h1 class="prose-2xl font-bold self-center my-4">
{{ $t('general.signUp') }}
{{ $route.query.redirect_to === '/referral' ? '& REFER' : '' }}
{{ $route.query.redirect_to === '/pricing' ? '& BUY' : '' }}
</h1>
<h2 v-if="appInfo.firstUser" class="prose !text-primary font-semibold self-center my-4">
{{ $t('msg.info.signUp.superAdmin') }}
</h2>
<a-form ref="formValidator" :model="form" layout="vertical" no-style @finish="signUp">
<Transition name="layout">
<div v-if="error" class="self-center mb-4 bg-red-500 text-white rounded-lg w-3/4 mx-auto p-1">
<div class="flex items-center gap-2 justify-center">
<MaterialSymbolsWarning />
<div style="flex: 0 0 auto" class="break-words">{{ error }}</div>
</div>
</div>
</Transition>
<a-form-item :label="$t('labels.email')" name="email" :rules="formRules.email">
<a-input v-model:value="form.email" size="large" :placeholder="$t('labels.email')" @focus="resetError" />
</a-form-item>
<a-form-item :label="$t('labels.password')" name="password" :rules="formRules.password">
<a-input-password
v-model:value="form.password"
size="large"
class="password"
:placeholder="$t('labels.password')"
@focus="resetError"
/>
</a-form-item>
<div class="self-center flex flex-col flex-wrap gap-4 items-center mt-4">
<button class="submit" type="submit">
<span class="flex items-center gap-2">
<MaterialSymbolsRocketLaunchOutline />
{{ $t('general.signUp') }}
</span>
</button>
<div class="flex items-center gap-2">
<a-switch
v-model:checked="subscribe"
size="small"
class="my-1 hover:(ring ring-pink-500) focus:(!ring !ring-pink-500)"
/>
<div class="prose-xs text-gray-500">Subscribe to our weekly newsletter</div>
</div>
<div class="text-end prose-sm">
{{ $t('msg.info.signUp.alreadyHaveAccount') }}
<nuxt-link to="/signin">{{ $t('general.signIn') }}</nuxt-link>
</div>
</div>
</a-form>
</div>
<div class="prose-sm mt-4 text-gray-500">
By signing up, you agree to the
<a class="prose-sm text-pink-500 underline" target="_blank" href="https://nocodb.com/policy-nocodb">Terms of Service</a>
</div>
</div>
</NuxtLayout>
</template>
<style lang="scss">
.signup {
.ant-input-affix-wrapper,
.ant-input {
@apply dark:(bg-gray-700 !text-white) !appearance-none my-1 border-1 border-solid border-primary/50 rounded;
}
.password {
input {
@apply !border-none;
}
.ant-input-password-icon {
@apply dark:!text-white;
}
}
.submit {
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary;
&::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-pink-500;
}
&:active::after {
@apply ring ring-pink-500;
}
}
}
</style>

5
packages/nocodb/src/lib/meta/api/projectApis.ts

@ -50,6 +50,11 @@ export async function projectUpdate(
const data: any = {
title: DOMPurify.sanitize(req?.body?.title),
};
if (await Project.getByTitle(data.title)) {
NcError.badRequest('Project title already in use');
}
const result = await Project.update(req.params.projectId, data);
Tele.emit('evt', { evt_type: 'project:update' });
res.json(result);

Loading…
Cancel
Save