Browse Source

Merge branch 'develop' into feat/gui-v2-smtp-rejectUnauthorized

pull/3244/head
Wing-Kam Wong 2 years ago
parent
commit
1238bfe1b0
  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. 51
      packages/nc-gui-v2/assets/style.scss
  6. 20
      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. 73
      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. 7
      packages/nc-gui-v2/components/dashboard/settings/UIAcl.vue
  19. 4
      packages/nc-gui-v2/components/dlg/AirtableImport.vue
  20. 50
      packages/nc-gui-v2/components/dlg/TableCreate.vue
  21. 3
      packages/nc-gui-v2/components/general/ColorPicker.vue
  22. 83
      packages/nc-gui-v2/components/general/HelpAndSupport.vue
  23. 12
      packages/nc-gui-v2/components/general/MiniSidebar.vue
  24. 7
      packages/nc-gui-v2/components/general/PreviewAs.vue
  25. 4
      packages/nc-gui-v2/components/general/ReleaseInfo.vue
  26. 14
      packages/nc-gui-v2/components/general/ShareBaseButton.vue
  27. 57
      packages/nc-gui-v2/components/general/SocialCard.vue
  28. 4
      packages/nc-gui-v2/components/general/Sponsors.vue
  29. 2
      packages/nc-gui-v2/components/general/language/Menu.vue
  30. 14
      packages/nc-gui-v2/components/shared-view/Form.vue
  31. 4
      packages/nc-gui-v2/components/shared-view/Grid.vue
  32. 13
      packages/nc-gui-v2/components/smartsheet-column/EditOrAdd.vue
  33. 2
      packages/nc-gui-v2/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  34. 4
      packages/nc-gui-v2/components/smartsheet-column/SelectOptions.vue
  35. 6
      packages/nc-gui-v2/components/smartsheet-header/VirtualCellIcon.vue
  36. 23
      packages/nc-gui-v2/components/smartsheet-toolbar/AddRow.vue
  37. 20
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilter.vue
  38. 12
      packages/nc-gui-v2/components/smartsheet-toolbar/ColumnFilterMenu.vue
  39. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue
  40. 29
      packages/nc-gui-v2/components/smartsheet-toolbar/FieldsMenu.vue
  41. 63
      packages/nc-gui-v2/components/smartsheet-toolbar/LockType.vue
  42. 5
      packages/nc-gui-v2/components/smartsheet-toolbar/MoreActions.vue
  43. 8
      packages/nc-gui-v2/components/smartsheet-toolbar/Reload.vue
  44. 49
      packages/nc-gui-v2/components/smartsheet-toolbar/SearchData.vue
  45. 20
      packages/nc-gui-v2/components/smartsheet-toolbar/ShareView.vue
  46. 2
      packages/nc-gui-v2/components/smartsheet-toolbar/SharedViewList.vue
  47. 23
      packages/nc-gui-v2/components/smartsheet-toolbar/SortListMenu.vue
  48. 280
      packages/nc-gui-v2/components/smartsheet-toolbar/ViewActions.vue
  49. 24
      packages/nc-gui-v2/components/smartsheet/Form.vue
  50. 96
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  51. 8
      packages/nc-gui-v2/components/smartsheet/Pagination.vue
  52. 25
      packages/nc-gui-v2/components/smartsheet/Toolbar.vue
  53. 9
      packages/nc-gui-v2/components/smartsheet/expanded-form/Comments.vue
  54. 32
      packages/nc-gui-v2/components/smartsheet/expanded-form/Header.vue
  55. 13
      packages/nc-gui-v2/components/smartsheet/expanded-form/index.vue
  56. 52
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuBottom.vue
  57. 36
      packages/nc-gui-v2/components/smartsheet/sidebar/MenuTop.vue
  58. 2
      packages/nc-gui-v2/components/smartsheet/sidebar/RenameableMenuItem.vue
  59. 39
      packages/nc-gui-v2/components/smartsheet/sidebar/index.vue
  60. 24
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/AddRow.vue
  61. 116
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/LockMenu.vue
  62. 19
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  63. 26
      packages/nc-gui-v2/components/smartsheet/sidebar/toolbar/index.vue
  64. 2
      packages/nc-gui-v2/components/tabs/Auth.vue
  65. 11
      packages/nc-gui-v2/components/tabs/Smartsheet.vue
  66. 10
      packages/nc-gui-v2/components/template/Editor.vue
  67. 15
      packages/nc-gui-v2/components/virtual-cell/BelongsTo.vue
  68. 2
      packages/nc-gui-v2/components/virtual-cell/Formula.vue
  69. 14
      packages/nc-gui-v2/components/virtual-cell/HasMany.vue
  70. 13
      packages/nc-gui-v2/components/virtual-cell/ManyToMany.vue
  71. 24
      packages/nc-gui-v2/components/virtual-cell/components/ItemChip.vue
  72. 38
      packages/nc-gui-v2/components/virtual-cell/components/ListChildItems.vue
  73. 30
      packages/nc-gui-v2/components/virtual-cell/components/ListItems.vue
  74. 22
      packages/nc-gui-v2/components/webhook/Drawer.vue
  75. 8
      packages/nc-gui-v2/components/webhook/Editor.vue
  76. 20
      packages/nc-gui-v2/components/webhook/List.vue
  77. 1
      packages/nc-gui-v2/composables/index.ts
  78. 7
      packages/nc-gui-v2/composables/useExpandedFormStore.ts
  79. 32
      packages/nc-gui-v2/composables/useProject.ts
  80. 14
      packages/nc-gui-v2/composables/useSmartsheetStore.ts
  81. 7
      packages/nc-gui-v2/composables/useTable.ts
  82. 60
      packages/nc-gui-v2/composables/useTheme/index.ts
  83. 16
      packages/nc-gui-v2/composables/useUIPermission/index.ts
  84. 11
      packages/nc-gui-v2/composables/useViewColumns.ts
  85. 13
      packages/nc-gui-v2/composables/useViewData.ts
  86. 164
      packages/nc-gui-v2/composables/useViewFilters.ts
  87. 73
      packages/nc-gui-v2/composables/useViewSorts.ts
  88. 2
      packages/nc-gui-v2/lang/nl.json
  89. 140
      packages/nc-gui-v2/layouts/base.vue
  90. 6
      packages/nc-gui-v2/layouts/default.vue
  91. 2
      packages/nc-gui-v2/layouts/shared-view.vue
  92. 1
      packages/nc-gui-v2/lib/constants.ts
  93. 6
      packages/nc-gui-v2/lib/enums.ts
  94. 61
      packages/nc-gui-v2/middleware/auth.global.ts
  95. 1
      packages/nc-gui-v2/nuxt-shim.d.ts
  96. 25
      packages/nc-gui-v2/nuxt.config.ts
  97. 586
      packages/nc-gui-v2/package-lock.json
  98. 7
      packages/nc-gui-v2/package.json
  99. 146
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  100. 29
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index/index.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;
}

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

@ -1,9 +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; --toolbar-height: 48px;
--tw-text-opacity: 1;
} }
.ant-layout-header { .ant-layout-header {
@ -27,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); @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 {
@ -62,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;
} }
} }
} }
@ -99,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 {
@ -159,25 +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-to {
.slide-leave-active {
transform: translate(-100%, 0); transform: translate(-100%, 0);
} }
@ -190,8 +175,8 @@ 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;
} }
@ -205,11 +190,11 @@ 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;
} }
} }
@ -246,10 +231,14 @@ h1, h2, h3, h4, h5, h6, p, label, button, textarea, select {
} }
.ant-dropdown-menu-item, .ant-menu-item { .ant-dropdown-menu-item, .ant-menu-item {
@apply !py-0 active:(ring ring-pink-500); @apply !py-0 active:(ring ring-accent);
} }
.ant-dropdown-menu-title-content, .ant-dropdown-menu-title-content,
.ant-menu-title-content { .ant-menu-title-content {
@apply !py-0; @apply !py-0;
} }
.ant-dropdown-menu-submenu-title{
@apply !pr-2;
}

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

@ -15,9 +15,11 @@ 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']
@ -32,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']
@ -69,6 +72,9 @@ declare module '@vue/runtime-core' {
BiFiletypeXlsx: typeof import('~icons/bi/filetype-xlsx')['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']
@ -76,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']
@ -90,7 +99,10 @@ declare module '@vue/runtime-core' {
MdiAccount: typeof import('~icons/mdi/account')['default'] MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default'] MdiAccountCircle: typeof import('~icons/mdi/account-circle')['default']
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']
@ -99,6 +111,8 @@ declare module '@vue/runtime-core' {
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'] 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']
@ -108,6 +122,7 @@ declare module '@vue/runtime-core' {
MdiCheck: typeof import('~icons/mdi/check')['default'] MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDoubleLeft: typeof import('~icons/mdi/chevron-double-left')['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']
@ -115,6 +130,7 @@ declare module '@vue/runtime-core' {
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'] MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
@ -147,10 +163,12 @@ declare module '@vue/runtime-core' {
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']
@ -158,6 +176,7 @@ declare module '@vue/runtime-core' {
MdiLinkVariant: typeof import('~icons/mdi/link-variant')['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'] 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']
@ -194,6 +213,7 @@ 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']

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>

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

@ -220,12 +220,9 @@ function openTableCreateDialog() {
<template> <template>
<div class="nc-treeview-container flex flex-col"> <div class="nc-treeview-container flex flex-col">
<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">
>
<div 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>
@ -234,6 +231,7 @@ function openTableCreateDialog() {
<div class="flex-1"> <div class="flex-1">
<div <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" 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" @click="openTableCreateDialog"
> >
@ -241,7 +239,7 @@ function openTableCreateDialog() {
<span class="text-gray-500 group-hover:(text-primary/100) flex-1">{{ $t('tooltip.addTable') }}</span> <span class="text-gray-500 group-hover:(text-primary/100) flex-1">{{ $t('tooltip.addTable') }}</span>
<a-dropdown :trigger="['click']" @click.stop> <a-dropdown v-if="!isSharedBase" :trigger="['click']" @click.stop>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" /> <MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay> <template #overlay>
@ -253,7 +251,7 @@ function openTableCreateDialog() {
@click="openAirtableImportDialog" @click="openAirtableImportDialog"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-project-menu-item group">
<MdiTableLarge class="group-hover:text-pink-500" /> <MdiTableLarge class="group-hover:text-accent" />
<!-- TODO: i18n --> <!-- TODO: i18n -->
Airtable Airtable
</div> </div>
@ -261,7 +259,7 @@ function openTableCreateDialog() {
<a-menu-item v-if="isUIAllowed('csvImport')" key="quick-import-csv" @click="openQuickImportDialog('csv')"> <a-menu-item v-if="isUIAllowed('csvImport')" key="quick-import-csv" @click="openQuickImportDialog('csv')">
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-project-menu-item group">
<MdiFileDocumentOutline class="group-hover:text-pink-500" /> <MdiFileDocumentOutline class="group-hover:text-accent" />
<!-- TODO: i18n --> <!-- TODO: i18n -->
CSV file CSV file
</div> </div>
@ -269,7 +267,7 @@ function openTableCreateDialog() {
<a-menu-item v-if="isUIAllowed('jsonImport')" key="quick-import-json" @click="openQuickImportDialog('json')"> <a-menu-item v-if="isUIAllowed('jsonImport')" key="quick-import-json" @click="openQuickImportDialog('json')">
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-project-menu-item group">
<MdiCodeJson class="group-hover:text-pink-500" /> <MdiCodeJson class="group-hover:text-accent" />
<!-- TODO: i18n --> <!-- TODO: i18n -->
JSON file JSON file
</div> </div>
@ -281,7 +279,7 @@ function openTableCreateDialog() {
@click="openQuickImportDialog('excel')" @click="openQuickImportDialog('excel')"
> >
<div class="color-transition nc-project-menu-item group"> <div class="color-transition nc-project-menu-item group">
<MdiFileExcel class="group-hover:text-pink-500" /> <MdiFileExcel class="group-hover:text-accent" />
<!-- TODO: i18n --> <!-- TODO: i18n -->
Microsoft Excel Microsoft Excel
</div> </div>
@ -297,7 +295,7 @@ function openTableCreateDialog() {
target="_blank" target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)" class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
> >
<MdiOpenInNew class="group-hover:text-pink-500" /> <MdiOpenInNew class="group-hover:text-accent" />
<!-- TODO: i18n --> <!-- TODO: i18n -->
Request a data source you need? Request a data source you need?
</a> </a>
@ -322,7 +320,7 @@ function openTableCreateDialog() {
: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')"
@ -341,7 +339,7 @@ function openTableCreateDialog() {
<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
> >
@ -355,7 +353,7 @@ function openTableCreateDialog() {
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item v-if="isUIAllowed('table-delete')" @click="() => $e('c:table:delete') && deleteTable(table)"> <a-menu-item v-if="isUIAllowed('table-delete')" @click="deleteTable(table)">
<div class="nc-project-menu-item"> <div class="nc-project-menu-item">
{{ $t('general.delete') }} {{ $t('general.delete') }}
</div> </div>
@ -368,19 +366,13 @@ function openTableCreateDialog() {
</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="openTableCreateDialog">
{{ $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="!py-0 rounded text-sm"> <a-menu class="!py-0 rounded text-sm">
<template v-if="contextMenuTarget.type === 'table'"> <template v-if="contextMenuTarget.type === 'table'">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value)"> <a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(contextMenuTarget.value)">
@ -389,10 +381,7 @@ function openTableCreateDialog() {
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item v-if="isUIAllowed('table-delete')" @click="deleteTable(contextMenuTarget.value)">
v-if="isUIAllowed('table-delete')"
@click="() => $e('c:table:delete') && deleteTable(contextMenuTarget.value)"
>
<div class="nc-project-menu-item"> <div class="nc-project-menu-item">
{{ $t('general.delete') }} {{ $t('general.delete') }}
</div> </div>
@ -410,10 +399,14 @@ function openTableCreateDialog() {
</template> </template>
</a-dropdown> </a-dropdown>
<a-divider class="mt-0 mb-0" /> <a-divider class="!my-0" />
<div class="flex items-start flex-col justify-start px-2 py-3 gap-2">
<GeneralShareBaseButton class="py-1.5 px-2 text-primary font-bold cursor-pointer select-none" />
<GeneralHelpAndSupport class="px-2 text-gray-500 cursor-pointer select-none" />
<div class="items-center flex justify-center p-2"> <DashboardGithubStarButton class="ml-2 py-1" />
<GeneralShareBaseButton class="!mr-0" />
</div> </div>
</div> </div>
</template> </template>
@ -424,7 +417,7 @@ function openTableCreateDialog() {
} }
.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) {
@ -460,7 +453,7 @@ function openTableCreateDialog() {
} }
.sortable-chosen { .sortable-chosen {
@apply !bg-primary/25 text-primary; @apply !bg-primary bg-opacity-25 text-primary;
} }
} }
@ -469,20 +462,20 @@ function openTableCreateDialog() {
} }
.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) {
@ -508,7 +501,7 @@ function openTableCreateDialog() {
} }
:deep(.ant-dropdown-menu-item) { :deep(.ant-dropdown-menu-item) {
@apply !py-0 active:(ring ring-pink-500); @apply !py-0 active:(ring ring-accent);
} }
:deep(.ant-dropdown-menu-title-content) { :deep(.ant-dropdown-menu-title-content) {

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>

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>

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

@ -229,7 +229,7 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" class="pa-2" @keydown.esc="dialogShow = false"> <a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" class="p-2" @keydown.esc="dialogShow = false">
<div class="px-5"> <div class="px-5">
<div class="mt-5 prose-xl font-weight-bold">QUICK IMPORT - AIRTABLE</div> <div class="mt-5 prose-xl font-weight-bold">QUICK IMPORT - AIRTABLE</div>
@ -245,7 +245,7 @@ onBeforeUnmount(() => {
</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"

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

@ -45,6 +45,11 @@ 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()
@ -53,7 +58,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" @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>
@ -78,8 +83,8 @@ onMounted(() => {
/> />
</a-form-item> </a-form-item>
<div class="flex justify-end"> <div class="flex justify-end items-center">
<div class="pointer" @click="isAdvanceOptVisible = !isAdvanceOptVisible"> <div class="pointer flex flex-row items-center gap-x-1" @click="isAdvanceOptVisible = !isAdvanceOptVisible">
{{ isAdvanceOptVisible ? 'Hide' : 'Show' }} more {{ isAdvanceOptVisible ? 'Hide' : 'Show' }} more
<MdiMinusCircleOutline v-if="isAdvanceOptVisible" class="text-gray-500" /> <MdiMinusCircleOutline v-if="isAdvanceOptVisible" class="text-gray-500" />
@ -95,32 +100,29 @@ onMounted(() => {
</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>
<span>ID column is required, you can rename this later if required.</span>
<a-col :span="6"> </template>
<a-checkbox v-model:checked="table.columns.title"> title </a-checkbox> ID
</a-col> </a-tooltip>
<div v-else class="flex">
<a-col :span="6"> {{ value }}
<a-checkbox v-model:checked="table.columns.created_at"> created_at </a-checkbox> </div>
</a-col> </template>
</a-checkbox-group>
<a-col :span="6">
<a-checkbox v-model:checked="table.columns.updated_at"> updated_at </a-checkbox>
</a-col>
</a-row> </a-row>
</div> </div>
</div> </div>

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

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

@ -0,0 +1,83 @@
<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
class="flex items-center space-x-1 w-full cursor-pointer pl-3 py-1.5 hover:(text-primary bg-primary bg-opacity-5)"
@click="showDrawer = true"
>
<MdiCommentTextOutline class="mr-1 nc-share-base" />
<!-- todo: i18n -->
<div>APIs & Support</div>
</div>
<a-drawer
v-model:visible="showDrawer"
class="h-full relative"
placement="right"
size="small"
:closable="false"
:body-style="{ padding: '12px 24px 0 24px', background: '#fafafa' }"
>
<div class="flex flex-col w-full h-full p-4 pb-0">
<!-- todo: i18n -->
<a-typography-title :level="4" class="!mb-6 !text-gray-500">Help center</a-typography-title>
<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>
</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>

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

@ -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,7 +125,7 @@ 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;
} }
} }
} }

7
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"
@ -79,7 +80,7 @@ watch(previewAs, () => window.location.reload())
<template v-for="role of roleList" :key="role.title"> <template v-for="role of roleList" :key="role.title">
<a-menu-item @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 }"> <span class="capitalize" :class="{ 'x-active--text': role.title === previewAs }">
{{ role.title }} {{ role.title }}
@ -91,7 +92,7 @@ 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"> <span class="text-capitalize text-xs whitespace-nowrap">
{{ $t('activity.resetReview') }} {{ $t('activity.resetReview') }}

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"> <div class="flex items-center w-full pl-3 hover:(text-primary bg-primary bg-opacity-5)">
<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="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 nc-share-base" /> <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>

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

@ -39,7 +39,7 @@ onMounted(() => {
<a-menu-item <a-menu-item
v-for="lang of languages" v-for="lang of languages"
:key="lang" :key="lang"
:class="lang === locale ? '!bg-primary/10 text-primary' : ''" :class="lang === locale ? '!bg-primary bg-opacity-10 text-primary' : ''"
class="group" class="group"
:value="lang" :value="lang"
@click="changeLanguage(lang)" @click="changeLanguage(lang)"

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>

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

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

@ -12,11 +12,15 @@ const { filterAutoSave } = useGlobal()
const filterComp = ref<typeof ColumnFilter>() 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)
@ -36,10 +40,10 @@ const applyChanges = async () => await filterComp.value?.applyChanges()
<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">
<MdiFilterOutline /> <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>
<MdiMenuDown class="text-grey" /> <MdiMenuDown class="text-grey" />
</div> </div>
</a-button> </a-button>
@ -51,7 +55,7 @@ const applyChanges = async () => await filterComp.value?.applyChanges()
: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>

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

@ -1,4 +1,6 @@
<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 { import {
ActiveViewInj, ActiveViewInj,
@ -14,6 +16,8 @@ import {
useViewColumns, useViewColumns,
watch, watch,
} from '#imports' } 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)!
@ -39,6 +43,7 @@ const {
showAll, showAll,
hideAll, hideAll,
saveOrUpdate, saveOrUpdate,
metaColumnById,
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger()) } = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
watch( watch(
@ -76,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>
@ -103,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" />
@ -113,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">
@ -139,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>

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

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

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>

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 {

96
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,7 +42,7 @@ 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 openNewRecordFormHook = inject(OpenNewRecordFormHookInj)
@ -87,7 +87,6 @@ const {
deleteRow, deleteRow,
deleteSelectedRows, deleteSelectedRows,
selectedAllRecords, selectedAllRecords,
loadAggCommentsCount,
removeLastEmptyRow, removeLastEmptyRow,
} = useViewData(meta, view as any, xWhere) } = useViewData(meta, view as any, xWhere)
@ -106,7 +105,6 @@ provide(ReadonlyInj, !isUIAllowed('xcDatatableEditable'))
reloadViewDataHook?.on(async () => { reloadViewDataHook?.on(async () => {
await loadData() await loadData()
loadAggCommentsCount()
}) })
const expandForm = (row: Row, state?: Record<string, any>) => { const expandForm = (row: Row, state?: Record<string, any>) => {
@ -128,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()
} }
}, },
@ -309,12 +307,12 @@ const onNavigate = (dir: NavigateDir) => {
</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>
@ -325,7 +323,7 @@ const onNavigate = (dir: NavigateDir) => {
<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" />
@ -360,7 +358,7 @@ const onNavigate = (dir: NavigateDir) => {
@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 nc-column-add" /> <MdiPlus class="text-sm nc-column-add" />
</div> </div>
@ -382,7 +380,7 @@ const onNavigate = (dir: NavigateDir) => {
<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"
@ -407,9 +405,12 @@ const onNavigate = (dir: NavigateDir) => {
> >
{{ 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>
@ -472,11 +473,11 @@ const onNavigate = (dir: NavigateDir) => {
<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') }}
@ -486,17 +487,32 @@ const onNavigate = (dir: NavigateDir) => {
</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 v-if="contextMenuTarget" @click="clearCell(contextMenuTarget)">
<div class="nc-project-menu-item">Clear cell</div>
</a-menu-item> </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>
@ -529,7 +545,7 @@ const onNavigate = (dir: NavigateDir) => {
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,
@ -561,7 +577,7 @@ const onNavigate = (dir: NavigateDir) => {
} }
td.active::before { td.active::before {
@apply bg-primary/5; @apply bg-primary bg-opacity-5;
} }
} }
@ -619,4 +635,8 @@ const onNavigate = (dir: NavigateDir) => {
} }
} }
} }
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-[var(--toolbar-height)] 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>

9
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,7 +60,7 @@ 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 nc-comment-box" class="!text-xs nc-comment-box"
@ -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 nc-toggle-comments" <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 -->

13
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'
@ -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()
@ -109,13 +109,14 @@ export default {
<Header @cancel="onClose" /> <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
@ -130,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 nc-webhook-btn"
@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>

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

@ -1,5 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ViewType, ViewTypes } from 'nocodb-sdk' import type { ViewType } from 'nocodb-sdk'
import { ViewTypes } from 'nocodb-sdk'
import type { SortableEvent } from 'sortablejs' import type { SortableEvent } from 'sortablejs'
import type { Menu as AntMenu } from 'ant-design-vue' import type { Menu as AntMenu } from 'ant-design-vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@ -18,6 +19,12 @@ interface Emits {
const emits = defineEmits<Emits>() const emits = defineEmits<Emits>()
const viewTypeAlias = {
[ViewTypes.GRID as any]: 'grid',
[ViewTypes.FORM as any]: 'form',
[ViewTypes.GALLERY as any]: 'gallery',
}
const activeView = inject(ActiveViewInj, ref()) const activeView = inject(ActiveViewInj, ref())
const views = inject<Ref<any[]>>(ViewListInj, ref([])) const views = inject<Ref<any[]>>(ViewListInj, ref([]))
@ -137,6 +144,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,21 +181,20 @@ 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, index) of views"
:id="view.id" :id="view.id"
:key="view.id" :key="view.id"
: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':
`nc-view-item nc-${view.type}-view-item`, (route.params.viewTitle && route.params.viewTitle === view.title) || (route.params.viewTitle === '' && index === 0),
]" [`nc-view-item nc-${viewTypeAlias[view.type] || view.type}-view-item`]: true,
}"
@change-view="changeView" @change-view="changeView"
@open-modal="$emit('openModal', $event)" @open-modal="$emit('openModal', $event)"
@delete="onDelete" @delete="onDelete"
@ -197,7 +207,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 +229,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>

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

@ -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>

39
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'
@ -81,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 || '' } })
@ -104,35 +104,12 @@ function onCreate(view: GridType | FormType | KanbanType | GalleryType) {
class="min-h-[var(--toolbar-height)] max-h-[var(--toolbar-height)]" class="min-h-[var(--toolbar-height)] max-h-[var(--toolbar-height)]"
:class="{ 'flex items-center py-3 px-3 justify-between border-b-1': !isForm }" :class="{ 'flex items-center py-3 px-3 justify-between border-b-1': !isForm }"
/> />
<div v-if="isOpen" class="flex-1 flex flex-col min-h-0">
<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 nc-webhook-icon">
<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>
@ -156,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>

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

@ -1,24 +0,0 @@
<script setup lang="ts">
import { OpenNewRecordFormHookInj, inject } from '#imports'
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const isLocked = inject(IsLockedInj)
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj)!
const onClick = () => {
if (!isLocked?.value) openNewRecordFormHook.trigger()
}
</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="onClick" />
</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>

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

@ -4,15 +4,12 @@ const { isOpen, toggle } = useSidebar({ storageKey: 'nc-right-sidebar' })
</script> </script>
<template> <template>
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'" :mouse-enter-delay="0.8"> <div :class="{ 'nc-active-btn': isOpen }">
<template #title> Toggle sidebar</template> <a-button size="small" @click="toggle(!isOpen)">
<div class="flex items-center gap-1 text-xs" :class="{ 'text-gray-500': !isOpen }">
<div class="nc-sidebar-right-item hover:after:bg-primary/75 group nc-sidebar-add-row"> <MdiMenu class="!text-xs" />
<MdiChevronDoubleLeft Views
class="cursor-pointer group-hover:(!text-white) transform transition-transform" </div>
:class="{ 'rotate-180': isOpen }" </a-button>
@click="toggle(!isOpen)" </div>
/>
</div>
</a-tooltip>
</template> </template>

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

@ -1,28 +1,21 @@
<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 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)
const clickCount = $ref(0) const clickCount = $ref(0)
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
</script> </script>
<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
@ -31,7 +24,8 @@ const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
" "
> >
<slot name="start" /> <slot name="start" />
<ToggleDrawer />
<span></span>
<template v-if="debug"> <template v-if="debug">
<ExportCache /> <ExportCache />
@ -46,20 +40,6 @@ const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
<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 />
<div :class="{ 'w-[calc(100%_+_16px)] h-[1px] bg-gray-200 mt-1 -ml-1': !isOpen, 'dot': isOpen }" />
<ToggleDrawer />
<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>

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

@ -76,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" />
@ -86,7 +86,12 @@ watch(isLocked, (nextValue) => (treeViewIsLockedInj.value = nextValue), { immedi
</div> </div>
</template> </template>
</div> </div>
<SmartsheetSidebar v-if="meta" class="nc-right-sidebar" />
<SmartsheetSidebar v-if="meta" />
</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>

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

@ -700,7 +700,7 @@ onMounted(() => {
<a-button class="group" @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="group-hover:!text-pink-500 flex text-lg" /> <mdi-numeric class="group-hover:!text-accent flex text-lg" />
</div> </div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -713,7 +713,7 @@ onMounted(() => {
<a-button class="group" @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="group-hover:!text-pink-500 text-lg" /> <mdi-alpha-a class="group-hover:!text-accent text-lg" />
</div> </div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -726,7 +726,7 @@ onMounted(() => {
<a-button class="group" @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="group-hover:!text-pink-500 text-lg" /> <mdi-text class="group-hover:!text-accent text-lg" />
</div> </div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -739,7 +739,7 @@ onMounted(() => {
<a-button class="group" @click="addNewColumnRow(table, 'SingleLineText')"> <a-button class="group" @click="addNewColumnRow(table, 'SingleLineText')">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<mdi-plus class="group-hover:!text-pink-500 text-lg" /> <mdi-plus class="group-hover:!text-accent text-lg" />
</div> </div>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
@ -761,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>

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

@ -3,6 +3,7 @@ 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'

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();

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

@ -18,6 +18,14 @@ 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 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 +36,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 +55,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,15 +75,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 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,
} }
} }

13
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()

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

@ -1,14 +1,15 @@
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 { message } from 'ant-design-vue'
import { import {
IsPublicInj, IsPublicInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
computed, computed,
extractSdkResponseErrorMsg,
inject, inject,
ref, ref,
useMetas, useMetas,
useNuxtApp, useNuxtApp,
useSharedView,
useUIPermission, useUIPermission,
watch, watch,
} from '#imports' } from '#imports'
@ -19,13 +20,12 @@ 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))
@ -34,21 +34,23 @@ export function useViewFilters(
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
}, },
}) })
@ -60,55 +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) {
@ -117,11 +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 {
await $api.dbTableFilter.delete(filter.id)
reloadData?.() reloadData?.()
filters.value.splice(i, 1) 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 {
@ -131,30 +148,29 @@ 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 }
filters.value = [...filters.value]
return
}
if (!view?.value) return if (!view?.value) return
if (!isUIAllowed('filterSync')) { try {
// skip if (nestedMode.value) {
} else if (!autoApply?.value && !force) { filters.value[i] = { ...filter }
filter.status = filter.id ? 'update' : 'create' filters.value = [...filters.value]
} else if (filter.id) { } else if (!autoApply?.value && !force) {
await $api.dbTableFilter.update(filter.id, { filter.status = filter.id ? 'update' : 'create'
...filter, } else if (filter.id) {
fk_parent_id: parentId, await $api.dbTableFilter.update(filter.id, {
}) ...filter,
} else { fk_parent_id: parentId,
// todo: return type of dbTableFilter is void? })
filters.value[i] = (await $api.dbTableFilter.create(view?.value?.id as string, { } 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, {
})) as any ...filter,
fk_parent_id: parentId,
})) as any
}
} catch (e: any) {
console.log(e)
message.error(await extractSdkResponseErrorMsg(e))
} }
reloadData?.() reloadData?.()
@ -170,7 +186,7 @@ export function useViewFilters(
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)

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 }
} }

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": {

140
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 && !route.meta.hideHeader" 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 /> <GeneralReleaseInfo />
<a-tooltip placement="bottom" :mouse-enter-delay="1"> <GeneralShareBaseButton v-if="!isSharedBase" />
<template #title> Switch language</template>
<div class="flex pr-4 items-center"> <a-tooltip placement="bottom" :mouse-enter-delay="1">
<GeneralLanguage class="cursor-pointer text-2xl hover:text-pink-500" /> <template #title> Switch language</template>
</div>
</a-tooltip>
<template v-if="signedIn && !isSharedBase"> <div class="flex pr-4 items-center text-white">
<a-dropdown :trigger="['click']"> <GeneralLanguage class="cursor-pointer text-2xl hover:text-accent" />
<MdiDotsVertical class="md:text-xl cursor-pointer hover:text-pink-500 nc-menu-accounts" @click.prevent /> </div>
</a-tooltip>
<template #overlay>
<a-menu class="!py-0 dark:(!bg-gray-800) leading-8 !rounded"> <template v-if="signedIn && !isSharedBase">
<a-menu-item key="0" class="!rounded-t"> <a-dropdown :trigger="['click']">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group no-underline" to="/user"> <MdiDotsVertical class="md:text-xl cursor-pointer hover:text-accent nc-menu-accounts text-white" @click.prevent />
<MdiAt class="mt-1 group-hover:text-pink-500" />&nbsp;
<template #overlay>
<span class="prose group-hover:text-primary"> {{ email }}</span> <a-menu class="!py-0 leading-8 !rounded">
</nuxt-link> <a-menu-item key="0" class="!rounded-t">
</a-menu-item> <nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-accent" />&nbsp;
<a-menu-divider class="!m-0" />
<span class="prose group-hover:text-primary"> {{ email }}</span>
<a-menu-item key="1" class="!rounded-b group"> </nuxt-link>
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout"> </a-menu-item>
<MdiLogout class="group-hover:text-pink-500" />&nbsp;
<a-menu-divider class="!m-0" />
<span class="prose group-hover:text-primary">
{{ $t('general.signOut') }} <a-menu-item key="1" class="!rounded-b group">
</span> <div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
</div> <MdiLogout class="group-hover:text-accent" />&nbsp;
</a-menu-item>
</a-menu> <span class="prose group-hover:text-primary">
</template> {{ $t('general.signOut') }}
</a-dropdown> </span>
</template> </div>
</a-layout-header> </a-menu-item>
</Transition> </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>
@ -109,7 +119,7 @@ const logout = () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.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;
@ -118,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 />

2
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>

1
packages/nc-gui-v2/lib/constants.ts

@ -1,2 +1,3 @@
export const NOCO = 'noco' export const NOCO = 'noco'
export const USER_PROJECT_ROLES = 'user_project_roles' export const USER_PROJECT_ROLES = 'user_project_roles'
export const SYSTEM_COLUMNS = ['id', 'title', 'created_at', 'updated_at']

6
packages/nc-gui-v2/lib/enums.ts

@ -54,3 +54,9 @@ export enum NavigateDir {
NEXT, NEXT,
PREV, PREV,
} }
export enum LockType {
Personal = 'personal',
Locked = 'locked',
Collaborative = 'collaborative',
}

61
packages/nc-gui-v2/middleware/auth.global.ts

@ -1,5 +1,6 @@
import { message } from 'ant-design-vue'
import { defineNuxtRouteMiddleware, navigateTo } from '#app' import { defineNuxtRouteMiddleware, navigateTo } from '#app'
import { useGlobal } from '#imports' import { useApi, useGlobal } from '#imports'
/** /**
* Global auth middleware * Global auth middleware
@ -10,7 +11,19 @@ import { useGlobal } from '#imports'
* the user is redirected to the home page. * the user is redirected to the home page.
* *
* By default, we assume that auth is required * By default, we assume that auth is required
* If not required, mark the page as `requiresAuth: false` using `definePageMeta` * If not required, mark the page as
* ```
* definePageMeta({
* requiresAuth: false
* })
* ```
*
* If auth should be circumvented completely mark the page as public
* ```
* definePageMeta({
* public: true
* })
* ```
* *
* @example * @example
* ``` * ```
@ -20,19 +33,25 @@ import { useGlobal } from '#imports'
* }) * })
* ``` * ```
*/ */
export default defineNuxtRouteMiddleware((to, from) => { export default defineNuxtRouteMiddleware(async (to, from) => {
const state = useGlobal() const state = useGlobal()
/** if public allow */ /** if user isn't signed in and google auth is enabled, try to check if sign-in data is present */
if (!state.signedIn && state.appInfo.value.googleAuthEnabled) await tryGoogleAuth()
/** if public allow all visitors */
if (to.meta.public) return if (to.meta.public) return
/** if shred base allow without validating */ /** if shared base allow without validating */
if (to.params?.projectType === 'base') return if (to.params?.projectType === 'base') return
if (to.meta.public) return
/** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */ /** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) { if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
/** If this is the first usern navigate to signup page directly */
if (state.appInfo.value.firstUser) {
return navigateTo('/signup')
}
return navigateTo('/signin') return navigateTo('/signin')
} else if (to.meta.requiresAuth === false && state.signedIn.value) { } else if (to.meta.requiresAuth === false && state.signedIn.value) {
/** /**
@ -48,3 +67,31 @@ export default defineNuxtRouteMiddleware((to, from) => {
} }
} }
}) })
/**
* If present, try using google auth data to sign user in before navigating to the next page
*/
async function tryGoogleAuth() {
const { signIn } = useGlobal()
const { api } = useApi()
if (window.location.search && /\bscope=|\bstate=/.test(window.location.search) && /\bcode=/.test(window.location.search)) {
try {
const {
data: { token },
} = await api.instance.post(
`/auth/${window.location.search.includes('state=github') ? 'github' : 'google'}/genTokenByCode${window.location.search}`,
)
signIn(token)
} catch (e: any) {
if (e.response && e.response.data && e.response.data.msg) {
message.error({ content: e.response.data.msg })
}
}
const newURL = window.location.href.split('?')[0]
window.history.pushState('object', document.title, newURL)
}
}

1
packages/nc-gui-v2/nuxt-shim.d.ts vendored

@ -29,5 +29,6 @@ declare module 'vue-router' {
interface RouteMeta { interface RouteMeta {
requiresAuth?: boolean requiresAuth?: boolean
public?: boolean public?: boolean
hideHeader?: boolean
} }
} }

25
packages/nc-gui-v2/nuxt.config.ts

@ -6,7 +6,6 @@ import IconsResolver from 'unplugin-icons/resolver'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers' import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
import monacoEditorPlugin from 'vite-plugin-monaco-editor' import monacoEditorPlugin from 'vite-plugin-monaco-editor'
import { themeColors } from './utils/colorsUtils'
// https://v3.nuxtjs.org/api/configuration/nuxt.config // https://v3.nuxtjs.org/api/configuration/nuxt.config
export default defineNuxtConfig({ export default defineNuxtConfig({
@ -17,11 +16,9 @@ export default defineNuxtConfig({
css: [ css: [
'virtual:windi.css', 'virtual:windi.css',
'virtual:windi-devtools', 'virtual:windi-devtools',
'vuetify/lib/styles/main.sass',
'~/assets/style/fonts.css', '~/assets/style/fonts.css',
'~/assets/css/global.css', '~/assets/css/global.css',
'~/assets/style.css', '~/assets/style.scss',
'~/assets/style-v2.scss',
], ],
meta: { meta: {
@ -55,14 +52,6 @@ export default defineNuxtConfig({
external: 'httpsnippet', external: 'httpsnippet',
}, },
}, },
css: {
preprocessorOptions: {
less: {
modifyVars: { 'primary-color': themeColors.primary, 'text-color': 'rgba(61, 61, 61, 1)' },
javascriptEnabled: true,
},
},
},
plugins: [ plugins: [
vueI18n({ vueI18n({
include: path.resolve(__dirname, './lang'), include: path.resolve(__dirname, './lang'),
@ -76,7 +65,8 @@ export default defineNuxtConfig({
Components({ Components({
resolvers: [ resolvers: [
AntDesignVueResolver({ AntDesignVueResolver({
importStyle: 'less', importStyle: false,
resolveIcons: false,
}), }),
IconsResolver({ IconsResolver({
prefix: false, prefix: false,
@ -108,4 +98,13 @@ export default defineNuxtConfig({
dirs: ['./context', './utils', './lib'], dirs: ['./context', './utils', './lib'],
imports: [{ name: 'useI18n', from: 'vue-i18n' }], imports: [{ name: 'useI18n', from: 'vue-i18n' }],
}, },
pageTransition: {
name: 'page',
mode: 'out-in',
},
layoutTransition: {
name: 'layout',
mode: 'out-in',
},
}) })

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

File diff suppressed because it is too large Load Diff

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

@ -16,7 +16,7 @@
"@vuelidate/validators": "^2.0.0-alpha.31", "@vuelidate/validators": "^2.0.0-alpha.31",
"@vueuse/core": "^9.0.2", "@vueuse/core": "^9.0.2",
"@vueuse/integrations": "^9.0.2", "@vueuse/integrations": "^9.0.2",
"ant-design-vue": "^3.2.10", "ant-design-vue": "^3.2.11",
"dayjs": "^1.11.3", "dayjs": "^1.11.3",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jsep": "^1.3.6", "jsep": "^1.3.6",
@ -33,7 +33,6 @@
"vue-github-button": "^3.0.3", "vue-github-button": "^3.0.3",
"vue-i18n": "^9.1.10", "vue-i18n": "^9.1.10",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
"vuetify": "^3.0.0-alpha.13",
"xlsx": "^0.17.3" "xlsx": "^0.17.3"
}, },
"devDependencies": { "devDependencies": {
@ -43,6 +42,7 @@
"@iconify-json/clarity": "^1.1.4", "@iconify-json/clarity": "^1.1.4",
"@iconify-json/eva": "^1.1.2", "@iconify-json/eva": "^1.1.2",
"@iconify-json/ic": "^1.1.7", "@iconify-json/ic": "^1.1.7",
"@iconify-json/logos": "^1.1.14",
"@iconify-json/lucide": "^1.1.36", "@iconify-json/lucide": "^1.1.36",
"@iconify-json/material-symbols": "^1.1.8", "@iconify-json/material-symbols": "^1.1.8",
"@iconify-json/mdi": "^1.1.25", "@iconify-json/mdi": "^1.1.25",
@ -66,13 +66,12 @@
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"happy-dom": "^6.0.3", "happy-dom": "^6.0.3",
"httpsnippet": "^2.0.0", "httpsnippet": "^2.0.0",
"less": "^4.1.3",
"nuxt": "3.0.0-rc.4", "nuxt": "3.0.0-rc.4",
"nuxt-windicss": "^2.5.0", "nuxt-windicss": "^2.5.0",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"sass": "^1.53.0", "sass": "^1.53.0",
"unplugin-icons": "^0.14.7", "unplugin-icons": "^0.14.7",
"unplugin-vue-components": "^0.21.1", "unplugin-vue-components": "^0.22.4",
"vite-plugin-monaco-editor": "^1.1.0", "vite-plugin-monaco-editor": "^1.1.0",
"vitest": "^0.18.0", "vitest": "^0.18.0",
"windicss": "^3.5.6" "windicss": "^3.5.6"

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

@ -1,6 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { Chrome } from '@ckpack/vue-color'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { import {
computed,
definePageMeta,
navigateTo, navigateTo,
onKeyStroke, onKeyStroke,
openLink, openLink,
@ -12,10 +15,16 @@ import {
useProject, useProject,
useRoute, useRoute,
useTabs, useTabs,
useTheme,
useUIPermission, useUIPermission,
watch,
} from '#imports' } from '#imports'
import { TabType } from '~/composables' import { TabType } from '~/composables'
definePageMeta({
hideHeader: true,
})
const route = useRoute() const route = useRoute()
const { appInfo, token, signOut, signedIn, user } = useGlobal() const { appInfo, token, signOut, signedIn, user } = useGlobal()
@ -41,16 +50,31 @@ const openDialogKey = ref<string>()
const dropdownOpen = ref(false) const dropdownOpen = ref(false)
/** Sidebar ref */
const sidebar = ref()
const pickedColor = ref<any>('#ffffff')
let pickerActive = $ref<boolean | 'primary' | 'accent'>(false)
const email = computed(() => user.value?.email ?? '---') const email = computed(() => user.value?.email ?? '---')
const { setTheme, theme } = useTheme()
watch(pickedColor, (nextColor) => {
if (pickerActive && nextColor.hex) {
setTheme({
primaryColor: pickerActive === 'primary' ? nextColor.hex : theme.value.primaryColor,
accentColor: pickerActive === 'accent' ? nextColor.hex : theme.value.accentColor,
})
}
})
const logout = () => { const logout = () => {
signOut() signOut()
navigateTo('/signin') navigateTo('/signin')
} }
/** Sidebar ref */
const sidebar = ref()
onKeyStroke( onKeyStroke(
'Escape', 'Escape',
() => { () => {
@ -61,10 +85,6 @@ onKeyStroke(
clearTabs() clearTabs()
if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' })
}
function toggleDialog(value?: boolean, key?: string) { function toggleDialog(value?: boolean, key?: string) {
dialogOpen.value = value ?? !dialogOpen.value dialogOpen.value = value ?? !dialogOpen.value
openDialogKey.value = key openDialogKey.value = key
@ -74,6 +94,10 @@ await loadProject()
await loadTables() await loadTables()
if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: 'Team & Auth' })
}
const copyProjectInfo = async () => { const copyProjectInfo = async () => {
try { try {
await loadProjectMetaInfo() await loadProjectMetaInfo()
@ -102,9 +126,21 @@ const copyAuthToken = async () => {
} }
} }
definePageMeta({ const openColorPicker = (type: 'primary' | 'accent') => {
hideHeader: true, if (!pickerActive || pickerActive !== type) {
}) pickedColor.value = type === 'primary' ? theme.value.primaryColor : theme.value.accentColor
pickerActive = type
} else {
pickerActive = false
}
}
const onMenuClose = (visible: boolean) => {
if (!visible) {
pickedColor.value = '#ffffff'
pickerActive = false
}
}
</script> </script>
<template> <template>
@ -152,7 +188,7 @@ definePageMeta({
</template> </template>
</div> </div>
<a-dropdown v-else class="h-full min-w-0 flex-1" :trigger="['click']" placement="bottom"> <a-dropdown v-else class="h-full min-w-0 flex-1" :trigger="['click']" placement="bottom" @visible-change="onMenuClose">
<div <div
:style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }" :style="{ width: isOpen ? 'calc(100% - 40px) pr-2' : '100%' }"
:class="[isOpen ? '' : 'justify-center']" :class="[isOpen ? '' : 'justify-center']"
@ -167,7 +203,7 @@ definePageMeta({
</a-tooltip> </a-tooltip>
<div v-else class="text-lg font-semibold truncate">{{ project.title }}</div> <div v-else class="text-lg font-semibold truncate">{{ project.title }}</div>
<MdiChevronDown class="min-w-[28.5px] group-hover:text-pink-500 text-2xl" /> <MdiChevronDown class="min-w-[17px] group-hover:text-accent text-md" />
</template> </template>
<template v-else> <template v-else>
@ -180,7 +216,7 @@ definePageMeta({
<a-menu-item-group> <a-menu-item-group>
<template #title> <template #title>
<div class="group select-none flex items-center gap-4 py-1"> <div class="group select-none flex items-center gap-4 py-1">
<MdiFolder class="group-hover:text-pink-500 text-xl" /> <MdiFolder class="group-hover:text-accent text-xl" />
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-lg group-hover:(!text-primary) font-semibold truncate">{{ project.title }}</div> <div class="text-lg group-hover:(!text-primary) font-semibold truncate">{{ project.title }}</div>
@ -188,7 +224,7 @@ definePageMeta({
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="group-hover:(!text-primary)">ID:</div> <div class="group-hover:(!text-primary)">ID:</div>
<div class="text-xs group-hover:text-pink-500 truncate font-italic">{{ project.id }}</div> <div class="text-xs group-hover:text-accent truncate font-italic">{{ project.id }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -196,7 +232,7 @@ definePageMeta({
<a-menu-item key="copy"> <a-menu-item key="copy">
<div class="nc-project-menu-item group" @click.stop="copyProjectInfo"> <div class="nc-project-menu-item group" @click.stop="copyProjectInfo">
<MdiContentCopy class="group-hover:text-pink-500" /> <MdiContentCopy class="group-hover:text-accent" />
Copy Project Info Copy Project Info
</div> </div>
</a-menu-item> </a-menu-item>
@ -210,14 +246,14 @@ definePageMeta({
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)" @click.stop="openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.ncSiteUrl)"
> >
<MdiApi class="group-hover:text-pink-500" /> <MdiApi class="group-hover:text-accent" />
Swagger: Rest APIs Swagger: Rest APIs
</div> </div>
</a-menu-item> </a-menu-item>
<a-menu-item key="copy"> <a-menu-item key="copy">
<div v-t="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken"> <div v-t="['a:navbar:user:copy-auth-token']" class="nc-project-menu-item group" @click.stop="copyAuthToken">
<MdiScriptTextKeyOutline class="group-hover:text-pink-500" /> <MdiScriptTextKeyOutline class="group-hover:text-accent" />
Copy Auth Token Copy Auth Token
</div> </div>
</a-menu-item> </a-menu-item>
@ -231,7 +267,7 @@ definePageMeta({
class="nc-project-menu-item group" class="nc-project-menu-item group"
@click="toggleDialog(true, 'teamAndAuth')" @click="toggleDialog(true, 'teamAndAuth')"
> >
<MdiCog class="group-hover:text-pink-500" /> <MdiCog class="group-hover:text-accent" />
Team & Settings Team & Settings
</div> </div>
</a-menu-item> </a-menu-item>
@ -241,13 +277,13 @@ definePageMeta({
<a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as"> <a-sub-menu v-if="isUIAllowed('previewAs')" key="preview-as">
<template #title> <template #title>
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group"> <div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiFileEyeOutline class="group-hover:text-pink-500" /> <MdiFileEyeOutline class="group-hover:text-accent" />
Preview Project As Preview Project As
<div class="flex-1" /> <div class="flex-1" />
<MaterialSymbolsChevronRightRounded <MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400" class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/> />
</div> </div>
</template> </template>
@ -260,12 +296,12 @@ definePageMeta({
<a-sub-menu key="language" class="lang-menu scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0"> <a-sub-menu key="language" class="lang-menu scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0">
<template #title> <template #title>
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<MaterialSymbolsTranslate class="group-hover:text-pink-500 nc-language" /> <MaterialSymbolsTranslate class="group-hover:text-accent nc-language" />
Language Language
<div class="flex-1" /> <div class="flex-1" />
<MaterialSymbolsChevronRightRounded <MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400" class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/> />
</div> </div>
</template> </template>
@ -275,15 +311,15 @@ definePageMeta({
</a-sub-menu> </a-sub-menu>
<template v-if="signedIn && !isSharedBase"> <template v-if="signedIn && !isSharedBase">
<a-sub-menu v-if="isUIAllowed('previewAs')" key="account"> <a-sub-menu key="account">
<template #title> <template #title>
<div class="nc-project-menu-item group"> <div class="nc-project-menu-item group">
<MdiAccount class="group-hover:text-pink-500" /> <MdiAccount class="group-hover:text-accent" />
Account Account
<div class="flex-1" /> <div class="flex-1" />
<MaterialSymbolsChevronRightRounded <MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-pink-500) text-xl text-gray-400" class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/> />
</div> </div>
</template> </template>
@ -291,8 +327,8 @@ definePageMeta({
<template #expandIcon></template> <template #expandIcon></template>
<a-menu-item key="0" class="!rounded-t"> <a-menu-item key="0" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group no-underline" to="/user"> <nuxt-link v-t="['c:navbar:user:email']" class="nc-project-menu-item group !no-underline" to="/user">
<MdiAt class="mt-1 group-hover:text-pink-500" />&nbsp; <MdiAt class="mt-1 group-hover:text-accent" />&nbsp;
<span class="prose-sm">{{ email }}</span> <span class="prose-sm">{{ email }}</span>
</nuxt-link> </nuxt-link>
@ -300,7 +336,7 @@ definePageMeta({
<a-menu-item key="1" class="!rounded-b"> <a-menu-item key="1" class="!rounded-b">
<div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout"> <div v-t="['a:navbar:user:sign-out']" class="nc-project-menu-item group" @click="logout">
<MdiLogout class="group-hover:(!text-pink-500)" />&nbsp; <MdiLogout class="group-hover:(!text-accent)" />&nbsp;
<span class="prose-sm"> <span class="prose-sm">
{{ $t('general.signOut') }} {{ $t('general.signOut') }}
@ -309,12 +345,53 @@ definePageMeta({
</a-menu-item> </a-menu-item>
</a-sub-menu> </a-sub-menu>
</template> </template>
<a-menu-divider />
<a-sub-menu>
<template #title>
<div class="nc-project-menu-item group">
<ClarityImageLine class="group-hover:text-accent" />
Theme
<div class="flex-1" />
<MaterialSymbolsChevronRightRounded
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400"
/>
</div>
</template>
<a-menu-item>
<div class="nc-project-menu-item group" @click.stop="openColorPicker('primary')">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Primary Color
</div>
</a-menu-item>
<a-menu-item>
<div class="nc-project-menu-item group" @click.stop="openColorPicker('accent')">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Accent Color
</div>
</a-menu-item>
</a-sub-menu>
<Chrome
v-if="pickerActive"
v-model="pickedColor"
class="z-99 absolute right-[-225px]"
@click.stop
@blur="onMenuClose(false)"
/>
</a-menu-item-group> </a-menu-item-group>
</a-menu> </a-menu>
</template> </template>
</a-dropdown> </a-dropdown>
<div class="nc-sidebar-left-toggle-icon hover:after:bg-primary/75 group nc-sidebar-add-row flex align-center px-2"> <div
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row flex items-center px-2"
>
<MdiBackburger <MdiBackburger
class="cursor-pointer transform transition-transform duration-500" class="cursor-pointer transform transition-transform duration-500"
:class="{ 'rotate-180': !isOpen }" :class="{ 'rotate-180': !isOpen }"
@ -326,12 +403,11 @@ definePageMeta({
<DashboardTreeView v-show="isOpen" /> <DashboardTreeView v-show="isOpen" />
</a-layout-sider> </a-layout-sider>
</template> </template>
<div :key="$route.fullPath">
<dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" /> <dashboard-settings-modal v-model="dialogOpen" :open-key="openDialogKey" />
<NuxtPage />
<NuxtPage /> <GeneralPreviewAs float />
</div>
<GeneralPreviewAs float />
</NuxtLayout> </NuxtLayout>
</template> </template>

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

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TabItem } from '~/composables' import type { TabItem } from '~/composables'
import { TabMetaInj, provide, useSidebar, useTabs } from '#imports' import { TabType } from '~/composables'
import { TabType, useGlobal } from '~/composables' import { TabMetaInj, provide, useGlobal, useSidebar, useTabs } from '#imports'
import MdiAirTableIcon from '~icons/mdi/table-large' import MdiAirTableIcon from '~icons/mdi/table-large'
import MdiView from '~icons/mdi/eye-circle-outline' import MdiView from '~icons/mdi/eye-circle-outline'
import MdiAccountGroup from '~icons/mdi/account-group' import MdiAccountGroup from '~icons/mdi/account-group'
@ -24,13 +24,20 @@ const icon = (tab: TabItem) => {
} }
const { isOpen, toggle } = useSidebar() const { isOpen, toggle } = useSidebar()
function onEdit(targetKey: number, action: 'add' | 'remove' | string) {
if (action === 'remove') closeTab(targetKey)
}
</script> </script>
<template> <template>
<div class="h-full w-full nc-container"> <div class="h-full w-full nc-container">
<div class="h-full w-full flex flex-col"> <div class="h-full w-full flex flex-col">
<div class="flex items-end !min-h-[50px] bg-primary/100"> <div class="flex items-end !min-h-[50px] !bg-primary">
<div v-if="!isOpen" class="nc-sidebar-left-toggle-icon hover:after:bg-primary/75 group nc-sidebar-add-row py-2 px-3"> <div
v-if="!isOpen"
class="nc-sidebar-left-toggle-icon hover:after:(bg-primary bg-opacity-75) group nc-sidebar-add-row py-2 px-3"
>
<MdiMenu <MdiMenu
class="cursor-pointer transform transition-transform duration-500 text-white" class="cursor-pointer transform transition-transform duration-500 text-white"
:class="{ 'rotate-180': !isOpen }" :class="{ 'rotate-180': !isOpen }"
@ -38,10 +45,10 @@ const { isOpen, toggle } = useSidebar()
/> />
</div> </div>
<a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs" type="editable-card" @edit="closeTab(activeTabIndex)"> <a-tabs v-model:activeKey="activeTabIndex" class="nc-root-tabs" type="editable-card" @edit="onEdit">
<a-tab-pane v-for="(tab, i) in tabs" :key="i"> <a-tab-pane v-for="(tab, i) of tabs" :key="i">
<template #tab> <template #tab>
<div class="flex align-center gap-2"> <div class="flex items-center gap-2">
<component :is="icon(tab)" class="text-sm" /> <component :is="icon(tab)" class="text-sm" />
{{ tab.title }} {{ tab.title }}
@ -49,12 +56,14 @@ const { isOpen, toggle } = useSidebar()
</template> </template>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<span class="flex-1" /> <span class="flex-1" />
<div class="flex justify-center align-self-center mr-2 min-w-[115px]">
<div v-show="isLoading" class="flex items-center gap-2 ml-3 text-white"> <div class="flex justify-center self-center mr-2 min-w-[115px]">
<div v-show="isLoading" class="flex items-center gap-2 ml-3 text-gray-200">
{{ $t('general.loading') }} {{ $t('general.loading') }}
<MdiReload :class="{ 'animate-infinite animate-spin': isLoading }" /> <MdiLoading :class="{ 'animate-infinite animate-spin': isLoading }" />
</div> </div>
</div> </div>
</div> </div>

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

Loading…
Cancel
Save