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"> <script setup lang="ts">
import { computed, useRoute } from '#imports' import { computed, useRoute, useTheme } from '#imports'
const route = useRoute() const route = useRoute()
const disableBaseLayout = computed(() => route.path.startsWith('/nc/view') || route.path.startsWith('/nc/form')) const disableBaseLayout = computed(() => route.path.startsWith('/nc/view') || route.path.startsWith('/nc/form'))
useTheme()
</script> </script>
<template> <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 */ /* roboto-regular - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face { @font-face {
font-family: 'Roboto'; 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.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 */ 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 */ /* roboto-700 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face { @font-face {
font-family: 'Roboto'; 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.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 */ 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 */ /* roboto-700italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face { @font-face {
font-family: 'Roboto'; 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.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 */ 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 */ /* roboto-900 - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face { @font-face {
font-family: 'Roboto'; 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.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 */ 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 */ /* roboto-900italic - vietnamese_latin-ext_latin_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Roboto';
@ -157,30 +83,6 @@
/* Vazirmatn */ /* Vazirmatn */
/* https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v32.102/Vazirmatn-font-face.css */ /* 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-face {
font-family: Vazirmatn; font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-Regular.woff2') format('woff2'); src: url('./vazirmatn/Vazirmatn-Regular.woff2') format('woff2');
@ -189,22 +91,6 @@
font-display: swap; 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-face {
font-family: Vazirmatn; font-family: Vazirmatn;
src: url('./vazirmatn/Vazirmatn-Bold.woff2') format('woff2'); 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'] IcRoundSearch: typeof import('~icons/ic/round-search')['default']
IcTwotoneWidthFull: typeof import('~icons/ic/twotone-width-full')['default'] IcTwotoneWidthFull: typeof import('~icons/ic/twotone-width-full')['default']
IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['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'] LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default'] LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSwagger: typeof import('~icons/logos/swagger')['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'] MaterialSymbolsArrowCircleLeftRounded: typeof import('~icons/material-symbols/arrow-circle-left-rounded')['default']
MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default'] MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['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'] MaterialSymbolsRocketLaunchOutline: typeof import('~icons/material-symbols/rocket-launch-outline')['default']
MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default'] MaterialSymbolsSendOutline: typeof import('~icons/material-symbols/send-outline')['default']
MaterialSymbolsTranslate: typeof import('~icons/material-symbols/translate')['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'] MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccount: typeof import('~icons/mdi/account')['default'] MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['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'] MdiAlpha: typeof import('~icons/mdi/alpha')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default'] MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default'] MdiApi: typeof import('~icons/mdi/api')['default']
MdiArrowCollapse: typeof import('~icons/mdi/arrow-collapse')['default'] MdiArrowDownDropCircle: typeof import('~icons/mdi/arrow-down-drop-circle')['default']
MdiArrowDownDropCircleOutline: typeof import('~icons/mdi/arrow-down-drop-circle-outline')['default']
MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default'] MdiArrowExpand: typeof import('~icons/mdi/arrow-expand')['default']
MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default'] MdiArrowLeftBold: typeof import('~icons/mdi/arrow-left-bold')['default']
MdiAt: typeof import('~icons/mdi/at')['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'] MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default'] MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default'] MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDatabaseSync: typeof import('~icons/mdi/database-sync')['default']
MdiDelete: typeof import('~icons/mdi/delete')['default'] MdiDelete: typeof import('~icons/mdi/delete')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default'] MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
MdiDiscord: typeof import('~icons/mdi/discord')['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'] MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFileExcel: typeof import('~icons/mdi/file-excel')['default'] MdiFileExcel: typeof import('~icons/mdi/file-excel')['default']
MdiFileEyeOutline: typeof import('~icons/mdi/file-eye-outline')['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'] MdiFilePlusOutline: typeof import('~icons/mdi/file-plus-outline')['default']
MdiFileUploadOutline: typeof import('~icons/mdi/file-upload-outline')['default'] MdiFileUploadOutline: typeof import('~icons/mdi/file-upload-outline')['default']
MdiFilterOutline: typeof import('~icons/mdi/filter-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'] MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiNumeric: typeof import('~icons/mdi/numeric')['default'] MdiNumeric: typeof import('~icons/mdi/numeric')['default']
MdiOpenInNew: typeof import('~icons/mdi/open-in-new')['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'] MdiPencil: typeof import('~icons/mdi/pencil')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default'] MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusCircleOutline: typeof import('~icons/mdi/plus-circle-outline')['default'] MdiPlusCircleOutline: typeof import('~icons/mdi/plus-circle-outline')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default'] MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiReddit: typeof import('~icons/mdi/reddit')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default'] MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default'] MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['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"> <script setup lang="ts">
import MdiPlusIcon from '~icons/mdi/plus' import { useVModel } from '#imports'
import MdiDeleteOutlineIcon from '~icons/mdi/delete-outline'
interface Props { const props = defineProps<{
modelValue: Record<string, any>[] modelValue: any[]
} }>()
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const headerList = ref([ const headerList = [
'A-IM', 'A-IM',
'Accept', 'Accept',
'Accept-Charset', 'Accept-Charset',
@ -52,11 +49,11 @@ const headerList = ref([
'Dnt', 'Dnt',
'X-Requested-With', 'X-Requested-With',
'X-CSRF-Token', 'X-CSRF-Token',
]) ]
const addHeaderRow = () => vModel.value.push({}) const addHeaderRow = () => vModel.value.push({})
const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1) const deleteHeaderRow = (i: number) => vModel.value.splice(i, 1)
</script> </script>
<template> <template>
@ -67,17 +64,21 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
<th> <th>
<!-- Intended to be empty - For checkbox --> <!-- Intended to be empty - For checkbox -->
</th> </th>
<th> <th>
<div class="text-center font-normal mb-2">Header Name</div> <div class="text-center font-normal mb-2">Header Name</div>
</th> </th>
<th> <th>
<div class="text-center font-normal mb-2">Value</div> <div class="text-center font-normal mb-2">Value</div>
</th> </th>
<th> <th>
<!-- Intended to be empty - For delete button --> <!-- Intended to be empty - For delete button -->
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(headerRow, idx) in vModel" :key="idx"> <tr v-for="(headerRow, idx) in vModel" :key="idx">
<td class="px-2 nc-hook-header-tab-checkbox"> <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-checkbox v-model:checked="headerRow.enabled" />
</a-form-item> </a-form-item>
</td> </td>
<td class="px-2 w-min-[400px]"> <td class="px-2 w-min-[400px]">
<a-form-item> <a-form-item>
<a-select <a-select
@ -101,22 +103,25 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
</a-select> </a-select>
</a-form-item> </a-form-item>
</td> </td>
<td class="px-2 w-min-[400px]"> <td class="px-2 w-min-[400px]">
<a-form-item> <a-form-item>
<a-input v-model:value="headerRow.value" size="large" placeholder="Value" class="nc-input-hook-header-value" /> <a-input v-model:value="headerRow.value" size="large" placeholder="Value" class="nc-input-hook-header-value" />
</a-form-item> </a-form-item>
</td> </td>
<td class="relative"> <td class="relative">
<div v-if="idx !== 0" class="absolute flex flex-col justify-start mt-2 -right-6 top-0"> <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> </div>
</td> </td>
</tr> </tr>
<tr> <tr>
<td :colspan="12" class="text-center"> <td :colspan="12" class="text-center">
<a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addHeaderRow"> <a-button type="default" class="!bg-gray-100 rounded-md border-none mr-1" @click="addHeaderRow">
<template #icon> <template #icon>
<MdiPlusIcon class="flex mx-auto" /> <MdiPlus class="flex mx-auto" />
</template> </template>
</a-button> </a-button>
</td> </td>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,8 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Modal as AModal } from 'ant-design-vue' import { Modal as AModal, EditModeInj, IsFormInj, ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
import Editor from '~/components/monaco/Editor.vue'
import { ReadonlyInj, computed, inject, ref, useVModel, watch } from '#imports'
import { EditModeInj, IsFormInj } from '~/context'
interface Props { interface Props {
modelValue: string | Record<string, any> | undefined modelValue: string | Record<string, any> | undefined
@ -113,7 +110,7 @@ watch(editEnabled, () => {
</div> </div>
</div> </div>
<Editor <LazyMonacoEditor
:model-value="localValue" :model-value="localValue"
class="min-w-full w-80" class="min-w-full w-80"
:class="{ 'expanded-editor': isExpanded, 'editor': !isExpanded }" :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> <span class="text-slate-500" :class="{ 'text-sm': isKanban }">{{ op.title }}</span>
</a-tag> </a-tag>
</a-select-option> </a-select-option>
<template #tagRender="{ value: val, onClose }"> <template #tagRender="{ value: val, onClose }">
<a-tag <a-tag
v-if="options.find((el) => el.title === val)" v-if="options.find((el) => el.title === val)"

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

@ -1,11 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { EditModeInj, inject } from '#imports' import { EditModeInj, inject, useVModel } from '#imports'
interface Props { interface Props {
modelValue: number | string | null | undefined modelValue: number | string | null | undefined
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue'])
const editEnabled = inject(EditModeInj) const editEnabled = inject(EditModeInj)
@ -15,62 +16,6 @@ const vModel = useVModel(props, 'modelValue', emits)
<template> <template>
<input v-if="editEnabled" v-model="vModel" type="number" /> <input v-if="editEnabled" v-model="vModel" type="number" />
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
</template> </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"> <script setup lang="ts">
import Text from './Text.vue' import { useVModel } from '#imports'
interface Props { interface Props {
modelValue: any modelValue: any
@ -17,7 +17,5 @@ const vModel = useVModel(props, 'modelValue', emits)
</script> </script>
<template> <template>
<Text v-model="vModel" /> <LazyCellText v-model="vModel" />
</template> </template>
<style scoped></style>

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

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ColumnInj, computed, inject } from '#imports' import { ColumnInj, EditModeInj, computed, inject } from '#imports'
import { EditModeInj } from '~/context'
interface Props { interface Props {
modelValue?: number | null | undefined 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" class="h-full w-full outline-none bg-transparent"
@blur="editEnabled = false" @blur="editEnabled = false"
/> />
<span v-else>{{ vModel }}</span> <span v-else>{{ vModel }}</span>
</template> </template>

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

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

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { onClickOutside } from '@vueuse/core'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ReadonlyInj, inject, onClickOutside, useProject, watch } from '#imports'
interface Props { interface Props {
modelValue?: string | null | undefined modelValue?: string | null | undefined
@ -16,7 +16,7 @@ const readOnly = inject(ReadonlyInj, false)
let isTimeInvalid = $ref(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({ const localState = $computed({
get() { get() {

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

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

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

@ -82,7 +82,9 @@ onClickOutside(carouselRef, () => {
<template #customPaging="props"> <template #customPaging="props">
<a> <a>
<nuxt-img <LazyNuxtImg
quality="90"
placeholder
class="!block" class="!block"
:alt="imageItems[props.i].title || `#${props.i}`" :alt="imageItems[props.i].title || `#${props.i}`"
:src="imageItems[props.i].url || imageItems[props.i].data" :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 { onKeyDown } from '@vueuse/core'
import { useProvideAttachmentCell } from './utils' import { useProvideAttachmentCell } from './utils'
import { useSortable } from './sort' import { useSortable } from './sort'
import Modal from './Modal.vue'
import Carousel from './Carousel.vue'
import { import {
IsFormInj, IsFormInj,
IsGalleryInj, IsGalleryInj,
@ -133,7 +131,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
ref="attachmentCellRef" ref="attachmentCellRef"
class="nc-attachment-cell relative flex-1 color-transition flex items-center justify-between gap-1" 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)"> <template v-if="isSharedForm || (!isReadonly && !dragging && !!currentCellRef)">
<general-overlay <general-overlay
@ -183,7 +181,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
<div class="text-center w-full">{{ item.title }}</div> <div class="text-center w-full">{{ item.title }}</div>
</template> </template>
<nuxt-img <LazyNuxtImg
v-if="isImage(item.title, item.mimetype ?? item.type) && (item.url || item.data)" v-if="isImage(item.title, item.mimetype ?? item.type) && (item.url || item.data)"
quality="75" quality="75"
placeholder placeholder
@ -214,7 +212,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
</div> </div>
</template> </template>
<Modal /> <LazyCellAttachmentModal />
</div> </div>
</template> </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 { import {
ColumnInj, ColumnInj,
EditModeInj, EditModeInj,
@ -11,6 +9,7 @@ import {
computed, computed,
inject, inject,
isImage, isImage,
message,
ref, ref,
useApi, useApi,
useFileDialog, useFileDialog,
@ -146,7 +145,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
/** download a file */ /** download a file */
async function downloadFile(item: Record<string, any>) { 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) => { const FileIcon = (icon: string) => {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,19 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue'
import type { PluginType } from 'nocodb-sdk' import type { PluginType } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n' import { extractSdkResponseErrorMsg, message, onMounted, ref, useI18n, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, ref, useNuxtApp } from '#imports'
interface Props { const { id } = defineProps<{
id: string id: string
} }>()
type Plugin = PluginType & {
formDetails: Record<string, any>
parsedInput: Record<string, any>
}
const { id } = defineProps<Props>()
const emits = defineEmits(['saved', 'close']) const emits = defineEmits(['saved', 'close'])
@ -22,6 +13,11 @@ enum Action {
Test = 'test', Test = 'test',
} }
type Plugin = PluginType & {
formDetails: Record<string, any>
parsedInput: Record<string, any>
}
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const formRef = ref() const formRef = ref()
@ -29,8 +25,11 @@ const formRef = ref()
const { t } = useI18n() const { t } = useI18n()
let plugin = $ref<Plugin | null>(null) let plugin = $ref<Plugin | null>(null)
let pluginFormData = $ref<Record<string, any>>({}) let pluginFormData = $ref<Record<string, any>>({})
let isLoading = $ref(true) let isLoading = $ref(true)
let loadingAction = $ref<null | Action>(null) let loadingAction = $ref<null | Action>(null)
const layout = { const layout = {
@ -153,6 +152,7 @@ onMounted(async () => {
<span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span> <span class="font-semibold text-lg">{{ plugin.formDetails.title }}</span>
</div> </div>
<div class="absolute -right-2 -top-0.5"> <div class="absolute -right-2 -top-0.5">
<a-button type="text" class="!rounded-md mr-1" @click="emits('close')"> <a-button type="text" class="!rounded-md mr-1" @click="emits('close')">
<template #icon> <template #icon>
@ -175,6 +175,7 @@ onMounted(async () => {
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="(itemRow, itemIndex) in plugin.parsedInput" :key="itemIndex"> <tr v-for="(itemRow, itemIndex) in plugin.parsedInput" :key="itemIndex">
<td v-for="(columnData, columnIndex) in plugin.formDetails.items" :key="columnIndex" class="px-2"> <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]" v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-textarea <a-textarea
v-else-if="columnData.type === 'LongText'" v-else-if="columnData.type === 'LongText'"
v-model:value="itemRow[columnData.key]" v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-switch <a-switch
v-else-if="columnData.type === 'Checkbox'" v-else-if="columnData.type === 'Checkbox'"
v-model:value="itemRow[columnData.key]" v-model:value="itemRow[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-input v-else v-model:value="itemRow[columnData.key]" :placeholder="columnData.placeholder" /> <a-input v-else v-model:value="itemRow[columnData.key]" :placeholder="columnData.placeholder" />
<div <div
v-if="itemIndex !== 0 && columnIndex === plugin.formDetails.items.length - 1" v-if="itemIndex !== 0 && columnIndex === plugin.formDetails.items.length - 1"
class="absolute flex flex-col justify-start mt-2 -right-6 top-0" 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]" v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-textarea <a-textarea
v-else-if="columnData.type === 'LongText'" v-else-if="columnData.type === 'LongText'"
v-model:value="pluginFormData[columnData.key]" v-model:value="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-switch <a-switch
v-else-if="columnData.type === 'Checkbox'" v-else-if="columnData.type === 'Checkbox'"
v-model:checked="pluginFormData[columnData.key]" v-model:checked="pluginFormData[columnData.key]"
:placeholder="columnData.placeholder" :placeholder="columnData.placeholder"
/> />
<a-input v-else v-model:value="pluginFormData[columnData.key]" :placeholder="columnData.placeholder" /> <a-input v-else v-model:value="pluginFormData[columnData.key]" :placeholder="columnData.placeholder" />
</a-form-item> </a-form-item>
</template> </template>
<div class="flex flex-row space-x-4 justify-center mt-4"> <div class="flex flex-row space-x-4 justify-center mt-4">
<a-button <a-button
v-for="(action, i) in plugin.formDetails.actions" v-for="(action, i) in plugin.formDetails.actions"
@ -265,5 +274,3 @@ onMounted(async () => {
</div> </div>
</template> </template>
</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 type { Socket } from 'socket.io-client'
import io from 'socket.io-client' import io from 'socket.io-client'
import type { Card as AntCard } from 'ant-design-vue' import type { Card as AntCard } from 'ant-design-vue'
import { Form, message } from 'ant-design-vue'
import { import {
Form,
computed, computed,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
fieldRequiredValidator, fieldRequiredValidator,
message,
nextTick, nextTick,
onBeforeUnmount, onBeforeUnmount,
onMounted, onMounted,
@ -17,11 +18,9 @@ import {
watch, watch,
} from '#imports' } from '#imports'
interface Props { const { modelValue } = defineProps<{
modelValue: boolean modelValue: boolean
} }>()
const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -201,7 +200,6 @@ onMounted(async () => {
// connect event does not provide data // connect event does not provide data
socket.on('connect', () => { socket.on('connect', () => {
console.log(socket?.id)
console.log('socket connected') console.log('socket connected')
}) })
@ -337,9 +335,9 @@ onBeforeUnmount(() => {
<!-- This feature is currently in beta and more information can be found here --> <!-- This feature is currently in beta and more information can be found here -->
<div> <div>
{{ $t('general.betaNote') }} {{ $t('general.betaNote') }}
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">{{ <a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">
$t('general.moreInfo') {{ $t('general.moreInfo') }}
}}</a> </a>
. .
</div> </div>
</div> </div>

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

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

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

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

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

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

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

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

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

@ -1,12 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { message } from 'ant-design-vue' import { extractSdkResponseErrorMsg, message, onKeyStroke, useApi, useI18n, useNuxtApp, useVModel } from '#imports'
import { useI18n } from 'vue-i18n'
import { extractSdkResponseErrorMsg } from '~/utils'
import { onKeyStroke, useApi, useNuxtApp, useVModel } from '#imports'
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const { t } = useI18n()
interface Props { interface Props {
modelValue: boolean modelValue: boolean
view?: Record<string, any> view?: Record<string, any>
@ -17,6 +11,12 @@ interface Emits {
(event: 'deleted'): void (event: 'deleted'): void
} }
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const { t } = useI18n()
const vModel = useVModel(props, 'modelValue', emits) const vModel = useVModel(props, 'modelValue', emits)
const { api, isLoading } = useApi() const { api, isLoading } = useApi()
@ -55,9 +55,10 @@ async function onDelete() {
<template #footer> <template #footer>
<a-button key="back" @click="vModel = false">{{ $t('general.cancel') }}</a-button> <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 key="submit" danger html-type="submit" :loading="isLoading" @click="onDelete">
}}</a-button> {{ $t('general.submit') }}
</a-button>
</template> </template>
</a-modal> </a-modal>
</template> </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 type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import dagre from 'dagre' import dagre from 'dagre'
import TableNode from './TableNode.vue' import { onScopeDispose, watch } from '#imports'
import RelationEdge from './RelationEdge.vue'
interface Props { interface Props {
tables: any[] 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" /> <Controls class="!left-auto right-2 !top-3.5 !bottom-auto" :show-fit-view="false" :show-interactive="false" />
<template #node-custom="props"> <template #node-custom="props">
<TableNode :data="props.data" /> <LazyErdTableNode :data="props.data" />
</template> </template>
<template #edge-custom="props"> <template #edge-custom="props">
<RelationEdge v-bind="props" /> <LazyErdRelationEdge v-bind="props" />
</template> </template>
<Background /> <Background />
@ -218,6 +217,7 @@ watch([() => tables, () => config], init, { deep: true, flush: 'pre' })
<MdiTableLarge class="text-primary" /> <MdiTableLarge class="text-primary" />
<div>{{ $t('objects.table') }}</div> <div>{{ $t('objects.table') }}</div>
</div> </div>
<div class="flex flex-row items-center space-x-1 pt-1"> <div class="flex flex-row items-center space-x-1 pt-1">
<MdiEyeCircleOutline class="text-primary" /> <MdiEyeCircleOutline class="text-primary" />
<div>{{ $t('objects.sqlVIew') }}</div> <div>{{ $t('objects.sqlVIew') }}</div>

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

@ -101,6 +101,7 @@ export default {
:d="edgePath" :d="edgePath"
:marker-end="markerEnd" :marker-end="markerEnd"
/> />
<path <path
:id="id" :id="id"
:style="style" :style="style"
@ -135,6 +136,7 @@ export default {
:stroke-width="1.5" :stroke-width="1.5"
:transform="`rotate(45,${sourceX + 2},${sourceY - 4})`" :transform="`rotate(45,${sourceX + 2},${sourceY - 4})`"
/> />
<rect <rect
v-if="isManyToMany" v-if="isManyToMany"
class="nc-erd-edge-rect" 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 type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { MetaInj, computed, provide, toRefs, useNuxtApp } from '#imports'
interface Props extends NodeProps { interface Props extends NodeProps {
data: TableType & { showPkAndFk: boolean; showAllColumns: boolean } data: TableType & { showPkAndFk: boolean; showAllColumns: boolean }
@ -58,6 +59,7 @@ const relatedColumnId = (col: Record<string, any>) =>
</div> </div>
</div> </div>
</GeneralTooltip> </GeneralTooltip>
<div> <div>
<div <div
v-for="col in pkAndFkColumns" 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="w-full border-b-1 py-2 border-gray-100 keys"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`" :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>
<div class="w-full mb-1"></div> <div class="w-full mb-1"></div>
@ -93,16 +95,18 @@ const relatedColumnId = (col: Record<string, any>) =>
type="target" type="target"
:position="Position.Left" :position="Position.Left"
/> />
<SmartsheetHeaderVirtualCell :column="col" :hide-menu="true" />
<LazySmartsheetHeaderVirtualCell :column="col" :hide-menu="true" />
</div> </div>
<SmartsheetHeaderVirtualCell
<LazySmartsheetHeaderVirtualCell
v-else-if="isVirtualCol(col)" v-else-if="isVirtualCol(col)"
:column="col" :column="col"
:hide-menu="true" :hide-menu="true"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`" :class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
/> />
<SmartsheetHeaderCell <LazySmartsheetHeaderCell
v-else v-else
:column="col" :column="col"
:hide-menu="true" :hide-menu="true"

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

@ -1,15 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk' import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { ref, useGlobal, useMetas, useProject, watch } from '#imports'
const { table } = defineProps<{ table?: TableType }>() const { table } = defineProps<{ table?: TableType }>()
const { includeM2M } = useGlobal() const { includeM2M } = useGlobal()
const { tables: projectTables } = useProject() const { tables: projectTables } = useProject()
const tables = ref<TableType[]>([])
const { metas, getMeta } = useMetas() const { metas, getMeta } = useMetas()
const tables = ref<TableType[]>([])
let isLoading = $ref(true) let isLoading = $ref(true)
const showAdvancedOptions = ref(false) const showAdvancedOptions = ref(false)
@ -105,8 +108,9 @@ watch(
<a-spin size="large" /> <a-spin size="large" />
</div> </div>
</div> </div>
<div v-else class="relative h-full"> <div v-else class="relative h-full">
<ErdFlow :tables="tables" :config="config" /> <LazyErdFlow :tables="tables" :config="config" />
<div <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" 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> <template>
<Chrome v-model="picked" /> <Chrome v-model="picked" />
</template> </template>
<style scoped></style>

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

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

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

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

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

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

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

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

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

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

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

@ -43,16 +43,27 @@ export default {
<template> <template>
<teleport :disabled="teleportDisabled || (inline && !target)" :to="target || 'body'"> <teleport :disabled="teleportDisabled || (inline && !target)" :to="target || 'body'">
<div <Transition :name="transition ? 'fade' : undefined" mode="out-in">
v-bind="$attrs" <div
:class="[ v-show="!!vModel"
vModel ? 'opacity-100' : 'opacity-0 pointer-events-none', v-bind="$attrs"
inline ? 'absolute' : 'fixed', :class="[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"
]" >
class="z-100 top-0 left-0 bottom-0 right-0 bg-gray-700/75" <slot :is-open="vModel" />
> </div>
<slot :is-open="vModel" /> </Transition>
</div>
</teleport> </teleport>
</template> </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"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { computed, extractSdkResponseErrorMsg, message, onMounted, useGlobal, useNuxtApp } from '#imports'
import { extractSdkResponseErrorMsg, onMounted } from '#imports'
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -47,22 +46,38 @@ onMounted(async () => await fetchReleaseInfo())
<mdi-menu-down /> <mdi-menu-down />
</div> </div>
</a-button> </a-button>
<template #overlay> <template #overlay>
<div class="mt-1 bg-white shadow-lg !border"> <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"> <div class="nc-menu-item">
<mdi-script-text-outline /> <mdi-script-text-outline />
{{ latestRelease }} {{ $t('activity.upgrade.releaseNote') }} {{ latestRelease }} {{ $t('activity.upgrade.releaseNote') }}
</div> </div>
</nuxt-link> </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"> <div class="nc-menu-item">
<mdi-rocket-launch-outline /> <mdi-rocket-launch-outline />
<!-- How to upgrade? --> <!-- How to upgrade? -->
{{ $t('activity.upgrade.howTo') }} {{ $t('activity.upgrade.howTo') }}
</div> </div>
</nuxt-link> </nuxt-link>
<a-divider class="!m-0" /> <a-divider class="!m-0" />
<div class="nc-menu-item" @click="releaseAlert = false"> <div class="nc-menu-item" @click="releaseAlert = false">
<mdi-close /> <mdi-close />
<!-- Hide menu --> <!-- Hide menu -->

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

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from '#imports'
interface Props { interface Props {
url: string url: string
socialMedias: 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"> <div class="flex items-center space-x-1">
<MdiAccountPlusOutline class="mr-1 nc-share-base" /> <MdiAccountPlusOutline class="mr-1 nc-share-base" />
<div>{{ $t('activity.inviteTeam') }}</div> <div>{{ $t('activity.inviteTeam') }}</div>
</div> </div>
</div> </div>
<TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" /> <LazyTabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
</div> </div>
</template> </template>

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

@ -1,9 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useI18n } from 'vue-i18n' import { useI18n } from '#imports'
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'
const { locale } = useI18n() 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"> <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')" /> <MdiDiscord v-e="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" />
<div <div
v-e="['e:community:discourse']" v-e="['e:community:discourse']"
class="icon flex items-center justify-center min-w-[43px]" 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 class="discourse" />
</div> </div>
<MdiReddit v-e="['e:community:reddit']" class="icon text-[#FF4600]" @click="open('https://www.reddit.com/r/NocoDB/')" /> <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')" /> <MdiTwitter v-e="['e:community:twitter']" class="icon text-[#1DA1F2]" @click="open('https://twitter.com/NocoDB')" />
<MdiCalendarMonth <MdiCalendarMonth
v-e="['e:community:book-demo']" v-e="['e:community:book-demo']"
class="icon text-green-500" class="icon text-green-500"

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

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { enumColor as colors } from '#imports' import { enumColor as colors, useGlobal } from '#imports'
const { lang: currentLang } = useGlobal() const { lang: currentLang } = useGlobal()
@ -14,6 +14,8 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:docs']" v-e="['e:docs']"
no-prefetch
no-rel
class="text-primary !no-underline !text-current" class="text-primary !no-underline !text-current"
target="_blank" target="_blank"
to="https://docs.nocodb.com/" to="https://docs.nocodb.com/"
@ -24,9 +26,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:api-docs']" v-e="['e:api-docs']"
no-prefetch
no-rel
class="text-primary !no-underline !text-current" class="text-primary !no-underline !text-current"
target="_blank" target="_blank"
to="https://apis.nocodb.com/" to="https://apis.nocodb.com/"
@ -38,9 +43,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:github']" v-e="['e:community:github']"
no-prefetch
no-rel
class="text-primary !no-underline !text-current" class="text-primary !no-underline !text-current"
to="https://github.com/nocodb/nocodb" to="https://github.com/nocodb/nocodb"
target="_blank" target="_blank"
@ -64,9 +72,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:book-demo']" v-e="['e:community:book-demo']"
no-prefetch
no-rel
class="!no-underline !text-current" class="!no-underline !text-current"
to="https://calendly.com/nocodb-meeting" to="https://calendly.com/nocodb-meeting"
target="_blank" target="_blank"
@ -80,9 +91,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:discord']" v-e="['e:community:discord']"
no-prefetch
no-rel
class="!no-underline !text-current" class="!no-underline !text-current"
to="https://discord.gg/5RgZmkW" to="https://discord.gg/5RgZmkW"
target="_blank" target="_blank"
@ -96,9 +110,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:twitter']" v-e="['e:community:twitter']"
no-prefetch
no-rel
class="!no-underline !text-current" class="!no-underline !text-current"
to="https://twitter.com/NocoDB" to="https://twitter.com/NocoDB"
target="_blank" target="_blank"
@ -112,8 +129,16 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<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"> <div class="flex items-center text-sm">
<!-- todo: i18n --> <!-- todo: i18n -->
<div class="ml-3"> <div class="ml-3">
@ -122,9 +147,12 @@ const isRtlLang = $computed(() => ['fa', 'ar'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link <nuxt-link
v-e="['e:community:reddit']" v-e="['e:community:reddit']"
no-prefetch
no-rel
class="!no-underline !text-current" class="!no-underline !text-current"
target="_blank" target="_blank"
to="https://www.reddit.com/r/NocoDB/" 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>
<div class="flex justify-center"> <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"> <a-button class="!shadow rounded" size="large">
<div class="flex items-center"> <div class="flex items-center">
<mdi-cards-heart class="text-red-500 mr-2" /> <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> <script lang="ts" setup>
import { onKeyStroke } from '@vueuse/core' import { onKeyStroke } from '@vueuse/core'
import { ref, watch } from '#imports'
interface Props { interface Props {
// Key to be pressed on hover to trigger the tooltip // Key to be pressed on hover to trigger the tooltip
@ -7,9 +8,10 @@ interface Props {
wrapperClass?: string wrapperClass?: string
} }
const { modifierKey } = defineProps<Props>() const { modifierKey, wrapperClass } = defineProps<Props>()
const showTooltip = ref(false) const showTooltip = ref(false)
const isMouseOver = ref(false) const isMouseOver = ref(false)
if (modifierKey) { if (modifierKey) {

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

@ -1,4 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from '#imports'
interface Props { interface Props {
placement?: placement?:
| 'top' | 'top'
@ -16,24 +18,21 @@ interface Props {
length?: number length?: number
} }
const props = withDefaults(defineProps<Props>(), { const { placement = 'bottom', length = 20 } = defineProps<Props>()
placement: 'bottom',
length: 20,
})
const text = ref() const text = ref()
const enableTooltip = computed(() => text?.value?.textContent.length > props.length)
const enableTooltip = computed(() => text.value?.textContent.length > length)
const shortName = computed(() => const shortName = computed(() =>
text?.value?.textContent.length > props.length text.value?.textContent.length > length ? `${text.value?.textContent.substr(0, length - 3)}...` : text.value?.textContent,
? `${text?.value?.textContent.substr(0, props.length - 3)}...`
: text?.value?.textContent,
) )
</script> </script>
<template> <template>
<a-tooltip v-if="enableTooltip" :placement="props.placement"> <a-tooltip v-if="enableTooltip" :placement="placement">
<template #title> <template #title>
<slot></slot> <slot />
</template> </template>
<div class="w-full">{{ shortName }}</div> <div class="w-full">{{ shortName }}</div>
</a-tooltip> </a-tooltip>
@ -41,4 +40,11 @@ const shortName = computed(() =>
<div ref="text" class="hidden"><slot></slot></div> <div ref="text" class="hidden"><slot></slot></div>
</template> </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"> <script setup lang="ts">
import * as monaco from 'monaco-editor'
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker' import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.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 TypescriptWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
import { onMounted } from '#imports' import type { editor as MonacoEditor } from 'monaco-editor'
import { deepCompare } from '~/utils' import { deepCompare, onMounted, ref, watch } from '#imports'
interface Props { interface Props {
modelValue: string | Record<string, any> modelValue: string | Record<string, any>
@ -60,7 +59,8 @@ self.MonacoEnvironment = {
} }
const root = ref<HTMLDivElement>() const root = ref<HTMLDivElement>()
let editor: monaco.editor.IStandaloneCodeEditor
let editor: MonacoEditor.IStandaloneCodeEditor
const format = () => { const format = () => {
editor.setValue(JSON.stringify(JSON.parse(editor?.getValue() as string), null, 2)) editor.setValue(JSON.stringify(JSON.parse(editor?.getValue() as string), null, 2))
@ -71,18 +71,20 @@ defineExpose({
isValid, isValid,
}) })
onMounted(() => { onMounted(async () => {
const { editor: monacoEditor, languages } = await import('monaco-editor')
if (root.value && lang) { if (root.value && lang) {
const model = monaco.editor.createModel(vModel, lang) const model = monacoEditor.createModel(vModel, lang)
if (lang === 'json') { if (lang === 'json') {
// configure the JSON language support with schemas and schema associations // configure the JSON language support with schemas and schema associations
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ languages.json.jsonDefaults.setDiagnosticsOptions({
validate: validate as boolean, validate: validate as boolean,
}) })
} }
editor = monaco.editor.create(root.value, { editor = monacoEditor.create(root.value, {
model, model,
theme: 'vs', theme: 'vs',
foldingStrategy: 'indentation', foldingStrategy: 'indentation',
@ -107,6 +109,7 @@ onMounted(() => {
vModel = editor.getValue() vModel = editor.getValue()
} else { } else {
const obj = JSON.parse(editor.getValue()) const obj = JSON.parse(editor.getValue())
if (!obj || !deepCompare(vModel, obj)) vModel = obj if (!obj || !deepCompare(vModel, obj)) vModel = obj
} }
} catch (e) { } catch (e) {

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,15 +1,25 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Row } from '~/composables' import type { Row } from '~/lib'
import { ReloadRowDataHookInj, useProvideSmartsheetRowStore, useSmartsheetStoreOrThrow } from '#imports' import {
ReloadRowDataHookInj,
interface Props { ReloadViewDataHookInj,
createEventHook,
inject,
provide,
toRef,
useProvideSmartsheetRowStore,
useSmartsheetStoreOrThrow,
watch,
} from '#imports'
const props = defineProps<{
row: Row row: Row
} }>()
const props = defineProps<Props>()
const currentRow = toRef(props, 'row') const currentRow = toRef(props, 'row')
const { meta } = useSmartsheetStoreOrThrow() const { meta } = useSmartsheetStoreOrThrow()
const { isNew, state, syncLTARRefs } = useProvideSmartsheetRowStore(meta, currentRow) const { isNew, state, syncLTARRefs } = useProvideSmartsheetRowStore(meta, currentRow)
// on changing isNew(new record insert) status sync LTAR cell values // 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"> <script setup lang="ts">
import { IsPublicInj, useSharedView, useSidebar, useSmartsheetStoreOrThrow } from '#imports' import { IsPublicInj, inject, ref, useSharedView, useSidebar, useSmartsheetStoreOrThrow, useUIPermission } from '#imports'
import ToggleDrawer from '~/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue'
const { isGrid, isForm, isGallery, isKanban, isSqlView } = useSmartsheetStoreOrThrow() 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" 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" style="z-index: 7"
> >
<SmartsheetToolbarViewActions <LazySmartsheetToolbarViewActions
v-if="(isGrid || isGallery || isKanban) && !isPublic && isUIAllowed('dataInsert')" v-if="(isGrid || isGallery || isKanban) && !isPublic && isUIAllowed('dataInsert')"
:show-system-fields="false" :show-system-fields="false"
class="ml-1" 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)" v-if="(!isPublic && !isUIAllowed('dataInsert') && !isKanban) || (isPublic && allowCSVDownload && !isKanban)"
/> />
<div class="flex-1" /> <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"> <template v-if="!isOpen && !isPublic">
<div class="border-l-1 pl-3"> <div class="border-l-1 pl-3">
<ToggleDrawer class="mr-2" /> <LazySmartsheetSidebarToolbarToggleDrawer class="mr-2" />
</div> </div>
</template> </template>
</div> </div>

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

@ -1,33 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import { ActiveCellInj, CellValueInj, ColumnInj, RowInj, provide, toRef, useVirtualCell } from '#imports' import { ActiveCellInj, CellValueInj, ColumnInj, RowInj, provide, toRef, useVirtualCell } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/lib'
import { NavigateDir } from '~/lib' import { NavigateDir } from '~/lib'
const props = defineProps<Props>() const props = defineProps<{
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 {
column: ColumnType column: ColumnType
modelValue: any modelValue: any
row: Row row: Row
active?: boolean active?: boolean
} }>()
const emit = defineEmits(['update:modelValue', 'navigate'])
const column = toRef(props, 'column') const column = toRef(props, 'column')
const active = toRef(props, 'active', false) 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.enter.exact="emit('navigate', NavigateDir.NEXT)"
@keydown.stop.shift.enter.exact="emit('navigate', NavigateDir.PREV)" @keydown.stop.shift.enter.exact="emit('navigate', NavigateDir.PREV)"
> >
<HasMany v-if="isHm" /> <LazyVirtualCellHasMany v-if="isHm" />
<ManyToMany v-else-if="isMm" /> <LazyVirtualCellManyToMany v-else-if="isMm" />
<BelongsTo v-else-if="isBt" /> <LazyVirtualCellBelongsTo v-else-if="isBt" />
<Rollup v-else-if="isRollup" /> <LazyVirtualCellRollup v-else-if="isRollup" />
<Formula v-else-if="isFormula" /> <LazyVirtualCellFormula v-else-if="isFormula" />
<Count v-else-if="isCount" /> <LazyVirtualCellCount v-else-if="isCount" />
<Lookup v-else-if="isLookup" /> <LazyVirtualCellLookup v-else-if="isLookup" />
</div> </div>
</template> </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"> <script setup lang="ts">
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { computed } from '#imports' import { computed, onBeforeMount, useColumnCreateStoreOrThrow, useProject, useVModel } from '#imports'
interface Props { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const { sqlUi, isPg } = useProject() const { sqlUi, isPg } = useProject()
@ -58,6 +58,7 @@ onBeforeMount(() => {
@change="onAlter" @change="onAlter"
/> />
</a-form-item> </a-form-item>
<a-form-item label="PK"> <a-form-item label="PK">
<a-checkbox <a-checkbox
v-model:checked="vModel.pk" v-model:checked="vModel.pk"
@ -66,6 +67,7 @@ onBeforeMount(() => {
@change="onAlter" @change="onAlter"
/> />
</a-form-item> </a-form-item>
<a-form-item label="AI"> <a-form-item label="AI">
<a-checkbox <a-checkbox
v-model:checked="vModel.ai" v-model:checked="vModel.ai"
@ -74,13 +76,16 @@ onBeforeMount(() => {
@change="onAlter" @change="onAlter"
/> />
</a-form-item> </a-form-item>
<a-form-item label="UN" :disabled="sqlUi.colPropUNDisabled(vModel) || !sqlUi.columnEditable(vModel)" @change="onAlter"> <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-checkbox v-model:checked="vModel.un" class="nc-column-checkbox-UN" />
</a-form-item> </a-form-item>
<a-form-item label="AU" :disabled="sqlUi.colPropAuDisabled(vModel) || !sqlUi.columnEditable(vModel)" @change="onAlter"> <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-checkbox v-model:checked="vModel.au" class="nc-column-checkbox-AU" />
</a-form-item> </a-form-item>
</div> </div>
<a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt"> <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 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"> <a-select-option v-for="type in dataTypes" :key="type" :value="type">
@ -88,6 +93,7 @@ onBeforeMount(() => {
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')"> <a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')">
<a-input <a-input
v-model:value="vModel.dtxp" v-model:value="vModel.dtxp"
@ -95,9 +101,11 @@ onBeforeMount(() => {
@input="onAlter" @input="onAlter"
/> />
</a-form-item> </a-form-item>
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale"> <a-form-item v-if="sqlUi.showScale(vModel)" label="Scale">
<a-input v-model:value="vModel.dtxs" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" /> <a-input v-model:value="vModel.dtxs" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item> </a-form-item>
<a-form-item :label="$t('placeholder.defaultValue')"> <a-form-item :label="$t('placeholder.defaultValue')">
<a-textarea v-model:value="vModel.cdf" auto-size @input="onAlter(2, true)" /> <a-textarea v-model:value="vModel.cdf" auto-size @input="onAlter(2, true)" />
<span class="text-gray-400 text-xs">{{ sampleValue }}</span> <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"> <script setup lang="ts">
import { getMdiIcon } from '@/utils' import { computed, getMdiIcon, useVModel, watch } from '#imports'
interface Props { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
// cater existing v1 cases // cater existing v1 cases
@ -88,6 +88,7 @@ watch(
color: vModel.meta.color, color: vModel.meta.color,
}" }"
/> />
<component <component
:is="getMdiIcon(icon.unchecked)" :is="getMdiIcon(icon.unchecked)"
:style="{ :style="{
@ -100,8 +101,9 @@ watch(
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row class="w-full justify-center"> <a-row class="w-full justify-center">
<GeneralColorPicker <LazyGeneralColorPicker
v-model="picked" v-model="picked"
:row-size="8" :row-size="8"
:colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']" :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"> <script setup lang="ts">
import { useProject } from '#imports' import {
import { currencyCodes, currencyLocales, validateCurrencyCode, validateCurrencyLocale } from '@/utils' computed,
currencyCodes,
interface Props { currencyLocales,
value: Record<string, any> useProject,
} useVModel,
validateCurrencyCode,
const props = defineProps<Props>() validateCurrencyLocale,
const emit = defineEmits(['update:value']) } from '#imports'
const vModel = useVModel(props, 'value', emit)
interface Option { interface Option {
label: string label: string
value: string value: string
} }
const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const validators = { const validators = {
'meta.currency_locale': [ 'meta.currency_locale': [
{ {
@ -91,6 +98,7 @@ vModel.value.meta = {
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item v-bind="validateInfos['meta.currency_code']" label="Currency Code"> <a-form-item v-bind="validateInfos['meta.currency_code']" label="Currency Code">
<a-select <a-select
@ -107,6 +115,7 @@ vModel.value.meta = {
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col v-if="isMoney && isPg"> <a-col v-if="isMoney && isPg">
<span class="text-[#FB8C00]">{{ message }}</span> <span class="text-[#FB8C00]">{{ message }}</span>
</a-col> </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"> <script setup lang="ts">
import { dateFormats } from '~/utils' import { dateFormats, useVModel } from '#imports'
interface Props { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta?.date_format) { 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"> <script setup lang="ts">
import { durationOptions } from '@/utils' import { durationOptions, useVModel } from '#imports'
interface Props { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const durationOptionList = const durationOptionList =
@ -28,6 +28,7 @@ vModel.value.meta = {
<a-col :span="24"> <a-col :span="24">
<span class="prose-sm mt-2">A duration of time in minutes or seconds (e.g. 1:23).</span> <span class="prose-sm mt-2">A duration of time in minutes or seconds (e.g. 1:23).</span>
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
<a-form-item label="Duration Format"> <a-form-item label="Duration Format">
<a-select v-model:value="vModel.meta.duration" class="w-52" dropdown-class-name="nc-dropdown-duration-option"> <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> <script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk' import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { import {
IsFormInj, IsFormInj,
IsKanbanInj, IsKanbanInj,
@ -9,7 +8,12 @@ import {
ReloadViewMetaHookInj, ReloadViewMetaHookInj,
computed, computed,
inject, inject,
message,
onMounted,
ref,
uiTypes, uiTypes,
useColumnCreateStoreOrThrow,
useI18n,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
watchEffect, watchEffect,
@ -75,7 +79,7 @@ async function onSubmit() {
if (!saved) return if (!saved) return
// add delay to complete the minimize transition // add delay to complete minimize transition
setTimeout(() => { setTimeout(() => {
advancedOptions.value = false advancedOptions.value = false
}, 500) }, 500)
@ -100,7 +104,7 @@ watchEffect(() => {
}) })
onMounted(() => { onMounted(() => {
if (isEdit.value === false) { if (!isEdit.value) {
generateNewColumnMeta() generateNewColumnMeta()
} else { } else {
if (formState.value.pk) { if (formState.value.pk) {
@ -122,7 +126,7 @@ onMounted(() => {
:class="{ '!w-[600px]': formState.uidt === UITypes.Formula }" :class="{ '!w-[600px]': formState.uidt === UITypes.Formula }"
@click.stop @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"> <div class="flex flex-col gap-2">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title"> <a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
<a-input <a-input
@ -154,20 +158,21 @@ onMounted(() => {
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </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" /> <LazySmartsheetColumnFormulaOptions v-if="formState.uidt === UITypes.Formula" v-model:value="formState" />
<SmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" /> <LazySmartsheetColumnCurrencyOptions v-if="formState.uidt === UITypes.Currency" v-model:value="formState" />
<SmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" /> <LazySmartsheetColumnDurationOptions v-if="formState.uidt === UITypes.Duration" v-model:value="formState" />
<SmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" /> <LazySmartsheetColumnRatingOptions v-if="formState.uidt === UITypes.Rating" v-model:value="formState" />
<SmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" /> <LazySmartsheetColumnCheckboxOptions v-if="formState.uidt === UITypes.Checkbox" v-model:value="formState" />
<SmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" /> <LazySmartsheetColumnLookupOptions v-if="!isEdit && formState.uidt === UITypes.Lookup" v-model:value="formState" />
<SmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" /> <LazySmartsheetColumnDateOptions v-if="formState.uidt === UITypes.Date" v-model:value="formState" />
<SmartsheetColumnLinkedToAnotherRecordOptions <LazySmartsheetColumnRollupOptions v-if="!isEdit && formState.uidt === UITypes.Rollup" v-model:value="formState" />
<LazySmartsheetColumnLinkedToAnotherRecordOptions
v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord" v-if="!isEdit && formState.uidt === UITypes.LinkToAnotherRecord"
v-model:value="formState" v-model:value="formState"
/> />
<SmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" /> <LazySmartsheetColumnSpecificDBTypeOptions v-if="formState.uidt === UITypes.SpecificDBType" />
<SmartsheetColumnSelectOptions <LazySmartsheetColumnSelectOptions
v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect" v-if="formState.uidt === UITypes.SingleSelect || formState.uidt === UITypes.MultiSelect"
v-model:value="formState" v-model:value="formState"
/> />
@ -194,7 +199,7 @@ onMounted(() => {
</span> </span>
</a-checkbox> </a-checkbox>
<SmartsheetColumnAdvancedOptions v-model:value="formState" /> <LazySmartsheetColumnAdvancedOptions v-model:value="formState" />
</div> </div>
</Transition> </Transition>
@ -204,6 +209,7 @@ onMounted(() => {
<!-- Cancel --> <!-- Cancel -->
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</a-button> </a-button>
<a-button html-type="submit" type="primary" @click.prevent="onSubmit"> <a-button html-type="submit" type="primary" @click.prevent="onSubmit">
<!-- Save --> <!-- Save -->
{{ $t('general.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> <script lang="ts" setup>
// todo: Remove this "Provider" component and use the "EditOrAdd" component directly
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue' import { MetaInj, inject, ref, toRef, useProvideColumnCreateStore } from '#imports'
import { MetaInj, inject } from '#imports'
interface Props { interface Props {
column?: Ref<ColumnType & { meta: any }> column?: ColumnType & { meta: any }
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -14,9 +14,10 @@ const emit = defineEmits(['submit', 'cancel'])
const meta = inject(MetaInj, ref()) const meta = inject(MetaInj, ref())
const column = toRef(props, 'column') const column = toRef(props, 'column')
useProvideColumnCreateStore(meta, column as Ref<ColumnType | undefined>)
useProvideColumnCreateStore(meta, column)
</script> </script>
<template> <template>
<SmartsheetColumnEditOrAdd @submit="emit('submit')" @cancel="emit('cancel')"></SmartsheetColumnEditOrAdd> <SmartsheetColumnEditOrAdd @submit="emit('submit')" @cancel="emit('cancel')" />
</template> </template>

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

@ -16,15 +16,16 @@ import {
onMounted, onMounted,
useColumnCreateStoreOrThrow, useColumnCreateStoreOrThrow,
useDebounceFn, useDebounceFn,
useVModel,
validateDateWithUnknownFormat, validateDateWithUnknownFormat,
} from '#imports' } from '#imports'
interface Props { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, sqlUi, column } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos, sqlUi, column } = useColumnCreateStoreOrThrow()
@ -619,6 +620,7 @@ onMounted(() => {
@change="handleInputDeb" @change="handleInputDeb"
/> />
</a-form-item> </a-form-item>
<div class="text-gray-600 mt-2 mb-4 prose-sm"> <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 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"> <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"> <a-col :span="6">
<span class="prose-sm text-gray-600">{{ item.text }}</span> <span class="prose-sm text-gray-600">{{ item.text }}</span>
</a-col> </a-col>
<a-col :span="18"> <a-col :span="18">
<div v-if="item.type === 'function'" class="text-xs text-gray-500"> <div v-if="item.type === 'function'" class="text-xs text-gray-500">
{{ item.description }} <br /><br /> {{ item.description }} <br /><br />
Syntax: <br /> Syntax: <br />
{{ item.syntax }} <br /><br /> {{ item.syntax }} <br /><br />
Examples: <br /> Examples: <br />
<div v-for="(example, idx) of item.examples" :key="idx"> <div v-for="(example, idx) of item.examples" :key="idx">
<div>({{ idx + 1 }}): {{ example }}</div> <div>({{ idx + 1 }}): {{ example }}</div>
</div> </div>
</div> </div>
<div v-if="item.type === 'column'" class="float-right mr-5 -mt-2"> <div v-if="item.type === 'column'" class="float-right mr-5 -mt-2">
<a-badge-ribbon :text="item.uidt" color="gray" /> <a-badge-ribbon :text="item.uidt" color="gray" />
</div> </div>
@ -663,7 +668,9 @@ onMounted(() => {
<template #avatar> <template #avatar>
<mdi-function v-if="item.type === 'function'" class="text-lg" /> <mdi-function v-if="item.type === 'function'" class="text-lg" />
<mdi-calculator v-if="item.type === 'op'" 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" /> <component :is="item.icon" v-if="item.type === 'column'" class="text-lg" />
</template> </template>
</a-list-item-meta> </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"> <script setup lang="ts">
import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk' 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 MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline' import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
interface Props { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref())) const meta = $(inject(MetaInj, ref()))
@ -57,6 +57,7 @@ const refTables = $computed(() => {
<a-radio value="mm">Many To Many</a-radio> <a-radio value="mm">Many To Many</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item <a-form-item
class="flex w-full pb-2 mt-4 nc-ltar-child-table" class="flex w-full pb-2 mt-4 nc-ltar-child-table"
:label="$t('labels.childTable')" :label="$t('labels.childTable')"
@ -81,6 +82,7 @@ const refTables = $computed(() => {
@click="advancedOptions = !advancedOptions" @click="advancedOptions = !advancedOptions"
> >
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }} {{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" /> <component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div> </div>
@ -99,6 +101,7 @@ const refTables = $computed(() => {
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.onDelete')"> <a-form-item class="flex w-1/2" :label="$t('labels.onDelete')">
<a-select <a-select
v-model:value="vModel.onDelete" v-model:value="vModel.onDelete"
@ -113,6 +116,7 @@ const refTables = $computed(() => {
</a-select> </a-select>
</a-form-item> </a-form-item>
</div> </div>
<div class="flex flex-row"> <div class="flex flex-row">
<a-form-item> <a-form-item>
<a-checkbox v-model:checked="vModel.virtual" name="virtual" @change="onDataTypeChange">Virtual Relation</a-checkbox> <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"> <script setup lang="ts">
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes, isSystemColumn } 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 { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref())) const meta = $(inject(MetaInj, ref()))
@ -77,6 +77,7 @@ const columns = $computed(() => {
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id"> <a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id">
<a-select <a-select
v-model:value="vModel.fk_lookup_column_id" 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 --> <!-- File not in use for now -->
<script setup lang="ts"> <script setup lang="ts">
import { precisions } from '#imports' import { precisions, useVModel } from '#imports'
interface Props { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
if (!vModel.value.meta) vModel.value.meta = {} 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"> <script setup lang="ts">
import { getMdiIcon } from '#imports' import { getMdiIcon, useVModel } from '#imports'
interface Props { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
// cater existing v1 cases // cater existing v1 cases
@ -82,6 +82,7 @@ watch(
color: vModel.meta.color, color: vModel.meta.color,
}" }"
/> />
<component <component
:is="getMdiIcon(icon.empty)" :is="getMdiIcon(icon.empty)"
:style="{ :style="{
@ -103,8 +104,9 @@ watch(
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-row class="w-full justify-center"> <a-row class="w-full justify-center">
<GeneralColorPicker <LazyGeneralColorPicker
v-model="picked" v-model="picked"
:row-size="8" :row-size="8"
:colors="['#fcb401', '#faa307', '#f48c06', '#e85d04', '#dc2f02', '#d00000', '#9d0208', '#777']" :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"> <script setup lang="ts">
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' 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 { const props = defineProps<{
value: Record<string, any> value: any
} }>()
const props = defineProps<Props>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj, ref())) const meta = $(inject(MetaInj, ref()))
@ -90,6 +90,7 @@ const columns = $computed(() => {
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_rollup_column_id"> <a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_rollup_column_id">
<a-select <a-select
v-model:value="vModel.fk_rollup_column_id" v-model:value="vModel.fk_rollup_column_id"
@ -103,6 +104,7 @@ const columns = $computed(() => {
</a-select> </a-select>
</a-form-item> </a-form-item>
</div> </div>
<a-form-item label="Aggregate function" v-bind="validateInfos.rollup_function"> <a-form-item label="Aggregate function" v-bind="validateInfos.rollup_function">
<a-select <a-select
v-model:value="vModel.rollup_function" 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"> <script setup lang="ts">
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import { enumColor } from '@/utils' import { enumColor, onMounted, useColumnCreateStoreOrThrow, useVModel, watch, IsKanbanInj } from '#imports'
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>
}
const props = defineProps<Props>() const props = defineProps<{
value: any
}>()
const emit = defineEmits(['update:value']) const emit = defineEmits(['update:value'])
@ -111,12 +104,17 @@ watch(inputs, () => {
overlay-class-name="nc-dropdown-select-color-options" overlay-class-name="nc-dropdown-select-color-options"
> >
<template #overlay> <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> </template>
<MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" /> <MdiArrowDownDropCircle :style="{ 'font-size': '1.5em', 'color': element.color }" class="mr-2" />
</a-dropdown> </a-dropdown>
<a-input ref="inputs" v-model:value="element.title" class="caption" /> <a-input ref="inputs" v-model:value="element.title" class="caption" />
<MdiClose class="ml-2" :style="{ color: 'red' }" @click="removeOption(index)" /> <MdiClose class="ml-2" :style="{ color: 'red' }" @click="removeOption(index)" />
</div> </div>
</template> </template>
@ -124,9 +122,10 @@ watch(inputs, () => {
<div v-if="validateInfos?.['colOptions.options']?.help?.[0]?.[0]" class="text-error text-[10px] my-2"> <div v-if="validateInfos?.['colOptions.options']?.help?.[0]?.[0]" class="text-error text-[10px] my-2">
{{ validateInfos['colOptions.options'].help[0][0] }} {{ validateInfos['colOptions.options'].help[0][0] }}
</div> </div>
<a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()"> <a-button type="dashed" class="w-full caption mt-2" @click="addNewOption()">
<div class="flex items-center"> <div class="flex items-center">
<MdiPlusIcon /> <MdiPlus />
<span class="flex-auto">Add option</span> <span class="flex-auto">Add option</span>
</div> </div>
</a-button> </a-button>
@ -134,5 +133,3 @@ watch(inputs, () => {
</Draggable> </Draggable>
</div> </div>
</template> </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> <template>
<div class="h-full flex flex-col w-full bg-[#eceff1] p-2"> <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"> <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> <template v-else>
<div v-for="log of commentsAndLogs" :key="log.id" class="flex gap-1 text-xs"> <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 }} {{ isYou(log.user) ? 'You' : log.user == null ? 'Shared base' : log.user }}
{{ log.op_type === 'COMMENT' ? 'commented' : log.op_sub_type === 'INSERT' ? 'created' : 'edited' }} {{ log.op_type === 'COMMENT' ? 'commented' : log.op_sub_type === 'INSERT' ? 'created' : 'edited' }}
</p> </p>
<p <p
v-if="log.op_type === 'COMMENT'" v-if="log.op_type === 'COMMENT'"
class="block caption my-2 nc-chip w-full min-h-20px p-2 rounded" class="block caption my-2 nc-chip w-full min-h-20px p-2 rounded"
@ -53,14 +54,19 @@ watch(
</div> </div>
</template> </template>
</div> </div>
<div class="border-1 my-2 w-full" /> <div class="border-1 my-2 w-full" />
<div class="p-0"> <div class="p-0">
<div class="flex justify-center"> <div class="flex justify-center">
<!-- Comments only --> <!-- Comments only -->
<a-checkbox v-model:checked="commentsOnly" v-e="['c:row-expand:comment-only']" @change="loadCommentsAndLogs" <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> {{ $t('labels.commentsOnly') }}
<span class="text-[11px] text-gray-500" />
</a-checkbox> </a-checkbox>
</div> </div>
<div class="shrink mt-2 flex"> <div class="shrink mt-2 flex">
<a-input <a-input
v-model:value="comment" v-model:value="comment"
@ -76,6 +82,7 @@ watch(
<mdi-account-circle class="text-lg text-pink-300" small @click="saveComment" /> <mdi-account-circle class="text-lg text-pink-300" small @click="saveComment" />
</div> </div>
</template> </template>
<template #suffix> <template #suffix>
<mdi-keyboard-return v-if="comment" class="text-sm" small @click="saveComment" /> <mdi-keyboard-return v-if="comment" class="text-sm" small @click="saveComment" />
</template> </template>

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

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

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

@ -1,18 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue'
import type { TableType, ViewType } from 'nocodb-sdk' import type { TableType, ViewType } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' 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 { import {
FieldsInj, FieldsInj,
IsFormInj, IsFormInj,
MetaInj, MetaInj,
ReloadRowDataHookInj, ReloadRowDataHookInj,
computedInject, computedInject,
message,
provide, provide,
ref, ref,
toRef, toRef,
@ -22,7 +18,7 @@ import {
useVModel, useVModel,
watch, watch,
} from '#imports' } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/lib'
interface Props { interface Props {
modelValue?: boolean modelValue?: boolean
@ -39,7 +35,7 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel']) const emits = defineEmits(['update:modelValue', 'cancel'])
const row = toRef(props, 'row') const row = ref(props.row)
const state = toRef(props, 'state') const state = toRef(props, 'state')
@ -135,7 +131,8 @@ export default {
:closable="false" :closable="false"
class="nc-drawer-expanded-form" 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="!bg-gray-100 rounded flex-1">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[max(70vh,100%)]"> <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"> <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="mt-2 py-2"
:class="`nc-expand-col-${col.title}`" :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"> <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-else
v-model="row.row[col.title]" v-model="row.row[col.title]"
:column="col" :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 v-if="!isNew" class="nc-comments-drawer min-w-0 min-h-full max-h-full" :class="{ active: commentsDrawer }">
<div class="h-full"> <div class="h-full">
<Comments v-if="commentsDrawer" /> <LazySmartsheetExpandedFormComments v-if="commentsDrawer" />
</div> </div>
</div> </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" /> <SmartsheetHeaderCellIcon v-if="column" />
<span v-if="column" class="name" style="white-space: nowrap" :title="column.title">{{ column.title }}</span> <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> <span v-if="(column.rqd && !column.cdf) || required" class="text-red-500">&nbsp;*</span>
<template v-if="!hideMenu"> <template v-if="!hideMenu">
<div class="flex-1" /> <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> </template>
<a-dropdown <a-dropdown
@ -41,6 +43,7 @@ const editColumnDropdown = ref(false)
overlay-class-name="nc-dropdown-edit-column" overlay-class-name="nc-dropdown-edit-column"
> >
<div /> <div />
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="editColumnDropdown" 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"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk' import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue' 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 FilePhoneIcon from '~icons/mdi/file-phone'
import KeyIcon from '~icons/mdi/key-variant' import KeyIcon from '~icons/mdi/key-variant'
import JSONIcon from '~icons/mdi/code-json' import JSONIcon from '~icons/mdi/code-json'
@ -28,6 +28,7 @@ import DurationIcon from '~icons/mdi/timer-outline'
const props = defineProps<{ columnMeta?: ColumnType }>() const props = defineProps<{ columnMeta?: ColumnType }>()
const columnMeta = toRef(props, 'columnMeta') const columnMeta = toRef(props, 'columnMeta')
const column = inject(ColumnInj, columnMeta) const column = inject(ColumnInj, columnMeta)
const additionalColMeta = useColumn(column as Ref<ColumnType>) 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> <script lang="ts" setup>
import { Modal, message } from 'ant-design-vue'
import type { LinkToAnotherRecordType } from 'nocodb-sdk' import type { LinkToAnotherRecordType } from 'nocodb-sdk'
import { UITypes } 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 }>() 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> <template>
<div class="flex items-center w-full text-xs text-gray-500 font-weight-medium" :class="{ 'h-full': column }"> <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"> <a-tooltip placement="bottom">
<template #title> <template #title>
@ -112,7 +112,8 @@ const tooltipMsg = computed(() => {
<template v-if="!hideMenu"> <template v-if="!hideMenu">
<div class="flex-1" /> <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> </template>
<a-dropdown <a-dropdown
@ -123,6 +124,7 @@ const tooltipMsg = computed(() => {
overlay-class-name="nc-dropdown-edit-column" overlay-class-name="nc-dropdown-edit-column"
> >
<div /> <div />
<template #overlay> <template #overlay>
<SmartsheetColumnEditOrAddProvider <SmartsheetColumnEditOrAddProvider
v-if="editColumnDropdown" 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 type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk' import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue' 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 GenericIcon from '~icons/mdi/square-rounded'
import HMIcon from '~icons/mdi/table-arrow-right' import HMIcon from '~icons/mdi/table-arrow-right'
import BTIcon from '~icons/mdi/table-arrow-left' 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' import TableColumnPlusBefore from '~icons/mdi/table-column-plus-before'
const props = defineProps<{ columnMeta?: ColumnType }>() const props = defineProps<{ columnMeta?: ColumnType }>()
const columnMeta = toRef(props, 'columnMeta') const columnMeta = toRef(props, 'columnMeta')
const column = inject(ColumnInj, ref(columnMeta)) as Ref<ColumnType & { colOptions: LookupType }> 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> <script lang="ts" setup>
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { useNuxtApp } from '#app' import { useNuxtApp, useSmartsheetStoreOrThrow, useUIPermission, viewIcons } from '#imports'
import { useSmartsheetStoreOrThrow, useUIPermission, viewIcons } from '#imports'
interface Emits { interface Emits {
(event: 'openModal', data: { type: ViewTypes; title?: string }): void (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 { ViewType, ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs' import type { SortableEvent } from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue' import type { Menu as AntMenu } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
import RenameableMenuItem from './RenameableMenuItem.vue'
import { import {
ActiveViewInj, ActiveViewInj,
ViewListInj, ViewListInj,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
inject, inject,
message,
onMounted, onMounted,
ref, ref,
resolveComponent,
useApi, useApi,
useDialog, useDialog,
useI18n, useI18n,
@ -22,7 +22,6 @@ import {
viewTypeAlias, viewTypeAlias,
watch, watch,
} from '#imports' } from '#imports'
import DlgViewDelete from '~/components/dlg/ViewDelete.vue'
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
@ -190,7 +189,7 @@ async function onRename(view: ViewType) {
function openDeleteDialog(view: Record<string, any>) { function openDeleteDialog(view: Record<string, any>) {
const isOpen = ref(true) const isOpen = ref(true)
const { close } = useDialog(DlgViewDelete, { const { close } = useDialog(resolveComponent('DlgViewDelete'), {
'modelValue': isOpen, 'modelValue': isOpen,
'view': view, 'view': view,
'onUpdate:modelValue': closeDialog, 'onUpdate:modelValue': closeDialog,
@ -219,7 +218,7 @@ function openDeleteDialog(view: Record<string, any>) {
<template> <template>
<a-menu ref="menuRef" :class="{ dragging }" class="nc-views-menu flex-1" :selected-keys="selected"> <a-menu ref="menuRef" :class="{ dragging }" class="nc-views-menu flex-1" :selected-keys="selected">
<RenameableMenuItem <LazySmartsheetSidebarRenameableMenuItem
v-for="(view, index) of views" v-for="(view, index) of views"
:id="view.id" :id="view.id"
:key="view.id" :key="view.id"

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

@ -1,8 +1,17 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ViewType, ViewTypes } from 'nocodb-sdk' import type { ViewType, ViewTypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import type { WritableComputedRef } from '@vue/reactivity' 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 { interface Props {
view: ViewType 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)" /> <a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" />
<div v-else> <div v-else>
<GeneralTruncateText>{{ vModel.alias || vModel.title }}</GeneralTruncateText> <LazyGeneralTruncateText>{{ vModel.alias || vModel.title }}</LazyGeneralTruncateText>
</div> </div>
<div class="flex-1" /> <div class="flex-1" />

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

@ -1,8 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ViewType, ViewTypes } from 'nocodb-sdk' import type { ViewType, ViewTypes } from 'nocodb-sdk'
import MenuTop from './MenuTop.vue'
import MenuBottom from './MenuBottom.vue'
import Toolbar from './toolbar/index.vue'
import { import {
ActiveViewInj, ActiveViewInj,
MetaInj, MetaInj,
@ -11,6 +8,7 @@ import {
inject, inject,
provide, provide,
ref, ref,
useNuxtApp,
useRoute, useRoute,
useRouter, useRouter,
useSidebar, useSidebar,
@ -36,7 +34,7 @@ const { $e } = useNuxtApp()
provide(ViewListInj, views) provide(ViewListInj, views)
/** Sidebar visible */ /** Sidebar visible */
const { isOpen } = useSidebar('nc-right-sidebar', { isOpen: true }) const { isOpen } = useSidebar('nc-right-sidebar')
const sidebarCollapsed = computed(() => !isOpen.value) const sidebarCollapsed = computed(() => !isOpen.value)
@ -105,24 +103,24 @@ function onCreate(view: ViewType) {
ref="sidebar" ref="sidebar"
:collapsed="sidebarCollapsed" :collapsed="sidebarCollapsed"
collapsiple collapsiple
collapsed-width="50" collapsed-width="0"
width="250" width="250"
class="relative shadow-md h-full" class="relative shadow-md h-full"
theme="light" theme="light"
> >
<Toolbar <LazySmartsheetSidebarToolbar
v-if="isOpen" 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" 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"> <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" /> <div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="!my-3 w-full border-b-1" />
<MenuBottom @open-modal="openModal" /> <LazySmartsheetSidebarMenuBottom @open-modal="openModal" />
</div> </div>
<dlg-view-create <LazyDlgViewCreate
v-if="views" v-if="views"
v-model="modalOpen" v-model="modalOpen"
:title="viewCreateTitle" :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> <template #title>
<span> Debug Meta </span> <span> Debug Meta </span>
</template> </template>
<mdi-bug-outline class="cursor-pointer" @click="editorOpen = true" /> <mdi-bug-outline class="cursor-pointer" @click="editorOpen = true" />
</a-tooltip> </a-tooltip>
<a-modal v-model:visible="editorOpen" :footer="null" width="80%" wrap-class-name="nc-modal-debug-meta"> <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-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"> <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-tab-pane>
</a-tabs> </a-tabs>
</a-modal> </a-modal>

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

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

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

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

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

Loading…
Cancel
Save