Browse Source

Merge branch 'develop' into feat/kanban-view

pull/3563/head
Wing-Kam Wong 2 years ago
parent
commit
842712e441
  1. 4
      packages/nc-gui/app.vue
  2. 122
      packages/nc-gui/assets/style/fonts.css
  3. 19
      packages/nc-gui/components.d.ts
  4. 29
      packages/nc-gui/components/api-client/Headers.vue
  5. 25
      packages/nc-gui/components/api-client/Params.vue
  6. 5
      packages/nc-gui/components/cell/Currency.vue
  7. 4
      packages/nc-gui/components/cell/DateTimePicker.vue
  8. 3
      packages/nc-gui/components/cell/Decimal.vue
  9. 14
      packages/nc-gui/components/cell/Duration.vue
  10. 5
      packages/nc-gui/components/cell/Email.vue
  11. 3
      packages/nc-gui/components/cell/Float.vue
  12. 3
      packages/nc-gui/components/cell/Integer.vue
  13. 7
      packages/nc-gui/components/cell/Json.vue
  14. 1
      packages/nc-gui/components/cell/MultiSelect.vue
  15. 61
      packages/nc-gui/components/cell/Percent.vue
  16. 6
      packages/nc-gui/components/cell/PhoneNumber.vue
  17. 3
      packages/nc-gui/components/cell/Rating.vue
  18. 1
      packages/nc-gui/components/cell/Text.vue
  19. 17
      packages/nc-gui/components/cell/TextArea.vue
  20. 4
      packages/nc-gui/components/cell/TimePicker.vue
  21. 7
      packages/nc-gui/components/cell/Url.vue
  22. 4
      packages/nc-gui/components/cell/attachment/Carousel.vue
  23. 8
      packages/nc-gui/components/cell/attachment/index.vue
  24. 5
      packages/nc-gui/components/cell/attachment/utils.ts
  25. 29
      packages/nc-gui/components/dashboard/TreeView.vue
  26. 30
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  27. 9
      packages/nc-gui/components/dashboard/settings/AuditTab.vue
  28. 25
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  29. 8
      packages/nc-gui/components/dashboard/settings/Misc.vue
  30. 31
      packages/nc-gui/components/dashboard/settings/Modal.vue
  31. 26
      packages/nc-gui/components/dashboard/settings/UIAcl.vue
  32. 35
      packages/nc-gui/components/dashboard/settings/app-store/AppInstall.vue
  33. 16
      packages/nc-gui/components/dlg/AirtableImport.vue
  34. 7
      packages/nc-gui/components/dlg/QuickImport.vue
  35. 11
      packages/nc-gui/components/dlg/TableCreate.vue
  36. 68
      packages/nc-gui/components/dlg/TableRename.vue
  37. 12
      packages/nc-gui/components/dlg/ViewCreate.vue
  38. 21
      packages/nc-gui/components/dlg/ViewDelete.vue
  39. 8
      packages/nc-gui/components/erd/Flow.vue
  40. 2
      packages/nc-gui/components/erd/RelationEdge.vue
  41. 12
      packages/nc-gui/components/erd/TableNode.vue
  42. 8
      packages/nc-gui/components/erd/View.vue
  43. 2
      packages/nc-gui/components/general/ChromeWrapper.vue
  44. 2
      packages/nc-gui/components/general/ColorModeSwitcher.vue
  45. 4
      packages/nc-gui/components/general/ColorPicker.vue
  46. 5
      packages/nc-gui/components/general/FlippingCard.vue
  47. 3
      packages/nc-gui/components/general/FullScreen.vue
  48. 10
      packages/nc-gui/components/general/HelpAndSupport.vue
  49. 3
      packages/nc-gui/components/general/MiniSidebar.vue
  50. 33
      packages/nc-gui/components/general/Overlay.vue
  51. 23
      packages/nc-gui/components/general/ReleaseInfo.vue
  52. 2
      packages/nc-gui/components/general/Share.vue
  53. 3
      packages/nc-gui/components/general/ShareBaseButton.vue
  54. 10
      packages/nc-gui/components/general/Social.vue
  55. 32
      packages/nc-gui/components/general/SocialCard.vue
  56. 2
      packages/nc-gui/components/general/Sponsors.vue
  57. 4
      packages/nc-gui/components/general/Tooltip.vue
  58. 28
      packages/nc-gui/components/general/TruncateText.vue
  59. 19
      packages/nc-gui/components/monaco/Editor.vue
  60. 17
      packages/nc-gui/components/shared-view/AskPassword.vue
  61. 32
      packages/nc-gui/components/shared-view/Grid.vue
  62. 28
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  63. 48
      packages/nc-gui/components/smartsheet/Cell.vue
  64. 27
      packages/nc-gui/components/smartsheet/Form.vue
  65. 64
      packages/nc-gui/components/smartsheet/Gallery.vue
  66. 35
      packages/nc-gui/components/smartsheet/Grid.vue
  67. 3
      packages/nc-gui/components/smartsheet/Pagination.vue
  68. 22
      packages/nc-gui/components/smartsheet/Row.vue
  69. 29
      packages/nc-gui/components/smartsheet/Toolbar.vue
  70. 40
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  71. 18
      packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue
  72. 14
      packages/nc-gui/components/smartsheet/column/CheckboxOptions.vue
  73. 29
      packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue
  74. 10
      packages/nc-gui/components/smartsheet/column/DateOptions.vue
  75. 11
      packages/nc-gui/components/smartsheet/column/DurationOptions.vue
  76. 38
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  77. 11
      packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue
  78. 15
      packages/nc-gui/components/smartsheet/column/FormulaOptions.vue
  79. 14
      packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue
  80. 11
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  81. 10
      packages/nc-gui/components/smartsheet/column/PercentOptions.vue
  82. 14
      packages/nc-gui/components/smartsheet/column/RatingOptions.vue
  83. 12
      packages/nc-gui/components/smartsheet/column/RollupOptions.vue
  84. 27
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  85. 0
      packages/nc-gui/components/smartsheet/column/SpecificDBTypeOptions.vue
  86. 13
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  87. 7
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  88. 23
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  89. 5
      packages/nc-gui/components/smartsheet/header/Cell.vue
  90. 3
      packages/nc-gui/components/smartsheet/header/CellIcon.vue
  91. 14
      packages/nc-gui/components/smartsheet/header/Menu.vue
  92. 6
      packages/nc-gui/components/smartsheet/header/VirtualCell.vue
  93. 3
      packages/nc-gui/components/smartsheet/header/VirtualCellIcon.vue
  94. 3
      packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue
  95. 9
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  96. 15
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  97. 16
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  98. 3
      packages/nc-gui/components/smartsheet/sidebar/toolbar/DebugMeta.vue
  99. 5
      packages/nc-gui/components/smartsheet/sidebar/toolbar/DeleteCache.vue
  100. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  101. Some files were not shown because too many files have changed in this diff Show More

4
packages/nc-gui/app.vue

@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed, useRoute } from '#imports'
import { computed, useRoute, useTheme } from '#imports'
const route = useRoute()
const disableBaseLayout = computed(() => route.path.startsWith('/nc/view') || route.path.startsWith('/nc/form'))
useTheme()
</script>
<template>

122
packages/nc-gui/assets/style/fonts.css

@ -1,55 +1,3 @@
/* roboto-100 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 100;
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.eot'); /* IE9 Compat Modes */
src: local(''),
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.woff2') format('woff2'), /* Super Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.woff') format('woff'), /* Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-100italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 100;
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.eot'); /* IE9 Compat Modes */
src: local(''),
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.woff2') format('woff2'), /* Super Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.woff') format('woff'), /* Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-100italic.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-300italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 300;
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.eot'); /* IE9 Compat Modes */
src: local(''),
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.woff2') format('woff2'), /* Super Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.woff') format('woff'), /* Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300italic.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-300 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.eot'); /* IE9 Compat Modes */
src: local(''),
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.woff2') format('woff2'), /* Super Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.woff') format('woff'), /* Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-300.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-regular - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
@ -76,32 +24,7 @@
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-italic.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-500 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.eot'); /* IE9 Compat Modes */
src: local(''),
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.woff2') format('woff2'), /* Super Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.woff') format('woff'), /* Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-500italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
font-style: italic;
font-weight: 500;
src: url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.eot'); /* IE9 Compat Modes */
src: local(''),
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.woff2') format('woff2'), /* Super Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.woff') format('woff'), /* Modern Browsers */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-500italic.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-700 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
@ -115,6 +38,7 @@
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-700italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
@ -128,6 +52,7 @@
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-700italic.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-900 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
@ -141,6 +66,7 @@
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900.ttf') format('truetype'), /* Safari, Android, iOS */
url('./roboto/roboto-v27-vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic-900.svg#Roboto') format('svg'); /* Legacy iOS */
}
/* roboto-900italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Roboto';
@ -157,30 +83,6 @@
/* Vazirmatn */
/* https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v32.102/Vazirmatn-font-face.css */
@font-face {
font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-Thin.woff2') format('woff2');
font-weight: 100;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-ExtraLight.woff2') format('woff2');
font-weight: 200;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-Light.woff2') format('woff2');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-Regular.woff2') format('woff2');
@ -189,22 +91,6 @@
font-display: swap;
}
@font-face {
font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-Medium.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-SemiBold.woff2') format('woff2');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-Bold.woff2') format('woff2');

19
packages/nc-gui/components.d.ts vendored

@ -82,9 +82,19 @@ declare module '@vue/runtime-core' {
IcRoundSearch: typeof import('~icons/ic/round-search')['default']
IcTwotoneWidthFull: typeof import('~icons/ic/twotone-width-full')['default']
IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['default']
LazyDashboardSettingsAppInstall: typeof import('~icons/la/zy-dashboard-settings-app-install')['default']
LazyMonacoEditor: typeof import('~icons/la/zy-monaco-editor')['default']
LazyVirtualCellBelongsTo: typeof import('~icons/la/zy-virtual-cell-belongs-to')['default']
LazyVirtualCellCount: typeof import('~icons/la/zy-virtual-cell-count')['default']
LazyVirtualCellFormula: typeof import('~icons/la/zy-virtual-cell-formula')['default']
LazyVirtualCellHasMany: typeof import('~icons/la/zy-virtual-cell-has-many')['default']
LazyVirtualCellLookup: typeof import('~icons/la/zy-virtual-cell-lookup')['default']
LazyVirtualCellManyToMany: typeof import('~icons/la/zy-virtual-cell-many-to-many')['default']
LazyVirtualCellRollup: typeof import('~icons/la/zy-virtual-cell-rollup')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSwagger: typeof import('~icons/logos/swagger')['default']
MaterialSymbolsAccountTreeRounded: typeof import('~icons/material-symbols/account-tree-rounded')['default']
MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default']
MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['default']
@ -94,6 +104,8 @@ declare module '@vue/runtime-core' {
MaterialSymbolsRocketLaunchOutline: typeof import('~icons/material-symbols/rocket-launch-outline')['default']
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['default']
MaterialSymbolsVisibility: typeof import('~icons/material-symbols/visibility')['default']
MaterialSymbolsVisibilityOff: typeof import('~icons/material-symbols/visibility-off')['default']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
@ -102,8 +114,7 @@ declare module '@vue/runtime-core' {
MdiAlpha: typeof import('~icons/mdi/alpha')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default']
MdiArrowCollapse: typeof import('~icons/mdi/arrow-collapse')['default']
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']
MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiAt: typeof import('~icons/mdi/at')['default']
@ -130,6 +141,7 @@ declare module '@vue/runtime-core' {
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default']
MdiDelete: typeof import('~icons/mdi/delete')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['default']
@ -149,6 +161,7 @@ declare module '@vue/runtime-core' {
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFileExcel: typeof import('~icons/mdi/file-excel')['default']
MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['default']
MdiFileImageBox: typeof import('~icons/mdi/file-image-box')['default']
MdiFilePlusOutline: typeof import('~icons/mdi/file-plus-outline')['default']
MdiFileUploadOutline: typeof import('~icons/mdi/file-upload-outline')['default']
MdiFilterOutline: typeof import('~icons/mdi/filter-outline')['default']
@ -178,11 +191,11 @@ declare module '@vue/runtime-core' {
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiNumeric: typeof import('~icons/mdi/numeric')['default']
MdiOpenInNew: typeof import('~icons/mdi/open-in-new')['default']
MdiOpenInNewIcon: typeof import('~icons/mdi/open-in-new-icon')['default']
MdiPencil: typeof import('~icons/mdi/pencil')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusCircleOutline: typeof import('~icons/mdi/plus-circle-outline')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiReddit: typeof import('~icons/mdi/reddit')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']

29
packages/nc-gui/components/api-client/Headers.vue

@ -1,18 +1,15 @@
<script setup lang="ts">
import MdiPlusIcon from '~icons/mdi/plus'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import { useVModel } from '#imports'
interface Props {
modelValue: Record<string, any>[]
}
const props = defineProps<Props>()
const props = defineProps<{
modelValue: any[]
}>()
const emits = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emits)
const headerList = ref([
const headerList = [
'A-IM',
'Accept',
'Accept-Charset',
@ -52,11 +49,11 @@ const headerList = ref([
'Dnt',
'X-Requested-With',
'X-CSRF-Token',
])
]
const addHeaderRow = () => vModel.value.push({})
const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
const deleteHeaderRow = (i: number) => vModel.value.splice(i, 1)
</script>
<template>
@ -67,17 +64,21 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
<th>
<!-- Intended to be empty - For checkbox -->
</th>
<th>
<div class="text-center font-normal mb-2">Header Name</div>
</th>
<th>
<div class="text-center font-normal mb-2">Value</div>
</th>
<th>
<!-- Intended to be empty - For delete button -->
</th>
</tr>
</thead>
<tbody>
<tr v-for="(headerRow, idx) in vModel" :key="idx">
<td class="px-2 nc-hook-header-tab-checkbox">
@ -85,6 +86,7 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
<a-checkbox v-model:checked="headerRow.enabled" />
</a-form-item>
</td>
<td class="px-2 w-min-[400px]">
<a-form-item>
<a-select
@ -101,22 +103,25 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
</a-select>
</a-form-item>
</td>
<td class="px-2 w-min-[400px]">
<a-form-item>
<a-input v-model:value="headerRow.value" size="large" placeholder="Value" class="nc-input-hook-header-value" />
</a-form-item>
</td>
<td class="relative">
<div v-if="idx !== 0" class="absolute flex flex-col justify-start mt-2 -right-6 top-0">
<MdiDeleteOutlineIcon class="cursor-pointer" @click="deleteHeaderRow(idx)" />
<MdiDeleteOutline class="cursor-pointer" @click="deleteHeaderRow(idx)" />
</div>
</td>
</tr>
<tr>
<td :colspan="12" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addHeaderRow">
<template #icon>
<MdiPlusIcon class="flex mx-auto" />
<MdiPlus class="flex mx-auto" />
</template>
</a-button>
</td>

25
packages/nc-gui/components/api-client/Params.vue

@ -1,12 +1,9 @@
<script setup lang="ts">
import MdiPlusIcon from '~icons/mdi/plus'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
import { useVModel } from '#imports'
interface Props {
modelValue: Record<string, any>[]
}
const props = defineProps<Props>()
const props = defineProps<{
modelValue: any[]
}>()
const emits = defineEmits(['update:modelValue'])
@ -14,7 +11,7 @@ const vModel = useVModel(props, 'modelValue', emits)
const addParamRow = () => vModel.value.push({})
const deleteParamRow = (idx: number) => vModel.value.splice(idx, 1)
const deleteParamRow = (i: number) => vModel.value.splice(i, 1)
</script>
<template>
@ -25,17 +22,21 @@ const deleteParamRow = (idx: number) => vModel.value.splice(idx, 1)
<th>
<!-- Intended to be empty - For checkbox -->
</th>
<th>
<div class="text-center font-normal mb-2">Param Name</div>
</th>
<th>
<div class="text-center font-normal mb-2">Value</div>
</th>
<th>
<!-- Intended to be empty - For delete button -->
</th>
</tr>
</thead>
<tbody>
<tr v-for="(paramRow, idx) in vModel" :key="idx">
<td class="px-2">
@ -43,27 +44,31 @@ const deleteParamRow = (idx: number) => vModel.value.splice(idx, 1)
<a-checkbox v-model:checked="paramRow.enabled" />
</a-form-item>
</td>
<td class="px-2">
<a-form-item>
<a-input v-model:value="paramRow.name" size="large" placeholder="Key" />
</a-form-item>
</td>
<td class="px-2">
<a-form-item>
<a-input v-model:value="paramRow.value" size="large" placeholder="Value" />
</a-form-item>
</td>
<td class="relative">
<div v-if="idx !== 0" class="absolute flex flex-col justify-start mt-2 -right-6 top-0">
<MdiDeleteOutlineIcon class="cursor-pointer" @click="deleteParamRow(idx)" />
<MdiDeleteOutline class="cursor-pointer" @click="deleteParamRow(idx)" />
</div>
</td>
</tr>
<tr>
<td :colspan="12" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addParamRow">
<template #icon>
<MdiPlusIcon class="flex mx-auto" />
<MdiPlus class="flex mx-auto" />
</template>
</a-button>
</td>

5
packages/nc-gui/components/cell/Currency.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { ColumnInj, computed, inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
import { ColumnInj, EditModeInj, computed, inject, useVModel } from '#imports'
interface Props {
modelValue: number | null | undefined
@ -49,6 +48,8 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
class="w-full h-full border-none outline-none"
@blur="editEnabled = false"
/>
<span v-else-if="vModel">{{ currency }}</span>
<span v-else />
</template>

4
packages/nc-gui/components/cell/DateTimePicker.vue

@ -1,9 +1,9 @@
<script setup lang="ts">
import dayjs from 'dayjs'
import { ReadonlyInj } from '#imports'
import { ReadonlyInj, inject, ref, useProject, watch } from '#imports'
interface Props {
modelValue: string | null | undefined
modelValue?: string | null
}
const { modelValue } = defineProps<Props>()

3
packages/nc-gui/components/cell/Decimal.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
modelValue: number | null | string | undefined

14
packages/nc-gui/components/cell/Duration.vue

@ -1,6 +1,14 @@
<script setup lang="ts">
import { ColumnInj, computed, convertDurationToSeconds, convertMS2Duration, durationOptions, inject, ref } from '#imports'
import { EditModeInj } from '~/context'
import {
ColumnInj,
EditModeInj,
computed,
convertDurationToSeconds,
convertMS2Duration,
durationOptions,
inject,
ref,
} from '#imports'
interface Props {
modelValue: number | string | null | undefined
@ -72,7 +80,9 @@ const submitDuration = () => {
@keypress="checkDurationFormat($event)"
@keydown.enter="submitDuration"
/>
<span v-else> {{ localState }}</span>
<div v-if="showWarningMessage" class="duration-warning">
<!-- TODO: i18n -->
Please enter a number

5
packages/nc-gui/components/cell/Email.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject, isEmail, useVModel } from '#imports'
import { EditModeInj } from '~/context'
import { EditModeInj, computed, inject, isEmail, useVModel } from '#imports'
interface Props {
modelValue: string | null | undefined
@ -26,8 +25,10 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<template>
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm" @blur="editEnabled = false" />
<a v-else-if="validEmail" class="text-sm underline hover:opacity-75" :href="`mailto:${vModel}`" target="_blank">
{{ vModel }}
</a>
<span v-else>{{ vModel }}</span>
</template>

3
packages/nc-gui/components/cell/Float.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { VNodeRef } from '@vue/runtime-core'
import { inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
modelValue: number | null | undefined

3
packages/nc-gui/components/cell/Integer.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { inject, useVModel } from '#imports'
import { EditModeInj } from '~/context'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
modelValue: number | null | undefined

7
packages/nc-gui/components/cell/Json.vue

@ -1,8 +1,5 @@
<script setup lang="ts">
import { Modal as AModal } from 'ant-design-vue'
import Editor from '~/components/monaco/Editor.vue'
import { ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
import { EditModeInj, IsFormInj } from '~/context'
import { Modal as AModal, EditModeInj, IsFormInj, ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
interface Props {
modelValue: string | Record<string, any> | undefined
@ -113,7 +110,7 @@ watch(editEnabled, () => {
</div>
</div>
<Editor
<LazyMonacoEditor
:model-value="localValue"
class="min-w-full w-80"
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }"

1
packages/nc-gui/components/cell/MultiSelect.vue

@ -146,6 +146,7 @@ watch(isOpen, (n, _o) => {
<span class="text-slate-500" :class="{ 'text-sm': isKanban }">{{ op.title }}</span>
</a-tag>
</a-select-option>
<template #tagRender="{ value: val, onClose }">
<a-tag
v-if="options.find((el) => el.title === val)"

61
packages/nc-gui/components/cell/Percent.vue

@ -1,11 +1,12 @@
<script setup lang="ts">
import { EditModeInj, inject } from '#imports'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
modelValue: number | string | null | undefined
}
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
@ -15,62 +16,6 @@ const vModel = useVModel(props, 'modelValue', emits)
<template>
<input v-if="editEnabled" v-model="vModel" type="number" />
<span v-else>{{ vModel }}</span>
</template>
<!-- <script setup lang="ts">
import { ColumnInj, computed, getPercentStep, inject, isValidPercent, renderPercent } from '#imports'
import { EditModeInj } from '~/context'
interface Props {
modelValue: number | string | null | undefined
}
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const column = inject(ColumnInj)
const percent = ref()
const isEdited = ref(false)
const percentType = computed(() => column?.value?.meta?.precision || 0)
const percentStep = computed(() => getPercentStep(percentType.value))
const localState = computed({
get: () => {
return renderPercent(modelValue, percentType.value, !isEdited.value)
},
set: (val) => {
if (val === null) val = 0
if (isValidPercent(val, column?.value?.meta?.negative)) {
percent.value = val / 100
}
},
})
function onKeyDown(evt: KeyboardEvent) {
isEdited.value = true
return ['e', 'E', '+', '-'].includes(evt.key) && evt.preventDefault()
}
function onBlur() {
if (isEdited.value) {
emit('update:modelValue', percent.value)
isEdited.value = false
}
}
function onKeyDownEnter() {
if (isEdited.value) {
emit('update:modelValue', percent.value)
isEdited.value = false
}
}
</script>
<template>
<input
v-if="isEdited"
v-model="localState"
type="number"
:step="percentStep"
@keydown="onKeyDown"
@blur="onBlur"
@keydown.enter="onKeyDownEnter"
/>
<input v-if="editEnabled" v-model="localState" type="text" @focus="isEdited = true" />
<span v-else>{{ localState }}</span>
</template> -->

6
packages/nc-gui/components/cell/PhoneNumber.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import Text from './Text.vue'
import { useVModel } from '#imports'
interface Props {
modelValue: any
@ -17,7 +17,5 @@ const vModel = useVModel(props, 'modelValue', emits)
</script>
<template>
<Text v-model="vModel" />
<LazyCellText v-model="vModel" />
</template>
<style scoped></style>

3
packages/nc-gui/components/cell/Rating.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { ColumnInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
import { ColumnInj, EditModeInj, computed, inject } from '#imports'
interface Props {
modelValue?: number | null | undefined

1
packages/nc-gui/components/cell/Text.vue

@ -25,5 +25,6 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
class="h-full w-full outline-none bg-transparent"
@blur="editEnabled = false"
/>
<span v-else>{{ vModel }}</span>
</template>

17
packages/nc-gui/components/cell/TextArea.vue

@ -1,22 +1,16 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { computed, inject } from '#imports'
import { EditModeInj } from '~/context'
import { EditModeInj, inject, useVModel } from '#imports'
interface Props {
modelValue: string | null | undefined
}
const { modelValue } = defineProps<Props>()
const props = defineProps<{
modelValue?: string | null
}>()
const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj)
const vModel = computed({
get: () => modelValue ?? '',
set: (value) => emits('update:modelValue', value),
})
const vModel = useVModel(props, 'modelValue', emits, { defaultValue: '' })
const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus()
</script>
@ -32,5 +26,6 @@ const focus: VNodeRef = (el) => (el as HTMLTextAreaElement)?.focus()
@keydown.alt.enter.stop
@keydown.shift.enter.stop
/>
<span v-else>{{ vModel }}</span>
</template>

4
packages/nc-gui/components/cell/TimePicker.vue

@ -1,6 +1,6 @@
<script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs'
import { ReadonlyInj, inject, onClickOutside, useProject, watch } from '#imports'
interface Props {
modelValue?: string | null | undefined
@ -16,7 +16,7 @@ const readOnly = inject(ReadonlyInj, false)
let isTimeInvalid = $ref(false)
const dateFormat = isMysql ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const dateFormat = isMysql.value ? 'YYYY-MM-DD HH:mm:ss' : 'YYYY-MM-DD HH:mm:ssZ'
const localState = $computed({
get() {

7
packages/nc-gui/components/cell/Url.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import {
CellUrlDisableOverlayInj,
ColumnInj,
@ -8,6 +7,7 @@ import {
computed,
inject,
isValidURL,
message,
ref,
useCellUrlConfig,
useI18n,
@ -77,14 +77,19 @@ watch(
<nuxt-link
v-else-if="isValid && !cellUrlOptions?.overlay"
no-prefetch
no-rel
class="z-3 text-sm underline hover:opacity-75"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
>
{{ value }}
</nuxt-link>
<nuxt-link
v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay"
no-prefetch
no-rel
class="z-3 w-full h-full text-center !no-underline hover:opacity-75"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"

4
packages/nc-gui/components/cell/attachment/Carousel.vue

@ -82,7 +82,9 @@ onClickOutside(carouselRef, () => {
<template #customPaging="props">
<a>
<nuxt-img
<LazyNuxtImg
quality="90"
placeholder
class="!block"
:alt="imageItems[props.i].title || `#${props.i}`"
:src="imageItems[props.i].url || imageItems[props.i].data"

8
packages/nc-gui/components/cell/attachment/index.vue

@ -2,8 +2,6 @@
import { onKeyDown } from '@vueuse/core'
import { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort'
import Modal from './Modal.vue'
import Carousel from './Carousel.vue'
import {
IsFormInj,
IsGalleryInj,
@ -133,7 +131,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
ref="attachmentCellRef"
class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1"
>
<Carousel />
<LazyCellAttachmentCarousel />
<template v-if="isSharedForm || (!isReadonly && !dragging && !!currentCellRef)">
<general-overlay
@ -183,7 +181,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
<div class="text-center w-full">{{ item.title }}</div>
</template>
<nuxt-img
<LazyNuxtImg
v-if="isImage(item.title, item.mimetype ?? item.type) && (item.url || item.data)"
quality="75"
placeholder
@ -214,7 +212,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
</div>
</template>
<Modal />
<LazyCellAttachmentModal />
</div>
</template>

5
packages/nc-gui/components/cell/attachment/utils.ts

@ -1,5 +1,3 @@
import { message } from 'ant-design-vue'
import FileSaver from 'file-saver'
import {
ColumnInj,
EditModeInj,
@ -11,6 +9,7 @@ import {
computed,
inject,
isImage,
message,
ref,
useApi,
useFileDialog,
@ -146,7 +145,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** download a file */
async function downloadFile(item: Record<string, any>) {
FileSaver.saveAs(item.url || item.data, item.title)
;(await import('file-saver')).saveAs(item.url || item.data, item.title)
}
const FileIcon = (icon: string) => {

29
packages/nc-gui/components/dashboard/TreeView.vue

@ -1,13 +1,14 @@
<script setup lang="ts">
import type { TableType } from 'nocodb-sdk'
import Sortable from 'sortablejs'
import { Empty } from 'ant-design-vue'
import GithubButton from 'vue-github-button'
import {
Empty,
computed,
inject,
reactive,
ref,
resolveComponent,
useDialog,
useNuxtApp,
useProject,
@ -16,15 +17,9 @@ import {
useUIPermission,
watchEffect,
} from '#imports'
import DlgAirtableImport from '~/components/dlg/AirtableImport.vue'
import DlgQuickImport from '~/components/dlg/QuickImport.vue'
import DlgTableCreate from '~/components/dlg/TableCreate.vue'
import DlgTableRename from '~/components/dlg/TableRename.vue'
import { TabType } from '~/composables'
import { TabType } from '~/lib'
import MdiView from '~icons/mdi/eye-circle-outline'
import MdiTableLarge from '~icons/mdi/table-large'
import MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiDrag from '~icons/mdi/drag-vertical'
const { addTab } = useTabs()
@ -49,7 +44,7 @@ const filterQuery = $ref('')
const activeTable = computed(() => ([TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null))
const tablesById = $computed(() =>
tables.value?.reduce((acc: Record<string, TableType>, table) => {
tables.value?.reduce<Record<string, TableType>>((acc, table) => {
acc[table.id!] = table
return acc
@ -150,7 +145,7 @@ function openRenameTableDialog(table: TableType, rightClick = false) {
const isOpen = ref(true)
const { close } = useDialog(DlgTableRename, {
const { close } = useDialog(resolveComponent('DlgTableRename'), {
'modelValue': isOpen,
'tableMeta': table,
'onUpdate:modelValue': closeDialog,
@ -168,7 +163,7 @@ function openQuickImportDialog(type: string) {
const isOpen = ref(true)
const { close } = useDialog(DlgQuickImport, {
const { close } = useDialog(resolveComponent('DlgQuickImport'), {
'modelValue': isOpen,
'importType': type,
'onUpdate:modelValue': closeDialog,
@ -186,7 +181,7 @@ function openAirtableImportDialog() {
const isOpen = ref(true)
const { close } = useDialog(DlgAirtableImport, {
const { close } = useDialog(resolveComponent('DlgAirtableImport'), {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
})
@ -203,7 +198,7 @@ function openTableCreateDialog() {
const isOpen = ref(true)
const { close } = useDialog(DlgTableCreate, {
const { close } = useDialog(resolveComponent('DlgTableCreate'), {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
})
@ -320,7 +315,7 @@ function openTableCreateDialog() {
<template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto">
<MdiDrag
<MdiDragVertical
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@ -343,7 +338,7 @@ function openTableCreateDialog() {
:trigger="['click']"
@click.stop
>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
@ -403,11 +398,11 @@ function openTableCreateDialog() {
<a-divider class="!my-0" />
<div class="flex items-start flex-col justify-start px-2 py-3 gap-2">
<GeneralShareBaseButton
<LazyGeneralShareBaseButton
class="color-transition py-1.5 px-2 text-primary font-bold cursor-pointer select-none hover:text-accent"
/>
<GeneralHelpAndSupport class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
<LazyGeneralHelpAndSupport class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" />
<GithubButton
class="ml-2 py-1"

30
packages/nc-gui/components/dashboard/settings/AppStore.vue

@ -1,18 +1,16 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import AppInstall from './app-store/AppInstall.vue'
import MdiEditIcon from '~icons/ic/round-edit'
import MdiCloseCircleIcon from '~icons/mdi/close-circle-outline'
import MdiPlusIcon from '~icons/mdi/plus'
import { extractSdkResponseErrorMsg } from '~/utils'
import { extractSdkResponseErrorMsg, message, onMounted, useI18n, useNuxtApp } from '#imports'
const { t } = useI18n()
const { $api, $e } = useNuxtApp()
let apps = $ref<null | Array<any>>(null)
let apps = $ref<null | any[]>(null)
let showPluginUninstallModal = $ref(false)
let showPluginInstallModal = $ref(false)
let pluginApp = $ref<any>(null)
const fetchPluginApps = async () => {
@ -66,7 +64,7 @@ const showResetPluginModal = async (app: any) => {
onMounted(async () => {
if (apps === null) {
fetchPluginApps()
await fetchPluginApps()
}
})
</script>
@ -80,7 +78,7 @@ onMounted(async () => {
:footer="null"
wrap-class-name="nc-modal-plugin-install"
>
<AppInstall
<LazyDashboardSettingsAppInstall
v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id"
@close="showPluginInstallModal = false"
@ -117,19 +115,21 @@ onMounted(async () => {
<div class="install-btn flex flex-row justify-end space-x-1">
<a-button v-if="app.parsedInput" size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-edit">
<MdiEditIcon class="pr-0.5" :height="12" />
<IcRoundEdit class="pr-0.5" :height="12" />
Edit
</div>
</a-button>
<a-button v-if="app.parsedInput" size="small" outlined @click="showResetPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-reset">
<MdiCloseCircleIcon />
<MdiCloseCircleOutline />
<div class="flex ml-0.5">Reset</div>
</div>
</a-button>
<a-button v-else size="small" type="primary" @click="showInstallPluginModal(app)">
<div class="flex flex-row justify-center items-center caption capitalize nc-app-store-card-install">
<MdiPlusIcon />
<MdiPlus />
Install
</div>
</a-button>
@ -140,15 +140,19 @@ onMounted(async () => {
<img
v-if="app.title !== 'SMTP'"
class="avatar"
alt="logo"
:style="{
backgroundColor: app.title === 'SES' ? '#242f3e' : '',
}"
:src="app.logo"
/>
<div v-else />
</div>
<div class="flex flex-col flex-1 w-3/5 pl-3">
<a-typography-title :level="5">{{ app.title }}</a-typography-title>
{{ app.description }}
</div>
</div>

9
packages/nc-gui/components/dashboard/settings/AuditTab.vue

@ -1,13 +1,12 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { Tooltip as ATooltip, Empty } from 'ant-design-vue'
import type { AuditType } from 'nocodb-sdk'
import { timeAgo } from '~/utils/dateTimeUtils'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import { h, onMounted, timeAgo, useI18n, useNuxtApp, useProject } from '#imports'
const { $api } = useNuxtApp()
const { project } = useProject()
const { t } = useI18n()
let isLoading = $ref(false)
@ -92,9 +91,11 @@ const columns = [
<!-- Reload -->
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
{{ $t('general.reload') }}
</div>
</a-button>
<a-pagination
v-model:current="currentPage"
:page-size="currentLimit"

25
packages/nc-gui/components/dashboard/settings/Metadata.vue

@ -1,17 +1,16 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { Empty, message } from 'ant-design-vue'
import { h, useNuxtApp, useProject } from '#imports'
import MdiReload from '~icons/mdi/reload'
import MdiDatabaseSync from '~icons/mdi/database-sync'
import { extractSdkResponseErrorMsg } from '~/utils'
import { Empty, extractSdkResponseErrorMsg, h, message, useI18n, useNuxtApp, useProject } from '#imports'
const { $api } = useNuxtApp()
const { project, loadTables } = useProject()
const { t } = useI18n()
let isLoading = $ref(false)
let isDifferent = $ref(false)
let metadiff = $ref<any[]>([])
async function loadMetaDiff() {
@ -58,6 +57,7 @@ onMounted(async () => {
})
const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gray-500' }, label)
const columns = [
{
// Models
@ -90,6 +90,7 @@ const columns = [
</div>
</a-button>
</div>
<div class="max-h-600px overflow-y-auto">
<a-table
class="w-full"
@ -105,10 +106,13 @@ const columns = [
:loading="isLoading"
bordered
>
<template #emptyText> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> </template
></a-table>
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
</a-table>
</div>
</div>
<div class="flex place-content-center w-2/5">
<!-- Sync Now -->
<div v-if="isDifferent">
@ -119,9 +123,12 @@ const columns = [
</div>
</a-button>
</div>
<div v-else>
<!-- Tables metadata is in sync -->
<span><a-alert :message="$t('msg.info.tablesMetadataInSync')" type="success" show-icon /></span>
<span>
<a-alert :message="$t('msg.info.tablesMetadataInSync')" type="success" show-icon />
</span>
</div>
</div>
</div>

8
packages/nc-gui/components/dashboard/settings/Misc.vue

@ -1,4 +1,6 @@
<script setup lang="ts">
import { useGlobal, useProject, watch } from '#imports'
const { includeM2M } = useGlobal()
const { loadTables } = useProject()
@ -10,9 +12,9 @@ watch(includeM2M, async () => await loadTables())
<div class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show M2M Tables -->
<a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc">{{
$t('msg.info.showM2mTables')
}}</a-checkbox>
<a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc">
{{ $t('msg.info.showM2mTables') }}
</a-checkbox>
</div>
</div>
</div>

31
packages/nc-gui/components/dashboard/settings/Modal.vue

@ -1,19 +1,10 @@
<script setup lang="ts">
import type { FunctionalComponent, SVGAttributes } from 'vue'
import AuditTab from './AuditTab.vue'
import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import Misc from './Misc.vue'
import Erd from './Erd.vue'
import { useNuxtApp } from '#app'
import { useI18n, useUIPermission, useVModel, watch } from '#imports'
import ApiTokenManagement from '~/components/tabs/auth/ApiTokenManagement.vue'
import UserManagement from '~/components/tabs/auth/UserManagement.vue'
import { resolveComponent, useI18n, useNuxtApp, useUIPermission, useVModel, watch } from '#imports'
import StoreFrontOutline from '~icons/mdi/storefront-outline'
import TeamFillIcon from '~icons/ri/team-fill'
import MultipleTableIcon from '~icons/mdi/table-multiple'
import NootbookOutline from '~icons/mdi/notebook-outline'
import NotebookOutline from '~icons/mdi/notebook-outline'
interface Props {
modelValue: boolean
@ -59,7 +50,7 @@ const tabsInfo: TabGroup = {
usersManagement: {
// Users Management
title: t('title.userMgmt'),
body: UserManagement,
body: resolveComponent('TabsAuthUserManagement'),
},
}
: {}),
@ -68,7 +59,7 @@ const tabsInfo: TabGroup = {
apiTokenManagement: {
// API Tokens Management
title: t('title.apiTokenMgmt'),
body: ApiTokenManagement,
body: resolveComponent('TabsAuthApiTokenManagement'),
},
}
: {}),
@ -86,7 +77,7 @@ const tabsInfo: TabGroup = {
subTabs: {
new: {
title: 'Apps',
body: AppStore,
body: resolveComponent('DashboardSettingsAppStore'),
},
},
onClick: () => {
@ -103,26 +94,26 @@ const tabsInfo: TabGroup = {
metaData: {
// Metadata
title: t('title.metadata'),
body: Metadata,
body: resolveComponent('DashboardSettingsMetadata'),
},
acl: {
// UI Access Control
title: t('title.uiACL'),
body: UIAcl,
body: resolveComponent('DashboardSettingsUIAcl'),
onClick: () => {
$e('c:table:ui-acl')
},
},
erd: {
title: t('title.erdView'),
body: Erd,
body: resolveComponent('DashboardSettingsErd'),
onClick: () => {
$e('c:settings:erd')
},
},
misc: {
title: t('general.misc'),
body: Misc,
body: resolveComponent('DashboardSettingsMisc'),
},
},
onClick: () => {
@ -132,12 +123,12 @@ const tabsInfo: TabGroup = {
audit: {
// Audit
title: t('title.audit'),
icon: NootbookOutline,
icon: NotebookOutline,
subTabs: {
audit: {
// Audit
title: t('title.audit'),
body: AuditTab,
body: resolveComponent('DashboardSettingsAuditTab'),
},
},
onClick: () => {

26
packages/nc-gui/components/dashboard/settings/UIAcl.vue

@ -1,8 +1,17 @@
<script setup lang="ts">
import { Empty, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg, viewIcons } from '~/utils'
import { computed, h, useNuxtApp, useProject } from '#imports'
import {
Empty,
computed,
extractSdkResponseErrorMsg,
h,
message,
onMounted,
useGlobal,
useI18n,
useNuxtApp,
useProject,
viewIcons,
} from '#imports'
const { t } = useI18n()
@ -109,12 +118,14 @@ const columns = [
<MdiMagnify />
</template>
</a-input>
<a-button class="self-start nc-acl-reload" @click="loadTableList">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
Reload
</div>
</a-button>
<a-button class="self-start nc-acl-save" @click="saveUIAcl">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiContentSave />
@ -122,6 +133,7 @@ const columns = [
</div>
</a-button>
</div>
<div class="max-h-600px overflow-y-auto">
<a-table
class="w-full"
@ -140,14 +152,17 @@ const columns = [
<template #emptyText>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</template>
<template #bodyCell="{ record, column }">
<div v-if="column.name === 'table_name'">{{ record._ptn }}</div>
<div v-if="column.name === 'view_name'">
<div class="flex items-center">
<component :is="viewIcons[record.type].icon" :class="`text-${viewIcons[record.type].color} mr-1`" />
{{ record.title }}
</div>
</div>
<div v-for="role in roles" :key="role">
<div v-if="column.name === role">
<a-tooltip>
@ -157,11 +172,12 @@ const columns = [
>
<span v-else>Click to hide '{{ record.title }}' for role:{{ role }} in UI dashboard</span>
</template>
<a-checkbox
:checked="!record.disabled[role]"
:class="`nc-acl-${record.title}-${role}-chkbox`"
@change="onRoleCheck(record, role)"
></a-checkbox>
/>
</a-tooltip>
</div>
</div>

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

@ -1,19 +1,10 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import type { PluginType } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg, ref, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
interface Props {
const { id } = defineProps<{
id: string
}
type Plugin = PluginType & {
formDetails: Record<string, any>
parsedInput: Record<string, any>
}
const { id } = defineProps<Props>()
}>()
const emits = defineEmits(['saved', 'close'])
@ -22,6 +13,11 @@ enum Action {
Test = 'test',
}
type Plugin = PluginType & {
formDetails: Record<string, any>
parsedInput: Record<string, any>
}
const { $api } = useNuxtApp()
const formRef = ref()
@ -29,8 +25,11 @@ const formRef = ref()
const { t } = useI18n()
let plugin = $ref<Plugin | null>(null)
let pluginFormData = $ref<Record<string, any>>({})
let isLoading = $ref(true)
let loadingAction = $ref<null | Action>(null)
const layout = {
@ -153,6 +152,7 @@ onMounted(async () => {
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>
</div>
<div class="absolute -right-2 -top-0.5">
<a-button type="text" class="!rounded-md mr-1" @click="emits('close')">
<template #icon>
@ -175,6 +175,7 @@ onMounted(async () => {
</th>
</tr>
</thead>
<tbody>
<tr v-for="(itemRow, itemIndex) in plugin.parsedInput" :key="itemIndex">
<td v-for="(columnData, columnIndex) in plugin.formDetails.items" :key="columnIndex" class="px-2">
@ -188,17 +189,21 @@ onMounted(async () => {
v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-textarea
v-else-if="columnData.type === 'LongText'"
v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-switch
v-else-if="columnData.type === 'Checkbox'"
v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-input v-else v-model:value="itemRow[columnData.key]" :placeholder="columnData.placeholder" />
<div
v-if="itemIndex !== 0 && columnIndex === plugin.formDetails.items.length - 1"
class="absolute flex flex-col justify-start mt-2 -right-6 top-0"
@ -236,19 +241,23 @@ onMounted(async () => {
v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-textarea
v-else-if="columnData.type === 'LongText'"
v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-switch
v-else-if="columnData.type === 'Checkbox'"
v-model:checked="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder"
/>
<a-input v-else v-model:value="pluginFormData[columnData.key]" :placeholder="columnData.placeholder" />
</a-form-item>
</template>
<div class="flex flex-row space-x-4 justify-center mt-4">
<a-button
v-for="(action, i) in plugin.formDetails.actions"
@ -265,5 +274,3 @@ onMounted(async () => {
</div>
</template>
</template>
<style scoped lang="scss"></style>

16
packages/nc-gui/components/dlg/AirtableImport.vue

@ -2,11 +2,12 @@
import type { Socket } from 'socket.io-client'
import io from 'socket.io-client'
import type { Card as AntCard } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import {
Form,
computed,
extractSdkResponseErrorMsg,
fieldRequiredValidator,
message,
nextTick,
onBeforeUnmount,
onMounted,
@ -17,11 +18,9 @@ import {
watch,
} from '#imports'
interface Props {
const { modelValue } = defineProps<{
modelValue: boolean
}
const { modelValue } = defineProps<Props>()
}>()
const emit = defineEmits(['update:modelValue'])
@ -201,7 +200,6 @@ onMounted(async () => {
// connect event does not provide data
socket.on('connect', () => {
console.log(socket?.id)
console.log('socket connected')
})
@ -337,9 +335,9 @@ onBeforeUnmount(() => {
<!-- This feature is currently in beta and more information can be found here -->
<div>
{{ $t('general.betaNote') }}
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">{{
$t('general.moreInfo')
}}</a>
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">
{{ $t('general.moreInfo') }}
</a>
.
</div>
</div>

7
packages/nc-gui/components/dlg/QuickImport.vue

@ -1,10 +1,10 @@
<script setup lang="ts">
import { Form, message } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk'
import type { UploadChangeParam, UploadFile } from 'ant-design-vue'
import {
ExcelTemplateAdapter,
ExcelUrlTemplateAdapter,
Form,
JSONTemplateAdapter,
JSONUrlTemplateAdapter,
computed,
@ -13,6 +13,7 @@ import {
importCsvUrlValidator,
importExcelUrlValidator,
importUrlValidator,
message,
reactive,
ref,
useI18n,
@ -294,7 +295,7 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => {
<div class="prose-xl font-weight-bold my-5">{{ importMeta.header }}</div>
<div class="mt-5">
<TemplateEditor
<LazyTemplateEditor
v-if="templateEditorModal"
ref="templateEditorRef"
:project-template="templateData"
@ -350,7 +351,7 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => {
</template>
<div class="pb-3 pt-3">
<MonacoEditor ref="jsonEditorRef" v-model="importState.jsonEditor" class="min-h-60 max-h-80" />
<LazyMonacoEditor ref="jsonEditorRef" v-model="importState.jsonEditor" class="min-h-60 max-h-80" />
</div>
</a-tab-pane>

11
packages/nc-gui/components/dlg/TableCreate.vue

@ -1,13 +1,10 @@
<script setup lang="ts">
import { Form } from 'ant-design-vue'
import { computed, onMounted, ref, useProject, useTable, useTabs, useVModel, validateTableName } from '#imports'
import { TabType } from '~/composables'
import { Form, computed, onMounted, ref, useProject, useTable, useTabs, useVModel, validateTableName } from '#imports'
import { TabType } from '~/lib'
interface Props {
const props = defineProps<{
modelValue: boolean
}
const props = defineProps<Props>()
}>()
const emit = defineEmits(['update:modelValue'])

68
packages/nc-gui/components/dlg/TableRename.vue

@ -1,11 +1,20 @@
<script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import { Form, message } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n'
import { useMetas, useProject, useTabs } from '#imports'
import { extractSdkResponseErrorMsg, validateTableName } from '~/utils'
import { useNuxtApp } from '#app'
import {
Form,
computed,
extractSdkResponseErrorMsg,
message,
nextTick,
reactive,
useI18n,
useMetas,
useNuxtApp,
useProject,
useTabs,
validateTableName,
watchEffect,
} from '#imports'
interface Props {
modelValue?: boolean
@ -32,14 +41,19 @@ const dialogShow = computed({
})
const { updateTab } = useTabs()
const { loadTables, tables, project, isMysql, isMssql, isPg } = useProject()
const inputEl = $ref<any>()
let loading = $ref(false)
const useForm = Form.useForm
const formState = reactive({
title: '',
})
const validators = computed(() => {
return {
title: [
@ -83,31 +97,41 @@ const validators = computed(() => {
],
}
})
const { validateInfos } = useForm(formState, validators)
watchEffect(() => {
if (tableMeta?.title) formState.title = tableMeta?.title
// todo: replace setTimeout and follow better approach
nextTick(() => {
const input = inputEl?.$el
input.setSelectionRange(0, formState.title.length)
input.focus()
})
})
watchEffect(
() => {
if (tableMeta?.title) formState.title = `${tableMeta.title}`
// todo: replace setTimeout and follow better approach
nextTick(() => {
const input = inputEl?.$el
input.setSelectionRange(0, formState.title.length)
input.focus()
})
},
{ flush: 'post' },
)
const renameTable = async () => {
if (!tableMeta) return
loading = true
try {
await $api.dbTable.update(tableMeta?.id as string, {
project_id: tableMeta?.project_id,
await $api.dbTable.update(tableMeta.id as string, {
project_id: tableMeta.project_id,
table_name: formState.title,
})
dialogShow.value = false
loadTables()
updateTab({ id: tableMeta?.id }, { title: formState.title })
await loadTables()
updateTab({ id: tableMeta.id }, { title: formState.title })
// update metas
setMeta(await $api.dbTable.read(tableMeta?.id as string))
await setMeta(await $api.dbTable.read(tableMeta.id as string))
// Table renamed successfully
message.success(t('msg.success.tableRenamed'))
@ -116,6 +140,7 @@ const renameTable = async () => {
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
loading = false
}
</script>
@ -131,12 +156,15 @@ const renameTable = async () => {
>
<template #footer>
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" type="primary" :loading="loading" @click="renameTable">{{ $t('general.submit') }}</a-button>
</template>
<div class="pl-10 pr-10 pt-5">
<a-form :model="formState" name="create-new-table-form">
<!-- hint="Enter table name" -->
<div class="mb-2">{{ $t('msg.info.enterTableName') }}</div>
<a-form-item v-bind="validateInfos.title">
<a-input
ref="inputEl"

12
packages/nc-gui/components/dlg/ViewCreate.vue

@ -1,21 +1,21 @@
<script setup lang="ts">
import type { ComponentPublicInstance } from '@vue/runtime-core'
import type { Form as AntForm, SelectProps } from 'ant-design-vue'
import type { Form as AntForm } from 'ant-design-vue'
import { capitalize } from '@vue/runtime-core'
import type { FormType, GalleryType, GridType, KanbanType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { capitalize, inject } from '@vue/runtime-core'
import { UITypes, ViewTypes } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n'
import { ViewTypes } from 'nocodb-sdk'
import {
FieldsInj,
MetaInj,
ViewListInj,
computed,
generateUniqueTitle,
inject,
message,
nextTick,
reactive,
unref,
useApi,
useI18n,
useVModel,
watch,
} from '#imports'

21
packages/nc-gui/components/dlg/ViewDelete.vue

@ -1,12 +1,6 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg } from '~/utils'
import { onKeyStroke, useApi, useNuxtApp, useVModel } from '#imports'
import { extractSdkResponseErrorMsg, message, onKeyStroke, useApi, useI18n, useNuxtApp, useVModel } from '#imports'
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const { t } = useI18n()
interface Props {
modelValue: boolean
view?: Record<string, any>
@ -17,6 +11,12 @@ interface Emits {
(event: 'deleted'): void
}
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const { t } = useI18n()
const vModel = useVModel(props, 'modelValue', emits)
const { api, isLoading } = useApi()
@ -55,9 +55,10 @@ async function onDelete() {
<template #footer>
<a-button key="back" @click="vModel = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" danger html-type="submit" :loading="isLoading" @click="onDelete">{{
$t('general.submit')
}}</a-button>
<a-button key="submit" danger html-type="submit" :loading="isLoading" @click="onDelete">
{{ $t('general.submit') }}
</a-button>
</template>
</a-modal>
</template>

8
packages/nc-gui/components/erd/Flow.vue

@ -4,8 +4,7 @@ import { Background, Controls, VueFlow, useVueFlow } from '@braks/vue-flow'
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dagre from 'dagre'
import TableNode from './TableNode.vue'
import RelationEdge from './RelationEdge.vue'
import { onScopeDispose, watch } from '#imports'
interface Props {
tables: any[]
@ -200,11 +199,11 @@ watch([() => tables, () => config], init, { deep: true, flush: 'pre' })
<Controls class="!left-auto right-2 !top-3.5 !bottom-auto" :show-fit-view="false" :show-interactive="false" />
<template #node-custom="props">
<TableNode :data="props.data" />
<LazyErdTableNode :data="props.data" />
</template>
<template #edge-custom="props">
<RelationEdge v-bind="props" />
<LazyErdRelationEdge v-bind="props" />
</template>
<Background />
@ -218,6 +217,7 @@ watch([() => tables, () => config], init, { deep: true, flush: 'pre' })
<MdiTableLarge class="text-primary" />
<div>{{ $t('objects.table') }}</div>
</div>
<div class="flex flex-row items-center space-x-1 pt-1">
<MdiEyeCircleOutline class="text-primary" />
<div>{{ $t('objects.sqlVIew') }}</div>

2
packages/nc-gui/components/erd/RelationEdge.vue

@ -101,6 +101,7 @@ export default {
:d="edgePath"
:marker-end="markerEnd"
/>
<path
:id="id"
:style="style"
@ -135,6 +136,7 @@ export default {
:stroke-width="1.5"
:transform="`rotate(45,${sourceX + 2},${sourceY - 4})`"
/>
<rect
v-if="isManyToMany"
class="nc-erd-edge-rect"

12
packages/nc-gui/components/erd/TableNode.vue

@ -4,6 +4,7 @@ import { Handle, Position } from '@braks/vue-flow'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { MetaInj, computed, provide, toRefs, useNuxtApp } from '#imports'
interface Props extends NodeProps {
data: TableType & { showPkAndFk: boolean; showAllColumns: boolean }
@ -58,6 +59,7 @@ const relatedColumnId = (col: Record<string, any>) =>
</div>
</div>
</GeneralTooltip>
<div>
<div
v-for="col in pkAndFkColumns"
@ -65,7 +67,7 @@ const relatedColumnId = (col: Record<string, any>) =>
class="w-full border-b-1 py-2 border-gray-100 keys"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
>
<SmartsheetHeaderCell v-if="col" :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-if="col" :column="col" :hide-menu="true" />
</div>
<div class="w-full mb-1"></div>
@ -93,16 +95,18 @@ const relatedColumnId = (col: Record<string, any>) =>
type="target"
:position="Position.Left"
/>
<SmartsheetHeaderVirtualCell :column="col" :hide-menu="true" />
<LazySmartsheetHeaderVirtualCell :column="col" :hide-menu="true" />
</div>
<SmartsheetHeaderVirtualCell
<LazySmartsheetHeaderVirtualCell
v-else-if="isVirtualCol(col)"
:column="col"
:hide-menu="true"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
/>
<SmartsheetHeaderCell
<LazySmartsheetHeaderCell
v-else
:column="col"
:hide-menu="true"

8
packages/nc-gui/components/erd/View.vue

@ -1,15 +1,18 @@
<script setup lang="ts">
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { ref, useGlobal, useMetas, useProject, watch } from '#imports'
const { table } = defineProps<{ table?: TableType }>()
const { includeM2M } = useGlobal()
const { tables: projectTables } = useProject()
const tables = ref<TableType[]>([])
const { metas, getMeta } = useMetas()
const tables = ref<TableType[]>([])
let isLoading = $ref(true)
const showAdvancedOptions = ref(false)
@ -105,8 +108,9 @@ watch(
<a-spin size="large" />
</div>
</div>
<div v-else class="relative h-full">
<ErdFlow :tables="tables" :config="config" />
<LazyErdFlow :tables="tables" :config="config" />
<div
class="absolute top-2 right-10 flex-col bg-white py-2 px-4 border-1 border-gray-100 rounded-md z-50 space-y-1 nc-erd-context-menu z-50"

2
packages/nc-gui/components/general/ChromeWrapper.vue

@ -26,5 +26,3 @@ const picked = computed({
<template>
<Chrome v-model="picked" />
</template>
<style scoped></style>

2
packages/nc-gui/components/general/ColorModeSwitcher.vue

@ -1,6 +1,4 @@
<script lang="ts" setup>
import MaterialSymbolsLightMode from '~icons/material-symbols/light-mode'
import MaterialSymbolsDarkMode from '~icons/material-symbols/dark-mode'
interface Props {
modelValue: boolean
}

4
packages/nc-gui/components/general/ColorPicker.vue

@ -57,14 +57,16 @@ watch(picked, (n, _o) => {
{{ compare(picked, color) ? '&#10003;' : '' }}
</button>
</div>
<a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false">
<a-collapse accordion ghost expand-icon-position="right">
<a-collapse-panel key="1" header="Advanced" class="">
<a-button v-if="props.pickButton" class="!bg-primary text-white w-full" @click="selectColor(picked)">
Pick Color
</a-button>
<div class="flex justify-center py-4">
<GeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" />
<LazyGeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" />
</div>
</a-collapse-panel>
</a-collapse>

5
packages/nc-gui/components/general/FlippingCard.vue

@ -1,4 +1,6 @@
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, watch } from '#imports'
type FlipTrigger = 'hover' | 'click' | { duration: number }
interface Props {
@ -12,7 +14,9 @@ const props = withDefaults(defineProps<Props>(), {
})
let flipped = $ref(false)
let hovered = $ref(false)
let flipTimer = $ref<NodeJS.Timer | null>(null)
onMounted(() => {
@ -76,6 +80,7 @@ watch($$(flipped), () => {
>
<slot name="front" />
</div>
<div
class="back"
:style="{ 'pointer-events': flipped ? 'auto' : 'none', 'opacity': !isFlipping ? (flipped ? 100 : 0) : flipped ? 0 : 100 }"

3
packages/nc-gui/components/general/FullScreen.vue

@ -1,7 +1,8 @@
<script setup lang="ts">
import { useSidebar } from '#imports'
import { computed, useSidebar } from '#imports'
const rightSidebar = useSidebar('nc-right-sidebar')
const leftSidebar = useSidebar('nc-left-sidebar')
const isSidebarsOpen = computed({

10
packages/nc-gui/components/general/HelpAndSupport.vue

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { useGlobal, useProject } from '#imports'
import { ref, useGlobal, useProject, useRoute } from '#imports'
const showDrawer = ref(false)
@ -20,6 +20,7 @@ const openSwaggerLink = () => {
@click="showDrawer = true"
>
<MdiCommentTextOutline class="mr-1" />
<!-- APIs & Support -->
<div>{{ $t('title.APIsAndSupport') }}</div>
</div>
@ -37,11 +38,13 @@ const openSwaggerLink = () => {
<!-- Help center -->
<a-typography-title :level="4" class="!mb-6 !text-gray-500">{{ $t('title.helpCenter') }}</a-typography-title>
<GeneralSocialCard class="!w-full nc-social-card">
<LazyGeneralSocialCard class="!w-full nc-social-card">
<template #before>
<a-list-item v-if="project">
<nuxt-link
v-e="['a:navbar:user:swagger']"
no-prefetch
no-rel
class="!no-underline !text-current py-4 font-semibold"
target="_blank"
@click="openSwaggerLink"
@ -54,7 +57,8 @@ const openSwaggerLink = () => {
</nuxt-link>
</a-list-item>
</template>
</GeneralSocialCard>
</LazyGeneralSocialCard>
<div class="min-h-10 w-full" />
</div>
</a-drawer>

3
packages/nc-gui/components/general/MiniSidebar.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { navigateTo } from '#app'
import { computed, useGlobal, useProject, useRoute, useSidebar } from '#imports'
import { computed, navigateTo, useGlobal, useProject, useRoute, useSidebar } from '#imports'
const { signOut, signedIn, user, currentVersion } = useGlobal()

33
packages/nc-gui/components/general/Overlay.vue

@ -43,16 +43,27 @@ export default {
<template>
<teleport :disabled="teleportDisabled || (inline && !target)" :to="target || 'body'">
<div
v-bind="$attrs"
:class="[
vModel ? 'opacity-100' : 'opacity-0 pointer-events-none',
inline ? 'absolute' : 'fixed',
transition ? 'transition-opacity duration-200 ease-in-out' : '',
]"
class="z-100 top-0 left-0 bottom-0 right-0 bg-gray-700/75"
>
<slot :is-open="vModel" />
</div>
<Transition :name="transition ? 'fade' : undefined" mode="out-in">
<div
v-show="!!vModel"
v-bind="$attrs"
:class="[inline ? 'absolute' : 'fixed']"
class="z-100 top-0 left-0 bottom-0 right-0 bg-gray-700/75"
>
<slot :is-open="vModel" />
</div>
</Transition>
</teleport>
</template>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

23
packages/nc-gui/components/general/ReleaseInfo.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg, onMounted } from '#imports'
import { computed, extractSdkResponseErrorMsg, message, onMounted, useGlobal, useNuxtApp } from '#imports'
const { $api } = useNuxtApp()
@ -47,22 +46,38 @@ onMounted(async () => await fetchReleaseInfo())
<mdi-menu-down />
</div>
</a-button>
<template #overlay>
<div class="mt-1 bg-white shadow-lg !border">
<nuxt-link class="!text-primary !no-underline" to="https://github.com/nocodb/nocodb/releases" target="_blank">
<nuxt-link
no-prefetch
no-rel
class="!text-primary !no-underline"
to="https://github.com/nocodb/nocodb/releases"
target="_blank"
>
<div class="nc-menu-item">
<mdi-script-text-outline />
{{ latestRelease }} {{ $t('activity.upgrade.releaseNote') }}
</div>
</nuxt-link>
<nuxt-link class="!text-primary !no-underline" to="https://docs.nocodb.com/getting-started/upgrading" target="_blank">
<nuxt-link
no-prefetch
no-rel
class="!text-primary !no-underline"
to="https://docs.nocodb.com/getting-started/upgrading"
target="_blank"
>
<div class="nc-menu-item">
<mdi-rocket-launch-outline />
<!-- How to upgrade? -->
{{ $t('activity.upgrade.howTo') }}
</div>
</nuxt-link>
<a-divider class="!m-0" />
<div class="nc-menu-item" @click="releaseAlert = false">
<mdi-close />
<!-- Hide menu -->

2
packages/nc-gui/components/general/Share.vue

@ -1,4 +1,6 @@
<script lang="ts" setup>
import { computed } from '#imports'
interface Props {
url: string
socialMedias: string[]

3
packages/nc-gui/components/general/ShareBaseButton.vue

@ -21,10 +21,11 @@ const { isUIAllowed } = useUIPermission()
>
<div class="flex items-center space-x-1">
<MdiAccountPlusOutline class="mr-1 nc-share-base" />
<div>{{ $t('activity.inviteTeam') }}</div>
</div>
</div>
<TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
<LazyTabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
</div>
</template>

10
packages/nc-gui/components/general/Social.vue

@ -1,9 +1,5 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
import MdiDiscord from '~icons/mdi/discord'
import MdiReddit from '~icons/mdi/reddit'
import MdiTwitter from '~icons/mdi/twitter'
import MdiCalendarMonth from '~icons/mdi/calendar-month'
import { useI18n } from '#imports'
const { locale } = useI18n()
@ -24,6 +20,7 @@ const isZhLang = $computed(() => locale.value.startsWith('zh'))
<div v-else class="flex justify-between gap-1 w-full px-2">
<MdiDiscord v-e="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" />
<div
v-e="['e:community:discourse']"
class="icon flex items-center justify-center min-w-[43px]"
@ -31,8 +28,11 @@ const isZhLang = $computed(() => locale.value.startsWith('zh'))
>
<div class="discourse" />
</div>
<MdiReddit v-e="['e:community:reddit']" class="icon text-[#FF4600]" @click="open('https://www.reddit.com/r/NocoDB/')" />
<MdiTwitter v-e="['e:community:twitter']" class="icon text-[#1DA1F2]" @click="open('https://twitter.com/NocoDB')" />
<MdiCalendarMonth
v-e="['e:community:book-demo']"
class="icon text-green-500"

32
packages/nc-gui/components/general/SocialCard.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { enumColor as colors } from '#imports'
import { enumColor as colors, useGlobal } from '#imports'
const { lang: currentLang } = useGlobal()
@ -14,6 +14,8 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
<a-list-item>
<nuxt-link
v-e="['e:docs']"
no-prefetch
no-rel
class="text-primary !no-underline !text-current"
target="_blank"
to="https://docs.nocodb.com/"
@ -24,9 +26,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link
v-e="['e:api-docs']"
no-prefetch
no-rel
class="text-primary !no-underline !text-current"
target="_blank"
to="https://apis.nocodb.com/"
@ -38,9 +43,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link
v-e="['e:community:github']"
no-prefetch
no-rel
class="text-primary !no-underline !text-current"
to="https://github.com/nocodb/nocodb"
target="_blank"
@ -64,9 +72,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link
v-e="['e:community:book-demo']"
no-prefetch
no-rel
class="!no-underline !text-current"
to="https://calendly.com/nocodb-meeting"
target="_blank"
@ -80,9 +91,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link
v-e="['e:community:discord']"
no-prefetch
no-rel
class="!no-underline !text-current"
to="https://discord.gg/5RgZmkW"
target="_blank"
@ -96,9 +110,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link
v-e="['e:community:twitter']"
no-prefetch
no-rel
class="!no-underline !text-current"
to="https://twitter.com/NocoDB"
target="_blank"
@ -112,8 +129,16 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link v-e="['e:hiring']" class="!no-underline !text-current" target="_blank" to="http://careers.nocodb.com">
<nuxt-link
v-e="['e:hiring']"
no-prefetch
no-rel
class="!no-underline !text-current"
target="_blank"
to="http://careers.nocodb.com"
>
<div class="flex items-center text-sm">
<!-- todo: i18n -->
<div class="ml-3">
@ -122,9 +147,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link
v-e="['e:community:reddit']"
no-prefetch
no-rel
class="!no-underline !text-current"
target="_blank"
to="https://www.reddit.com/r/NocoDB/"

2
packages/nc-gui/components/general/Sponsors.vue

@ -25,7 +25,7 @@ const { nav = false } = defineProps<Props>()
</div>
<div class="flex justify-center">
<nuxt-link href="https://github.com/sponsors/nocodb" target="_blank">
<nuxt-link no-prefetch no-rel href="https://github.com/sponsors/nocodb" target="_blank">
<a-button class="!shadow rounded" size="large">
<div class="flex items-center">
<mdi-cards-heart class="text-red-500 mr-2" />

4
packages/nc-gui/components/general/Tooltip.vue

@ -1,5 +1,6 @@
<script lang="ts" setup>
import { onKeyStroke } from '@vueuse/core'
import { ref, watch } from '#imports'
interface Props {
// Key to be pressed on hover to trigger the tooltip
@ -7,9 +8,10 @@ interface Props {
wrapperClass?: string
}
const { modifierKey } = defineProps<Props>()
const { modifierKey, wrapperClass } = defineProps<Props>()
const showTooltip = ref(false)
const isMouseOver = ref(false)
if (modifierKey) {

28
packages/nc-gui/components/general/TruncateText.vue

@ -1,4 +1,6 @@
<script lang="ts" setup>
import { computed, ref } from '#imports'
interface Props {
placement?:
| 'top'
@ -16,24 +18,21 @@ interface Props {
length?: number
}
const props = withDefaults(defineProps<Props>(), {
placement: 'bottom',
length: 20,
})
const { placement = 'bottom', length = 20 } = defineProps<Props>()
const text = ref()
const enableTooltip = computed(() => text?.value?.textContent.length > props.length)
const enableTooltip = computed(() => text.value?.textContent.length > length)
const shortName = computed(() =>
text?.value?.textContent.length > props.length
? `${text?.value?.textContent.substr(0, props.length - 3)}...`
: text?.value?.textContent,
text.value?.textContent.length > length ? `${text.value?.textContent.substr(0, length - 3)}...` : text.value?.textContent,
)
</script>
<template>
<a-tooltip v-if="enableTooltip" :placement="props.placement">
<a-tooltip v-if="enableTooltip" :placement="placement">
<template #title>
<slot></slot>
<slot />
</template>
<div class="w-full">{{ shortName }}</div>
</a-tooltip>
@ -41,4 +40,11 @@ const shortName = computed(() =>
<div ref="text" class="hidden"><slot></slot></div>
</template>
<style scoped></style>
<div v-else>
<slot />
</div>
<div ref="text" class="hidden">
<slot />
</div>
</template>

19
packages/nc-gui/components/monaco/Editor.vue

@ -1,10 +1,9 @@
<script setup lang="ts">
import * as monaco from 'monaco-editor'
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import TypescriptWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import { onMounted } from '#imports'
import { deepCompare } from '~/utils'
import type { editor as MonacoEditor } from 'monaco-editor'
import { deepCompare, onMounted, ref, watch } from '#imports'
interface Props {
modelValue: string | Record<string, any>
@ -60,7 +59,8 @@ self.MonacoEnvironment = {
}
const root = ref<HTMLDivElement>()
let editor: monaco.editor.IStandaloneCodeEditor
let editor: MonacoEditor.IStandaloneCodeEditor
const format = () => {
editor.setValue(JSON.stringify(JSON.parse(editor?.getValue() as string), null, 2))
@ -71,18 +71,20 @@ defineExpose({
isValid,
})
onMounted(() => {
onMounted(async () => {
const { editor: monacoEditor, languages } = await import('monaco-editor')
if (root.value && lang) {
const model = monaco.editor.createModel(vModel, lang)
const model = monacoEditor.createModel(vModel, lang)
if (lang === 'json') {
// configure the JSON language support with schemas and schema associations
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
languages.json.jsonDefaults.setDiagnosticsOptions({
validate: validate as boolean,
})
}
editor = monaco.editor.create(root.value, {
editor = monacoEditor.create(root.value, {
model,
theme: 'vs',
foldingStrategy: 'indentation',
@ -107,6 +109,7 @@ onMounted(() => {
vModel = editor.getValue()
} else {
const obj = JSON.parse(editor.getValue())
if (!obj || !deepCompare(vModel, obj)) vModel = obj
}
} catch (e) {

17
packages/nc-gui/components/shared-view/AskPassword.vue

@ -1,18 +1,19 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { extractSdkResponseErrorMsg } from '~/utils'
import { extractSdkResponseErrorMsg, message, ref, useRoute, useSharedView, useVModel } from '#imports'
interface Props {
const props = defineProps<{
modelValue: boolean
}
const props = defineProps<Props>()
}>()
const emit = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emit)
const route = useRoute()
const { loadSharedView } = useSharedView()
const formState = ref({ password: undefined })
const vModel = useVModel(props, 'modelValue', emit)
const onFinish = async () => {
try {
@ -38,14 +39,14 @@ const onFinish = async () => {
>
<div class="w-full flex flex-col">
<a-typography-title :level="4">This shared view is protected</a-typography-title>
<a-form ref="formRef" :model="formState" class="mt-2" @finish="onFinish">
<a-form-item name="password" :rules="[{ required: true, message: 'Password is required' }]">
<a-input-password v-model:value="formState.password" placeholder="Enter password" />
</a-form-item>
<a-button type="primary" html-type="submit">Unlock</a-button>
</a-form>
</div>
</a-modal>
</template>
<style scoped lang="scss"></style>

32
packages/nc-gui/components/shared-view/Grid.vue

@ -1,12 +1,31 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '#imports'
import {
ActiveViewInj,
FieldsInj,
IsPublicInj,
MetaInj,
ReadonlyInj,
ReloadViewDataHookInj,
createEventHook,
extractSdkResponseErrorMsg,
message,
provide,
ref,
useGlobal,
useProject,
useProvideSmartsheetStore,
useSharedView,
} from '#imports'
const { sharedView, meta, sorts, nestedFilters } = useSharedView()
const { signedIn } = useGlobal()
const { loadProject } = useProject(meta.value?.project_id)
const reloadEventHook = createEventHook<void>()
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
const reloadEventHook = createEventHook()
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, true)
@ -15,8 +34,6 @@ provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
if (signedIn.value) {
try {
await loadProject()
@ -29,8 +46,9 @@ if (signedIn.value) {
<template>
<div class="nc-container flex flex-col h-full mt-1.5 px-12">
<SmartsheetToolbar />
<SmartsheetGrid />
<LazySmartsheetToolbar />
<LazySmartsheetGrid />
</div>
</template>

28
packages/nc-gui/components/smartsheet/ApiSnippet.vue

@ -1,10 +1,11 @@
<script setup lang="ts">
import HTTPSnippet from 'httpsnippet'
import { message } from 'ant-design-vue'
import {
ActiveViewInj,
MetaInj,
inject,
message,
ref,
useCopy,
useGlobal,
useI18n,
@ -12,16 +13,15 @@ import {
useSmartsheetStoreOrThrow,
useVModel,
useViewData,
watch,
} from '#imports'
const props = defineProps<Props>()
const props = defineProps<{
modelValue: boolean
}>()
const emits = defineEmits(['update:modelValue'])
interface Props {
modelValue: boolean
}
const { t } = useI18n()
const { project } = $(useProject())
@ -81,7 +81,7 @@ const selectedLangName = $ref(langs[0].name)
const apiUrl = $computed(
() =>
new URL(`/api/v1/db/data/noco/${project.id}/${meta.title}/views/${view.title}`, (appInfo && appInfo.ncSiteUrl) || '/').href,
new URL(`/api/v1/db/data/noco/${project.id}/${meta?.title}/views/${view?.title}`, (appInfo && appInfo.ncSiteUrl) || '/').href,
)
const snippet = $computed(
@ -114,8 +114,8 @@ const api = new Api({
api.dbViewRow.list(
"noco",
${JSON.stringify(project.title)},
${JSON.stringify(meta.title)},
${JSON.stringify(view.title)}, ${JSON.stringify(queryParams, null, 4)}).then(function (data) {
${JSON.stringify(meta?.title)},
${JSON.stringify(view?.title)}, ${JSON.stringify(queryParams, null, 4)}).then(function (data) {
console.log(data);
}).catch(function (error) {
console.error(error);
@ -145,7 +145,6 @@ watch($$(activeLang), (newLang) => {
<a-drawer
v-model:visible="vModel"
class="h-full relative nc-drawer-api-snippet"
style="color: red"
placement="right"
size="large"
:closable="false"
@ -154,6 +153,7 @@ watch($$(activeLang), (newLang) => {
<div class="flex flex-col w-full h-full p-4">
<!-- Code Snippet -->
<a-typography-title :level="4" class="pb-1">{{ $t('title.codeSnippet') }}</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>
@ -161,7 +161,8 @@ watch($$(activeLang), (newLang) => {
{{ item.name }}
</div>
</template>
<monaco-editor
<LazyMonacoEditor
class="h-[60vh] border-1 border-gray-100 py-4 rounded-sm"
:model-value="code"
:read-only="true"
@ -170,6 +171,7 @@ watch($$(activeLang), (newLang) => {
:disable-deep-compare="true"
hide-minimap
/>
<div v-if="activeLang.clients" class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase">
<a-select
v-if="activeLang"
@ -181,6 +183,7 @@ watch($$(activeLang), (newLang) => {
{{ client }}
</a-select-option>
</a-select>
<a-button
v-e="[
'c:snippet:copy',
@ -188,7 +191,8 @@ watch($$(activeLang), (newLang) => {
]"
type="primary"
@click="onCopyToClipboard"
>{{ $t('general.copy') }}
>
{{ $t('general.copy') }}
</a-button>
</div>

48
packages/nc-gui/components/smartsheet/Cell.vue

@ -82,9 +82,7 @@ const isAutoSaved = $computed(() => {
].includes(column?.value?.uidt as UITypes)
})
const isManualSaved = $computed(() => {
return [UITypes.Currency, UITypes.Duration].includes(column?.value?.uidt as UITypes)
})
const isManualSaved = $computed(() => [UITypes.Currency, UITypes.Duration].includes(column?.value?.uidt as UITypes))
const vModel = computed({
get: () => props.modelValue,
@ -149,28 +147,28 @@ const syncAndNavigate = (dir: NavigateDir) => {
@keydown.stop.enter.exact="syncAndNavigate(NavigateDir.NEXT)"
@keydown.stop.shift.enter.exact="syncAndNavigate(NavigateDir.PREV)"
>
<CellTextArea v-if="isTextArea" v-model="vModel" />
<CellCheckbox v-else-if="isBoolean" v-model="vModel" />
<CellAttachment v-else-if="isAttachment" v-model="vModel" :row-index="props.rowIndex" />
<CellSingleSelect v-else-if="isSingleSelect" v-model="vModel" />
<CellMultiSelect v-else-if="isMultiSelect" v-model="vModel" />
<CellDatePicker v-else-if="isDate" v-model="vModel" />
<CellYearPicker v-else-if="isYear" v-model="vModel" />
<CellDateTimePicker v-else-if="isDateTime" v-model="vModel" />
<CellTimePicker v-else-if="isTime" v-model="vModel" />
<CellRating v-else-if="isRating" v-model="vModel" />
<CellDuration v-else-if="isDuration" v-model="vModel" />
<CellEmail v-else-if="isEmail" v-model="vModel" />
<CellUrl v-else-if="isURL" v-model="vModel" />
<CellPhoneNumber v-else-if="isPhoneNumber" v-model="vModel" />
<CellPercent v-else-if="isPercent" v-model="vModel" />
<CellCurrency v-else-if="isCurrency" v-model="vModel" />
<CellDecimal v-else-if="isDecimal" v-model="vModel" />
<CellInteger v-else-if="isInt" v-model="vModel" />
<CellFloat v-else-if="isFloat" v-model="vModel" />
<CellText v-else-if="isString" v-model="vModel" />
<CellJson v-else-if="isJSON" v-model="vModel" />
<CellText v-else v-model="vModel" />
<LazyCellTextArea v-if="isTextArea" v-model="vModel" />
<LazyCellCheckbox v-else-if="isBoolean" v-model="vModel" />
<LazyCellAttachment v-else-if="isAttachment" v-model="vModel" :row-index="props.rowIndex" />
<LazyCellSingleSelect v-else-if="isSingleSelect" v-model="vModel" />
<LazyCellMultiSelect v-else-if="isMultiSelect" v-model="vModel" />
<LazyCellDatePicker v-else-if="isDate" v-model="vModel" />
<LazyCellYearPicker v-else-if="isYear" v-model="vModel" />
<LazyCellDateTimePicker v-else-if="isDateTime" v-model="vModel" />
<LazyCellTimePicker v-else-if="isTime" v-model="vModel" />
<LazyCellRating v-else-if="isRating" v-model="vModel" />
<LazyCellDuration v-else-if="isDuration" v-model="vModel" />
<LazyCellEmail v-else-if="isEmail" v-model="vModel" />
<LazyCellUrl v-else-if="isURL" v-model="vModel" />
<LazyCellPhoneNumber v-else-if="isPhoneNumber" v-model="vModel" />
<LazyCellPercent v-else-if="isPercent" v-model="vModel" />
<LazyCellCurrency v-else-if="isCurrency" v-model="vModel" />
<LazyCellDecimal v-else-if="isDecimal" v-model="vModel" />
<LazyCellInteger v-else-if="isInt" v-model="vModel" />
<LazyCellFloat v-else-if="isFloat" v-model="vModel" />
<LazyCellText v-else-if="isString" v-model="vModel" />
<LazyCellJson v-else-if="isJSON" v-model="vModel" />
<LazyCellText v-else v-model="vModel" />
<div v-if="(isLocked || (isPublic && readOnly && !isForm)) && !isAttachment" class="nc-locked-overlay" @click.stop.prevent />
</div>
</template>

27
packages/nc-gui/components/smartsheet/Form.vue

@ -1,15 +1,17 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import { RelationTypes, UITypes, getSystemColumns, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import {
ActiveViewInj,
IsFormInj,
IsGalleryInj,
MetaInj,
ReloadViewDataHookInj,
computed,
createEventHook,
extractSdkResponseErrorMsg,
inject,
message,
onClickOutside,
provide,
reactive,
@ -23,7 +25,7 @@ import {
useViewData,
watch,
} from '#imports'
import type { Permission } from '~/composables/useUIPermission/rolePermissions'
import type { Permission } from '~/lib'
provide(IsFormInj, ref(true))
provide(IsGalleryInj, ref(false))
@ -39,7 +41,7 @@ const { $api, $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission()
const formState: Record<any, any> = reactive({})
const formState = reactive({})
const secondsRemain = ref(0)
@ -51,7 +53,8 @@ const view = inject(ActiveViewInj, ref())
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view)
const reloadEventHook = createEventHook<void>()
const reloadEventHook = createEventHook<boolean | void>()
provide(ReloadViewDataHookInj, reloadEventHook)
reloadEventHook.on(async () => {
@ -446,13 +449,13 @@ onMounted(async () => {
>
<div class="flex">
<div class="flex flex-row flex-1">
<SmartsheetHeaderVirtualCell
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(element)"
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
<LazySmartsheetHeaderCell
v-else
class="w-full"
:column="{ ...element, title: element.label || element.title }"
@ -461,7 +464,7 @@ onMounted(async () => {
/>
</div>
<div class="flex flex-row">
<mdi-drag-vertical class="flex flex-1" />
<MdiDragVertical class="flex flex-1" />
</div>
</div>
</a-card>
@ -480,7 +483,7 @@ onMounted(async () => {
</div>
</a-button>
<template #overlay>
<SmartsheetColumnEditOrAddProvider
<LazySmartsheetColumnEditOrAddProvider
v-if="showColumnDropdown"
@submit="submitCallback"
@cancel="showColumnDropdown = false"
@ -625,13 +628,13 @@ onMounted(async () => {
</div>
</template>
<div>
<SmartsheetHeaderVirtualCell
<LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(element)"
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
:hide-menu="true"
/>
<SmartsheetHeaderCell
<LazySmartsheetHeaderCell
v-else
:column="{ ...element, title: element.label || element.title }"
:required="isRequired(element, element.required)"
@ -645,7 +648,7 @@ onMounted(async () => {
:name="element.title"
:rules="[{ required: isRequired(element, element.required), message: `${element.title} is required` }]"
>
<SmartsheetVirtualCell
<LazySmartsheetVirtualCell
v-model="formState[element.title]"
:row="row"
class="nc-input"
@ -661,7 +664,7 @@ onMounted(async () => {
:name="element.title"
:rules="[{ required: isRequired(element, element.required), message: `${element.title} is required` }]"
>
<SmartsheetCell
<LazySmartsheetCell
v-model="formState[element.title]"
class="nc-input"
:class="`nc-form-input-${element.title.replaceAll(' ', '')}`"

64
packages/nc-gui/components/smartsheet/Gallery.vue

@ -1,5 +1,4 @@
<script lang="ts" setup>
import { onMounted } from '@vue/runtime-core'
import type { ColumnType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
import {
@ -14,15 +13,20 @@ import {
PaginationDataInj,
ReadonlyInj,
ReloadRowDataHookInj,
ReloadViewDataHookInj,
ReloadViewMetaHookInj,
computed,
createEventHook,
extractPkFromRow,
inject,
nextTick,
onMounted,
provide,
ref,
useUIPermission,
useViewData,
} from '#imports'
import Row from '~/components/smartsheet/Row.vue'
import type { Row as RowType } from '~/composables'
import ImageIcon from '~icons/mdi/file-image-box'
import type { Row as RowType } from '~/lib'
interface Attachment {
url: string
@ -80,7 +84,7 @@ const isRowEmpty = (record: any, col: any) => {
return Array.isArray(val) && val.length === 0
}
const attachments = (record: any): Array<Attachment> => {
const attachments = (record: any): Attachment[] => {
try {
return coverImageColumn?.title && record.row[coverImageColumn.title] ? JSON.parse(record.row[coverImageColumn.title]) : []
} catch (e) {
@ -91,7 +95,7 @@ const attachments = (record: any): Array<Attachment> => {
const expandForm = (row: RowType, state?: Record<string, any>) => {
if (!isUIAllowed('xcDatatableEditable')) return
const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
const rowId = extractPkFromRow(row.row, meta.value!.columns!)
if (rowId) {
router.push({
@ -138,7 +142,9 @@ const reloadAttachments = ref(false)
reloadViewMetaHook?.on(async () => {
await loadGalleryData()
reloadAttachments.value = true
nextTick(() => {
reloadAttachments.value = false
})
@ -149,6 +155,7 @@ reloadViewDataHook?.on(async () => {
onMounted(async () => {
await loadData()
await loadGalleryData()
})
@ -160,7 +167,7 @@ provide(ReloadRowDataHookInj, reloadViewDataHook)
<div class="flex flex-col h-full w-full overflow-auto nc-gallery">
<div class="nc-gallery-container grid gap-2 my-4 px-3">
<div v-for="record in data" :key="`record-${record.row.id}`">
<Row :row="record">
<LazySmartsheetRow :row="record">
<a-card
hoverable
class="!rounded-lg h-full overflow-hidden break-all max-w-[450px]"
@ -175,20 +182,26 @@ provide(ReloadRowDataHookInj, reloadViewDataHook)
</div>
</a>
</template>
<template #prevArrow>
<div style="z-index: 1"></div>
</template>
<template #nextArrow>
<div style="z-index: 1"></div>
</template>
<img
<LazyNuxtImg
v-for="(attachment, index) in attachments(record)"
:key="`carousel-${record.row.id}-${index}`"
quality="90"
placeholder
class="h-52 object-cover"
:src="attachment.url"
/>
</a-carousel>
<ImageIcon v-else class="w-full h-48 my-4 text-cool-gray-200" />
<MdiFileImageBox v-else class="w-full h-48 my-4 text-cool-gray-200" />
</template>
<div
@ -198,26 +211,43 @@ provide(ReloadRowDataHookInj, reloadViewDataHook)
>
<div class="flex flex-row w-full justify-start border-b-1 border-gray-100 py-2.5">
<div class="w-full text-gray-600">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
</div>
</div>
<div class="flex flex-row w-full pb-3 pt-2 pl-2 items-center justify-start">
<div v-if="isRowEmpty(record, col)" class="h-3 bg-gray-200 px-5 rounded-lg"></div>
<template v-else>
<SmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="record.row[col.title]" :column="col" :row="record" />
<SmartsheetCell v-else v-model="record.row[col.title]" :column="col" :edit-enabled="false" :read-only="true" />
<LazySmartsheetVirtualCell
v-if="isVirtualCol(col)"
v-model="record.row[col.title]"
:column="col"
:row="record"
/>
<LazySmartsheetCell
v-else
v-model="record.row[col.title]"
:column="col"
:edit-enabled="false"
:read-only="true"
/>
</template>
</div>
</div>
</a-card>
</Row>
</LazySmartsheetRow>
</div>
</div>
<div class="flex-1" />
<SmartsheetPagination />
<SmartsheetExpandedForm
<LazySmartsheetPagination />
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="expandedFormRow"
@ -226,7 +256,7 @@ provide(ReloadRowDataHookInj, reloadViewDataHook)
:view="view"
/>
<SmartsheetExpandedForm
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
:key="route.query.rowId"
v-model="expandedFormOnRowIdDlg"

35
packages/nc-gui/components/smartsheet/Grid.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import type { ColumnType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import {
ActiveViewInj,
CellUrlDisableOverlayInj,
@ -19,6 +18,7 @@ import {
createEventHook,
extractPkFromRow,
inject,
message,
onClickOutside,
onMounted,
provide,
@ -34,7 +34,7 @@ import {
useViewData,
watch,
} from '#imports'
import type { Row } from '~/composables'
import type { Row } from '~/lib'
import { NavigateDir } from '~/lib'
const { t } = useI18n()
@ -53,7 +53,7 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
const { isUIAllowed } = useUIPermission()
const hasEditPermission = isUIAllowed('xcDatatableEditable')
const hasEditPermission = $computed(() => isUIAllowed('xcDatatableEditable'))
const route = useRoute()
const router = useRouter()
@ -419,12 +419,13 @@ reloadViewDataHook.trigger()
</script>
<template>
<div class="flex flex-col h-full min-h-0 w-full">
<div class="relative flex flex-col h-full min-h-0 w-full">
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15">
<div class="flex items-center justify-center h-full w-full">
<div class="flex items-center justify-center h-full w-full !bg-white !bg-opacity-85 z-1000">
<a-spin size="large" />
</div>
</general-overlay>
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<a-dropdown
v-model:visible="contextMenu"
@ -467,9 +468,9 @@ reloadViewDataHook.trigger()
@xcresized="resizingCol = null"
>
<div class="w-full h-full bg-gray-100 flex items-center">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" :hide-menu="readOnly" />
<SmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="readOnly" />
</div>
</th>
<th
@ -501,7 +502,7 @@ reloadViewDataHook.trigger()
</tr>
</thead>
<tbody>
<SmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<LazySmartsheetRow v-for="(row, rowIndex) of data" ref="rowRefs" :key="rowIndex" :row="row">
<template #default="{ state }">
<tr class="nc-grid-row">
<td key="row-index" class="caption nc-grid-cell pl-5 pr-1">
@ -559,7 +560,7 @@ reloadViewDataHook.trigger()
@contextmenu="showContextMenu($event, { row: rowIndex, col: colIndex })"
>
<div class="w-full h-full">
<SmartsheetVirtualCell
<LazySmartsheetVirtualCell
v-if="isVirtualCol(columnObj)"
v-model="row.row[columnObj.title]"
:column="columnObj"
@ -568,7 +569,7 @@ reloadViewDataHook.trigger()
@navigate="onNavigate"
/>
<SmartsheetCell
<LazySmartsheetCell
v-else
v-model="row.row[columnObj.title]"
:column="columnObj"
@ -589,7 +590,7 @@ reloadViewDataHook.trigger()
</td>
</tr>
</template>
</SmartsheetRow>
</LazySmartsheetRow>
<tr v-if="!isView && !isLocked && isUIAllowed('xcDatatableEditable') && !isSqlView">
<td
@ -642,23 +643,19 @@ reloadViewDataHook.trigger()
</a-dropdown>
</div>
<SmartsheetPagination />
<LazySmartsheetPagination />
<SmartsheetExpandedForm
<LazySmartsheetExpandedForm
v-if="expandedFormRow && expandedFormDlg"
v-model="expandedFormDlg"
:row="expandedFormRow"
:state="expandedFormRowState"
:meta="meta"
:view="view"
@update:model-value="
() => {
if (!skipRowRemovalOnCancel) removeRowIfNew(expandedFormRow)
}
"
@update:model-value="!skipRowRemovalOnCancel && removeRowIfNew(expandedFormRow)"
/>
<SmartsheetExpandedForm
<LazySmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
:key="route.query.rowId"
v-model="expandedFormOnRowIdDlg"

3
packages/nc-gui/components/smartsheet/Pagination.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { computed, inject } from '#imports'
import { ChangePageInj, PaginationDataInj } from '~/context'
import { ChangePageInj, PaginationDataInj, computed, inject } from '#imports'
const paginatedData = inject(PaginationDataInj)!

22
packages/nc-gui/components/smartsheet/Row.vue

@ -1,15 +1,25 @@
<script lang="ts" setup>
import type { Row } from '~/composables'
import { ReloadRowDataHookInj, useProvideSmartsheetRowStore, useSmartsheetStoreOrThrow } from '#imports'
interface Props {
import type { Row } from '~/lib'
import {
ReloadRowDataHookInj,
ReloadViewDataHookInj,
createEventHook,
inject,
provide,
toRef,
useProvideSmartsheetRowStore,
useSmartsheetStoreOrThrow,
watch,
} from '#imports'
const props = defineProps<{
row: Row
}
}>()
const props = defineProps<Props>()
const currentRow = toRef(props, 'row')
const { meta } = useSmartsheetStoreOrThrow()
const { isNew, state, syncLTARRefs } = useProvideSmartsheetRowStore(meta, currentRow)
// on changing isNew(new record insert) status sync LTAR cell values

29
packages/nc-gui/components/smartsheet/Toolbar.vue

@ -1,6 +1,5 @@
<script setup lang="ts">
import { IsPublicInj, useSharedView, useSidebar, useSmartsheetStoreOrThrow } from '#imports'
import ToggleDrawer from '~/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue'
import { IsPublicInj, inject, ref, useSharedView, useSidebar, useSmartsheetStoreOrThrow, useUIPermission } from '#imports'
const { isGrid, isForm, isGallery, isKanban, isSqlView } = useSmartsheetStoreOrThrow()
@ -18,40 +17,40 @@ const { allowCSVDownload } = useSharedView()
class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[var(--toolbar-height)] px-2 border-b overflow-x-hidden"
style="z-index: 7"
>
<SmartsheetToolbarViewActions
<LazySmartsheetToolbarViewActions
v-if="(isGrid || isGallery || isKanban) && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false"
class="ml-1"
/>
<SmartsheetToolbarViewInfo v-if="!isUIAllowed('dataInsert') && !isPublic" />
<LazySmartsheetToolbarViewInfo v-if="!isUIAllowed('dataInsert') && !isPublic" />
<SmartsheetToolbarStackedBy v-if="isKanban" />
<LazySmartsheetToolbarStackedBy v-if="isKanban" />
<SmartsheetToolbarKanbanStackEditOrAdd v-if="isKanban" />
<LazySmartsheetToolbarKanbanStackEditOrAdd v-if="isKanban" />
<SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery || isKanban" :show-system-fields="false" class="ml-1" />
<LazySmartsheetToolbarFieldsMenu v-if="isGrid || isGallery || isKanban" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery || isKanban" />
<LazySmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery || isKanban" />
<SmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban" />
<LazySmartsheetToolbarSortListMenu v-if="isGrid || isGallery || isKanban" />
<SmartsheetToolbarShareView v-if="(isForm || isGrid || isKanban) && !isPublic" />
<LazySmartsheetToolbarShareView v-if="(isForm || isGrid || isKanban) && !isPublic" />
<SmartsheetToolbarExport
<LazySmartsheetToolbarExport
v-if="(!isPublic && !isUIAllowed('dataInsert') && !isKanban) || (isPublic && allowCSVDownload && !isKanban)"
/>
<div class="flex-1" />
<SmartsheetToolbarReload v-if="!isPublic && !isForm" class="mx-1" />
<LazySmartsheetToolbarReload v-if="!isPublic && !isForm" class="mx-1" />
<SmartsheetToolbarAddRow v-if="isUIAllowed('dataInsert') && !isPublic && !isForm && !isSqlView" class="mx-1" />
<LazySmartsheetToolbarAddRow v-if="isUIAllowed('dataInsert') && !isPublic && !isForm && !isSqlView" class="mx-1" />
<SmartsheetToolbarSearchData v-if="(isGrid || isGallery || isKanban) && !isPublic" class="shrink mr-2 ml-2" />
<LazySmartsheetToolbarSearchData v-if="(isGrid || isGallery || isKanban) && !isPublic" class="shrink mr-2 ml-2" />
<template v-if="!isOpen && !isPublic">
<div class="border-l-1 pl-3">
<ToggleDrawer class="mr-2" />
<LazySmartsheetSidebarToolbarToggleDrawer class="mr-2" />
</div>
</template>
</div>

40
packages/nc-gui/components/smartsheet/VirtualCell.vue

@ -1,33 +1,17 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj, provide, toRef, useVirtualCell } from '#imports'
import type { Row } from '~/composables'
import type { Row } from '~/lib'
import { NavigateDir } from '~/lib'
const props = defineProps<Props>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const HasMany = defineAsyncComponent(() => import('../virtual-cell/HasMany.vue'))
const ManyToMany = defineAsyncComponent(() => import('../virtual-cell/ManyToMany.vue'))
const BelongsTo = defineAsyncComponent(() => import('../virtual-cell/BelongsTo.vue'))
const Rollup = defineAsyncComponent(() => import('../virtual-cell/Rollup.vue') as any)
const Formula = defineAsyncComponent(() => import('../virtual-cell/Formula.vue'))
const Count = defineAsyncComponent(() => import('../virtual-cell/Count.vue'))
const Lookup = defineAsyncComponent(() => import('../virtual-cell/Lookup.vue') as any)
interface Props {
const props = defineProps<{
column: ColumnType
modelValue: any
row: Row
active?: boolean
}
}>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const column = toRef(props, 'column')
const active = toRef(props, 'active', false)
@ -47,12 +31,12 @@ const { isLookup, isBt, isRollup, isMm, isHm, isFormula, isCount } = useVirtualC
@keydown.stop.enter.exact="emit('navigate', NavigateDir.NEXT)"
@keydown.stop.shift.enter.exact="emit('navigate', NavigateDir.PREV)"
>
<HasMany v-if="isHm" />
<ManyToMany v-else-if="isMm" />
<BelongsTo v-else-if="isBt" />
<Rollup v-else-if="isRollup" />
<Formula v-else-if="isFormula" />
<Count v-else-if="isCount" />
<Lookup v-else-if="isLookup" />
<LazyVirtualCellHasMany v-if="isHm" />
<LazyVirtualCellManyToMany v-else-if="isMm" />
<LazyVirtualCellBelongsTo v-else-if="isBt" />
<LazyVirtualCellRollup v-else-if="isRollup" />
<LazyVirtualCellFormula v-else-if="isFormula" />
<LazyVirtualCellCount v-else-if="isCount" />
<LazyVirtualCellLookup v-else-if="isLookup" />
</div>
</template>

18
packages/nc-gui/components/smartsheet-column/AdvancedOptions.vue → packages/nc-gui/components/smartsheet/column/AdvancedOptions.vue

@ -1,13 +1,13 @@
<script setup lang="ts">
import { UITypes } from 'nocodb-sdk'
import { computed } from '#imports'
import { computed, onBeforeMount, useColumnCreateStoreOrThrow, useProject, useVModel } from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { sqlUi, isPg } = useProject()
@ -58,6 +58,7 @@ onBeforeMount(() => {
@change="onAlter"
/>
</a-form-item>
<a-form-item label="PK">
<a-checkbox
v-model:checked="vModel.pk"
@ -66,6 +67,7 @@ onBeforeMount(() => {
@change="onAlter"
/>
</a-form-item>
<a-form-item label="AI">
<a-checkbox
v-model:checked="vModel.ai"
@ -74,13 +76,16 @@ onBeforeMount(() => {
@change="onAlter"
/>
</a-form-item>
<a-form-item label="UN" :disabled="sqlUi.colPropUNDisabled(vModel) || !sqlUi.columnEditable(vModel)" @change="onAlter">
<a-checkbox v-model:checked="vModel.un" class="nc-column-checkbox-UN" />
</a-form-item>
<a-form-item label="AU" :disabled="sqlUi.colPropAuDisabled(vModel) || !sqlUi.columnEditable(vModel)" @change="onAlter">
<a-checkbox v-model:checked="vModel.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="vModel.dt" dropdown-class-name="nc-dropdown-db-type" @change="onDataTypeChange">
<a-select-option v-for="type in dataTypes" :key="type" :value="type">
@ -88,6 +93,7 @@ onBeforeMount(() => {
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')">
<a-input
v-model:value="vModel.dtxp"
@ -95,9 +101,11 @@ onBeforeMount(() => {
@input="onAlter"
/>
</a-form-item>
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale">
<a-input v-model:value="vModel.dtxs" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item>
<a-form-item :label="$t('placeholder.defaultValue')">
<a-textarea v-model:value="vModel.cdf" auto-size @input="onAlter(2, true)" />
<span class="text-gray-400 text-xs">{{ sampleValue }}</span>

14
packages/nc-gui/components/smartsheet-column/CheckboxOptions.vue → packages/nc-gui/components/smartsheet/column/CheckboxOptions.vue

@ -1,12 +1,12 @@
<script setup lang="ts">
import { getMdiIcon } from '@/utils'
import { computed, getMdiIcon, useVModel, watch } from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
// cater existing v1 cases
@ -88,6 +88,7 @@ watch(
color: vModel.meta.color,
}"
/>
<component
:is="getMdiIcon(icon.unchecked)"
:style="{
@ -100,8 +101,9 @@ watch(
</a-form-item>
</a-col>
</a-row>
<a-row class="w-full justify-center">
<GeneralColorPicker
<LazyGeneralColorPicker
v-model="picked"
:row-size="8"
:colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']"

29
packages/nc-gui/components/smartsheet-column/CurrencyOptions.vue → packages/nc-gui/components/smartsheet/column/CurrencyOptions.vue

@ -1,20 +1,27 @@
<script setup lang="ts">
import { useProject } from '#imports'
import { currencyCodes, currencyLocales, validateCurrencyCode, validateCurrencyLocale } from '@/utils'
interface Props {
value: Record<string, any>
}
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
import {
computed,
currencyCodes,
currencyLocales,
useProject,
useVModel,
validateCurrencyCode,
validateCurrencyLocale,
} from '#imports'
interface Option {
label: string
value: string
}
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const validators = {
'meta.currency_locale': [
{
@ -91,6 +98,7 @@ vModel.value.meta = {
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item v-bind="validateInfos['meta.currency_code']" label="Currency Code">
<a-select
@ -107,6 +115,7 @@ vModel.value.meta = {
</a-select>
</a-form-item>
</a-col>
<a-col v-if="isMoney && isPg">
<span class="text-[#FB8C00]">{{ message }}</span>
</a-col>

10
packages/nc-gui/components/smartsheet-column/DateOptions.vue → packages/nc-gui/components/smartsheet/column/DateOptions.vue

@ -1,12 +1,12 @@
<script setup lang="ts">
import { dateFormats } from '~/utils'
import { dateFormats, useVModel } from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta?.date_format) {

11
packages/nc-gui/components/smartsheet-column/DurationOptions.vue → packages/nc-gui/components/smartsheet/column/DurationOptions.vue

@ -1,12 +1,12 @@
<script setup lang="ts">
import { durationOptions } from '@/utils'
import { durationOptions, useVModel } from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const durationOptionList =
@ -28,6 +28,7 @@ vModel.value.meta = {
<a-col :span="24">
<span class="prose-sm mt-2">A duration of time in minutes or seconds (e.g. 1:23).</span>
</a-col>
<a-col :span="24">
<a-form-item label="Duration Format">
<a-select v-model:value="vModel.meta.duration" class="w-52" dropdown-class-name="nc-dropdown-duration-option">

38
packages/nc-gui/components/smartsheet-column/EditOrAdd.vue → packages/nc-gui/components/smartsheet/column/EditOrAdd.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import {
IsFormInj,
IsKanbanInj,
@ -9,7 +8,12 @@ import {
ReloadViewMetaHookInj,
computed,
inject,
message,
onMounted,
ref,
uiTypes,
useColumnCreateStoreOrThrow,
useI18n,
useMetas,
useNuxtApp,
watchEffect,
@ -75,7 +79,7 @@ async function onSubmit() {
if (!saved) return
// add delay to complete the minimize transition
// add delay to complete minimize transition
setTimeout(() => {
advancedOptions.value = false
}, 500)
@ -100,7 +104,7 @@ watchEffect(() => {
})
onMounted(() => {
if (isEdit.value === false) {
if (!isEdit.value) {
generateNewColumnMeta()
} else {
if (formState.value.pk) {
@ -122,7 +126,7 @@ onMounted(() => {
:class="{ '!w-[600px]': formState.uidt === UITypes.Formula }"
@click.stop
>
<a-form v-if="formState" v-model="formState" no-style name="column-create-or-edit" layout="vertical">
<a-form v-model="formState" no-style 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
@ -154,20 +158,21 @@ onMounted(() => {
</a-select-option>
</a-select>
</a-form-item>
<SmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<SmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<SmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<SmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<SmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<SmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<SmartsheetColumnLinkedToAnotherRecordOptions
<LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<LazySmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<LazySmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"
v-model:value="formState"
/>
<SmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnSelectOptions
<LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<LazySmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
v-model:value="formState"
/>
@ -194,7 +199,7 @@ onMounted(() => {
</span>
</a-checkbox>
<SmartsheetColumnAdvancedOptions v-model:value="formState" />
<LazySmartsheetColumnAdvancedOptions v-model:value="formState" />
</div>
</Transition>
@ -204,6 +209,7 @@ onMounted(() => {
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button html-type="submit" type="primary" @click.prevent="onSubmit">
<!-- Save -->
{{ $t('general.save') }}

11
packages/nc-gui/components/smartsheet-column/EditOrAddProvider.vue → packages/nc-gui/components/smartsheet/column/EditOrAddProvider.vue

@ -1,10 +1,10 @@
<script lang="ts" setup>
// todo: Remove this "Provider" component and use the "EditOrAdd" component directly
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { MetaInj, inject } from '#imports'
import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#imports'
interface Props {
column?: Ref<ColumnType & { meta: any }>
column?: ColumnType & { meta: any }
}
const props = defineProps<Props>()
@ -14,9 +14,10 @@ const emit = defineEmits(['submit', 'cancel'])
const meta = inject(MetaInj, ref())
const column = toRef(props, 'column')
useProvideColumnCreateStore(meta, column as Ref<ColumnType | undefined>)
useProvideColumnCreateStore(meta, column)
</script>
<template>
<SmartsheetColumnEditOrAdd @submit="emit('submit')" @cancel="emit('cancel')"></SmartsheetColumnEditOrAdd>
<SmartsheetColumnEditOrAdd @submit="emit('submit')" @cancel="emit('cancel')" />
</template>

15
packages/nc-gui/components/smartsheet-column/FormulaOptions.vue → packages/nc-gui/components/smartsheet/column/FormulaOptions.vue

@ -16,15 +16,16 @@ import {
onMounted,
useColumnCreateStoreOrThrow,
useDebounceFn,
useVModel,
validateDateWithUnknownFormat,
} from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, sqlUi, column } = useColumnCreateStoreOrThrow()
@ -619,6 +620,7 @@ onMounted(() => {
@change="handleInputDeb"
/>
</a-form-item>
<div class="text-gray-600 mt-2 mb-4 prose-sm">
Hint: Use {} to reference columns, e.g: {column_name}. For more, please check out
<a class="prose-sm" href="https://docs.nocodb.com/setup-and-usages/formulas#available-formula-features" target="_blank">
@ -644,16 +646,19 @@ onMounted(() => {
<a-col :span="6">
<span class="prose-sm text-gray-600">{{ item.text }}</span>
</a-col>
<a-col :span="18">
<div v-if="item.type === 'function'" class="text-xs text-gray-500">
{{ item.description }} <br /><br />
Syntax: <br />
{{ item.syntax }} <br /><br />
Examples: <br />
<div v-for="(example, idx) of item.examples" :key="idx">
<div>({{ idx + 1 }}): {{ example }}</div>
</div>
</div>
<div v-if="item.type === 'column'" class="float-right mr-5 -mt-2">
<a-badge-ribbon :text="item.uidt" color="gray" />
</div>
@ -663,7 +668,9 @@ onMounted(() => {
<template #avatar>
<mdi-function v-if="item.type === 'function'" class="text-lg" />
<mdi-calculator v-if="item.type === 'op'" class="text-lg" />
<component :is="item.icon" v-if="item.type === 'column'" class="text-lg" />
</template>
</a-list-item-meta>

14
packages/nc-gui/components/smartsheet-column/LinkedToAnotherRecordOptions.vue → packages/nc-gui/components/smartsheet/column/LinkedToAnotherRecordOptions.vue

@ -1,15 +1,15 @@
<script setup lang="ts">
import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk'
import { MetaInj, inject, useProject } from '#imports'
import { MetaInj, inject, ref, useProject, useVModel } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref()))
@ -57,6 +57,7 @@ const refTables = $computed(() => {
<a-radio value="mm">Many To Many</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table"
:label="$t('labels.childTable')"
@ -81,6 +82,7 @@ const refTables = $computed(() => {
@click="advancedOptions = !advancedOptions"
>
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
@ -99,6 +101,7 @@ const refTables = $computed(() => {
</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="vModel.onDelete"
@ -113,6 +116,7 @@ const refTables = $computed(() => {
</a-select>
</a-form-item>
</div>
<div class="flex flex-row">
<a-form-item>
<a-checkbox v-model:checked="vModel.virtual" name="virtual" @change="onDataTypeChange">Virtual Relation</a-checkbox>

11
packages/nc-gui/components/smartsheet-column/LookupOptions.vue → packages/nc-gui/components/smartsheet/column/LookupOptions.vue

@ -1,14 +1,14 @@
<script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } from 'nocodb-sdk'
import { MetaInj } from '#imports'
import { MetaInj, inject, ref, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref()))
@ -77,6 +77,7 @@ const columns = $computed(() => {
</a-select-option>
</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="vModel.fk_lookup_column_id"

10
packages/nc-gui/components/smartsheet-column/PercentOptions.vue → packages/nc-gui/components/smartsheet/column/PercentOptions.vue

@ -1,14 +1,14 @@
<!-- File not in use for now -->
<script setup lang="ts">
import { precisions } from '#imports'
import { precisions, useVModel } from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta) vModel.value.meta = {}

14
packages/nc-gui/components/smartsheet-column/RatingOptions.vue → packages/nc-gui/components/smartsheet/column/RatingOptions.vue

@ -1,12 +1,12 @@
<script setup lang="ts">
import { getMdiIcon } from '#imports'
import { getMdiIcon, useVModel } from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
// cater existing v1 cases
@ -82,6 +82,7 @@ watch(
color: vModel.meta.color,
}"
/>
<component
:is="getMdiIcon(icon.empty)"
:style="{
@ -103,8 +104,9 @@ watch(
</a-form-item>
</a-col>
</a-row>
<a-row class="w-full justify-center">
<GeneralColorPicker
<LazyGeneralColorPicker
v-model="picked"
:row-size="8"
:colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']"

12
packages/nc-gui/components/smartsheet-column/RollupOptions.vue → packages/nc-gui/components/smartsheet/column/RollupOptions.vue

@ -1,13 +1,13 @@
<script setup lang="ts">
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { MetaInj, inject, useMetas, useProject } from '#imports'
import { MetaInj, inject, ref, useColumnCreateStoreOrThrow, useMetas, useProject, useVModel } from '#imports'
interface Props {
value: Record<string, any>
}
const props = defineProps<{
value: any
}>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref()))
@ -90,6 +90,7 @@ const columns = $computed(() => {
</a-select-option>
</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="vModel.fk_rollup_column_id"
@ -103,6 +104,7 @@ const columns = $computed(() => {
</a-select>
</a-form-item>
</div>
<a-form-item label="Aggregate function" v-bind="validateInfos.rollup_function">
<a-select
v-model:value="vModel.rollup_function"

27
packages/nc-gui/components/smartsheet-column/SelectOptions.vue → packages/nc-gui/components/smartsheet/column/SelectOptions.vue

@ -1,18 +1,11 @@
<script setup lang="ts">
import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk'
import { enumColor } from '@/utils'
import MdiDragIcon from '~icons/mdi/drag-vertical'
import MdiArrowDownDropCircle from '~icons/mdi/arrow-down-drop-circle'
import MdiClose from '~icons/mdi/close'
import MdiPlusIcon from '~icons/mdi/plus'
import { IsKanbanInj } from '#imports'
interface Props {
value: Record<string, any>
}
import { enumColor, onMounted, useColumnCreateStoreOrThrow, useVModel, watch, IsKanbanInj } from '#imports'
const props = defineProps<Props>()
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
@ -111,12 +104,17 @@ watch(inputs, () => {
overlay-class-name="nc-dropdown-select-color-options"
>
<template #overlay>
<GeneralColorPicker v-model="element.color" :pick-button="true" @update:model-value="colorMenus[index] = false" />
<LazyGeneralColorPicker
v-model="element.color"
:pick-button="true"
@update:model-value="colorMenus[index] = false"
/>
</template>
<MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" />
</a-dropdown>
<a-input ref="inputs" v-model:value="element.title" class="caption" />
<MdiClose class="ml-2" :style="{ color: 'red' }" @click="removeOption(index)" />
</div>
</template>
@ -124,9 +122,10 @@ watch(inputs, () => {
<div v-if="validateInfos?.['colOptions.options']?.help?.[0]?.[0]" class="text-error text-[10px] my-2">
{{ validateInfos['colOptions.options'].help[0][0] }}
</div>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()">
<div class="flex items-center">
<MdiPlusIcon />
<MdiPlus />
<span class="flex-auto">Add option</span>
</div>
</a-button>
@ -134,5 +133,3 @@ watch(inputs, () => {
</Draggable>
</div>
</template>
<style scoped lang="scss"></style>

0
packages/nc-gui/components/smartsheet-column/SpecificDBTypeOptions.vue → packages/nc-gui/components/smartsheet/column/SpecificDBTypeOptions.vue

13
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -25,7 +25,7 @@ watch(
<template>
<div class="h-full flex flex-col w-full bg-[#eceff1] p-2">
<div ref="commentsWrapperEl" class="flex-1 min-h-[100px] overflow-y-auto scrollbar-thin-dull p-2 space-y-2">
<v-skeleton-loader v-if="isCommentsLoading && !commentsAndLogs" type="list-item-avatar-two-line@8" />
<a-skeleton v-if="isCommentsLoading && !commentsAndLogs" type="list-item-avatar-two-line@8" />
<template v-else>
<div v-for="log of commentsAndLogs" :key="log.id" class="flex gap-1 text-xs">
@ -36,6 +36,7 @@ watch(
{{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }}
{{ log.op_type === 'COMMENT' ? 'commented' : log.op_sub_type === 'INSERT' ? 'created' : 'edited' }}
</p>
<p
v-if="log.op_type === 'COMMENT'"
class="block caption my-2 nc-chip w-full min-h-20px p-2 rounded"
@ -53,14 +54,19 @@ watch(
</div>
</template>
</div>
<div class="border-1 my-2 w-full" />
<div class="p-0">
<div class="flex justify-center">
<!-- Comments only -->
<a-checkbox v-model:checked="commentsOnly" v-e="['c:row-expand:comment-only']" @change="loadCommentsAndLogs"
>{{ $t('labels.commentsOnly') }}<span class="text-[11px] text-gray-500"></span>
<a-checkbox v-model:checked="commentsOnly" v-e="['c:row-expand:comment-only']" @change="loadCommentsAndLogs">
{{ $t('labels.commentsOnly') }}
<span class="text-[11px] text-gray-500" />
</a-checkbox>
</div>
<div class="shrink mt-2 flex">
<a-input
v-model:value="comment"
@ -76,6 +82,7 @@ watch(
<mdi-account-circle class="text-lg text-pink-300" small @click="saveComment" />
</div>
</template>
<template #suffix>
<mdi-keyboard-return v-if="comment" class="text-sm" small @click="saveComment" />
</template>

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

@ -62,15 +62,11 @@ const copyRecordUrl = () => {
{{ meta.title }}
</template>
<!-- todo: table doesn't exist?
<template v-else>
{{ table }}
</template>
-->
<template v-if="primaryValue">: {{ primaryValue }}</template>
</h5>
<div class="flex-1" />
<a-tooltip placement="bottom">
<template #title>
<div class="text-center w-full">{{ $t('general.reload') }}</div>
@ -88,6 +84,7 @@ const copyRecordUrl = () => {
@click="copyRecordUrl"
/>
</a-tooltip>
<a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Toggle comments draw -->
<template #title>

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

@ -1,18 +1,14 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import type { TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import Cell from '../Cell.vue'
import VirtualCell from '../VirtualCell.vue'
import Comments from './Comments.vue'
import Header from './Header.vue'
import {
FieldsInj,
IsFormInj,
MetaInj,
ReloadRowDataHookInj,
computedInject,
message,
provide,
ref,
toRef,
@ -22,7 +18,7 @@ import {
useVModel,
watch,
} from '#imports'
import type { Row } from '~/composables'
import type { Row } from '~/lib'
interface Props {
modelValue?: boolean
@ -39,7 +35,7 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel'])
const row = toRef(props, 'row')
const row = ref(props.row)
const state = toRef(props, 'state')
@ -135,7 +131,8 @@ export default {
:closable="false"
class="nc-drawer-expanded-form"
>
<Header :view="view" @cancel="onClose" />
<SmartsheetExpandedFormHeader :view="view" @cancel="onClose" />
<div class="!bg-gray-100 rounded flex-1">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[max(70vh,100%)]">
<div class="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container">
@ -147,14 +144,14 @@ export default {
class="mt-2 py-2"
:class="`nc-expand-col-${col.title}`"
>
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />
<LazySmartsheetHeaderCell v-else :column="col" />
<div class="!bg-white rounded px-1 min-h-[35px] flex items-center mt-2">
<VirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<Cell
<LazySmartsheetCell
v-else
v-model="row.row[col.title]"
:column="col"
@ -168,7 +165,7 @@ export default {
<div v-if="!isNew" class="nc-comments-drawer min-w-0 min-h-full max-h-full" :class="{ active: commentsDrawer }">
<div class="h-full">
<Comments v-if="commentsDrawer" />
<LazySmartsheetExpandedFormComments v-if="commentsDrawer" />
</div>
</div>
</div>

5
packages/nc-gui/components/smartsheet-header/Cell.vue → packages/nc-gui/components/smartsheet/header/Cell.vue

@ -26,11 +26,13 @@ const editColumnDropdown = ref(false)
>
<SmartsheetHeaderCellIcon v-if="column" />
<span v-if="column" class="name" style="white-space: nowrap" :title="column.title">{{ column.title }}</span>
<span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>
<template v-if="!hideMenu">
<div class="flex-1" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" @edit="editColumnDropdown = true" />
<LazySmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" @edit="editColumnDropdown = true" />
</template>
<a-dropdown
@ -41,6 +43,7 @@ const editColumnDropdown = ref(false)
overlay-class-name="nc-dropdown-edit-column"
>
<div />
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="editColumnDropdown"

3
packages/nc-gui/components/smartsheet-header/CellIcon.vue → packages/nc-gui/components/smartsheet/header/CellIcon.vue

@ -1,7 +1,7 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { ColumnInj, toRef, useColumn } from '#imports'
import { ColumnInj, computed, inject, toRef, useColumn } from '#imports'
import FilePhoneIcon from '~icons/mdi/file-phone'
import KeyIcon from '~icons/mdi/key-variant'
import JSONIcon from '~icons/mdi/code-json'
@ -28,6 +28,7 @@ import DurationIcon from '~icons/mdi/timer-outline'
const props = defineProps<{ columnMeta?: ColumnType }>()
const columnMeta = toRef(props, 'columnMeta')
const column = inject(ColumnInj, columnMeta)
const additionalColMeta = useColumn(column as Ref<ColumnType>)

14
packages/nc-gui/components/smartsheet-header/Menu.vue → packages/nc-gui/components/smartsheet/header/Menu.vue

@ -1,8 +1,18 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue'
import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { ColumnInj, IsLockedInj, MetaInj, extractSdkResponseErrorMsg, inject, useI18n, useMetas, useNuxtApp } from '#imports'
import {
ColumnInj,
IsLockedInj,
MetaInj,
Modal,
extractSdkResponseErrorMsg,
inject,
message,
useI18n,
useMetas,
useNuxtApp,
} from '#imports'
const { virtual = false } = defineProps<{ virtual?: boolean }>()

6
packages/nc-gui/components/smartsheet-header/VirtualCell.vue → packages/nc-gui/components/smartsheet/header/VirtualCell.vue

@ -99,7 +99,7 @@ const tooltipMsg = computed(() => {
<template>
<div class="flex items-center w-full text-xs text-gray-500 font-weight-medium" :class="{ 'h-full': column }">
<SmartsheetHeaderVirtualCellIcon v-if="column" />
<LazySmartsheetHeaderVirtualCellIcon v-if="column" />
<a-tooltip placement="bottom">
<template #title>
@ -112,7 +112,8 @@ const tooltipMsg = computed(() => {
<template v-if="!hideMenu">
<div class="flex-1" />
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
<LazySmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
</template>
<a-dropdown
@ -123,6 +124,7 @@ const tooltipMsg = computed(() => {
overlay-class-name="nc-dropdown-edit-column"
>
<div />
<template #overlay>
<SmartsheetColumnEditOrAddProvider
v-if="editColumnDropdown"

3
packages/nc-gui/components/smartsheet-header/VirtualCellIcon.vue → packages/nc-gui/components/smartsheet/header/VirtualCellIcon.vue

@ -2,7 +2,7 @@
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { ColumnInj, toRef } from '#imports'
import { ColumnInj, inject, ref, toRef } from '#imports'
import GenericIcon from '~icons/mdi/square-rounded'
import HMIcon from '~icons/mdi/table-arrow-right'
import BTIcon from '~icons/mdi/table-arrow-left'
@ -14,6 +14,7 @@ import SpecificDBTypeIcon from '~icons/mdi/database-settings'
import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before'
const props = defineProps<{ columnMeta?: ColumnType }>()
const columnMeta = toRef(props, 'columnMeta')
const column = inject(ColumnInj, ref(columnMeta)) as Ref<ColumnType & { colOptions: LookupType }>

3
packages/nc-gui/components/smartsheet/sidebar/MenuBottom.vue

@ -1,7 +1,6 @@
<script lang="ts" setup>
import { ViewTypes } from 'nocodb-sdk'
import { useNuxtApp } from '#app'
import { useSmartsheetStoreOrThrow, useUIPermission, viewIcons } from '#imports'
import { useNuxtApp, useSmartsheetStoreOrThrow, useUIPermission, viewIcons } from '#imports'
interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void

9
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -2,17 +2,17 @@
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Ref } from 'vue'
import Sortable from 'sortablejs'
import RenameableMenuItem from './RenameableMenuItem.vue'
import {
ActiveViewInj,
ViewListInj,
extractSdkResponseErrorMsg,
inject,
message,
onMounted,
ref,
resolveComponent,
useApi,
useDialog,
useI18n,
@ -22,7 +22,6 @@ import {
viewTypeAlias,
watch,
} from '#imports'
import DlgViewDelete from '~/components/dlg/ViewDelete.vue'
const emits = defineEmits<Emits>()
@ -190,7 +189,7 @@ async function onRename(view: ViewType) {
function openDeleteDialog(view: Record<string, any>) {
const isOpen = ref(true)
const { close } = useDialog(DlgViewDelete, {
const { close } = useDialog(resolveComponent('DlgViewDelete'), {
'modelValue': isOpen,
'view': view,
'onUpdate:modelValue': closeDialog,
@ -219,7 +218,7 @@ function openDeleteDialog(view: Record<string, any>) {
<template>
<a-menu ref="menuRef" :class="{ dragging }" class="nc-views-menu flex-1" :selected-keys="selected">
<RenameableMenuItem
<LazySmartsheetSidebarRenameableMenuItem
v-for="(view, index) of views"
:id="view.id"
:key="view.id"

15
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -1,8 +1,17 @@
<script lang="ts" setup>
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import type { WritableComputedRef } from '@vue/reactivity'
import { IsLockedInj, inject, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel, viewIcons } from '#imports'
import {
IsLockedInj,
inject,
message,
onKeyStroke,
useDebounceFn,
useNuxtApp,
useUIPermission,
useVModel,
viewIcons,
} from '#imports'
interface Props {
view: ViewType
@ -168,7 +177,7 @@ function onStopEdit() {
<a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" />
<div v-else>
<GeneralTruncateText>{{ vModel.alias || vModel.title }}</GeneralTruncateText>
<LazyGeneralTruncateText>{{ vModel.alias || vModel.title }}</LazyGeneralTruncateText>
</div>
<div class="flex-1" />

16
packages/nc-gui/components/smartsheet/sidebar/index.vue

@ -1,8 +1,5 @@
<script setup lang="ts">
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import MenuTop from './MenuTop.vue'
import MenuBottom from './MenuBottom.vue'
import Toolbar from './toolbar/index.vue'
import {
ActiveViewInj,
MetaInj,
@ -11,6 +8,7 @@ import {
inject,
provide,
ref,
useNuxtApp,
useRoute,
useRouter,
useSidebar,
@ -36,7 +34,7 @@ const { $e } = useNuxtApp()
provide(ViewListInj, views)
/** Sidebar visible */
const { isOpen } = useSidebar('nc-right-sidebar', { isOpen: true })
const { isOpen } = useSidebar('nc-right-sidebar')
const sidebarCollapsed = computed(() => !isOpen.value)
@ -105,24 +103,24 @@ function onCreate(view: ViewType) {
ref="sidebar"
:collapsed="sidebarCollapsed"
collapsiple
collapsed-width="50"
collapsed-width="0"
width="250"
class="relative shadow-md h-full"
theme="light"
>
<Toolbar
<LazySmartsheetSidebarToolbar
v-if="isOpen"
class="min-h-[var(--toolbar-height)] max-h-[var(--toolbar-height)] flex items-center py-3 px-3 justify-between border-b-1"
/>
<div v-if="isOpen" class="flex-1 flex flex-col min-h-0">
<MenuTop @open-modal="openModal" @deleted="loadViews" />
<LazySmartsheetSidebarMenuTop @open-modal="openModal" @deleted="loadViews" />
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="!my-3 w-full border-b-1" />
<MenuBottom @open-modal="openModal" />
<LazySmartsheetSidebarMenuBottom @open-modal="openModal" />
</div>
<dlg-view-create
<LazyDlgViewCreate
v-if="views"
v-model="modalOpen"
:title="viewCreateTitle"

3
packages/nc-gui/components/smartsheet/sidebar/toolbar/DebugMeta.vue

@ -15,13 +15,14 @@ const localTables = tables.value.filter((t) => metas[t.id as string])
<template #title>
<span> Debug Meta </span>
</template>
<mdi-bug-outline class="cursor-pointer" @click="editorOpen = true" />
</a-tooltip>
<a-modal v-model:visible="editorOpen" :footer="null" width="80%" wrap-class-name="nc-modal-debug-meta">
<a-tabs v-model:activeKey="tabKey" type="card" closeable="false" class="shadow-sm">
<a-tab-pane v-for="table in localTables" :key="table.id" :tab="table.title">
<MonacoEditor v-model="metas[table.id]" class="h-max-[70vh]" :read-only="true" />
<LazyMonacoEditor v-model="metas[table.id]" class="h-max-[70vh]" :read-only="true" />
</a-tab-pane>
</a-tabs>
</a-modal>

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

@ -1,8 +1,8 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { message, useApi, useI18n } from '#imports'
const { t } = useI18n()
const { api } = useApi()
async function deleteCache() {
@ -21,6 +21,7 @@ async function deleteCache() {
<template #title>
<span> Delete Cache </span>
</template>
<mdi-delete class="cursor-pointer" @click="deleteCache" />
</a-tooltip>
</template>

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { MetaInj, inject, useTable } from '#imports'
import { MetaInj, inject, ref, useSidebar, useTable } from '#imports'
const meta = inject(MetaInj, ref())

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save