Browse Source

Merge branch 'develop' into fix/gui-v2-table-rename

pull/3117/head
Wing-Kam Wong 2 years ago
parent
commit
41b984fe7a
  1. 14
      packages/nc-gui-v2/app.vue
  2. 4
      packages/nc-gui-v2/assets/css/color.css
  3. 23
      packages/nc-gui-v2/assets/css/global.css
  4. 19
      packages/nc-gui-v2/assets/style.css
  5. 94
      packages/nc-gui-v2/assets/style.scss
  6. 39
      packages/nc-gui-v2/components.d.ts
  7. 2
      packages/nc-gui-v2/components/cell/Decimal.vue
  8. 2
      packages/nc-gui-v2/components/cell/Float.vue
  9. 2
      packages/nc-gui-v2/components/cell/Integer.vue
  10. 8
      packages/nc-gui-v2/components/cell/Text.vue
  11. 4
      packages/nc-gui-v2/components/cell/attachment/Carousel.vue
  12. 14
      packages/nc-gui-v2/components/cell/attachment/Modal.vue
  13. 12
      packages/nc-gui-v2/components/cell/attachment/index.vue
  14. 363
      packages/nc-gui-v2/components/dashboard/TreeView.vue
  15. 2
      packages/nc-gui-v2/components/dashboard/settings/AppStore.vue
  16. 10
      packages/nc-gui-v2/components/dashboard/settings/Metadata.vue
  17. 2
      packages/nc-gui-v2/components/dashboard/settings/Misc.vue
  18. 2
      packages/nc-gui-v2/components/dashboard/settings/Modal.vue
  19. 7
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue
  20. 139
      packages/nc-gui-v2/components/dlg/AirtableImport.vue
  21. 314
      packages/nc-gui-v2/components/dlg/QuickImport.vue
  22. 94
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  23. 3
      packages/nc-gui-v2/components/general/ColorPicker.vue
  24. 84
      packages/nc-gui-v2/components/general/HelpAndSupport.vue
  25. 73
      packages/nc-gui-v2/components/general/Language.vue
  26. 22
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  27. 17
      packages/nc-gui-v2/components/general/PreviewAs.vue
  28. 4
      packages/nc-gui-v2/components/general/ReleaseInfo.vue
  29. 14
      packages/nc-gui-v2/components/general/ShareBaseButton.vue
  30. 57
      packages/nc-gui-v2/components/general/SocialCard.vue
  31. 4
      packages/nc-gui-v2/components/general/Sponsors.vue
  32. 61
      packages/nc-gui-v2/components/general/language/Menu.vue
  33. 11
      packages/nc-gui-v2/components/general/language/index.vue
  34. 14
      packages/nc-gui-v2/components/shared-view/Form.vue
  35. 4
      packages/nc-gui-v2/components/shared-view/Grid.vue
  36. 13
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  37. 2
      packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  38. 4
      packages/nc-gui-v2/components/smartsheet-column/SelectOptions.vue
  39. 31
      packages/nc-gui-v2/components/smartsheet-header/Menu.vue
  40. 6
      packages/nc-gui-v2/components/smartsheet-header/VirtualCellIcon.vue
  41. 23
      packages/nc-gui-v2/components/smartsheet-toolbar/AddRow.vue
  42. 20
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  43. 38
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  44. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue
  45. 54
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  46. 63
      packages/nc-gui-v2/components/smartsheet-toolbar/LockType.vue
  47. 5
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  48. 8
      packages/nc-gui-v2/components/smartsheet-toolbar/Reload.vue
  49. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue
  50. 20
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  51. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/SharedViewList.vue
  52. 23
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  53. 280
      packages/nc-gui-v2/components/smartsheet-toolbar/ViewActions.vue
  54. 8
      packages/nc-gui-v2/components/smartsheet/Cell.vue
  55. 24
      packages/nc-gui-v2/components/smartsheet/Form.vue
  56. 122
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  57. 8
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  58. 25
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  59. 11
      packages/nc-gui-v2/components/smartsheet/expanded-form/Comments.vue
  60. 32
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  61. 22
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  62. 52
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  63. 24
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  64. 4
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  65. 71
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  66. 20
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue
  67. 116
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue
  68. 15
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  69. 21
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  70. 2
      packages/nc-gui-v2/components/tabs/Auth.vue
  71. 16
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  72. 135
      packages/nc-gui-v2/components/template/Editor.vue
  73. 15
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  74. 2
      packages/nc-gui-v2/components/virtual-cell/Formula.vue
  75. 14
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  76. 13
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  77. 24
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  78. 38
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  79. 30
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  80. 22
      packages/nc-gui-v2/components/webhook/Drawer.vue
  81. 8
      packages/nc-gui-v2/components/webhook/Editor.vue
  82. 20
      packages/nc-gui-v2/components/webhook/List.vue
  83. 2
      packages/nc-gui-v2/composables/index.ts
  84. 103
      packages/nc-gui-v2/composables/useDialog/index.ts
  85. 7
      packages/nc-gui-v2/composables/useExpandedFormStore.ts
  86. 9
      packages/nc-gui-v2/composables/useGlobal/actions.ts
  87. 34
      packages/nc-gui-v2/composables/useProject.ts
  88. 14
      packages/nc-gui-v2/composables/useSmartsheetStore.ts
  89. 7
      packages/nc-gui-v2/composables/useTable.ts
  90. 60
      packages/nc-gui-v2/composables/useTheme/index.ts
  91. 16
      packages/nc-gui-v2/composables/useUIPermission/index.ts
  92. 11
      packages/nc-gui-v2/composables/useViewColumns.ts
  93. 24
      packages/nc-gui-v2/composables/useViewData.ts
  94. 190
      packages/nc-gui-v2/composables/useViewFilters.ts
  95. 73
      packages/nc-gui-v2/composables/useViewSorts.ts
  96. 1
      packages/nc-gui-v2/context/index.ts
  97. 2
      packages/nc-gui-v2/lang/nl.json
  98. 148
      packages/nc-gui-v2/layouts/base.vue
  99. 6
      packages/nc-gui-v2/layouts/default.vue
  100. 12
      packages/nc-gui-v2/layouts/shared-view.vue
  101. Some files were not shown because too many files have changed in this diff Show More

14
packages/nc-gui-v2/app.vue

@ -1,11 +1,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, provideTheme, useRoute } 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'))
provideTheme()
</script> </script>
<template> <template>
<NuxtLayout :name="disableBaseLayout ? false : 'base'"> <a-config-provider>
<NuxtPage /> <NuxtLayout :name="disableBaseLayout ? false : 'base'">
</NuxtLayout> <NuxtPage />
</NuxtLayout>
</a-config-provider>
</template> </template>

4
packages/nc-gui-v2/assets/css/color.css

@ -1,4 +0,0 @@
:root {
--primary: #00b786;
--secondary: #8ceaf6;
}

23
packages/nc-gui-v2/assets/css/global.css

@ -1,4 +1,3 @@
@import './color.css';
html { html {
font-size: 16px; font-size: 16px;
word-spacing: 1px; word-spacing: 1px;
@ -8,30 +7,10 @@ html {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, Vazirmatn, sans-serif; font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, Vazirmatn, sans-serif;
} }
*, *:before, *:after {
box-sizing: border-box;
margin: 0;
}
.btn, .pointer {
cursor: pointer;
}
.primary {
color: var(--primary);
}
.secondary {
color: var(--secondary);
}
.btn-primary {
background-color: var(--primary);
color: #fff;
}
.btn-secondary {
background-color: var(--secondary);
color: #000;
}
/* /*
Apply Vazirmatn for rtl Apply Vazirmatn for rtl

19
packages/nc-gui-v2/assets/style.css

@ -1,19 +0,0 @@
::-webkit-scrollbar {
width: .7em;
height: .7em
}
::-webkit-scrollbar-button {
background: #77777722
}
::-webkit-scrollbar-track-piece {
background: #66666622
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: .7em;
border: .15em solid #00000000;
background-clip: padding-box;
}

94
packages/nc-gui-v2/assets/style-v2.scss → packages/nc-gui-v2/assets/style.scss

@ -1,8 +1,9 @@
@import 'ant-design-vue/dist/antd.variable.min.css'; @import 'ant-design-vue/dist/antd.variable.min.css';
@import 'ant-design-vue/dist/antd.min.css';
:root { :root {
--header-height: 50px; --header-height: 50px;
--toolbar-height: 48px;
--tw-text-opacity: 1;
} }
.ant-layout-header { .ant-layout-header {
@ -14,7 +15,7 @@ body,
#__nuxt, #__nuxt,
.ant-layout, .ant-layout,
main { main {
@apply m-0 h-full w-full bg-white dark:(bg-black text-white); @apply m-0 h-full w-full bg-white;
} }
html { html {
@ -26,21 +27,8 @@ main {
overflow-x: hidden; overflow-x: hidden;
} }
nav,
nav .v-list {
@apply dark:(!bg-gray-900 text-white)
}
.v-divider {
@apply dark:bg-white
}
a { a {
@apply prose text-primary underline hover:opacity-75 dark:(text-secondary) hover:(opacity-75); @apply !text-primary !underline hover:!text-accent;
}
h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
@apply dark:(!text-white);
} }
.nc-icon { .nc-icon {
@ -61,7 +49,7 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
&:hover { &:hover {
.nc-icon { .nc-icon {
@apply text-pink-500; @apply text-accent;
} }
} }
} }
@ -98,7 +86,7 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
// for highlighting toolbar menu item // for highlighting toolbar menu item
.nc-active-btn > .ant-btn{ .nc-active-btn > .ant-btn{
@apply bg-primary/20 hover:(bg-primary/20); @apply bg-primary bg-opacity-20 hover:(bg-primary bg-opacity-20);
} }
.nc-locked-overlay { .nc-locked-overlay {
@ -158,24 +146,23 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
.page-leave-active, .page-leave-active,
.layout-enter-active, .layout-enter-active,
.layout-leave-active { .layout-leave-active {
@apply transition-opacity duration-300 ease-in-out; @apply transition-all duration-200 ease;
} }
.page-enter-active, .page-enter-from,
.page-leave-active, .page-leave-to,
.layout-enter-active, .layout-enter-from,
.layout-leave-active { .layout-leave-to {
@apply opacity-0; @apply opacity-0;
} }
.slide-enter-active, .slide-enter-active,
.slide-leave-active { .slide-leave-active {
@apply transition-all duration-200 ease-in-out; @apply transition-all duration-200 ease-in-out;
transform: translate(100%, 0);
} }
.slide-enter, .slide-enter-from,
.slide-leave-active { .slide-leave-to {
transform: translate(-100%, 0); transform: translate(-100%, 0);
} }
@ -188,13 +175,13 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
@apply ring ring-xl; @apply ring ring-xl;
} }
.glow-enter, .glow-enter-from,
.glow-leave-active { .glow-leave-to {
@apply opacity-0; @apply opacity-0;
} }
.scaling-btn { .scaling-btn {
@apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white bg-primary; @apply z-1 relative color-transition border border-gray-300 rounded-md p-3 bg-gray-100/50 text-white;
&::after { &::after {
@apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary; @apply rounded-md absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
@ -203,10 +190,55 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
} }
&:hover::after { &:hover::after {
@apply transform scale-110 ring ring-pink-500; @apply transform scale-110 ring ring-accent;
} }
&:active::after { &:active::after {
@apply ring ring-pink-500; @apply ring ring-accent;
}
}
.ant-dropdown-menu-submenu {
@apply !py-0;
.ant-dropdown-menu, .ant-menu {
@apply m-0 p-0;
}
.ant-menu-item {
@apply !m-0 !px-2;
} }
} }
.ant-dropdown-menu-submenu-popup {
@apply scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !shadow !rounded;
}
.ant-tabs-dropdown-menu-title-content {
@apply flex items-center;
}
.ant-dropdown-menu-item-group-list {
@apply !mx-0;
}
.ant-dropdown-menu-item-group-title {
@apply border-b-1;
}
.ant-dropdown-menu-item-group-list {
@apply m-0;
}
.ant-dropdown-menu-item, .ant-menu-item {
@apply !py-0 active:(ring ring-accent);
}
.ant-dropdown-menu-title-content,
.ant-menu-title-content {
@apply !py-0;
}
.ant-dropdown-menu-submenu-title{
@apply !pr-2;
}

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

@ -15,13 +15,16 @@ declare module '@vue/runtime-core' {
ACardMeta: typeof import('ant-design-vue/es')['CardMeta'] ACardMeta: typeof import('ant-design-vue/es')['CardMeta']
ACarousel: typeof import('ant-design-vue/es')['Carousel'] ACarousel: typeof import('ant-design-vue/es')['Carousel']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox'] ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col'] ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse'] ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker'] ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADivider: typeof import('ant-design-vue/es')['Divider'] ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer'] ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown'] ADropdown: typeof import('ant-design-vue/es')['Dropdown']
ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton']
AEmpty: typeof import('ant-design-vue/es')['Empty'] AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form'] AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem'] AFormItem: typeof import('ant-design-vue/es')['FormItem']
@ -31,6 +34,7 @@ declare module '@vue/runtime-core' {
AInputSearch: typeof import('ant-design-vue/es')['InputSearch'] AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout'] ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent'] ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader'] ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider'] ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
AList: typeof import('ant-design-vue/es')['List'] AList: typeof import('ant-design-vue/es')['List']
@ -64,8 +68,13 @@ declare module '@vue/runtime-core' {
ATypography: typeof import('ant-design-vue/es')['Typography'] ATypography: typeof import('ant-design-vue/es')['Typography']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle'] ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger'] AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
BiFiletypeJson: typeof import('~icons/bi/filetype-json')['default']
BiFiletypeXlsx: typeof import('~icons/bi/filetype-xlsx')['default']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default'] CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default'] CilFullscreenExit: typeof import('~icons/cil/fullscreen-exit')['default']
ClarityColorPickerLine: typeof import('~icons/clarity/color-picker-line')['default']
ClarityColorPickerSolid: typeof import('~icons/clarity/color-picker-solid')['default']
ClarityImageLine: typeof import('~icons/clarity/image-line')['default']
ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default'] ClaritySuccessLine: typeof import('~icons/clarity/success-line')['default']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default'] EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default'] IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
@ -73,6 +82,9 @@ declare module '@vue/runtime-core' {
IcRoundEdit: typeof import('~icons/ic/round-edit')['default'] IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default'] IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['default'] IcRoundSearch: typeof import('~icons/ic/round-search')['default']
LogosGoogleGmail: typeof import('~icons/logos/google-gmail')['default']
LogosRedditIcon: typeof import('~icons/logos/reddit-icon')['default']
LogosSwagger: typeof import('~icons/logos/swagger')['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']
@ -84,9 +96,13 @@ declare module '@vue/runtime-core' {
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']
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default'] MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default'] MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
MdiAccountGroup: typeof import('~icons/mdi/account-group')['default'] MdiAccountGroup: typeof import('~icons/mdi/account-group')['default']
MdiAccountGroupIcon: typeof import('~icons/mdi/account-group-icon')['default']
MdiAccountIcon: typeof import('~icons/mdi/account-icon')['default']
MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default'] MdiAccountOutline: typeof import('~icons/mdi/account-outline')['default']
MdiAccountPlus: typeof import('~icons/mdi/account-plus')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default'] MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default'] MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default'] MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
@ -94,6 +110,9 @@ declare module '@vue/runtime-core' {
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']
MdiBackburger: typeof import('~icons/mdi/backburger')['default']
MdiBookOpenOutline: typeof import('~icons/mdi/book-open-outline')['default']
MdiBookOpenPageVariantOutline: typeof import('~icons/mdi/book-open-page-variant-outline')['default']
MdiBugOutline: typeof import('~icons/mdi/bug-outline')['default'] MdiBugOutline: typeof import('~icons/mdi/bug-outline')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default'] MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default'] MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default']
@ -101,15 +120,21 @@ declare module '@vue/runtime-core' {
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default'] MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default'] MdiChat: typeof import('~icons/mdi/chat')['default']
MdiCheck: typeof import('~icons/mdi/check')['default'] MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDoubleLeft: typeof import('~icons/mdi/chevron-double-left')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiClose: typeof import('~icons/mdi/close')['default'] MdiClose: typeof import('~icons/mdi/close')['default']
MdiCloseBox: typeof import('~icons/mdi/close-box')['default'] MdiCloseBox: typeof import('~icons/mdi/close-box')['default']
MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default'] MdiCloseCircle: typeof import('~icons/mdi/close-circle')['default']
MdiCloseCircleOutline: typeof import('~icons/mdi/close-circle-outline')['default']
MdiCloseThick: typeof import('~icons/mdi/close-thick')['default'] MdiCloseThick: typeof import('~icons/mdi/close-thick')['default']
MdiCodeJson: typeof import('~icons/mdi/code-json')['default'] MdiCodeJson: typeof import('~icons/mdi/code-json')['default']
MdiCog: typeof import('~icons/mdi/cog')['default'] MdiCog: typeof import('~icons/mdi/cog')['default']
MdiCommentTextOutline: typeof import('~icons/mdi/comment-text-outline')['default']
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default'] MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default'] MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabase: typeof import('~icons/mdi/database')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default'] MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['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']
@ -129,32 +154,44 @@ 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']
MdiFilePlusOutline: typeof import('~icons/mdi/file-plus-outline')['default']
MdiFileUploadOutline: typeof import('~icons/mdi/file-upload-outline')['default']
MdiFilterOutline: typeof import('~icons/mdi/filter-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default'] MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFlashOutline: typeof import('~icons/mdi/flash-outline')['default'] MdiFlashOutline: typeof import('~icons/mdi/flash-outline')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default'] MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiFunction: typeof import('~icons/mdi/function')['default'] MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default'] MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default'] MdiGithub: typeof import('~icons/mdi/github')['default']
MdiGmail: typeof import('~icons/mdi/gmail')['default']
MdiGridLarge: typeof import('~icons/mdi/grid-large')['default'] MdiGridLarge: typeof import('~icons/mdi/grid-large')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default'] MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default'] MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default'] MdiInformation: typeof import('~icons/mdi/information')['default']
MdiJson: typeof import('~icons/mdi/json')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default'] MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyPlus: typeof import('~icons/mdi/key-plus')['default'] MdiKeyPlus: typeof import('~icons/mdi/key-plus')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default'] MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default'] MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default'] MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
MdiLoading: typeof import('~icons/mdi/loading')['default']
MdiLockOutlineIcon: typeof import('~icons/mdi/lock-outline-icon')['default']
MdiLogin: typeof import('~icons/mdi/login')['default'] MdiLogin: typeof import('~icons/mdi/login')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default'] MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default'] MdiMagnify: typeof import('~icons/mdi/magnify')['default']
MdiMenu: typeof import('~icons/mdi/menu')['default']
MdiMenuDown: typeof import('~icons/mdi/menu-down')['default'] MdiMenuDown: typeof import('~icons/mdi/menu-down')['default']
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default'] MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default'] MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiNotebookCheckOutline: typeof import('~icons/mdi/notebook-check-outline')['default'] MdiNotebookCheckOutline: typeof import('~icons/mdi/notebook-check-outline')['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']
MdiPencil: typeof import('~icons/mdi/pencil')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default'] MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusBoxOutline: typeof import('~icons/mdi/plus-box-outline')['default'] MdiPlusBoxOutline: typeof import('~icons/mdi/plus-box-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']
MdiPlusRoundedOutline: typeof import('~icons/mdi/plus-rounded-outline')['default'] MdiPlusRoundedOutline: typeof import('~icons/mdi/plus-rounded-outline')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default'] MdiRefresh: typeof import('~icons/mdi/refresh')['default']
@ -176,10 +213,12 @@ declare module '@vue/runtime-core' {
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default'] MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']
MdiTrashCan: typeof import('~icons/mdi/trash-can')['default'] MdiTrashCan: typeof import('~icons/mdi/trash-can')['default']
MdiTwitter: typeof import('~icons/mdi/twitter')['default'] MdiTwitter: typeof import('~icons/mdi/twitter')['default']
MdiUpload: typeof import('~icons/mdi/upload')['default']
MdiUploadOutline: typeof import('~icons/mdi/upload-outline')['default'] MdiUploadOutline: typeof import('~icons/mdi/upload-outline')['default']
MdiViewListOutline: typeof import('~icons/mdi/view-list-outline')['default'] MdiViewListOutline: typeof import('~icons/mdi/view-list-outline')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default'] MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MdiXml: typeof import('~icons/mdi/xml')['default'] MdiXml: typeof import('~icons/mdi/xml')['default']
PhFileCsv: typeof import('~icons/ph/file-csv')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
} }

2
packages/nc-gui-v2/components/cell/Decimal.vue

@ -27,7 +27,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="outline-none pa-0 border-none w-full h-full prose-sm" class="outline-none p-0 border-none w-full h-full prose-sm"
type="number" type="number"
step="0.1" step="0.1"
@blur="editEnabled = false" @blur="editEnabled = false"

2
packages/nc-gui-v2/components/cell/Float.vue

@ -27,7 +27,7 @@ const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="outline-none pa-0 border-none w-full h-full prose-sm" class="outline-none p-0 border-none w-full h-full prose-sm"
type="number" type="number"
step="0.1" step="0.1"
@blur="editEnabled = false" @blur="editEnabled = false"

2
packages/nc-gui-v2/components/cell/Integer.vue

@ -31,7 +31,7 @@ function onKeyDown(evt: KeyboardEvent) {
v-if="editEnabled" v-if="editEnabled"
:ref="focus" :ref="focus"
v-model="vModel" v-model="vModel"
class="outline-none pa-0 border-none w-full h-full prose-sm" class="outline-none p-0 border-none w-full h-full prose-sm"
type="number" type="number"
@blur="editEnabled = false" @blur="editEnabled = false"
@keydown="onKeyDown" @keydown="onKeyDown"

8
packages/nc-gui-v2/components/cell/Text.vue

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

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

@ -55,7 +55,7 @@ onClickOutside(carouselRef, () => {
</div> </div>
<div <div
class="select-none group hover:ring active:ring-pink-500 cursor-pointer leading-8 inline-block px-3 py-1 bg-gray-300 text-white mb-4 text-center rounded shadow" class="select-none group hover:ring active:ring-accent cursor-pointer leading-8 inline-block px-3 py-1 bg-gray-300 text-white mb-4 text-center rounded shadow"
@click.stop="downloadFile(selectedImage)" @click.stop="downloadFile(selectedImage)"
> >
<h3 class="group-hover:text-primary">{{ selectedImage && selectedImage.title }}</h3> <h3 class="group-hover:text-primary">{{ selectedImage && selectedImage.title }}</h3>
@ -133,7 +133,7 @@ onClickOutside(carouselRef, () => {
} }
.ant-carousel :deep(.slick-arrow.custom-slick-arrow) { .ant-carousel :deep(.slick-arrow.custom-slick-arrow) {
@apply text-4xl text-white hover:text-primary active:text-pink-500 opacity-100 cursor-pointer z-1; @apply text-4xl text-white hover:text-primary active:text-accent opacity-100 cursor-pointer z-1;
} }
.ant-carousel :deep(.custom-slick-arrow:before) { .ant-carousel :deep(.custom-slick-arrow:before) {
display: none; display: none;

14
packages/nc-gui-v2/components/cell/attachment/Modal.vue

@ -66,7 +66,7 @@ function onClick(item: Record<string, any>) {
class="nc-attach-file group" class="nc-attach-file group"
@click="open" @click="open"
> >
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" /> <MaterialSymbolsAttachFile class="transform group-hover:(text-accent scale-120)" />
Attach File Attach File
</div> </div>
@ -83,9 +83,9 @@ function onClick(item: Record<string, any>) {
<general-overlay <general-overlay
v-model="isOverDropZone" v-model="isOverDropZone"
inline inline
class="text-white ring ring-pink-500 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl" class="text-white ring ring-accent bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl"
> >
<MaterialSymbolsFileCopyOutline class="text-pink-500" height="35" width="35" /> <MaterialSymbolsFileCopyOutline class="text-accent" height="35" width="35" />
<div class="text-white text-3xl">Drop here</div> <div class="text-white text-3xl">Drop here</div>
</general-overlay> </general-overlay>
</template> </template>
@ -154,8 +154,8 @@ function onClick(item: Record<string, any>) {
.nc-attachment-modal { .nc-attachment-modal {
.nc-attach-file { .nc-attach-file {
@apply select-none cursor-pointer color-transition flex items-center gap-1 border-1 p-2 rounded @apply select-none cursor-pointer color-transition flex items-center gap-1 border-1 p-2 rounded
@apply hover:(bg-primary/10 text-primary ring); @apply hover:(bg-primary bg-opacity-10 text-primary ring);
@apply active:(ring-pink-500 bg-primary/20); @apply active:(ring-accent bg-primary bg-opacity-20);
} }
.nc-attachment-item { .nc-attachment-item {
@ -176,7 +176,7 @@ function onClick(item: Record<string, any>) {
} }
&:active::after { &:active::after {
@apply ring ring-pink-500 shadow transform scale-103; @apply ring ring-accent shadow transform scale-103;
} }
} }
} }
@ -185,7 +185,7 @@ function onClick(item: Record<string, any>) {
@apply bg-white absolute bottom-2 right-2; @apply bg-white absolute bottom-2 right-2;
@apply transition-opacity duration-150 ease-in opacity-0 hover:ring; @apply transition-opacity duration-150 ease-in opacity-0 hover:ring;
@apply cursor-pointer rounded shadow flex items-center p-1 border-1; @apply cursor-pointer rounded shadow flex items-center p-1 border-1;
@apply active:(ring border-0 ring-pink-500); @apply active:(ring border-0 ring-accent);
} }
.nc-attachment-remove { .nc-attachment-remove {

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

@ -100,16 +100,16 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
v-model="isOverDropZone" v-model="isOverDropZone"
inline inline
:target="currentCellRef" :target="currentCellRef"
class="text-white text-lg ring ring-pink-500 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl" class="text-white text-lg ring ring-accent bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl"
> >
<MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here <MaterialSymbolsFileCopyOutline class="text-accent" /> Drop here
</general-overlay> </general-overlay>
</template> </template>
<div <div
v-if="!isReadonly" v-if="!isReadonly"
:class="{ 'mx-auto px-4': !visibleItems.length }" :class="{ 'mx-auto px-4': !visibleItems.length }"
class="group flex gap-1 items-center active:ring rounded border-1 p-1 hover:bg-primary/10" class="group flex gap-1 items-center active:ring rounded border-1 p-1 hover:(bg-primary bg-opacity-10)"
@click.stop="open" @click.stop="open"
> >
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
@ -118,7 +118,7 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
<template #title> Click or drop a file into cell </template> <template #title> Click or drop a file into cell </template>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120) text-gray-500 text-[10px]" /> <MaterialSymbolsAttachFile class="transform group-hover:(text-accent scale-120) text-gray-500 text-[10px]" />
<div v-if="!visibleItems.length" class="group-hover:text-primary text-gray-500 text-xs">Add file(s)</div> <div v-if="!visibleItems.length" class="group-hover:text-primary text-gray-500 text-xs">Add file(s)</div>
</div> </div>
@ -160,14 +160,14 @@ const { isSharedForm } = useSmartsheetStoreOrThrow()
</div> </div>
</div> </div>
<div class="group flex gap-1 items-center border-1 active:ring rounded p-1 hover:bg-primary/10"> <div class="group flex gap-1 items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)">
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<a-tooltip v-else placement="bottom"> <a-tooltip v-else placement="bottom">
<template #title> View attachments </template> <template #title> View attachments </template>
<MdiArrowExpand <MdiArrowExpand
class="select-none transform group-hover:(text-pink-500 scale-120) text-[10px] text-gray-500" class="select-none transform group-hover:(text-accent scale-120) text-[10px] text-gray-500"
@click.stop="modalVisible = true" @click.stop="modalVisible = true"
/> />
</a-tooltip> </a-tooltip>

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

@ -2,14 +2,28 @@
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 { Empty } from 'ant-design-vue'
import { useNuxtApp } from '#app' import {
import { computed, useProject, useTable, useTabs, useUIPermission, watchEffect } from '#imports' computed,
inject,
reactive,
ref,
useDialog,
useNuxtApp,
useProject,
useTable,
useTabs,
useUIPermission,
watchEffect,
} from '#imports'
import DlgAirtableImport from '~/components/dlg/AirtableImport.vue'
import DlgQuickImport from '~/components/dlg/QuickImport.vue'
import DlgTableCreate from '~/components/dlg/TableCreate.vue'
import DlgTableRename from '~/components/dlg/TableRename.vue'
import { TabType } from '~/composables' import { TabType } from '~/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 MdiMenuIcon from '~icons/mdi/dots-vertical'
import MdiDrag from '~icons/mdi/drag-vertical' import MdiDrag from '~icons/mdi/drag-vertical'
import GithubStarButton from '~/components/dashboard/GithubStarButton.vue'
const { addTab } = useTabs() const { addTab } = useTabs()
@ -25,18 +39,25 @@ const { isUIAllowed } = useUIPermission()
const isLocked = inject('TreeViewIsLockedInj') const isLocked = inject('TreeViewIsLockedInj')
const tablesById = $computed<Record<string, TableType>>(() => let key = $ref(0)
tables?.value?.reduce((acc: Record<string, TableType>, table: TableType) => {
acc[table.id as string] = table const menuRef = $ref<HTMLLIElement>()
const filterQuery = $ref('')
const activeTable = computed(() => ([TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null))
const tablesById = $computed(() =>
tables.value?.reduce((acc: Record<string, TableType>, table) => {
acc[table.id!] = table
return acc return acc
}, {}), }, {}),
) )
const tableCreateDlg = ref(false) const filteredTables = $computed(() =>
tables.value?.filter((table) => !filterQuery || table.title.toLowerCase().includes(filterQuery.toLowerCase())),
let key = $ref(0) )
const menuRef = $ref<HTMLLIElement>()
let sortable: Sortable let sortable: Sortable
@ -104,70 +125,186 @@ const icon = (table: TableType) => {
} }
} }
const filterQuery = $ref('')
const filteredTables = $computed(() => {
return tables?.value?.filter((table) => !filterQuery || table?.title.toLowerCase()?.includes(filterQuery.toLowerCase()))
})
const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({}) const contextMenuTarget = reactive<{ type?: 'table' | 'main'; value?: any }>({})
const setMenuContext = (type: 'table' | 'main', value?: any) => { const setMenuContext = (type: 'table' | 'main', value?: any) => {
contextMenuTarget.type = type contextMenuTarget.type = type
contextMenuTarget.value = value contextMenuTarget.value = value
$e('c:table:create:navdraw:right-click') $e('c:table:create:navdraw:right-click')
} }
const renameTableDlg = ref(false)
const renameTableMeta = ref()
const showRenameTableDlg = (table: TableType, rightClick = false) => {
$e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
renameTableMeta.value = table
renameTableDlg.value = true
}
const reloadTables = async () => { const reloadTables = async () => {
$e('a:table:refresh:navdraw') $e('a:table:refresh:navdraw')
await loadTables() await loadTables()
} }
const addTableTab = (table: TableType) => { const addTableTab = (table: TableType) => {
$e('a:table:open') $e('a:table:open')
addTab({ title: table.title, id: table.id, type: table.type as any }) addTab({ title: table.title, id: table.id, type: table.type as any })
} }
const activeTable = computed(() => { function openRenameTableDialog(table: TableType, rightClick = false) {
return [TabType.TABLE, TabType.VIEW].includes(activeTab.value?.type) ? activeTab.value.title : null $e(rightClick ? 'c:table:rename:navdraw:right-click' : 'c:table:rename:navdraw:options')
})
const isOpen = ref(true)
const { close } = useDialog(DlgTableRename, {
'modelValue': isOpen,
'tableMeta': table,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openQuickImportDialog(type: string) {
$e(`a:actions:import-${type}`)
const isOpen = ref(true)
const { close } = useDialog(DlgQuickImport, {
'modelValue': isOpen,
'importType': type,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openAirtableImportDialog() {
$e('a:actions:import-airtable')
const isOpen = ref(true)
const { close } = useDialog(DlgAirtableImport, {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
function openTableCreateDialog() {
$e('a:actions:create-table')
const isOpen = ref(true)
const { close } = useDialog(DlgTableCreate, {
'modelValue': isOpen,
'onUpdate:modelValue': closeDialog,
})
function closeDialog() {
isOpen.value = false
close(1000)
}
}
</script> </script>
<template> <template>
<div class="nc-treeview-container flex flex-col"> <div class="nc-treeview-container flex flex-col">
<div class="px-6 py-[9px] border-b-1 nc-filter-input">
<div class="flex items-center bg-gray-50 rounded relative">
<a-input
v-model:value="filterQuery"
class="nc-filter-input !bg-transparent"
:placeholder="$t('placeholder.searchProjectTree')"
/>
<MdiSearch class="nc-filter-input-icon text-gray-400 mx-3 absolute right-[-4px] top-[7px]" />
</div>
</div>
<a-dropdown :trigger="['contextmenu']"> <a-dropdown :trigger="['contextmenu']">
<div <div class="pt-2 pl-2 pb-2 flex-1 overflow-y-auto flex flex-col scrollbar-thin-dull" :class="{ 'mb-[20px]': isSharedBase }">
class="pt-2 pl-2 pb-2 flex-1 overflow-y-auto flex flex-column scrollbar-thin-dull" <div class="py-1 px-3 flex w-full items-center gap-1 cursor-pointer" @contextmenu="setMenuContext('main')">
:class="{ 'mb-[20px]': isSharedBase }" <span class="flex-1 text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
style="direction: rtl"
>
<div
style="direction: ltr"
class="py-1 px-3 flex w-full align-center gap-1 cursor-pointer"
@contextmenu="setMenuContext('main')"
>
<span class="flex-grow text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
{{ $t('objects.tables') }} {{ $t('objects.tables') }}
<template v-if="tables?.length"> ({{ tables.length }}) </template> <template v-if="tables?.length"> ({{ tables.length }}) </template>
</span> </span>
</div> </div>
<div style="direction: ltr" class="flex-1">
<div class="flex-1">
<div
v-if="isUIAllowed('table-create')"
class="group flex items-center gap-2 pl-5 pr-3 py-2 text-primary/70 hover:(text-primary/100) cursor-pointer select-none"
@click="openTableCreateDialog"
>
<MdiPlus />
<span class="text-gray-500 group-hover:(text-primary/100) flex-1">{{ $t('tooltip.addTable') }}</span>
<a-dropdown v-if="!isSharedBase" :trigger="['click']" @click.stop>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item-group title="QUICK IMPORT FROM" class="!px-0 !mx-0">
<a-menu-item
v-if="isUIAllowed('airtableImport')"
key="quick-import-airtable"
@click="openAirtableImportDialog"
>
<div class="color-transition nc-project-menu-item group">
<MdiTableLarge class="group-hover:text-accent" />
<!-- TODO: i18n -->
Airtable
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('csvImport')" key="quick-import-csv" @click="openQuickImportDialog('csv')">
<div class="color-transition nc-project-menu-item group">
<MdiFileDocumentOutline class="group-hover:text-accent" />
<!-- TODO: i18n -->
CSV file
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('jsonImport')" key="quick-import-json" @click="openQuickImportDialog('json')">
<div class="color-transition nc-project-menu-item group">
<MdiCodeJson class="group-hover:text-accent" />
<!-- TODO: i18n -->
JSON file
</div>
</a-menu-item>
<a-menu-item
v-if="isUIAllowed('excelImport')"
key="quick-import-excel"
@click="openQuickImportDialog('excel')"
>
<div class="color-transition nc-project-menu-item group">
<MdiFileExcel class="group-hover:text-accent" />
<!-- TODO: i18n -->
Microsoft Excel
</div>
</a-menu-item>
</a-menu-item-group>
<a-menu-divider class="my-0" />
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-t="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
>
<MdiOpenInNew class="group-hover:text-accent" />
<!-- TODO: i18n -->
Request a data source you need?
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<div v-if="tables.length" class="transition-height duration-200 overflow-hidden"> <div v-if="tables.length" class="transition-height duration-200 overflow-hidden">
<div :key="key" ref="menuRef" class="border-none sortable-list"> <div :key="key" ref="menuRef" class="border-none sortable-list">
<div <div
@ -183,7 +320,7 @@ const activeTable = computed(() => {
:data-id="table.id" :data-id="table.id"
@click="addTableTab(table)" @click="addTableTab(table)"
> >
<div class="flex align-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 <MdiDrag
v-if="isUIAllowed('treeview-drag-n-drop')" v-if="isUIAllowed('treeview-drag-n-drop')"
@ -202,30 +339,25 @@ const activeTable = computed(() => {
<div class="nc-tbl-title flex-1">{{ table.title }}</div> <div class="nc-tbl-title flex-1">{{ table.title }}</div>
<a-dropdown <a-dropdown
v-if="!isLocked && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))" v-if="!isSharedBase && !isLocked && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))"
:trigger="['click']" :trigger="['click']"
@click.stop @click.stop
> >
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay> <template #overlay>
<a-menu class="cursor-pointer"> <a-menu class="!py-0 rounded text-sm">
<a-menu-item <a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table)">
v-if="isUIAllowed('table-rename')" <div class="nc-project-menu-item">
v-t="['c:table:rename']" {{ $t('general.rename') }}
class="!text-xs" </div>
@click="showRenameTableDlg(table)" </a-menu-item>
><div>{{ $t('general.rename') }}</div></a-menu-item
> <a-menu-item v-if="isUIAllowed('table-delete')" @click="deleteTable(table)">
<div class="nc-project-menu-item">
<a-menu-item {{ $t('general.delete') }}
v-if="isUIAllowed('table-delete')" </div>
v-t="['c:table:delete']" </a-menu-item>
class="!text-xs"
@click="deleteTable(table)"
>
{{ $t('general.delete') }}</a-menu-item
>
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
@ -234,53 +366,48 @@ const activeTable = computed(() => {
</div> </div>
</div> </div>
<a-card v-else class="mt-4 mx-4 !bg-gray-50"> <div v-else class="mt-0.5 pt-16 mx-3 flex flex-col items-center border-t-1 border-gray-50">
<div class="flex flex-col align-center"> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" />
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" /> </div>
<a-button type="primary" @click.stop="tableCreateDlg = true">{{ $t('tooltip.addTable') }}</a-button>
</div>
</a-card>
</div> </div>
</div> </div>
<template v-if="!isLocked" #overlay> <template v-if="!isLocked && !isSharedBase" #overlay>
<a-menu class="cursor-pointer"> <a-menu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'table'"> <template v-if="contextMenuTarget.type === 'table'">
<a-menu-item <a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value)">
v-if="isUIAllowed('table-rename')" <div class="nc-project-menu-item">
v-t="['c:table:rename']" {{ $t('general.rename') }}
class="!text-xs" </div>
@click="showRenameTableDlg(contextMenuTarget.value)"
>
{{ $t('general.rename') }}
</a-menu-item> </a-menu-item>
<a-menu-item
v-if="isUIAllowed('table-delete')" <a-menu-item v-if="isUIAllowed('table-delete')" @click="deleteTable(contextMenuTarget.value)">
v-t="['c:table:delete']" <div class="nc-project-menu-item">
class="!text-xs" {{ $t('general.delete') }}
@click="deleteTable(contextMenuTarget.value)" </div>
>
{{ $t('general.delete') }}
</a-menu-item> </a-menu-item>
</template> </template>
<template v-else> <template v-else>
<a-menu-item v-t="['c:table:reload']" class="!text-xs" @click="reloadTables"> <a-menu-item @click="reloadTables">
{{ $t('general.reload') }} <div class="nc-project-menu-item">
{{ $t('general.reload') }}
</div>
</a-menu-item> </a-menu-item>
</template> </template>
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<a-divider class="mt-0 mb-2" /> <a-divider class="!my-0" />
<div class="items-center flex justify-center mb-1"> <div class="flex items-start flex-col justify-start px-4 py-3 gap-2">
<GithubStarButton /> <GeneralShareBaseButton class="py-1 px-2 text-primary font-bold cursor-pointer select-none" />
</div>
<DlgTableCreate v-if="tableCreateDlg" v-model="tableCreateDlg" /> <GeneralHelpAndSupport class="py-1 px-2 text-gray-500 cursor-pointer select-none" />
<DlgTableRename v-if="renameTableMeta" v-model="renameTableDlg" :table-meta="renameTableMeta" />
<DashboardGithubStarButton class="ml-2 py-1" />
</div>
</div> </div>
</template> </template>
@ -290,7 +417,7 @@ const activeTable = computed(() => {
} }
.nc-treeview-footer-item { .nc-treeview-footer-item {
@apply cursor-pointer px-4 py-2 flex align-center hover:bg-gray-200/20 text-xs text-current; @apply cursor-pointer px-4 py-2 flex items-center hover:bg-gray-200/20 text-xs text-current;
} }
:deep(.nc-filter-input input::placeholder) { :deep(.nc-filter-input input::placeholder) {
@ -326,7 +453,7 @@ const activeTable = computed(() => {
} }
.sortable-chosen { .sortable-chosen {
@apply !bg-primary/25 text-primary; @apply !bg-primary bg-opacity-25 text-primary;
} }
} }
@ -335,20 +462,20 @@ const activeTable = computed(() => {
} }
.nc-tree-item svg { .nc-tree-item svg {
@apply text-primary/60; @apply text-primary text-opacity-60;
} }
.nc-tree-item.active { .nc-tree-item.active {
@apply !text-primary font-weight-bold after:(!opacity-20); @apply text-primary font-weight-bold after:(!opacity-20);
@apply border-r-3 border-indigo-500; @apply border-r-3 border-primary;
svg { svg {
@apply !text-primary; @apply text-primary !text-opacity-100;
} }
} }
.nc-tree-item:hover { .nc-tree-item:hover {
@apply !text-grey after:(!opacity-5); @apply text-primary after:(!opacity-5);
} }
:deep(.nc-filter-input) { :deep(.nc-filter-input) {
@ -356,4 +483,28 @@ const activeTable = computed(() => {
@apply pr-6 !border-0; @apply pr-6 !border-0;
} }
} }
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply !mx-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
:deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-accent);
}
:deep(.ant-dropdown-menu-title-content) {
@apply !p-0;
}
</style> </style>

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

@ -130,7 +130,7 @@ onMounted(async () => {
/> />
<div v-else /> <div v-else />
</div> </div>
<div class="flex flex-col flex-grow-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>

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

@ -6,7 +6,7 @@ import MdiDatabaseSync from '~icons/mdi/database-sync'
import { extractSdkResponseErrorMsg } from '~/utils' import { extractSdkResponseErrorMsg } from '~/utils'
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { project } = useProject() const { project, loadTables } = useProject()
let isLoading = $ref(false) let isLoading = $ref(false)
let isDifferent = $ref(false) let isDifferent = $ref(false)
@ -39,6 +39,7 @@ async function syncMetaDiff() {
isLoading = true isLoading = true
await $api.project.metaDiffSync(project.value.id) await $api.project.metaDiffSync(project.value.id)
message.info('Table metadata recreated successfully') message.info('Table metadata recreated successfully')
await loadTables()
await loadMetaDiff() await loadMetaDiff()
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
@ -58,8 +59,9 @@ const tableHeaderRenderer = (label: string) => () => h('div', { class: 'text-gra
const columns = [ const columns = [
{ {
title: tableHeaderRenderer('Models'), title: tableHeaderRenderer('Models'),
dataIndex: 'title', key: 'table_name',
key: 'title', customRender: ({ record }: { record: { table_name: string; title?: string } }) =>
h('div', {}, record.title || record.table_name),
}, },
{ {
title: tableHeaderRenderer('Sync State'), title: tableHeaderRenderer('Sync State'),
@ -73,7 +75,7 @@ const columns = [
<template> <template>
<div class="flex flex-row w-full"> <div class="flex flex-row w-full">
<div class="flex flex-column w-3/5"> <div class="flex flex-col w-3/5">
<div class="flex flex-row justify-end items-center w-full mb-4"> <div class="flex flex-row justify-end items-center w-full mb-4">
<a-button class="self-start nc-btn-metasync-reload" @click="loadMetaDiff"> <a-button class="self-start nc-btn-metasync-reload" @click="loadMetaDiff">
<div class="flex items-center gap-2 text-gray-600 font-light"> <div class="flex items-center gap-2 text-gray-600 font-light">

2
packages/nc-gui-v2/components/dashboard/settings/Misc.vue

@ -7,7 +7,7 @@ watch(includeM2M, async () => await loadTables())
<template> <template>
<div class="flex flex-row w-full"> <div class="flex flex-row w-full">
<div class="flex flex-column 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">
<a-checkbox v-model:checked="includeM2M">Show M2M Tables</a-checkbox> <a-checkbox v-model:checked="includeM2M">Show M2M Tables</a-checkbox>
</div> </div>

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

@ -142,7 +142,7 @@ watch(
<a-typography-title class="ml-4 select-none" type="secondary" :level="5">SETTINGS</a-typography-title> <a-typography-title class="ml-4 select-none" type="secondary" :level="5">SETTINGS</a-typography-title>
<a-button type="text" class="!rounded-md border-none -mt-1.5 -mr-1" @click="vModel = false"> <a-button type="text" class="!rounded-md border-none -mt-1.5 -mr-1" @click="vModel = false">
<template #icon> <template #icon>
<MdiCloseIcon class="cursor-pointer mt-1" /> <MdiCloseIcon class="cursor-pointer mt-1 nc-modal-close" />
</template> </template>
</a-button> </a-button>
</div> </div>

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

@ -32,8 +32,7 @@ async function loadTableList() {
isLoading = true isLoading = true
tables = await $api.project.modelVisibilityList(project.value?.id, { tables = await $api.project.modelVisibilityList(project.value?.id, {
// FIXME: type includeM2M: includeM2M.value,
includeM2M: includeM2M.value || '',
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@ -99,7 +98,7 @@ const columns = [
<template> <template>
<div class="flex flex-row w-full"> <div class="flex flex-row w-full">
<div class="flex flex-column 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">
<a-input v-model:value="searchInput" placeholder="Search models" class="nc-acl-search"> <a-input v-model:value="searchInput" placeholder="Search models" class="nc-acl-search">
<template #prefix> <template #prefix>
@ -137,7 +136,7 @@ const columns = [
<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 align-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>

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

@ -1,12 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import io from 'socket.io-client'
import type { Socket } from 'socket.io-client' import type { Socket } from 'socket.io-client'
import { Form, message } from 'ant-design-vue' 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 { extractSdkResponseErrorMsg, fieldRequiredValidator } from '~/utils' import { Form, message } from 'ant-design-vue'
import MdiCloseCircleOutlineIcon from '~icons/mdi/close-circle-outline' import {
import MdiCurrencyUsdIcon from '~icons/mdi/currency-usd' computed,
import MdiLoadingIcon from '~icons/mdi/loading' extractSdkResponseErrorMsg,
fieldRequiredValidator,
nextTick,
onBeforeUnmount,
onMounted,
ref,
useNuxtApp,
useProject,
watch,
} from '#imports'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
@ -54,29 +62,21 @@ const syncSource = ref({
}, },
}) })
const validators = computed(() => { const validators = computed(() => ({
return { 'details.apiKey': [fieldRequiredValidator],
'details.apiKey': [fieldRequiredValidator], 'details.syncSourceUrlOrId': [fieldRequiredValidator],
'details.syncSourceUrlOrId': [fieldRequiredValidator], }))
}
})
const dialogShow = computed({ const dialogShow = computed({
get() { get: () => modelValue,
return modelValue set: (v) => emit('update:modelValue', v),
},
set(v) {
emit('update:modelValue', v)
},
}) })
const useForm = Form.useForm const useForm = Form.useForm
const { validateInfos } = useForm(syncSource, validators) const { validateInfos } = useForm(syncSource, validators)
const disableImportButton = computed(() => { const disableImportButton = computed(() => !syncSource.value.details.apiKey || !syncSource.value.details.syncSourceUrlOrId)
return !syncSource.value.details.apiKey || !syncSource.value.details.syncSourceUrlOrId
})
async function saveAndSync() { async function saveAndSync() {
await createOrUpdate() await createOrUpdate()
@ -86,6 +86,7 @@ async function saveAndSync() {
async function createOrUpdate() { async function createOrUpdate() {
try { try {
const { id, ...payload } = syncSource.value const { id, ...payload } = syncSource.value
if (id !== '') { if (id !== '') {
await $fetch(`/api/v1/db/meta/syncs/${id}`, { await $fetch(`/api/v1/db/meta/syncs/${id}`, {
baseURL, baseURL,
@ -94,13 +95,12 @@ async function createOrUpdate() {
body: payload, body: payload,
}) })
} else { } else {
const data: any = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs`, { syncSource.value = await $fetch(`/api/v1/db/meta/projects/${project.value.id}/syncs`, {
baseURL, baseURL,
method: 'POST', method: 'POST',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
body: payload, body: payload,
}) })
syncSource.value = data
} }
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
@ -113,7 +113,9 @@ async function loadSyncSrc() {
method: 'GET', method: 'GET',
headers: { 'xc-auth': $state.token.value as string }, headers: { 'xc-auth': $state.token.value as string },
}) })
const { list: srcs } = data const { list: srcs } = data
if (srcs && srcs[0]) { if (srcs && srcs[0]) {
srcs[0].details = srcs[0].details || {} srcs[0].details = srcs[0].details || {}
syncSource.value = migrateSync(srcs[0]) syncSource.value = migrateSync(srcs[0])
@ -171,6 +173,7 @@ function migrateSync(src: any) {
src.details.options.syncViews = src.syncViews src.details.options.syncViews = src.syncViews
delete src.syncViews delete src.syncViews
} }
return src return src
} }
@ -188,6 +191,7 @@ onMounted(async () => {
socket = io(new URL(baseURL, window.location.href.split(/[?#]/)[0]).href, { socket = io(new URL(baseURL, window.location.href.split(/[?#]/)[0]).href, {
extraHeaders: { 'xc-auth': $state.token.value as string }, extraHeaders: { 'xc-auth': $state.token.value as string },
}) })
socket.on('connect_error', () => { socket.on('connect_error', () => {
socket?.disconnect() socket?.disconnect()
socket = null socket = null
@ -203,7 +207,7 @@ onMounted(async () => {
progress.value.push(d) progress.value.push(d)
// FIXME: this doesn't work // FIXME: this doesn't work
nextTick(() => { await nextTick(() => {
;(logRef.value?.$el as HTMLDivElement).scrollTo() ;(logRef.value?.$el as HTMLDivElement).scrollTo()
}) })
@ -213,6 +217,7 @@ onMounted(async () => {
// TODO: add tab of the first table // TODO: add tab of the first table
} }
}) })
await loadSyncSrc() await loadSyncSrc()
}) })
@ -224,32 +229,14 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<a-modal <a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" class="p-2" @keydown.esc="dialogShow = false">
v-model:visible="dialogShow" <div class="px-5">
width="max(30vw, 600px)" <div class="mt-5 prose-xl font-weight-bold">QUICK IMPORT - AIRTABLE</div>
:mask-closable="false"
class="pa-2"
@keydown.esc="dialogShow = false"
>
<template #footer>
<div v-if="step === 1">
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button
key="submit"
v-t="['c:sync-airtable:save-and-sync']"
type="primary"
class="nc-btn-airtable-import"
:disabled="disableImportButton"
@click="saveAndSync"
>Import
</a-button>
</div>
</template>
<span class="ml-5 mt-5 prose-xl font-weight-bold" type="secondary" :level="5">QUICK IMPORT - AIRTABLE</span>
<div class="ml-5 mr-5">
<div v-if="step === 1"> <div v-if="step === 1">
<div class="mb-4"> <div class="mb-4">
<span class="mr-3 pt-2 text-gray-500 text-xs">Credentials</span> <span class="mr-3 pt-2 text-gray-500 text-xs">Credentials</span>
<a <a
href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials" href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials"
class="prose-sm underline text-grey text-xs" class="prose-sm underline text-grey text-xs"
@ -257,7 +244,8 @@ onBeforeUnmount(() => {
>Where to find this? >Where to find this?
</a> </a>
</div> </div>
<a-form ref="form" :model="syncSource" name="quick-import-airtable-form" layout="horizontal" class="ma-0">
<a-form ref="form" :model="syncSource" name="quick-import-airtable-form" layout="horizontal" class="m-0">
<a-form-item v-bind="validateInfos['details.apiKey']"> <a-form-item v-bind="validateInfos['details.apiKey']">
<a-input-password <a-input-password
v-model:value="syncSource.details.apiKey" v-model:value="syncSource.details.apiKey"
@ -266,6 +254,7 @@ onBeforeUnmount(() => {
size="large" size="large"
/> />
</a-form-item> </a-form-item>
<a-form-item v-bind="validateInfos['details.syncSourceUrlOrId']"> <a-form-item v-bind="validateInfos['details.syncSourceUrlOrId']">
<a-input <a-input
v-model:value="syncSource.details.syncSourceUrlOrId" v-model:value="syncSource.details.syncSourceUrlOrId"
@ -274,23 +263,31 @@ onBeforeUnmount(() => {
size="large" size="large"
/> />
</a-form-item> </a-form-item>
<span class="prose-lg self-center my-4 text-gray-500">Advanced Settings</span>
<div class="prose-lg self-center my-4 text-gray-500">Advanced Settings</div>
<a-divider class="mt-2 mb-5" /> <a-divider class="mt-2 mb-5" />
<div class="mt-0 my-2"> <div class="mt-0 my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncData">Import Data</a-checkbox> <a-checkbox v-model:checked="syncSource.details.options.syncData">Import Data</a-checkbox>
</div> </div>
<div class="my-2"> <div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncViews">Import Secondary Views</a-checkbox> <a-checkbox v-model:checked="syncSource.details.options.syncViews">Import Secondary Views</a-checkbox>
</div> </div>
<div class="my-2"> <div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncRollup">Import Rollup Columns</a-checkbox> <a-checkbox v-model:checked="syncSource.details.options.syncRollup">Import Rollup Columns</a-checkbox>
</div> </div>
<div class="my-2"> <div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncLookup">Import Lookup Columns</a-checkbox> <a-checkbox v-model:checked="syncSource.details.options.syncLookup">Import Lookup Columns</a-checkbox>
</div> </div>
<div class="my-2"> <div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncAttachment">Import Attachment Columns</a-checkbox> <a-checkbox v-model:checked="syncSource.details.options.syncAttachment">Import Attachment Columns</a-checkbox>
</div> </div>
<a-tooltip placement="top"> <a-tooltip placement="top">
<template #title> <template #title>
<span>Coming Soon!</span> <span>Coming Soon!</span>
@ -298,29 +295,39 @@ onBeforeUnmount(() => {
<a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>Import Formula Columns</a-checkbox> <a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>Import Formula Columns</a-checkbox>
</a-tooltip> </a-tooltip>
</a-form> </a-form>
<a-divider /> <a-divider />
<div> <div>
<a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank">Questions / Help - Reach out here</a> <a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank">Questions / Help - Reach out here</a>
<br /> <br />
<div> <div>
This feature is currently in beta and more information can be found This feature is currently in beta and more information can be found
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">here</a>. <a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">here</a>.
</div> </div>
</div> </div>
</div> </div>
<div v-if="step === 2"> <div v-if="step === 2">
<div class="mb-4 prose-xl font-bold">Logs</div> <div class="mb-4 prose-xl font-bold">Logs</div>
<a-card ref="logRef" body-style="background-color: #000000; height:400px; overflow: auto;">
<a-card ref="logRef" :body-style="{ backgroundColor: '#000000', height: '400px', overflow: 'auto' }">
<div v-for="({ msg, status }, i) in progress" :key="i"> <div v-for="({ msg, status }, i) in progress" :key="i">
<div v-if="status === 'FAILED'" class="flex items-center"> <div v-if="status === 'FAILED'" class="flex items-center">
<MdiCloseCircleOutlineIcon class="text-red-500" /> <MdiCloseCircleOutline class="text-red-500" />
<span class="text-red-500 ml-2">{{ msg }}</span> <span class="text-red-500 ml-2">{{ msg }}</span>
</div> </div>
<div v-else class="flex items-center"> <div v-else class="flex items-center">
<MdiCurrencyUsdIcon class="text-green-500" /> <MdiCurrencyUsd class="text-green-500" />
<span class="text-green-500 ml-2">{{ msg }}</span> <span class="text-green-500 ml-2">{{ msg }}</span>
</div> </div>
</div> </div>
<div <div
v-if=" v-if="
!progress || !progress ||
@ -329,18 +336,34 @@ onBeforeUnmount(() => {
" "
class="flex items-center" class="flex items-center"
> >
<MdiLoadingIcon class="text-green-500 animate-spin" /> <MdiLoading class="text-green-500 animate-spin" />
<span class="text-green-500 ml-2"> Importing</span> <span class="text-green-500 ml-2"> Importing</span>
</div> </div>
</a-card> </a-card>
<div class="flex justify-center items-center"> <div class="flex justify-center items-center">
<a-button v-if="showGoToDashboardButton" class="mt-4" size="large" @click="dialogShow = false" <a-button v-if="showGoToDashboardButton" class="mt-4" size="large" @click="dialogShow = false">
>Go to Dashboard</a-button Go to Dashboard
> </a-button>
</div> </div>
</div> </div>
</div> </div>
<template #footer>
<div v-if="step === 1">
<a-button key="back" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button
key="submit"
v-t="['c:sync-airtable:save-and-sync']"
type="primary"
class="nc-btn-airtable-import"
:disabled="disableImportButton"
@click="saveAndSync"
>
Import
</a-button>
</div>
</template>
</a-modal> </a-modal>
</template> </template>
<style scoped lang="scss"></style>

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

@ -1,24 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form, message } from 'ant-design-vue' import { Form, message } from 'ant-design-vue'
import type { TableType } from 'nocodb-sdk' import type { TableType } from 'nocodb-sdk'
import type { UploadChangeParam } from 'ant-design-vue' import type { UploadChangeParam, UploadFile } from 'ant-design-vue'
import { useI18n } from 'vue-i18n' import {
import MdiFileIcon from '~icons/mdi/file-plus-outline' ExcelTemplateAdapter,
import MdiFileUploadOutlineIcon from '~icons/mdi/file-upload-outline' ExcelUrlTemplateAdapter,
import MdiLinkVariantIcon from '~icons/mdi/link-variant' JSONTemplateAdapter,
import MdiCodeJSONIcon from '~icons/mdi/code-json' JSONUrlTemplateAdapter,
import { fieldRequiredValidator, importCsvUrlValidator, importExcelUrlValidator, importUrlValidator } from '~/utils/validation' computed,
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils' extractSdkResponseErrorMsg,
import { ExcelTemplateAdapter, ExcelUrlTemplateAdapter, JSONTemplateAdapter, JSONUrlTemplateAdapter } from '~/utils/parsers' fieldRequiredValidator,
import { useProject } from '#imports' importCsvUrlValidator,
importExcelUrlValidator,
importUrlValidator,
reactive,
ref,
useI18n,
useProject,
useVModel,
} from '#imports'
interface Props { interface Props {
modelValue: boolean modelValue: boolean
importType: 'csv' | 'json' | 'excel' importType: 'csv' | 'json' | 'excel'
importOnly: boolean importOnly?: boolean
} }
const { importType, importOnly, ...rest } = defineProps<Props>() const { importType, importOnly = false, ...rest } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -45,7 +53,7 @@ const templateEditorModal = ref(false)
const useForm = Form.useForm const useForm = Form.useForm
const importState = reactive({ const importState = reactive({
fileList: [] as Record<string, any>, fileList: [] as (UploadFile & { data: string | ArrayBuffer })[],
url: '', url: '',
jsonEditor: {}, jsonEditor: {},
parserConfig: { parserConfig: {
@ -61,12 +69,10 @@ const isImportTypeCsv = computed(() => importType === 'csv')
const IsImportTypeExcel = computed(() => importType === 'excel') const IsImportTypeExcel = computed(() => importType === 'excel')
const validators = computed(() => { const validators = computed(() => ({
return { url: [fieldRequiredValidator, importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator],
url: [fieldRequiredValidator, importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator], maxRowsToParse: [fieldRequiredValidator],
maxRowsToParse: [fieldRequiredValidator], }))
}
})
const { validate, validateInfos } = useForm(importState, validators) const { validate, validateInfos } = useForm(importState, validators)
@ -104,15 +110,14 @@ const disablePreImportButton = computed(() => {
return !(importState.fileList.length > 0) return !(importState.fileList.length > 0)
} else if (activeKey.value === 'urlTab') { } else if (activeKey.value === 'urlTab') {
if (!validateInfos.url.validateStatus) return true if (!validateInfos.url.validateStatus) return true
return validateInfos.url.validateStatus === 'error' return validateInfos.url.validateStatus === 'error'
} else if (activeKey.value === 'jsonEditorTab') { } else if (activeKey.value === 'jsonEditorTab') {
return !jsonEditorRef.value?.isValid return !jsonEditorRef.value?.isValid
} }
}) })
const disableImportButton = computed(() => { const disableImportButton = computed(() => !templateEditorRef.value?.isValid)
return !templateEditorRef.value?.isValid
})
const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid) const disableFormatJsonButton = computed(() => !jsonEditorRef.value?.isValid)
@ -120,16 +125,19 @@ const modalWidth = computed(() => {
if (importType === 'excel' && templateEditorModal.value) { if (importType === 'excel' && templateEditorModal.value) {
return 'max(90vw, 600px)' return 'max(90vw, 600px)'
} }
return 'max(60vw, 600px)' return 'max(60vw, 600px)'
}) })
async function handlePreImport() { async function handlePreImport() {
loading.value = true loading.value = true
if (activeKey.value === 'uploadTab') { if (activeKey.value === 'uploadTab') {
await parseAndExtractData(importState.fileList[0].data, importState.fileList[0].name) await parseAndExtractData(importState.fileList[0].data, importState.fileList[0].name)
} else if (activeKey.value === 'urlTab') { } else if (activeKey.value === 'urlTab') {
try { try {
await validate() await validate()
await parseAndExtractData(importState.url, '') await parseAndExtractData(importState.url, '')
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
@ -137,44 +145,53 @@ async function handlePreImport() {
} else if (activeKey.value === 'jsonEditorTab') { } else if (activeKey.value === 'jsonEditorTab') {
await parseAndExtractData(JSON.stringify(importState.jsonEditor), '') await parseAndExtractData(JSON.stringify(importState.jsonEditor), '')
} }
loading.value = false loading.value = false
} }
async function handleImport() { async function handleImport() {
try { try {
loading.value = true loading.value = true
await templateEditorRef.value.importTemplate() await templateEditorRef.value.importTemplate()
} catch (e: any) { } catch (e: any) {
return message.error(await extractSdkResponseErrorMsg(e)) return message.error(await extractSdkResponseErrorMsg(e))
} finally { } finally {
loading.value = false loading.value = false
} }
dialogShow.value = false dialogShow.value = false
} }
async function parseAndExtractData(val: any, name: string) { async function parseAndExtractData(val: string | ArrayBuffer, name: string) {
try { try {
templateData.value = null templateData.value = null
importData.value = null importData.value = null
importColumns.value = [] importColumns.value = []
const templateGenerator: any = getAdapter(name, val)
const templateGenerator = getAdapter(name, val)
if (!templateGenerator) { if (!templateGenerator) {
message.error('Template Generator cannot be found!') message.error('Template Generator cannot be found!')
return return
} }
await templateGenerator.init() await templateGenerator.init()
templateGenerator.parse() templateGenerator.parse()
templateData.value = templateGenerator.getTemplate() templateData.value = templateGenerator.getTemplate()
templateData.value.tables[0].table_name = populateUniqueTableName() templateData.value.tables[0].table_name = populateUniqueTableName()
importData.value = templateGenerator.getData() importData.value = templateGenerator.getData()
if (importOnly) importColumns.value = templateGenerator.getColumns() if (importOnly) importColumns.value = templateGenerator.getColumns()
templateEditorModal.value = true templateEditorModal.value = true
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
} }
function rejectDrop(fileList: any[]) { function rejectDrop(fileList: UploadFile[]) {
fileList.map((file) => { fileList.map((file) => {
return message.error(`Failed to upload file ${file.name}`) return message.error(`Failed to upload file ${file.name}`)
}) })
@ -182,16 +199,31 @@ function rejectDrop(fileList: any[]) {
function handleChange(info: UploadChangeParam) { function handleChange(info: UploadChangeParam) {
const status = info.file.status const status = info.file.status
if (status !== 'uploading') {
const reader: any = new FileReader() if (status !== 'uploading' && status !== 'removed') {
reader.onload = (e: any) => { const reader = new FileReader()
const target: any = importState.fileList.find((f: any) => f?.uid === info.file.uid)
if (target) { reader.onload = (e: ProgressEvent<FileReader>) => {
target.data = e.target.result const target = importState.fileList.find((f) => f.uid === info.file.uid)
if (e.target && e.target.result) {
/** if the file was pushed into the list by `<a-upload-dragger>` we just add the data to the file */
if (target) {
target.data = e.target.result
} else if (!target) {
/** if the file was added programmatically and not with d&d, we create file infos and push it into the list */
importState.fileList.push({
...info.file,
status: 'done',
data: e.target.result,
})
}
} }
} }
reader.readAsArrayBuffer(info.file.originFileObj)
reader.readAsArrayBuffer(info.file.originFileObj!)
} }
if (status === 'done') { if (status === 'done') {
message.success(`Uploaded file ${info.file.name} successfully`) message.success(`Uploaded file ${info.file.name} successfully`)
} else if (status === 'error') { } else if (status === 'error') {
@ -205,9 +237,11 @@ function formatJson() {
function populateUniqueTableName() { function populateUniqueTableName() {
let c = 1 let c = 1
while (tables.value.some((t: TableType) => t.title === `Sheet${c}`)) { while (tables.value.some((t: TableType) => t.title === `Sheet${c}`)) {
c++ c++
} }
return `Sheet${c}` return `Sheet${c}`
} }
@ -229,23 +263,133 @@ function getAdapter(name: string, val: any) {
return new JSONTemplateAdapter(name, val, importState.parserConfig) return new JSONTemplateAdapter(name, val, importState.parserConfig)
} }
} }
return null return null
} }
defineExpose({
handleChange,
})
</script> </script>
<template> <template>
<a-modal v-model:visible="dialogShow" :width="modalWidth" :mask-closable="false" @keydown.esc="dialogShow = false"> <a-modal v-model:visible="dialogShow" :width="modalWidth" @keydown.esc="dialogShow = false">
<span class="prose-xl font-weight-bold ml-5 mt-5 mb-5" type="secondary" :level="5">{{ importMeta.header }}</span> <div class="px-5">
<div class="prose-xl font-weight-bold my-5">{{ importMeta.header }}</div>
<div class="mt-5">
<TemplateEditor
v-if="templateEditorModal"
ref="templateEditorRef"
:project-template="templateData"
:import-data="importData"
:import-columns="importColumns"
:import-only="importOnly"
:quick-import-type="importType"
:max-rows-to-parse="importState.parserConfig.maxRowsToParse"
@import="handleImport"
/>
<a-tabs v-else v-model:activeKey="activeKey" hide-add type="editable-card" tab-position="top">
<a-tab-pane key="uploadTab" :closable="false">
<template #tab>
<div class="flex items-center gap-2">
<MdiFileUploadOutline />
Upload
</div>
</template>
<div class="py-6">
<a-upload-dragger
v-model:fileList="importState.fileList"
name="file"
class="nc-input-import !scrollbar-thin-dull"
:accept="importMeta.acceptTypes"
:max-count="1"
list-type="picture"
@change="handleChange"
@reject="rejectDrop"
>
<MdiFilePlusOutline size="large" />
<p class="ant-upload-text">Click or drag file to this area to upload</p>
<p class="ant-upload-hint">
{{ importMeta.uploadHint }}
</p>
</a-upload-dragger>
</div>
</a-tab-pane>
<a-tab-pane v-if="isImportTypeJson" key="jsonEditorTab" :closable="false">
<template #tab>
<span class="flex items-center gap-2">
<MdiCodeJson />
JSON Editor
</span>
</template>
<div class="pb-3 pt-3">
<MonacoEditor ref="jsonEditorRef" v-model="importState.jsonEditor" class="min-h-60 max-h-80" />
</div>
</a-tab-pane>
<a-tab-pane v-else key="urlTab" :closable="false">
<template #tab>
<span class="flex items-center gap-2">
<MdiLinkVariant />
URL
</span>
</template>
<div class="pr-10 pt-5">
<a-form :model="importState" name="quick-import-url-form" layout="horizontal" class="mb-0">
<a-form-item :label="importMeta.urlInputLabel" v-bind="validateInfos.url">
<a-input v-model:value="importState.url" size="large" />
</a-form-item>
</a-form>
</div>
</a-tab-pane>
</a-tabs>
</div>
<div v-if="!templateEditorModal">
<a-divider />
<div class="mb-4">
<span class="prose-lg">Advanced Settings</span>
<a-form-item class="mt-4 mb-2" :label="t('msg.info.footMsg')" v-bind="validateInfos.maxRowsToParse">
<a-input-number v-model:value="importState.parserConfig.maxRowsToParse" :min="1" :max="50000" />
</a-form-item>
<div v-if="isImportTypeJson" class="mt-3">
<a-checkbox v-model:checked="importState.parserConfig.normalizeNested">
<span class="caption">Flatten nested</span>
</a-checkbox>
</div>
<div v-if="isImportTypeJson" class="mt-4">
<a-checkbox v-model:checked="importState.parserConfig.importData">Import data</a-checkbox>
</div>
</div>
</div>
</div>
<template #footer> <template #footer>
<a-button v-if="templateEditorModal" key="back" @click="templateEditorModal = false">Back</a-button> <a-button v-if="templateEditorModal" key="back" @click="templateEditorModal = false">Back</a-button>
<a-button v-else key="cancel" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button> <a-button v-else key="cancel" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button <a-button
v-if="activeKey === 'jsonEditorTab' && !templateEditorModal" v-if="activeKey === 'jsonEditorTab' && !templateEditorModal"
key="format" key="format"
:disabled="disableFormatJsonButton" :disabled="disableFormatJsonButton"
@click="formatJson" @click="formatJson"
>Format JSON</a-button
> >
Format JSON
</a-button>
<a-button <a-button
v-if="!templateEditorModal" v-if="!templateEditorModal"
key="pre-import" key="pre-import"
@ -254,101 +398,13 @@ function getAdapter(name: string, val: any) {
:loading="loading" :loading="loading"
:disabled="disablePreImportButton" :disabled="disablePreImportButton"
@click="handlePreImport" @click="handlePreImport"
>{{ $t('activity.import') }} >
{{ $t('activity.import') }}
</a-button>
<a-button v-else key="import" type="primary" :loading="loading" :disabled="disableImportButton" @click="handleImport">
{{ $t('activity.import') }}
</a-button> </a-button>
<a-button v-else key="import" type="primary" :loading="loading" :disabled="disableImportButton" @click="handleImport">{{
$t('activity.import')
}}</a-button>
</template> </template>
<div class="ml-5 mr-5 mt-5">
<TemplateEditor
v-if="templateEditorModal"
ref="templateEditorRef"
:project-template="templateData"
:import-data="importData"
:import-columns="importColumns"
:import-only="importOnly"
:quick-import-type="importType"
:max-rows-to-parse="importState.parserConfig.maxRowsToParse"
@import="handleImport"
/>
<a-tabs v-else v-model:activeKey="activeKey" hide-add type="editable-card" :tab-position="top">
<a-tab-pane key="uploadTab" :closable="false">
<template #tab>
<span class="flex items-center gap-2">
<MdiFileUploadOutlineIcon />
Upload
</span>
</template>
<div class="pr-10 pb-0 pt-5">
<a-upload-dragger
v-model:fileList="importState.fileList"
name="file"
class="nc-input-import"
:accept="importMeta.acceptTypes"
:max-count="1"
list-type="picture"
@change="handleChange"
@reject="rejectDrop"
>
<MdiFileIcon size="large" />
<p class="ant-upload-text">Click or drag file to this area to upload</p>
<p class="ant-upload-hint">
{{ importMeta.uploadHint }}
</p>
</a-upload-dragger>
</div>
</a-tab-pane>
<a-tab-pane v-if="isImportTypeJson" key="jsonEditorTab" :closable="false">
<template #tab>
<span class="flex items-center gap-2">
<MdiCodeJSONIcon />
JSON Editor
</span>
</template>
<div class="pb-3 pt-3">
<MonacoEditor ref="jsonEditorRef" v-model="importState.jsonEditor" class="min-h-60 max-h-80" />
</div>
</a-tab-pane>
<a-tab-pane v-else key="urlTab" :closable="false">
<template #tab>
<span class="flex items-center gap-2">
<MdiLinkVariantIcon />
URL
</span>
</template>
<div class="pr-10 pt-5">
<a-form :model="importState" name="quick-import-url-form" layout="horizontal" class="mb-0">
<a-form-item :label="importMeta.urlInputLabel" v-bind="validateInfos.url">
<a-input v-model:value="importState.url" size="large" />
</a-form-item>
</a-form>
</div>
</a-tab-pane>
</a-tabs>
</div>
<div v-if="!templateEditorModal" class="ml-5 mr-5">
<a-divider />
<div class="mb-4">
<span class="prose-lg">Advanced Settings</span>
<a-form-item class="mt-4 mb-2" :label="t('msg.info.footMsg')" v-bind="validateInfos.maxRowsToParse">
<a-input-number v-model:value="importState.parserConfig.maxRowsToParse" :min="1" :max="50000" />
</a-form-item>
<div v-if="isImportTypeJson" class="mt-3">
<a-checkbox v-model:checked="importState.parserConfig.normalizeNested">
<span class="caption">Flatten nested</span>
</a-checkbox>
</div>
<div v-if="isImportTypeJson" class="mt-4">
<a-checkbox v-model:checked="importState.parserConfig.importData">Import data</a-checkbox>
</div>
</div>
</div>
</a-modal> </a-modal>
</template> </template>
<style scoped lang="scss">
:deep(.ant-upload-list) {
@apply max-h-80 overflow-auto;
}
</style>

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

@ -1,11 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Form } from 'ant-design-vue' import { Form } from 'ant-design-vue'
import { onMounted, useProject, useTable, useTabs } from '#imports' import { computed, onMounted, ref, useProject, useTable, useTabs, useVModel, validateTableName } from '#imports'
import { validateTableName } from '~/utils/validation'
import { TabType } from '~/composables' import { TabType } from '~/composables'
interface Props { interface Props {
modelValue?: boolean modelValue: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -16,6 +15,8 @@ const dialogShow = useVModel(props, 'modelValue', emit)
const isAdvanceOptVisible = ref(false) const isAdvanceOptVisible = ref(false)
const inputEl = ref<HTMLInputElement>()
const { addTab } = useTabs() const { addTab } = useTabs()
const { loadTables, isMysql, isMssql, isPg } = useProject() const { loadTables, isMysql, isMssql, isPg } = useProject()
@ -28,16 +29,14 @@ const { table, createTable, generateUniqueTitle, tables, project } = useTable(as
title: table.title, title: table.title,
type: TabType.TABLE, type: TabType.TABLE,
}) })
dialogShow.value = false dialogShow.value = false
}) })
const validateDuplicateAlias = (v: string) => {
return (tables?.value || []).every((t) => t.title !== (v || '')) || 'Duplicate table alias'
}
const inputEl = ref<HTMLInputElement>()
const useForm = Form.useForm const useForm = Form.useForm
const validateDuplicateAlias = (v: string) => (tables.value || []).every((t) => t.title !== (v || '')) || 'Duplicate table alias'
const validators = computed(() => { const validators = computed(() => {
return { return {
title: [ title: [
@ -68,24 +67,34 @@ const validators = computed(() => {
}) })
const { validateInfos } = useForm(table, validators) const { validateInfos } = useForm(table, validators)
const systemColumnsCheckboxInfo = SYSTEM_COLUMNS.map((c, index) => ({
value: c,
disabled: index === 0,
}))
onMounted(() => { onMounted(() => {
generateUniqueTitle() generateUniqueTitle()
inputEl.value?.focus() inputEl.value?.focus()
}) })
</script> </script>
<template> <template>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" :mask-closable="false" @keydown.esc="dialogShow = false"> <a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" centered @keydown.esc="dialogShow = false">
<template #footer> <template #footer>
<a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button> <a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>
<a-button key="submit" size="large" type="primary" @click="createTable()">{{ $t('general.submit') }}</a-button> <a-button key="submit" size="large" type="primary" @click="createTable()">{{ $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="table" name="create-new-table-form" @keydown.enter="createTable"> <a-form :model="table" name="create-new-table-form" @keydown.enter="createTable">
<!-- Create A New Table --> <!-- Create A New Table -->
<div class="prose-xl font-bold self-center my-4">{{ $t('activity.createTable') }}</div> <div class="prose-xl font-bold self-center my-4">{{ $t('activity.createTable') }}</div>
<!-- hint="Enter table name" --> <!-- hint="Enter table name" -->
<div class="mb-2">Table Name</div> <div class="mb-2">Table Name</div>
<a-form-item v-bind="validateInfos.title"> <a-form-item v-bind="validateInfos.title">
<a-input <a-input
ref="inputEl" ref="inputEl"
@ -95,43 +104,47 @@ onMounted(() => {
:placeholder="$t('msg.info.enterTableName')" :placeholder="$t('msg.info.enterTableName')"
/> />
</a-form-item> </a-form-item>
<div class="flex justify-end">
<div class="pointer" @click="isAdvanceOptVisible = !isAdvanceOptVisible"> <div class="flex justify-end items-center">
<div class="pointer flex flex-row items-center gap-x-1" @click="isAdvanceOptVisible = !isAdvanceOptVisible">
{{ isAdvanceOptVisible ? 'Hide' : 'Show' }} more {{ isAdvanceOptVisible ? 'Hide' : 'Show' }} more
<v-icon x-small color="grey">
{{ isAdvanceOptVisible ? 'mdi-minus-circle-outline' : 'mdi-plus-circle-outline' }} <MdiMinusCircleOutline v-if="isAdvanceOptVisible" class="text-gray-500" />
</v-icon> <MdiPlusCircleOutline v-else class="text-gray-500" />
</div> </div>
</div> </div>
<div class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }"> <div class="nc-table-advanced-options" :class="{ active: isAdvanceOptVisible }">
<!-- hint="Table name as saved in database" --> <!-- hint="Table name as saved in database" -->
<div v-if="!project.prefix" class="mb-2">{{ $t('msg.info.tableNameInDb') }}</div> <div v-if="!project.prefix" class="mb-2">{{ $t('msg.info.tableNameInDb') }}</div>
<a-form-item v-if="!project.prefix" v-bind="validateInfos.table_name"> <a-form-item v-if="!project.prefix" v-bind="validateInfos.table_name">
<a-input v-model:value="table.table_name" size="large" hide-details :placeholder="$t('msg.info.tableNameInDb')" /> <a-input v-model:value="table.table_name" size="large" hide-details :placeholder="$t('msg.info.tableNameInDb')" />
</a-form-item> </a-form-item>
<div> <div>
<div class="mb-5"> <div class="mb-1">
<!-- Add Default Columns --> <!-- Add Default Columns -->
{{ $t('msg.info.addDefaultColumns') }} {{ $t('msg.info.addDefaultColumns') }}
</div> </div>
<a-row> <a-row>
<a-col :span="6"> <a-checkbox-group
<a-tooltip placement="top"> v-model:value="table.columns"
<template #title> :options="systemColumnsCheckboxInfo"
<span>ID column is required, you can rename this later if required.</span> class="!flex flex-row justify-between w-full"
</template> >
<a-checkbox v-model:checked="table.columns.id" disabled>ID</a-checkbox> <template #label="{ value }">
</a-tooltip> <a-tooltip v-if="value === 'id'" placement="top" class="!flex">
</a-col> <template #title>
<a-col :span="6"> <span>ID column is required, you can rename this later if required.</span>
<a-checkbox v-model:checked="table.columns.title"> title </a-checkbox> </template>
</a-col> ID
<a-col :span="6"> </a-tooltip>
<a-checkbox v-model:checked="table.columns.created_at"> created_at </a-checkbox> <div v-else class="flex">
</a-col> {{ value }}
<a-col :span="6"> </div>
<a-checkbox v-model:checked="table.columns.updated_at"> updated_at </a-checkbox> </template>
</a-col> </a-checkbox-group>
</a-row> </a-row>
</div> </div>
</div> </div>
@ -141,23 +154,6 @@ onMounted(() => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
::v-deep {
.v-text-field__details {
padding: 0 2px !important;
.v-messages:not(.error--text) {
.v-messages__message {
color: grey;
font-size: 0.65rem;
}
}
}
}
.add-default-title {
font-size: 0.65rem;
}
.nc-table-advanced-options { .nc-table-advanced-options {
max-height: 0; max-height: 0;
transition: 0.3s max-height; transition: 0.3s max-height;

3
packages/nc-gui-v2/components/general/ColorPicker.vue

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Chrome } from '@ckpack/vue-color' import { Chrome } from '@ckpack/vue-color'
import { enumColor } from '@/utils' import { computed, enumColor, ref, watch } from '#imports'
import { computed, ref, watch } from '#imports'
interface Props { interface Props {
modelValue?: string | any modelValue?: string | any

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

@ -0,0 +1,84 @@
<script lang="ts" setup>
import { useGlobal, useProject } from '#imports'
const showDrawer = ref(false)
const { appInfo } = useGlobal()
const { project } = useProject()
const route = useRoute()
const openSwaggerLink = () => {
openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.value.ncSiteUrl)
}
</script>
<template>
<div>
<div @click="showDrawer = true">
<div class="flex items-center space-x-1">
<MdiCommentTextOutline class="mr-1 nc-share-base" />
<!-- todo: i18n -->
<div>APIs & Support</div>
</div>
</div>
<a-drawer
v-model:visible="showDrawer"
class="h-full relative"
placement="right"
size="small"
:closable="false"
:body-style="{ padding: '12px 24px 0 24px', background: '#fafafa' }"
>
<div class="flex flex-col w-full h-full p-4 pb-0">
<!-- todo: i18n -->
<a-typography-title :level="4" class="!mb-6 !text-gray-500">Help center</a-typography-title>
<GeneralSocialCard show-swagger-link class="!w-full nc-social-card">
<template #before>
<a-list-item v-if="project">
<nuxt-link
v-t="['e:docs']"
class="text-primary !no-underline !text-current py-4 font-weight-medium"
target="_blank"
@click="openSwaggerLink"
>
<div class="ml-3 flex items-center text-sm">
<LogosSwagger />
<!-- todo: i18n -->
<span class="ml-3">{{ project.title }} : Swagger Documentation</span>
</div>
</nuxt-link>
</a-list-item>
</template>
</GeneralSocialCard>
<div class="flex-1 my-2"></div>
<GeneralSponsors class="!w-full" />
<div class="min-h-10 w-full" />
</div>
</a-drawer>
</div>
</template>
<style scoped lang="scss">
/* Social card style */
.nc-social-card {
@apply !shadow-none !border-0 bg-transparent;
:deep(.ant-spin-container) {
@apply !gap-3;
.ant-list-item {
@apply mb-2 border-1 bg-white border-gray-200;
&:last-child {
@apply !border-solid;
}
}
}
}
</style>

73
packages/nc-gui-v2/components/general/Language.vue

@ -1,73 +0,0 @@
<script lang="ts" setup>
import { Language } from '~/lib'
import { onMounted, useGlobal, useI18n, useNuxtApp } from '#imports'
const { $e } = useNuxtApp()
const { lang: currentLang } = useGlobal()
const { availableLocales = ['en'], locale } = useI18n()
const languages = $computed(() => availableLocales.sort())
const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
function applyDirection() {
const targetDirection = isRtlLang ? 'rtl' : 'ltr'
const oppositeDirection = targetDirection === 'ltr' ? 'rtl' : 'ltr'
document.body.classList.remove(oppositeDirection)
document.body.classList.add(targetDirection)
document.body.style.direction = targetDirection
}
function changeLanguage(lang: string) {
currentLang.value = lang
locale.value = lang
applyDirection()
$e('c:navbar:lang', { lang })
}
onMounted(() => {
applyDirection()
})
</script>
<template>
<a-dropdown class="select-none" :trigger="['click']">
<MaterialSymbolsTranslate v-bind="$attrs" class="md:text-xl cursor-pointer nc-menu-translate" />
<template #overlay>
<a-menu class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0 dark:(!bg-gray-800 !text-white)">
<a-menu-item
v-for="lang of languages"
:key="lang"
:class="lang === locale ? '!bg-primary/10 text-primary dark:(!bg-gray-700 !text-secondary)' : ''"
class="!min-h-8 group"
:value="lang"
@click="changeLanguage(lang)"
>
<div
:class="lang === locale ? '!font-semibold !text-primary' : ''"
class="capitalize md:(!leading-8) group-hover:(text-primary font-semibold) dark:(group-hover:text-secondary)"
>
{{ Language[lang] || lang }}
</div>
</a-menu-item>
<a-menu-item>
<a
href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members"
target="_blank"
class="caption py-2 text-primary underline hover:opacity-75"
>
{{ $t('activity.translate') }}
</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>

22
packages/nc-gui-v2/components/general/MiniSidebar.vue

@ -34,7 +34,7 @@ const logout = () => {
</div> </div>
<template v-if="signedIn" #overlay> <template v-if="signedIn" #overlay>
<a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded"> <a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded nc-menu-account">
<a-menu-item-group title="User Settings"> <a-menu-item-group title="User Settings">
<a-menu-item key="email" class="!rounded-t"> <a-menu-item key="email" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user"> <nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
@ -48,7 +48,7 @@ const logout = () => {
<a-menu-item key="signout" class="!rounded-b"> <a-menu-item key="signout" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout"> <div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp; <MdiLogout class="group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout"> <span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }} {{ $t('general.signOut') }}
</span> </span>
@ -74,24 +74,24 @@ const logout = () => {
</span> </span>
</template> </template>
<a-menu-item class="active:(ring ring-pink-500)"> <a-menu-item class="active:(ring ring-accent)">
<div <div
v-t="['c:project:create:xcdb']" v-t="['c:project:create:xcdb']"
class="group flex items-center gap-2 py-2 hover:text-primary" class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create')" @click="navigateTo('/project/create')"
> >
<MdiPlus class="text-lg group-hover:text-pink-500" /> <MdiPlus class="text-lg group-hover:text-accent" />
{{ $t('activity.createProject') }} {{ $t('activity.createProject') }}
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item class="rounded-b active:(ring ring-pink-500)"> <a-menu-item class="rounded-b active:(ring ring-accent)">
<div <div
v-t="['c:project:create:extdb']" v-t="['c:project:create:extdb']"
class="group flex items-center gap-2 py-2 hover:text-primary" class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create-external')" @click="navigateTo('/project/create-external')"
> >
<MdiDatabaseOutline class="text-lg group-hover:text-pink-500" /> <MdiDatabaseOutline class="text-lg group-hover:text-accent" />
<div v-html="$t('activity.createProjectExtended.extDB')" /> <div v-html="$t('activity.createProjectExtended.extDB')" />
</div> </div>
</a-menu-item> </a-menu-item>
@ -125,16 +125,8 @@ const logout = () => {
@apply flex w-full justify-center items-center h-12 group p-2; @apply flex w-full justify-center items-center h-12 group p-2;
&.active { &.active {
@apply bg-pink-500 border-t-1 border-b-1; @apply bg-accent border-t-1 border-b-1;
} }
} }
} }
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style> </style>

17
packages/nc-gui-v2/components/general/PreviewAs.vue

@ -58,7 +58,8 @@ watch(previewAs, () => window.location.reload())
<div class="divider" /> <div class="divider" />
<div class="pointer flex items-center gap-4"> <div class="pointer flex items-center gap-4">
<span>Preview as :</span> <!-- Preview as -->
<span>{{ $t('activity.previewAs') }}</span>
<a-radio-group v-model:value="previewAs" name="radioGroup"> <a-radio-group v-model:value="previewAs" name="radioGroup">
<a-radio v-for="role of roleList" :key="role.title" class="capitalize !text-white" :value="role.title" <a-radio v-for="role of roleList" :key="role.title" class="capitalize !text-white" :value="role.title"
@ -77,11 +78,13 @@ watch(previewAs, () => window.location.reload())
<template v-else> <template v-else>
<template v-for="role of roleList" :key="role.title"> <template v-for="role of roleList" :key="role.title">
<a-menu-item :class="`pointer nc-preview-${role.title}`" @click="previewAs = role.title"> <a-menu-item @click="previewAs = role.title">
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<component :is="roleIcon[role.title]" class="group-hover:text-pink-500" /> <component :is="roleIcon[role.title]" class="group-hover:text-accent" />
<span class="capitalize" :class="{ 'x-active--text': role.title === previewAs }">{{ role.title }}</span> <span class="capitalize" :class="{ 'x-active--text': role.title === previewAs }">
{{ role.title }}
</span>
</div> </div>
</a-menu-item> </a-menu-item>
</template> </template>
@ -89,9 +92,11 @@ watch(previewAs, () => window.location.reload())
<template v-if="previewAs"> <template v-if="previewAs">
<a-menu-item @click="previewAs = null"> <a-menu-item @click="previewAs = null">
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<MdiClose class="group-hover:text-pink-500" /> <MdiClose class="group-hover:text-accent" />
<!-- Reset Preview --> <!-- Reset Preview -->
<span class="text-capitalize text-xs whitespace-nowrap">{{ $t('activity.resetReview') }}</span> <span class="text-capitalize text-xs whitespace-nowrap">
{{ $t('activity.resetReview') }}
</span>
</div> </div>
</a-menu-item> </a-menu-item>
</template> </template>

4
packages/nc-gui-v2/components/general/ReleaseInfo.vue

@ -36,7 +36,7 @@ onMounted(async () => await fetchReleaseInfo())
<div v-if="releaseAlert" class="flex items-center"> <div v-if="releaseAlert" class="flex items-center">
<a-dropdown :trigger="['click']" placement="bottom"> <a-dropdown :trigger="['click']" placement="bottom">
<a-button class="bg-primary border-none"> <a-button class="bg-primary border-none">
<div class="flex gap-1 align-center text-white"> <div class="flex gap-1 items-center text-white">
<span class="text-sm font-weight-medium">{{ $t('activity.upgrade.available') }}</span> <span class="text-sm font-weight-medium">{{ $t('activity.upgrade.available') }}</span>
<mdi-menu-down /> <mdi-menu-down />
</div> </div>
@ -56,7 +56,7 @@ onMounted(async () => await fetchReleaseInfo())
{{ $t('activity.upgrade.howTo') }} {{ $t('activity.upgrade.howTo') }}
</div> </div>
</nuxt-link> </nuxt-link>
<a-divider class="ma-0" /> <a-divider class="m-0" />
<div class="nc-menu-item" @click="latestRelease = null"> <div class="nc-menu-item" @click="latestRelease = null">
<mdi-close /> <mdi-close />
<!-- Hide menu --> <!-- Hide menu -->

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

@ -9,8 +9,8 @@ const { isUIAllowed } = useUIPermission()
</script> </script>
<template> <template>
<div class="flex items-center mr-4"> <div class="flex items-center">
<a-button <div
v-if=" v-if="
isUIAllowed('newUser') && isUIAllowed('newUser') &&
route.name !== 'index' && route.name !== 'index' &&
@ -18,16 +18,14 @@ const { isUIAllowed } = useUIPermission()
route.name !== 'project-index-create-external' && route.name !== 'project-index-create-external' &&
route.name !== 'index-user-index' route.name !== 'index-user-index'
" "
size="middle"
type="primary"
class="!bg-white !text-primary rounded"
@click="showUserModal = true" @click="showUserModal = true"
> >
<div class="flex items-center space-x-1"> <div class="flex items-center space-x-1">
<mdi-account-supervisor-outline class="mr-1" /> <MdiAccountPlusOutline class="mr-1 nc-share-base" />
<div>{{ $t('activity.share') }}</div> <!-- todo: i18n <div>{{ $t('activity.share') }}</div> -->
<div>{{ $t('activity.inviteTeam') }}</div>
</div> </div>
</a-button> </div>
<TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" /> <TabsAuthUserManagementUsersModal :key="showUserModal" :show="showUserModal" @closed="showUserModal = false" />
</div> </div>
</template> </template>

57
packages/nc-gui-v2/components/general/SocialCard.vue

@ -7,10 +7,40 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</script> </script>
<template> <template>
<a-card :body-style="{ padding: '0' }" class="w-[300px] shadow-sm rounded-lg"> <a-card :body-style="{ padding: '0px' }" class="w-[300px] shadow-sm !rounded-lg">
<a-list class="w-full" dense> <a-list class="w-full" dense>
<slot name="before" />
<a-list-item>
<nuxt-link
v-t="['e:docs']"
class="text-primary !no-underline !text-current"
target="_blank"
to="https://docs.nocodb.com/"
>
<div class="ml-3 flex items-center text-sm">
<MdiBookOpenOutline class="text-lg text-accent" />
<!-- todo: i18n -->
<span class="ml-3">Documentation</span>
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link
v-t="['e:docs']"
class="text-primary !no-underline !text-current"
target="_blank"
to="https://apis.nocodb.com/"
>
<div class="ml-3 flex items-center text-sm">
<MdiJson class="text-lg text-green-500" />
<!-- todo: i18n -->
<span class="ml-3">API Documentation</span>
</div>
</nuxt-link>
</a-list-item>
<a-list-item> <a-list-item>
<nuxt-link class="text-primary" to="https://github.com/nocodb/nocodb" target="_blank"> <nuxt-link class="text-primary !no-underline !text-current" to="https://github.com/nocodb/nocodb" target="_blank">
<div class="flex items-center text-sm"> <div class="flex items-center text-sm">
<mdi-github class="mx-3 text-lg" /> <mdi-github class="mx-3 text-lg" />
<div v-if="isRtlLang"> <div v-if="isRtlLang">
@ -23,7 +53,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
<div v-else class="flex items-center"> <div v-else class="flex items-center">
<!-- Star --> <!-- Star -->
{{ $t('labels.community.starUs1') }} {{ $t('labels.community.starUs1') }}
<mdi-star-outline class="mx-1" /> {{ ' ' }}
<!-- us on Github --> <!-- us on Github -->
{{ $t('labels.community.starUs2') }} {{ $t('labels.community.starUs2') }}
</div> </div>
@ -31,7 +61,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link class="text-primary" to="https://calendly.com/nocodb-meeting" target="_blank"> <nuxt-link class="!no-underline !text-current" to="https://calendly.com/nocodb-meeting" target="_blank">
<div class="flex items-center text-sm"> <div class="flex items-center text-sm">
<mdi-calendar-month class="mx-3 text-lg" :color="colors.dark[3 % colors.dark.length]" /> <mdi-calendar-month class="mx-3 text-lg" :color="colors.dark[3 % colors.dark.length]" />
<!-- Book a Free DEMO --> <!-- Book a Free DEMO -->
@ -42,7 +72,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link class="text-primary" to="https://discord.gg/5RgZmkW" target="_blank"> <nuxt-link class="!no-underline !text-current" to="https://discord.gg/5RgZmkW" target="_blank">
<div class="flex items-center text-sm"> <div class="flex items-center text-sm">
<mdi-discord class="mx-3 text-lg" :color="colors.dark[0 % colors.dark.length]" /> <mdi-discord class="mx-3 text-lg" :color="colors.dark[0 % colors.dark.length]" />
<!-- Get your questions answered --> <!-- Get your questions answered -->
@ -53,7 +83,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item> <a-list-item>
<nuxt-link class="text-primary" to="https://twitter.com/NocoDB" target="_blank"> <nuxt-link class="!no-underline !text-current" to="https://twitter.com/NocoDB" target="_blank">
<div class="flex items-center text-sm"> <div class="flex items-center text-sm">
<mdi-twitter class="mx-3 text-lg" :color="colors.dark[1 % colors.dark.length]" /> <mdi-twitter class="mx-3 text-lg" :color="colors.dark[1 % colors.dark.length]" />
<!-- Follow NocoDB --> <!-- Follow NocoDB -->
@ -63,10 +93,19 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>
<a-list-item v-t="['e:hiring']"> <a-list-item>
<nuxt-link class="text-primary" target="_blank" to="http://careers.nocodb.com"> <nuxt-link v-t="['e:hiring']" 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">
<div class="ml-3">🚀 <span class="ml-2">We are Hiring!!!</span></div> <!-- todo: i18n -->
<div class="ml-3">🚀 <span class="ml-3">We are Hiring!!!</span></div>
</div>
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link v-t="['e:reddit']" class="!no-underline !text-current" target="_blank" to="https://www.reddit.com/r/NocoDB/">
<div class="ml-3 flex items-center text-sm">
<LogosRedditIcon />
<span class="ml-4">/r/NocoDB/</span>
</div> </div>
</nuxt-link> </nuxt-link>
</a-list-item> </a-list-item>

4
packages/nc-gui-v2/components/general/Sponsors.vue

@ -7,9 +7,9 @@ const { nav = false } = defineProps<Props>()
</script> </script>
<template> <template>
<a-card class="w-[300px] shadow-sm rounded-lg"> <a-card class="w-[300px] shadow-sm !rounded-lg">
<template #cover> <template #cover>
<img class="max-h-[180px] rounded-t-lg" alt="cover" src="/ants-leaf-cutter.jpeg" /> <img class="max-h-[180px] !rounded-t-lg" alt="cover" src="/ants-leaf-cutter.jpeg" />
</template> </template>
<a-card-meta> <a-card-meta>

61
packages/nc-gui-v2/components/general/language/Menu.vue

@ -0,0 +1,61 @@
<script lang="ts" setup>
import { Language } from '~/lib'
import { onMounted, useGlobal, useI18n, useNuxtApp } from '#imports'
const { $e } = useNuxtApp()
const { lang: currentLang } = useGlobal()
const { availableLocales = ['en'], locale } = useI18n()
const languages = $computed(() => availableLocales.sort())
const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
function applyDirection() {
const targetDirection = isRtlLang ? 'rtl' : 'ltr'
const oppositeDirection = targetDirection === 'ltr' ? 'rtl' : 'ltr'
document.body.classList.remove(oppositeDirection)
document.body.classList.add(targetDirection)
document.body.style.direction = targetDirection
}
function changeLanguage(lang: string) {
currentLang.value = lang
locale.value = lang
applyDirection()
$e('c:navbar:lang', { lang })
}
onMounted(() => {
applyDirection()
})
</script>
<template>
<a-menu-item
v-for="lang of languages"
:key="lang"
:class="lang === locale ? '!bg-primary bg-opacity-10 text-primary' : ''"
class="group"
:value="lang"
@click="changeLanguage(lang)"
>
<div :class="lang === locale ? '!font-semibold !text-primary' : ''" class="nc-project-menu-item capitalize">
{{ Language[lang] || lang }}
</div>
</a-menu-item>
<a-menu-item class="mt-1">
<a
href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members"
target="_blank"
class="caption py-2 text-primary underline hover:opacity-75"
>
{{ $t('activity.translate') }}
</a>
</a-menu-item>
</template>

11
packages/nc-gui-v2/components/general/language/index.vue

@ -0,0 +1,11 @@
<template>
<a-dropdown class="select-none color-transition" :trigger="['click']">
<MaterialSymbolsTranslate v-bind="$attrs" class="md:text-xl cursor-pointer nc-menu-translate" />
<template #overlay>
<a-menu class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0 rounded">
<GeneralLanguageMenu />
</a-menu>
</template>
</a-dropdown>
</template>

14
packages/nc-gui-v2/components/shared-view/Form.vue

@ -34,7 +34,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
</script> </script>
<template> <template>
<div class="bg-primary/100 !h-[100vh] overflow-auto w-100 flex flex-col"> <div class="bg-primary !h-[100vh] overflow-auto w-full flex flex-col">
<div> <div>
<img src="~/assets/img/icons/512x512-trans.png" width="30" class="mx-4 mt-2" /> <img src="~/assets/img/icons/512x512-trans.png" width="30" class="mx-4 mt-2" />
</div> </div>
@ -59,21 +59,21 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
<a-row class="justify-center"> <a-row class="justify-center">
<a-col :md="20"> <a-col :md="20">
<div> <div>
<div class="h-full ma-0 rounded-b-0"> <div class="h-full m-0 rounded-b-0">
<div <div
class="nc-form-wrapper pb-10 rounded shadow-xl" class="nc-form-wrapper pb-10 rounded shadow-xl"
style="background: linear-gradient(180deg, #dbdbdb 0, #dbdbdb 200px, white 200px)" style="background: linear-gradient(180deg, #dbdbdb 0, #dbdbdb 200px, white 200px)"
> >
<div class="mt-10 flex items-center justify-center flex-col"> <div class="mt-10 flex items-center justify-center flex-col">
<div class="nc-form-banner backgroundColor darken-1 flex-column justify-center d-flex"> <div class="nc-form-banner backgroundColor darken-1 flex-col justify-center flex">
<div class="flex items-center justify-center grow h-[100px]"> <div class="flex items-center justify-center flex-1 h-[100px]">
<img src="~/assets/img/icon.png" width="50" class="mx-4" /> <img src="~/assets/img/icon.png" width="50" class="mx-4" />
<span class="text-4xl font-weight-bold">NocoDB</span> <span class="text-4xl font-weight-bold">NocoDB</span>
</div> </div>
</div> </div>
</div> </div>
<div class="mx-auto nc-form bg-white shadow-lg pa-2 mb-10 max-w-[600px] mx-auto rounded"> <div class="mx-auto nc-form bg-white shadow-lg p-2 mb-10 max-w-[600px] mx-auto rounded">
<h2 class="mt-4 text-4xl font-weight-bold text-left mx-4 mb-3 px-1"> <h2 class="mt-4 text-4xl font-weight-bold text-left mx-4 mb-3 px-1">
{{ sharedFormView.heading }} {{ sharedFormView.heading }}
</h2> </h2>
@ -81,7 +81,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
<div class="text-lg text-left mx-4 py-2 px-1 text-gray-500"> <div class="text-lg text-left mx-4 py-2 px-1 text-gray-500">
{{ sharedFormView.subheading }} {{ sharedFormView.subheading }}
</div> </div>
<div class="h-100"> <div class="h-full">
<div v-for="(field, index) in formColumns" :key="index" class="flex flex-col mt-4 px-4 space-y-2"> <div v-for="(field, index) in formColumns" :key="index" class="flex flex-col mt-4 px-4 space-y-2">
<div class="flex"> <div class="flex">
<SmartsheetHeaderVirtualCell <SmartsheetHeaderVirtualCell
@ -159,7 +159,7 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
<style scoped lang="scss"> <style scoped lang="scss">
.nc-input { .nc-input {
@apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex align-center border-solid border-1 border-primary; @apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex items-center border-solid border-1 border-primary;
} }
.nc-form-wrapper { .nc-form-wrapper {

4
packages/nc-gui-v2/components/shared-view/Grid.vue

@ -4,7 +4,7 @@ import type { TableType } from 'nocodb-sdk'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '~/context' import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '~/context'
const { sharedView, meta } = useSharedView() const { sharedView, meta, sorts, nestedFilters } = useSharedView()
const reloadEventHook = createEventHook<void>() const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook) provide(ReloadViewDataHookInj, reloadEventHook)
@ -14,7 +14,7 @@ provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value.columns as any[])) provide(FieldsInj, ref(meta.value.columns as any[]))
provide(IsPublicInj, ref(true)) provide(IsPublicInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta) useProvideSmartsheetStore(sharedView as Ref<TableType>, meta, true, sorts, nestedFilters)
</script> </script>
<template> <template>

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

@ -99,7 +99,12 @@ useEventListener(document, 'click', handleClose)
</script> </script>
<template> <template>
<div ref="editOrAddRef" class="min-w-[400px] max-h-[95vh] bg-gray-50 shadow-lg p-6 overflow-auto !border" @click.stop> <div
ref="editOrAddRef"
class="w-[400px] max-h-[95vh] bg-gray-50 shadow-lg p-6 overflow-auto !border"
:class="{ '!w-[600px]': formState.uidt === UITypes.Formula }"
@click.stop
>
<a-form v-if="formState" v-model="formState" name="column-create-or-edit" layout="vertical"> <a-form v-if="formState" v-model="formState" 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">
@ -112,7 +117,7 @@ useEventListener(document, 'click', handleClose)
> >
<a-select v-model:value="formState.uidt" show-search class="nc-column-type-input" @change="onUidtOrIdTypeChange"> <a-select v-model:value="formState.uidt" show-search class="nc-column-type-input" @change="onUidtOrIdTypeChange">
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt"> <a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="flex gap-1 align-center"> <div class="flex gap-1 items-center">
<component :is="opt.icon" class="text-grey" /> <component :is="opt.icon" class="text-grey" />
{{ opt.name }} {{ opt.name }}
</div> </div>
@ -139,7 +144,7 @@ useEventListener(document, 'click', handleClose)
</div> </div>
<div <div
v-if="!isVirtualCol(formState.uidt)" v-if="!isVirtualCol(formState.uidt)"
class="text-xs cursor-pointer text-grey nc-more-options mb-1 mt-4 flex align-center gap-1 justify-end" class="text-xs cursor-pointer text-grey nc-more-options mb-1 mt-4 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions" @click="advancedOptions = !advancedOptions"
> >
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }} {{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}
@ -192,7 +197,7 @@ useEventListener(document, 'click', handleClose)
} }
:deep(.ant-select-selection-item) { :deep(.ant-select-selection-item) {
@apply flex align-center; @apply flex items-center;
} }
:deep(.ant-form-item-explain-error) { :deep(.ant-form-item-explain-error) {

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

@ -68,7 +68,7 @@ const refTables = $computed(() => {
</div> </div>
<div <div
class="text-xs cursor-pointer text-grey nc-more-options my-2 flex align-center gap-1 justify-end" class="text-xs cursor-pointer text-grey nc-more-options my-2 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions" @click="advancedOptions = !advancedOptions"
> >
{{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }} {{ advancedOptions ? $t('general.hideAll') : $t('general.showMore') }}

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

@ -95,7 +95,7 @@ watch(inputs, () => {
<div class="w-full"> <div class="w-full">
<Draggable :list="options" item-key="id" handle=".nc-child-draggable-icon"> <Draggable :list="options" item-key="id" handle=".nc-child-draggable-icon">
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="flex py-1 align-center"> <div class="flex py-1 items-center">
<MdiDragIcon small class="nc-child-draggable-icon handle" /> <MdiDragIcon small class="nc-child-draggable-icon handle" />
<a-dropdown v-model:visible="colorMenus[index]" :trigger="['click']"> <a-dropdown v-model:visible="colorMenus[index]" :trigger="['click']">
<template #overlay> <template #overlay>
@ -109,7 +109,7 @@ watch(inputs, () => {
</template> </template>
<template #footer> <template #footer>
<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 align-center"><MdiPlusIcon /><span class="flex-auto">Add option</span></div> <div class="flex items-center"><MdiPlusIcon /><span class="flex-auto">Add option</span></div>
</a-button> </a-button>
</template> </template>
</Draggable> </Draggable>

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

@ -1,13 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Modal, message } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
import { inject } from 'vue' import { ColumnInj, IsLockedInj, MetaInj, extractSdkResponseErrorMsg, inject, useI18n, useMetas, useNuxtApp } from '#imports'
import { useI18n } from 'vue-i18n'
import { useNuxtApp } from '#app'
import { ColumnInj, IsLockedInj, MetaInj, extractSdkResponseErrorMsg, useMetas } from '#imports'
import MdiEditIcon from '~icons/mdi/pencil'
import MdiStarIcon from '~icons/mdi/star'
import MdiDeleteIcon from '~icons/mdi/delete-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down'
const { virtual = false } = defineProps<{ virtual?: boolean }>() const { virtual = false } = defineProps<{ virtual?: boolean }>()
@ -34,8 +27,9 @@ const deleteColumn = () =>
async onOk() { async onOk() {
try { try {
await $api.dbTableColumn.delete(column?.value?.id as string) await $api.dbTableColumn.delete(column?.value?.id as string)
getMeta(meta?.value?.id as string, true)
} catch (e) { await getMeta(meta?.value?.id as string, true)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
}, },
@ -44,8 +38,11 @@ const deleteColumn = () =>
const setAsPrimaryValue = async () => { const setAsPrimaryValue = async () => {
try { try {
await $api.dbTableColumn.primaryColumnSet(column?.value?.id as string) await $api.dbTableColumn.primaryColumnSet(column?.value?.id as string)
getMeta(meta?.value?.id as string, true)
await getMeta(meta?.value?.id as string, true)
message.success('Successfully updated as primary column') message.success('Successfully updated as primary column')
$e('a:column:set-primary') $e('a:column:set-primary')
} catch (e) { } catch (e) {
message.error('Failed to update primary column') message.error('Failed to update primary column')
@ -55,29 +52,31 @@ const setAsPrimaryValue = async () => {
<template> <template>
<a-dropdown v-if="!isLocked" placement="bottomRight" :trigger="['click']"> <a-dropdown v-if="!isLocked" placement="bottomRight" :trigger="['click']">
<MdiMenuDownIcon class="h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0" /> <MdiMenuDown class="h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0" />
<template #overlay> <template #overlay>
<a-menu class="shadow bg-white"> <a-menu class="shadow bg-white">
<a-menu-item @click="emit('edit')"> <a-menu-item @click="emit('edit')">
<div class="nc-column-edit nc-header-menu-item"> <div class="nc-column-edit nc-header-menu-item">
<MdiEditIcon class="text-primary" /> <MdiPencil class="text-primary" />
<!-- Edit --> <!-- Edit -->
{{ $t('general.edit') }} {{ $t('general.edit') }}
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="!virtual" @click="setAsPrimaryValue"> <a-menu-item v-if="!virtual" @click="setAsPrimaryValue">
<div class="nc-column-set-primary nc-header-menu-item"> <div class="nc-column-set-primary nc-header-menu-item">
<MdiStarIcon class="text-primary" /> <MdiStar class="text-primary" />
<!-- todo : tooltip --> <!-- todo : tooltip -->
<!-- Set as Primary value --> <!-- Set as Primary value -->
{{ $t('activity.setPrimary') }} {{ $t('activity.setPrimary') }}
</div> </div>
<!-- <span class="caption font-weight-bold">Primary value will be shown in place of primary key</span> -->
</a-menu-item> </a-menu-item>
<a-menu-item @click="deleteColumn"> <a-menu-item @click="deleteColumn">
<div class="nc-column-delete nc-header-menu-item"> <div class="nc-column-delete nc-header-menu-item">
<MdiDeleteIcon class="text-error" /> <MdiDeleteOutline class="text-error" />
<!-- Delete --> <!-- Delete -->
{{ $t('general.delete') }} {{ $t('general.delete') }}
</div> </div>

6
packages/nc-gui-v2/components/smartsheet-header/VirtualCellIcon.vue

@ -38,7 +38,7 @@ const icon = computed(() => {
case UITypes.LinkToAnotherRecord: case UITypes.LinkToAnotherRecord:
switch ((column?.value?.colOptions as LinkToAnotherRecordType)?.type) { switch ((column?.value?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY: case RelationTypes.MANY_TO_MANY:
return { icon: MMIcon, color: 'text-pink-500' } return { icon: MMIcon, color: 'text-accent' }
case RelationTypes.HAS_MANY: case RelationTypes.HAS_MANY:
return { icon: HMIcon, color: 'text-yellow-500' } return { icon: HMIcon, color: 'text-yellow-500' }
case RelationTypes.BELONGS_TO: case RelationTypes.BELONGS_TO:
@ -52,7 +52,7 @@ const icon = computed(() => {
case UITypes.Lookup: case UITypes.Lookup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) { switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY: case RelationTypes.MANY_TO_MANY:
return { icon: TableColumnPlusBefore, color: 'text-pink-500' } return { icon: TableColumnPlusBefore, color: 'text-accent' }
case RelationTypes.HAS_MANY: case RelationTypes.HAS_MANY:
return { icon: TableColumnPlusBefore, color: 'text-yellow-500' } return { icon: TableColumnPlusBefore, color: 'text-yellow-500' }
case RelationTypes.BELONGS_TO: case RelationTypes.BELONGS_TO:
@ -62,7 +62,7 @@ const icon = computed(() => {
case UITypes.Rollup: case UITypes.Rollup:
switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) { switch ((relationColumn?.colOptions as LinkToAnotherRecordType)?.type) {
case RelationTypes.MANY_TO_MANY: case RelationTypes.MANY_TO_MANY:
return { icon: RollupIcon, color: 'text-pink-500' } return { icon: RollupIcon, color: 'text-accent' }
case RelationTypes.HAS_MANY: case RelationTypes.HAS_MANY:
return { icon: RollupIcon, color: 'text-yellow-500' } return { icon: RollupIcon, color: 'text-yellow-500' }
case RelationTypes.BELONGS_TO: case RelationTypes.BELONGS_TO:

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

@ -0,0 +1,23 @@
<script setup lang="ts">
import { OpenNewRecordFormHookInj, inject } from '#imports'
const isLocked = inject(IsLockedInj)
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj)!
const onClick = () => {
if (!isLocked?.value) openNewRecordFormHook.trigger()
}
</script>
<template>
<a-tooltip placement="bottom">
<template #title> {{ $t('activity.addRow') }} </template>
<div :class="{ 'group': !isLocked, 'disabled-ring': isLocked }" class="nc-add-row flex align-center">
<MdiPlusOutline
:class="{ 'cursor-pointer text-gray-500 group-hover:(text-primary)': !isLocked, 'disabled': isLocked }"
@click="onClick"
/>
</div>
</a-tooltip>
</template>

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

@ -41,6 +41,7 @@ const reloadDataHook = inject(ReloadViewDataHookInj)!
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { nestedFilters } = useSmartsheetStoreOrThrow()
const { filters, deleteFilter, saveOrUpdate, loadFilters, addFilter, addFilterGroup, sync } = useViewFilters( const { filters, deleteFilter, saveOrUpdate, loadFilters, addFilter, addFilterGroup, sync } = useViewFilters(
activeView, activeView,
parentId, parentId,
@ -48,10 +49,11 @@ const { filters, deleteFilter, saveOrUpdate, loadFilters, addFilter, addFilterGr
() => { () => {
reloadDataHook.trigger() reloadDataHook.trigger()
}, },
modelValue, modelValue || nestedFilters.value,
!modelValue,
) )
const nestedFilters = ref() const localNestedFilters = ref()
const filterUpdateCondition = (filter: FilterType, i: number) => { const filterUpdateCondition = (filter: FilterType, i: number) => {
saveOrUpdate(filter, i) saveOrUpdate(filter, i)
@ -115,9 +117,9 @@ watch(
const applyChanges = async (hookId?: string) => { const applyChanges = async (hookId?: string) => {
await sync(hookId) await sync(hookId)
if (!nestedFilters.value.length) return if (!localNestedFilters.value?.length) return
for (const nestedFilter of nestedFilters.value) { for (const nestedFilter of localNestedFilters.value) {
if (nestedFilter.parentId) { if (nestedFilter.parentId) {
await nestedFilter.applyChanges(hookId, true) await nestedFilter.applyChanges(hookId, true)
} }
@ -148,7 +150,7 @@ defineExpose({
/> />
<span v-else :key="`${i}dummy`" /> <span v-else :key="`${i}dummy`" />
<div :key="`${i}nested`" class="d-flex"> <div :key="`${i}nested`" class="flex">
<a-select <a-select
v-model:value="filter.logical_op" v-model:value="filter.logical_op"
:dropdown-match-select-width="false" :dropdown-match-select-width="false"
@ -166,7 +168,7 @@ defineExpose({
<div class="col-span-5"> <div class="col-span-5">
<SmartsheetToolbarColumnFilter <SmartsheetToolbarColumnFilter
v-if="filter.id || filter.children" v-if="filter.id || filter.children"
ref="nestedFilters" ref="localNestedFilters"
v-model="filter.children" v-model="filter.children"
:parent-id="filter.id" :parent-id="filter.id"
nested nested
@ -187,7 +189,7 @@ defineExpose({
<MdiCloseBox <MdiCloseBox
v-if="!filter.readOnly" v-if="!filter.readOnly"
class="nc-filter-item-remove-btn text-grey align-self-center" class="nc-filter-item-remove-btn text-grey self-center"
@click.stop="deleteFilter(filter, i)" @click.stop="deleteFilter(filter, i)"
/> />
<span v-else /> <span v-else />
@ -269,7 +271,7 @@ defineExpose({
<div class="flex gap-2 mb-2 mt-4"> <div class="flex gap-2 mb-2 mt-4">
<a-button class="elevation-0 text-capitalize" type="primary" ghost @click.stop="addFilter"> <a-button class="elevation-0 text-capitalize" type="primary" ghost @click.stop="addFilter">
<div class="flex align-center gap-1"> <div class="flex items-center gap-1">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> --> <!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<MdiPlus /> <MdiPlus />
<!-- Add Filter --> <!-- Add Filter -->
@ -277,7 +279,7 @@ defineExpose({
</div> </div>
</a-button> </a-button>
<a-button class="text-capitalize !text-gray-500" @click.stop="addFilterGroup"> <a-button class="text-capitalize !text-gray-500" @click.stop="addFilterGroup">
<div class="flex align-center gap-1"> <div class="flex items-center gap-1">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> --> <!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<MdiPlus /> <MdiPlus />
Add Filter Group Add Filter Group

38
packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue

@ -1,46 +1,50 @@
<script setup lang="ts"> <script setup lang="ts">
import { watchEffect } from '@vue/runtime-core'
import type ColumnFilter from './ColumnFilter.vue' import type ColumnFilter from './ColumnFilter.vue'
import { ActiveViewInj, IsLockedInj, IsPublicInj } from '~/context' import { ActiveViewInj, IsLockedInj, IsPublicInj, computed, inject, ref, useGlobal, useViewFilters, watchEffect } from '#imports'
import MdiFilterIcon from '~icons/mdi/filter-outline'
import MdiMenuDownIcon from '~icons/mdi/menu-down' const isLocked = inject(IsLockedInj, ref(false))
const isLocked = inject(IsLockedInj)
const activeView = inject(ActiveViewInj) const activeView = inject(ActiveViewInj)
const isPublic = inject(IsPublicInj)
const isPublic = inject(IsPublicInj, ref(false))
const { filterAutoSave } = useGlobal() const { filterAutoSave } = useGlobal()
const filterComp = ref<typeof ColumnFilter>()
const { nestedFilters } = useSmartsheetStoreOrThrow()
// todo: avoid duplicate api call by keeping a filter store // todo: avoid duplicate api call by keeping a filter store
const { filters, loadFilters } = useViewFilters( const { filters, loadFilters } = useViewFilters(
activeView, activeView,
undefined, undefined,
computed(() => false), computed(() => true),
() => false,
nestedFilters.value,
true,
) )
const filtersLength = ref(0) const filtersLength = ref(0)
watchEffect(async () => { watchEffect(async () => {
if (activeView?.value) { if (activeView?.value) {
await loadFilters() await loadFilters()
filtersLength.value = filters?.value?.length ?? 0
filtersLength.value = filters.value.length || 0
} }
}) })
const filterComp = ref<typeof ColumnFilter>()
const applyChanges = async () => { const applyChanges = async () => await filterComp.value?.applyChanges()
await filterComp?.value?.applyChanges()
}
</script> </script>
<template> <template>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<div :class="{ 'nc-badge nc-active-btn': filtersLength }"> <div :class="{ 'nc-badge nc-active-btn': filtersLength }">
<a-button v-t="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked"> <a-button v-t="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked">
<div class="flex align-center gap-1"> <div class="flex items-center gap-1">
<MdiFilterIcon /> <MdiFilterOutline />
<!-- Filter --> <!-- Filter -->
<span class="text-capitalize !text-sm font-weight-medium">{{ $t('activity.filter') }}</span> <span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.filter') }}</span>
<MdiMenuDownIcon class="text-grey" /> <MdiMenuDown class="text-grey" />
</div> </div>
</a-button> </a-button>
</div> </div>
@ -51,7 +55,7 @@ const applyChanges = async () => {
:auto-save="filterAutoSave" :auto-save="filterAutoSave"
@update:filters-length="filtersLength = $event" @update:filters-length="filtersLength = $event"
> >
<div v-if="!isPublic" class="d-flex align-end mt-2 min-h-[30px]" @click.stop> <div v-if="!isPublic" class="flex items-end mt-2 min-h-[30px]" @click.stop>
<a-checkbox id="col-filter-checkbox" v-model:checked="filterAutoSave" class="col-filter-checkbox" hide-details dense> <a-checkbox id="col-filter-checkbox" v-model:checked="filterAutoSave" class="col-filter-checkbox" hide-details dense>
<span class="text-grey text-xs"> <span class="text-grey text-xs">
{{ $t('msg.info.filterAutoApply') }} {{ $t('msg.info.filterAutoApply') }}

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

@ -86,7 +86,7 @@ const filterOption = (input: string, option: any) => {
:filter-option="filterOption" :filter-option="filterOption"
> >
<a-select-option v-for="option in options" :key="option.value" :value="option.value"> <a-select-option v-for="option in options" :key="option.value" :value="option.value">
<div class="flex gap-2 items-center align-center h-full"> <div class="flex gap-2 items-center items-center h-full">
<component :is="option.icon" class="min-w-5 !mx-0" /> <component :is="option.icon" class="min-w-5 !mx-0" />
<span class="min-w-0"> {{ option.label }}</span> <span class="min-w-0"> {{ option.label }}</span>
</div> </div>

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

@ -1,14 +1,35 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk'
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { ActiveViewInj, FieldsInj, IsLockedInj, IsPublicInj, MetaInj, ReloadViewDataHookInj } from '~/context' import {
import { computed, inject, useNuxtApp, useViewColumns, watch } from '#imports' ActiveViewInj,
FieldsInj,
IsLockedInj,
IsPublicInj,
MetaInj,
ReloadViewDataHookInj,
computed,
inject,
ref,
useNuxtApp,
useViewColumns,
watch,
} from '#imports'
import CellIcon from '~/components/smartsheet-header/CellIcon.vue'
import VirtualCellIcon from '~/components/smartsheet-header/VirtualCellIcon.vue'
const meta = inject(MetaInj)! const meta = inject(MetaInj)!
const activeView = inject(ActiveViewInj)! const activeView = inject(ActiveViewInj)!
const reloadDataHook = inject(ReloadViewDataHookInj)! const reloadDataHook = inject(ReloadViewDataHookInj)!
const rootFields = inject(FieldsInj) const rootFields = inject(FieldsInj)
const isLocked = inject(IsLockedInj)
const isPublic = inject(IsPublicInj) const isLocked = inject(IsLockedInj, ref(false))
const isPublic = inject(IsPublicInj, ref(false))
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -22,6 +43,7 @@ const {
showAll, showAll,
hideAll, hideAll,
saveOrUpdate, saveOrUpdate,
metaColumnById,
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger()) } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
watch( watch(
@ -59,17 +81,22 @@ const onMove = (event: { moved: { newIndex: number } }) => {
$e('a:fields:reorder') $e('a:fields:reorder')
} }
const getIcon = (c: ColumnType) =>
h(isVirtualCol(c) ? VirtualCellIcon : CellIcon, {
columnMeta: c,
})
</script> </script>
<template> <template>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<div :class="{ 'nc-badge nc-active-btn': isAnyFieldHidden }"> <div :class="{ 'nc-badge nc-active-btn': isAnyFieldHidden }">
<a-button v-t="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked"> <a-button v-t="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex align-center gap-1"> <div class="flex items-center gap-1">
<MdiEyeOffOutline /> <MdiEyeOffOutline />
<!-- Fields --> <!-- Fields -->
<span class="text-capitalize !text-sm font-weight-medium">{{ $t('objects.fields') }}</span> <span class="text-capitalize !text-sm font-weight-normal">{{ $t('objects.fields') }}</span>
<MdiMenuDown class="text-grey" /> <MdiMenuDown class="text-grey" />
</div> </div>
@ -86,9 +113,12 @@ const onMove = (event: { moved: { newIndex: number } }) => {
<div class="nc-fields-list py-1"> <div class="nc-fields-list py-1">
<Draggable v-model="fields" item-key="id" @change="onMove($event)"> <Draggable v-model="fields" item-key="id" @change="onMove($event)">
<template #item="{ element: field, index: index }"> <template #item="{ element: field, index: index }">
<div v-show="filteredFieldList.includes(field)" :key="field.id" class="px-2 py-1 flex" @click.stop> <div v-show="filteredFieldList.includes(field)" :key="field.id" class="px-2 py-1 flex items-center" @click.stop>
<a-checkbox v-model:checked="field.show" class="flex-shrink" @change="saveOrUpdate(field, index)"> <a-checkbox v-model:checked="field.show" class="shrink" @change="saveOrUpdate(field, index)">
<span class="">{{ field.title }}</span> <div class="flex items-center">
<component :is="getIcon(metaColumnById[field.fk_column_id])" />
<span>{{ field.title }}</span>
</div>
</a-checkbox> </a-checkbox>
<div class="flex-1" /> <div class="flex-1" />
<MdiDrag class="cursor-move" /> <MdiDrag class="cursor-move" />
@ -96,7 +126,8 @@ const onMove = (event: { moved: { newIndex: number } }) => {
</template> </template>
</Draggable> </Draggable>
</div> </div>
<v-divider class="my-2" />
<a-divider class="!my-2" />
<div v-if="!isPublic" class="p-2 py-1 flex" @click.stop> <div v-if="!isPublic" class="p-2 py-1 flex" @click.stop>
<a-checkbox v-model:checked="showSystemFields"> <a-checkbox v-model:checked="showSystemFields">
@ -122,4 +153,7 @@ const onMove = (event: { moved: { newIndex: number } }) => {
:deep(.ant-checkbox-inner) { :deep(.ant-checkbox-inner) {
@apply transform scale-60; @apply transform scale-60;
} }
:deep(.ant-checkbox) {
@apply top-auto;
}
</style> </style>

63
packages/nc-gui-v2/components/smartsheet-toolbar/LockType.vue

@ -0,0 +1,63 @@
<script setup lang="ts">
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
import { LockType } from '~/lib'
const { type, hideTick } = defineProps<{ hideTick?: boolean; type: LockType }>()
const emit = defineEmits(['select'])
const types = {
[LockType.Personal]: {
title: 'title.personalView',
icon: MdiAccountIcon,
subtitle: 'Only you can edit the view configuration. Other collaborators’ personal views are hidden by default.',
},
[LockType.Collaborative]: {
title: 'title.collabView',
icon: MdiAccountGroupIcon,
subtitle: 'Collaborators with edit permissions or higher can change the view configuration.',
},
[LockType.Locked]: {
title: 'title.lockedView',
icon: MdiLockOutlineIcon,
subtitle: 'No one can edit the view configuration until it is unlocked.',
},
}
const selectedView = inject(ActiveViewInj)
</script>
<template>
<div class="nc-locked-menu-item" @click="emit('select', type)">
<div :class="{ 'show-tick': !hideTick }">
<template v-if="!hideTick">
<MdiCheck v-if="selectedView?.lock_type === type" />
<span v-else />
</template>
<div>
<component :is="types[type].icon" class="text-gray-500" />
{{ $t(types[type].title) }}
<div class="nc-subtitle whitespace-normal">
{{ types[type].subtitle }}
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.nc-locked-menu-item > div {
@apply p-2 items-center min-w-[350px] max-w-[350px];
&.show-tick {
@apply grid gap-2 grid-cols-[30px,auto];
}
.nc-subtitle {
@apply text-xs text-gray-500 font-weight-light;
}
}
</style>

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

@ -92,7 +92,7 @@ const exportFile = async (exportType: ExportTypes) => {
<div> <div>
<a-dropdown> <a-dropdown>
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> <a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-1 align-center"> <div class="flex gap-1 items-center">
<MdiFlashOutline /> <MdiFlashOutline />
<!-- More --> <!-- More -->
@ -130,7 +130,7 @@ const exportFile = async (exportType: ExportTypes) => {
</div> </div>
<div <div
v-if="isUIAllowed('SharedViewList') && !isView && !isPublicView" v-if="isUIAllowed('sharedViewList') && !isView && !isPublicView"
v-t="['a:actions:shared-view-list']" v-t="['a:actions:shared-view-list']"
class="nc-menu-item" class="nc-menu-item"
@click="sharedViewListDlg = true" @click="sharedViewListDlg = true"
@ -139,7 +139,6 @@ const exportFile = async (exportType: ExportTypes) => {
<!-- Shared View List --> <!-- Shared View List -->
{{ $t('activity.listSharedView') }} {{ $t('activity.listSharedView') }}
</div> </div>
<div <div
v-if="isUIAllowed('webhook') && !isView && !isPublicView" v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-t="['c:actions:webhook']" v-t="['c:actions:webhook']"

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

@ -3,17 +3,15 @@ import { ReloadViewDataHookInj, inject } from '#imports'
const reloadHook = inject(ReloadViewDataHookInj)! const reloadHook = inject(ReloadViewDataHookInj)!
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const onClick = () => reloadHook.trigger() const onClick = () => reloadHook.trigger()
</script> </script>
<template> <template>
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'"> <a-tooltip placement="bottom">
<template #title> {{ $t('general.reload') }} </template> <template #title> {{ $t('general.reload') }} </template>
<div class="nc-sidebar-right-item hover:after:bg-green-500 group"> <div class="group flex align-center">
<MdiReload class="cursor-pointer group-hover:(!text-white)" @click="onClick" /> <MdiReload class="cursor-pointer text-gray-500 group-hover:(text-primary)" @click="onClick" />
</div> </div>
</a-tooltip> </a-tooltip>
</template> </template>

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

@ -24,7 +24,7 @@ function onPressEnter() {
<template> <template>
<a-input v-model:value="search.query" size="small" class="max-w-[200px]" placeholder="Filter query" @press-enter="onPressEnter"> <a-input v-model:value="search.query" size="small" class="max-w-[200px]" placeholder="Filter query" @press-enter="onPressEnter">
<template #addonBefore> <template #addonBefore>
<div class="flex align-center relative" @click="isDropdownOpen = true"> <div class="flex items-center relative" @click="isDropdownOpen = true">
<MdiMagnify class="text-grey" /> <MdiMagnify class="text-grey" />
<MdiMenuDown class="text-grey" /> <MdiMenuDown class="text-grey" />

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

@ -1,11 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { computed } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app' import { computed, extractSdkResponseErrorMsg, useNuxtApp, useProject, useSmartsheetStoreOrThrow } from '#imports'
import { useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import MdiOpenInNewIcon from '~icons/mdi/open-in-new' import MdiOpenInNewIcon from '~icons/mdi/open-in-new'
import MdiCopyIcon from '~icons/mdi/content-copy' import MdiCopyIcon from '~icons/mdi/content-copy'
@ -19,6 +16,8 @@ const { dashboardUrl } = useDashboard()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { isSharedBase } = useProject()
let showShareModel = $ref(false) let showShareModel = $ref(false)
let passwordProtected = $ref(false) let passwordProtected = $ref(false)
@ -118,11 +117,16 @@ onMounted(() => {
<template> <template>
<div> <div>
<a-button v-if="isUIAllowed('share-view')" v-t="['c:view:share']" outlined class="nc-btn-share-view nc-toolbar-btn"> <a-button
<div class="flex align-center gap-1" @click="genShareLink"> v-if="isUIAllowed('share-view') && !isSharedBase"
v-t="['c:view:share']"
outlined
class="nc-btn-share-view nc-toolbar-btn"
>
<div class="flex items-center gap-1" @click="genShareLink">
<MdiOpenInNewIcon /> <MdiOpenInNewIcon />
<!-- Share View --> <!-- Share View -->
<span class="!text-sm font-weight-medium"> {{ $t('activity.shareView') }}</span> <span class="!text-sm font-weight-normal"> {{ $t('activity.shareView') }}</span>
</div> </div>
</a-button> </a-button>
@ -173,7 +177,7 @@ onMounted(() => {
<style scoped> <style scoped>
.share-link-box { .share-link-box {
@apply flex p-2 w-full items-center align-center gap-1 bg-gray-100 rounded; @apply flex p-2 w-full items-center items-center gap-1 bg-gray-100 rounded;
} }
:deep(.ant-collapse-header) { :deep(.ant-collapse-header) {

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

@ -110,7 +110,7 @@ const deleteLink = async (id: string) => {
<!-- Password --> <!-- Password -->
<a-table-column key="password" :title="$t('labels.password')" data-index="title"> <a-table-column key="password" :title="$t('labels.password')" data-index="title">
<template #default="{ record }"> <template #default="{ record }">
<div class="flex align-center items-center gap-1"> <div class="flex items-center items-center gap-1">
<template v-if="record.password"> <template v-if="record.password">
<span class="h-min">{{ record.showPassword ? record.password : '***************************' }}</span> <span class="h-min">{{ record.showPassword ? record.password : '***************************' }}</span>
<component <component

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

@ -37,10 +37,10 @@ watch(
<a-dropdown offset-y class="" :trigger="['click']"> <a-dropdown offset-y class="" :trigger="['click']">
<div :class="{ 'nc-badge nc-active-btn': sorts?.length }"> <div :class="{ 'nc-badge nc-active-btn': sorts?.length }">
<a-button v-t="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked" <a-button v-t="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked"
><div class="flex align-center gap-1"> ><div class="flex items-center gap-1">
<MdiSortIcon /> <MdiSortIcon />
<!-- Sort --> <!-- Sort -->
<span class="text-capitalize !text-sm font-weight-medium">{{ $t('activity.sort') }}</span> <span class="text-capitalize !text-sm font-weight-normal">{{ $t('activity.sort') }}</span>
<MdiMenuDownIcon class="text-grey" /> <MdiMenuDownIcon class="text-grey" />
</div> </div>
</a-button> </a-button>
@ -49,12 +49,8 @@ watch(
<div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border"> <div class="bg-gray-50 p-6 shadow-lg menu-filter-dropdown min-w-[400px] max-h-[max(80vh,500px)] overflow-auto !border">
<div v-if="sorts?.length" class="sort-grid mb-2" @click.stop> <div v-if="sorts?.length" class="sort-grid mb-2" @click.stop>
<template v-for="(sort, i) in sorts || []" :key="i"> <template v-for="(sort, i) in sorts || []" :key="i">
<!-- <v-icon :key="`${i}icon`" class="nc-sort-item-remove-btn" small @click.stop="deleteSort(sort)"> mdi-close-box </v-icon> --> <MdiDeleteIcon class="nc-sort-item-remove-btn text-grey self-center" small @click.stop="deleteSort(sort, i)" />
<MdiDeleteIcon
class="nc-sort-item-remove-btn text-grey align-self-center"
small
@click.stop="deleteSort(sort, i)"
></MdiDeleteIcon>
<FieldListAutoCompleteDropdown <FieldListAutoCompleteDropdown
v-model="sort.fk_column_id" v-model="sort.fk_column_id"
class="caption nc-sort-field-select" class="caption nc-sort-field-select"
@ -63,12 +59,13 @@ watch(
@click.stop @click.stop
@update:model-value="saveOrUpdate(sort, i)" @update:model-value="saveOrUpdate(sort, i)"
/> />
<a-select <a-select
v-model:value="sort.direction" v-model:value="sort.direction"
class="flex-shrink-1 flex-grow-0 caption nc-sort-dir-select !text-xs" class="shrink grow-0 nc-sort-dir-select !text-xs"
:label="$t('labels.operation')" :label="$t('labels.operation')"
@click.stop @click.stop
@update:value="saveOrUpdate(sort, i)" @select="saveOrUpdate(sort, i)"
> >
<a-select-option <a-select-option
v-for="(option, j) in getSortDirectionOptions(columnByID[sort.fk_column_id]?.uidt)" v-for="(option, j) in getSortDirectionOptions(columnByID[sort.fk_column_id]?.uidt)"
@ -78,14 +75,10 @@ watch(
<span>{{ option.text }}</span> <span>{{ option.text }}</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
<!-- <template #item="{ item }"> -->
<!-- <span class="caption font-weight-regular">{{ item.text }}</span> -->
<!-- </template> -->
<!-- </v-select> -->
</template> </template>
</div> </div>
<a-button class="text-capitalize mb-1 mt-4" type="primary" ghost @click.stop="addSort"> <a-button class="text-capitalize mb-1 mt-4" type="primary" ghost @click.stop="addSort">
<div class="flex gap-1 align-center"> <div class="flex gap-1 items-center">
<MdiAddIcon /> <MdiAddIcon />
<!-- Add Sort Option --> <!-- Add Sort Option -->
{{ $t('activity.addSort') }} {{ $t('activity.addSort') }}

280
packages/nc-gui-v2/components/smartsheet-toolbar/ViewActions.vue

@ -0,0 +1,280 @@
<script lang="ts" setup>
import * as XLSX from 'xlsx'
import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver'
import { message } from 'ant-design-vue'
import { LockType } from '~/lib'
import { viewIcons } from '~/utils'
import {
ActiveViewInj,
FieldsInj,
IsLockedInj,
IsPublicInj,
MetaInj,
extractSdkResponseErrorMsg,
inject,
ref,
useNuxtApp,
useProject,
useUIPermission,
} from '#imports'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
const sharedViewListDlg = ref(false)
const isPublicView = inject(IsPublicInj, ref(false))
const isView = false
const { project } = useProject()
const { $api, $e } = useNuxtApp()
const meta = inject(MetaInj)
const fields = inject(FieldsInj, ref([]))
const selectedView = inject(ActiveViewInj)
const isLocked = inject(IsLockedInj)
const showWebhookDrawer = ref(false)
const quickImportDialog = ref(false)
const { isUIAllowed } = useUIPermission()
const exportFile = async (exportType: ExportTypes) => {
let offset = 0
let c = 1
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob'
try {
while (!isNaN(offset) && offset > -1) {
let res
if (isPublicView.value) {
const { exportFile: sharedViewExportFile } = useSharedView()
res = await sharedViewExportFile(fields.value, offset, exportType, responseType)
} else {
res = await $api.dbViewRow.export(
'noco',
project?.value.title as string,
meta?.value.title as string,
selectedView?.value.title as string,
exportType,
{
responseType,
query: {
offset,
},
} as any,
)
}
const { data, headers } = res
if (exportType === ExportTypes.EXCEL) {
const workbook = XLSX.read(data, { type: 'base64' })
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`)
} else if (exportType === ExportTypes.CSV) {
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' })
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`)
}
offset = +headers['nc-export-offset']
if (offset > -1) {
message.info('Downloading more files')
} else {
message.success('Successfully exported all table data')
}
}
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const Icon = computed(() => {
switch ((selectedView?.value as any)?.lock_type) {
case LockType.Personal:
return MdiAccountIcon
case LockType.Locked:
return MdiLockOutlineIcon
case LockType.Collaborative:
default:
return MdiAccountGroupIcon
}
})
async function changeLockType(type: LockType) {
$e('a:grid:lockmenu', { lockType: type })
if (!selectedView?.value) return
if (type === 'personal') {
return message.info('Coming soon')
}
try {
;(selectedView.value as any).lock_type = type
$api.dbView.update(selectedView.value.id as string, {
lock_type: type,
})
message.success(`Successfully Switched to ${type} view`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script>
<template>
<div>
<a-dropdown :trigger="['click']">
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center">
<component
:is="viewIcons[selectedView?.type].icon"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[selectedView?.type].color }"
/>
<span class="!text-sm font-weight-normal">{{ selectedView?.title }}</span>
<component :is="Icon" class="text-gray-500" />
<MdiMenuDown class="text-grey" />
</div>
</a-button>
<template #overlay>
<a-menu class="ml-6 !text-sm !px-0 !py-2 !rounded">
<a-menu-item-group>
<a-sub-menu
v-if="isUIAllowed('view-type')"
key="lock-type"
class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0"
>
<template #title>
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group px-0 !py-0">
<SmartsheetToolbarLockType hide-tick :type="selectedView?.lock_type || LockType.Collaborative" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu-item @click="changeLockType(LockType.Collaborative)">
<SmartsheetToolbarLockType :type="LockType.Collaborative" />
</a-menu-item>
<a-menu-item @click="changeLockType(LockType.Locked)">
<SmartsheetToolbarLockType :type="LockType.Locked" />
</a-menu-item>
<a-menu-item @click="changeLockType(LockType.Personal)">
<SmartsheetToolbarLockType :type="LockType.Personal" />
</a-menu-item>
</a-sub-menu>
<a-menu-divider />
<a-sub-menu key="download">
<template #title>
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiDownload class="group-hover:text-accent text-gray-500" />
Download
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu-item>
<div v-t="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
</a-menu-item>
<a-menu-item>
<div v-t="['a:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
</div>
</a-menu-item>
</a-sub-menu>
<template v-if="isUIAllowed('csvImport') && !isView && !isPublicView">
<a-sub-menu key="upload">
<template #title>
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiUpload class="group-hover:text-accent text-gray-500" />
Upload
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<template #expandIcon></template>
<a-menu-item>
<div
v-if="isUIAllowed('csvImport') && !isView && !isPublicView"
v-t="['a:actions:upload-csv']"
class="nc-project-menu-item"
:class="{ disabled: isLocked }"
@click="!isLocked ? (quickImportDialog = true) : {}"
>
<MdiUploadOutline class="text-gray-500" />
<!-- Upload CSV -->
{{ $t('activity.uploadCSV') }}
</div>
</a-menu-item>
</a-sub-menu>
</template>
<a-menu-divider />
<a-menu-item>
<div
v-if="isUIAllowed('SharedViewList') && !isView && !isPublicView"
v-t="['a:actions:shared-view-list']"
class="py-2 flex gap-2 items-center"
@click="sharedViewListDlg = true"
>
<MdiViewListOutline class="text-gray-500" />
<!-- Shared View List -->
{{ $t('activity.listSharedView') }}
</div>
</a-menu-item>
<a-menu-item>
<div
v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-t="['c:actions:webhook']"
class="py-2 flex gap-2 items-center"
@click="showWebhookDrawer = true"
>
<MdiHook class="text-gray-500" />
{{ $t('objects.webhooks') }}
</div>
</a-menu-item>
</a-menu-item-group>
</a-menu>
</template>
</a-dropdown>
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" import-type="csv" :import-only="true" />
<WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" />
<a-modal v-model:visible="sharedViewListDlg" title="Shared view list" width="max(900px,60vw)" :footer="null">
<SmartsheetToolbarSharedViewList v-if="sharedViewListDlg" />
</a-modal>
</div>
</template>
<style scoped>
:deep(.ant-dropdown-menu-submenu-title) {
@apply py-0;
}
:deep(.ant-dropdown-menu-item-group-title) {
@apply hidden;
}
</style>

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

@ -9,7 +9,9 @@ import {
IsLockedInj, IsLockedInj,
IsPublicInj, IsPublicInj,
computed, computed,
inject,
provide, provide,
ref,
toRef, toRef,
useColumn, useColumn,
useDebounceFn, useDebounceFn,
@ -42,11 +44,11 @@ provide(EditModeInj, useVModel(props, 'editEnabled', emit))
provide(ActiveCellInj, active) provide(ActiveCellInj, active)
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj, ref(false))
const isPublic = inject(IsPublicInj) const isPublic = inject(IsPublicInj, ref(false))
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj, ref(false))
let changed = $ref(false) let changed = $ref(false)

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

@ -357,7 +357,7 @@ onMounted(async () => {
<template> <template>
<a-row v-if="submitted" class="h-full"> <a-row v-if="submitted" class="h-full">
<a-col :span="24"> <a-col :span="24">
<div v-if="formViewData" class="align-center justify-center text-center mt-2"> <div v-if="formViewData" class="items-center justify-center text-center mt-2">
<a-alert type="success"> <a-alert type="success">
<template #message> <template #message>
<div class="text-center">{{ formViewData.success_msg || 'Successfully submitted form data' }}</div> <div class="text-center">{{ formViewData.success_msg || 'Successfully submitted form data' }}</div>
@ -376,7 +376,7 @@ onMounted(async () => {
<a-col <a-col
v-if="isEditable" v-if="isEditable"
:span="8" :span="8"
class="bg-[#f7f7f7] shadow-md pa-5 h-full overflow-auto scrollbar-thin-primary nc-form-left-drawer" class="bg-[#f7f7f7] shadow-md p-5 h-full overflow-auto scrollbar-thin-primary nc-form-left-drawer"
> >
<div class="flex"> <div class="flex">
<div class="flex flex-row flex-1 text-lg"> <div class="flex flex-row flex-1 text-lg">
@ -412,7 +412,7 @@ onMounted(async () => {
<template #item="{ element }"> <template #item="{ element }">
<a-card <a-card
size="small" size="small"
class="ma-0 pa-0 cursor-pointer item mb-2" class="m-0 p-0 cursor-pointer item mb-2"
@mousedown="moved = false" @mousedown="moved = false"
@mousemove="moved = false" @mousemove="moved = false"
@mouseup="handleMouseUp(element)" @mouseup="handleMouseUp(element)"
@ -470,7 +470,7 @@ onMounted(async () => {
<!-- for future implementation of cover image --> <!-- for future implementation of cover image -->
</div> </div>
<a-card <a-card
class="h-full ma-0 rounded-b-0 pa-4 border-none" class="h-full m-0 rounded-b-0 p-4 border-none"
:body-style="{ :body-style="{
maxWidth: '700px', maxWidth: '700px',
margin: '0 auto', margin: '0 auto',
@ -478,9 +478,9 @@ onMounted(async () => {
}" }"
> >
<a-form ref="formRef" :model="formState" class="nc-form"> <a-form ref="formRef" :model="formState" class="nc-form">
<a-card class="rounded ma-2 py-10 px-5"> <a-card class="rounded m-2 py-10 px-5">
<!-- Header --> <!-- Header -->
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0"> <a-form-item v-if="isEditable" class="m-0 gap-0 p-0">
<a-input <a-input
v-model:value="formViewData.heading" v-model:value="formViewData.heading"
class="w-full text-bold text-h3" class="w-full text-bold text-h3"
@ -496,7 +496,7 @@ onMounted(async () => {
<div v-else class="ml-3 w-full text-bold text-h3">{{ formViewData.heading }}</div> <div v-else class="ml-3 w-full text-bold text-h3">{{ formViewData.heading }}</div>
<!-- Sub Header --> <!-- Sub Header -->
<a-form-item v-if="isEditable" class="ma-0 gap-0 pa-0"> <a-form-item v-if="isEditable" class="m-0 gap-0 p-0">
<a-input <a-input
v-model:value="formViewData.subheading" v-model:value="formViewData.subheading"
class="w-full" class="w-full"
@ -518,7 +518,7 @@ onMounted(async () => {
item-key="fk_column_id" item-key="fk_column_id"
draggable=".item" draggable=".item"
group="form-inputs" group="form-inputs"
class="h-100" class="h-full"
:move="onMoveCallback" :move="onMoveCallback"
@change="onMove($event)" @change="onMove($event)"
@start="drag = true" @start="drag = true"
@ -526,7 +526,7 @@ onMounted(async () => {
> >
<template #item="{ element, index }"> <template #item="{ element, index }">
<div <div
class="nc-editable item cursor-pointer hover:bg-primary/10 pa-3" class="nc-editable item cursor-pointer hover:(bg-primary bg-opacity-10) p-3"
:class="`nc-form-drag-${element.title.replaceAll(' ', '')}`" :class="`nc-form-drag-${element.title.replaceAll(' ', '')}`"
@click="activeRow = element.title" @click="activeRow = element.title"
> >
@ -555,7 +555,7 @@ onMounted(async () => {
<a-form-item <a-form-item
v-if="isVirtualCol(element)" v-if="isVirtualCol(element)"
class="ma-0 gap-0 pa-0" class="m-0 gap-0 p-0"
:name="element.title" :name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]" :rules="[{ required: element.required, message: `${element.title} is required` }]"
> >
@ -570,7 +570,7 @@ onMounted(async () => {
<a-form-item <a-form-item
v-else v-else
class="ma-0 gap-0 pa-0" class="m-0 gap-0 p-0"
:name="element.title" :name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]" :rules="[{ required: element.required, message: `${element.title} is required` }]"
> >
@ -710,7 +710,7 @@ onMounted(async () => {
} }
.nc-input { .nc-input {
@apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex align-center border-solid border-1 border-primary; @apply w-full !bg-white rounded px-2 py-2 min-h-[40px] mt-2 mb-2 flex items-center border-solid border-1 border-primary;
} }
.form-meta-input::placeholder { .form-meta-input::placeholder {

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

@ -3,6 +3,19 @@ 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 { message } from 'ant-design-vue'
import { import {
ActiveViewInj,
ChangePageInj,
FieldsInj,
IsFormInj,
IsGridInj,
IsLockedInj,
IsPublicInj,
MetaInj,
OpenNewRecordFormHookInj,
PaginationDataInj,
ReadonlyInj,
ReloadViewDataHookInj,
enumColor,
inject, inject,
onClickOutside, onClickOutside,
onMounted, onMounted,
@ -12,25 +25,12 @@ import {
useEventListener, useEventListener,
useGridViewColumnWidth, useGridViewColumnWidth,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useUIPermission,
useViewData, useViewData,
watch, watch,
} from '#imports' } from '#imports'
import type { Row } from '~/composables' import type { Row } from '~/composables'
import {
ActiveViewInj,
ChangePageInj,
FieldsInj,
IsFormInj,
IsGridInj,
IsLockedInj,
IsPublicInj,
MetaInj,
PaginationDataInj,
ReadonlyInj,
ReloadViewDataHookInj,
} from '~/context'
import { NavigateDir } from '~/lib' import { NavigateDir } from '~/lib'
import { enumColor } from '~/utils'
const meta = inject(MetaInj) const meta = inject(MetaInj)
@ -42,9 +42,10 @@ const isPublicView = inject(IsPublicInj, ref(false))
// fields menu and get used in grid and gallery // fields menu and get used in grid and gallery
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const readOnly = inject(ReadonlyInj, false) const readOnly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj, false) const isLocked = inject(IsLockedInj, ref(false))
const reloadViewDataHook = inject(ReloadViewDataHookInj) const reloadViewDataHook = inject(ReloadViewDataHookInj)
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj)
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
@ -86,7 +87,7 @@ const {
deleteRow, deleteRow,
deleteSelectedRows, deleteSelectedRows,
selectedAllRecords, selectedAllRecords,
loadAggCommentsCount, removeLastEmptyRow,
} = useViewData(meta, view as any, xWhere) } = useViewData(meta, view as any, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any) const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any)
@ -104,7 +105,17 @@ provide(ReadonlyInj, !isUIAllowed('xcDatatableEditable'))
reloadViewDataHook?.on(async () => { reloadViewDataHook?.on(async () => {
await loadData() await loadData()
loadAggCommentsCount() })
const expandForm = (row: Row, state?: Record<string, any>) => {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
}
openNewRecordFormHook?.on(async () => {
const newRow = await addEmptyRow()
expandForm(newRow)
}) })
const selectCell = (row: number, col: number) => { const selectCell = (row: number, col: number) => {
@ -115,7 +126,7 @@ const selectCell = (row: number, col: number) => {
watch( watch(
() => (view?.value as any)?.id, () => (view?.value as any)?.id,
async (n?: string, o?: string) => { async (n?: string, o?: string) => {
if (n && n !== o) { if (n && o && n !== o) {
await loadData() await loadData()
} }
}, },
@ -293,21 +304,15 @@ const onNavigate = (dir: NavigateDir) => {
break break
} }
} }
const expandForm = (row: Row, state: Record<string, any>) => {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
}
</script> </script>
<template> <template>
<div class="flex flex-col h-100 min-h-0 w-100"> <div class="flex flex-col h-full min-h-0 w-full">
<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 v-model:visible="contextMenu" :trigger="['contextmenu']"> <a-dropdown v-model:visible="contextMenu" :trigger="['contextmenu']">
<table <table
ref="smartTable" ref="smartTable"
class="xc-row-table nc-grid backgroundColorDefault !h-auto" class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
@contextmenu.prevent="contextMenu = true" @contextmenu.prevent="contextMenu = true"
> >
<thead> <thead>
@ -318,7 +323,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div> <div class="nc-no-label text-gray-500" :class="{ hidden: selectedAllRecords }">#</div>
<div <div
:class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }" :class="{ hidden: !selectedAllRecords, flex: selectedAllRecords }"
class="nc-check-all w-full align-center" class="nc-check-all w-full items-center"
> >
<a-checkbox v-model:checked="selectedAllRecords" /> <a-checkbox v-model:checked="selectedAllRecords" />
@ -353,8 +358,8 @@ const expandForm = (row: Row, state: Record<string, any>) => {
@click.stop="addColumnDropdown = true" @click.stop="addColumnDropdown = true"
> >
<a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']"> <a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']">
<div class="h-full w-[60px] flex align-center justify-center"> <div class="h-full w-[60px] flex items-center justify-center">
<MdiPlus class="text-sm" /> <MdiPlus class="text-sm nc-column-add" />
</div> </div>
<template #overlay> <template #overlay>
@ -375,9 +380,9 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<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">
<div class="align-center flex gap-1 min-w-[55px]"> <div class="items-center flex gap-1 min-w-[55px]">
<div <div
v-if="!readonly && !isLocked" v-if="!readOnly && !isLocked"
class="nc-row-no text-xs text-gray-500" class="nc-row-no text-xs text-gray-500"
:class="{ hidden: row.rowMeta.selected }" :class="{ hidden: row.rowMeta.selected }"
> >
@ -391,7 +396,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<a-checkbox v-model:checked="row.rowMeta.selected" /> <a-checkbox v-model:checked="row.rowMeta.selected" />
</div> </div>
<span class="flex-1" /> <span class="flex-1" />
<div v-if="!readonly && !isLocked" class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }"> <div v-if="!readOnly && !isLocked" class="nc-expand" :class="{ 'nc-comment': row.rowMeta?.commentCount }">
<span <span
v-if="row.rowMeta?.commentCount" v-if="row.rowMeta?.commentCount"
class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)" class="py-1 px-3 rounded-full text-xs cursor-pointer select-none transform hover:(scale-110)"
@ -400,9 +405,12 @@ const expandForm = (row: Row, state: Record<string, any>) => {
> >
{{ row.rowMeta.commentCount }} {{ row.rowMeta.commentCount }}
</span> </span>
<div v-else class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:bg-primary/10"> <div
v-else
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
>
<MdiArrowExpand <MdiArrowExpand
class="select-none transform hover:(text-pink-500 scale-120) nc-row-expand" class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)" @click="expandForm(row, state)"
/> />
</div> </div>
@ -465,11 +473,11 @@ const expandForm = (row: Row, state: Record<string, any>) => {
<td <td
v-t="['c:row:add:grid-bottom']" v-t="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1" :colspan="visibleColLength + 1"
class="text-left pointer nc-grid-add-new-cell" class="text-left pointer nc-grid-add-new-cell cursor-pointer"
@click="addEmptyRow()" @click="addEmptyRow()"
> >
<div class="px-2 w-full flex items-center text-gray-500"> <div class="px-2 w-full flex items-center text-gray-500">
<MdiPlus class="text-pint-500 text-xs ml-2" /> <MdiPlus class="text-pint-500 text-xs ml-2 text-primary" />
<span class="ml-1"> <span class="ml-1">
{{ $t('activity.addRow') }} {{ $t('activity.addRow') }}
@ -479,17 +487,32 @@ const expandForm = (row: Row, state: Record<string, any>) => {
</tr> </tr>
</tbody> </tbody>
</table> </table>
<template v-if="!isLocked" #overlay>
<a-menu class="bg-white shadow" @click="contextMenu = false"> <template v-if="!isLocked && isUIAllowed('xcDatatableEditable')" #overlay>
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)" <a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
><span class="text-xs">Delete row</span></a-menu-item <a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)">
> <div class="nc-project-menu-item">
<a-menu-item @click="deleteSelectedRows"><span class="text-xs">Delete all selected rows</span></a-menu-item> <!-- Delete Row -->
<a-menu-item v-if="contextMenuTarget" @click="clearCell(contextMenuTarget)" {{ $t('activity.deleteRow') }}
><span class="text-xs">Clear cell</span> </div>
</a-menu-item>
<a-menu-item @click="deleteSelectedRows">
<div class="nc-project-menu-item">
<!-- Delete Selected Rows -->
{{ $t('activity.deleteSelectedRow') }}
</div>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="clearCell(contextMenuTarget)">
<div class="nc-project-menu-item">Clear cell</div>
</a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="addEmptyRow(contextMenuTarget.row + 1)"> <a-menu-item v-if="contextMenuTarget" @click="addEmptyRow(contextMenuTarget.row + 1)">
<span class="text-xs">Insert new row</span> <div class="nc-project-menu-item">
<!-- Insert New Row -->
{{ $t('activity.insertRow') }}
</div>
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</template> </template>
@ -504,6 +527,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
:row="expandedFormRow" :row="expandedFormRow"
:state="expandedFormRowState" :state="expandedFormRowState"
:meta="meta" :meta="meta"
@cancel="removeLastEmptyRow"
/> />
</div> </div>
</template> </template>
@ -521,7 +545,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
td:not(:first-child) > div { td:not(:first-child) > div {
overflow: hidden; overflow: hidden;
@apply flex align-center h-auto px-1; @apply flex items-center h-auto px-1;
} }
table, table,
@ -553,7 +577,7 @@ const expandForm = (row: Row, state: Record<string, any>) => {
} }
td.active::before { td.active::before {
@apply bg-primary/5; @apply bg-primary bg-opacity-5;
} }
} }
@ -611,4 +635,8 @@ const expandForm = (row: Row, state: Record<string, any>) => {
} }
} }
} }
tbody tr:hover {
@apply bg-gray-100 bg-opacity-50;
}
</style> </style>

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

@ -34,7 +34,7 @@ const page = computed({
show-less-items show-less-items
:show-size-changer="false" :show-size-changer="false"
/> />
<div v-else class="mx-auto d-flex align-center mt-n1" style="max-width: 250px"> <div v-else class="mx-auto flex items-center mt-n1" style="max-width: 250px">
<span class="text-xs" style="white-space: nowrap"> Change page:</span> <span class="text-xs" style="white-space: nowrap"> Change page:</span>
<a-input :value="page" size="small" class="ml-1 !text-xs" type="number" @keydown.enter="changePage(page)"> <a-input :value="page" size="small" class="ml-1 !text-xs" type="number" @keydown.enter="changePage(page)">
<template #suffix> <template #suffix>
@ -49,12 +49,14 @@ const page = computed({
<style scoped> <style scoped>
:deep(.ant-pagination-item a) { :deep(.ant-pagination-item a) {
@apply text-sm !leading-[21px]; @apply text-sm !leading-[21px] !no-underline;
} }
:deep(.ant-pagination-item:not(.ant-pagination-item-active) a) { :deep(.ant-pagination-item:not(.ant-pagination-item-active) a) {
line-height: 21px !important; line-height: 21px !important;
@apply text-sm text-gray-500; @apply text-sm !text-gray-500;
} }
:deep(.ant-pagination-item-link) { :deep(.ant-pagination-item-link) {
@apply text-gray-500; @apply text-gray-500;
} }

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

@ -1,13 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { IsPublicInj, useSmartsheetStoreOrThrow } from '#imports' import { IsPublicInj, useSmartsheetStoreOrThrow } from '#imports'
import ToggleDrawer from '~/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue'
const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow() const { isGrid, isForm, isGallery } = useSmartsheetStoreOrThrow()
const { allowCSVDownload } = useSharedView()
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const { allowCSVDownload } = useSharedView()
const { isOpen } = useSidebar()
</script> </script>
<template> <template>
<div class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[48px] px-2 border-b" style="z-index: 7"> <div
class="nc-table-toolbar w-full py-1 flex gap-1 items-center h-[var(--toolbar-height)] px-2 border-b overflow-x-hidden"
style="z-index: 7"
>
<SmartsheetToolbarViewActions
v-if="(isGrid && !isPublic) || (isGrid && isPublic && allowCSVDownload)"
:show-system-fields="false"
class="ml-1"
/>
<SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery" :show-system-fields="false" class="ml-1" /> <SmartsheetToolbarFieldsMenu v-if="isGrid || isGallery" :show-system-fields="false" class="ml-1" />
<SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery" /> <SmartsheetToolbarColumnFilterMenu v-if="isGrid || isGallery" />
@ -16,9 +28,14 @@ const isPublic = inject(IsPublicInj, ref(false))
<SmartsheetToolbarShareView v-if="(isForm || isGrid) && !isPublic" /> <SmartsheetToolbarShareView v-if="(isForm || isGrid) && !isPublic" />
<SmartsheetToolbarMoreActions v-if="(isGrid && !isPublic) || (isGrid && isPublic && allowCSVDownload)" />
<div class="flex-1" /> <div class="flex-1" />
<SmartsheetToolbarSearchData v-if="(isGrid || isGallery) && !isPublic" class="shrink mr-2" />
<SmartsheetToolbarReload class="mx-1" />
<SmartsheetToolbarAddRow class="mx-1" />
<SmartsheetToolbarSearchData v-if="(isGrid || isGallery) && !isPublic" class="shrink mr-2 ml-2" />
<ToggleDrawer v-if="!isOpen" class="mr-2" />
</div> </div>
</template> </template>

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

@ -24,13 +24,14 @@ 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-grow-1 min-h-[100px] overflow-y-auto scrollbar-thin-primary p-2 space-y-2"> <div ref="commentsWrapperEl" class="flex-1 min-h-[100px] overflow-y-auto scrollbar-thin-primary p-2 space-y-2">
<v-skeleton-loader v-if="isCommentsLoading && !commentsAndLogs" type="list-item-avatar-two-line@8" /> <v-skeleton-loader 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">
<MdiAccountCircle class="row-span-2" :class="isYou(log.user) ? 'text-pink-300' : 'text-blue-300 '" /> <MdiAccountCircle class="row-span-2" :class="isYou(log.user) ? 'text-pink-300' : 'text-blue-300 '" />
<div class="flex-grow">
<div class="flex-1">
<p class="mb-1 caption edited-text text-[10px] text-gray-500"> <p class="mb-1 caption edited-text text-[10px] text-gray-500">
{{ 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' }}
@ -59,10 +60,10 @@ watch(
><span class="text-[11px] text-gray-500">Comments only</span> ><span class="text-[11px] text-gray-500">Comments only</span>
</a-checkbox> </a-checkbox>
</div> </div>
<div class="flex-shrink-1 mt-2 d-flex"> <div class="shrink mt-2 flex">
<a-input <a-input
v-model:value="comment" v-model:value="comment"
class="!text-xs" class="!text-xs nc-comment-box"
ghost ghost
:class="{ focus: showborder }" :class="{ focus: showborder }"
@focusin="showborder = true" @focusin="showborder = true"
@ -70,7 +71,7 @@ watch(
@keyup.enter.prevent="saveComment" @keyup.enter.prevent="saveComment"
> >
<template #addonBefore> <template #addonBefore>
<div class="flex align-center"> <div class="flex items-center">
<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>

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

@ -35,8 +35,8 @@ const iconColor = '#1890ff'
</script> </script>
<template> <template>
<div class="flex p-2 align-center gap-2 p-4"> <div class="flex p-2 items-center gap-2 p-4">
<h5 class="text-lg font-weight-medium flex align-center gap-1 mb-0 min-w-0 overflow-x-hidden truncate"> <h5 class="text-lg font-weight-medium flex items-center gap-1 mb-0 min-w-0 overflow-x-hidden truncate">
<mdi-table-arrow-right :style="{ color: iconColor }" /> <mdi-table-arrow-right :style="{ color: iconColor }" />
<template v-if="meta"> <template v-if="meta">
@ -51,16 +51,24 @@ const iconColor = '#1890ff'
<template v-if="primaryValue">: {{ primaryValue }}</template> <template v-if="primaryValue">: {{ primaryValue }}</template>
</h5> </h5>
<div class="flex-grow" /> <div class="flex-1" />
<a-tooltip placement="bottom">
<mdi-reload class="cursor-pointer select-none" /> <template #title>
<div class="text-center w-full">Reload</div>
<component </template>
:is="drawerToggleIcon" <mdi-reload class="cursor-pointer select-none" />
v-if="isUIAllowed('rowComments')" </a-tooltip>
class="cursor-pointer select-none" <a-tooltip placement="bottom">
@click="commentsDrawer = !commentsDrawer" <template #title>
/> <div class="text-center w-full">Toggle comments draw</div>
</template>
<component
:is="drawerToggleIcon"
v-if="isUIAllowed('rowComments') && !isNew"
class="cursor-pointer select-none nc-toggle-comments"
@click="commentsDrawer = !commentsDrawer"
/>
</a-tooltip>
<a-button class="!text" @click="emit('cancel')"> <a-button class="!text" @click="emit('cancel')">
<!-- Cancel --> <!-- Cancel -->

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnType, TableType, ViewType } from 'nocodb-sdk'
import { isVirtualCol } from 'nocodb-sdk' import { isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import Cell from '../Cell.vue' import Cell from '../Cell.vue'
import VirtualCell from '../VirtualCell.vue' import VirtualCell from '../VirtualCell.vue'
@ -36,7 +36,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue']) const emits = defineEmits(['update:modelValue', 'cancel'])
const row = toRef(props, 'row') const row = toRef(props, 'row')
@ -46,14 +46,14 @@ const meta = toRef(props, 'meta')
const fields = computedInject(FieldsInj, (_fields) => { const fields = computedInject(FieldsInj, (_fields) => {
if (props.useMetaFields) { if (props.useMetaFields) {
return meta.value.columns ?? [] return (meta.value.columns ?? []).filter((col) => !isSystemColumn(col))
} }
return _fields?.value ?? [] return _fields?.value ?? []
}) })
provide(MetaInj, meta) provide(MetaInj, meta)
const { commentsDrawer, changedColumns, state: rowState } = useProvideExpandedFormStore(meta, row) const { commentsDrawer, changedColumns, state: rowState, isNew } = useProvideExpandedFormStore(meta, row)
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -91,6 +91,11 @@ watch(
const isExpanded = useVModel(props, 'modelValue', emits, { const isExpanded = useVModel(props, 'modelValue', emits, {
defaultValue: false, defaultValue: false,
}) })
const onClose = () => {
if (row.value?.rowMeta?.new) emits('cancel')
isExpanded.value = false
}
</script> </script>
<script lang="ts"> <script lang="ts">
@ -101,16 +106,17 @@ export default {
<template> <template>
<a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false"> <a-modal v-model:visible="isExpanded" :footer="null" width="min(90vw,1000px)" :body-style="{ padding: 0 }" :closable="false">
<Header @cancel="isExpanded = false" /> <Header @cancel="onClose" />
<div class="!bg-gray-100 rounded"> <div class="!bg-gray-100 rounded">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[70vh]"> <div class="flex h-full nc-form-wrapper items-stretch min-h-[70vh]">
<div class="flex-grow overflow-auto scrollbar-thin-primary"> <div class="flex-1 overflow-auto scrollbar-thin-primary">
<div class="w-[500px] mx-auto"> <div class="w-[500px] mx-auto">
<div v-for="col of fields" :key="col.title" class="mt-2 py-2" :class="`nc-expand-col-${col.title}`"> <div v-for="col of fields" :key="col.title" class="mt-2 py-2" :class="`nc-expand-col-${col.title}`">
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" /> <SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" /> <SmartsheetHeaderCell v-else :column="col" />
<div class="!bg-white rounded px-1 min-h-[35px] flex align-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" /> <VirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<Cell <Cell
@ -125,7 +131,7 @@ export default {
</div> </div>
</div> </div>
<div 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" /> <Comments v-if="commentsDrawer" />
</div> </div>

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

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import { ref, useNuxtApp } from '#imports' import { ref } from '#imports'
import { viewIcons } from '~/utils' import { viewIcons } from '~/utils'
interface Emits { interface Emits {
@ -9,33 +9,20 @@ interface Emits {
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const { $e } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const isView = ref(false) const isView = ref(false)
let showApiSnippet = $ref(false)
const showWebhookDrawer = ref(false) const showWebhookDrawer = ref(false)
function onApiSnippet() {
// get API snippet
showApiSnippet = true
$e('a:view:api-snippet')
}
function onWebhooks() {
showWebhookDrawer.value = true
}
function onOpenModal(type: ViewTypes, title = '') { function onOpenModal(type: ViewTypes, title = '') {
emits('openModal', { type, title }) emits('openModal', { type, title })
} }
</script> </script>
<template> <template>
<a-menu :selected-keys="[]" class="flex-1 flex flex-col"> <a-menu :selected-keys="[]" class="flex flex-col">
<div class="flex-1"></div>
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')"> <div v-if="isUIAllowed('virtualViewsCreateOrEdit')">
<h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500"> <h3 class="px-3 py-1 text-xs font-semibold flex items-center gap-4 text-gray-500">
{{ $t('activity.createView') }} {{ $t('activity.createView') }}
@ -52,7 +39,7 @@ function onOpenModal(type: ViewTypes, title = '') {
</template> </template>
<div class="text-xs flex items-center h-full w-full gap-2"> <div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GRID].icon" :class="`text-${viewIcons[ViewTypes.GRID].color}`" /> <component :is="viewIcons[ViewTypes.GRID].icon" :style="{ color: viewIcons[ViewTypes.GRID].color }" />
<div>{{ $t('objects.viewType.grid') }}</div> <div>{{ $t('objects.viewType.grid') }}</div>
@ -74,7 +61,7 @@ function onOpenModal(type: ViewTypes, title = '') {
</template> </template>
<div class="text-xs flex items-center h-full w-full gap-2"> <div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.GALLERY].icon" :class="`text-${viewIcons[ViewTypes.GALLERY].color}`" /> <component :is="viewIcons[ViewTypes.GALLERY].icon" :style="{ color: viewIcons[ViewTypes.GALLERY].color }" />
<div>{{ $t('objects.viewType.gallery') }}</div> <div>{{ $t('objects.viewType.gallery') }}</div>
@ -97,7 +84,7 @@ function onOpenModal(type: ViewTypes, title = '') {
</template> </template>
<div class="text-xs flex items-center h-full w-full gap-2"> <div class="text-xs flex items-center h-full w-full gap-2">
<component :is="viewIcons[ViewTypes.FORM].icon" :class="`text-${viewIcons[ViewTypes.FORM].color}`" /> <component :is="viewIcons[ViewTypes.FORM].icon" :style="{ color: viewIcons[ViewTypes.FORM].color }" />
<div>{{ $t('objects.viewType.form') }}</div> <div>{{ $t('objects.viewType.form') }}</div>
@ -107,28 +94,13 @@ function onOpenModal(type: ViewTypes, title = '') {
</div> </div>
</a-tooltip> </a-tooltip>
</a-menu-item> </a-menu-item>
</div>
<SmartsheetSidebarMenuApiSnippet v-model="showApiSnippet" />
<div class="flex-auto justify-end flex flex-col gap-3 mt-3"> <div class="w-full h-4"></div>
<button
v-if="isUIAllowed('virtualViewsCreateOrEdit')"
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease !text-xs"
@click="onWebhooks"
>
<mdi-hook />{{ $t('objects.webhooks') }}
</button>
<button
class="flex items-center gap-2 w-full mx-3 px-4 py-3 rounded border transform translate-x-4 hover:(translate-x-0 shadow-lg) transition duration-150 ease !text-xs"
@click="onApiSnippet"
>
<mdi-xml />Get API Snippet
</button>
</div> </div>
<general-flipping-card class="my-4 lg:my-6 min-h-[100px]" :triggers="['click', { duration: 15000 }]"> <!--
todo: bring back later
<general-flipping-card class="my-4 lg:my-6 min-h-[100px]" :triggers="['click', { duration: 15000 }]">
<template #front> <template #front>
<div class="flex h-full w-full gap-6 flex-col"> <div class="flex h-full w-full gap-6 flex-col">
<general-social /> <general-social />
@ -148,7 +120,7 @@ function onOpenModal(type: ViewTypes, title = '') {
</template> </template>
<template #back> <template #back>
<!-- todo: add project cost --> &lt;!&ndash; todo: add project cost &ndash;&gt;
<a <a
href="https://github.com/sponsors/nocodb" href="https://github.com/sponsors/nocodb"
target="_blank" target="_blank"
@ -159,7 +131,7 @@ function onOpenModal(type: ViewTypes, title = '') {
{{ $t('activity.sponsorUs') }} {{ $t('activity.sponsorUs') }}
</a> </a>
</template> </template>
</general-flipping-card> </general-flipping-card> -->
<WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" /> <WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" />
</a-menu> </a-menu>

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

@ -137,6 +137,10 @@ onMounted(() => menuRef && initSortable(menuRef.$el))
/** Navigate to view by changing url param */ /** Navigate to view by changing url param */
function changeView(view: { id: string; alias?: string; title?: string; type: ViewTypes }) { function changeView(view: { id: string; alias?: string; title?: string; type: ViewTypes }) {
router.push({ params: { viewTitle: view.title || '' } }) router.push({ params: { viewTitle: view.title || '' } })
if (view.type === 1 && selected.value[0] === view.id) {
// reload the page if the same form view is clicked
router.go(0)
}
} }
/** Rename a view */ /** Rename a view */
@ -170,9 +174,7 @@ function onDeleted() {
</script> </script>
<template> <template>
<h3 class="pt-3 px-3 text-xs text-gray-500 font-semibold">{{ $t('objects.views') }}</h3> <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" :selected-keys="selected">
<RenameableMenuItem <RenameableMenuItem
v-for="view of views" v-for="view of views"
:id="view.id" :id="view.id"
@ -180,11 +182,11 @@ function onDeleted() {
:view="view" :view="view"
:on-validate="validate" :on-validate="validate"
class="transition-all ease-in duration-300" class="transition-all ease-in duration-300"
:class="[ :class="{
isMarked === view.id ? 'bg-gray-200' : '', 'bg-gray-100': isMarked === view.id,
route.params.viewTitle && route.params.viewTitle.includes(view.title) ? 'active' : '', 'active': route.params.viewTitle && route.params.viewTitle === view.title,
`nc-view-item nc-${view.type}-view-item`, [`nc-view-item nc-${view.type}-view-item`]: true,
]" }"
@change-view="changeView" @change-view="changeView"
@open-modal="$emit('openModal', $event)" @open-modal="$emit('openModal', $event)"
@delete="onDelete" @delete="onDelete"
@ -197,7 +199,7 @@ function onDeleted() {
<style lang="scss"> <style lang="scss">
.nc-views-menu { .nc-views-menu {
@apply flex-1 max-h-[30vh] overflow-y-scroll scrollbar-thin-dull; @apply flex-1 min-h-[100px] overflow-y-scroll scrollbar-thin-dull;
.ghost, .ghost,
.ghost > * { .ghost > * {
@ -219,11 +221,11 @@ function onDeleted() {
} }
.sortable-chosen { .sortable-chosen {
@apply !bg-primary/25 text-primary; @apply !bg-primary bg-opacity-25 text-primary;
} }
.active { .active {
@apply bg-primary/20 text-primary font-medium; @apply bg-primary bg-opacity-25 text-primary font-medium;
} }
} }
</style> </style>

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

@ -147,7 +147,7 @@ function onStopEdit() {
<template> <template>
<a-menu-item <a-menu-item
class="select-none group !flex !items-center !my-0" class="select-none group !flex !items-center !my-0"
@dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick" @dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick()"
@click.stop="onClick" @click.stop="onClick"
> >
<div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2"> <div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">
@ -160,7 +160,7 @@ function onStopEdit() {
<component <component
:is="viewIcons[vModel.type].icon" :is="viewIcons[vModel.type].icon"
class="nc-view-icon group-hover:hidden" class="nc-view-icon group-hover:hidden"
:class="`text-${viewIcons[vModel.type].color}`" :style="{ color: viewIcons[vModel?.type]?.color }"
/> />
</div> </div>

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

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormType, GalleryType, GridType, KanbanType, ViewTypes } from 'nocodb-sdk' import type { ViewType, ViewTypes } from 'nocodb-sdk'
import MenuTop from './MenuTop.vue' import MenuTop from './MenuTop.vue'
import MenuBottom from './MenuBottom.vue' import MenuBottom from './MenuBottom.vue'
import Toolbar from './toolbar/index.vue' import Toolbar from './toolbar/index.vue'
@ -12,7 +12,6 @@ import {
inject, inject,
provide, provide,
ref, ref,
useElementHover,
useRoute, useRoute,
useRouter, useRouter,
useViews, useViews,
@ -55,8 +54,6 @@ let selectedViewId = $ref('')
/** is view creation modal open */ /** is view creation modal open */
let modalOpen = $ref(false) let modalOpen = $ref(false)
const isHovered = useElementHover(sidebar)
/** Watch route param and change active view based on `viewTitle` */ /** Watch route param and change active view based on `viewTitle` */
watch( watch(
[views, () => route.params.viewTitle], [views, () => route.params.viewTitle],
@ -84,7 +81,7 @@ function openModal({ type, title = '', copyViewId }: { type: ViewTypes; title: s
} }
/** Handle view creation */ /** Handle view creation */
function onCreate(view: GridType | FormType | KanbanType | GalleryType) { function onCreate(view: ViewType) {
views.value.push(view) views.value.push(view)
activeView.value = view activeView.value = view
router.push({ params: { viewTitle: view.title || '' } }) router.push({ params: { viewTitle: view.title || '' } })
@ -102,59 +99,17 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
class="relative shadow-md h-full" class="relative shadow-md h-full"
theme="light" theme="light"
> >
<a-tooltip :mouse-enter-delay="1" placement="left"> <Toolbar
<template #title> Toggle sidebar </template> v-if="isOpen"
class="min-h-[var(--toolbar-height)] max-h-[var(--toolbar-height)]"
<Transition name="glow"> :class="{ 'flex items-center py-3 px-3 justify-between border-b-1': !isForm }"
<div />
v-show="sidebarCollapsed || isHovered" <div v-if="isOpen" class="flex-1 flex flex-col min-h-0">
class="group color-transition cursor-pointer hover:ring active:ring-pink-500 z-1 flex items-center p-[1px] absolute top-1/2 left-[-1rem] shadow bg-gray-100 rounded-full"
>
<MaterialSymbolsChevronRightRounded
v-if="isOpen"
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400 nc-right-sidebar-toggle"
@click="isOpen = false"
/>
<MaterialSymbolsChevronLeftRounded
v-else
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400 nc-right-sidebar-toggle"
@click="isOpen = true"
/>
</div>
</Transition>
</a-tooltip>
<Toolbar v-if="isOpen" :class="{ 'flex items-center py-3 px-3 justify-between border-b-1': !isForm }" />
<Toolbar v-else class="py-3 px-2 max-w-[50px] flex !flex-col-reverse gap-4 items-center mt-[-1px]">
<template #start>
<a-tooltip v-if="isUIAllowed('virtualViewsCreateOrEdit')" placement="left">
<template #title> {{ $t('objects.webhooks') }}</template>
<div class="nc-sidebar-right-item hover:after:bg-gray-300">
<MdiHook @click.stop />
</div>
</a-tooltip>
<div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="dot" />
<a-tooltip placement="left">
<template #title> Get API Snippet</template>
<div class="nc-sidebar-right-item group hover:after:bg-yellow-500">
<MdiXml class="group-hover:(!text-white)" @click.stop />
</div>
</a-tooltip>
<div v-if="!isForm" class="dot" />
</template>
</Toolbar>
<div v-if="isOpen" class="flex-1 flex flex-col">
<MenuTop @open-modal="openModal" @deleted="loadViews" @sorted="loadViews" /> <MenuTop @open-modal="openModal" @deleted="loadViews" @sorted="loadViews" />
<a-divider v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="my-2" /> <div v-if="isUIAllowed('virtualViewsCreateOrEdit')" class="px-3">
<div class="!my-3 w-full border-b-1 border-dashed" />
</div>
<MenuBottom @open-modal="openModal" /> <MenuBottom @open-modal="openModal" />
</div> </div>
@ -178,8 +133,4 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
:deep(.ant-layout-sider-children) { :deep(.ant-layout-sider-children) {
@apply flex flex-col; @apply flex flex-col;
} }
.dot {
@apply w-[3px] h-[3px] bg-gray-300 rounded-full;
}
</style> </style>

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

@ -1,20 +0,0 @@
<script setup lang="ts">
const emits = defineEmits(['addRow'])
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const isLocked = inject(IsLockedInj)
</script>
<template>
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'">
<template #title> {{ $t('activity.addRow') }} </template>
<div
:class="{ 'hover:after:bg-primary/75 group': !isLocked, 'disabled-ring': isLocked }"
class="nc-sidebar-right-item nc-sidebar-add-row"
>
<MdiPlusOutline
:class="{ 'cursor-pointer group-hover:(!text-white)': !isLocked, 'disabled': isLocked }"
@click="!isLocked ? emits('addRow') : {}"
/>
</div>
</a-tooltip>
</template>

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

@ -1,116 +0,0 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { computed, useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
enum LockType {
Personal = 'personal',
Locked = 'locked',
Collaborative = 'collaborative',
}
const { view, $api } = useSmartsheetStoreOrThrow()
const { $e } = useNuxtApp()
async function changeLockType(type: LockType) {
$e('a:grid:lockmenu', { lockType: type })
if (type === 'personal') {
return message.info('Coming soon')
}
try {
;(view.value as any).lock_type = type
$api.dbView.update(view.value.id as string, {
lock_type: type,
})
message.success(`Successfully Switched to ${type} view`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
const Icon = computed(() => {
switch ((view.value as any)?.lock_type) {
case LockType.Personal:
return MdiAccountIcon
case LockType.Locked:
return MdiLockOutlineIcon
case LockType.Collaborative:
default:
return MdiAccountGroupIcon
}
})
</script>
<template>
<a-dropdown max-width="350" :trigger="['click']">
<div class="nc-sidebar-right-item hover:after:bg-indigo-500 group nc-sidebar-lock-menu">
<Icon class="cursor-pointer group-hover:(!text-white)" />
</div>
<template #overlay>
<div class="min-w-[350px] max-w-[500px] shadow bg-white">
<div>
<div class="nc-menu-item" @click="changeLockType(LockType.Collaborative)">
<div>
<MdiCheck v-if="!view?.lock_type || view?.lock_type === LockType.Collaborative" />
<span v-else />
<div>
<MdiAccountGroupIcon />
Collaborative view
<div class="nc-subtitle">Collaborators with edit permissions or higher can change the view configuration.</div>
</div>
</div>
</div>
<div class="nc-menu-item" @click="changeLockType(LockType.Locked)">
<div>
<MdiCheck v-if="view.lock_type === LockType.Locked" />
<span v-else />
<div>
<MdiLockOutlineIcon />
Locked View
<div class="nc-subtitle">No one can edit the view configuration until it is unlocked.</div>
</div>
</div>
</div>
<div class="nc-menu-item" @click="changeLockType(LockType.Personal)">
<div>
<MdiCheck v-if="view.lock_type === LockType.Personal" />
<span v-else />
<div>
<MdiAccountIcon />
Personal view
<div class="nc-subtitle">
Only you can edit the view configuration. Other collaborators personal views are hidden by default.
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</a-dropdown>
</template>
<style scoped>
.nc-menu-item > div {
@apply grid grid-cols-[30px,auto] gap-2 p-2 align-center;
}
.nc-menu-item > div > svg {
align-self: center;
}
.nc-menu-option > :first-child {
@apply align-self-center;
}
.nc-subtitle {
@apply font-size-sm font-weight-light;
}
</style>

15
packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue

@ -0,0 +1,15 @@
<script setup lang="ts">
/** Sidebar visible */
const { isOpen, toggle } = useSidebar({ storageKey: 'nc-right-sidebar' })
</script>
<template>
<div :class="{ 'nc-active-btn': isOpen }">
<a-button size="small" @click="toggle(!isOpen)">
<div class="flex items-center gap-1 text-xs" :class="{ 'text-gray-500': !isOpen }">
<MdiMenu class="!text-xs" />
Views
</div>
</a-button>
</div>
</template>

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

@ -1,14 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import AddRow from './AddRow.vue'
import LockMenu from './LockMenu.vue'
import Reload from './Reload.vue'
import ExportCache from './ExportCache.vue' import ExportCache from './ExportCache.vue'
import DeleteCache from './DeleteCache.vue' import DeleteCache from './DeleteCache.vue'
import DebugMeta from './DebugMeta.vue' import DebugMeta from './DebugMeta.vue'
import ToggleDrawer from './ToggleDrawer.vue'
import { IsFormInj } from '#imports' import { IsFormInj } from '#imports'
const { isUIAllowed } = useUIPermission()
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
const debug = $ref(false) const debug = $ref(false)
@ -19,7 +15,7 @@ const clickCount = $ref(0)
<template> <template>
<div <div
v-if="!isForm" v-if="!isForm"
class="flex gap-2" class="flex gap-2 justify-start"
@click=" @click="
() => { () => {
clickCount = clickCount + 1 clickCount = clickCount + 1
@ -28,7 +24,8 @@ const clickCount = $ref(0)
" "
> >
<slot name="start" /> <slot name="start" />
<ToggleDrawer />
<span></span>
<template v-if="debug"> <template v-if="debug">
<ExportCache /> <ExportCache />
@ -43,16 +40,6 @@ const clickCount = $ref(0)
<div class="dot" /> <div class="dot" />
</template> </template>
<LockMenu v-if="isUIAllowed('view-type')" @click.stop />
<div v-if="isUIAllowed('view-type')" class="dot" />
<Reload @click.stop />
<div class="dot" />
<AddRow v-if="isUIAllowed('xcDatatableEditable')" @click.stop />
<slot name="end" /> <slot name="end" />
</div> </div>
<div v-else> <div v-else>

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

@ -26,7 +26,7 @@ const selectedTab = $computed(() => tabsInfo[selectedTabKey])
<template> <template>
<div> <div>
<a-tabs v-model:active-key="selectedTabKey" :open-keys="[]" mode="horizontal" class="nc-auth-tabs mx-6"> <a-tabs v-model:active-key="selectedTabKey" :open-keys="[]" mode="horizontal" class="nc-auth-tabs !mx-6">
<a-tab-pane v-for="(tab, key) of tabsInfo" :key="key" class="select-none"> <a-tab-pane v-for="(tab, key) of tabsInfo" :key="key" class="select-none">
<template #tab> <template #tab>
<span> <span>

16
packages/nc-gui-v2/components/tabs/Smartsheet.vue

@ -8,6 +8,7 @@ import {
IsFormInj, IsFormInj,
IsLockedInj, IsLockedInj,
MetaInj, MetaInj,
OpenNewRecordFormHookInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
TabMetaInj, TabMetaInj,
computed, computed,
@ -42,6 +43,7 @@ watchEffect(async () => {
}) })
const reloadEventHook = createEventHook<void>() const reloadEventHook = createEventHook<void>()
const openNewRecordFormHook = createEventHook<void>()
const { isGallery, isGrid, isForm, isLocked } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta) const { isGallery, isGrid, isForm, isLocked } = useProvideSmartsheetStore(activeView as Ref<TableType>, meta)
@ -54,6 +56,7 @@ provide(TabMetaInj, tabMeta)
provide(ActiveViewInj, activeView) provide(ActiveViewInj, activeView)
provide(IsLockedInj, isLocked) provide(IsLockedInj, isLocked)
provide(ReloadViewDataHookInj, reloadEventHook) provide(ReloadViewDataHookInj, reloadEventHook)
provide(OpenNewRecordFormHookInj, openNewRecordFormHook)
provide(FieldsInj, fields) provide(FieldsInj, fields)
provide(IsFormInj, isForm) provide(IsFormInj, isForm)
@ -73,7 +76,7 @@ watch(isLocked, (nextValue) => (treeViewIsLockedInj.value = nextValue), { immedi
<template v-if="meta"> <template v-if="meta">
<div class="flex flex-1 min-h-0"> <div class="flex flex-1 min-h-0">
<div v-if="activeView" class="h-full flex-grow min-w-0 min-h-0"> <div v-if="activeView" class="h-full flex-1 min-w-0 min-h-0 bg-gray-50">
<SmartsheetGrid v-if="isGrid" :ref="el" /> <SmartsheetGrid v-if="isGrid" :ref="el" />
<SmartsheetGallery v-else-if="isGallery" /> <SmartsheetGallery v-else-if="isGallery" />
@ -81,11 +84,14 @@ watch(isLocked, (nextValue) => (treeViewIsLockedInj.value = nextValue), { immedi
<SmartsheetForm v-else-if="isForm" /> <SmartsheetForm v-else-if="isForm" />
</div> </div>
</div> </div>
<teleport to="#content">
<SmartsheetSidebar />
</teleport>
</template> </template>
</div> </div>
<SmartsheetSidebar v-if="meta" class="nc-right-sidebar" />
</div> </div>
</template> </template>
<style scoped>
:deep(.nc-right-sidebar.ant-layout-sider-collapsed) {
@apply !w-0 !max-w-0 !min-w-0 overflow-x-hidden;
}
</style>

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

@ -7,13 +7,16 @@ import {
MetaInj, MetaInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
computed, computed,
createEventHook,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
fieldRequiredValidator, fieldRequiredValidator,
getUIDTIcon, getUIDTIcon,
inject,
nextTick, nextTick,
onMounted, onMounted,
reactive, reactive,
ref, ref,
useNuxtApp,
useProject, useProject,
useTabs, useTabs,
useTemplateRefsList, useTemplateRefsList,
@ -38,16 +41,20 @@ const { quickImportType, projectTemplate, importData, importColumns, importOnly,
const emit = defineEmits(['import']) const emit = defineEmits(['import'])
const meta = inject(MetaInj) const meta = inject(MetaInj, ref({} as TableType))
const columns = computed(() => meta?.value?.columns || []) const columns = computed(() => meta.value?.columns || [])
const reloadHook = inject(ReloadViewDataHookInj)! const reloadHook = inject(ReloadViewDataHookInj, createEventHook())
const useForm = Form.useForm const useForm = Form.useForm
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { addTab } = useTabs()
const { sqlUi, project, loadTables } = useProject()
const hasSelectColumn = ref<boolean[]>([]) const hasSelectColumn = ref<boolean[]>([])
const expansionPanel = ref<number[]>([]) const expansionPanel = ref<number[]>([])
@ -75,24 +82,14 @@ const uiTypeOptions = ref<Option[]>(
})), })),
) )
const data = reactive<{ title: string | null; name: string; tables: TableType[] }>({ const srcDestMapping = ref<Record<string, any>[]>([])
const data = reactive<{ title: string | null; name: string; tables: (TableType & { ref_table_name: string })[] }>({
title: null, title: null,
name: 'Project Name', name: 'Project Name',
tables: [], tables: [],
}) })
const { addTab } = useTabs()
const { sqlUi, project, loadTables } = useProject()
onMounted(() => {
parseAndLoadTemplate()
nextTick(() => {
inputRefs.value[0]?.focus()
})
})
const validators = computed(() => const validators = computed(() =>
data.tables.reduce<Record<string, [typeof fieldRequiredValidator]>>((acc, table, tableIdx) => { data.tables.reduce<Record<string, [typeof fieldRequiredValidator]>>((acc, table, tableIdx) => {
acc[`tables.${tableIdx}.table_name`] = [fieldRequiredValidator] acc[`tables.${tableIdx}.table_name`] = [fieldRequiredValidator]
@ -110,10 +107,35 @@ const validators = computed(() =>
}, {}), }, {}),
) )
const srcDestMapping = ref<Record<string, any>[]>([])
const { validate, validateInfos } = useForm(data, validators) const { validate, validateInfos } = useForm(data, validators)
const isValid = computed(() => {
if (importOnly) {
for (const record of srcDestMapping.value) {
if (!fieldsValidation(record)) {
return false
}
}
} else {
for (const [_, o] of Object.entries(validateInfos)) {
if (o?.validateStatus) {
if (o.validateStatus === 'error') {
return false
}
}
}
}
return true
})
onMounted(() => {
parseAndLoadTemplate()
nextTick(() => {
inputRefs.value[0]?.focus()
})
})
function filterOption(input: string, option: Option) { function filterOption(input: string, option: Option) {
return option.value.toUpperCase().includes(input.toUpperCase()) return option.value.toUpperCase().includes(input.toUpperCase())
} }
@ -121,7 +143,9 @@ function filterOption(input: string, option: Option) {
function parseAndLoadTemplate() { function parseAndLoadTemplate() {
if (projectTemplate) { if (projectTemplate) {
parseTemplate(projectTemplate) parseTemplate(projectTemplate)
expansionPanel.value = Array.from({ length: data.tables.length || 0 }, (_, i) => i) expansionPanel.value = Array.from({ length: data.tables.length || 0 }, (_, i) => i)
hasSelectColumn.value = Array.from({ length: data.tables.length || 0 }, () => false) hasSelectColumn.value = Array.from({ length: data.tables.length || 0 }, () => false)
} }
} }
@ -145,6 +169,7 @@ function parseTemplate({ tables = [], ...rest }: Props['projectTemplate']) {
], ],
})), })),
} }
Object.assign(data, parsedTemplate) Object.assign(data, parsedTemplate)
} }
@ -166,8 +191,10 @@ function addNewColumnRow(table: Record<string, any>, uidt?: string) {
column_name: `title${table.columns.length + 1}`, column_name: `title${table.columns.length + 1}`,
uidt, uidt,
}) })
nextTick(() => { nextTick(() => {
const input = inputRefs.value[table.columns.length - 1] const input = inputRefs.value[table.columns.length - 1]
input.focus() input.focus()
input.select() input.select()
}) })
@ -194,10 +221,12 @@ function missingRequiredColumnsValidation() {
(c: Record<string, any>) => (c: Record<string, any>) =>
(c.pk ? !c.ai && !c.cdf : !c.cdf && c.rqd) && !srcDestMapping.value.some((r) => r.destCn === c.title), (c.pk ? !c.ai && !c.cdf : !c.cdf && c.rqd) && !srcDestMapping.value.some((r) => r.destCn === c.title),
) )
if (missingRequiredColumns.length) { if (missingRequiredColumns.length) {
message.error(`Following columns are required : ${missingRequiredColumns.map((c) => c.title).join(', ')}`) message.error(`Following columns are required : ${missingRequiredColumns.map((c) => c.title).join(', ')}`)
return false return false
} }
return true return true
} }
@ -206,6 +235,7 @@ function atLeastOneEnabledValidation() {
message.error('At least one column has to be selected') message.error('At least one column has to be selected')
return false return false
} }
return true return true
} }
@ -252,6 +282,7 @@ function fieldsValidation(record: Record<string, any>) {
message.error('Source data contains some invalid numbers') message.error('Source data contains some invalid numbers')
return false return false
} }
break break
case UITypes.Checkbox: case UITypes.Checkbox:
if ( if (
@ -271,16 +302,20 @@ function fieldsValidation(record: Record<string, any>) {
input === '1' input === '1'
) )
} }
return input !== 1 && input !== 0 && input !== true && input !== false return input !== 1 && input !== 0 && input !== true && input !== false
} }
return false return false
}) })
) { ) {
message.error('Source data contains some invalid boolean values') message.error('Source data contains some invalid boolean values')
return false return false
} }
break break
} }
return true return true
} }
@ -288,24 +323,33 @@ async function importTemplate() {
if (importOnly) { if (importOnly) {
// validate required columns // validate required columns
if (!missingRequiredColumnsValidation()) return if (!missingRequiredColumnsValidation()) return
// validate at least one column needs to be selected // validate at least one column needs to be selected
if (!atLeastOneEnabledValidation()) return if (!atLeastOneEnabledValidation()) return
try { try {
isImporting.value = true isImporting.value = true
const tableName = meta?.value.title as string
const tableName = meta.value.title
const data = importData[tableName] const data = importData[tableName]
const projectName = project.value.title as string
const projectName = project.value.title!
const total = data.length const total = data.length
for (let i = 0, progress = 0; i < total; i += maxRowsToParse) { for (let i = 0, progress = 0; i < total; i += maxRowsToParse) {
const batchData = data.slice(i, i + maxRowsToParse).map((row: Record<string, any>) => const batchData = data.slice(i, i + maxRowsToParse).map((row: Record<string, any>) =>
srcDestMapping.value.reduce((res: Record<string, any>, col: Record<string, any>) => { srcDestMapping.value.reduce((res: Record<string, any>, col: Record<string, any>) => {
if (col.enabled && col.destCn) { if (col.enabled && col.destCn) {
const v = columns.value.find((c: Record<string, any>) => c.title === col.destCn) as Record<string, any> const v = columns.value.find((c: Record<string, any>) => c.title === col.destCn) as Record<string, any>
let input = row[col.srcCn] let input = row[col.srcCn]
// parse potential boolean values // parse potential boolean values
if (v.uidt === UITypes.Checkbox) { if (v.uidt === UITypes.Checkbox) {
input = input.replace(/["']/g, '').toLowerCase().trim() input = input.replace(/["']/g, '').toLowerCase().trim()
if (input === 'false' || input === 'no' || input === 'n') { if (input === 'false' || input === 'no' || input === 'n') {
input = '0' input = '0'
} else if (input === 'true' || input === 'yes' || input === 'y') { } else if (input === 'true' || input === 'yes' || input === 'y') {
@ -325,8 +369,11 @@ async function importTemplate() {
return res return res
}, {}), }, {}),
) )
await $api.dbTableRow.bulkCreate('noco', projectName, tableName, batchData) await $api.dbTableRow.bulkCreate('noco', projectName, tableName, batchData)
importingTip.value = `Importing data to ${projectName}: ${progress}/${total} records` importingTip.value = `Importing data to ${projectName}: ${progress}/${total} records`
progress += batchData.length progress += batchData.length
} }
@ -421,6 +468,7 @@ async function importTemplate() {
} }
// reload table list // reload table list
await loadTables() await loadTables()
addTab({ addTab({
...tab, ...tab,
type: TabType.TABLE, type: TabType.TABLE,
@ -433,25 +481,6 @@ async function importTemplate() {
} }
} }
const isValid = computed(() => {
if (importOnly) {
for (const record of srcDestMapping.value) {
if (!fieldsValidation(record)) {
return false
}
}
} else {
for (const [_, o] of Object.entries(validateInfos)) {
if (o?.validateStatus) {
if (o.validateStatus === 'error') {
return false
}
}
}
}
return true
})
function mapDefaultColumns() { function mapDefaultColumns() {
srcDestMapping.value = [] srcDestMapping.value = []
for (const col of importColumns[0]) { for (const col of importColumns[0]) {
@ -645,6 +674,7 @@ onMounted(() => {
<mdi-key-star class="text-lg" /> <mdi-key-star class="text-lg" />
</div> </div>
</a-tooltip> </a-tooltip>
<a-tooltip v-else> <a-tooltip v-else>
<template #title> <template #title>
<!-- TODO: i18n --> <!-- TODO: i18n -->
@ -660,16 +690,17 @@ onMounted(() => {
</template> </template>
</template> </template>
</a-table> </a-table>
<div class="text-center mt-5">
<div class="mt-5 flex gap-2 justify-center">
<a-tooltip bottom> <a-tooltip bottom>
<template #title> <template #title>
<!-- TODO: i18n --> <!-- TODO: i18n -->
<span>Add Number Column</span> <span>Add Number Column</span>
</template> </template>
<a-button @click="addNewColumnRow(table, 'Number')"> <a-button class="group" @click="addNewColumnRow(table, 'Number')">
<div class="flex items-center"> <div class="flex items-center">
<mdi-numeric class="text-lg" /> <mdi-numeric class="group-hover:!text-accent flex text-lg" />
</div> </div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -679,9 +710,10 @@ onMounted(() => {
<!-- TODO: i18n --> <!-- TODO: i18n -->
<span>Add SingleLineText Column</span> <span>Add SingleLineText Column</span>
</template> </template>
<a-button @click="addNewColumnRow(table, 'SingleLineText')">
<a-button class="group" @click="addNewColumnRow(table, 'SingleLineText')">
<div class="flex items-center"> <div class="flex items-center">
<mdi-alpha-a class="text-lg" /> <mdi-alpha-a class="group-hover:!text-accent text-lg" />
</div> </div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -691,9 +723,10 @@ onMounted(() => {
<!-- TODO: i18n --> <!-- TODO: i18n -->
<span>Add LongText Column</span> <span>Add LongText Column</span>
</template> </template>
<a-button @click="addNewColumnRow(table, 'LongText')">
<a-button class="group" @click="addNewColumnRow(table, 'LongText')">
<div class="flex items-center"> <div class="flex items-center">
<mdi-text class="text-lg" /> <mdi-text class="group-hover:!text-accent text-lg" />
</div> </div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -703,10 +736,10 @@ onMounted(() => {
<!-- TODO: i18n --> <!-- TODO: i18n -->
<span>Add Other Column</span> <span>Add Other Column</span>
</template> </template>
<a-button @click="addNewColumnRow(table, 'SingleLineText')">
<div class="flex items-center"> <a-button class="group" @click="addNewColumnRow(table, 'SingleLineText')">
<mdi-plus class="text-lg" /> <div class="flex items-center gap-1">
Column <mdi-plus class="group-hover:!text-accent text-lg" />
</div> </div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -728,7 +761,7 @@ onMounted(() => {
@apply bg-white; @apply bg-white;
} }
:deep(.template-form-row) > td { :deep(.template-form-row) > td {
@apply pa-0 mb-0; @apply p-0 mb-0;
.ant-form-item { .ant-form-item {
@apply mb-0; @apply mb-0;
} }

15
packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue

@ -13,6 +13,7 @@ import {
ref, ref,
useProvideLTARStore, useProvideLTARStore,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
import MdiArrowExpand from '~icons/mdi/arrow-expand' import MdiArrowExpand from '~icons/mdi/arrow-expand'
import MdiPlus from '~icons/mdi/plus' import MdiPlus from '~icons/mdi/plus'
@ -31,13 +32,16 @@ const row = inject(RowInj)!
const active = inject(ActiveCellInj)! const active = inject(ActiveCellInj)!
const readonly = inject(ReadonlyInj, false) const readOnly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
const { isUIAllowed } = useUIPermission()
const listItemsDlg = ref(false) const listItemsDlg = ref(false)
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore( const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
@ -68,13 +72,16 @@ const unlinkRef = async (rec: Record<string, any>) => {
</script> </script>
<template> <template>
<div class="flex w-full chips-wrapper align-center" :class="{ active }"> <div class="flex w-full chips-wrapper items-center" :class="{ active }">
<div class="chips d-flex align-center flex-grow"> <div class="chips flex items-center flex-1">
<template v-if="value && relatedTablePrimaryValueProp"> <template v-if="value && relatedTablePrimaryValueProp">
<ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" /> <ItemChip :item="value" :value="value[relatedTablePrimaryValueProp]" @unlink="unlinkRef(value)" />
</template> </template>
</div> </div>
<div v-if="!readonly || !isLocked" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center"> <div
v-if="!readOnly && !isLocked && isUIAllowed('xcDatatableEditable')"
class="flex justify-end gap-1 min-h-[30px] items-center"
>
<component <component
:is="addIcon" :is="addIcon"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500) nc-plus" class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 select-none group-hover:(text-gray-500) nc-plus"

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

@ -32,7 +32,7 @@ const urls = computed(() => replaceUrlsWithLink(result.value))
<span>ERR!</span> <span>ERR!</span>
</a-tooltip> </a-tooltip>
<div class="pa-2" @dblclick="showEditFormulaWarningMessage"> <div class="p-2" @dblclick="showEditFormulaWarningMessage">
<div v-if="urls" v-html="urls" /> <div v-if="urls" v-html="urls" />
<div v-else>{{ result }}</div> <div v-else>{{ result }}</div>
<div v-if="showEditFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100]"> <div v-if="showEditFormulaWarning" class="text-left text-wrap mt-2 text-[#e65100]">

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

@ -15,6 +15,7 @@ import {
ref, ref,
useProvideLTARStore, useProvideLTARStore,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue')) const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
@ -33,7 +34,7 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
const readonly = inject(ReadonlyInj, false) const readOnly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
@ -41,7 +42,10 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore( const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
row, row,
@ -81,9 +85,9 @@ const unlinkRef = async (rec: Record<string, any>) => {
</script> </script>
<template> <template>
<div class="flex align-center items-center gap-1 w-full chips-wrapper"> <div class="flex items-center items-center gap-1 w-full chips-wrapper">
<template v-if="!isForm"> <template v-if="!isForm">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden"> <div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells"> <template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" /> <ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" />
<span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true"> <span v-if="cellValue?.length === 10" class="caption pointer ml-1 grey--text" @click="childListDlg = true">
@ -91,13 +95,13 @@ const unlinkRef = async (rec: Record<string, any>) => {
</span> </span>
</template> </template>
</div> </div>
<div v-if="!isLocked" class="flex-grow flex justify-end gap-1 min-h-[30px] align-center"> <div v-if="!isLocked && isUIAllowed('xcDatatableEditable')" class="flex justify-end gap-1 min-h-[30px] items-center">
<MdiArrowExpand <MdiArrowExpand
class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" class="select-none transform text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click="childListDlg = true" @click="childListDlg = true"
/> />
<MdiPlus <MdiPlus
v-if="!readonly" v-if="!readOnly"
class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus" class="select-none text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click="listItemsDlg = true" @click="listItemsDlg = true"
/> />

13
packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue

@ -14,6 +14,7 @@ import {
ref, ref,
useProvideLTARStore, useProvideLTARStore,
useSmartsheetRowStoreOrThrow, useSmartsheetRowStoreOrThrow,
useUIPermission,
} from '#imports' } from '#imports'
const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue')) const ItemChip = defineAsyncComponent(() => import('./components/ItemChip.vue'))
@ -32,7 +33,7 @@ const reloadTrigger = inject(ReloadViewDataHookInj)!
const isForm = inject(IsFormInj) const isForm = inject(IsFormInj)
const readonly = inject(ReadonlyInj, false) const readOnly = inject(ReadonlyInj, false)
const isLocked = inject(IsLockedInj) const isLocked = inject(IsLockedInj)
@ -40,6 +41,8 @@ const listItemsDlg = ref(false)
const childListDlg = ref(false) const childListDlg = ref(false)
const { isUIAllowed } = useUIPermission()
const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow() const { state, isNew, removeLTARRef } = useSmartsheetRowStoreOrThrow()
const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore( const { loadRelatedTableMeta, relatedTablePrimaryValueProp, unlink } = useProvideLTARStore(
column as Ref<Required<ColumnType>>, column as Ref<Required<ColumnType>>,
@ -81,9 +84,9 @@ const unlinkRef = async (rec: Record<string, any>) => {
</script> </script>
<template> <template>
<div class="flex align-center gap-1 w-full h-full chips-wrapper"> <div class="flex items-center gap-1 w-full h-full chips-wrapper">
<template v-if="!isForm"> <template v-if="!isForm">
<div class="chips flex align-center img-container flex-grow hm-items flex-nowrap min-w-0 overflow-hidden"> <div class="chips flex items-center img-container flex-1 hm-items flex-nowrap min-w-0 overflow-hidden">
<template v-if="cells"> <template v-if="cells">
<ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" /> <ItemChip v-for="(cell, i) of cells" :key="i" :item="cell.item" :value="cell.value" @unlink="unlinkRef(cell.item)" />
@ -91,14 +94,14 @@ const unlinkRef = async (rec: Record<string, any>) => {
</template> </template>
</div> </div>
<div v-if="!isLocked" class="flex-1 flex justify-end gap-1 min-h-[30px] align-center"> <div v-if="!isLocked && isUIAllowed('xcDatatableEditable')" class="flex justify-end gap-1 min-h-[30px] items-center">
<MdiArrowExpand <MdiArrowExpand
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand" class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-arrow-expand"
@click="childListDlg = true" @click="childListDlg = true"
/> />
<MdiPlus <MdiPlus
v-if="!readonly" v-if="!readOnly"
class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus" class="text-sm nc-action-icon text-gray-500/50 hover:text-gray-500 nc-plus"
@click="listItemsDlg = true" @click="listItemsDlg = true"
/> />

24
packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue

@ -1,5 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ActiveCellInj, IsFormInj, ReadonlyInj, defineAsyncComponent, inject, ref, useLTARStoreOrThrow } from '#imports' import {
ActiveCellInj,
IsFormInj,
ReadonlyInj,
defineAsyncComponent,
inject,
ref,
useLTARStoreOrThrow,
useUIPermission,
} from '#imports'
interface Props { interface Props {
value?: string | number | boolean value?: string | number | boolean
@ -14,7 +23,9 @@ const ExpandedForm: any = defineAsyncComponent(() => import('../../smartsheet/ex
const { relatedTableMeta } = useLTARStoreOrThrow()! const { relatedTableMeta } = useLTARStoreOrThrow()!
const readonly = inject(ReadonlyInj, false) const { isUIAllowed } = useUIPermission()
const readOnly = inject(ReadonlyInj, false)
const active = inject(ActiveCellInj, ref(false)) const active = inject(ActiveCellInj, ref(false))
@ -33,21 +44,21 @@ export default {
<template> <template>
<div <div
class="group py-1 px-2 mr-1 my-1 flex align-center bg-blue-100/60 hover:bg-blue-100/40 rounded-[2px]" class="chip group py-1 px-2 mr-1 my-1 flex items-center bg-blue-100/60 hover:bg-blue-100/40 rounded-[2px]"
:class="{ active }" :class="{ active }"
@click="expandedFormDlg = true" @click="expandedFormDlg = true"
> >
<span class="name">{{ value }}</span> <span class="name">{{ value }}</span>
<div v-show="active || isForm" v-if="!readonly && !isLocked" class="flex align-center"> <div v-show="active || isForm" v-if="!readOnly && !isLocked && isUIAllowed('xcDatatableEditable')" class="flex items-center">
<MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" /> <MdiCloseThick class="unlink-icon text-xs text-gray-500/50 group-hover:text-gray-500" @click.stop="emit('unlink')" />
</div> </div>
<Suspense> <Suspense>
<ExpandedForm <ExpandedForm
v-if="!readonly && !isLocked && expandedFormDlg" v-if="!readOnly && !isLocked && expandedFormDlg"
v-model="expandedFormDlg" v-model="expandedFormDlg"
:row="{ row: item }" :row="{ row: item, rowMeta: {}, oldRow: { ...item } }"
:meta="relatedTableMeta" :meta="relatedTableMeta"
load-row load-row
use-meta-fields use-meta-fields
@ -63,6 +74,7 @@ export default {
.name { .name {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap;
} }
} }
</style> </style>

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

@ -81,26 +81,26 @@ const expandedFormRow = ref()
</script> </script>
<template> <template>
<component :is="container" v-model:visible="vModel" :footer="null" title="Child list"> <component :is="container" v-model:visible="vModel" :footer="null" title="Child list" :body-style="{ padding: 0 }">
<div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col"> <div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col py-6">
<div class="flex mb-4 align-center gap-2"> <div class="flex mb-4 items-center gap-2 px-12">
<div class="flex-1" /> <div class="flex-1" />
<MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" @click="loadChildrenList" /> <MdiReload v-if="!isForm" class="cursor-pointer text-gray-500" @click="loadChildrenList" />
<a-button v-if="!readonly" type="primary" ghost class="!text-xs" size="small" @click="emit('attachRecord')"> <a-button v-if="!readonly" type="primary" ghost class="!text-xs" size="small" @click="emit('attachRecord')">
<div class="flex align-center gap-1"> <div class="flex items-center gap-1">
<MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" /> <MdiLinkVariantRemove class="text-xs" type="primary" @click="unlinkRow(row)" />
Link to '{{ meta.title }}' Link to '{{ meta.title }}'
</div> </div>
</a-button> </a-button>
</div> </div>
<template v-if="(isNew && state?.[column?.title]?.length) || childrenList?.pageInfo?.totalRows"> <template v-if="(isNew && state?.[column?.title]?.length) || childrenList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0"> <div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12">
<a-card <a-card
v-for="(row, i) of childrenList?.list ?? state?.[column?.title] ?? []" v-for="(row, i) of childrenList?.list ?? state?.[column?.title] ?? []"
:key="i" :key="i"
class="ma-2 hover:(!bg-gray-200/50 shadow-md)" class="!my-4 hover:(!bg-gray-200/50 shadow-md)"
@click=" @click="
() => { () => {
expandedFormRow = row expandedFormRow = row
@ -108,12 +108,11 @@ const expandedFormRow = ref()
} }
" "
> >
<div class="flex align-center"> <div class="flex items-center">
<div class="flex-grow overflow-hidden min-w-0"> <div class="flex-1 overflow-hidden min-w-0">
{{ row[relatedTablePrimaryValueProp] }} {{ row[relatedTablePrimaryValueProp] }}
<span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span> <span class="text-gray-400 text-[11px] ml-1">(Primary key : {{ getRelatedTableRowId(row) }})</span>
</div> </div>
<div class="flex-1"></div>
<div v-if="!readonly" class="flex gap-2"> <div v-if="!readonly" class="flex gap-2">
<MdiLinkVariantRemove <MdiLinkVariantRemove
class="text-xs text-grey hover:(!text-red-500) cursor-pointer" class="text-xs text-grey hover:(!text-red-500) cursor-pointer"
@ -128,15 +127,18 @@ const expandedFormRow = ref()
</div> </div>
</a-card> </a-card>
</div> </div>
<a-pagination
v-if="!isNew && childrenList?.pageInfo" <div class="flex justify-center mt-6">
v-model:current="childrenListPagination.page" <a-pagination
v-model:page-size="childrenListPagination.size" v-if="!isNew && childrenList?.pageInfo"
class="mt-2 mx-auto" v-model:current="childrenListPagination.page"
size="small" v-model:page-size="childrenListPagination.size"
:total="childrenList.pageInfo.totalRows" class="mt-2 mx-auto"
show-less-items size="small"
/> :total="childrenList.pageInfo.totalRows"
show-less-items
/>
</div>
</template> </template>
<a-empty <a-empty
v-else v-else

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

@ -97,9 +97,9 @@ const newRowState = computed(() => {
</script> </script>
<template> <template>
<a-modal v-model:visible="vModel" :footer="null" title="Link Record"> <a-modal v-model:visible="vModel" :footer="null" title="Link Record" :body-style="{ padding: 0 }">
<div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col"> <div class="max-h-[max(calc(100vh_-_300px)_,500px)] flex flex-col py-6">
<div class="flex mb-4 align-center gap-2"> <div class="flex mb-4 items-center gap-2 px-12">
<a-input <a-input
v-model:value="childrenExcludedListPagination.query" v-model:value="childrenExcludedListPagination.query"
placeholder="Filter query" placeholder="Filter query"
@ -111,11 +111,11 @@ const newRowState = computed(() => {
<a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true">Add new record</a-button> <a-button v-if="!isPublic" type="primary" size="small" @click="expandedFormDlg = true">Add new record</a-button>
</div> </div>
<template v-if="childrenExcludedList?.pageInfo?.totalRows"> <template v-if="childrenExcludedList?.pageInfo?.totalRows">
<div class="flex-1 overflow-auto min-h-0"> <div class="flex-1 overflow-auto min-h-0 scrollbar-thin-dull px-12">
<a-card <a-card
v-for="(refRow, i) in childrenExcludedList?.list ?? []" v-for="(refRow, i) in childrenExcludedList?.list ?? []"
:key="i" :key="i"
class="ma-2 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group" class="!my-4 cursor-pointer hover:(!bg-gray-200/50 shadow-md) group"
@click="linkRow(refRow)" @click="linkRow(refRow)"
> >
{{ refRow[relatedTablePrimaryValueProp] {{ refRow[relatedTablePrimaryValueProp]
@ -124,15 +124,17 @@ const newRowState = computed(() => {
> >
</a-card> </a-card>
</div> </div>
<a-pagination <div class="flex justify-center mt-6">
v-if="childrenExcludedList?.pageInfo" <a-pagination
v-model:current="childrenExcludedListPagination.page" v-if="childrenExcludedList?.pageInfo"
v-model:page-size="childrenExcludedListPagination.size" v-model:current="childrenExcludedListPagination.page"
class="mt-2 mx-auto !text-xs" v-model:page-size="childrenExcludedListPagination.size"
size="small" class="mt-2 !text-xs"
:total="childrenExcludedList.pageInfo.totalRows" size="small"
show-less-items :total="childrenExcludedList.pageInfo.totalRows"
/> show-less-items
/>
</div>
</template> </template>
<a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" /> <a-empty v-else class="my-10" :image="Empty.PRESENTED_IMAGE_SIMPLE" />

22
packages/nc-gui-v2/components/webhook/Drawer.vue

@ -28,17 +28,19 @@ async function editHook(hook: Record<string, any>) {
:closable="false" :closable="false"
placement="right" placement="right"
width="700px" width="700px"
:body-style="{ background: 'rgba(67, 81, 232, 0.05)', padding: '50px' }" :body-style="{ background: 'rgba(67, 81, 232, 0.05)', padding: '0px 0px', overflow: 'hidden' }"
@keydown.esc="vModel = false" @keydown.esc="vModel = false"
> >
<div> <a-layout class="">
<WebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" /> <a-layout-content class="px-10 py-5 scrollbar-thin-primary">
<WebhookList v-else @edit="editHook" @add="editOrAdd = true" /> <WebhookEditor v-if="editOrAdd" ref="webhookEditorRef" @back-to-list="editOrAdd = false" />
</div> <WebhookList v-else @edit="editHook" @add="editOrAdd = true" />
<div class="self-center flex flex-column flex-wrap gap-4 items-center mt-4 md:mx-8 md:justify-between justify-center"> </a-layout-content>
<a-button v-t="['e:hiring']" href="https://angel.co/company/nocodb" target="_blank" size="large"> <a-layout-footer class="!bg-white flex">
🚀 We are Hiring! 🚀 <a-button v-t="['e:hiring']" class="mx-auto mb-4" href="https://angel.co/company/nocodb" target="_blank" size="large">
</a-button> 🚀 We are Hiring! 🚀
</div> </a-button>
</a-layout-footer>
</a-layout>
</a-drawer> </a-drawer>
</template> </template>

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

@ -383,14 +383,14 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div class="mb-4"> <div class="flex">
<div class="float-left mt-2"> <div class="flex-1">
<div class="flex items-center"> <div class="flex items-center mt-2">
<MdiArrowLeftBold class="mr-3 text-xl cursor-pointer" @click="emit('backToList')" /> <MdiArrowLeftBold class="mr-3 text-xl cursor-pointer" @click="emit('backToList')" />
<span class="inline text-xl font-bold">{{ meta.title }} : {{ hook.title || 'Webhooks' }} </span> <span class="inline text-xl font-bold">{{ meta.title }} : {{ hook.title || 'Webhooks' }} </span>
</div> </div>
</div> </div>
<div class="float-right mb-5"> <div>
<a-button class="mr-3" size="large" @click="testWebhook"> <a-button class="mr-3" size="large" @click="testWebhook">
<div class="flex items-center"> <div class="flex items-center">
<MdiGestureDoubleTap class="mr-2" /> <MdiGestureDoubleTap class="mr-2" />

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

@ -49,22 +49,18 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="h-5/6"> <div class="">
<div class="mb-4"> <div class="mb-2">
<div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div> <div class="float-left font-bold text-xl mt-2 mb-4">{{ meta.title }} : Webhooks</div>
<a-button class="float-right" type="primary" size="large" @click="emit('add')"> <a-button class="float-right" type="primary" size="large" @click="emit('add')">
{{ $t('activity.addWebhook') }} {{ $t('activity.addWebhook') }}
</a-button> </a-button>
</div> </div>
<a-divider /> <a-divider />
<div v-if="hooks.length"> <div v-if="hooks.length" class="">
<a-list <a-list item-layout="horizontal" :data-source="hooks" class="cursor-pointer scrollbar-thin-primary">
item-layout="horizontal"
:data-source="hooks"
class="cursor-pointer max-h-[75vh] overflow-y-auto scrollbar-thin-primary"
>
<template #renderItem="{ item, index }"> <template #renderItem="{ item, index }">
<a-list-item class="pa-2" @click="emit('edit', item)"> <a-list-item class="p-2" @click="emit('edit', item)">
<a-list-item-meta> <a-list-item-meta>
<template #description> <template #description>
<span class="uppercase"> {{ item.event }} {{ item.operation }}</span> <span class="uppercase"> {{ item.event }} {{ item.operation }}</span>
@ -93,8 +89,10 @@ onMounted(() => {
</template> </template>
</a-list> </a-list>
</div> </div>
<div v-else class="pa-4 bg-gray-100 text-gray-600"> <div v-else class="min-h-[75vh]">
Webhooks list is empty, create new webhook by clicking 'Create webhook' button. <div class="p-4 bg-gray-100 text-gray-600">
Webhooks list is empty, create new webhook by clicking 'Create webhook' button.
</div>
</div> </div>
</div> </div>
</template> </template>

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

@ -1,7 +1,9 @@
export * from './useApi' export * from './useApi'
export * from './useDialog'
export * from './useGlobal' export * from './useGlobal'
export * from './useInjectionState' export * from './useInjectionState'
export * from './useSidebar' export * from './useSidebar'
export * from './useTheme'
export * from './useUIPermission' export * from './useUIPermission'
export * from './useAttachment' export * from './useAttachment'
export * from './useColors' export * from './useColors'

103
packages/nc-gui-v2/composables/useDialog/index.ts

@ -0,0 +1,103 @@
import type { DefineComponent, VNode } from '@vue/runtime-dom'
import { isVNode, render } from '@vue/runtime-dom'
import type { ComponentPublicInstance } from '@vue/runtime-core'
import { isClient } from '@vueuse/core'
import { createEventHook, h, ref, toReactive, tryOnScopeDispose, useNuxtApp, watch } from '#imports'
/**
* Programmatically create a component and attach it to the body (or a specific mount target), like a dialog or modal.
* This composable is not SSR friendly - it should be used only on the client.
*
* @param componentOrVNode The component to create and attach. Can be a VNode or a component definition.
* @param props The props to pass to the component.
* @param mountTarget The target to attach the component to. Defaults to the document body
*
* @example
* import { useDialog } from '#imports'
* import DlgQuickImport from '~/components/dlg/QuickImport.vue'
*
* function openQuickImportDialog(type: string) {
* // create a ref for showing/hiding the modal
* const isOpen = ref(true)
*
* const { close, vNode } = useDialog(DlgQuickImport, {
* 'modelValue': isOpen,
* 'importType': type,
* 'onUpdate:modelValue': closeDialog,
* })
*
* function closeDialog() {
* // hide the modal
* isOpen.value = false
*
* // debounce destroying the component, so the modal transition can finish
* close(1000)
* }
* }
*/
export function useDialog(
componentOrVNode: DefineComponent<any, any, any> | VNode,
props: NonNullable<Parameters<typeof h>[1]> = {},
mountTarget?: Element | ComponentPublicInstance,
) {
if (typeof document === 'undefined' || !isClient) {
console.warn('[useDialog]: Cannot use outside of browser!')
}
const closeHook = createEventHook<void>()
const mountedHook = createEventHook<void>()
const isMounted = $ref(false)
const domNode = document.createElement('div')
const vNodeRef = ref<VNode>()
mountTarget = mountTarget ? ('$el' in mountTarget ? (mountTarget.$el as HTMLElement) : mountTarget) : document.body
/** if specified, append vnode to mount target instead of document.body */
mountTarget.appendChild(domNode)
/** When props change, we want to re-render the element with the new prop values */
const stop = watch(
toReactive(props),
(reactiveProps) => {
const vNode = isVNode(componentOrVNode) ? componentOrVNode : h(componentOrVNode, reactiveProps)
vNode.appContext = useNuxtApp().vueApp._context
vNodeRef.value = vNode
render(vNode, domNode)
if (!isMounted) mountedHook.trigger()
},
{ deep: true, immediate: true, flush: 'post' },
)
/** When calling scope is disposed, destroy component */
tryOnScopeDispose(close)
/** destroy component, can be debounced */
function close(debounce = 0) {
setTimeout(() => {
stop()
render(null, domNode)
setTimeout(() => {
;(mountTarget as HTMLElement)!.removeChild(domNode)
}, 100)
closeHook.trigger()
}, debounce)
}
return {
close,
onClose: closeHook.on,
onMounted: mountedHook.on,
domNode,
vNode: vNodeRef,
}
}

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

@ -121,7 +121,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
return obj return obj
}, {} as Record<string, any>) }, {} as Record<string, any>)
if (row.value.rowMeta.new) { if (row.value.rowMeta?.new) {
data = await $api.dbTableRow.create('noco', project.value.title as string, meta.value.title, updateOrInsertObj) data = await $api.dbTableRow.create('noco', project.value.title as string, meta.value.title, updateOrInsertObj)
/* todo: /* todo:
@ -133,11 +133,12 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
} }
} }
} */ } */
row.value = {
Object.assign(row.value, {
row: data, row: data,
rowMeta: {}, rowMeta: {},
oldRow: { ...data }, oldRow: { ...data },
} })
/// todo: /// todo:
// await this.reload(); // await this.reload();

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

@ -1,11 +1,10 @@
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { Api } from 'nocodb-sdk'
import type { Actions, State } from './types' import type { Actions, State } from './types'
import { useNuxtApp } from '#imports'
export function useGlobalActions(state: State): Actions { export function useGlobalActions(state: State): Actions {
// todo replace with just `new Api()`? Would solve recursion issues /** detached api instance, will not trigger global loading */
/** we have to use the globally injected api instance, otherwise we run into recursion as `useApi` calls `useGlobal` */ const api = new Api()
const { $api } = useNuxtApp()
/** Sign out by deleting the token from localStorage */ /** Sign out by deleting the token from localStorage */
const signOut: Actions['signOut'] = () => { const signOut: Actions['signOut'] = () => {
@ -30,7 +29,7 @@ export function useGlobalActions(state: State): Actions {
/** manually try to refresh token */ /** manually try to refresh token */
const refreshToken = async () => { const refreshToken = async () => {
$api.instance api.instance
.post('/auth/refresh-token', null, { .post('/auth/refresh-token', null, {
withCredentials: true, withCredentials: true,
}) })

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

@ -18,6 +18,15 @@ export function useProject(projectId?: MaybeRef<string>) {
// todo: refactor path param name and variable name // todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string) const projectType = $computed(() => route.params.projectType as string)
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
const isMssql = computed(() => projectBaseType === 'mssql')
const isPg = computed(() => projectBaseType === 'pg')
const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
)
const isSharedBase = computed(() => projectType === 'base')
async function loadProjectMetaInfo(force?: boolean) { async function loadProjectMetaInfo(force?: boolean) {
if (!projectMetaInfo.value || force) { if (!projectMetaInfo.value || force) {
const data = await $api.project.metaGet(project.value.id!, {}, {}) const data = await $api.project.metaGet(project.value.id!, {}, {})
@ -28,7 +37,17 @@ export function useProject(projectId?: MaybeRef<string>) {
async function loadProjectRoles() { async function loadProjectRoles() {
projectRoles.value = {} projectRoles.value = {}
if (project.value.id) { if (isSharedBase.value) {
const user = await $api.auth.me(
{},
{
headers: {
'xc-shared-base-id': route.params.projectId,
},
},
)
projectRoles.value = user.roles
} else if (project.value.id) {
const user = await $api.auth.me({ project_id: project.value.id }) const user = await $api.auth.me({ project_id: project.value.id })
projectRoles.value = user.roles projectRoles.value = user.roles
} }
@ -37,8 +56,7 @@ export function useProject(projectId?: MaybeRef<string>) {
async function loadTables() { async function loadTables() {
if (project.value.id) { if (project.value.id) {
const tablesResponse = await $api.dbTable.list(project.value.id, { const tablesResponse = await $api.dbTable.list(project.value.id, {
// FIXME: type includeM2M: includeM2M.value,
includeM2M: includeM2M.value || '',
}) })
if (tablesResponse.list) tables.value = tablesResponse.list if (tablesResponse.list) tables.value = tablesResponse.list
} }
@ -58,16 +76,6 @@ export function useProject(projectId?: MaybeRef<string>) {
await loadTables() await loadTables()
} }
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
const isMssql = computed(() => projectBaseType === 'mssql')
const isPg = computed(() => projectBaseType === 'pg')
const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
)
const isSharedBase = computed(() => projectType === 'base')
return { return {
project, project,
tables, tables,

14
packages/nc-gui-v2/composables/useSmartsheetStore.ts

@ -1,10 +1,16 @@
import { ViewTypes } from 'nocodb-sdk' import { ViewTypes } from 'nocodb-sdk'
import type { TableType, ViewType } from 'nocodb-sdk' import type { FilterType, SortType, TableType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { computed, reactive, useInjectionState, useNuxtApp, useProject, useTemplateRefsList } from '#imports' import { computed, reactive, useInjectionState, useNuxtApp, useProject, useTemplateRefsList } from '#imports'
const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState( const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
(view: Ref<ViewType>, meta: Ref<TableType>, shared = false) => { (
view: Ref<ViewType>,
meta: Ref<TableType>,
shared = false,
initalSorts?: Ref<SortType[]>,
initialFilters?: Ref<FilterType[]>,
) => {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { sqlUi } = useProject() const { sqlUi } = useProject()
@ -37,6 +43,8 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
} }
return where return where
}) })
const sorts = initalSorts ?? ref<SortType[]>([])
const nestedFilters: Ref<FilterType[]> = initialFilters ?? ref<FilterType[]>([])
return { return {
view, view,
@ -51,6 +59,8 @@ const [useProvideSmartsheetStore, useSmartsheetStore] = useInjectionState(
isGallery, isGallery,
cellRefs, cellRefs,
isSharedForm, isSharedForm,
sorts,
nestedFilters,
} }
}, },
'smartsheet-store', 'smartsheet-store',

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

@ -1,16 +1,15 @@
import { Modal, message } from 'ant-design-vue' import { Modal, message } from 'ant-design-vue'
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 { useProject } from './useProject'
import { TabType } from '~/composables/useTabs'
import { extractSdkResponseErrorMsg } from '~/utils'
import { useNuxtApp } from '#app' import { useNuxtApp } from '#app'
import { TabType } from '~/composables/useTabs'
import { SYSTEM_COLUMNS, extractSdkResponseErrorMsg, useProject } from '#imports'
export function useTable(onTableCreate?: (tableMeta: TableType) => void) { export function useTable(onTableCreate?: (tableMeta: TableType) => void) {
const table = reactive<{ title: string; table_name: string; columns: string[] }>({ const table = reactive<{ title: string; table_name: string; columns: string[] }>({
title: '', title: '',
table_name: '', table_name: '',
columns: ['id', 'title', 'created_at', 'updated_at'], columns: SYSTEM_COLUMNS,
}) })
const { $e, $api } = useNuxtApp() const { $e, $api } = useNuxtApp()

60
packages/nc-gui-v2/composables/useTheme/index.ts

@ -0,0 +1,60 @@
import { ConfigProvider } from 'ant-design-vue'
import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider'
import { useStorage } from '@vueuse/core'
import { NOCO, hexToRGB, themeV2Colors, useCssVar, useInjectionState } from '#imports'
interface ThemeConfig extends AntTheme {
primaryColor: string
accentColor: string
}
const [setup, use] = useInjectionState((config?: Partial<ThemeConfig>) => {
const primaryColor = useCssVar('--color-primary', typeof document !== 'undefined' ? document.documentElement : null)
const accentColor = useCssVar('--color-accent', typeof document !== 'undefined' ? document.documentElement : null)
/** current theme config */
const currentTheme = useStorage<ThemeConfig>(
`${NOCO}db-theme`,
{
primaryColor: themeV2Colors['royal-blue'].DEFAULT,
accentColor: themeV2Colors.pink['500'],
},
localStorage,
{ mergeDefaults: true },
)
/** set initial config */
setTheme(config ?? currentTheme.value)
/** set theme (persists in localstorage) */
function setTheme(theme: Partial<ThemeConfig>) {
// convert hex colors to rgb values
if (theme.primaryColor) primaryColor.value = hexToRGB(theme.primaryColor)
if (theme.accentColor) accentColor.value = hexToRGB(theme.accentColor)
currentTheme.value = theme as ThemeConfig
ConfigProvider.config({
theme,
})
}
return {
theme: currentTheme,
setTheme,
}
}, 'theme')
export const provideTheme = setup
export function useTheme(config?: Partial<ThemeConfig>) {
const theme = use()
if (!theme) {
return setup(config)
} else {
if (config) theme.setTheme(config)
}
return theme
}

16
packages/nc-gui-v2/composables/useUIPermission/index.ts

@ -7,7 +7,7 @@ export function useUIPermission() {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({})) const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const getRoles = (skipPreviewAs = false) => { const baseRoles = computed(() => {
let userRoles = user.value?.roles || {} let userRoles = user.value?.roles || {}
// if string populate key-value paired object // if string populate key-value paired object
@ -19,24 +19,24 @@ export function useUIPermission() {
} }
// merge user role and project specific user roles // merge user role and project specific user roles
let roles = { const roles = {
...userRoles, ...userRoles,
...projectRoles.value, ...projectRoles.value,
} }
return roles
})
const isUIAllowed = (permission: Permission | string, skipPreviewAs = false) => {
let roles = baseRoles.value
if (previewAs.value && !skipPreviewAs) { if (previewAs.value && !skipPreviewAs) {
roles = { roles = {
[previewAs.value]: true, [previewAs.value]: true,
} }
} }
return roles return Object.entries<boolean>(roles).some(([role, hasRole]) => {
}
const isUIAllowed = (permission: Permission | string, skipPreviewAs = false) => {
return Object.entries<boolean>(getRoles(skipPreviewAs)).some(([role, hasRole]) => {
const rolePermission = rolePermissions[role as keyof typeof rolePermissions] as '*' | Record<Permission, true> const rolePermission = rolePermissions[role as keyof typeof rolePermissions] as '*' | Record<Permission, true>
return hasRole && (rolePermission === '*' || rolePermission?.[permission as Permission]) return hasRole && (rolePermission === '*' || rolePermission?.[permission as Permission])
}) })
} }

11
packages/nc-gui-v2/composables/useViewColumns.ts

@ -8,6 +8,7 @@ import type { Field } from '~/lib'
export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRef<TableType>, reloadData?: () => void) { export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRef<TableType>, reloadData?: () => void) {
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const fields = ref<Field[]>() const fields = ref<Field[]>()
const filterQuery = ref('') const filterQuery = ref('')
@ -16,10 +17,13 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { isSharedBase } = useProject()
const loadViewColumns = async () => { const loadViewColumns = async () => {
if (!meta || !view) return if (!meta || !view) return
let order = 1 let order = 1
if (view.value?.id) { if (view.value?.id) {
const data = (isPublic.value ? meta.value?.columns : await $api.dbViewColumn.list(view.value.id)) as any[] const data = (isPublic.value ? meta.value?.columns : await $api.dbViewColumn.list(view.value.id)) as any[]
@ -48,7 +52,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
} }
const showAll = async (ignoreIds?: any) => { const showAll = async (ignoreIds?: any) => {
if (isPublic.value) { if (isPublic.value || isSharedBase.value) {
fields.value = fields.value?.map((field: Field) => ({ fields.value = fields.value?.map((field: Field) => ({
...field, ...field,
show: true, show: true,
@ -71,7 +75,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
reloadData?.() reloadData?.()
} }
const hideAll = async (ignoreIds?: any) => { const hideAll = async (ignoreIds?: any) => {
if (isPublic.value) { if (isPublic.value || isSharedBase.value) {
fields.value = fields.value?.map((field: Field) => ({ fields.value = fields.value?.map((field: Field) => ({
...field, ...field,
show: false, show: false,
@ -140,7 +144,7 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
}, },
set(v: boolean) { set(v: boolean) {
if (view?.value?.id) { if (view?.value?.id) {
if (!isPublic.value) { if (!isPublic.value && !isSharedBase.value) {
$api.dbView $api.dbView
.update(view.value.id, { .update(view.value.id, {
show_system_fields: v, show_system_fields: v,
@ -200,5 +204,6 @@ export function useViewColumns(view: Ref<ViewType> | undefined, meta: ComputedRe
saveOrUpdate, saveOrUpdate,
sortedAndFilteredFields, sortedAndFilteredFields,
showSystemFields, showSystemFields,
metaColumnById,
} }
} }

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

@ -2,7 +2,7 @@ import type { Api, ColumnType, FormType, GalleryType, PaginatedType, TableType,
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app' import { useNuxtApp } from '#app'
import { IsPublicInj, NOCO, extractPkFromRow, extractSdkResponseErrorMsg, useProject } from '#imports' import { IsPublicInj, NOCO, extractPkFromRow, extractSdkResponseErrorMsg, useProject, useUIPermission } from '#imports'
const formatData = (list: Record<string, any>[]) => const formatData = (list: Record<string, any>[]) =>
list.map((row) => ({ list.map((row) => ({
@ -39,9 +39,11 @@ export function useViewData(
const formattedData = ref<Row[]>([]) const formattedData = ref<Row[]>([])
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const { project } = useProject() const { project, isSharedBase } = useProject()
const { fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView() const { fetchSharedViewData, paginationData: sharedPaginationData } = useSharedView()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
const { isUIAllowed } = useUIPermission()
const paginationData = computed({ const paginationData = computed({
get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value), get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value),
@ -81,8 +83,7 @@ export function useViewData(
/** load row comments count */ /** load row comments count */
const loadAggCommentsCount = async () => { const loadAggCommentsCount = async () => {
// todo: handle in public api if (isPublic.value || isSharedBase.value) return
if (isPublic.value) return
const ids = formattedData.value const ids = formattedData.value
?.filter(({ rowMeta: { new: isNew } }) => !isNew) ?.filter(({ rowMeta: { new: isNew } }) => !isNew)
@ -90,7 +91,7 @@ export function useViewData(
return extractPkFromRow(row, meta?.value?.columns as ColumnType[]) return extractPkFromRow(row, meta?.value?.columns as ColumnType[])
}) })
if (!ids?.length) return if (!ids?.length || ids?.some((id) => !id)) return
aggCommentCount.value = await $api.utils.commentCount({ aggCommentCount.value = await $api.utils.commentCount({
ids, ids,
@ -109,6 +110,8 @@ export function useViewData(
const response = !isPublic.value const response = !isPublic.value
? await $api.dbViewRow.list('noco', project.value.id!, meta.value.id!, viewMeta!.value.id, { ? await $api.dbViewRow.list('noco', project.value.id!, meta.value.id!, viewMeta!.value.id, {
...params, ...params,
...(isUIAllowed('sortSync') ? {} : { sortArrJson: JSON.stringify(sorts.value) }),
...(isUIAllowed('filterSync') ? {} : { filterArrJson: JSON.stringify(nestedFilters.value) }),
where: where?.value, where: where?.value,
}) })
: await fetchSharedViewData() : await fetchSharedViewData()
@ -214,6 +217,16 @@ export function useViewData(
oldRow: {}, oldRow: {},
rowMeta: { new: true }, rowMeta: { new: true },
}) })
return formattedData.value[addAfter]
}
const removeLastEmptyRow = () => {
const lastRow = formattedData.value[formattedData.value.length - 1]
if (lastRow.rowMeta.new) {
formattedData.value.pop()
}
} }
const deleteRowById = async (id: string) => { const deleteRowById = async (id: string) => {
@ -356,5 +369,6 @@ export function useViewData(
updateFormView, updateFormView,
aggCommentCount, aggCommentCount,
loadAggCommentsCount, loadAggCommentsCount,
removeLastEmptyRow,
} }
} }

190
packages/nc-gui-v2/composables/useViewFilters.ts

@ -1,6 +1,18 @@
import type { ViewType } from 'nocodb-sdk' import type { ViewType } from 'nocodb-sdk'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import { IsPublicInj, ReloadViewDataHookInj, useMetas, useNuxtApp, useUIPermission } from '#imports' import { message } from 'ant-design-vue'
import {
IsPublicInj,
ReloadViewDataHookInj,
computed,
extractSdkResponseErrorMsg,
inject,
ref,
useMetas,
useNuxtApp,
useUIPermission,
watch,
} from '#imports'
import type { Filter } from '~/lib' import type { Filter } from '~/lib'
export function useViewFilters( export function useViewFilters(
@ -8,33 +20,37 @@ export function useViewFilters(
parentId?: string, parentId?: string,
autoApply?: ComputedRef<boolean>, autoApply?: ComputedRef<boolean>,
reloadData?: () => void, reloadData?: () => void,
siblingFilters?: Filter[], currentFilters?: Filter[],
isNestedRoot?: boolean,
) { ) {
const { nestedFilters } = useSharedView()
const reloadHook = inject(ReloadViewDataHookInj) const reloadHook = inject(ReloadViewDataHookInj)
const _filters = ref<Filter[]>([]) const { nestedFilters } = useSmartsheetStoreOrThrow()
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { metas } = useMetas() const { metas } = useMetas()
const _filters = ref<Filter[]>([])
const nestedMode = computed(() => isPublic.value || !isUIAllowed('filterSync' || !isUIAllowed('filterChildrenRead')))
const filters = computed<Filter[]>({
get: () => (nestedMode.value ? currentFilters! : _filters.value),
set: (value: Filter[]) => {
if (nestedMode.value) {
currentFilters = value
if (isNestedRoot) nestedFilters.value = value
const filters = computed({
get: () => (isPublic.value ? siblingFilters || nestedFilters.value : _filters.value),
set: (value) => {
if (isPublic.value) {
if (siblingFilters) {
siblingFilters = value
} else {
nestedFilters.value = value
}
nestedFilters.value = [...nestedFilters.value] nestedFilters.value = [...nestedFilters.value]
reloadHook?.trigger() reloadHook?.trigger()
} else { return
_filters.value = value
} }
_filters.value = value
}, },
}) })
@ -46,54 +62,65 @@ export function useViewFilters(
} }
const loadFilters = async (hookId?: string) => { const loadFilters = async (hookId?: string) => {
if (isPublic.value) return if (nestedMode.value) return
if (hookId) { try {
if (parentId) { if (hookId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId) if (parentId) {
} else { filters.value = await $api.dbTableFilter.childrenRead(parentId)
filters.value = (await $api.dbTableWebhookFilter.read(hookId as string)) as any } else {
} filters.value = (await $api.dbTableWebhookFilter.read(hookId as string)) as any
} else { }
if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else { } else {
filters.value = await $api.dbTableFilter.read(view?.value?.id as string) if (parentId) {
filters.value = await $api.dbTableFilter.childrenRead(parentId)
} else {
filters.value = await $api.dbTableFilter.read(view?.value?.id as string)
}
} }
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
} }
} }
const sync = async (hookId?: string, _nested = false) => { const sync = async (hookId?: string, _nested = false) => {
for (const [i, filter] of Object.entries(filters.value)) { try {
if (filter.status === 'delete') { for (const [i, filter] of Object.entries(filters.value)) {
await $api.dbTableFilter.delete(filter.id as string) if (filter.status === 'delete') {
} else if (filter.status === 'update') { await $api.dbTableFilter.delete(filter.id as string)
await $api.dbTableFilter.update(filter.id as string, { } else if (filter.status === 'update') {
...filter, await $api.dbTableFilter.update(filter.id as string, {
fk_parent_id: parentId,
})
} else if (filter.status === 'create') {
if (hookId) {
filters.value[+i] = (await $api.dbTableWebhookFilter.create(hookId, {
...filter,
fk_parent_id: parentId,
})) as any
} else {
filters.value[+i] = (await $api.dbTableFilter.create(view?.value?.id as string, {
...filter, ...filter,
fk_parent_id: parentId, fk_parent_id: parentId,
})) as any })
} else if (filter.status === 'create') {
if (hookId) {
filters.value[+i] = (await $api.dbTableWebhookFilter.create(hookId, {
...filter,
fk_parent_id: parentId,
})) as any
} else {
filters.value[+i] = (await $api.dbTableFilter.create(view?.value?.id as string, {
...filter,
fk_parent_id: parentId,
})) as any
}
} }
} }
}
reloadData?.() reloadData?.()
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
}
} }
const deleteFilter = async (filter: Filter, i: number) => { const deleteFilter = async (filter: Filter, i: number) => {
// if shared or sync permission not allowed simply remove it from array // if shared or sync permission not allowed simply remove it from array
if (isPublic.value || !isUIAllowed('filterSync')) { if (nestedMode.value) {
filters.value.splice(i, 1) filters.value.splice(i, 1)
filters.value = [...filters.value]
reloadData?.() reloadData?.()
} else { } else {
if (filter.id) { if (filter.id) {
@ -102,9 +129,16 @@ export function useViewFilters(
filter.status = 'delete' filter.status = 'delete'
// if auto-apply enabled invoke delete api and remove from array // if auto-apply enabled invoke delete api and remove from array
} else { } else {
await $api.dbTableFilter.delete(filter.id) try {
reloadData?.() await $api.dbTableFilter.delete(filter.id)
filters.value.splice(i, 1)
reloadData?.()
filters.value.splice(i, 1)
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
}
} }
// if not synced yet remove it from array // if not synced yet remove it from array
} else { } else {
@ -114,34 +148,35 @@ export function useViewFilters(
} }
const saveOrUpdate = async (filter: Filter, i: number, force = false) => { const saveOrUpdate = async (filter: Filter, i: number, force = false) => {
if (isPublic.value) {
filters.value[i] = { ...filter } as any
filters.value = [...filters.value]
return
}
if (!view?.value) return if (!view?.value) return
if (!isUIAllowed('filterSync')) {
// skip try {
} else if (!autoApply?.value && !force) { if (nestedMode.value) {
filter.status = filter.id ? 'update' : 'create' filters.value[i] = { ...filter }
} else if (filter.id) { filters.value = [...filters.value]
await $api.dbTableFilter.update(filter.id, { } else if (!autoApply?.value && !force) {
...filter, filter.status = filter.id ? 'update' : 'create'
fk_parent_id: parentId, } else if (filter.id) {
}) await $api.dbTableFilter.update(filter.id, {
} else { ...filter,
// todo: return type of dbTableFilter is void? fk_parent_id: parentId,
filters.value[i] = (await $api.dbTableFilter.create(view?.value?.id as string, { })
...filter, } else {
fk_parent_id: parentId, // todo: return type of dbTableFilter is void?
})) as any filters.value[i] = (await $api.dbTableFilter.create(view?.value?.id as string, {
...filter,
fk_parent_id: parentId,
})) as any
}
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
} }
reloadData?.() reloadData?.()
} }
const addFilter = () => { const addFilter = () => filters.value.push(placeholderFilter)
filters.value.push(placeholderFilter)
}
const addFilterGroup = async () => { const addFilterGroup = async () => {
const child = placeholderFilter const child = placeholderFilter
@ -150,10 +185,13 @@ export function useViewFilters(
status: 'create', status: 'create',
logical_op: 'and', logical_op: 'and',
} }
if (isPublic.value) placeHolderGroupFilter.children = [child]
if (nestedMode.value) placeHolderGroupFilter.children = [child]
filters.value.push(placeHolderGroupFilter) filters.value.push(placeHolderGroupFilter)
const index = filters.value.length - 1 const index = filters.value.length - 1
await saveOrUpdate(filters.value[index], index, true) await saveOrUpdate(filters.value[index], index, true)
} }
@ -167,9 +205,7 @@ export function useViewFilters(
return metas?.value?.[view?.value?.fk_model_id as string]?.columns?.length || 0 return metas?.value?.[view?.value?.fk_model_id as string]?.columns?.length || 0
}, },
async (nextColsLength, oldColsLength) => { async (nextColsLength, oldColsLength) => {
if (nextColsLength < oldColsLength) { if (nextColsLength < oldColsLength) await loadFilters()
await loadFilters()
}
}, },
) )

73
packages/nc-gui-v2/composables/useViewSorts.ts

@ -1,32 +1,23 @@
import type { GalleryType, GridType, KanbanType, SortType } from 'nocodb-sdk' import type { GalleryType, GridType, KanbanType, SortType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { IsPublicInj, ReloadViewDataHookInj, useNuxtApp } from '#imports' import { message } from 'ant-design-vue'
import { IsPublicInj, ReloadViewDataHookInj, extractSdkResponseErrorMsg, useNuxtApp } from '#imports'
export function useViewSorts( export function useViewSorts(
view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined, view: Ref<(GridType | KanbanType | GalleryType) & { id?: string }> | undefined,
reloadData?: () => void, reloadData?: () => void,
) { ) {
const _sorts = ref<SortType[]>([]) const { sharedView } = useSharedView()
const { sorts: sharedViewSorts, sharedView } = useSharedView() const { sorts } = useSmartsheetStoreOrThrow()
const reloadHook = inject(ReloadViewDataHookInj) const reloadHook = inject(ReloadViewDataHookInj)
const isPublic = inject(IsPublicInj, ref(false))
const sorts = computed<SortType[]>({ const isPublic = inject(IsPublicInj, ref(false))
get: () => (isPublic.value ? sharedViewSorts.value : _sorts.value),
set: (value) => {
if (isPublic.value) {
sharedViewSorts.value = value
} else {
_sorts.value = value
}
reloadHook?.trigger()
},
})
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const { isUIAllowed } = useUIPermission() const { isUIAllowed } = useUIPermission()
const { isSharedBase } = useProject()
const loadSorts = async () => { const loadSorts = async () => {
if (isPublic.value) { if (isPublic.value) {
@ -34,27 +25,36 @@ export function useViewSorts(
sorts.value = [...sharedSorts] sorts.value = [...sharedSorts]
return return
} }
if (!view?.value) return
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list try {
if (!view?.value) return
sorts.value = ((await $api.dbTableSort.list(view?.value?.id as string)) as any)?.sorts?.list
} catch (e: any) {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
}
} }
const saveOrUpdate = async (sort: SortType, i: number) => { const saveOrUpdate = async (sort: SortType, i: number) => {
// TODO: if (isPublic.value || isSharedBase.value) {
// if (!this.shared && this._isUIAllowed('sortSync')) {
if (isPublic.value) {
sorts.value[i] = sort sorts.value[i] = sort
sorts.value = [...sorts.value] sorts.value = [...sorts.value]
return return
} }
if (isUIAllowed('sortSync')) { try {
if (sort.id) { if (isUIAllowed('sortSync')) {
await $api.dbTableSort.update(sort.id, sort) if (sort.id) {
} else { await $api.dbTableSort.update(sort.id, sort)
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any } else {
sorts.value[i] = (await $api.dbTableSort.create(view?.value?.id as string, sort)) as any
}
} }
reloadData?.()
} catch (e: any) {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
} }
reloadData?.()
} }
const addSort = () => { const addSort = () => {
sorts.value = [ sorts.value = [
@ -66,14 +66,21 @@ export function useViewSorts(
} }
const deleteSort = async (sort: SortType, i: number) => { const deleteSort = async (sort: SortType, i: number) => {
// TOOD: try {
// if (!this.shared && sort.id && this._isUIAllowed('sortSync')) { if (isUIAllowed('sortSync') && sort.id && !isPublic.value && !isSharedBase.value) {
await $api.dbTableSort.delete(sort.id)
if (isUIAllowed('sortSync') && sort.id && !isPublic.value) { }
await $api.dbTableSort.delete(sort.id) sorts.value.splice(i, 1)
sorts.value = [...sorts.value]
} catch (e: any) {
console.error(e)
message.error(await extractSdkResponseErrorMsg(e))
} }
sorts.value.splice(i, 1)
sorts.value = [...sorts.value]
} }
watch(sorts, () => {
reloadHook?.trigger()
})
return { sorts, loadSorts, addSort, deleteSort, saveOrUpdate } return { sorts, loadSorts, addSort, deleteSort, saveOrUpdate }
} }

1
packages/nc-gui-v2/context/index.ts

@ -21,6 +21,7 @@ export const CellValueInj: InjectionKey<Ref<any>> = Symbol('cell-value-injection
export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection') export const ActiveViewInj: InjectionKey<Ref<ViewType>> = Symbol('active-view-injection')
export const ReadonlyInj: InjectionKey<boolean> = Symbol('readonly-injection') export const ReadonlyInj: InjectionKey<boolean> = Symbol('readonly-injection')
export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection') export const ReloadViewDataHookInj: InjectionKey<EventHook<void>> = Symbol('reload-view-data-injection')
export const OpenNewRecordFormHookInj: InjectionKey<EventHook<void>> = Symbol('open-new-record-form-injection')
export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection') export const FieldsInj: InjectionKey<Ref<any[]>> = Symbol('fields-injection')
export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection') export const ViewListInj: InjectionKey<Ref<ViewType[]>> = Symbol('view-list-injection')
export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection') export const EditModeInj: InjectionKey<Ref<boolean>> = Symbol('edit-mode-injection')

2
packages/nc-gui-v2/lang/nl.json

@ -447,7 +447,7 @@
"importZip": "Import project meta zip-bestand en start opnieuw op.", "importZip": "Import project meta zip-bestand en start opnieuw op.",
"importText": "Import NocoDB-project door het uploaden van het metadata zip-bestand", "importText": "Import NocoDB-project door het uploaden van het metadata zip-bestand",
"metaNoChange": "Geen verandering gevonden", "metaNoChange": "Geen verandering gevonden",
"sqlMigration": "Schema-migraties worden automatisch gemaakt. Maak een tabel en vernieuw deze pagina.", "sqlMigration": "Schem-migraties worden automatisch gemaakt. Maak een tabel en vernieuw deze pagina.",
"dbConnectionStatus": "Omgeving gevalideerd", "dbConnectionStatus": "Omgeving gevalideerd",
"dbConnected": "Succesvolle verbinding", "dbConnected": "Succesvolle verbinding",
"notifications": { "notifications": {

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

@ -1,6 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import { navigateTo } from '#app' import { computed, navigateTo, ref, useGlobal, useNuxtApp, useProject, useRoute } from '#imports'
import { computed, useGlobal, useRoute } from '#imports'
const { signOut, signedIn, isLoading, user } = useGlobal() const { signOut, signedIn, isLoading, user } = useGlobal()
@ -10,85 +9,96 @@ const route = useRoute()
const email = computed(() => user.value?.email ?? '---') const email = computed(() => user.value?.email ?? '---')
const hasSider = ref(false)
const sidebar = ref<HTMLDivElement>()
const logout = () => { const logout = () => {
signOut() signOut()
navigateTo('/signin') navigateTo('/signin')
} }
const { hooks } = useNuxtApp()
/** when page suspensions have finished, check if a sidebar element was teleported into the layout */
hooks.hook('page:finish', () => {
if (sidebar.value) {
hasSider.value = sidebar.value?.children.length > 0
}
})
</script> </script>
<template> <template>
<a-layout id="nc-app" has-sider> <a-layout id="nc-app" has-sider>
<div id="nc-sidebar-left" /> <Transition name="slide">
<div v-show="hasSider" id="nc-sidebar-left" ref="sidebar" />
</Transition>
<a-layout class="!flex-col"> <a-layout class="!flex-col">
<Transition name="layout"> <a-layout-header
<a-layout-header v-if="!route.meta.public && signedIn && !route.meta.hideHeader"
v-if="!route.meta.public && signedIn" class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg"
class="flex !bg-primary items-center text-white pl-4 pr-5 shadow-lg" >
<div
v-if="route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
> >
<div <img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
v-if=" </div>
route.name === 'index' || route.name === 'project-index-create' || route.name === 'project-index-create-external'
"
class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon"
@click="navigateTo('/')"
>
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
<div class="flex justify-center"> <div class="!text-white flex justify-center">
<div v-show="isLoading" class="flex items-center gap-2 ml-3"> <div v-show="isLoading" class="flex items-center gap-2 ml-3">
{{ $t('general.loading') }} {{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" /> <MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
</div> </div>
</div>
<div class="flex-1" /> <div class="flex-1" />
<GeneralReleaseInfo />
<GeneralShareBaseButton />
<a-tooltip placement="bottom">
<template #title> Switch language</template>
<div class="flex pr-4 items-center"> <GeneralReleaseInfo />
<GeneralLanguage class="cursor-pointer text-2xl" />
</div>
</a-tooltip>
<template v-if="signedIn && !isSharedBase"> <GeneralShareBaseButton v-if="!isSharedBase" />
<a-dropdown :trigger="['click']">
<MdiDotsVertical class="md:text-xl cursor-pointer nc-user-menu" @click.prevent />
<template #overlay> <a-tooltip placement="bottom" :mouse-enter-delay="1">
<a-menu class="!py-0 nc-user-menu dark:(!bg-gray-800) leading-8 !rounded"> <template #title> Switch language</template>
<a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span> <div class="flex pr-4 items-center text-white">
</nuxt-link> <GeneralLanguage class="cursor-pointer text-2xl hover:text-accent" />
</a-menu-item> </div>
</a-tooltip>
<a-menu-divider class="!m-0" />
<template v-if="signedIn && !isSharedBase">
<a-menu-item key="1" class="!rounded-b"> <a-dropdown :trigger="['click']">
<div v-t="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout"> <MdiDotsVertical class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts text-white" @click.prevent />
<MdiLogout class="dark:text-white group-hover:(!text-red-500)" />&nbsp;
<template #overlay>
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout"> <a-menu class="!py-0 leading-8 !rounded">
{{ $t('general.signOut') }} <a-menu-item key="0" class="!rounded-t">
</span> <nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
</div> <MdiAt class="mt-1 group-hover:text-accent" />&nbsp;
</a-menu-item>
</a-menu> <span class="prose group-hover:text-primary"> {{ email }}</span>
</template> </nuxt-link>
</a-dropdown> </a-menu-item>
</template>
</a-layout-header> <a-menu-divider class="!m-0" />
</Transition>
<a-menu-item key="1" class="!rounded-b group">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:text-accent" />&nbsp;
<span class="prose group-hover:text-primary">
{{ $t('general.signOut') }}
</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-layout-header>
<a-tooltip placement="bottom"> <a-tooltip placement="bottom">
<template #title> Switch language</template> <template #title> Switch language</template>
@ -108,16 +118,8 @@ const logout = () => {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
.nc-lang-btn { .nc-lang-btn {
@apply color-transition flex items-center justify-center fixed bottom-10 right-10 z-99 w-12 h-12 rounded-full shadow-md shadow-gray-500 p-2 !bg-primary text-white active:(ring ring-pink-500) hover:(ring ring-pink-500); @apply color-transition flex items-center justify-center fixed bottom-10 right-10 z-99 w-12 h-12 rounded-full shadow-md shadow-gray-500 p-2 !bg-primary text-white active:(ring ring-accent) hover:(ring ring-accent);
&::after { &::after {
@apply rounded-full absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary; @apply rounded-full absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out bg-primary;
@ -126,11 +128,11 @@ const logout = () => {
} }
&:hover::after { &:hover::after {
@apply transform scale-110 ring ring-pink-500; @apply transform scale-110 ring ring-accent;
} }
&:active::after { &:active::after {
@apply ring ring-pink-500; @apply ring ring-accent;
} }
} }
</style> </style>

6
packages/nc-gui-v2/layouts/default.vue

@ -13,15 +13,15 @@ useHead({
<script lang="ts"> <script lang="ts">
export default { export default {
name: 'Default', name: 'DefaultLayout',
} }
</script> </script>
<template> <template>
<div class="w-full h-full"> <div class="w-full h-full">
<teleport v-if="$slots.sidebar" to="#nc-sidebar-left"> <Teleport to="#nc-sidebar-left">
<slot name="sidebar" /> <slot name="sidebar" />
</teleport> </Teleport>
<a-layout-content> <a-layout-content>
<slot /> <slot />

12
packages/nc-gui-v2/layouts/shared-view.vue

@ -25,7 +25,7 @@ export default {
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" /> <MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" />
</template> </template>
<div v-else class="text-xl font-semibold truncate"> <div v-else class="text-xl font-semibold truncate text-white">
{{ sharedView?.title }} {{ sharedView?.title }}
</div> </div>
</div> </div>
@ -40,13 +40,3 @@ export default {
</a-layout> </a-layout>
</a-layout> </a-layout>
</template> </template>
<style lang="scss" scoped>
:deep(.ant-dropdown-menu-item-group-title) {
@apply border-b-1;
}
:deep(.ant-dropdown-menu-item-group-list) {
@apply m-0;
}
</style>

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

Loading…
Cancel
Save