Browse Source

Merge pull request #3755 from nocodb/develop

pull/3756/head 0.97.0
github-actions[bot] 2 years ago committed by GitHub
parent
commit
a8190a49b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      .all-contributorsrc
  2. 24
      .do/deploy.template.yaml
  3. 2
      .run/Clear metadb.run.xml
  4. 12
      .run/Run GUI v2.run.xml
  5. 2
      .run/Run NocoDB Sqlite.run.xml
  6. 4
      .run/build.run.xml
  7. 2
      .run/dev.run.xml
  8. 7
      .run/start_nocodb.run.xml
  9. 2
      .run/watch_run_mysql.run.xml
  10. 1
      README.md
  11. 7
      packages/nc-gui/assets/style.scss
  12. 34
      packages/nc-gui/components.d.ts
  13. 8
      packages/nc-gui/components/api-client/Headers.vue
  14. 21
      packages/nc-gui/components/cell/Checkbox.vue
  15. 2
      packages/nc-gui/components/cell/DatePicker.vue
  16. 2
      packages/nc-gui/components/cell/DateTimePicker.vue
  17. 1
      packages/nc-gui/components/cell/MultiSelect.vue
  18. 1
      packages/nc-gui/components/cell/SingleSelect.vue
  19. 2
      packages/nc-gui/components/cell/TimePicker.vue
  20. 45
      packages/nc-gui/components/cell/Url.vue
  21. 2
      packages/nc-gui/components/cell/YearPicker.vue
  22. 12
      packages/nc-gui/components/cell/attachment/Modal.vue
  23. 4
      packages/nc-gui/components/cell/attachment/index.vue
  24. 10
      packages/nc-gui/components/cell/attachment/utils.ts
  25. 103
      packages/nc-gui/components/dashboard/TreeView.vue
  26. 18
      packages/nc-gui/components/dashboard/settings/AppStore.vue
  27. 5
      packages/nc-gui/components/dashboard/settings/Erd.vue
  28. 4
      packages/nc-gui/components/dashboard/settings/Metadata.vue
  29. 2
      packages/nc-gui/components/dashboard/settings/Misc.vue
  30. 45
      packages/nc-gui/components/dashboard/settings/Modal.vue
  31. 60
      packages/nc-gui/components/dlg/AirtableImport.vue
  32. 12
      packages/nc-gui/components/dlg/QuickImport.vue
  33. 8
      packages/nc-gui/components/dlg/TableCreate.vue
  34. 1
      packages/nc-gui/components/dlg/TableRename.vue
  35. 4
      packages/nc-gui/components/dlg/ViewCreate.vue
  36. 2
      packages/nc-gui/components/dlg/ViewDelete.vue
  37. 227
      packages/nc-gui/components/erd/Flow.vue
  38. 161
      packages/nc-gui/components/erd/RelationEdge.vue
  39. 121
      packages/nc-gui/components/erd/TableNode.vue
  40. 162
      packages/nc-gui/components/erd/View.vue
  41. 31
      packages/nc-gui/components/general/FullScreen.vue
  42. 4
      packages/nc-gui/components/general/HelpAndSupport.vue
  43. 14
      packages/nc-gui/components/general/MiniSidebar.vue
  44. 2
      packages/nc-gui/components/general/ReleaseInfo.vue
  45. 10
      packages/nc-gui/components/general/Social.vue
  46. 16
      packages/nc-gui/components/general/SocialCard.vue
  47. 45
      packages/nc-gui/components/general/Tooltip.vue
  48. 43
      packages/nc-gui/components/general/language/Menu.vue
  49. 10
      packages/nc-gui/components/general/language/index.vue
  50. 1
      packages/nc-gui/components/shared-view/AskPassword.vue
  51. 12
      packages/nc-gui/components/shared-view/Grid.vue
  52. 36
      packages/nc-gui/components/smartsheet-column/AdvancedOptions.vue
  53. 2
      packages/nc-gui/components/smartsheet-column/CheckboxOptions.vue
  54. 2
      packages/nc-gui/components/smartsheet-column/CurrencyOptions.vue
  55. 2
      packages/nc-gui/components/smartsheet-column/DateOptions.vue
  56. 2
      packages/nc-gui/components/smartsheet-column/DurationOptions.vue
  57. 50
      packages/nc-gui/components/smartsheet-column/EditOrAdd.vue
  58. 15
      packages/nc-gui/components/smartsheet-column/EditOrAddProvider.vue
  59. 12
      packages/nc-gui/components/smartsheet-column/FormulaOptions.vue
  60. 28
      packages/nc-gui/components/smartsheet-column/LinkedToAnotherRecordOptions.vue
  61. 17
      packages/nc-gui/components/smartsheet-column/LookupOptions.vue
  62. 4
      packages/nc-gui/components/smartsheet-column/PercentOptions.vue
  63. 6
      packages/nc-gui/components/smartsheet-column/RatingOptions.vue
  64. 26
      packages/nc-gui/components/smartsheet-column/RollupOptions.vue
  65. 6
      packages/nc-gui/components/smartsheet-column/SelectOptions.vue
  66. 8
      packages/nc-gui/components/smartsheet-header/Cell.vue
  67. 11
      packages/nc-gui/components/smartsheet-header/CellIcon.vue
  68. 4
      packages/nc-gui/components/smartsheet-header/Menu.vue
  69. 14
      packages/nc-gui/components/smartsheet-header/VirtualCell.vue
  70. 7
      packages/nc-gui/components/smartsheet-header/VirtualCellIcon.vue
  71. 2
      packages/nc-gui/components/smartsheet-toolbar/AddRow.vue
  72. 25
      packages/nc-gui/components/smartsheet-toolbar/ColumnFilter.vue
  73. 8
      packages/nc-gui/components/smartsheet-toolbar/ColumnFilterMenu.vue
  74. 42
      packages/nc-gui/components/smartsheet-toolbar/Erd.vue
  75. 4
      packages/nc-gui/components/smartsheet-toolbar/Export.vue
  76. 17
      packages/nc-gui/components/smartsheet-toolbar/ExportSubActions.vue
  77. 32
      packages/nc-gui/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue
  78. 24
      packages/nc-gui/components/smartsheet-toolbar/FieldsMenu.vue
  79. 35
      packages/nc-gui/components/smartsheet-toolbar/MoreActions.vue
  80. 2
      packages/nc-gui/components/smartsheet-toolbar/SearchData.vue
  81. 26
      packages/nc-gui/components/smartsheet-toolbar/ShareView.vue
  82. 11
      packages/nc-gui/components/smartsheet-toolbar/SharedViewList.vue
  83. 14
      packages/nc-gui/components/smartsheet-toolbar/SortListMenu.vue
  84. 64
      packages/nc-gui/components/smartsheet-toolbar/ViewActions.vue
  85. 37
      packages/nc-gui/components/smartsheet/ApiSnippet.vue
  86. 45
      packages/nc-gui/components/smartsheet/Form.vue
  87. 124
      packages/nc-gui/components/smartsheet/Gallery.vue
  88. 140
      packages/nc-gui/components/smartsheet/Grid.vue
  89. 5
      packages/nc-gui/components/smartsheet/Toolbar.vue
  90. 2
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  91. 41
      packages/nc-gui/components/smartsheet/expanded-form/Header.vue
  92. 33
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  93. 28
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  94. 24
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  95. 4
      packages/nc-gui/components/smartsheet/sidebar/index.vue
  96. 2
      packages/nc-gui/components/smartsheet/sidebar/toolbar/DebugMeta.vue
  97. 4
      packages/nc-gui/components/smartsheet/sidebar/toolbar/DeleteTable.vue
  98. 8
      packages/nc-gui/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue
  99. 3
      packages/nc-gui/components/smartsheet/sidebar/toolbar/index.vue
  100. 14
      packages/nc-gui/components/tabs/Auth.vue
  101. Some files were not shown because too many files have changed in this diff Show More

9
.all-contributorsrc

@ -864,6 +864,15 @@
"contributions": [
"code"
]
},
{
"login": "dolsem",
"name": "Denis Olsem",
"avatar_url": "https://avatars.githubusercontent.com/u/14323955?v=4",
"profile": "https://github.com/dolsem",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

24
.do/deploy.template.yaml

@ -0,0 +1,24 @@
spec:
name: nocodb
services:
- name: nocodb
image:
registry_type: DOCKER_HUB
registry: nocodb
repository: nocodb
tag: latest
run_command: "./server/scripts/digitalocean-postbuild.sh"
instance_size_slug: "basic-s"
health_check:
initial_delay_seconds: 10
http_path: /api/health
envs:
- key: NODE_ENV
value: "production"
- key: DATABASE_URL
scope: RUN_TIME
value: ${postgres.DATABASE_URL}
databases:
- name: postgres
engine: PG
production: false

2
.run/Clear metadb.run.xml

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Drop metadb" type="NodeJSConfigurationType" path-to-js-file="$PROJECT_DIR$/packages/nocodb/src/run/deleteMetaDb.js" working-dir="$PROJECT_DIR$/packages/nocodb/src/run">
<configuration default="false" name="Drop metadb" type="NodeJSConfigurationType" path-to-js-file="deleteMetaDb.js" working-dir="$PROJECT_DIR$/packages/nocodb/src/run">
<method v="2" />
</configuration>
</component>

12
.run/Run GUI v2.run.xml

@ -1,12 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run GUI" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/packages/nc-gui/package.json" />
<command value="run" />
<scripts>
<script value="dev" />
</scripts>
<node-interpreter value="project" />
<envs />
<method v="2" />
</configuration>
</component>

2
.run/Run NocoDB Sqlite.run.xml

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run NocoDB Sqlite" type="js.build_tools.npm" activateToolWindowBeforeRun="false">
<configuration default="false" name="Run::Backend" type="js.build_tools.npm" activateToolWindowBeforeRun="false">
<package-json value="$PROJECT_DIR$/packages/nocodb/package.json" />
<command value="run" />
<scripts>

4
.run/build.run.xml

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build Nc Common" type="js.build_tools.npm">
<configuration default="false" name="Build::SDK" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/packages/nocodb-sdk/package.json" />
<command value="run" />
<scripts>
@ -11,4 +11,4 @@
</envs>
<method v="2" />
</configuration>
</component>
</component>

2
.run/dev.run.xml

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run GUI v2" type="js.build_tools.npm">
<configuration default="false" name="Run::Frontend" type="js.build_tools.npm">
<package-json value="$PROJECT_DIR$/packages/nc-gui/package.json" />
<command value="run" />
<scripts>

7
.run/start_nocodb.run.xml

@ -0,0 +1,7 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Start::IDE" type="CompoundRunConfigurationType">
<toRun name="Run::Backend" type="js.build_tools.npm" />
<toRun name="Run::Frontend" type="js.build_tools.npm" />
<method v="2" />
</configuration>
</component>

2
.run/watch_run_mysql.run.xml

@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run NocoDB Mysql" type="js.build_tools.npm" activateToolWindowBeforeRun="false">
<configuration default="false" name="Run::Backend::Mysql" type="js.build_tools.npm" activateToolWindowBeforeRun="false">
<package-json value="$PROJECT_DIR$/packages/nocodb/package.json" />
<command value="run" />
<scripts>

1
README.md

@ -453,6 +453,7 @@ Our mission is to provide the most powerful no-code interface for databases whic
<td align="center"><a href="https://github.com/drsantam"><img src="https://avatars.githubusercontent.com/u/10681456?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Santam Chakraborty</b></sub></a><br /><a href="#translation-drsantam" title="Translation">🌍</a></td>
<td align="center"><a href="https://bandism.net/"><img src="https://avatars.githubusercontent.com/u/22633385?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Ikko Ashimine</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=eltociear" title="Code">💻</a></td>
<td align="center"><a href="http://asheerrizvi.com"><img src="https://avatars.githubusercontent.com/u/17976252?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Asheer Rizvi</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=asheerrizvi" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/dolsem"><img src="https://avatars.githubusercontent.com/u/14323955?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Denis Olsem</b></sub></a><br /><a href="https://github.com/nocodb/nocodb/commits?author=dolsem" title="Code">💻</a></td>
</tr>
</tbody>
</table>

7
packages/nc-gui/assets/style.scss

@ -1,4 +1,6 @@
@import 'ant-design-vue/dist/antd.variable.min.css';
@import '@braks/vue-flow/dist/style.css';
@import '@braks/vue-flow/dist/theme-default.css';
:root {
--header-height: 42px;
@ -242,3 +244,8 @@ a {
.ant-dropdown-menu-submenu-title{
@apply !pr-2;
}
.vue-flow__minimap {
transform: scale(75%);
transform-origin: bottom right;
}

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

@ -8,7 +8,6 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAutoComplete: typeof import('ant-design-vue/es')['AutoComplete']
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
@ -24,7 +23,6 @@ declare module '@vue/runtime-core' {
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
ADropdownButton: typeof import('ant-design-vue/es')['DropdownButton']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
@ -67,30 +65,28 @@ declare module '@vue/runtime-core' {
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATypography: typeof import('ant-design-vue/es')['Typography']
ATypographyTitle: typeof import('ant-design-vue/es')['TypographyTitle']
AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
BiFiletypeJson: typeof import('~icons/bi/filetype-json')['default']
BiFiletypeXlsx: typeof import('~icons/bi/filetype-xlsx')['default']
CilFullscreen: typeof import('~icons/cil/fullscreen')['default']
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']
EvaEmailOutline: typeof import('~icons/eva/email-outline')['default']
IcBaselineMoreVert: typeof import('~icons/ic/baseline-more-vert')['default']
IcOutlineInsertDriveFile: typeof import('~icons/ic/outline-insert-drive-file')['default']
IcRoundEdit: typeof import('~icons/ic/round-edit')['default']
IcRoundKeyboardArrowDown: typeof import('~icons/ic/round-keyboard-arrow-down')['default']
IcRoundSearch: typeof import('~icons/ic/round-search')['default']
IcTwotoneWidthFull: typeof import('~icons/ic/twotone-width-full')['default']
IcTwotoneWidthNormal: typeof import('~icons/ic/twotone-width-normal')['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']
MaterialSymbolsArrowCircleRightRounded: typeof import('~icons/material-symbols/arrow-circle-right-rounded')['default']
MaterialSymbolsAttachFile: typeof import('~icons/material-symbols/attach-file')['default']
MaterialSymbolsChevronLeftRounded: typeof import('~icons/material-symbols/chevron-left-rounded')['default']
MaterialSymbolsChevronRightRounded: typeof import('~icons/material-symbols/chevron-right-rounded')['default']
MaterialSymbolsCloseRounded: typeof import('~icons/material-symbols/close-rounded')['default']
MaterialSymbolsFileCopyOutline: typeof import('~icons/material-symbols/file-copy-outline')['default']
@ -100,13 +96,8 @@ declare module '@vue/runtime-core' {
MaterialSymbolsWarning: typeof import('~icons/material-symbols/warning')['default']
MdiAccount: typeof import('~icons/mdi/account')['default']
MdiAccountCircle: typeof import('~icons/mdi/account-circle')['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']
MdiAccountPlus: typeof import('~icons/mdi/account-plus')['default']
MdiAccountPlusOutline: typeof import('~icons/mdi/account-plus-outline')['default']
MdiAccountSupervisorOutline: typeof import('~icons/mdi/account-supervisor-outline')['default']
MdiAlpha: typeof import('~icons/mdi/alpha')['default']
MdiAlphaA: typeof import('~icons/mdi/alpha-a')['default']
MdiApi: typeof import('~icons/mdi/api')['default']
@ -115,7 +106,6 @@ declare module '@vue/runtime-core' {
MdiAt: typeof import('~icons/mdi/at')['default']
MdiBackburger: typeof import('~icons/mdi/backburger')['default']
MdiBookOpenOutline: typeof import('~icons/mdi/book-open-outline')['default']
MdiBookOpenPageVariantOutline: typeof import('~icons/mdi/book-open-page-variant-outline')['default']
MdiBugOutline: typeof import('~icons/mdi/bug-outline')['default']
MdiCalculator: typeof import('~icons/mdi/calculator')['default']
MdiCalendarMonth: typeof import('~icons/mdi/calendar-month')['default']
@ -123,7 +113,6 @@ declare module '@vue/runtime-core' {
MdiCellphoneMessage: typeof import('~icons/mdi/cellphone-message')['default']
MdiChat: typeof import('~icons/mdi/chat')['default']
MdiCheck: typeof import('~icons/mdi/check')['default']
MdiChevronDoubleLeft: typeof import('~icons/mdi/chevron-double-left')['default']
MdiChevronDown: typeof import('~icons/mdi/chevron-down')['default']
MdiChevronLeft: typeof import('~icons/mdi/chevron-left')['default']
MdiClose: typeof import('~icons/mdi/close')['default']
@ -137,7 +126,6 @@ declare module '@vue/runtime-core' {
MdiContentCopy: typeof import('~icons/mdi/content-copy')['default']
MdiContentSave: typeof import('~icons/mdi/content-save')['default']
MdiCurrencyUsd: typeof import('~icons/mdi/currency-usd')['default']
MdiDatabase: typeof import('~icons/mdi/database')['default']
MdiDatabaseOutline: typeof import('~icons/mdi/database-outline')['default']
MdiDelete: typeof import('~icons/mdi/delete')['default']
MdiDeleteOutline: typeof import('~icons/mdi/delete-outline')['default']
@ -153,6 +141,7 @@ declare module '@vue/runtime-core' {
MdiEmailArrowRightOutline: typeof import('~icons/mdi/email-arrow-right-outline')['default']
MdiExitToApp: typeof import('~icons/mdi/exit-to-app')['default']
MdiExport: typeof import('~icons/mdi/export')['default']
MdiEyeCircleOutline: typeof import('~icons/mdi/eye-circle-outline')['default']
MdiEyeOffOutline: typeof import('~icons/mdi/eye-off-outline')['default']
MdiFileDocumentOutline: typeof import('~icons/mdi/file-document-outline')['default']
MdiFileExcel: typeof import('~icons/mdi/file-excel')['default']
@ -161,26 +150,20 @@ declare module '@vue/runtime-core' {
MdiFileUploadOutline: typeof import('~icons/mdi/file-upload-outline')['default']
MdiFilterOutline: typeof import('~icons/mdi/filter-outline')['default']
MdiFlag: typeof import('~icons/mdi/flag')['default']
MdiFlashOutline: typeof import('~icons/mdi/flash-outline')['default']
MdiFolder: typeof import('~icons/mdi/folder')['default']
MdiFunction: typeof import('~icons/mdi/function')['default']
MdiGestureDoubleTap: typeof import('~icons/mdi/gesture-double-tap')['default']
MdiGithub: typeof import('~icons/mdi/github')['default']
MdiGmail: typeof import('~icons/mdi/gmail')['default']
MdiGridLarge: typeof import('~icons/mdi/grid-large')['default']
MdiHeart: typeof import('~icons/mdi/heart')['default']
MdiHook: typeof import('~icons/mdi/hook')['default']
MdiInformation: typeof import('~icons/mdi/information')['default']
MdiJson: typeof import('~icons/mdi/json')['default']
MdiKeyboardReturn: typeof import('~icons/mdi/keyboard-return')['default']
MdiKeyChange: typeof import('~icons/mdi/key-change')['default']
MdiKeyPlus: typeof import('~icons/mdi/key-plus')['default']
MdiKeyStar: typeof import('~icons/mdi/key-star')['default']
MdiLink: typeof import('~icons/mdi/link')['default']
MdiLinkVariant: typeof import('~icons/mdi/link-variant')['default']
MdiLinkVariantRemove: typeof import('~icons/mdi/link-variant-remove')['default']
MdiLoading: typeof import('~icons/mdi/loading')['default']
MdiLockOutlineIcon: typeof import('~icons/mdi/lock-outline-icon')['default']
MdiLogin: typeof import('~icons/mdi/login')['default']
MdiLogout: typeof import('~icons/mdi/logout')['default']
MdiMagnify: typeof import('~icons/mdi/magnify')['default']
@ -189,31 +172,24 @@ declare module '@vue/runtime-core' {
MdiMicrosoftTeams: typeof import('~icons/mdi/microsoft-teams')['default']
MdiMinusCircleOutline: typeof import('~icons/mdi/minus-circle-outline')['default']
MdiMoonFull: typeof import('~icons/mdi/moon-full')['default']
MdiNotebookCheckOutline: typeof import('~icons/mdi/notebook-check-outline')['default']
MdiNull: typeof import('~icons/mdi/null')['default']
MdiNumeric: typeof import('~icons/mdi/numeric')['default']
MdiOpenInNew: typeof import('~icons/mdi/open-in-new')['default']
MdiOpenInNewIcon: typeof import('~icons/mdi/open-in-new-icon')['default']
MdiPencil: typeof import('~icons/mdi/pencil')['default']
MdiPlus: typeof import('~icons/mdi/plus')['default']
MdiPlusBoxOutline: typeof import('~icons/mdi/plus-box-outline')['default']
MdiPlusCircleOutline: typeof import('~icons/mdi/plus-circle-outline')['default']
MdiPlusOutline: typeof import('~icons/mdi/plus-outline')['default']
MdiPlusRoundedOutline: typeof import('~icons/mdi/plus-rounded-outline')['default']
MdiRefresh: typeof import('~icons/mdi/refresh')['default']
MdiReload: typeof import('~icons/mdi/reload')['default']
MdiRocketLaunchOutline: typeof import('~icons/mdi/rocket-launch-outline')['default']
MdiScriptTextKeyOutline: typeof import('~icons/mdi/script-text-key-outline')['default']
MdiScriptTextOutline: typeof import('~icons/mdi/script-text-outline')['default']
MdiSearch: typeof import('~icons/mdi/search')['default']
MdiShieldLockOutline: typeof import('~icons/mdi/shield-lock-outline')['default']
MdiSlack: typeof import('~icons/mdi/slack')['default']
MdiSort: typeof import('~icons/mdi/sort')['default']
MdiStar: typeof import('~icons/mdi/star')['default']
MdiStarOutline: typeof import('~icons/mdi/star-outline')['default']
MdiStore: typeof import('~icons/mdi/store')['default']
MdiTable: typeof import('~icons/mdi/table')['default']
MdiTableArrowRight: typeof import('~icons/mdi/table-arrow-right')['default']
MdiTableBorder: typeof import('~icons/mdi/table-border')['default']
MdiTableLarge: typeof import('~icons/mdi/table-large')['default']
MdiText: typeof import('~icons/mdi/text')['default']
MdiThumbUp: typeof import('~icons/mdi/thumb-up')['default']
@ -222,8 +198,10 @@ declare module '@vue/runtime-core' {
MdiUpload: typeof import('~icons/mdi/upload')['default']
MdiUploadOutline: typeof import('~icons/mdi/upload-outline')['default']
MdiViewListOutline: typeof import('~icons/mdi/view-list-outline')['default']
MdiWarning: typeof import('~icons/mdi/warning')['default']
MdiWhatsapp: typeof import('~icons/mdi/whatsapp')['default']
MdiXml: typeof import('~icons/mdi/xml')['default']
MiCircleWarning: typeof import('~icons/mi/circle-warning')['default']
PhFileCsv: typeof import('~icons/ph/file-csv')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']

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

@ -87,7 +87,13 @@ const deleteHeaderRow = (idx: number) => vModel.value.splice(idx, 1)
</td>
<td class="px-2 w-min-[400px]">
<a-form-item>
<a-select v-model:value="headerRow.name" size="large" placeholder="Key" class="nc-input-hook-header-key">
<a-select
v-model:value="headerRow.name"
size="large"
placeholder="Key"
class="nc-input-hook-header-key"
dropdown-class-name="nc-dropdown-webhook-header"
>
<a-select-option v-for="(header, i) in headerList" :key="i" :value="header">
{{ header }}
</a-select-option>

21
packages/nc-gui/components/cell/Checkbox.vue

@ -2,18 +2,23 @@
import { ColumnInj, IsFormInj, ReadonlyInj, getMdiIcon, inject } from '#imports'
interface Props {
modelValue?: boolean | undefined | number
// If the previous cell value was a text, the initial checkbox value is a string type
// otherwise it can be either a boolean, or a string representing a boolean, i.e '0' or '1'
modelValue?: boolean | string | '0' | '1'
}
interface Emits {
(event: 'update:modelValue', model: boolean | undefined | number): void
(event: 'update:modelValue', model: boolean): void
}
const props = defineProps<Props>()
const emits = defineEmits<Emits>()
let vModel = $(useVModel(props, 'modelValue', emits))
let vModel = $computed({
get: () => !!props.modelValue && props.modelValue !== '0',
set: (val) => emits('update:modelValue', val),
})
const column = inject(ColumnInj)
@ -42,9 +47,15 @@ function onClick() {
<template>
<div
class="flex"
:class="{ 'justify-center': !isForm, 'nc-cell-hover-show': !vModel && !readOnly, 'opacity-0': readOnly && !vModel }"
:class="{
'justify-center': !isForm,
'w-full': isForm,
'nc-cell-hover-show': !vModel && !readOnly,
'opacity-0': readOnly && !vModel,
}"
@click="onClick"
>
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }" @click="onClick">
<div class="px-1 pt-1 rounded-full items-center" :class="{ 'bg-gray-100': !vModel }">
<component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"
:style="{

2
packages/nc-gui/components/cell/DatePicker.vue

@ -67,7 +67,7 @@ const placeholder = computed(() => (isDateInvalid ? 'Invalid date' : ''))
:placeholder="placeholder"
:allow-clear="!readOnly"
:input-read-only="true"
:dropdown-class-name="randomClass"
:dropdown-class-name="`${randomClass} nc-picker-date`"
:open="readOnly ? false : open"
@click="open = !open"
>

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

@ -67,7 +67,7 @@ watch(
:placeholder="isDateInvalid ? 'Invalid date' : ''"
:allow-clear="!readOnly"
:input-read-only="true"
:dropdown-class-name="randomClass"
:dropdown-class-name="`${randomClass} nc-picker-datetime`"
:open="readOnly ? false : open"
:disabled="readOnly"
@click="open = !open"

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

@ -134,6 +134,7 @@ watch(isOpen, (n, _o) => {
:show-search="false"
:open="isOpen"
:disabled="readOnly"
dropdown-class-name="nc-dropdown-multi-select-cell"
@keydown="handleKeys"
@click="isOpen = !isOpen"
>

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

@ -73,6 +73,7 @@ watch(isOpen, (n, _o) => {
:open="isOpen"
:disabled="readOnly"
:show-arrow="!readOnly && (active || vModel === null)"
dropdown-class-name="nc-dropdown-single-select-cell"
@select="isOpen = false"
@keydown="handleKeys"
@click="isOpen = !isOpen"

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

@ -79,7 +79,7 @@ watch(
:allow-clear="!readOnly"
:input-read-only="true"
:open="readOnly ? false : open"
:popup-class-name="randomClass"
:popup-class-name="`${randomClass} nc-picker-time`"
@click="open = !open"
@ok="open = !open"
>

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

@ -1,21 +1,35 @@
<script setup lang="ts">
import type { VNodeRef } from '@vue/runtime-core'
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { ColumnInj, EditModeInj, computed, inject, isValidURL } from '#imports'
import MiCircleWarning from '~icons/mi/circle-warning'
import {
CellUrlDisableOverlayInj,
ColumnInj,
EditModeInj,
computed,
inject,
isValidURL,
ref,
useCellUrlConfig,
useI18n,
watch,
} from '#imports'
const { modelValue: value } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
interface Props {
modelValue?: string | null
}
const { modelValue: value } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj)!
const disableOverlay = inject(CellUrlDisableOverlayInj)
// Used in the logic of when to display error since we are not storing the url if its not valid
const localState = ref(value)
@ -40,6 +54,8 @@ const url = computed(() => {
return `https://${value}`
})
const { cellUrlOptions } = useCellUrlConfig(url)
const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
watch(
@ -59,7 +75,22 @@ watch(
<div class="flex flex-row items-center justify-between">
<input v-if="editEnabled" :ref="focus" v-model="vModel" class="outline-none text-sm w-full" @blur="editEnabled = false" />
<nuxt-link v-else-if="isValid" class="text-sm underline hover:opacity-75" :to="url" target="_blank">{{ value }} </nuxt-link>
<nuxt-link
v-else-if="isValid && !cellUrlOptions?.overlay"
class="z-3 text-sm underline hover:opacity-75"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
>
{{ value }}
</nuxt-link>
<nuxt-link
v-else-if="isValid && !disableOverlay && cellUrlOptions?.overlay"
class="z-3 w-full h-full text-center !no-underline hover:opacity-75"
:to="url"
:target="cellUrlOptions?.behavior === 'replace' ? undefined : '_blank'"
>
{{ cellUrlOptions.overlay }}
</nuxt-link>
<span v-else class="w-9/10 overflow-ellipsis overflow-hidden">{{ value }}</span>

2
packages/nc-gui/components/cell/YearPicker.vue

@ -66,7 +66,7 @@ const placeholder = computed(() => (isYearInvalid ? 'Invalid year' : ''))
:allow-clear="!readOnly"
:input-read-only="true"
:open="readOnly ? false : open"
:dropdown-class-name="randomClass"
:dropdown-class-name="`${randomClass} nc-picker-year`"
@click="open = !open"
@change="open = !open"
>

12
packages/nc-gui/components/cell/attachment/Modal.vue

@ -9,7 +9,7 @@ const { isUIAllowed } = useUIPermission()
const {
open,
isLoading,
isPublicGrid,
isPublic,
isReadonly,
visibleItems,
modalVisible,
@ -58,11 +58,17 @@ function onClick(item: Record<string, any>) {
</script>
<template>
<a-modal v-model:visible="modalVisible" class="nc-attachment-modal" width="80%" :footer="null">
<a-modal
v-model:visible="modalVisible"
class="nc-attachment-modal"
width="80%"
:footer="null"
wrap-class-name="nc-modal-attachment-expand-cell"
>
<template #title>
<div class="flex gap-4">
<div
v-if="isSharedForm || (!isReadonly && isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked)"
v-if="isSharedForm || (!isReadonly && isUIAllowed('tableAttachment') && !isPublic && !isLocked)"
class="nc-attach-file group"
@click="open"
>

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

@ -65,7 +65,7 @@ watch(
} else {
nextTick(() => {
const nextCell = cellRefs.value.reduceRight((cell, curr) => {
if (!cell && curr.dataset.key === `${rowIndex}${column.value.id}`) cell = curr
if (!cell && curr.dataset.key === `${rowIndex}${column.value!.id}`) cell = curr
return cell
}, undefined as HTMLTableDataCellElement | undefined)
@ -118,7 +118,7 @@ onKeyDown('Escape', () => {
watch(
() => storedFiles.value.length || 0,
() => {
rowState.value[column.value.title!] = storedFiles.value
rowState.value[column.value!.title!] = storedFiles.value
},
)

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

@ -1,9 +1,9 @@
import { message } from 'ant-design-vue'
import FileSaver from 'file-saver'
import { useI18n } from 'vue-i18n'
import {
ColumnInj,
EditModeInj,
IsFormInj,
IsPublicInj,
MetaInj,
NOCO,
@ -14,11 +14,11 @@ import {
ref,
useApi,
useFileDialog,
useI18n,
useInjectionState,
useProject,
watch,
} from '#imports'
import { IsFormInj } from '~/context'
import MdiPdfBox from '~icons/mdi/pdf-box'
import MdiFileWordOutline from '~icons/mdi/file-word-outline'
import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box'
@ -40,9 +40,9 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
const isForm = inject(IsFormInj, ref(false))
const meta = inject(MetaInj)!
const meta = inject(MetaInj, ref())
const column = inject(ColumnInj)!
const column = inject(ColumnInj, ref())
const editEnabled = inject(EditModeInj, ref(false))
@ -119,7 +119,7 @@ export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
try {
const data = await api.storage.upload(
{
path: [NOCO, project.value.title, meta.value.title, column.value.title].join('/'),
path: [NOCO, project.value.title, meta.value?.title, column.value?.title].join('/'),
},
{
files: file,

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

@ -104,7 +104,7 @@ const initSortable = (el: Element) => {
// update the item order
await $api.dbTable.reorder(item.id as string, {
order: item.order as any,
order: item.order,
})
},
animation: 150,
@ -142,7 +142,7 @@ const reloadTables = async () => {
}
const addTableTab = (table: TableType) => {
addTab({ title: table.title, id: table.id, type: table.type as any })
addTab({ title: table.title, id: table.id, type: table.type as TabType })
}
function openRenameTableDialog(table: TableType, rightClick = false) {
@ -218,7 +218,7 @@ function openTableCreateDialog() {
<template>
<div class="nc-treeview-container flex flex-col">
<a-dropdown :trigger="['contextmenu']">
<a-dropdown :trigger="['contextmenu']" overlay-class-name="nc-dropdown-tree-view-context-menu">
<div class="pt-2 pl-2 pb-2 flex-1 overflow-y-auto flex flex-col scrollbar-thin-dull" :class="{ 'mb-[20px]': isSharedBase }">
<div class="py-1 px-3 flex w-full items-center gap-1 cursor-pointer" @contextmenu="setMenuContext('main')">
<span class="flex-1 text-bold uppercase nc-project-tree text-gray-500 font-weight-bold">
@ -238,7 +238,7 @@ function openTableCreateDialog() {
<span class="text-gray-500 group-hover:(text-primary/100) flex-1 nc-add-new-table">{{ $t('tooltip.addTable') }}</span>
<a-dropdown v-if="!isSharedBase" :trigger="['click']" @click.stop>
<a-dropdown v-if="!isSharedBase" :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop>
<MdiDotsVertical class="transition-opacity opacity-0 group-hover:opacity-100 nc-import-menu" />
<template #overlay>
@ -286,7 +286,7 @@ function openTableCreateDialog() {
<a-menu-item v-if="isUIAllowed('importRequest')" key="add-new-table" class="py-1 rounded-b">
<a
v-t="['e:datasource:import-request']"
v-e="['e:datasource:import-request']"
href="https://github.com/nocodb/nocodb/issues/2052"
target="_blank"
class="prose-sm hover:(!text-primary !opacity-100) color-transition nc-project-menu-item group after:(!rounded-b)"
@ -306,60 +306,63 @@ function openTableCreateDialog() {
<div
v-for="table of tables"
:key="table.id"
v-t="['a:table:open']"
v-e="['a:table:open']"
:class="[
{ hidden: !filteredTables?.includes(table), active: activeTable === table.title },
`nc-project-tree-tbl nc-project-tree-tbl-${table.title}`,
]"
class="nc-tree-item pl-5 pr-3 py-2 text-sm cursor-pointer group"
class="nc-tree-item text-sm cursor-pointer group"
:data-order="table.order"
:data-id="table.id"
@click="addTableTab(table)"
>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto">
<MdiDrag
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@click.stop.prevent
/>
<component
:is="icon(table)"
class="nc-view-icon text-xs"
:class="{ 'group-hover:hidden group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
</div>
<div class="nc-tbl-title flex-1">
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText>
<GeneralTooltip wrapper-class="pl-5 pr-3 py-2" modifier-key="Alt">
<template #title>{{ table.table_name }}</template>
<div class="flex items-center gap-2 h-full" @contextmenu="setMenuContext('table', table)">
<div class="flex w-auto">
<MdiDrag
v-if="isUIAllowed('treeview-drag-n-drop')"
:class="`nc-child-draggable-icon-${table.title}`"
class="nc-drag-icon text-xs hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 cursor-move"
@click.stop.prevent
/>
<component
:is="icon(table)"
class="nc-view-icon text-xs"
:class="{ 'group-hover:hidden group-hover:text-gray-500': isUIAllowed('treeview-drag-n-drop') }"
/>
</div>
<div class="nc-tbl-title flex-1">
<GeneralTruncateText>{{ table.title }}</GeneralTruncateText>
</div>
<a-dropdown
v-if="!isSharedBase && !isLocked && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))"
:trigger="['click']"
@click.stop
>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table)">
<div class="nc-project-menu-item">
{{ $t('general.rename') }}
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('table-delete')" @click="deleteTable(table)">
<div class="nc-project-menu-item">
{{ $t('general.delete') }}
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<a-dropdown
v-if="!isSharedBase && !isLocked && (isUIAllowed('table-rename') || isUIAllowed('table-delete'))"
:trigger="['click']"
@click.stop
>
<MdiMenuIcon class="transition-opacity opacity-0 group-hover:opacity-100" />
<template #overlay>
<a-menu class="!py-0 rounded text-sm">
<a-menu-item v-if="isUIAllowed('table-rename')" @click="openRenameTableDialog(table)">
<div class="nc-project-menu-item">
{{ $t('general.rename') }}
</div>
</a-menu-item>
<a-menu-item v-if="isUIAllowed('table-delete')" @click="deleteTable(table)">
<div class="nc-project-menu-item">
{{ $t('general.delete') }}
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</GeneralTooltip>
</div>
</div>
</div>

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

@ -72,7 +72,14 @@ onMounted(async () => {
</script>
<template>
<a-modal v-model:visible="showPluginInstallModal" :closable="false" centered min-height="300" :footer="null">
<a-modal
v-model:visible="showPluginInstallModal"
:closable="false"
centered
min-height="300"
:footer="null"
wrap-class-name="nc-modal-plugin-install"
>
<AppInstall
v-if="pluginApp && showPluginInstallModal"
:id="pluginApp.id"
@ -81,7 +88,14 @@ onMounted(async () => {
/>
</a-modal>
<a-modal v-model:visible="showPluginUninstallModal" :closable="false" width="24rem" centered :footer="null">
<a-modal
v-model:visible="showPluginUninstallModal"
:closable="false"
width="24rem"
centered
:footer="null"
wrap-class-name="nc-modal-plugin-uninstall"
>
<div class="flex flex-col h-full">
<div class="flex flex-row justify-center mt-2 text-center w-full text-base">
{{ `Click on confirm to reset ${pluginApp && pluginApp.title}` }}

5
packages/nc-gui/components/dashboard/settings/Erd.vue

@ -0,0 +1,5 @@
<template>
<div class="w-full h-full !py-0 !px-2" style="height: 70vh">
<ErdView />
</div>
</template>

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

@ -83,7 +83,7 @@ const columns = [
<div class="flex flex-col w-3/5">
<div class="flex flex-row justify-end items-center w-full mb-4">
<!-- Reload -->
<a-button v-t="['a:proj-meta:meta-data:reload']" class="self-start nc-btn-metasync-reload" @click="loadMetaDiff">
<a-button v-e="['a:proj-meta:meta-data:reload']" class="self-start nc-btn-metasync-reload" @click="loadMetaDiff">
<div class="flex items-center gap-2 text-gray-600 font-light">
<MdiReload :class="{ 'animate-infinite animate-spin !text-success': isLoading }" />
{{ $t('general.reload') }}
@ -112,7 +112,7 @@ const columns = [
<div class="flex place-content-center w-2/5">
<!-- Sync Now -->
<div v-if="isDifferent">
<a-button v-t="['a:proj-meta:meta-data:sync']" class="nc-btn-metasync-sync-now" type="primary" @click="syncMetaDiff">
<a-button v-e="['a:proj-meta:meta-data:sync']" class="nc-btn-metasync-sync-now" type="primary" @click="syncMetaDiff">
<div class="flex items-center gap-2">
<MdiDatabaseSync />
{{ $t('activity.metaSync') }}

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

@ -10,7 +10,7 @@ watch(includeM2M, async () => await loadTables())
<div class="flex flex-col w-full">
<div class="flex flex-row items-center w-full mb-4 gap-2">
<!-- Show M2M Tables -->
<a-checkbox v-model:checked="includeM2M" v-t="['c:themes:show-m2m-tables']">{{
<a-checkbox v-model:checked="includeM2M" v-e="['c:themes:show-m2m-tables']" class="nc-settings-meta-misc">{{
$t('msg.info.showM2mTables')
}}</a-checkbox>
</div>

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

@ -5,6 +5,7 @@ import AppStore from './AppStore.vue'
import Metadata from './Metadata.vue'
import UIAcl from './UIAcl.vue'
import Misc from './Misc.vue'
import Erd from './Erd.vue'
import { useNuxtApp } from '#app'
import { useI18n, useUIPermission, useVModel, watch } from '#imports'
import ApiTokenManagement from '~/components/tabs/auth/ApiTokenManagement.vue'
@ -53,20 +54,24 @@ const tabsInfo: TabGroup = {
title: t('title.teamAndAuth'),
icon: TeamFillIcon,
subTabs: {
...(isUIAllowed('userMgmtTab') && {
usersManagement: {
// Users Management
title: t('title.userMgmt'),
body: UserManagement,
},
}),
...(isUIAllowed('apiTokenTab') && {
apiTokenManagement: {
// API Tokens Management
title: t('title.apiTokenMgmt'),
body: ApiTokenManagement,
},
}),
...(isUIAllowed('userMgmtTab')
? {
usersManagement: {
// Users Management
title: t('title.userMgmt'),
body: UserManagement,
},
}
: {}),
...(isUIAllowed('apiTokenTab')
? {
apiTokenManagement: {
// API Tokens Management
title: t('title.apiTokenMgmt'),
body: ApiTokenManagement,
},
}
: {}),
},
onClick: () => {
$e('c:settings:team-auth')
@ -86,7 +91,7 @@ const tabsInfo: TabGroup = {
$e('c:settings:appstore')
},
},
metaData: {
projMetaData: {
// Project Metadata
title: t('title.projMeta'),
icon: MultipleTableIcon,
@ -104,6 +109,13 @@ const tabsInfo: TabGroup = {
$e('c:table:ui-acl')
},
},
erd: {
title: t('title.erdView'),
body: Erd,
onClick: () => {
$e('c:settings:erd')
},
},
misc: {
title: t('general.misc'),
body: Misc,
@ -159,6 +171,7 @@ watch(
:footer="null"
width="max(90vw, 600px)"
:closable="false"
wrap-class-name="nc-modal-settings"
@cancel="emits('update:modelValue', false)"
>
<!-- Settings -->
@ -207,7 +220,7 @@ watch(
</a-menu-item>
</a-menu>
<component :is="selectedSubTab.body" class="px-2 py-6" />
<component :is="selectedSubTab?.body" class="px-2 py-6" />
</a-layout-content>
</a-layout>
</a-modal>

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

@ -65,8 +65,8 @@ const syncSource = ref({
})
const validators = computed(() => ({
'details.apiKey': [fieldRequiredValidator],
'details.syncSourceUrlOrId': [fieldRequiredValidator],
'details.apiKey': [fieldRequiredValidator()],
'details.syncSourceUrlOrId': [fieldRequiredValidator()],
}))
const dialogShow = computed({
@ -208,9 +208,10 @@ onMounted(async () => {
socket.on('progress', async (d: Record<string, any>) => {
progress.value.push(d)
// FIXME: this doesn't work
await nextTick(() => {
;(logRef.value?.$el as HTMLDivElement).scrollTo()
const container: HTMLDivElement = logRef.value?.$el?.firstElementChild
if (!container) return
container.scrollTop = container.scrollHeight
})
if (d.status === 'COMPLETED') {
@ -231,7 +232,13 @@ onBeforeUnmount(() => {
</script>
<template>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" class="p-2" @keydown.esc="dialogShow = false">
<a-modal
v-model:visible="dialogShow"
width="max(30vw, 600px)"
class="p-2"
wrap-class-name="nc-modal-airtable-import"
@keydown.esc="dialogShow = false"
>
<div class="px-5">
<!-- Quick Import -->
<div class="mt-5 prose-xl font-weight-bold">{{ $t('title.quickImport') }} - AIRTABLE</div>
@ -245,7 +252,8 @@ onBeforeUnmount(() => {
href="https://docs.nocodb.com/setup-and-usages/import-airtable-to-sql-database-within-a-minute-for-free/#get-airtable-credentials"
class="prose-sm underline text-grey text-xs"
target="_blank"
>{{ $t('msg.info.airtable.credentials') }}
>
{{ $t('msg.info.airtable.credentials') }}
</a>
</div>
@ -280,30 +288,30 @@ onBeforeUnmount(() => {
<!-- Import Secondary Views -->
<div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncViews">{{
$t('labels.importSecondaryViews')
}}</a-checkbox>
<a-checkbox v-model:checked="syncSource.details.options.syncViews">
{{ $t('labels.importSecondaryViews') }}
</a-checkbox>
</div>
<!-- Import Rollup Columns -->
<div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncRollup">{{
$t('labels.importRollupColumns')
}}</a-checkbox>
<a-checkbox v-model:checked="syncSource.details.options.syncRollup">
{{ $t('labels.importRollupColumns') }}
</a-checkbox>
</div>
<!-- Import Lookup Columns -->
<div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncLookup">{{
$t('labels.importLookupColumns')
}}</a-checkbox>
<a-checkbox v-model:checked="syncSource.details.options.syncLookup">
{{ $t('labels.importLookupColumns') }}
</a-checkbox>
</div>
<!-- Import Attachment Columns -->
<div class="my-2">
<a-checkbox v-model:checked="syncSource.details.options.syncAttachment">{{
$t('labels.importAttachmentColumns')
}}</a-checkbox>
<a-checkbox v-model:checked="syncSource.details.options.syncAttachment">
{{ $t('labels.importAttachmentColumns') }}
</a-checkbox>
</div>
<!-- Import Formula Columns -->
@ -311,9 +319,9 @@ onBeforeUnmount(() => {
<template #title>
<span>Coming Soon!</span>
</template>
<a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>{{
$t('labels.importFormulaColumns')
}}</a-checkbox>
<a-checkbox v-model:checked="syncSource.details.options.syncFormula" disabled>
{{ $t('labels.importFormulaColumns') }}
</a-checkbox>
</a-tooltip>
</a-form>
@ -321,8 +329,8 @@ onBeforeUnmount(() => {
<!-- Questions / Help - Reach out here -->
<div>
<a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank"
>{{ $t('general.questions') }} / {{ $t('general.help') }} - {{ $t('general.reachOut') }}</a
<a href="https://github.com/nocodb/nocodb/issues/2052" target="_blank">
{{ $t('general.questions') }} / {{ $t('general.help') }} - {{ $t('general.reachOut') }}</a
>
<br />
@ -331,8 +339,8 @@ onBeforeUnmount(() => {
{{ $t('general.betaNote') }}
<a class="prose-sm" href="https://github.com/nocodb/nocodb/discussions/2122" target="_blank">{{
$t('general.moreInfo')
}}</a
>.
}}</a>
.
</div>
</div>
</div>
@ -385,7 +393,7 @@ onBeforeUnmount(() => {
<!-- Import -->
<a-button
key="submit"
v-t="['c:sync-airtable:save-and-sync']"
v-e="['c:sync-airtable:save-and-sync']"
type="primary"
class="nc-btn-airtable-import"
:disabled="disableImportButton"

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

@ -70,8 +70,8 @@ const isImportTypeCsv = computed(() => importType === 'csv')
const IsImportTypeExcel = computed(() => importType === 'excel')
const validators = computed(() => ({
url: [fieldRequiredValidator, importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator],
maxRowsToParse: [fieldRequiredValidator],
url: [fieldRequiredValidator(), importUrlValidator, isImportTypeCsv.value ? importCsvUrlValidator : importExcelUrlValidator],
maxRowsToParse: [fieldRequiredValidator()],
}))
const { validate, validateInfos } = useForm(importState, validators)
@ -284,7 +284,12 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => {
</script>
<template>
<a-modal v-model:visible="dialogShow" :width="modalWidth" @keydown.esc="dialogShow = false">
<a-modal
v-model:visible="dialogShow"
:width="modalWidth"
wrap-class-name="nc-modal-quick-import"
@keydown.esc="dialogShow = false"
>
<div class="px-5">
<div class="prose-xl font-weight-bold my-5">{{ importMeta.header }}</div>
@ -298,6 +303,7 @@ const customReqCbk = (customReqArgs: { file: any; onSuccess: () => void }) => {
:import-only="importOnly"
:quick-import-type="importType"
:max-rows-to-parse="importState.parserConfig.maxRowsToParse"
class="nc-quick-import-template-editor"
@import="handleImport"
/>

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

@ -80,7 +80,13 @@ onMounted(() => {
</script>
<template>
<a-modal v-model:visible="dialogShow" width="max(30vw, 600px)" centered @keydown.esc="dialogShow = false">
<a-modal
v-model:visible="dialogShow"
width="max(30vw, 600px)"
centered
wrap-class-name="nc-modal-table-create"
@keydown.esc="dialogShow = false"
>
<template #footer>
<a-button key="back" size="large" @click="dialogShow = false">{{ $t('general.cancel') }}</a-button>

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

@ -125,6 +125,7 @@ const renameTable = async () => {
v-model:visible="dialogShow"
:title="$t('activity.renameTable')"
:mask-closable="false"
wrap-class-name="nc-modal-table-rename"
@keydown.esc="dialogShow = false"
@finish="renameTable"
>

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

@ -42,7 +42,7 @@ const { t } = useI18n()
const { isLoading: loading, api } = useApi()
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const viewList = inject(ViewListInj)
@ -140,7 +140,7 @@ async function onSubmit() {
</script>
<template>
<a-modal v-model:visible="vModel" class="!top-[35%]" :confirm-loading="loading">
<a-modal v-model:visible="vModel" class="!top-[35%]" :confirm-loading="loading" wrap-class-name="nc-modal-view-create">
<template #title>
{{ $t('general.create') }} <span class="text-capitalize">{{ typeAlias }}</span> {{ $t('objects.view') }}
</template>

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

@ -48,7 +48,7 @@ async function onDelete() {
</script>
<template>
<a-modal v-model:visible="vModel" class="!top-[35%]" :confirm-loading="isLoading">
<a-modal v-model:visible="vModel" class="!top-[35%]" :confirm-loading="isLoading" wrap-class-name="nc-modal-view-delete">
<template #title> {{ $t('general.delete') }} {{ $t('objects.view') }} </template>
{{ $t('msg.info.deleteViewConfirmation') }}

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

@ -0,0 +1,227 @@
<script setup lang="ts">
import type { Edge, Node } from '@braks/vue-flow'
import { Background, Controls, VueFlow, useVueFlow } from '@braks/vue-flow'
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import dagre from 'dagre'
import TableNode from './TableNode.vue'
import RelationEdge from './RelationEdge.vue'
interface Props {
tables: any[]
config: {
showPkAndFk: boolean
showViews: boolean
showAllColumns: boolean
singleTableMode: boolean
showJunctionTableNames: boolean
}
}
const { tables, config } = defineProps<Props>()
const { metasWithIdAsKey } = useMetas()
const { $destroy, fitView } = useVueFlow()
const nodes = ref<Node[]>([])
const edges = ref<Edge[]>([])
let dagreGraph: dagre.graphlib.Graph
const initDagre = () => {
dagreGraph = new dagre.graphlib.Graph()
dagreGraph.setDefaultEdgeLabel(() => ({}))
dagreGraph.setGraph({ rankdir: 'LR' })
}
const populateInitialNodes = () => {
nodes.value = tables.flatMap((table) => {
if (!table.id) return []
const columns =
metasWithIdAsKey.value[table.id].columns?.filter(
(col) => config.showAllColumns || (!config.showAllColumns && col.uidt === UITypes.LinkToAnotherRecord),
) || []
dagreGraph.setNode(table.id, { width: 250, height: 50 * columns.length })
return [
{
id: table.id,
data: { ...metasWithIdAsKey.value[table.id], showPkAndFk: config.showPkAndFk, showAllColumns: config.showAllColumns },
type: 'custom',
position: { x: 0, y: 0 },
},
]
})
}
const populateEdges = () => {
const ltarColumns = tables.reduce<ColumnType[]>((acc, table) => {
const meta = metasWithIdAsKey.value[table.id!]
const columns = meta.columns?.filter(
(column: ColumnType) => column.uidt === UITypes.LinkToAnotherRecord && column.system !== 1,
)
columns?.forEach((column: ColumnType) => {
if ((column.colOptions as LinkToAnotherRecordType)?.type === 'hm') {
acc.push(column)
}
if ((column.colOptions as LinkToAnotherRecordType).type === 'mm') {
// Avoid duplicate mm connections
const correspondingColumn = acc.find(
(c) =>
(c.colOptions as LinkToAnotherRecordType | FormulaType | RollupType | LookupType).type === 'mm' &&
(c.colOptions as LinkToAnotherRecordType).fk_parent_column_id ===
(column.colOptions as LinkToAnotherRecordType).fk_child_column_id &&
(c.colOptions as LinkToAnotherRecordType).fk_child_column_id ===
(column.colOptions as LinkToAnotherRecordType).fk_parent_column_id,
)
if (!correspondingColumn) {
acc.push(column)
}
}
})
return acc
}, [] as ColumnType[])
const edgeMMTableLabel = (modelId: string) => {
const mmModel = metasWithIdAsKey.value[modelId]
if (mmModel.title !== mmModel.table_name) {
return `${mmModel.title} (${mmModel.table_name})`
}
return mmModel.title
}
edges.value = ltarColumns.map((column) => {
const source = column.fk_model_id!
const target = (column.colOptions as LinkToAnotherRecordType).fk_related_model_id!
let sourceColumnId, targetColumnId
let edgeLabel = ''
if ((column.colOptions as LinkToAnotherRecordType).type === 'hm') {
sourceColumnId = (column.colOptions as LinkToAnotherRecordType).fk_child_column_id
targetColumnId = (column.colOptions as LinkToAnotherRecordType).fk_child_column_id
}
if ((column.colOptions as LinkToAnotherRecordType).type === 'mm') {
sourceColumnId = (column.colOptions as LinkToAnotherRecordType).fk_parent_column_id
targetColumnId = (column.colOptions as LinkToAnotherRecordType).fk_child_column_id
edgeLabel = config.showJunctionTableNames
? edgeMMTableLabel((column.colOptions as LinkToAnotherRecordType).fk_mm_model_id!)
: ''
}
if (source !== target) dagreGraph.setEdge(source, target)
return {
id: `e-${sourceColumnId}-${source}-${targetColumnId}-${target}-#${edgeLabel}`,
source: `${source}`,
target: `${target}`,
sourceHandle: `s-${sourceColumnId}-${source}`,
targetHandle: `d-${targetColumnId}-${target}`,
type: 'custom',
data: {
column,
isSelfRelation: source === target && sourceColumnId === targetColumnId,
label: edgeLabel,
},
}
})
}
const connectNonConnectedNodes = () => {
const connectedNodes = new Set<string>()
edges.value.forEach((edge) => {
connectedNodes.add(edge.source)
connectedNodes.add(edge.target)
})
const nonConnectedNodes = tables.filter((table) => !connectedNodes.has(table.id!))
if (nonConnectedNodes.length === 0) return
if (nonConnectedNodes.length === 1) {
const firstTable = tables.find((table) => table.type === 'table' && table.id !== nonConnectedNodes[0].id)
if (!firstTable) return
dagreGraph.setEdge(nonConnectedNodes[0].id, firstTable.id)
return
}
const firstNode = nonConnectedNodes[0]
nonConnectedNodes.forEach((node, index) => {
if (index === 0) return
const source = firstNode.id
const target = node.id
dagreGraph.setEdge(source, target)
})
}
const layoutNodes = () => {
if (!config.singleTableMode) connectNonConnectedNodes()
dagre.layout(dagreGraph)
nodes.value = nodes.value.flatMap((node) => {
const nodeWithPosition = dagreGraph.node(node.id)
if (!nodeWithPosition) return []
return [{ ...node, position: { x: nodeWithPosition.x, y: nodeWithPosition.y } } as Node]
})
}
const init = () => {
initDagre()
populateInitialNodes()
populateEdges()
layoutNodes()
setTimeout(() => fitView({ duration: 300 }))
}
init()
onScopeDispose($destroy)
watch([() => tables, () => config], init, { deep: true, flush: 'pre' })
</script>
<template>
<VueFlow :nodes="nodes" :edges="edges" elevate-edges-on-select>
<Controls class="!left-auto right-2 !top-3.5 !bottom-auto" :show-fit-view="false" :show-interactive="false" />
<template #node-custom="props">
<TableNode :data="props.data" />
</template>
<template #edge-custom="props">
<RelationEdge v-bind="props" />
</template>
<Background />
<div
v-if="!config.singleTableMode"
class="absolute bottom-0 right-0 flex flex-col text-xs bg-white px-2 py-1 border-1 rounded-md border-gray-200 z-50 nc-erd-histogram"
style="font-size: 0.6rem"
>
<div class="flex flex-row items-center space-x-1 border-b-1 pb-1 border-gray-100">
<MdiTableLarge class="text-primary" />
<div>{{ $t('objects.table') }}</div>
</div>
<div class="flex flex-row items-center space-x-1 pt-1">
<MdiEyeCircleOutline class="text-primary" />
<div>{{ $t('objects.sqlVIew') }}</div>
</div>
</div>
</VueFlow>
</template>

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

@ -0,0 +1,161 @@
<script setup>
import { EdgeText, getBezierPath, getEdgeCenter } from '@braks/vue-flow'
import { computed } from 'vue'
const props = defineProps({
id: {
type: String,
required: true,
},
sourceX: {
type: Number,
required: true,
},
sourceY: {
type: Number,
required: true,
},
targetX: {
type: Number,
required: true,
},
targetY: {
type: Number,
required: true,
},
sourcePosition: {
type: String,
required: true,
},
targetPosition: {
type: String,
required: true,
},
data: {
type: Object,
required: false,
},
markerEnd: {
type: String,
required: false,
},
style: {
type: Object,
required: false,
},
sourceHandleId: {
type: String,
required: false,
},
targetHandleId: {
type: String,
required: false,
},
})
const data = toRef(props, 'data')
const isManyToMany = computed(() => data.value.column?.colOptions?.type === 'mm')
const edgePath = computed(() => {
if (data.value.isSelfRelation) {
const { sourceX, sourceY, targetX, targetY } = props
const radiusX = (sourceX - targetX) * 0.6
const radiusY = 50
return `M ${sourceX} ${sourceY} A ${radiusX} ${radiusY} 0 1 0 ${targetX} ${targetY}`
}
return getBezierPath({
sourceX: props.sourceX,
sourceY: props.sourceY,
sourcePosition: props.sourcePosition,
targetX: props.targetX,
targetY: props.targetY,
targetPosition: props.targetPosition,
})
})
const center = computed(() =>
getEdgeCenter({
sourceX: props.sourceX,
sourceY: props.sourceY,
targetX: props.targetX,
targetY: props.targetY,
}),
)
</script>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<path
:id="id"
:style="style"
class="path-wrapper p-4 hover:cursor-pointer"
:stroke-width="8"
fill="none"
:d="edgePath"
:marker-end="markerEnd"
/>
<path
:id="id"
:style="style"
class="path stroke-gray-500 hover:stroke-green-500 hover:cursor-pointer"
:stroke-width="1.5"
fill="none"
:d="edgePath"
:marker-end="markerEnd"
/>
<EdgeText
v-if="data.label?.length > 0"
:class="`nc-erd-table-label-${data.label.toLowerCase().replace(' ', '-').replace('\(', '').replace(')', '')}`"
:x="center[0]"
:y="center[1]"
:label="data.label"
:label-style="{ fill: 'white' }"
:label-show-bg="true"
:label-bg-style="{ fill: '#10b981' }"
:label-bg-padding="[2, 4]"
:label-bg-border-radius="2"
/>
<rect
class="nc-erd-edge-rect"
:x="sourceX"
:y="sourceY - 4"
width="8"
height="8"
fill="#fff"
stroke="#6F3381"
:stroke-width="1.5"
:transform="`rotate(45,${sourceX + 2},${sourceY - 4})`"
/>
<rect
v-if="isManyToMany"
class="nc-erd-edge-rect"
:x="targetX"
:y="targetY - 4"
width="8"
height="8"
fill="#fff"
stroke="#6F3381"
:stroke-width="1.5"
:transform="`rotate(45,${targetX + 2},${targetY - 4})`"
/>
<circle v-else class="nc-erd-edge-circle" :cx="targetX" :cy="targetY" fill="#fff" :r="5" stroke="#6F3381" :stroke-width="1.5" />
</template>
<style scoped lang="scss">
.path-wrapper:hover + .path {
@apply stroke-green-500;
stroke-width: 2;
}
.path:hover {
stroke-width: 2;
}
</style>

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

@ -0,0 +1,121 @@
<script lang="ts" setup>
import type { NodeProps } from '@braks/vue-flow'
import { Handle, Position } from '@braks/vue-flow'
import type { ColumnType, TableType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
interface Props extends NodeProps {
data: TableType & { showPkAndFk: boolean; showAllColumns: boolean }
}
const props = defineProps<Props>()
const { data } = toRefs(props)
provide(MetaInj, data as Ref<TableType>)
const { $e } = useNuxtApp()
const columns = computed(() => {
// Hide hm ltar created for `mm` relations
return data.value.columns?.filter((col) => !(col.uidt === UITypes.LinkToAnotherRecord && col.system === 1))
})
const pkAndFkColumns = computed(() => {
return columns.value
?.filter(() => data.value.showPkAndFk && data.value.showAllColumns)
.filter((col) => col.pk || col.uidt === UITypes.ForeignKey)
})
const nonPkColumns = computed(() => {
return columns.value
?.filter(
(col: ColumnType) => data.value.showAllColumns || (!data.value.showAllColumns && col.uidt === UITypes.LinkToAnotherRecord),
)
.filter((col: ColumnType) => !col.pk && col.uidt !== UITypes.ForeignKey)
})
const relatedColumnId = (col: Record<string, any>) =>
col.colOptions.type === 'mm' ? col.colOptions.fk_parent_column_id : col.colOptions.fk_child_column_id
</script>
<template>
<div
class="h-full flex flex-col min-w-16 bg-gray-50 rounded-lg border-1 nc-erd-table-node"
:class="`nc-erd-table-node-${data.table_name}`"
@click="$e('c:erd:node-click')"
>
<GeneralTooltip modifier-key="Alt">
<template #title> {{ data.table_name }} </template>
<div
class="text-gray-600 text-md py-2 border-b-1 border-gray-200 rounded-t-lg w-full pr-3 pl-2 bg-gray-100 font-semibold flex flex-row items-center"
>
<MdiTableLarge v-if="data.type === 'table'" class="text-primary" />
<MdiEyeCircleOutline v-else class="text-primary" />
<div class="flex pl-1.5">
{{ data.title }}
</div>
</div>
</GeneralTooltip>
<div>
<div
v-for="col in pkAndFkColumns"
:key="col.title"
class="w-full border-b-1 py-2 border-gray-100 keys"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
>
<SmartsheetHeaderCell v-if="col" :column="col" :hide-menu="true" />
</div>
<div class="w-full mb-1"></div>
<div v-for="(col, index) in nonPkColumns" :key="col.title">
<div
class="w-full h-full flex items-center min-w-32 border-gray-100 py-2 px-1"
:class="index + 1 === nonPkColumns.length ? 'rounded-b-lg' : 'border-b-1'"
>
<div
v-if="col.uidt === UITypes.LinkToAnotherRecord"
class="flex relative w-full"
:class="`nc-erd-table-node-${data.table_name}-column-${col.title?.toLowerCase().replace(' ', '_')}`"
>
<Handle
:id="`s-${relatedColumnId(col)}-${data.id}`"
class="-right-4 opacity-0"
type="source"
:position="Position.Right"
/>
<Handle
:id="`d-${relatedColumnId(col)}-${data.id}`"
class="-left-1 opacity-0"
type="target"
:position="Position.Left"
/>
<SmartsheetHeaderVirtualCell :column="col" :hide-menu="true" />
</div>
<SmartsheetHeaderVirtualCell
v-else-if="isVirtualCol(col)"
:column="col"
:hide-menu="true"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
/>
<SmartsheetHeaderCell
v-else
:column="col"
:hide-menu="true"
:class="`nc-erd-table-node-${data.table_name}-column-${col.column_name}`"
/>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.keys {
background-color: #f6f6f6;
}
</style>

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

@ -0,0 +1,162 @@
<script setup lang="ts">
import type { LinkToAnotherRecordType, TableType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
const { table } = defineProps<{ table?: TableType }>()
const { includeM2M } = useGlobal()
const { tables: projectTables } = useProject()
const tables = ref<TableType[]>([])
const { metas, getMeta } = useMetas()
let isLoading = $ref(true)
const showAdvancedOptions = ref(false)
const config = ref({
showPkAndFk: true,
showViews: false,
showAllColumns: true,
singleTableMode: !!table,
showMMTables: false,
showJunctionTableNames: false,
})
const loadMetaOfTablesNotInMetas = async (localTables: TableType[]) => {
await Promise.all(
localTables
.filter((table) => !metas.value[table.id!])
.map(async (table) => {
await getMeta(table.id!)
}),
)
}
const populateTables = async () => {
let localTables: TableType[] = []
if (table) {
// if table is provided only get the table and its related tables
localTables = projectTables.value.filter(
(t) =>
t.id === table.id ||
table.columns?.find(
(column) =>
column.uidt === UITypes.LinkToAnotherRecord &&
(column.colOptions as LinkToAnotherRecordType)?.fk_related_model_id === t.id,
),
)
} else {
localTables = projectTables.value
}
await loadMetaOfTablesNotInMetas(localTables)
tables.value = localTables
.filter(
(t) =>
// todo: table type is missing mm property in type definition
config.value.showMMTables ||
(!config.value.showMMTables && !t.mm) ||
// Show mm table if it's the selected table
t.id === table?.id,
)
.filter((t) => (!config.value.showViews && t.type !== 'view') || config.value.showViews)
isLoading = false
}
watch(
[config, metas],
async () => {
await populateTables()
},
{
deep: true,
},
)
watch(
[projectTables],
async () => {
await populateTables()
},
{ immediate: true },
)
watch(
() => config.value.showAllColumns,
() => {
config.value.showPkAndFk = config.value.showAllColumns
},
)
</script>
<template>
<div
class="w-full"
style="height: inherit"
:class="{
'nc-erd-vue-flow': !config.singleTableMode,
'nc-erd-vue-flow-single-table': config.singleTableMode,
}"
>
<div v-if="isLoading" class="h-full w-full flex flex-col justify-center items-center">
<div class="flex flex-row justify-center">
<a-spin size="large" />
</div>
</div>
<div v-else class="relative h-full">
<ErdFlow :tables="tables" :config="config" />
<div
class="absolute top-2 right-10 flex-col bg-white py-2 px-4 border-1 border-gray-100 rounded-md z-50 space-y-1 nc-erd-context-menu z-50"
>
<div class="flex flex-row items-center">
<a-checkbox
v-model:checked="config.showAllColumns"
v-e="['c:erd:showAllColumns']"
class="nc-erd-showColumns-checkbox"
/>
<span
class="ml-2 select-none nc-erd-showColumns-label"
style="font-size: 0.65rem"
@dblclick="showAdvancedOptions = true"
>
{{ $t('activity.erd.showColumns') }}
</span>
</div>
<div class="flex flex-row items-center">
<a-checkbox
v-model:checked="config.showPkAndFk"
v-e="['c:erd:showPkAndFk']"
class="nc-erd-showPkAndFk-checkbox"
:class="{
'nc-erd-showPkAndFk-checkbox-enabled': config.showAllColumns,
'nc-erd-showPkAndFk-checkbox-disabled': !config.showAllColumns,
'nc-erd-showPkAndFk-checkbox-checked': config.showPkAndFk,
'nc-erd-showPkAndFk-checkbox-unchecked': !config.showPkAndFk,
}"
:disabled="!config.showAllColumns"
/>
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showPkAndFk') }}</span>
</div>
<div v-if="!table" class="flex flex-row items-center">
<a-checkbox v-model:checked="config.showViews" v-e="['c:erd:showViews']" class="nc-erd-showViews-checkbox" />
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showSqlViews') }}</span>
</div>
<div v-if="!table && showAdvancedOptions && includeM2M" class="flex flex-row items-center">
<a-checkbox v-model:checked="config.showMMTables" v-e="['c:erd:showMMTables']" class="nc-erd-showMMTables-checkbox" />
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showMMTables') }}</span>
</div>
<div v-if="showAdvancedOptions && includeM2M" class="flex flex-row items-center">
<a-checkbox
v-model:checked="config.showJunctionTableNames"
v-e="['c:erd:showJunctionTableNames']"
class="nc-erd-showJunctionTableNames-checkbox"
/>
<span class="ml-2 select-none text-[0.65rem]">{{ $t('activity.erd.showJunctionTableNames') }}</span>
</div>
</div>
</div>
</div>
</template>

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

@ -0,0 +1,31 @@
<script setup lang="ts">
import { useSidebar } from '#imports'
const rightSidebar = useSidebar('nc-right-sidebar')
const leftSidebar = useSidebar('nc-left-sidebar')
const isSidebarsOpen = computed({
get: () => rightSidebar.isOpen.value || leftSidebar.isOpen.value,
set: (value) => {
rightSidebar.toggle(value)
leftSidebar.toggle(value)
},
})
</script>
<template>
<a-tooltip placement="left">
<!-- todo: i18n -->
<template #title>
<span class="text-xs">{{ isSidebarsOpen ? 'Full width' : 'Exit full width' }}</span>
</template>
<div
v-e="['c:toolbar:fullscreen']"
class="nc-fullscreen-btn cursor-pointer flex align-center self-center px-2 py-2 mr-2"
@click="isSidebarsOpen = !isSidebarsOpen"
>
<IcTwotoneWidthFull v-if="isSidebarsOpen" class="text-gray-300" />
<IcTwotoneWidthNormal v-else class="text-gray-300" />
</div>
</a-tooltip>
</template>

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

@ -27,7 +27,7 @@ const openSwaggerLink = () => {
<a-drawer
v-bind="$attrs"
v-model:visible="showDrawer"
class="h-full relative"
class="h-full relative nc-drawer-help-and-support"
placement="right"
size="small"
:closable="false"
@ -41,7 +41,7 @@ const openSwaggerLink = () => {
<template #before>
<a-list-item v-if="project">
<nuxt-link
v-t="['a:navbar:user:swagger']"
v-e="['a:navbar:user:swagger']"
class="!no-underline !text-current py-4 font-semibold"
target="_blank"
@click="openSwaggerLink"

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

@ -4,7 +4,7 @@ import { computed, useGlobal, useProject, useRoute, useSidebar } from '#imports'
const { signOut, signedIn, user } = useGlobal()
const { isOpen } = useSidebar({ isOpen: true })
const { isOpen } = useSidebar('nc-mini-sidebar', { isOpen: true })
const { project } = useProject()
@ -28,7 +28,7 @@ const logout = () => {
collapsible
theme="light"
>
<a-dropdown placement="bottom" :trigger="['click']">
<a-dropdown placement="bottom" :trigger="['click']" overlay-class-name="nc-dropdown">
<div class="transition-all duration-200 p-2 cursor-pointer transform hover:scale-105 nc-noco-brand-icon">
<img width="35" alt="NocoDB" src="~/assets/img/icons/512x512-trans.png" />
</div>
@ -37,7 +37,7 @@ const logout = () => {
<a-menu class="ml-2 !py-0 min-w-32 leading-8 !rounded nc-menu-account">
<a-menu-item-group title="User Settings">
<a-menu-item key="email" class="!rounded-t">
<nuxt-link v-t="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<nuxt-link v-e="['c:navbar:user:email']" class="group flex items-center no-underline py-2" to="/user">
<MdiAt class="mt-1 group-hover:text-success" />
&nbsp;
<span class="prose group-hover:text-black nc-user-menu-email">{{ email }}</span>
@ -47,7 +47,7 @@ const logout = () => {
<a-menu-divider class="!m-0" />
<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-e="['a:navbar:user:sign-out']" class="group flex items-center py-2" @click="logout">
<MdiLogout class="group-hover:(!text-red-500)" />&nbsp;
<span class="prose font-semibold text-gray-500 group-hover:text-black nc-user-menu-signout">
{{ $t('general.signOut') }}
@ -60,7 +60,7 @@ const logout = () => {
</a-dropdown>
<div id="sidebar" ref="sidebar" class="text-white flex-auto flex flex-col items-center w-full">
<a-dropdown :trigger="['contextmenu']" placement="right">
<a-dropdown :trigger="['contextmenu']" placement="right" overlay-class-name="nc-dropdown">
<div :class="[route.name === 'index' ? 'active' : '']" class="nc-mini-sidebar-item" @click="navigateTo('/')">
<MdiFolder class="cursor-pointer transform hover:scale-105 text-2xl" />
</div>
@ -76,7 +76,7 @@ const logout = () => {
<a-menu-item class="active:(ring ring-accent)">
<div
v-t="['c:project:create:xcdb']"
v-e="['c:project:create:xcdb']"
class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create')"
>
@ -87,7 +87,7 @@ const logout = () => {
<a-menu-item class="rounded-b active:(ring ring-accent)">
<div
v-t="['c:project:create:extdb']"
v-e="['c:project:create:extdb']"
class="group flex items-center gap-2 py-2 hover:text-primary"
@click="navigateTo('/project/create-external')"
>

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

@ -40,7 +40,7 @@ onMounted(async () => await fetchReleaseInfo())
<template>
<div v-if="releaseAlert" class="flex items-center">
<a-dropdown :trigger="['click']" placement="bottom">
<a-dropdown :trigger="['click']" placement="bottom" overlay-class-name="nc-dropdown-upgrade-menu">
<a-button class="!bg-primary !border-none">
<div class="flex gap-1 items-center text-white">
<span class="text-sm font-weight-medium">{{ $t('activity.upgrade.available') }}</span>

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

@ -23,18 +23,18 @@ const isZhLang = $computed(() => locale.value.startsWith('zh'))
/>
<div v-else class="flex justify-between gap-1 w-full px-2">
<MdiDiscord v-t="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" />
<MdiDiscord v-e="['e:community:discord']" class="icon text-[#7289DA]" @click="open('https://discord.gg/5RgZmkW')" />
<div
v-t="['e:community:discourse']"
v-e="['e:community:discourse']"
class="icon flex items-center justify-center min-w-[43px]"
@click="open('https://community.nocodb.com/')"
>
<div class="discourse" />
</div>
<MdiReddit v-t="['e:community:reddit']" class="icon text-[#FF4600]" @click="open('https://www.reddit.com/r/NocoDB/')" />
<MdiTwitter v-t="['e:community:twitter']" class="icon text-[#1DA1F2]" @click="open('https://twitter.com/NocoDB')" />
<MdiReddit v-e="['e:community:reddit']" class="icon text-[#FF4600]" @click="open('https://www.reddit.com/r/NocoDB/')" />
<MdiTwitter v-e="['e:community:twitter']" class="icon text-[#1DA1F2]" @click="open('https://twitter.com/NocoDB')" />
<MdiCalendarMonth
v-t="['e:community:book-demo']"
v-e="['e:community:book-demo']"
class="icon text-green-500"
@click="open('https://calendly.com/nocodb-meeting')"
/>

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

@ -13,7 +13,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
<a-list-item>
<nuxt-link
v-t="['e:docs']"
v-e="['e:docs']"
class="text-primary !no-underline !text-current"
target="_blank"
to="https://docs.nocodb.com/"
@ -26,7 +26,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item>
<a-list-item>
<nuxt-link
v-t="['e:api-docs']"
v-e="['e:api-docs']"
class="text-primary !no-underline !text-current"
target="_blank"
to="https://apis.nocodb.com/"
@ -40,7 +40,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item>
<a-list-item>
<nuxt-link
v-t="['e:community:github']"
v-e="['e:community:github']"
class="text-primary !no-underline !text-current"
to="https://github.com/nocodb/nocodb"
target="_blank"
@ -66,7 +66,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item>
<a-list-item>
<nuxt-link
v-t="['e:community:book-demo']"
v-e="['e:community:book-demo']"
class="!no-underline !text-current"
to="https://calendly.com/nocodb-meeting"
target="_blank"
@ -82,7 +82,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item>
<a-list-item>
<nuxt-link
v-t="['e:community:discord']"
v-e="['e:community:discord']"
class="!no-underline !text-current"
to="https://discord.gg/5RgZmkW"
target="_blank"
@ -98,7 +98,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item>
<a-list-item>
<nuxt-link
v-t="['e:community:twitter']"
v-e="['e:community:twitter']"
class="!no-underline !text-current"
to="https://twitter.com/NocoDB"
target="_blank"
@ -113,7 +113,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</nuxt-link>
</a-list-item>
<a-list-item>
<nuxt-link v-t="['e:hiring']" class="!no-underline !text-current" target="_blank" to="http://careers.nocodb.com">
<nuxt-link v-e="['e:hiring']" class="!no-underline !text-current" target="_blank" to="http://careers.nocodb.com">
<div class="flex items-center text-sm">
<!-- todo: i18n -->
<div class="ml-3">
@ -124,7 +124,7 @@ const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
</a-list-item>
<a-list-item>
<nuxt-link
v-t="['e:community:reddit']"
v-e="['e:community:reddit']"
class="!no-underline !text-current"
target="_blank"
to="https://www.reddit.com/r/NocoDB/"

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

@ -0,0 +1,45 @@
<script lang="ts" setup>
import { onKeyStroke } from '@vueuse/core'
interface Props {
// Key to be pressed on hover to trigger the tooltip
modifierKey?: string
wrapperClass?: string
}
const { modifierKey } = defineProps<Props>()
const showTooltip = ref(false)
const isMouseOver = ref(false)
if (modifierKey) {
onKeyStroke(modifierKey, (e) => {
e.preventDefault()
if (modifierKey && isMouseOver.value) {
showTooltip.value = true
}
})
}
watch(isMouseOver, (val) => {
if (!val) {
showTooltip.value = false
}
// Show tooltip on mouseover if no modifier key is provided
if (val && !modifierKey) {
showTooltip.value = true
}
})
</script>
<template>
<a-tooltip v-model:visible="showTooltip" :trigger="[]">
<template #title>
<slot name="title" />
</template>
<div class="w-full" :class="wrapperClass" @mouseenter="isMouseOver = true" @mouseleave="isMouseOver = false">
<slot />
</div>
</a-tooltip>
</template>

43
packages/nc-gui/components/general/language/Menu.vue

@ -1,14 +1,15 @@
<script lang="ts" setup>
import { Language } from '~/lib'
import { onMounted, useGlobal, useI18n, useNuxtApp } from '#imports'
import { setI18nLanguage } from '~/plugins/a.i18n'
const { $e } = useNuxtApp()
const { lang: currentLang } = useGlobal()
const { availableLocales = ['en'], locale } = useI18n()
const { locale } = useI18n()
const languages = $computed(() => availableLocales.sort())
const languages = $computed(() => Object.entries(Language).sort())
const isRtlLang = $computed(() => ['fa'].includes(currentLang.value))
@ -21,9 +22,11 @@ function applyDirection() {
document.body.style.direction = targetDirection
}
function changeLanguage(lang: string) {
currentLang.value = lang
locale.value = lang
async function changeLanguage(lang: string) {
const nextLang = lang as keyof typeof Language
await setI18nLanguage(nextLang)
currentLang.value = nextLang
applyDirection()
@ -36,26 +39,26 @@ onMounted(() => {
</script>
<template>
<a-menu-item
v-for="lang of languages"
:key="lang"
:class="lang === locale ? '!bg-primary bg-opacity-10 text-primary' : ''"
class="group"
:value="lang"
@click="changeLanguage(lang)"
>
<div :class="lang === locale ? '!font-semibold !text-primary' : ''" class="nc-project-menu-item capitalize">
{{ Language[lang] || lang }}
</div>
</a-menu-item>
<a-menu-item class="mt-1">
<a-menu-item class="mt-1 group">
<a
href="https://docs.nocodb.com/engineering/translation/#how-to-contribute--for-community-members"
target="_blank"
class="caption py-2 text-primary underline hover:opacity-75"
class="caption nc-project-menu-item py-2 text-primary underline hover:opacity-75"
>
{{ $t('activity.translate') }}
</a>
</a-menu-item>
<a-menu-item
v-for="[key, lang] of languages"
:key="key"
:class="key === locale ? '!bg-primary bg-opacity-10 text-primary' : ''"
class="group"
:value="key"
@click="changeLanguage(key)"
>
<div :class="key === locale ? '!font-semibold !text-primary' : ''" class="nc-project-menu-item capitalize">
{{ Language[key] || lang }}
</div>
</a-menu-item>
</template>

10
packages/nc-gui/components/general/language/index.vue

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

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

@ -33,6 +33,7 @@ const onFinish = async () => {
centered
:footer="null"
:mask-closable="false"
wrap-class-name="nc-modal-shared-view-password-dlg"
@close="vModel = false"
>
<div class="w-full flex flex-col">

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

@ -1,23 +1,21 @@
<script setup lang="ts">
import type { Ref } from 'vue'
import type { TableType } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '~/context'
import { ActiveViewInj, FieldsInj, IsPublicInj, MetaInj, ReadonlyInj, ReloadViewDataHookInj } from '#imports'
const { sharedView, meta, sorts, nestedFilters } = useSharedView()
const { signedIn } = useGlobal()
const { loadProject } = useProject(meta?.value.project_id)
const { loadProject } = useProject(meta.value?.project_id)
const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook)
provide(ReadonlyInj, true)
provide(MetaInj, meta)
provide(ActiveViewInj, sharedView)
provide(FieldsInj, ref(meta.value.columns as any[]))
provide(FieldsInj, ref(meta.value?.columns || []))
provide(IsPublicInj, ref(true))
useProvideSmartsheetStore(sharedView as Ref<TableType>, meta, true, sorts, nestedFilters)
useProvideSmartsheetStore(sharedView, meta, true, sorts, nestedFilters)
if (signedIn.value) {
try {

36
packages/nc-gui/components/smartsheet-column/AdvancedOptions.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import type { UITypes } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { computed } from '#imports'
interface Props {
@ -10,12 +10,27 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { sqlUi } = useProject()
const { sqlUi, isPg } = useProject()
const { onAlter, onDataTypeChange, validateInfos } = useColumnCreateStoreOrThrow()
// todo: 2nd argument of `getDataTypeListForUiType` is missing!
const dataTypes = computed(() => sqlUi?.value?.getDataTypeListForUiType(vModel.value as { uidt: UITypes }, '' as any))
const dataTypes = computed(() => sqlUi.value.getDataTypeListForUiType(vModel.value as { uidt: UITypes }, '' as any))
const sampleValue = computed(() => {
switch (vModel.value.uidt) {
case UITypes.SingleSelect:
return 'eg : a'
case UITypes.MultiSelect:
return 'eg : a,b,c'
default:
return sqlUi.value.getDefaultValueForDatatype(vModel.value.dt)
}
})
const hideLength = computed(() => {
return [UITypes.SingleSelect, UITypes.MultiSelect].includes(vModel.value.uidt)
})
// to avoid type error with checkbox
vModel.value.rqd = !!vModel.value.rqd
@ -23,6 +38,13 @@ vModel.value.pk = !!vModel.value.pk
vModel.value.un = !!vModel.value.un
vModel.value.ai = !!vModel.value.ai
vModel.value.au = !!vModel.value.au
onBeforeMount(() => {
// Postgres returns default value wrapped with single quotes & casted with type so we have to get value between single quotes to keep it unified for all databases
if (isPg.value && vModel.value.cdf) {
vModel.value.cdf = vModel.value.cdf.substring(vModel.value.cdf.indexOf(`'`) + 1, vModel.value.cdf.lastIndexOf(`'`))
}
})
</script>
<template>
@ -60,13 +82,13 @@ vModel.value.au = !!vModel.value.au
</a-form-item>
</div>
<a-form-item :label="$t('labels.databaseType')" v-bind="validateInfos.dt">
<a-select v-model:value="vModel.dt" @change="onDataTypeChange">
<a-select v-model:value="vModel.dt" dropdown-class-name="nc-dropdown-db-type" @change="onDataTypeChange">
<a-select-option v-for="type in dataTypes" :key="type" :value="type">
{{ type }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('labels.lengthValue')">
<a-form-item v-if="!hideLength" :label="$t('labels.lengthValue')">
<a-input
v-model:value="vModel.dtxp"
:disabled="sqlUi.getDefaultLengthIsDisabled(vModel.dt) || !sqlUi.columnEditable(vModel)"
@ -74,11 +96,11 @@ vModel.value.au = !!vModel.value.au
/>
</a-form-item>
<a-form-item v-if="sqlUi.showScale(vModel)" label="Scale">
<a-input v-model="vModel.dtxs" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
<a-input v-model:value="vModel.dtxs" :disabled="!sqlUi.columnEditable(vModel)" @input="onAlter" />
</a-form-item>
<a-form-item :label="$t('placeholder.defaultValue')">
<a-textarea v-model:value="vModel.cdf" auto-size @input="onAlter(2, true)" />
<span class="text-gray-400 text-xs">{{ sqlUi.getDefaultValueForDatatype(vModel.dt) }}</span>
<span class="text-gray-400 text-xs">{{ sampleValue }}</span>
</a-form-item>
</div>
</template>

2
packages/nc-gui/components/smartsheet-column/CheckboxOptions.vue

@ -78,7 +78,7 @@ watch(
<a-row>
<a-col :span="24">
<a-form-item label="Icon">
<a-select v-model:value="vModel.meta.iconIdx" class="w-52">
<a-select v-model:value="vModel.meta.iconIdx" class="w-52" dropdown-class-name="nc-dropdown-checkbox-icon">
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<div class="flex items-center">
<component

2
packages/nc-gui/components/smartsheet-column/CurrencyOptions.vue

@ -83,6 +83,7 @@ vModel.value.meta = {
show-search
:filter-option="filterOption"
:disabled="isMoney && isPg"
dropdown-class-name="nc-dropdown-currency-cell-locale"
>
<a-select-option v-for="(currencyLocale, i) of currencyLocaleList" :key="i" :value="currencyLocale.value">
{{ currencyLocale.text }}
@ -98,6 +99,7 @@ vModel.value.meta = {
show-search
:filter-option="filterOption"
:disabled="isMoney && isPg"
dropdown-class-name="nc-dropdown-currency-cell-code"
>
<a-select-option v-for="(currencyCode, i) of currencyList" :key="i" :value="currencyCode">
{{ currencyCode }}

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

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

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

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

50
packages/nc-gui/components/smartsheet-column/EditOrAdd.vue

@ -1,10 +1,7 @@
<script lang="ts" setup>
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { useNuxtApp } from '#app'
import { computed, inject, useMetas, watchEffect } from '#imports'
import { IsFormInj, MetaInj, ReloadViewDataHookInj } from '~/context'
import { uiTypes } from '~/utils/columnUtils'
import { IsFormInj, MetaInj, ReloadViewDataHookInj, computed, inject, uiTypes, useMetas, useNuxtApp, watchEffect } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
import MdiIdentifierIcon from '~icons/mdi/identifier'
@ -20,7 +17,7 @@ const { t } = useI18n()
const { $e } = useNuxtApp()
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const isForm = inject(IsFormInj, ref(false))
@ -48,7 +45,7 @@ const uiTypesOptions = computed<typeof uiTypes>(() => {
})
const reloadMetaAndData = async () => {
await getMeta(meta?.value.id as string, true)
await getMeta(meta.value?.id as string, true)
reloadDataTrigger?.trigger()
}
@ -100,11 +97,11 @@ onMounted(() => {
<template>
<div
class="w-[400px] max-h-[95vh] bg-gray-50 shadow-lg p-6 overflow-auto !border"
class="w-[400px] bg-gray-50 shadow p-4 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" no-style name="column-create-or-edit" layout="vertical">
<div class="flex flex-col gap-2">
<a-form-item :label="$t('labels.columnName')" v-bind="validateInfos.title">
<a-input ref="antInput" v-model:value="formState.title" class="nc-column-name-input" @input="onAlter(8)" />
@ -114,7 +111,13 @@ onMounted(() => {
v-if="!(isEdit && !!onlyNameUpdateOnEditColumns.find((col) => col === formState.uidt))"
:label="$t('labels.columnType')"
>
<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"
dropdown-class-name="nc-dropdown-column-type"
@change="onUidtOrIdTypeChange"
>
<a-select-option v-for="opt of uiTypesOptions" :key="opt.name" :value="opt.name" v-bind="validateInfos.uidt">
<div class="flex gap-1 items-center">
<component :is="opt.icon" class="text-grey" />
@ -141,6 +144,7 @@ onMounted(() => {
v-model:value="formState"
/>
</div>
<div
v-if="!isVirtualCol(formState.uidt)"
class="text-xs cursor-pointer text-grey nc-more-options mb-1 mt-4 flex items-center gap-1 justify-end"
@ -150,18 +154,22 @@ onMounted(() => {
<component :is="advancedOptions ? MdiMinusIcon : MdiPlusIcon" />
</div>
<div class="overflow-hidden" :class="advancedOptions ? 'h-min mb-2' : 'h-0'">
<a-checkbox
v-if="formState.meta && columnToValidate.includes(formState.uidt)"
v-model:checked="formState.meta.validate"
class="ml-1 mb-1"
>
<span class="text-[10px] text-gray-600">
{{ `Accept only valid ${formState.uidt}` }}
</span>
</a-checkbox>
<SmartsheetColumnAdvancedOptions v-model:value="formState" />
</div>
<Transition name="layout" mode="out-in">
<div v-if="advancedOptions" class="overflow-hidden">
<a-checkbox
v-if="formState.meta && columnToValidate.includes(formState.uidt)"
v-model:checked="formState.meta.validate"
class="ml-1 mb-1"
>
<span class="text-[10px] text-gray-600">
{{ `Accept only valid ${formState.uidt}` }}
</span>
</a-checkbox>
<SmartsheetColumnAdvancedOptions v-model:value="formState" />
</div>
</Transition>
<a-form-item>
<div class="flex justify-end gap-1 mt-4">
<a-button html-type="button" @click="emit('cancel')">

15
packages/nc-gui/components/smartsheet-column/EditOrAddProvider.vue

@ -1,8 +1,7 @@
<script lang="ts" setup>
import type { ColumnType, TableType } from 'nocodb-sdk'
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { inject } from '#imports'
import { MetaInj } from '~/context'
import { MetaInj, inject } from '#imports'
interface Props {
column?: Ref<ColumnType & { meta: any }>
@ -12,14 +11,10 @@ const props = defineProps<Props>()
const emit = defineEmits(['submit', 'cancel'])
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
if (props?.column) {
const column = toRef(props, 'column')
useProvideColumnCreateStore(meta as Ref<TableType>, column)
} else {
useProvideColumnCreateStore(meta as Ref<TableType>)
}
const column = toRef(props, 'column')
useProvideColumnCreateStore(meta, column as Ref<ColumnType | undefined>)
</script>
<template>

12
packages/nc-gui/components/smartsheet-column/FormulaOptions.vue

@ -41,7 +41,7 @@ enum JSEPNode {
ARRAY_EXP = 'ArrayExpression',
}
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const columns = computed(() => meta?.value?.columns || [])
@ -102,7 +102,7 @@ const suggestionsList = computed(() => {
if (c.uidt === UITypes.LinkToAnotherRecord && c.system) return false
// v1 logic? skip the current column
if (!column) return true
return column.value.id !== c.id
return column.value?.id !== c.id
})
.map((c: any) => ({
text: c.title,
@ -238,7 +238,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
} else if (parsedTree.type === JSEPNode.IDENTIFIER) {
if (
columns.value
.filter((c: Record<string, any>) => !column || column.value.id !== c.id)
.filter((c: Record<string, any>) => !column || column.value?.id !== c.id)
.every((c: Record<string, any>) => c.title !== parsedTree.name)
) {
errors.add(`Column '${parsedTree.name}' is not available`)
@ -249,7 +249,7 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
// get all formula columns excluding itself
const formulaPaths = columns.value
.filter((c: Record<string, any>) => c.id !== column?.value.id && c.uidt === UITypes.Formula)
.filter((c: Record<string, any>) => c.id !== column.value?.id && c.uidt === UITypes.Formula)
.reduce((res: Record<string, any>[], c: Record<string, any>) => {
// in `formula`, get all the target neighbours
// i.e. all column id (e.g. cl_xxxxxxxxxxxxxx) with formula type
@ -265,9 +265,9 @@ function validateAgainstMeta(parsedTree: any, errors = new Set(), typeErrors = n
// include target formula column (i.e. the one to be saved if applicable)
const targetFormulaCol = columns.value.find((c: ColumnType) => c.title === parsedTree.name && c.uidt === UITypes.Formula)
if (targetFormulaCol && column?.value.id) {
if (targetFormulaCol && column.value?.id) {
formulaPaths.push({
[column?.value?.id as string]: [targetFormulaCol.id],
[column.value?.id as string]: [targetFormulaCol.id],
})
}
const vertices = formulaPaths.length

28
packages/nc-gui/components/smartsheet-column/LinkedToAnotherRecordOptions.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import { ModelTypes, MssqlUi, SqliteUi } from 'nocodb-sdk'
import { inject, useProject } from '#imports'
import { MetaInj } from '~/context'
import { MetaInj, inject, useProject } from '#imports'
import MdiPlusIcon from '~icons/mdi/plus-circle-outline'
import MdiMinusIcon from '~icons/mdi/minus-circle-outline'
@ -13,7 +12,7 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!)
const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
@ -25,10 +24,10 @@ setAdditionalValidations({
const onUpdateDeleteOptions = sqlUi === MssqlUi ? ['NO ACTION'] : ['NO ACTION', 'CASCADE', 'RESTRICT', 'SET NULL', 'SET DEFAULT']
if (!vModel.value.parentId) vModel.value.parentId = meta.id
if (!vModel.value.parentId) vModel.value.parentId = meta?.id
if (!vModel.value.childId) vModel.value.childId = null
if (!vModel.value.childColumn) vModel.value.childColumn = `${meta.table_name}_id`
if (!vModel.value.childTable) vModel.value.childTable = meta.table_name
if (!vModel.value.childColumn) vModel.value.childColumn = `${meta?.table_name}_id`
if (!vModel.value.childTable) vModel.value.childTable = meta?.table_name
if (!vModel.value.parentTable) vModel.value.parentTable = vModel.value.rtn || ''
if (!vModel.value.parentColumn) vModel.value.parentColumn = vModel.value.rcn || ''
@ -67,6 +66,7 @@ const refTables = $computed(() => {
v-model:value="vModel.childId"
show-search
:filter-option="(value, option) => option.key.toLowerCase().includes(value.toLowerCase())"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onDataTypeChange"
>
<a-select-option v-for="table in refTables" :key="table.title" :value="table.id">
@ -87,14 +87,26 @@ const refTables = $computed(() => {
<div v-if="advancedOptions" class="flex flex-col p-6 gap-4 border-2 mt-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" :label="$t('labels.onUpdate')">
<a-select v-model:value="vModel.onUpdate" :disabled="vModel.virtual" name="onUpdate" @change="onDataTypeChange">
<a-select
v-model:value="vModel.onUpdate"
:disabled="vModel.virtual"
name="onUpdate"
dropdown-class-name="nc-dropdown-on-update"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, index) in onUpdateDeleteOptions" :key="index" :value="option">
{{ option }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.onDelete')">
<a-select v-model:value="vModel.onDelete" :disabled="vModel.virtual" name="onDelete" @change="onDataTypeChange">
<a-select
v-model:value="vModel.onDelete"
:disabled="vModel.virtual"
name="onDelete"
dropdown-class-name="nc-dropdown-on-delete"
@change="onDataTypeChange"
>
<a-select-option v-for="(option, index) in onUpdateDeleteOptions" :key="index" :value="option">
{{ option }}
</a-select-option>

17
packages/nc-gui/components/smartsheet-column/LookupOptions.vue

@ -11,7 +11,7 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!)
const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
@ -38,7 +38,7 @@ const refTables = $computed(() => {
return []
}
return meta.columns
return meta?.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && !c.system)
.map((c: ColumnType) => ({
col: c.colOptions,
@ -62,7 +62,11 @@ const columns = $computed(() => {
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.childTable')" v-bind="validateInfos.fk_relation_column_id">
<a-select v-model:value="vModel.fk_relation_column_id" dropdown-class-name="!w-64" @change="onDataTypeChange">
<a-select
v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onDataTypeChange"
>
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
@ -74,7 +78,12 @@ const columns = $computed(() => {
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id">
<a-select v-model:value="vModel.fk_lookup_column_id" name="fk_lookup_column_id" @change="onDataTypeChange">
<a-select
v-model:value="vModel.fk_lookup_column_id"
name="fk_lookup_column_id"
dropdown-class-name="nc-dropdown-relation-column"
@change="onDataTypeChange"
>
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
{{ column.title }}
</a-select-option>

4
packages/nc-gui/components/smartsheet-column/PercentOptions.vue

@ -1,7 +1,7 @@
<!-- File not in use for now -->
<script setup lang="ts">
import { precisions } from '@/utils/percentUtils'
import { precisions } from '#imports'
interface Props {
value: Record<string, any>
@ -21,7 +21,7 @@ if (!vModel.value.meta?.precision) vModel.value.meta.precision = precisions[0].i
<div class="flex flex-col mt-2 gap-2">
<div class="flex flex-row space-x-2">
<a-form-item class="flex w-1/2" label="Precision">
<a-select v-model:value="vModel.meta.precision">
<a-select v-model:value="vModel.meta.precision" dropdown-class-name="nc-dropdown-precision">
<a-select-option v-for="(precision, i) of precisions" :key="i" :value="precision.id">
<div class="flex flex-row items-center">
<div class="text-xs">

6
packages/nc-gui/components/smartsheet-column/RatingOptions.vue

@ -1,5 +1,5 @@
<script setup lang="ts">
import { getMdiIcon } from '@/utils'
import { getMdiIcon } from '#imports'
interface Props {
value: Record<string, any>
@ -72,7 +72,7 @@ watch(
<a-row :gutter="8">
<a-col :span="12">
<a-form-item label="Icon">
<a-select v-model:value="vModel.meta.iconIdx" class="w-52">
<a-select v-model:value="vModel.meta.iconIdx" class="w-52" dropdown-class-name="nc-dropdown-rating-icon">
<a-select-option v-for="(icon, i) of iconList" :key="i" :value="i">
<div class="flex items-center">
<component
@ -95,7 +95,7 @@ watch(
</a-col>
<a-col :span="12">
<a-form-item label="Max">
<a-select v-model:value="vModel.meta.max" class="w-52">
<a-select v-model:value="vModel.meta.max" class="w-52" dropdown-class-name="nc-dropdown-rating-color">
<a-select-option v-for="(v, i) in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]" :key="i" :value="v">
{{ v }}
</a-select-option>

26
packages/nc-gui/components/smartsheet-column/RollupOptions.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { inject, useMetas, useProject } from '#imports'
import { MetaInj } from '~/context'
import { MetaInj, inject, useMetas, useProject } from '#imports'
interface Props {
value: Record<string, any>
@ -11,7 +10,7 @@ const props = defineProps<Props>()
const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const meta = $(inject(MetaInj)!)
const meta = $(inject(MetaInj, ref()))
const { setAdditionalValidations, validateInfos, onDataTypeChange } = useColumnCreateStoreOrThrow()
@ -51,7 +50,7 @@ const refTables = $computed(() => {
}
return (
meta.columns
meta?.columns
?.filter((c: any) => c.uidt === UITypes.LinkToAnotherRecord && c.colOptions.type !== 'bt' && !c.system)
.map((c) => ({
col: c.colOptions,
@ -76,7 +75,11 @@ const columns = $computed(() => {
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.childTable')" v-bind="validateInfos.fk_relation_column_id">
<a-select v-model:value="vModel.fk_relation_column_id" dropdown-class-name="!w-64" @change="onDataTypeChange">
<a-select
v-model:value="vModel.fk_relation_column_id"
dropdown-class-name="!w-64 nc-dropdown-relation-table"
@change="onDataTypeChange"
>
<a-select-option v-for="(table, index) in refTables" :key="index" :value="table.col.fk_column_id">
<div class="flex flex-row space-x-0.5 h-full pb-0.5 items-center justify-between">
<div class="font-semibold text-xs">{{ table.column.title }}</div>
@ -88,7 +91,12 @@ const columns = $computed(() => {
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_rollup_column_id">
<a-select v-model:value="vModel.fk_rollup_column_id" name="fk_rollup_column_id" @change="onDataTypeChange">
<a-select
v-model:value="vModel.fk_rollup_column_id"
name="fk_rollup_column_id"
dropdown-class-name="nc-dropdown-relation-column"
@change="onDataTypeChange"
>
<a-select-option v-for="(column, index) of columns" :key="index" :value="column.id">
{{ column.title }}
</a-select-option>
@ -96,7 +104,11 @@ const columns = $computed(() => {
</a-form-item>
</div>
<a-form-item label="Aggregate function" v-bind="validateInfos.rollup_function">
<a-select v-model:value="vModel.rollup_function" @change="onDataTypeChange">
<a-select
v-model:value="vModel.rollup_function"
dropdown-class-name="nc-dropdown-rollup-function"
@change="onDataTypeChange"
>
<a-select-option v-for="(func, index) of aggrFunctionsList" :key="index" :value="func.value">
{{ func.text }}
</a-select-option>

6
packages/nc-gui/components/smartsheet-column/SelectOptions.vue

@ -97,7 +97,11 @@ watch(inputs, () => {
<template #item="{ element, index }">
<div class="flex py-1 items-center">
<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']"
overlay-class-name="nc-dropdown-select-color-options"
>
<template #overlay>
<GeneralColorPicker v-model="element.color" :pick-button="true" @update:model-value="colorMenus[index] = false" />
</template>

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

@ -28,7 +28,13 @@ const editColumnDropdown = ref(false)
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" @edit="editColumnDropdown = true" />
</template>
<a-dropdown v-model:visible="editColumnDropdown" class="h-full" :trigger="['click']" placement="bottomRight">
<a-dropdown
v-model:visible="editColumnDropdown"
class="h-full"
:trigger="['click']"
placement="bottomRight"
overlay-class-name="nc-dropdown-edit-column"
>
<div />
<template #overlay>
<SmartsheetColumnEditOrAddProvider

11
packages/nc-gui/components/smartsheet-header/CellIcon.vue

@ -1,14 +1,11 @@
<script setup lang="ts">
import type { ColumnType } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { toRef } from 'vue'
import { ColumnInj } from '~/context'
import { ColumnInj, toRef, useColumn } from '#imports'
import FilePhoneIcon from '~icons/mdi/file-phone'
import { useColumn } from '#imports'
import KeyIcon from '~icons/mdi/key-variant'
import JSONIcon from '~icons/mdi/code-json'
import ClockIcon from '~icons/mdi/clock-time-five'
// import FKIcon from '~icons/mdi/link-variant'
import WebIcon from '~icons/mdi/web'
import TextAreaIcon from '~icons/mdi/card-text-outline'
import StringIcon from '~icons/mdi/alpha-a-box-outline'
@ -65,11 +62,7 @@ const icon = computed(() => {
return DecimalIcon
} else if (additionalColMeta.isPhoneNumber.value) {
return FilePhoneIcon
}
// else if(additionalColMeta.isForeignKey) {
// return FKIcon
// }
else if (additionalColMeta.isURL.value) {
} else if (additionalColMeta.isURL.value) {
return WebIcon
} else if (additionalColMeta.isCurrency.value) {
return CurrencyIcon

4
packages/nc-gui/components/smartsheet-header/Menu.vue

@ -10,7 +10,7 @@ const emit = defineEmits(['edit'])
const column = inject(ColumnInj)
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const isLocked = inject(IsLockedInj)
@ -61,7 +61,7 @@ const setAsPrimaryValue = async () => {
</script>
<template>
<a-dropdown v-if="!isLocked" placement="bottomRight" :trigger="['click']">
<a-dropdown v-if="!isLocked" placement="bottomRight" :trigger="['click']" overlay-class-name="nc-dropdown-column-operations">
<MdiMenuDown class="h-full text-grey nc-ui-dt-dropdown cursor-pointer outline-0" />
<template #overlay>

14
packages/nc-gui/components/smartsheet-header/VirtualCell.vue

@ -1,7 +1,6 @@
<script setup lang="ts">
import type { ColumnType, FormulaType, LinkToAnotherRecordType, LookupType, RollupType } from 'nocodb-sdk'
import { substituteColumnIdWithAliasInFormula } from 'nocodb-sdk'
import { useI18n } from 'vue-i18n'
import {
ColumnInj,
IsFormInj,
@ -11,12 +10,13 @@ import {
provide,
ref,
toRef,
useI18n,
useMetas,
useUIPermission,
useVirtualCell,
} from '#imports'
const props = defineProps<{ column: ColumnType & { meta: any }; hideMenu?: boolean; required?: boolean | number }>()
const props = defineProps<{ column: ColumnType; hideMenu?: boolean; required?: boolean | number }>()
const { t } = useI18n()
@ -32,7 +32,7 @@ const { metas } = useMetas()
const { isUIAllowed } = useUIPermission()
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const isForm = inject(IsFormInj, ref(false))
@ -115,7 +115,13 @@ const tooltipMsg = computed(() => {
<SmartsheetHeaderMenu v-if="!isForm && isUIAllowed('edit-column')" :virtual="true" @edit="editColumnDropdown = true" />
</template>
<a-dropdown v-model:visible="editColumnDropdown" class="h-full" :trigger="['click']" placement="bottomRight">
<a-dropdown
v-model:visible="editColumnDropdown"
class="h-full"
:trigger="['click']"
placement="bottomRight"
overlay-class-name="nc-dropdown-edit-column"
>
<div />
<template #overlay>
<SmartsheetColumnEditOrAddProvider

7
packages/nc-gui/components/smartsheet-header/VirtualCellIcon.vue

@ -2,8 +2,7 @@
import type { ColumnType, LinkToAnotherRecordType, LookupType } from 'nocodb-sdk'
import { RelationTypes, UITypes } from 'nocodb-sdk'
import type { Ref } from 'vue'
import { toRef } from 'vue'
import { ColumnInj } from '~/context'
import { ColumnInj, toRef } from '#imports'
import GenericIcon from '~icons/mdi/square-rounded'
import HMIcon from '~icons/mdi/table-arrow-right'
import BTIcon from '~icons/mdi/table-arrow-left'
@ -25,9 +24,9 @@ if (column) {
const { isLookup, isBt, isRollup, isMm, isHm } = useVirtualCell(column as Ref<ColumnType>)
if (isLookup || isBt || isRollup || isMm || isHm) {
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
relationColumn = meta?.value.columns?.find((c) => c.id === column.value?.colOptions?.fk_relation_column_id) as ColumnType & {
relationColumn = meta.value?.columns?.find((c) => c.id === column.value?.colOptions?.fk_relation_column_id) as ColumnType & {
colOptions: LinkToAnotherRecordType
}
}

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

@ -14,7 +14,7 @@ const onClick = () => {
<a-tooltip placement="bottom">
<template #title> {{ $t('activity.addRow') }} </template>
<div
v-t="['c:row:add:grid-top']"
v-e="['c:row:add:grid-top']"
:class="{ 'group': !isLocked, 'disabled-ring': isLocked }"
class="nc-add-new-row-btn flex align-center"
>

25
packages/nc-gui/components/smartsheet-toolbar/ColumnFilter.vue

@ -21,10 +21,12 @@ interface Props {
parentId?: string
autoSave: boolean
hookId?: string
showLoading?: boolean
modelValue?: Filter[]
webHook?: boolean
}
const { nested = false, parentId, autoSave = true, hookId = null, modelValue } = defineProps<Props>()
const { nested = false, parentId, autoSave = true, hookId = null, modelValue, showLoading = true, webHook } = defineProps<Props>()
const emit = defineEmits(['update:filtersLength'])
@ -33,9 +35,9 @@ const logicalOps = [
{ value: 'or', text: 'OR' },
]
const meta = inject(MetaInj)!
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj)!
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
@ -46,9 +48,7 @@ const { filters, deleteFilter, saveOrUpdate, loadFilters, addFilter, addFilterGr
activeView,
parentId,
computed(() => autoSave),
() => {
reloadDataHook.trigger()
},
() => reloadDataHook.trigger(showLoading),
modelValue || nestedFilters.value,
!modelValue,
)
@ -102,7 +102,7 @@ const types = computed(() => {
watch(
() => activeView.value?.id,
(n, o) => {
if (n !== o) loadFilters(hookId as string)
if (n !== o && (hookId || !webHook)) loadFilters(hookId as string)
},
{ immediate: true },
)
@ -134,11 +134,11 @@ defineExpose({
<template>
<div
class="p-6 menu-filter-dropdown bg-gray-50 !border"
:class="{ 'shadow-xl min-w-[430px] max-w-[630px] max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }"
class="p-4 menu-filter-dropdown bg-gray-50 !border mt-4"
:class="{ 'shadow min-w-[430px] max-w-[630px] max-h-[max(80vh,500px)] overflow-auto': !nested, 'border-1 w-full': nested }"
>
<div v-if="filters && filters.length" class="nc-filter-grid mb-2" @click.stop>
<template v-for="(filter, i) in filters" :key="filter.id || i">
<template v-for="(filter, i) in filters" :key="i">
<template v-if="filter.status !== 'delete'">
<template v-if="filter.is_group">
<MdiCloseBox
@ -156,6 +156,7 @@ defineExpose({
:dropdown-match-select-width="false"
class="shrink grow-0"
placeholder="Group op"
dropdown-class-name="nc-dropdown-filter-logical-op-group"
@click.stop
@change="saveOrUpdate(filter, i)"
>
@ -203,6 +204,7 @@ defineExpose({
class="h-full"
hide-details
:disabled="filter.readOnly"
dropdown-class-name="nc-dropdown-filter-logical-op"
@click.stop
@change="filterUpdateCondition(filter, i)"
>
@ -230,6 +232,7 @@ defineExpose({
variant="solo"
:disabled="filter.readOnly"
hide-details
dropdown-class-name="nc-dropdown-filter-comp-op"
@change="filterUpdateCondition(filter, i)"
>
<a-select-option v-for="compOp in comparisonOpList" :key="compOp.value" :value="compOp.value" class="">
@ -278,7 +281,7 @@ defineExpose({
{{ $t('activity.addFilter') }}
</div>
</a-button>
<a-button class="text-capitalize !text-gray-500" @click.stop="addFilterGroup">
<a-button v-if="!webHook" class="text-capitalize !text-gray-500" @click.stop="addFilterGroup">
<div class="flex items-center gap-1">
<!-- <v-icon small color="grey"> mdi-plus </v-icon> -->
<!-- Add Filter Group -->

8
packages/nc-gui/components/smartsheet-toolbar/ColumnFilterMenu.vue

@ -5,7 +5,7 @@ import { ActiveViewInj, IsLockedInj, IsPublicInj, computed, inject, ref, useGlob
const isLocked = inject(IsLockedInj, ref(false))
const activeView = inject(ActiveViewInj)
const activeView = inject(ActiveViewInj, ref())
const isPublic = inject(IsPublicInj, ref(false))
@ -51,9 +51,9 @@ const filterAutoSaveLoc = computed({
</script>
<template>
<a-dropdown :trigger="['click']">
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-filter-menu">
<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-e="['c:filter']" class="nc-filter-menu-btn nc-toolbar-btn txt-sm" :disabled="isLocked">
<div class="flex items-center gap-1">
<MdiFilterOutline />
<!-- Filter -->
@ -80,7 +80,7 @@ const filterAutoSaveLoc = computed({
<div class="flex-1" />
<a-button
v-show="!filterAutoSave"
v-t="['a:filter:auto-apply']"
v-e="['a:filter:auto-apply']"
size="small"
class="text-xs ml-2"
@click="applyChanges"

42
packages/nc-gui/components/smartsheet-toolbar/Erd.vue

@ -0,0 +1,42 @@
<script lang="ts" setup>
const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue'])
const meta = inject(MetaInj)
interface Props {
modelValue: boolean
}
const vModel = useVModel(props, 'modelValue', emits)
const selectedView = inject(ActiveViewInj)
</script>
<template>
<a-modal
v-model:visible="vModel"
size="small"
:footer="null"
width="max(900px,60vw)"
:closable="false"
wrap-class-name="erd-single-table-modal"
transition-name="fade"
>
<div class="flex flex-row justify-between w-full items-center mb-1">
<a-typography-title class="ml-4 select-none" type="secondary" :level="5">
{{ `${$t('title.erdView')}: ${selectedView?.title}` }}
</a-typography-title>
<a-button type="text" class="!rounded-md border-none -mt-1.5 -mr-1" @click="vModel = false">
<template #icon>
<MdiClose class="cursor-pointer mt-1 nc-modal-close" />
</template>
</a-button>
</div>
<div class="w-full h-full !py-0 !px-2" style="height: 70vh">
<ErdView :table="meta" />
</div>
</a-modal>
</template>

4
packages/nc-gui/components/smartsheet-toolbar/Export.vue

@ -1,6 +1,6 @@
<template>
<a-dropdown :trigger="['click']">
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center">
<MdiDownload class="group-hover:text-accent text-gray-500" />
<span class="text-capitalize !text-sm font-weight-normal">Download</span>

17
packages/nc-gui/components/smartsheet-toolbar/ExportSubActions.vue

@ -1,4 +1,5 @@
<script setup lang="ts">
import type { RequestParams } from 'nocodb-sdk'
import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver'
import * as XLSX from 'xlsx'
@ -15,7 +16,7 @@ const { project } = useProject()
const { $api } = useNuxtApp()
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const selectedView = inject(ActiveViewInj)
@ -35,8 +36,8 @@ const exportFile = async (exportType: ExportTypes) => {
} else {
res = await $api.dbViewRow.export(
'noco',
project?.value.title as string,
meta?.value.title as string,
project.value?.title as string,
meta.value?.title as string,
selectedView?.value.title as string,
exportType,
{
@ -47,16 +48,16 @@ const exportFile = async (exportType: ExportTypes) => {
sortArrJson: JSON.stringify(sorts.value),
filterArrJson: JSON.stringify(nestedFilters.value),
},
} as any,
} as RequestParams,
)
}
const { data, headers } = res
if (exportType === ExportTypes.EXCEL) {
const workbook = XLSX.read(data, { type: 'base64' })
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`)
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`)
FileSaver.saveAs(blob, `${meta.value?.title}_exported_${c++}.csv`)
}
offset = +headers['nc-export-offset']
if (offset > -1) {
@ -75,14 +76,14 @@ const exportFile = async (exportType: ExportTypes) => {
<template>
<a-menu-item>
<div v-t="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)">
<div v-e="['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)">
<div v-e="['a:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}

32
packages/nc-gui/components/smartsheet-toolbar/FieldListAutoCompleteDropdown.vue

@ -2,8 +2,7 @@
import type { SelectProps } from 'ant-design-vue'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { RelationTypes, UITypes, isVirtualCol } from 'nocodb-sdk'
import { computed } from 'vue'
import { MetaInj } from '~/context'
import { MetaInj, computed } from '#imports'
import VirtualCellIcon from '~/components/smartsheet-header/VirtualCellIcon.vue'
import CellIcon from '~/components/smartsheet-header/CellIcon.vue'
@ -16,39 +15,13 @@ const { modelValue, isSort } = defineProps<Props>()
const emit = defineEmits(['update:modelValue'])
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const localValue = computed({
get: () => modelValue,
set: (val) => emit('update:modelValue', val),
})
/* export default {
name: 'FieldListAutoCompleteDropdown',
props: {
columns: Array,
value: String,
},
computed: {
localValue: {
set(v) {
this.$emit('input', v)
},
get() {
return this.value
},
},
},
mounted() {
const autocompleteInput = this.$refs.field.$refs.input
autocompleteInput.addEventListener('focus', this.onFocus, true)
},
methods: {
onFocus(e) {
this.$refs.field.isMenuActive = true // open item list
},
},
} */
const options = computed<SelectProps['options']>(() =>
meta?.value?.columns
?.filter((c: ColumnType) => {
@ -84,6 +57,7 @@ const filterOption = (input: string, option: any) => {
show-search
:placeholder="$t('placeholder.selectField')"
:filter-option="filterOption"
dropdown-class-name="nc-dropdown-toolbar-field-list"
>
<a-select-option v-for="option in options" :key="option.value" :value="option.value">
<div class="flex gap-2 items-center items-center h-full">

24
packages/nc-gui/components/smartsheet-toolbar/FieldsMenu.vue

@ -20,12 +20,14 @@ import {
import CellIcon from '~/components/smartsheet-header/CellIcon.vue'
import VirtualCellIcon from '~/components/smartsheet-header/VirtualCellIcon.vue'
const meta = inject(MetaInj)!
const meta = inject(MetaInj, ref())
const activeView = inject(ActiveViewInj)!
const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const reloadViewMetaHook = inject(ReloadViewMetaHookInj)!
const rootFields = inject(FieldsInj)
const isLocked = inject(IsLockedInj, ref(false))
@ -48,7 +50,7 @@ const {
} = useViewColumns(activeView, meta, () => reloadDataHook.trigger())
watch(
() => (activeView.value as any)?.id,
() => activeView.value?.id,
async (newVal, oldVal) => {
if (newVal !== oldVal && meta.value) {
await loadViewColumns()
@ -93,7 +95,7 @@ const coverImageColumnId = computed({
fk_cover_image_col_id: val,
})
;(activeView.value?.view as GalleryType).fk_cover_image_col_id = val
reloadDataHook.trigger()
reloadViewMetaHook.trigger()
}
},
})
@ -116,9 +118,9 @@ const getIcon = (c: ColumnType) =>
</script>
<template>
<a-dropdown :trigger="['click']">
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-fields-menu">
<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-e="['c:fields']" class="nc-fields-menu-btn nc-toolbar-btn" :disabled="isLocked">
<div class="flex items-center gap-1">
<MdiEyeOffOutline />
@ -135,7 +137,13 @@ const getIcon = (c: ColumnType) =>
@click.stop
>
<a-card v-if="activeView.type === ViewTypes.GALLERY" size="small" title="Cover image">
<a-select v-model:value="coverImageColumnId" class="w-full" :options="coverOptions" @click.stop></a-select>
<a-select
v-model:value="coverImageColumnId"
class="w-full"
:options="coverOptions"
dropdown-class-name="nc-dropdown-cover-image"
@click.stop
></a-select>
</a-card>
<div class="p-1" @click.stop>
<a-input v-model:value="filterQuery" size="small" :placeholder="$t('placeholder.searchFields')" />
@ -146,7 +154,7 @@ const getIcon = (c: ColumnType) =>
<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"
v-t="['a:fields:show-hide']"
v-e="['a:fields:show-hide']"
class="shrink"
@change="saveOrUpdate(field, index)"
>

35
packages/nc-gui/components/smartsheet-toolbar/MoreActions.vue

@ -1,5 +1,6 @@
<script lang="ts" setup>
import * as XLSX from 'xlsx'
import type { RequestParams } from 'nocodb-sdk'
import { ExportTypes } from 'nocodb-sdk'
import FileSaver from 'file-saver'
import { message } from 'ant-design-vue'
@ -29,11 +30,11 @@ const { project } = useProject()
const { $api } = useNuxtApp()
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const fields = inject(FieldsInj, ref([]))
const selectedView = inject(ActiveViewInj)
const selectedView = inject(ActiveViewInj, ref())
const { sorts, nestedFilters } = useSmartsheetStoreOrThrow()
@ -60,8 +61,8 @@ const exportFile = async (exportType: ExportTypes) => {
res = await $api.dbViewRow.export(
'noco',
project?.value.title as string,
meta?.value.title as string,
selectedView?.value.title as string,
meta.value?.title as string,
selectedView.value?.title as string,
exportType,
{
responseType,
@ -71,16 +72,16 @@ const exportFile = async (exportType: ExportTypes) => {
sortArrJson: JSON.stringify(sorts.value),
filterArrJson: JSON.stringify(nestedFilters.value),
},
} as any,
} as RequestParams,
)
}
const { data, headers } = res
if (exportType === ExportTypes.EXCEL) {
const workbook = XLSX.read(data, { type: 'base64' })
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`)
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`)
FileSaver.saveAs(blob, `${meta.value?.title}_exported_${c++}.csv`)
}
offset = +headers['nc-export-offset']
if (offset > -1) {
@ -100,7 +101,7 @@ const exportFile = async (exportType: ExportTypes) => {
<template>
<div>
<a-dropdown>
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-1 items-center">
<MdiFlashOutline />
@ -114,13 +115,13 @@ const exportFile = async (exportType: ExportTypes) => {
<template #overlay>
<div class="bg-gray-50 py-2 shadow-lg !border">
<div>
<div v-t="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<div v-e="['a:actions:download-csv']" class="nc-menu-item" @click="exportFile(ExportTypes.CSV)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as CSV -->
{{ $t('activity.downloadCSV') }}
</div>
<div v-t="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<div v-e="['a:actions:download-excel']" class="nc-menu-item" @click="exportFile(ExportTypes.EXCEL)">
<MdiDownloadOutline class="text-gray-500" />
<!-- Download as XLSX -->
{{ $t('activity.downloadExcel') }}
@ -128,7 +129,7 @@ const exportFile = async (exportType: ExportTypes) => {
<div
v-if="isUIAllowed('csvImport') && !isView && !isPublicView"
v-t="['a:actions:upload-csv']"
v-e="['a:actions:upload-csv']"
class="nc-menu-item"
:class="{ disabled: isLocked }"
@click="!isLocked ? (quickImportDialog = true) : {}"
@ -140,7 +141,7 @@ const exportFile = async (exportType: ExportTypes) => {
<div
v-if="isUIAllowed('sharedViewList') && !isView && !isPublicView"
v-t="['a:actions:shared-view-list']"
v-e="['a:actions:shared-view-list']"
class="nc-menu-item"
@click="sharedViewListDlg = true"
>
@ -150,7 +151,7 @@ const exportFile = async (exportType: ExportTypes) => {
</div>
<div
v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-t="['c:actions:webhook']"
v-e="['c:actions:webhook']"
class="nc-menu-item"
@click="showWebhookDrawer = true"
>
@ -166,7 +167,13 @@ const exportFile = async (exportType: ExportTypes) => {
<WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" />
<a-modal v-model:visible="sharedViewListDlg" :title="$t('activity.listSharedView')" width="max(900px,60vw)" :footer="null">
<a-modal
v-model:visible="sharedViewListDlg"
:title="$t('activity.listSharedView')"
width="max(900px,60vw)"
:footer="null"
wrap-class-name="nc-modal-shared-view-list"
>
<SmartsheetToolbarSharedViewList v-if="sharedViewListDlg" />
</a-modal>
</div>

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

@ -41,7 +41,7 @@ function onPressEnter() {
size="small"
:dropdown-match-select-width="false"
:options="columns"
dropdown-class-name="!py-0 !rounded"
dropdown-class-name="!py-0 !rounded nc-dropdown-toolbar-search-field-option"
class="!absolute top-0 left-0 w-full h-full z-10 !text-xs opacity-0"
/>
</div>

26
packages/nc-gui/components/smartsheet-toolbar/ShareView.vue

@ -1,9 +1,15 @@
<script lang="ts" setup>
import { useClipboard } from '@vueuse/core'
import { ViewTypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { computed, extractSdkResponseErrorMsg, useNuxtApp, useProject, useSmartsheetStoreOrThrow } from '#imports'
import {
computed,
extractSdkResponseErrorMsg,
useCopy,
useI18n,
useNuxtApp,
useProject,
useSmartsheetStoreOrThrow,
} from '#imports'
import MdiOpenInNewIcon from '~icons/mdi/open-in-new'
import MdiCopyIcon from '~icons/mdi/content-copy'
@ -11,7 +17,7 @@ const { t } = useI18n()
const { view, $api } = useSmartsheetStoreOrThrow()
const { copy } = useClipboard()
const { copy } = useCopy()
const { $e } = useNuxtApp()
@ -39,7 +45,7 @@ const allowCSVDownload = computed({
})
const genShareLink = async () => {
shared.value = await $api.dbViewShare.create(view.value.id as string)
shared.value = await $api.dbViewShare.create(view.value?.id as string)
shared.value.meta =
shared.value.meta && typeof shared.value.meta === 'string' ? JSON.parse(shared.value.meta) : shared.value.meta
passwordProtected.value = shared.value.password !== null && shared.value.password !== ''
@ -69,7 +75,7 @@ async function saveAllowCSVDownload() {
const meta = shared.value.meta && typeof shared.value.meta === 'string' ? JSON.parse(shared.value.meta) : shared.value.meta
await $api.dbViewShare.update(shared.value.id, {
meta,
} as any)
})
// Successfully updated
message.success(t('msg.success.updated'))
} catch (e: any) {
@ -114,7 +120,7 @@ watch(passwordProtected, (value) => {
<div>
<a-button
v-if="isUIAllowed('share-view') && !isSharedBase"
v-t="['c:view:share']"
v-e="['c:view:share']"
outlined
class="nc-btn-share-view nc-toolbar-btn"
>
@ -132,14 +138,14 @@ watch(passwordProtected, (value) => {
:title="$t('msg.info.privateLink')"
:footer="null"
width="min(100vw,640px)"
wrap-class-name="nc-modal-share-view"
>
<div class="share-link-box nc-share-link-box bg-primary-50">
<div class="flex-1 h-min text-xs">{{ sharedViewUrl }}</div>
<!-- <v-spacer /> -->
<a v-t="['c:view:share:open-url']" :href="sharedViewUrl" target="_blank">
<a v-e="['c:view:share:open-url']" :href="sharedViewUrl" target="_blank">
<MdiOpenInNewIcon class="text-sm text-gray-500 mt-2" />
</a>
<MdiCopyIcon v-t="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" />
<MdiCopyIcon v-e="['c:view:share:copy-url']" class="text-gray-500 text-sm cursor-pointer" @click="copyLink" />
</div>
<a-collapse ghost>

11
packages/nc-gui/components/smartsheet-toolbar/SharedViewList.vue

@ -1,17 +1,12 @@
<script lang="ts" setup>
import { useClipboard } from '@vueuse/core'
import { ViewTypes } from 'nocodb-sdk'
import { Empty, message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { onMounted, useSmartsheetStoreOrThrow } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils/errorUtils'
import { extractSdkResponseErrorMsg, onMounted, useCopy, useI18n, useSmartsheetStoreOrThrow } from '#imports'
import MdiVisibilityOnIcon from '~icons/mdi/visibility'
import MdiVisibilityOffIcon from '~icons/mdi/visibility-off'
import MdiCopyIcon from '~icons/mdi/content-copy'
import MdiDeleteIcon from '~icons/mdi/delete-outline'
const { t } = useI18n()
interface SharedViewType {
password: string
title: string
@ -21,9 +16,11 @@ interface SharedViewType {
showPassword?: boolean
}
const { t } = useI18n()
const { $api, meta } = useSmartsheetStoreOrThrow()
const { copy } = useClipboard()
const { copy } = useCopy()
const { dashboardUrl } = useDashboard()

14
packages/nc-gui/components/smartsheet-toolbar/SortListMenu.vue

@ -14,14 +14,14 @@ import {
watch,
} from '#imports'
const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false))
const reloadDataHook = inject(ReloadViewDataHookInj)
const { sorts, saveOrUpdate, loadSorts, addSort, deleteSort } = useViewSorts(view, () => reloadDataHook?.trigger())
const columns = computed(() => meta?.value?.columns || [])
const columns = computed(() => meta.value?.columns || [])
const columnByID = computed(() =>
columns.value.reduce((obj, col) => {
@ -32,7 +32,7 @@ const columnByID = computed(() =>
)
watch(
() => (view?.value as any)?.id,
() => view.value?.id,
() => {
loadSorts()
},
@ -41,9 +41,9 @@ watch(
</script>
<template>
<a-dropdown offset-y class="" :trigger="['click']" overlay-class-name="sort-menu-overlay">
<a-dropdown offset-y class="" :trigger="['click']" overlay-class-name="nc-dropdown-sort-menu">
<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-e="['c:sort']" class="nc-sort-menu-btn nc-toolbar-btn" :disabled="isLocked"
><div class="flex items-center gap-1">
<MdiSort />
<!-- Sort -->
@ -71,9 +71,9 @@ watch(
v-model:value="sort.direction"
class="shrink grow-0 nc-sort-dir-select !text-xs"
:label="$t('labels.operation')"
dropdown-class-name="sort-dir-dropdown nc-dropdown-sort-dir"
@click.stop
@select="saveOrUpdate(sort, i)"
dropdown-class-name="sort-dir-dropdown"
>
<a-select-option
v-for="(option, j) in getSortDirectionOptions(columnByID[sort.fk_column_id]?.uidt)"

64
packages/nc-gui/components/smartsheet-toolbar/ViewActions.vue

@ -1,6 +1,5 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import {
ActiveViewInj,
IsLockedInj,
@ -8,16 +7,18 @@ import {
extractSdkResponseErrorMsg,
inject,
ref,
useI18n,
useNuxtApp,
useProject,
useSmartsheetStoreOrThrow,
useUIPermission,
viewIcons,
} from '#imports'
import { LockType } from '~/lib'
import { viewIcons } from '~/utils'
import MdiLockOutlineIcon from '~icons/mdi/lock-outline'
import MdiAccountIcon from '~icons/mdi/account'
import MdiAccountGroupIcon from '~icons/mdi/account-group'
import AcountTreeRoundedIcon from '~icons/material-symbols/account-tree-rounded'
const { t } = useI18n()
@ -37,6 +38,8 @@ const showWebhookDrawer = ref(false)
const showApiSnippetDrawer = ref(false)
const showErd = ref(false)
const quickImportDialog = ref(false)
const { isUIAllowed } = useUIPermission()
@ -44,7 +47,7 @@ const { isUIAllowed } = useUIPermission()
const { isSharedBase } = useProject()
const Icon = computed(() => {
switch ((selectedView?.value as any)?.lock_type) {
switch (selectedView?.value.lock_type) {
case LockType.Personal:
return MdiAccountIcon
case LockType.Locked:
@ -65,8 +68,8 @@ async function changeLockType(type: LockType) {
return message.info(t('msg.toast.futureRelease'))
}
try {
;(selectedView.value as any).lock_type = type
$api.dbView.update(selectedView.value.id as string, {
selectedView.value.lock_type = type
await $api.dbView.update(selectedView.value.id as string, {
lock_type: type,
})
@ -81,8 +84,8 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<template>
<div>
<a-dropdown :trigger="['click']">
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center">
<component
:is="viewIcons[selectedView?.type].icon"
@ -106,7 +109,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
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">
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group px-0 !py-0">
<SmartsheetToolbarLockType hide-tick :type="selectedView?.lock_type || LockType.Collaborative" />
<MaterialSymbolsChevronRightRounded
@ -130,7 +133,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-sub-menu key="download">
<template #title>
<!-- Download -->
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiDownload class="group-hover:text-accent text-gray-500" />
{{ $t('general.download') }}
<div class="flex-1" />
@ -148,7 +151,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-sub-menu key="upload">
<!-- Upload -->
<template #title>
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group">
<MdiUpload class="group-hover:text-accent text-gray-500" />
{{ $t('general.upload') }}
<div class="flex-1" />
@ -160,10 +163,9 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
</template>
<template #expandIcon></template>
<a-menu-item>
<a-menu-item v-if="isUIAllowed('csvImport') && !isView && !isPublicView">
<div
v-if="isUIAllowed('csvImport') && !isView && !isPublicView"
v-t="['a:actions:upload-csv']"
v-e="['a:actions:upload-csv']"
class="nc-project-menu-item"
:class="{ disabled: isLocked }"
@click="!isLocked ? (quickImportDialog = true) : {}"
@ -177,13 +179,8 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
</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"
>
<a-menu-item v-if="isUIAllowed('SharedViewList') && !isView && !isPublicView">
<div v-e="['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') }}
@ -192,7 +189,7 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<a-menu-item v-if="!isSqlView">
<div
v-if="isUIAllowed('webhook') && !isView && !isPublicView"
v-t="['c:actions:webhook']"
v-e="['c:actions:webhook']"
class="py-2 flex gap-2 items-center"
@click="showWebhookDrawer = true"
>
@ -200,18 +197,19 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
{{ $t('objects.webhooks') }}
</div>
</a-menu-item>
<a-menu-item>
<div
v-if="!isSharedBase && !isPublicView"
v-t="['c:snippet:open']"
class="py-2 flex gap-2 items-center"
@click="showApiSnippetDrawer = true"
>
<a-menu-item v-if="!isSharedBase && !isPublicView">
<div v-e="['c:snippet:open']" class="py-2 flex gap-2 items-center" @click="showApiSnippetDrawer = true">
<MdiXml class="text-gray-500" />
<!-- Get API Snippet -->
{{ $t('activity.getApiSnippet') }}
</div>
</a-menu-item>
<a-menu-item>
<div v-e="['c:erd:open']" class="py-2 flex gap-2 items-center nc-view-action-erd" @click="showErd = true">
<AcountTreeRoundedIcon class="text-gray-500" />
{{ $t('title.erdView') }}
</div>
</a-menu-item>
</a-menu-item-group>
</a-menu>
</template>
@ -221,7 +219,15 @@ const { isSqlView } = useSmartsheetStoreOrThrow()
<WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" />
<a-modal v-model:visible="sharedViewListDlg" :title="$t('activity.listSharedView')" width="max(900px,60vw)" :footer="null">
<SmartsheetToolbarErd v-model="showErd" />
<a-modal
v-model:visible="sharedViewListDlg"
:title="$t('activity.listSharedView')"
width="max(900px,60vw)"
:footer="null"
wrap-class-name="nc-modal-shared-view-list"
>
<SmartsheetToolbarSharedViewList v-if="sharedViewListDlg" />
</a-modal>
<SmartsheetApiSnippet v-model="showApiSnippetDrawer" />

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

@ -1,10 +1,18 @@
<script setup lang="ts">
import HTTPSnippet from 'httpsnippet'
import { useClipboard } from '@vueuse/core'
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import { ActiveViewInj, MetaInj } from '~/context'
import { inject, useGlobal, useProject, useSmartsheetStoreOrThrow, useVModel, useViewData } from '#imports'
import {
ActiveViewInj,
MetaInj,
inject,
useCopy,
useGlobal,
useI18n,
useProject,
useSmartsheetStoreOrThrow,
useVModel,
useViewData,
} from '#imports'
const props = defineProps<Props>()
@ -20,15 +28,15 @@ const { project } = $(useProject())
const { appInfo, token } = $(useGlobal())
const meta = $(inject(MetaInj)!)
const meta = $(inject(MetaInj, ref()))
const view = $(inject(ActiveViewInj)!)
const view = $(inject(ActiveViewInj, ref()))
const { xWhere } = useSmartsheetStoreOrThrow()
const { queryParams } = $(useViewData($$(meta), view as any, xWhere))
const { queryParams } = $(useViewData($$(meta), $$(view), xWhere))
const { copy } = useClipboard()
const { copy } = useCopy()
let vModel = $(useVModel(props, 'modelValue', emits))
@ -136,7 +144,7 @@ watch($$(activeLang), (newLang) => {
<template>
<a-drawer
v-model:visible="vModel"
class="h-full relative"
class="h-full relative nc-drawer-api-snippet"
style="color: red"
placement="right"
size="large"
@ -163,13 +171,18 @@ watch($$(activeLang), (newLang) => {
hide-minimap
/>
<div v-if="activeLang.clients" class="flex flex-row w-full justify-end space-x-3 mt-4 uppercase">
<a-select v-if="activeLang" v-model:value="selectedClient" style="width: 6rem">
<a-select
v-if="activeLang"
v-model:value="selectedClient"
style="width: 6rem"
dropdown-class-name="nc-dropdown-snippet-active-lang"
>
<a-select-option v-for="(client, i) in activeLang?.clients" :key="i" class="!w-full uppercase" :value="client">
{{ client }}
</a-select-option>
</a-select>
<a-button
v-t="[
v-e="[
'c:snippet:copy',
{ client: activeLang?.clients && (selectedClient || activeLang?.clients[0]), lang: activeLang?.name },
]"
@ -181,7 +194,7 @@ watch($$(activeLang), (newLang) => {
<div class="absolute bottom-4 flex flex-row justify-center w-[95%]">
<a
v-t="['e:hiring']"
v-e="['e:hiring']"
class="px-4 py-2 ! rounded shadow"
href="https://angel.co/company/nocodb"
target="_blank"

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

@ -2,8 +2,6 @@
import Draggable from 'vuedraggable'
import { RelationTypes, UITypes, getSystemColumns, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import type { Permission } from '~/composables/useUIPermission/rolePermissions'
import {
ActiveViewInj,
IsFormInj,
@ -18,12 +16,14 @@ import {
ref,
useDebounceFn,
useGlobal,
useI18n,
useNuxtApp,
useUIPermission,
useViewColumns,
useViewData,
watch,
} from '#imports'
import type { Permission } from '~/composables/useUIPermission/rolePermissions'
provide(IsFormInj, ref(true))
provide(IsGalleryInj, ref(false))
@ -45,11 +45,11 @@ const secondsRemain = ref(0)
const isEditable = isUIAllowed('editFormView' as Permission)
const meta = inject(MetaInj)!
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj)
const view = inject(ActiveViewInj, ref())
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view as any)
const { loadFormView, insertRow, formColumnData, formViewData, updateFormView } = useViewData(meta, view)
const reloadEventHook = createEventHook<void>()
provide(ReloadViewDataHookInj, reloadEventHook)
@ -59,7 +59,7 @@ reloadEventHook.on(async () => {
setFormData()
})
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta as any, async () => reloadEventHook.trigger())
const { showAll, hideAll, saveOrUpdate } = useViewColumns(view, meta, async () => reloadEventHook.trigger())
const { syncLTARRefs, row } = useProvideSmartsheetRowStore(
meta,
@ -243,8 +243,10 @@ async function checkSMTPStatus() {
emailMe.value = false
// Please activate SMTP plugin in App store for enabling email notification
message.info(t('msg.toast.formEmailSMTP'))
return false
}
}
return true
}
function setFormData() {
@ -252,10 +254,9 @@ function setFormData() {
formViewData.value = {
...formViewData.value,
submit_another_form: !!(formViewData?.value?.submit_another_form ?? 0),
// todo: show_blank_form missing from FormType
show_blank_form: !!((formViewData?.value as any)?.show_blank_form ?? 0),
} as any
submit_another_form: !!(formViewData.value?.submit_another_form ?? 0),
show_blank_form: !!(formViewData.value?.show_blank_form ?? 0),
}
// email me
let data: Record<string, boolean> = {}
@ -264,7 +265,6 @@ function setFormData() {
} catch (e) {}
emailMe.value = data[state.user.value?.email as string]
checkSMTPStatus()
localColumns.value = col
.filter(
@ -307,12 +307,13 @@ function isRequired(_columnObj: Record<string, any>, required = false) {
return required || (columnObj && columnObj.rqd && !columnObj.cdf)
}
function updateEmail() {
async function updateEmail() {
try {
const data = JSON.parse(formViewData.value?.email) || {}
if (!(await checkSMTPStatus())) return
const data = formViewData.value?.email ? JSON.parse(formViewData.value?.email) : {}
data[state.user.value?.email as string] = emailMe.value
formViewData.value!.email = JSON.stringify(data)
checkSMTPStatus()
} catch (e) {}
}
@ -338,7 +339,7 @@ const updateColMeta = useDebounceFn(async (col: Record<string, any>) => {
}, 250)
watch(submitted, (v) => {
if (v && (formViewData?.value as any)?.show_blank_form) {
if (v && formViewData.value?.show_blank_form) {
secondsRemain.value = 5
const intvl = setInterval(() => {
if (--secondsRemain.value < 0) {
@ -470,7 +471,7 @@ onMounted(async () => {
<!-- Drag and drop fields here to hide -->
{{ $t('msg.info.dragDropHide') }}
</div>
<a-dropdown v-model:visible="showColumnDropdown" :trigger="['click']">
<a-dropdown v-model:visible="showColumnDropdown" :trigger="['click']" overlay-class-name="nc-dropdown-form-add-column">
<a-button type="link" class="w-full caption mt-2" size="large" @click.stop="showColumnDropdown = true">
<div class="flex items-center prose-sm justify-center text-gray-400">
<mdi-plus />
@ -591,7 +592,7 @@ onMounted(async () => {
<a-switch
v-model:checked="element.required"
v-t="['a:form-view:field:mark-required']"
v-e="['a:form-view:field:mark-required']"
size="small"
class="ml-2"
@change="updateColMeta(element)"
@ -642,7 +643,7 @@ onMounted(async () => {
v-if="isVirtualCol(element)"
class="!m-0 gap-0 p-0"
:name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]"
:rules="[{ required: isRequired(element, element.required), message: `${element.title} is required` }]"
>
<SmartsheetVirtualCell
v-model="formState[element.title]"
@ -658,7 +659,7 @@ onMounted(async () => {
v-else
class="!m-0 gap-0 p-0"
:name="element.title"
:rules="[{ required: element.required, message: `${element.title} is required` }]"
:rules="[{ required: isRequired(element, element.required), message: `${element.title} is required` }]"
>
<SmartsheetCell
v-model="formState[element.title]"
@ -715,7 +716,7 @@ onMounted(async () => {
<!-- Show "Submit Another Form" button -->
<a-switch
v-model:checked="formViewData.submit_another_form"
v-t="[`a:form-view:submit-another-form`]"
v-e="[`a:form-view:submit-another-form`]"
size="small"
class="nc-form-checkbox-submit-another-form"
@change="updateView"
@ -727,7 +728,7 @@ onMounted(async () => {
<!-- Show a blank form after 5 seconds -->
<a-switch
v-model:checked="formViewData.show_blank_form"
v-t="[`a:form-view:show-blank-form`]"
v-e="[`a:form-view:show-blank-form`]"
size="small"
class="nc-form-checkbox-show-blank-form"
@change="updateView"
@ -738,7 +739,7 @@ onMounted(async () => {
<div class="my-4">
<a-switch
v-model:checked="emailMe"
v-t="[`a:form-view:email-me`]"
v-e="[`a:form-view:email-me`]"
size="small"
class="nc-form-checkbox-send-email"
@change="onEmailChange"

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

@ -1,8 +1,6 @@
<script lang="ts" setup>
import { onMounted } from '@vue/runtime-core'
import { isVirtualCol } from 'nocodb-sdk'
import { inject, provide, useViewData } from '#imports'
import Row from '~/components/smartsheet/Row.vue'
import type { Row as RowType } from '~/composables'
import {
ActiveViewInj,
ChangePageInj,
@ -14,16 +12,23 @@ import {
OpenNewRecordFormHookInj,
PaginationDataInj,
ReadonlyInj,
} from '~/context'
ReloadViewMetaHookInj,
extractPkFromRow,
inject,
provide,
useViewData,
} from '#imports'
import Row from '~/components/smartsheet/Row.vue'
import type { Row as RowType } from '~/composables'
import ImageIcon from '~icons/mdi/file-image-box'
interface Attachment {
url: string
}
const meta = inject(MetaInj)
const view = inject(ActiveViewInj)
const reloadViewDataHook = inject(ReloadViewDataHookInj)
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const reloadViewMetaHook = inject(ReloadViewMetaHookInj)
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
const expandedFormDlg = ref(false)
@ -38,7 +43,7 @@ const {
galleryData,
changePage,
addEmptyRow,
} = useViewData(meta, view as any)
} = useViewData(meta, view)
const { isUIAllowed } = useUIPermission()
@ -51,27 +56,20 @@ provide(ReadonlyInj, !isUIAllowed('xcDatatableEditable'))
const fields = inject(FieldsInj, ref([]))
const route = useRoute()
const router = useRouter()
const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== galleryData.value?.fk_cover_image_col_id))
const coverImageColumn: any = $(
computed(() =>
meta?.value.columnsById
meta.value?.columnsById
? meta.value.columnsById[galleryData.value?.fk_cover_image_col_id as keyof typeof meta.value.columnsById]
: {},
),
)
watch(
[meta, view],
async () => {
if (meta?.value && view?.value) {
await loadData()
await loadGalleryData()
}
},
{ immediate: true },
)
const isRowEmpty = (record: any, col: any) => {
const val = record.row[col.title]
if (!val) return true
@ -87,22 +85,23 @@ const attachments = (record: any): Array<Attachment> => {
}
}
const reloadAttachments = ref(false)
const expandForm = (row: RowType, _state?: Record<string, any>) => {
if (!isUIAllowed('xcDatatableEditable')) return
reloadViewDataHook?.on(async () => {
await loadData()
await loadGalleryData()
reloadAttachments.value = true
nextTick(() => {
reloadAttachments.value = false
})
})
const rowId = extractPkFromRow(row.row, meta.value.columns)
const expandForm = (row: RowType, state?: Record<string, any>) => {
if (!isUIAllowed('xcDatatableEditable')) return
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
if (rowId) {
router.push({
query: {
...route.query,
rowId,
},
})
} else {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
}
}
const expandFormClick = async (e: MouseEvent, row: RowType) => {
@ -116,10 +115,40 @@ openNewRecordFormHook?.on(async () => {
const newRow = await addEmptyRow()
expandForm(newRow)
})
const expandedFormOnRowIdDlg = computed({
get() {
return !!route.query.rowId
},
set(val) {
if (!val)
router.push({
query: {
...route.query,
rowId: undefined,
},
})
},
})
const reloadAttachments = ref(false)
reloadViewMetaHook?.on(async () => {
await loadGalleryData()
reloadAttachments.value = true
nextTick(() => {
reloadAttachments.value = false
})
})
onMounted(async () => {
await loadData()
await loadGalleryData()
})
</script>
<template>
<div class="flex flex-col h-full w-full overflow-auto">
<div class="flex flex-col h-full w-full overflow-auto nc-gallery">
<div class="nc-gallery-container grid gap-2 my-4 px-3">
<div v-for="record in data" :key="`record-${record.row.id}`">
<Row :row="record">
@ -132,7 +161,9 @@ openNewRecordFormHook?.on(async () => {
<a-carousel v-if="!reloadAttachments && attachments(record).length" autoplay class="gallery-carousel" arrows>
<template #customPaging>
<a>
<div class="pt-[12px]"><div></div></div>
<div class="pt-[12px]">
<div></div>
</div>
</a>
</template>
<template #prevArrow>
@ -144,7 +175,7 @@ openNewRecordFormHook?.on(async () => {
<img
v-for="(attachment, index) in attachments(record)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52"
class="h-52 object-cover"
:src="attachment.url"
/>
</a-carousel>
@ -183,6 +214,17 @@ openNewRecordFormHook?.on(async () => {
:row="expandedFormRow"
:state="expandedFormRowState"
:meta="meta"
:view="view"
/>
<SmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
:key="route.query.rowId"
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"
:row-id="route.query.rowId"
:view="view"
/>
</div>
</template>
@ -192,14 +234,17 @@ openNewRecordFormHook?.on(async () => {
grid-auto-rows: 1fr;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
:depp(.slick-dots li button) {
:deep(.slick-dots li button) {
background-color: black;
}
.ant-carousel.gallery-carousel :deep(.slick-dots) {
position: relative;
height: auto;
bottom: 0px;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
background: #000;
border: 0;
@ -215,15 +260,18 @@ openNewRecordFormHook?.on(async () => {
transition: all 0.5s;
width: 100%;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li.slick-active div > div) {
opacity: 1;
}
.ant-carousel.gallery-carousel :deep(.slick-prev) {
left: 0;
height: 100%;
top: 12px;
width: 50%;
}
.ant-carousel.gallery-carousel :deep(.slick-next) {
right: 0;
height: 100%;

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

@ -2,9 +2,9 @@
import type { ColumnType } from 'nocodb-sdk'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { useI18n } from 'vue-i18n'
import {
ActiveViewInj,
CellUrlDisableOverlayInj,
ChangePageInj,
FieldsInj,
IsFormInj,
@ -17,15 +17,18 @@ import {
ReadonlyInj,
ReloadViewDataHookInj,
createEventHook,
enumColor,
extractPkFromRow,
inject,
onClickOutside,
onMounted,
provide,
reactive,
ref,
useCopy,
useEventListener,
useGridViewColumnWidth,
useI18n,
useRoute,
useSmartsheetStoreOrThrow,
useUIPermission,
useViewData,
@ -36,9 +39,9 @@ import { NavigateDir } from '~/lib'
const { t } = useI18n()
const meta = inject(MetaInj)
const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj)
const view = inject(ActiveViewInj, ref())
// keep a root fields variable and will get modified from
// fields menu and get used in grid and gallery
@ -52,6 +55,9 @@ const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook()
const { isUIAllowed } = useUIPermission()
const hasEditPermission = isUIAllowed('xcDatatableEditable')
const route = useRoute()
const router = useRouter()
// todo: get from parent ( inject or use prop )
const isView = false
@ -92,9 +98,12 @@ const {
deleteSelectedRows,
selectedAllRecords,
removeRowIfNew,
} = useViewData(meta, view as any, xWhere)
} = useViewData(meta, view, xWhere)
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view)
const { copy } = useCopy()
const { loadGridViewColumns, updateWidth, resizingColWidth, resizingCol } = useGridViewColumnWidth(view as any)
onMounted(loadGridViewColumns)
provide(IsFormInj, ref(false))
@ -109,17 +118,38 @@ provide(ChangePageInj, changePage)
provide(ReadonlyInj, !hasEditPermission)
reloadViewDataHook?.on(async () => {
const disableUrlOverlay = ref(false)
provide(CellUrlDisableOverlayInj, disableUrlOverlay)
const showLoading = ref(true)
reloadViewDataHook?.on(async (shouldShowLoading) => {
// set value if spinner should be hidden
showLoading.value = !!shouldShowLoading
await loadData()
// reset to default (showing spinner on load)
showLoading.value = true
})
const skipRowRemovalOnCancel = ref(false)
const expandForm = (row: Row, state?: Record<string, any>, fromToolbar = false) => {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
skipRowRemovalOnCancel.value = !fromToolbar
const rowId = extractPkFromRow(row.row, meta.value.columns)
if (rowId) {
router.push({
query: {
...route.query,
rowId,
},
})
} else {
expandedFormRow.value = row
expandedFormRowState.value = state
expandedFormDlg.value = true
skipRowRemovalOnCancel.value = !fromToolbar
}
}
openNewRecordFormHook?.on(async () => {
@ -133,9 +163,9 @@ const selectCell = (row: number, col: number) => {
}
watch(
() => (view?.value as any)?.id,
async (n?: string, o?: string) => {
if (n && o && n !== o) {
() => view.value?.id,
async (next, old) => {
if (next && old && next !== old) {
await loadData()
}
},
@ -175,8 +205,6 @@ const clearCell = async (ctx: { row: number; col: number }) => {
await updateOrSaveRow(rowObj, columnObj.title)
}
const { copy } = useClipboard()
const makeEditable = (row: Row, col: ColumnType) => {
if (!hasEditPermission || editEnabled || isView) {
return
@ -201,6 +229,10 @@ const makeEditable = (row: Row, col: ColumnType) => {
/** handle keypress events */
const onKeyDown = async (e: KeyboardEvent) => {
if (e.key === 'Alt') {
disableUrlOverlay.value = true
return
}
if (selected.row === null || selected.col === null) return
/** on tab key press navigate through cells */
switch (e.key) {
@ -284,8 +316,14 @@ const onKeyDown = async (e: KeyboardEvent) => {
break
}
}
const onKeyUp = async (e: KeyboardEvent) => {
if (e.key === 'Alt') {
disableUrlOverlay.value = false
}
}
useEventListener(document, 'keydown', onKeyDown)
useEventListener(document, 'keyup', onKeyUp)
/** On clicking outside of table reset active cell */
const smartTable = ref(null)
@ -346,7 +384,7 @@ onBeforeUnmount(async () => {
/** if existing row check updated cell and invoke update method */
if (currentRow.rowMeta.changed) {
currentRow.rowMeta.changed = false
for (const field of meta?.value.columns ?? []) {
for (const field of meta.value?.columns ?? []) {
if (isVirtualCol(field)) continue
if (currentRow.row[field.title!] !== currentRow.oldRow[field.title!]) {
await updateOrSaveRow(currentRow, field.title!)
@ -355,15 +393,42 @@ onBeforeUnmount(async () => {
}
}
})
const expandedFormOnRowIdDlg = computed({
get() {
return !!route.query.rowId
},
set(val) {
if (!val)
router.push({
query: {
...route.query,
rowId: undefined,
},
})
},
})
// reload table data reload hook as fallback to rowdatareload
provide(ReloadRowDataHookInj, reloadViewDataHook)
// trigger initial data load in grid
reloadViewDataHook.trigger()
</script>
<template>
<div class="flex flex-col h-full min-h-0 w-full">
<div v-if="isLoading" class="flex items-center justify-center h-full w-full">
<a-spin size="large" />
</div>
<div v-else class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<a-dropdown v-model:visible="contextMenu" :trigger="isSqlView ? [] : ['contextmenu']">
<general-overlay :model-value="isLoading" inline transition class="!bg-opacity-15">
<div class="flex items-center justify-center h-full w-full">
<a-spin size="large" />
</div>
</general-overlay>
<div class="nc-grid-wrapper min-h-0 flex-1 scrollbar-thin-dull">
<a-dropdown
v-model:visible="contextMenu"
:trigger="isSqlView ? [] : ['contextmenu']"
overlay-class-name="nc-dropdown-grid-context-menu"
>
<table
ref="smartTable"
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
@ -407,11 +472,15 @@ onBeforeUnmount(async () => {
</th>
<th
v-if="!readOnly && !isLocked && isUIAllowed('add-column') && !isSqlView"
v-t="['c:column:add']"
v-e="['c:column:add']"
class="cursor-pointer"
@click.stop="addColumnDropdown = true"
>
<a-dropdown v-model:visible="addColumnDropdown" :trigger="['click']">
<a-dropdown
v-model:visible="addColumnDropdown"
:trigger="['click']"
overlay-class-name="nc-dropdown-grid-add-column"
>
<div class="h-full w-[60px] flex items-center justify-center">
<MdiPlus class="text-sm nc-column-add" />
</div>
@ -464,7 +533,7 @@ onBeforeUnmount(async () => {
class="cursor-pointer flex items-center border-1 active:ring rounded p-1 hover:(bg-primary bg-opacity-10)"
>
<MdiArrowExpand
v-t="['c:row-expand']"
v-e="['c:row-expand']"
class="select-none transform hover:(text-accent scale-120) nc-row-expand"
@click="expandForm(row, state)"
/>
@ -522,7 +591,7 @@ onBeforeUnmount(async () => {
<tr v-if="!isView && !isLocked && isUIAllowed('xcDatatableEditable') && !isSqlView">
<td
v-t="['c:row:add:grid-bottom']"
v-e="['c:row:add:grid-bottom']"
:colspan="visibleColLength + 1"
class="text-left pointer nc-grid-add-new-cell cursor-pointer"
@click="addEmptyRow()"
@ -542,14 +611,14 @@ onBeforeUnmount(async () => {
<template v-if="!isLocked && isUIAllowed('xcDatatableEditable')" #overlay>
<a-menu class="shadow !rounded !py-0" @click="contextMenu = false">
<a-menu-item v-if="contextMenuTarget" @click="deleteRow(contextMenuTarget.row)">
<div v-t="['a:row:delete']" class="nc-project-menu-item">
<div v-e="['a:row:delete']" class="nc-project-menu-item">
<!-- Delete Row -->
{{ $t('activity.deleteRow') }}
</div>
</a-menu-item>
<a-menu-item @click="deleteSelectedRows">
<div v-t="['a:row:delete-bulk']" class="nc-project-menu-item">
<div v-e="['a:row:delete-bulk']" class="nc-project-menu-item">
<!-- Delete Selected Rows -->
{{ $t('activity.deleteSelectedRow') }}
</div>
@ -557,11 +626,11 @@ onBeforeUnmount(async () => {
<!-- Clear cell -->
<a-menu-item v-if="contextMenuTarget" @click="clearCell(contextMenuTarget)">
<div v-t="['a:row:clear']" class="nc-project-menu-item">{{ $t('activity.clearCell') }}</div>
<div v-e="['a:row:clear']" class="nc-project-menu-item">{{ $t('activity.clearCell') }}</div>
</a-menu-item>
<a-menu-item v-if="contextMenuTarget" @click="addEmptyRow(contextMenuTarget.row + 1)">
<div v-t="['a:row:insert']" class="nc-project-menu-item">
<div v-e="['a:row:insert']" class="nc-project-menu-item">
<!-- Insert New Row -->
{{ $t('activity.insertRow') }}
</div>
@ -579,12 +648,23 @@ onBeforeUnmount(async () => {
:row="expandedFormRow"
:state="expandedFormRowState"
:meta="meta"
:view="view"
@update:model-value="
() => {
if (!skipRowRemovalOnCancel) removeRowIfNew(expandedFormRow)
}
"
/>
<SmartsheetExpandedForm
v-if="expandedFormOnRowIdDlg"
:key="route.query.rowId"
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"
:row-id="route.query.rowId"
:view="view"
/>
</div>
</template>

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { IsPublicInj, useSharedView, useSmartsheetStoreOrThrow } from '#imports'
import { IsPublicInj, useSharedView, useSidebar, useSmartsheetStoreOrThrow } from '#imports'
import ToggleDrawer from '~/components/smartsheet/sidebar/toolbar/ToggleDrawer.vue'
const { isGrid, isForm, isGallery, isSqlView } = useSmartsheetStoreOrThrow()
@ -8,7 +8,7 @@ const isPublic = inject(IsPublicInj, ref(false))
const { isUIAllowed } = useUIPermission()
const { isOpen } = useSidebar()
const { isOpen } = useSidebar('nc-right-sidebar')
const { allowCSVDownload } = useSharedView()
</script>
@ -42,6 +42,7 @@ const { allowCSVDownload } = useSharedView()
<SmartsheetToolbarAddRow v-if="isUIAllowed('dataInsert') && !isPublic && !isForm && !isSqlView" class="mx-1" />
<SmartsheetToolbarSearchData v-if="(isGrid || isGallery) && !isPublic" class="shrink mr-2 ml-2" />
<template v-if="!isOpen && !isPublic">
<div class="border-l-1 pl-3">
<ToggleDrawer class="mr-2" />

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

@ -57,7 +57,7 @@ watch(
<div class="p-0">
<div class="flex justify-center">
<!-- Comments only -->
<a-checkbox v-model:checked="commentsOnly" v-t="['c:row-expand:comment-only']" @change="loadCommentsAndLogs"
<a-checkbox v-model:checked="commentsOnly" v-e="['c:row-expand:comment-only']" @change="loadCommentsAndLogs"
>{{ $t('labels.commentsOnly') }}<span class="text-[11px] text-gray-500"></span>
</a-checkbox>
</div>

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

@ -1,4 +1,6 @@
<script lang="ts" setup>
import { message } from 'ant-design-vue'
import type { ViewType } from 'nocodb-sdk'
import {
ReloadRowDataHookInj,
useExpandedFormStoreOrThrow,
@ -7,11 +9,15 @@ import {
useUIPermission,
} from '#imports'
const props = defineProps<{ view?: ViewType }>()
const emit = defineEmits(['cancel'])
const route = useRoute()
const { meta, isSqlView } = useSmartsheetStoreOrThrow()
const { commentsDrawer, primaryValue, save: _save, loadRow } = useExpandedFormStoreOrThrow()
const { commentsDrawer, primaryValue, primaryKey, save: _save, loadRow } = useExpandedFormStoreOrThrow()
const { isNew, syncLTARRefs } = useSmartsheetRowStoreOrThrow()
@ -26,15 +32,29 @@ const save = async () => {
reloadTrigger?.trigger()
} else {
await _save()
reloadTrigger?.trigger()
}
}
// todo: accept as a prop / inject
const iconColor = '#1890ff'
const { dashboardUrl } = useDashboard()
const { copy } = useClipboard()
const copyRecordUrl = () => {
copy(
`${dashboardUrl?.value}#/${route.params.projectType}/${route.params.projectId}/${route.params.type}/${meta.value?.title}${
props.view ? `/${props.view.title}` : ''
}?rowId=${primaryKey.value}`,
)
message.success('Copied to clipboard')
}
</script>
<template>
<div class="flex p-2 items-center gap-2 p-4">
<div class="flex p-2 items-center gap-2 p-4 nc-expanded-form-header">
<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 }" />
@ -55,7 +75,14 @@ const iconColor = '#1890ff'
<template #title>
<div class="text-center w-full">{{ $t('general.reload') }}</div>
</template>
<mdi-reload class="cursor-pointer select-none text-gray-500" @click="loadRow" />
<mdi-reload v-if="!isNew" class="cursor-pointer select-none text-gray-500 mx-1" @click="loadRow" />
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>
<!-- todo: i18n -->
<div class="text-center w-full">Copy record URL</div>
</template>
<mdi-link v-if="!isNew" class="cursor-pointer select-none text-gray-500 mx-1 nc-copy-row-url" @click="copyRecordUrl" />
</a-tooltip>
<a-tooltip v-if="!isSqlView" placement="bottom">
<!-- Toggle comments draw -->
@ -64,18 +91,18 @@ const iconColor = '#1890ff'
</template>
<MdiCommentTextOutline
v-if="isUIAllowed('rowComments') && !isNew"
v-t="['c:row-expand:comment-toggle']"
class="cursor-pointer select-none nc-toggle-comments text-gray-500"
v-e="['c:row-expand:comment-toggle']"
class="cursor-pointer select-none nc-toggle-comments text-gray-500 mx-1"
@click="commentsDrawer = !commentsDrawer"
/>
</a-tooltip>
<a-button class="!text" @click="emit('cancel')">
<a-button class="!text mx-1 nc-expand-form-close-btn" @click="emit('cancel')">
<!-- Cancel -->
{{ $t('general.cancel') }}
</a-button>
<a-button :disabled="!isUIAllowed('tableRowUpdate')" type="primary" @click="save">
<a-button :disabled="!isUIAllowed('tableRowUpdate')" type="primary" class="mx-1" @click="save">
<!-- Save Row -->
{{ $t('activity.saveRow') }}
</a-button>

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

@ -1,6 +1,7 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import type { TableType, ViewType } from 'nocodb-sdk'
import { isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import { UITypes, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue'
import Cell from '../Cell.vue'
import VirtualCell from '../VirtualCell.vue'
@ -17,6 +18,7 @@ import {
toRef,
useProvideExpandedFormStore,
useProvideSmartsheetStore,
useRouter,
useVModel,
watch,
} from '#imports'
@ -29,6 +31,8 @@ interface Props {
meta: TableType
loadRow?: boolean
useMetaFields?: boolean
rowId?: string
view?: ViewType
}
const props = defineProps<Props>()
@ -41,6 +45,8 @@ const state = toRef(props, 'state')
const meta = toRef(props, 'meta')
const router = useRouter()
const fields = computedInject(FieldsInj, (_fields) => {
if (props.useMetaFields) {
return (meta.value.columns ?? []).filter((col) => !isSystemColumn(col))
@ -56,6 +62,18 @@ if (props.loadRow) {
await loadRow()
}
if (props.rowId) {
try {
await loadRow(props.rowId)
} catch (e) {
if (e.response?.status === 404) {
// todo: i18n
message.error('Record not found')
router.replace({ query: {} })
} else throw e
}
}
useProvideSmartsheetStore(ref({}) as Ref<ViewType>, meta)
provide(IsFormInj, ref(true))
@ -108,13 +126,20 @@ export default {
width="min(90vw,1000px)"
:body-style="{ 'padding': 0, 'display': 'flex', 'flex-direction': 'column' }"
:closable="false"
class="nc-drawer-expanded-form"
>
<Header @cancel="onClose" />
<Header :view="view" @cancel="onClose" />
<div class="!bg-gray-100 rounded flex-1">
<div class="flex h-full nc-form-wrapper items-stretch min-h-[max(70vh,100%)]">
<div class="flex-1 overflow-auto scrollbar-thin-dull">
<div class="flex-1 overflow-auto scrollbar-thin-dull nc-form-fields-container">
<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"
v-show="!isVirtualCol(col) || !isNew || col.uidt === UITypes.LinkToAnotherRecord"
:key="col.title"
class="mt-2 py-2"
:class="`nc-expand-col-${col.title}`"
>
<SmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<SmartsheetHeaderCell v-else :column="col" />

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

@ -5,9 +5,7 @@ import type { Menu as AntMenu } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Ref } from 'vue'
import Sortable from 'sortablejs'
import { useI18n } from 'vue-i18n'
import RenameableMenuItem from './RenameableMenuItem.vue'
import { useNuxtApp } from '#app'
import {
ActiveViewInj,
ViewListInj,
@ -17,6 +15,8 @@ import {
ref,
useApi,
useDialog,
useI18n,
useNuxtApp,
useRoute,
useRouter,
viewTypeAlias,
@ -40,7 +40,7 @@ const { $e } = useNuxtApp()
const activeView = inject(ActiveViewInj, ref())
const views = inject<Ref<any[]>>(ViewListInj, ref([]))
const views = inject<Ref<ViewType[]>>(ViewListInj, ref([]))
const { api } = useApi()
@ -76,7 +76,7 @@ function markItem(id: string) {
}
/** validate view title */
function validate(view: Record<string, any>) {
function validate(view: ViewType) {
if (!view.title || view.title.trim().length < 0) {
return 'View name is required'
}
@ -110,28 +110,32 @@ async function onSortEnd(evt: SortableEvent) {
const previousEl = children[newIndex - 1]
const nextEl = children[newIndex + 1]
const currentItem: Record<string, any> = views.value.find((v) => v.id === evt.item.id)
const previousItem: Record<string, any> = previousEl ? views.value.find((v) => v.id === previousEl.id) : {}
const nextItem: Record<string, any> = nextEl ? views.value.find((v) => v.id === nextEl.id) : {}
const currentItem = views.value.find((v) => v.id === evt.item.id)
if (!currentItem || !currentItem.id) return
const previousItem = (previousEl ? views.value.find((v) => v.id === previousEl.id) : {}) as ViewType
const nextItem = (nextEl ? views.value.find((v) => v.id === nextEl.id) : {}) as ViewType
let nextOrder: number
// set new order value based on the new order of the items
if (views.value.length - 1 === newIndex) {
nextOrder = parseFloat(previousItem.order) + 1
nextOrder = parseFloat(String(previousItem.order)) + 1
} else if (newIndex === 0) {
nextOrder = parseFloat(nextItem.order) / 2
nextOrder = parseFloat(String(nextItem.order)) / 2
} else {
nextOrder = (parseFloat(previousItem.order) + parseFloat(nextItem.order)) / 2
nextOrder = (parseFloat(String(previousItem.order)) + parseFloat(String(nextItem.order))) / 2
}
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder.toString() : oldIndex.toString()
const _nextOrder = !isNaN(Number(nextOrder)) ? nextOrder : oldIndex
currentItem.order = _nextOrder
await api.dbView.update(currentItem.id, { order: _nextOrder })
markItem(currentItem.id)
$e('a:view:reorder')
}
@ -168,7 +172,7 @@ async function onRename(view: ViewType) {
try {
await api.dbView.update(view.id!, {
title: view.title,
order: String(view.order),
order: view.order,
})
await router.replace({

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

@ -1,19 +1,19 @@
<script lang="ts" setup>
import type { ViewTypes } from 'nocodb-sdk'
import type { ViewType, ViewTypes } from 'nocodb-sdk'
import { message } from 'ant-design-vue'
import { viewIcons } from '~/utils'
import { IsLockedInj, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel } from '#imports'
import type { WritableComputedRef } from '@vue/reactivity'
import { IsLockedInj, onKeyStroke, useDebounceFn, useNuxtApp, useUIPermission, useVModel, viewIcons } from '#imports'
interface Props {
view: Record<string, any>
onValidate: (view: Record<string, any>) => boolean | string
view: ViewType
onValidate: (view: ViewType) => boolean | string
}
interface Emits {
(event: 'update:view', data: Record<string, any>): void
(event: 'changeView', view: Record<string, any>): void
(event: 'rename', view: Record<string, any>): void
(event: 'delete', view: Record<string, any>): void
(event: 'rename', view: ViewType): void
(event: 'delete', view: ViewType): void
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string }): void
}
@ -21,7 +21,7 @@ const props = defineProps<Props>()
const emits = defineEmits<Emits>()
const vModel = useVModel(props, 'view', emits)
const vModel = useVModel(props, 'view', emits) as WritableComputedRef<any>
const { $e } = useNuxtApp()
@ -92,7 +92,7 @@ function focusInput(el: HTMLInputElement) {
/** Duplicate a view */
// todo: This is not really a duplication, maybe we need to implement a true duplication?
function onDuplicate() {
emits('openModal', { type: vModel.value.type, title: vModel.value.title, copyViewId: vModel.value.id })
emits('openModal', { type: vModel.value.type!, title: vModel.value.title, copyViewId: vModel.value.id })
$e('c:view:copy', { view: vModel.value.type })
}
@ -129,7 +129,7 @@ async function onRename() {
function onCancel() {
if (!isEditing) return
vModel.value.title = originalTitle
vModel.value.title = originalTitle || ''
onStopEdit()
}
@ -151,7 +151,7 @@ function onStopEdit() {
@dblclick.stop="isUIAllowed('virtualViewsCreateOrEdit') && onDblClick()"
@click.stop="onClick"
>
<div v-t="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">
<div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2">
<div class="flex w-auto">
<MdiDrag
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 !cursor-move"
@ -161,7 +161,7 @@ function onStopEdit() {
<component
:is="viewIcons[vModel.type].icon"
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[vModel?.type]?.color }"
:style="{ color: viewIcons[vModel.type].color }"
/>
</div>

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

@ -13,6 +13,7 @@ import {
ref,
useRoute,
useRouter,
useSidebar,
useViews,
watch,
} from '#imports'
@ -34,7 +35,7 @@ const { $e } = useNuxtApp()
provide(ViewListInj, views)
/** Sidebar visible */
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const { isOpen } = useSidebar('nc-right-sidebar', { isOpen: true })
const sidebarCollapsed = computed(() => !isOpen.value)
@ -92,7 +93,6 @@ function openModal({ type, title = '', copyViewId }: { type: ViewTypes; title: s
/** Handle view creation */
function onCreate(view: ViewType) {
views.value.push(view)
activeView.value = view
router.push({ params: { viewTitle: view.title || '' } })
modalOpen = false
$e('a:view:create', { view: view.type })

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

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

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

@ -1,11 +1,11 @@
<script setup lang="ts">
import { MetaInj, inject, useTable } from '#imports'
const meta = inject(MetaInj)!
const meta = inject(MetaInj, ref())
const { deleteTable } = useTable()
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' })
const { isOpen } = useSidebar('nc-right-sidebar')
</script>
<template>

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

@ -1,11 +1,15 @@
<script setup lang="ts">
/** Sidebar visible */
const { isOpen, toggle } = useSidebar({ storageKey: 'nc-right-sidebar' })
const { isOpen, toggle } = useSidebar('nc-right-sidebar')
const onClick = () => {
toggle(!isOpen.value)
}
</script>
<template>
<div :class="{ 'nc-active-btn': isOpen }">
<a-button size="small" class="nc-toggle-right-navbar" @click="toggle(!isOpen)">
<a-button size="small" class="nc-toggle-right-navbar" @click="onClick">
<div class="flex items-center gap-1 text-xs" :class="{ 'text-gray-500': !isOpen }">
<AntDesignMenuUnfoldOutlined v-if="isOpen" />
<AntDesignMenuFoldOutlined v-else />

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

@ -20,8 +20,11 @@ const clickCount = $ref(0)
"
>
<slot name="start" />
<ToggleDrawer />
<span></span>
<template v-if="debug">
<ExportCache />

14
packages/nc-gui/components/tabs/Auth.vue

@ -1,15 +1,15 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import UserManagement from './auth/UserManagement.vue'
import ApiTokenManagement from './auth/ApiTokenManagement.vue'
import { useUIPermission } from '#imports'
import { useI18n, useUIPermission } from '#imports'
interface Tab {
title: string
label: string
body: any
isUIAllowed: boolean
isUIAllowed: () => boolean
}
const { t } = useI18n()
const { isUIAllowed } = useUIPermission()
@ -19,24 +19,22 @@ const tabsInfo: Tab[] = [
title: 'Users Management',
label: t('title.userMgmt'),
body: () => UserManagement,
isUIAllowed: isUIAllowed('userMgmtTab'),
isUIAllowed: () => isUIAllowed('userMgmtTab'),
},
{
title: 'API Token Management',
label: t('title.apiTokenMgmt'),
body: () => ApiTokenManagement,
isUIAllowed: isUIAllowed('apiTokenTab'),
isUIAllowed: () => isUIAllowed('apiTokenTab'),
},
]
// const firstKeyOfObject = (obj: object) => Object.keys(obj)[0]
const selectedTabKey = $ref(0)
const selectedTab = $computed(() => tabsInfo[selectedTabKey])
</script>
<template>
<div v-if="selectedTab.isUIAllowed">
<div v-if="selectedTab.isUIAllowed()">
<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">
<template #tab>

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

Loading…
Cancel
Save