Browse Source

Merge branch 'develop' into docs/v2

pull/6467/head
Raju Udava 1 year ago
parent
commit
e422a7ae82
  1. 20
      .github/workflows/publish-api-docs.yml
  2. 2
      SECURITY.md
  3. 4
      charts/nocodb/templates/deployment.yaml
  4. 2
      charts/nocodb/templates/pvc.yaml
  5. 4
      charts/nocodb/values.yaml
  6. 9
      packages/nc-gui/assets/nc-icons/owner.svg
  7. 12
      packages/nc-gui/components/account/Profile.vue
  8. 20
      packages/nc-gui/components/account/ResetPassword.vue
  9. 4
      packages/nc-gui/components/account/SignupSettings.vue
  10. 124
      packages/nc-gui/components/account/Token.vue
  11. 87
      packages/nc-gui/components/account/UserList.vue
  12. 26
      packages/nc-gui/components/account/UsersModal.vue
  13. 6
      packages/nc-gui/components/api-client/Headers.vue
  14. 6
      packages/nc-gui/components/api-client/Params.vue
  15. 6
      packages/nc-gui/components/cell/Checkbox.vue
  16. 8
      packages/nc-gui/components/cell/Json.vue
  17. 13
      packages/nc-gui/components/cell/attachment/index.vue
  18. 2
      packages/nc-gui/components/dashboard/Sidebar.vue
  19. 20
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  20. 11
      packages/nc-gui/components/dashboard/View.vue
  21. 250
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  22. 519
      packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue
  23. 2
      packages/nc-gui/components/dlg/ProjectDelete.vue
  24. 100
      packages/nc-gui/components/dlg/SharedBaseDuplicate.vue
  25. 2
      packages/nc-gui/components/dlg/TableDuplicate.vue
  26. 38
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  27. 13
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  28. 4
      packages/nc-gui/components/erd/TableNode.vue
  29. 4
      packages/nc-gui/components/general/BaseLogo.vue
  30. 6
      packages/nc-gui/components/general/Modal.vue
  31. 10
      packages/nc-gui/components/general/ShareProject.vue
  32. 15
      packages/nc-gui/components/nc/Pagination.vue
  33. 5
      packages/nc-gui/components/nc/Tooltip.vue
  34. 10
      packages/nc-gui/components/project/AllTables.vue
  35. 14
      packages/nc-gui/components/project/View.vue
  36. 2
      packages/nc-gui/components/smartsheet/Details.vue
  37. 183
      packages/nc-gui/components/smartsheet/Kanban.vue
  38. 13
      packages/nc-gui/components/smartsheet/Pagination.vue
  39. 14
      packages/nc-gui/components/smartsheet/column/EditOrAdd.vue
  40. 10
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  41. 118
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  42. 9
      packages/nc-gui/components/smartsheet/details/Erd.vue
  43. 11
      packages/nc-gui/components/smartsheet/details/Fields.vue
  44. 33
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  45. 19
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  46. 17
      packages/nc-gui/components/smartsheet/grid/GroupBy.vue
  47. 89
      packages/nc-gui/components/smartsheet/grid/Table.vue
  48. 75
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilter.vue
  49. 7
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  50. 2
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  51. 2
      packages/nc-gui/components/smartsheet/toolbar/GroupByMenu.vue
  52. 38
      packages/nc-gui/components/smartsheet/toolbar/SortListMenu.vue
  53. 7
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  54. 12
      packages/nc-gui/components/smartsheet/topbar/SelectMode.vue
  55. 7
      packages/nc-gui/components/template/Editor.vue
  56. 10
      packages/nc-gui/components/virtual-cell/Links.vue
  57. 5
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  58. 4
      packages/nc-gui/components/virtual-cell/components/ListItem.vue
  59. 19
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  60. 44
      packages/nc-gui/components/workspace/Billing.vue
  61. 11
      packages/nc-gui/components/workspace/View.vue
  62. 2
      packages/nc-gui/composables/useColumnCreateStore.ts
  63. 9
      packages/nc-gui/composables/useCopySharedBase.ts
  64. 2
      packages/nc-gui/composables/useData.ts
  65. 19
      packages/nc-gui/composables/useExpandedFormStore.ts
  66. 6
      packages/nc-gui/composables/useGlobal/actions.ts
  67. 1
      packages/nc-gui/composables/useGlobal/types.ts
  68. 32
      packages/nc-gui/composables/useGridViewColumn.ts
  69. 2
      packages/nc-gui/composables/useKanbanViewStore.ts
  70. 2
      packages/nc-gui/composables/useLTARStore.ts
  71. 3
      packages/nc-gui/context/index.ts
  72. 196
      packages/nc-gui/ee/assets/img/fieldPlaceholder.svg
  73. 1
      packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts
  74. 139
      packages/nc-gui/lang/en.json
  75. 1
      packages/nc-gui/lib/types.ts
  76. 16
      packages/nc-gui/middleware/auth.global.ts
  77. 13
      packages/nc-gui/pages/account/index.vue
  78. 21
      packages/nc-gui/pages/copy-shared-base.vue
  79. 72
      packages/nc-gui/pages/index.vue
  80. 10
      packages/nc-gui/plugins/a.i18n.ts
  81. 10
      packages/nc-gui/plugins/api.ts
  82. 1
      packages/nc-gui/plugins/resizeDirective.ts
  83. 10
      packages/nc-gui/plugins/state.ts
  84. BIN
      packages/nc-gui/public/favicon.ico
  85. 71
      packages/nc-gui/store/views.ts
  86. 5
      packages/nc-gui/store/workspace.ts
  87. 88
      packages/nc-gui/utils/colorsUtils.ts
  88. 18
      packages/nc-gui/utils/validation.ts
  89. BIN
      packages/noco-docs/static/img/favicon.ico
  90. 36
      packages/nocodb-sdk/src/lib/Api.ts
  91. 30
      packages/nocodb-sdk/src/lib/enums.ts
  92. 1
      packages/nocodb/package.json
  93. 4
      packages/nocodb/src/app.config.ts
  94. 3
      packages/nocodb/src/app.module.ts
  95. 1
      packages/nocodb/src/cache/CacheMgr.ts
  96. 5
      packages/nocodb/src/cache/NocoCache.ts
  97. 5
      packages/nocodb/src/cache/RedisCacheMgr.ts
  98. 5
      packages/nocodb/src/cache/RedisMockCacheMgr.ts
  99. 6
      packages/nocodb/src/controllers/api-docs/api-docs.controller.ts
  100. 3
      packages/nocodb/src/controllers/api-tokens.controller.ts
  101. Some files were not shown because too many files have changed in this diff Show More

20
.github/workflows/publish-api-docs.yml

@ -18,26 +18,38 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Pushes swagger file to src - name: Pushes swagger file to data-apis-v1
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658 uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env: env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }} API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with: with:
source_file: 'packages/nocodb/src/schema/swagger.json' source_file: 'packages/nocodb/src/schema/swagger.json'
destination_repo: 'nocodb/noco-apis-doc' destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'src' destination_folder: 'data-apis-v1'
user_email: 'oof1lab@gmail.com' user_email: 'oof1lab@gmail.com'
user_name: 'o1lab' user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb' commit_message: 'Autorelease from github.com/nocodb/nocodb'
- name: Pushes swagger file to meta-src - name: Pushes swagger file to data-apis-v2
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658 uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env: env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }} API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with: with:
source_file: 'packages/nocodb/src/schema/swagger.json' source_file: 'packages/nocodb/src/schema/swagger.json'
destination_repo: 'nocodb/noco-apis-doc' destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'meta-src' destination_folder: 'data-apis-v2'
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb'
- name: Pushes swagger file to meta-apis-v1
uses: dmnemec/copy_file_to_another_repo_action@1b29cbd9a323185f20b175dc6d5f8f31be5c0658
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'packages/nocodb/src/schema/swagger.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'meta-apis-v1'
user_email: 'oof1lab@gmail.com' user_email: 'oof1lab@gmail.com'
user_name: 'o1lab' user_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb' commit_message: 'Autorelease from github.com/nocodb/nocodb'

2
SECURITY.md

@ -3,5 +3,5 @@
### Reporting a Vulnerability ### Reporting a Vulnerability
Please report (suspected) security vulnerabilities to security@nocodb.com Please report (suspected) security vulnerabilities to security@nocodb.com
- You will receive a response from us within 3 working days. - You will receive a response from us within 7 working days.
- If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. - If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.

4
charts/nocodb/templates/deployment.yaml

@ -33,9 +33,11 @@ spec:
{{- toYaml .Values.securityContext | nindent 12 }} {{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }} imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- if .Values.storage.enabled }}
volumeMounts: volumeMounts:
- name: {{ include "nocodb.fullname" . }} - name: {{ include "nocodb.fullname" . }}
mountPath: /usr/app/data mountPath: /usr/app/data
{{- end }}
envFrom: envFrom:
- configMapRef: - configMapRef:
name: {{ include "nocodb.fullname" . }} name: {{ include "nocodb.fullname" . }}
@ -67,7 +69,9 @@ spec:
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{- if .Values.storage.enabled }}
volumes: volumes:
- name: {{ include "nocodb.fullname" . }} - name: {{ include "nocodb.fullname" . }}
persistentVolumeClaim: persistentVolumeClaim:
claimName: {{ include "nocodb.fullname" . }} claimName: {{ include "nocodb.fullname" . }}
{{- end }}

2
charts/nocodb/templates/pvc.yaml

@ -1,3 +1,4 @@
{{ if .Values.storage.enabled }}
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
@ -12,3 +13,4 @@ spec:
accessModes: accessModes:
{{- default (toYaml .Values.storage.accessModes) "- ReadWriteMany" | nindent 4 }} {{- default (toYaml .Values.storage.accessModes) "- ReadWriteMany" | nindent 4 }}
volumeMode: Filesystem volumeMode: Filesystem
{{ end }}

4
charts/nocodb/values.yaml

@ -86,6 +86,10 @@ extraSecretEnvs:
NC_DB: "mysql2://mysql:3306?u=nocodb&p=secretPass&d=nocodb" NC_DB: "mysql2://mysql:3306?u=nocodb&p=secretPass&d=nocodb"
storage: storage:
# If disabled, another persistent storage should be configured for attachments to work.
# We recommend setting NC_S3_BUCKET_NAME and other NC_S3* environment variables.
# Refer documentation for more details.
enabled: true
size: 3Gi size: 3Gi
storageClassName: "" storageClassName: ""

9
packages/nc-gui/assets/nc-icons/owner.svg

@ -1,9 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_18_1042" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"> <mask id="mask0_18_1022" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/> <rect width="16" height="16" fill="currentColor"/>
</mask> </mask>
<g mask="url(#mask0_18_1042)"> <g mask="url(#mask0_18_1022)">
<path d="M6.66665 8.00002C5.93331 8.00002 5.30554 7.73891 4.78331 7.21669C4.26109 6.69447 3.99998 6.06669 3.99998 5.33335C3.99998 4.60002 4.26109 3.97224 4.78331 3.45002C5.30554 2.9278 5.93331 2.66669 6.66665 2.66669C7.39998 2.66669 8.02776 2.9278 8.54998 3.45002C9.0722 3.97224 9.33331 4.60002 9.33331 5.33335C9.33331 6.06669 9.0722 6.69447 8.54998 7.21669C8.02776 7.73891 7.39998 8.00002 6.66665 8.00002ZM1.33331 12V11.4667C1.33331 11.1 1.42776 10.7556 1.61665 10.4334C1.80554 10.1111 2.06665 9.86669 2.39998 9.70002C2.96665 9.41113 3.60554 9.16669 4.31665 8.96669C4.71274 8.85529 4.93609 8.67502 5.79165 8.67502H6.02498C6.09165 8.67502 6.15831 8.68613 6.22498 8.70835C5.99998 9.00002 5.33331 9.66669 4.99998 10C4.99998 10 4.8376 10.2057 4.54165 10.3C3.91387 10.5 3.39998 10.7 2.99998 10.9C2.89998 10.9556 2.81942 11.0334 2.75831 11.1334C2.6972 11.2334 2.66665 11.3445 2.66665 11.4667V12H4.99998C5.33331 12.3334 5.33331 12.3334 5.66665 12.6917C5.90596 12.949 6.19998 13.1334 6.33331 13.3334H2.66665C2.29998 13.3334 1.98609 13.2028 1.72498 12.9417C1.46387 12.6806 1.33331 12.3667 1.33331 12ZM6.66665 6.66669C7.03331 6.66669 7.3472 6.53613 7.60831 6.27502C7.86942 6.01391 7.99998 5.70002 7.99998 5.33335C7.99998 4.96669 7.86942 4.6528 7.60831 4.39169C7.3472 4.13058 7.03331 4.00002 6.66665 4.00002C6.29998 4.00002 5.98609 4.13058 5.72498 4.39169C5.46387 4.6528 5.33331 4.96669 5.33331 5.33335C5.33331 5.70002 5.46387 6.01391 5.72498 6.27502C5.98609 6.53613 6.29998 6.66669 6.66665 6.66669Z" fill="currentColor" stroke="none"/> <path d="M7.99999 9.20002C7.33332 9.20002 6.76665 8.96668 6.29999 8.50002C5.83332 8.03335 5.59999 7.46668 5.59999 6.80002C5.59999 6.13335 5.83332 5.56668 6.29999 5.10002C6.76665 4.63335 7.33332 4.40002 7.99999 4.40002C8.66665 4.40002 9.23332 4.63335 9.69999 5.10002C10.1667 5.56668 10.4 6.13335 10.4 6.80002C10.4 7.46668 10.1667 8.03335 9.69999 8.50002C9.23332 8.96668 8.66665 9.20002 7.99999 9.20002ZM7.99999 8.00002C8.33332 8.00002 8.61665 7.88335 8.84999 7.65002C9.08332 7.41668 9.19999 7.13335 9.19999 6.80002C9.19999 6.46668 9.08332 6.18335 8.84999 5.95002C8.61665 5.71668 8.33332 5.60002 7.99999 5.60002C7.66665 5.60002 7.38332 5.71668 7.14999 5.95002C6.91665 6.18335 6.79999 6.46668 6.79999 6.80002C6.79999 7.13335 6.91665 7.41668 7.14999 7.65002C7.38332 7.88335 7.66665 8.00002 7.99999 8.00002ZM7.99999 2.88335L3.99999 4.41668V7.41668C3.99999 8.00557 4.08332 8.57779 4.24999 9.13335C4.41665 9.6889 4.64443 10.2056 4.93332 10.6833C5.4111 10.4611 5.90832 10.2917 6.42499 10.175C6.94165 10.0583 7.46665 10 7.99999 10C8.53332 10 9.05832 10.0583 9.57499 10.175C10.0917 10.2917 10.5889 10.4611 11.0667 10.6833C11.3555 10.2056 11.5833 9.6889 11.75 9.13335C11.9167 8.57779 12 8.00557 12 7.41668V4.41668L7.99999 2.88335ZM7.99999 11.2C7.59999 11.2 7.20554 11.2389 6.81665 11.3167C6.42777 11.3945 6.04999 11.5111 5.68332 11.6667C6.00554 12.0111 6.3611 12.3111 6.74999 12.5667C7.13888 12.8222 7.55554 13.0167 7.99999 13.15C8.44443 13.0167 8.8611 12.8222 9.24999 12.5667C9.63888 12.3111 9.99443 12.0111 10.3167 11.6667C9.94999 11.5111 9.57221 11.3945 9.18332 11.3167C8.79443 11.2389 8.39999 11.2 7.99999 11.2ZM7.99999 14.3333C7.93332 14.3333 7.86665 14.3306 7.79999 14.325C7.73332 14.3195 7.67221 14.3056 7.61665 14.2833C6.1611 13.8056 4.99443 12.9222 4.11665 11.6333C3.23888 10.3445 2.79999 8.93891 2.79999 7.41668V4.41668C2.79999 4.16113 2.86943 3.93335 3.00832 3.73335C3.14721 3.53335 3.33332 3.38891 3.56665 3.30002L7.56665 1.76668C7.7111 1.71113 7.85554 1.68335 7.99999 1.68335C8.14443 1.68335 8.28888 1.71113 8.43332 1.76668L12.4333 3.30002C12.6667 3.38891 12.8528 3.53335 12.9917 3.73335C13.1305 3.93335 13.2 4.16113 13.2 4.41668V7.41668C13.2 8.93891 12.7611 10.3445 11.8833 11.6333C11.0055 12.9222 9.83888 13.8056 8.38332 14.2833C8.32777 14.3056 8.26665 14.3195 8.19999 14.325C8.13332 14.3306 8.06665 14.3333 7.99999 14.3333Z" fill="currentColor" stroke="none"/>
<path d="M9.99998 12.6667C10.6555 12.6667 11.2639 12.5167 11.825 12.2167C12.3861 11.9167 12.8333 11.5111 13.1666 11C12.8333 10.4889 12.3861 10.0833 11.825 9.78333C11.2639 9.48333 10.6555 9.33333 9.99998 9.33333C9.34442 9.33333 8.73609 9.48333 8.17498 9.78333C7.61387 10.0833 7.16665 10.4889 6.83331 11C7.16665 11.5111 7.61387 11.9167 8.17498 12.2167C8.73609 12.5167 9.34442 12.6667 9.99998 12.6667ZM9.99998 14C8.93331 14 7.98054 13.7194 7.14165 13.1583C6.30276 12.5972 5.69998 11.8778 5.33331 11C5.69998 10.1222 6.30276 9.40278 7.14165 8.84167C7.98054 8.28056 8.93331 8 9.99998 8C11.0666 8 12.0194 8.28056 12.8583 8.84167C13.6972 9.40278 14.3 10.1222 14.6666 11C14.3 11.8778 13.6972 12.5972 12.8583 13.1583C12.0194 13.7194 11.0666 14 9.99998 14ZM9.99998 12C9.7222 12 9.48609 11.9028 9.29165 11.7083C9.0972 11.5139 8.99998 11.2778 8.99998 11C8.99998 10.7222 9.0972 10.4861 9.29165 10.2917C9.48609 10.0972 9.7222 10 9.99998 10C10.2778 10 10.5139 10.0972 10.7083 10.2917C10.9028 10.4861 11 10.7222 11 11C11 11.2778 10.9028 11.5139 10.7083 11.7083C10.5139 11.9028 10.2778 12 9.99998 12Z" fill="currentColor" stroke="none"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

12
packages/nc-gui/components/account/Profile.vue

@ -63,10 +63,10 @@ const onValidate = async (_: any, valid: boolean) => {
<template> <template>
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div class="flex flex-col w-150"> <div class="flex flex-col w-150">
<div class="flex font-bold text-xl">{{ $t('labels.profile') }}</div> <div class="flex font-bold text-xl" data-rec="true">{{ $t('labels.profile') }}</div>
<div class="mt-5 flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2"> <div class="mt-5 flex flex-col border-1 rounded-2xl border-gray-200 p-6 gap-y-2">
<div class="flex font-medium text-base">{{ $t('labels.accountDetails') }}</div> <div class="flex font-medium text-base" data-rec="true">{{ $t('labels.accountDetails') }}</div>
<div class="flex text-gray-500">{{ $t('labels.controlAppearance') }}</div> <div class="flex text-gray-500" data-rec="true">{{ $t('labels.controlAppearance') }}</div>
<div class="flex flex-row mt-4"> <div class="flex flex-row mt-4">
<div class="flex h-20 mt-1.5"> <div class="flex h-20 mt-1.5">
<GeneralUserIcon size="xlarge" :email="user?.email" /> <GeneralUserIcon size="xlarge" :email="user?.email" />
@ -81,7 +81,7 @@ const onValidate = async (_: any, valid: boolean) => {
@finish="onSubmit" @finish="onSubmit"
@validate="onValidate" @validate="onValidate"
> >
<div class="text-gray-800 mb-1.5">{{ $t('general.name') }}</div> <div class="text-gray-800 mb-1.5" data-rec="true">{{ $t('general.name') }}</div>
<a-form-item name="title" :rules="formRules.title"> <a-form-item name="title" :rules="formRules.title">
<a-input <a-input
v-model:value="form.title" v-model:value="form.title"
@ -90,7 +90,7 @@ const onValidate = async (_: any, valid: boolean) => {
data-testid="nc-account-settings-rename-input" data-testid="nc-account-settings-rename-input"
/> />
</a-form-item> </a-form-item>
<div class="text-gray-800 mb-1.5">{{ $t('labels.accountEmailID') }}</div> <div class="text-gray-800 mb-1.5" data-rec="true">{{ $t('labels.accountEmailID') }}</div>
<a-input <a-input
v-model:value="email" v-model:value="email"
class="w-full !rounded-md !py-1.5" class="w-full !rounded-md !py-1.5"
@ -98,7 +98,7 @@ const onValidate = async (_: any, valid: boolean) => {
disabled disabled
data-testid="nc-account-settings-email-input" data-testid="nc-account-settings-email-input"
/> />
<div class="flex flex-row w-full justify-end mt-8"> <div class="flex flex-row w-full justify-end mt-8" data-rec="true">
<NcButton <NcButton
type="primary" type="primary"
html-type="submit" html-type="submit"

20
packages/nc-gui/components/account/ResetPassword.vue

@ -72,14 +72,19 @@ const resetError = () => {
> >
<Transition name="layout"> <Transition name="layout">
<div v-if="error" class="mx-auto mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1"> <div v-if="error" class="mx-auto mb-4 bg-red-500 text-white rounded-lg w-3/4 p-1">
<div data-testid="nc-user-settings-form__error" class="flex items-center gap-2 justify-center"> <div data-testid="nc-user-settings-form__error" class="flex items-center gap-2 justify-center" data-rec="true">
<MaterialSymbolsWarning /> <MaterialSymbolsWarning />
{{ error }} {{ error }}
</div> </div>
</div> </div>
</Transition> </Transition>
<a-form-item :label="$t('placeholder.password.current')" name="currentPassword" :rules="formRules.currentPassword"> <a-form-item
:label="$t('placeholder.password.current')"
data-rec="true"
name="currentPassword"
:rules="formRules.currentPassword"
>
<a-input-password <a-input-password
v-model:value="form.currentPassword" v-model:value="form.currentPassword"
data-testid="nc-user-settings-form__current-password" data-testid="nc-user-settings-form__current-password"
@ -90,7 +95,7 @@ const resetError = () => {
/> />
</a-form-item> </a-form-item>
<a-form-item :label="$t('placeholder.password.new')" name="password" :rules="formRules.password"> <a-form-item :label="$t('placeholder.password.new')" data-rec="true" name="password" :rules="formRules.password">
<a-input-password <a-input-password
v-model:value="form.password" v-model:value="form.password"
data-testid="nc-user-settings-form__new-password" data-testid="nc-user-settings-form__new-password"
@ -101,7 +106,12 @@ const resetError = () => {
/> />
</a-form-item> </a-form-item>
<a-form-item :label="$t('placeholder.password.confirm')" name="passwordRepeat" :rules="formRules.passwordRepeat"> <a-form-item
:label="$t('placeholder.password.confirm')"
data-rec="true"
name="passwordRepeat"
:rules="formRules.passwordRepeat"
>
<a-input-password <a-input-password
v-model:value="form.passwordRepeat" v-model:value="form.passwordRepeat"
data-testid="nc-user-settings-form__new-password-repeat" data-testid="nc-user-settings-form__new-password-repeat"
@ -120,7 +130,7 @@ const resetError = () => {
type="primary" type="primary"
html-type="submit" html-type="submit"
> >
<div class="flex justify-center items-center gap-2"> <div class="flex justify-center items-center gap-2" data-rec="true">
<component :is="iconMap.passwordChange" /> <component :is="iconMap.passwordChange" />
{{ $t('activity.changePwd') }} {{ $t('activity.changePwd') }}
</div> </div>

4
packages/nc-gui/components/account/SignupSettings.vue

@ -42,7 +42,9 @@ loadSettings()
@change="saveSettings" @change="saveSettings"
/> />
</a-form-item> </a-form-item>
{{ $t('labels.inviteOnlySignup') }} <span data-rec="true">
{{ $t('labels.inviteOnlySignup') }}
</span>
</div> </div>
</div> </div>
</template> </template>

124
packages/nc-gui/components/account/Token.vue

@ -160,10 +160,10 @@ const handleCancel = () => {
</script> </script>
<template> <template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2"> <div class="h-full pt-2">
<div class="max-w-[810px] mx-auto p-4" data-testid="nc-token-list"> <div class="max-w-202 mx-auto px-4 h-full" data-testid="nc-token-list">
<div class="py-2 flex gap-4 items-center justify-between"> <div class="py-2 flex gap-4 items-baseline justify-between">
<h6 class="text-2xl my-4 text-left font-bold">{{ $t('title.apiTokens') }}</h6> <h6 class="text-2xl text-left font-bold" data-rec="true">{{ $t('title.apiTokens') }}</h6>
<NcTooltip :disabled="!(isEeUI && tokens.length)"> <NcTooltip :disabled="!(isEeUI && tokens.length)">
<template #title>{{ $t('labels.tokenLimit') }}</template> <template #title>{{ $t('labels.tokenLimit') }}</template>
<NcButton <NcButton
@ -175,28 +175,39 @@ const handleCancel = () => {
tooltip="bottom" tooltip="bottom"
@click="showNewTokenModal = true" @click="showNewTokenModal = true"
> >
<span class="hidden md:block"> <span class="hidden md:block" data-rec="true">
{{ $t('title.addNewToken') }} {{ $t('title.addNewToken') }}
</span> </span>
<span class="flex items-center justify-center md:hidden"> <span class="flex items-center justify-center md:hidden" data-rec="true">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
</span> </span>
</NcButton> </NcButton>
</NcTooltip> </NcTooltip>
</div> </div>
<span>{{ $t('msg.apiTokenCreate') }}</span> <span data-rec="true">{{ $t('msg.apiTokenCreate') }}</span>
<div class="w-full mt-5 rounded-md h-136 overflow-y-scroll"> <div class="mt-5 h-[calc(100%-13rem)]">
<div> <div class="h-full w-full !overflow-hidden rounded-md">
<div class="flex w-full pl-5 bg-gray-50 border-1"> <div class="flex w-full pl-5 bg-gray-50 border-1 rounded-t-md">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9">{{ $t('title.tokenName') }}</span> <span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9" data-rec="true">{{ $t('title.tokenName') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9 text-start">{{ $t('title.creator') }}</span> <span class="py-3.5 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-3/9 text-start">{{ $t('labels.token') }}</span> $t('title.creator')
<span class="py-3.5 pl-19 text-gray-500 font-medium text-3.5 w-2/9 text-start">{{ $t('labels.actions') }}</span> }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-3/9 text-start" data-rec="true">{{
$t('labels.token')
}}</span>
<span class="py-3.5 pl-19 text-gray-500 font-medium text-3.5 w-2/9 text-start" data-rec="true">{{
$t('labels.actions')
}}</span>
</div> </div>
<main> <div class="nc-scrollbar-md !overflow-y-auto flex flex-col h-[calc(100%-5rem)]">
<div v-if="showNewTokenModal"> <div v-if="showNewTokenModal">
<div class="flex gap-5 px-3 py-3.5 text-gray-500 font-medium text-3.5 w-full nc-token-generate"> <div
<div class="flex flex-col w-full"> class="flex gap-5 px-3 py-2.5 text-gray-500 font-medium text-3.5 w-full nc-token-generate border-b-1 border-l-1 border-r-1"
:class="{
'rounded-b-md': !tokens.length,
}"
>
<div class="flex w-full">
<a-input <a-input
:ref="selectInputOnMount" :ref="selectInputOnMount"
v-model:value="selectedTokenData.description" v-model:value="selectedTokenData.description"
@ -207,7 +218,9 @@ const handleCancel = () => {
data-testid="nc-token-input" data-testid="nc-token-input"
@press-enter="generateToken" @press-enter="generateToken"
/> />
<span v-if="!isValidTokenName" class="text-red-500 text-xs font-light mt-1.5 ml-1">{{ errorMessage }} </span> <span v-if="!isValidTokenName" class="text-red-500 text-xs font-light mt-1.5 ml-1" data-rec="true"
>{{ errorMessage }}
</span>
</div> </div>
<div class="flex gap-2 justify-start"> <div class="flex gap-2 justify-start">
<NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel"> <NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel">
@ -224,17 +237,19 @@ const handleCancel = () => {
</NcButton> </NcButton>
</div> </div>
</div> </div>
<NcDivider />
</div> </div>
<div v-if="!tokens.length" class="h-118 justify-center flex items-center"> <div
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('title.noLabels')" /> v-if="!tokens.length && !showNewTokenModal"
class="border-l-1 border-r-1 border-b-1 rounded-b-md justify-center flex items-center"
>
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noToken')" />
</div> </div>
<div <div
v-for="el of tokens" v-for="el of tokens"
:key="el.id" :key="el.id"
data-testid="nc-token-list" data-testid="nc-token-list"
class="flex border-1 pl-5 py-3 justify-between token" class="flex pl-5 py-3 justify-between token items-center border-l-1 border-r-1 border-b-1"
> >
<span class="text-black font-bold text-3.5 text-start w-2/9"> <span class="text-black font-bold text-3.5 text-start w-2/9">
<GeneralTruncateText placement="top" length="20"> <GeneralTruncateText placement="top" length="20">
@ -250,39 +265,42 @@ const handleCancel = () => {
<GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" length="29"> <GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" length="29">
{{ el.token }} {{ el.token }}
</GeneralTruncateText> </GeneralTruncateText>
<span v-else>**************************************</span> <span v-else>************************************</span>
</span> </span>
<!-- ACTIONS --> <!-- ACTIONS -->
<span class="text-gray-500 font-medium text-3.5 w-2/9"> <div class="flex justify-end items-center gap-3 pr-5 text-gray-500 font-medium text-3.5 w-2/9">
<div class="flex justify-end items-center gap-3 pr-5"> <NcTooltip placement="top">
<NcTooltip placement="top"> <template #title>{{ $t('labels.showOrHide') }}</template>
<template #title>{{ $t('labels.showOrHide') }}</template> <component
<component :is="iconMap.eye"
:is="iconMap.eye" class="nc-toggle-token-visibility hover::cursor-pointer w-h-4 mb-[1.8px]"
class="nc-toggle-token-visibility hover::cursor-pointer" @click="hideOrShowToken(el.token as string)"
@click="hideOrShowToken(el.token as string)" />
/> </NcTooltip>
</NcTooltip> <NcTooltip placement="top" class="h-4">
<NcTooltip placement="top" class="h-4"> <template #title>{{ $t('general.copy') }}</template>
<template #title>{{ $t('general.copy') }}</template> <component
<component :is="iconMap.copy" class="hover::cursor-pointer" @click="copyToken(el.token)" /> :is="iconMap.copy"
</NcTooltip> class="hover::cursor-pointer w-4 h-4 text-gray-600 mt-0.25"
<NcTooltip placement="top" class="mb-0.5"> @click="copyToken(el.token)"
<template #title>{{ $t('general.delete') }}</template> />
<component </NcTooltip>
:is="iconMap.delete" <NcTooltip placement="top" class="mb-0.5">
data-testid="nc-token-row-action-icon" <template #title>{{ $t('general.delete') }}</template>
class="nc-delete-icon hover::cursor-pointer" <component
@click="triggerDeleteModal(el.token as string, el.description as string)" :is="iconMap.delete"
/> data-testid="nc-token-row-action-icon"
</NcTooltip> class="nc-delete-icon hover::cursor-pointer w-4 h-4"
</div> @click="triggerDeleteModal(el.token as string, el.description as string)"
</span> />
</NcTooltip>
</div>
</div> </div>
</main> </div>
</div> </div>
</div> </div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-15">
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-5">
<a-pagination <a-pagination
v-model:current="currentPage" v-model:current="currentPage"
:total="pagination.total" :total="pagination.total"
@ -313,3 +331,9 @@ const handleCancel = () => {
</GeneralDeleteModal> </GeneralDeleteModal>
</div> </div>
</template> </template>
<style>
.token:last-child {
@apply border-b-1 rounded-b-md;
}
</style>

87
packages/nc-gui/components/account/UserList.vue

@ -153,9 +153,9 @@ const openDeleteModal = (user: UserType) => {
</script> </script>
<template> <template>
<div data-testid="nc-super-user-list"> <div data-testid="nc-super-user-list" class="h-full">
<div class="max-w-195 mx-auto"> <div class="max-w-195 mx-auto h-full">
<div class="text-2xl my-4 text-left font-weight-bold">{{ $t('title.userManagement') }}</div> <div class="text-2xl text-left font-weight-bold mb-4" data-rec="true">{{ $t('title.userManagement') }}</div>
<div class="py-2 flex gap-4 items-center justify-between"> <div class="py-2 flex gap-4 items-center justify-between">
<a-input v-model:value="searchText" class="!max-w-90 !rounded-md" placeholder="Search members" @change="loadUsers()"> <a-input v-model:value="searchText" class="!max-w-90 !rounded-md" placeholder="Search members" @change="loadUsers()">
<template #prefix> <template #prefix>
@ -165,42 +165,50 @@ const openDeleteModal = (user: UserType) => {
<div class="flex gap-3 items-center justify-center"> <div class="flex gap-3 items-center justify-center">
<component :is="iconMap.reload" class="cursor-pointer" @click="loadUsers(currentPage, currentLimit)" /> <component :is="iconMap.reload" class="cursor-pointer" @click="loadUsers(currentPage, currentLimit)" />
<NcButton data-testid="nc-super-user-invite" size="small" type="primary" @click="openInviteModal"> <NcButton data-testid="nc-super-user-invite" size="small" type="primary" @click="openInviteModal">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1" data-rec="true">
<component :is="iconMap.plus" /> <component :is="iconMap.plus" />
{{ $t('activity.inviteUser') }} {{ $t('activity.inviteUser') }}
</div> </div>
</NcButton> </NcButton>
</div> </div>
</div> </div>
<div class="w-full mt-5 border-1 rounded-md h-[613px] max-w-250"> <div class="w-full rounded-md max-w-250 h-[calc(100%-12rem)] rounded-md overflow-hidden mt-5">
<div class="flex w-full bg-gray-50 border-b-1"> <div class="flex w-full bg-gray-50 border-1 rounded-t-md">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start pl-10">{{ $t('labels.email') }}</span> <div class="py-3.5 text-gray-500 font-medium text-3.5 w-2/3 text-start pl-6" data-rec="true">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start pl-20">{{ $t('objects.role') }}</span> {{ $t('labels.email') }}
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-end pl-42">{{ $t('labels.action') }}</span> </div>
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start" data-rec="true">{{ $t('objects.role') }}</div>
<div class="flex py-3.5 text-gray-500 font-medium text-3.5 w-28 justify-end mr-4" data-rec="true">
{{ $t('labels.action') }}
</div>
</div> </div>
<div v-if="isLoading" class="flex items-center justify-center text-center h-[513px]"> <div v-if="isLoading" class="flex items-center justify-center text-center h-[513px]">
<GeneralLoader size="xlarge" /> <GeneralLoader size="xlarge" />
</div> </div>
<!-- if users are empty --> <!-- if users are empty -->
<div v-else-if="!users.length" class="flex items-center justify-center text-center h-128.25"> <div v-else-if="!users.length" class="flex items-center justify-center text-center h-full">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" /> <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" />
</div> </div>
<section v-else class="tbody"> <section v-else class="tbody h-[calc(100%-4rem)] nc-scrollbar-md border-t-0 !overflow-auto">
<div <div
v-for="el of users" v-for="el of users"
:key="el.id" :key="el.id"
data-testid="nc-token-list" data-testid="nc-token-list"
class="flex py-3 justify-around px-5 border-b-1" class="user flex py-3 justify-around px-1 border-b-1 border-l-1 border-r-1"
:class="{ :class="{
'py-4': el.roles?.includes('super'), 'py-4': el.roles?.includes('super'),
}" }"
> >
<span class="text-3.5 text-start w-1/3 pl-5"> <div class="text-3.5 text-start w-2/3 pl-5 flex items-center">
{{ el.email }} <GeneralTruncateText length="29">
</span> {{ el.email }}
<span class="text-3.5 text-start w-1/3 pl-18"> </GeneralTruncateText>
<div v-if="el?.roles?.includes('super')" class="font-weight-bold">{{ $t('labels.superAdmin') }}</div> </div>
<a-select <div class="text-3.5 text-start w-1/3">
<div v-if="el?.roles?.includes('super')" class="font-weight-bold" data-rec="true">
{{ $t('labels.superAdmin') }}
</div>
<NcSelect
v-else v-else
v-model:value="el.roles" v-model:value="el.roles"
class="w-55 nc-user-roles" class="w-55 nc-user-roles"
@ -212,8 +220,8 @@ const openDeleteModal = (user: UserType) => {
:value="OrgUserRoles.CREATOR" :value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)" :label="$t(`objects.roleType.orgLevelCreator`)"
> >
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div> <div data-rec="true">{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"> <span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }} {{ $t('msg.info.roles.orgCreator') }}
</span> </span>
</a-select-option> </a-select-option>
@ -223,14 +231,14 @@ const openDeleteModal = (user: UserType) => {
:value="OrgUserRoles.VIEWER" :value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)" :label="$t(`objects.roleType.orgLevelViewer`)"
> >
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div> <div data-rec="true">{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"> <span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }} {{ $t('msg.info.roles.orgViewer') }}
</span> </span>
</a-select-option> </a-select-option>
</a-select> </NcSelect>
</span> </div>
<span class="w-1/3 pl-43"> <span class="w-26 flex items-center justify-end mr-4">
<div <div
class="flex items-center gap-2" class="flex items-center gap-2"
:class="{ :class="{
@ -238,28 +246,31 @@ const openDeleteModal = (user: UserType) => {
}" }"
> >
<NcDropdown :trigger="['click']"> <NcDropdown :trigger="['click']">
<MdiDotsVertical <NcButton size="xsmall" type="ghost">
class="border-1 !text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)" <MdiDotsVertical
/> class="text-gray-600 h-5.5 w-5.5 rounded outline-0 p-0.5 nc-workspace-menu transform transition-transform !text-gray-400 cursor-pointer hover:(!text-gray-500 bg-gray-100)"
/>
</NcButton>
<template #overlay> <template #overlay>
<NcMenu> <NcMenu>
<template v-if="!el.roles?.includes('super')"> <template v-if="!el.roles?.includes('super')">
<!-- Resend invite Email --> <!-- Resend invite Email -->
<NcMenuItem @click="resendInvite(el)"> <NcMenuItem @click="resendInvite(el)">
<component :is="iconMap.email" class="flex text-gray-500" /> <component :is="iconMap.email" class="flex text-gray-600" />
<div>{{ $t('activity.resendInvite') }}</div> <div data-rec="true">{{ $t('activity.resendInvite') }}</div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem @click="copyInviteUrl(el)"> <NcMenuItem @click="copyInviteUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-500" /> <component :is="iconMap.copy" class="flex text-gray-600" />
<div>{{ $t('activity.copyInviteURL') }}</div> <div data-rec="true">{{ $t('activity.copyInviteURL') }}</div>
</NcMenuItem> </NcMenuItem>
<NcMenuItem @click="copyPasswordResetUrl(el)"> <NcMenuItem @click="copyPasswordResetUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-500" /> <component :is="iconMap.copy" class="flex text-gray-600" />
<div>{{ $t('activity.copyPasswordResetURL') }}</div> <div>{{ $t('activity.copyPasswordResetURL') }}</div>
</NcMenuItem> </NcMenuItem>
</template> </template>
<NcDivider v-if="!el.roles?.includes('super')" /> <NcDivider v-if="!el.roles?.includes('super')" />
<NcMenuItem class="!text-red-500 !hover:bg-red-50" @click="openDeleteModal(el)"> <NcMenuItem data-rec="true" class="!text-red-500 !hover:bg-red-50" @click="openDeleteModal(el)">
<MaterialSymbolsDeleteOutlineRounded /> <MaterialSymbolsDeleteOutlineRounded />
{{ $t('general.remove') }} {{ $t('objects.user') }} {{ $t('general.remove') }} {{ $t('objects.user') }}
</NcMenuItem> </NcMenuItem>
@ -271,7 +282,7 @@ const openDeleteModal = (user: UserType) => {
</div> </div>
</section> </section>
</div> </div>
<div v-if="pagination.total > 10" class="flex items-center justify-center mt-7"> <div v-if="pagination.total > 10" class="flex items-center justify-center mt-4">
<a-pagination <a-pagination
v-model:current="currentPage" v-model:current="currentPage"
:total="pagination.total" :total="pagination.total"
@ -285,7 +296,7 @@ const openDeleteModal = (user: UserType) => {
<div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4"> <div class="flex flex-row items-center py-2.25 px-2.5 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralIcon icon="account" class="nc-view-icon"></GeneralIcon> <GeneralIcon icon="account" class="nc-view-icon"></GeneralIcon>
<div <div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75" class="text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
> >
{{ deleteModalInfo?.email }} {{ deleteModalInfo?.email }}
@ -301,7 +312,7 @@ const openDeleteModal = (user: UserType) => {
</template> </template>
<style scoped> <style scoped>
.tbody div:nth-child(10) { .user:last-child {
border-bottom: none; @apply rounded-b-md;
} }
</style> </style>

26
packages/nc-gui/components/account/UsersModal.vue

@ -80,7 +80,7 @@ const copyUrl = async () => {
await copy(inviteUrl.value) await copy(inviteUrl.value)
// Copied shareable source url to clipboard! // Copied shareable source url to clipboard!
message.success(t('msg.success.shareableURLCopied')) message.success(t('msg.toast.inviteUrlCopy'))
} catch (e: any) { } catch (e: any) {
message.error(e.message) message.error(e.message)
} }
@ -110,7 +110,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
> >
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full"> <div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full">
<a-typography-title class="select-none" :level="4"> {{ $t('activity.inviteUser') }}</a-typography-title> <a-typography-title class="select-none" :level="4" data-rec="true"> {{ $t('activity.inviteUser') }}</a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')"> <a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')">
<template #icon> <template #icon>
@ -124,13 +124,13 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<div class="flex flex-col mt-1 pb-5"> <div class="flex flex-col mt-1 pb-5">
<div class="flex flex-row items-center pl-1.5 pb-1 h-[1.1rem]"> <div class="flex flex-row items-center pl-1.5 pb-1 h-[1.1rem]">
<component :is="iconMap.account" /> <component :is="iconMap.account" />
<div class="text-xs ml-0.5 mt-0.5">{{ $t('activity.copyInviteToken') }}</div> <div class="text-xs ml-0.5 mt-0.5" data-rec="true">{{ $t('activity.copyInviteURL') }}</div>
</div> </div>
<a-alert class="!mt-2" type="success" show-icon> <a-alert class="!mt-2" type="success" show-icon>
<template #message> <template #message>
<div class="flex flex-row justify-between items-center py-1"> <div class="flex flex-row justify-between items-center py-1">
<div class="flex pl-2 text-green-700 text-xs"> <div class="flex pl-2 text-green-700 text-xs" data-rec="true">
{{ inviteUrl }} {{ inviteUrl }}
</div> </div>
@ -143,7 +143,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</template> </template>
</a-alert> </a-alert>
<div class="flex text-xs text-gray-500 mt-2 justify-start ml-2"> <div class="flex text-xs text-gray-500 mt-2 justify-start ml-2" data-rec="true">
{{ $t('msg.info.userInviteNoSMTP') }} {{ $t('msg.info.userInviteNoSMTP') }}
{{ usersData.invitationToken && usersData.emails }} {{ usersData.invitationToken && usersData.emails }}
</div> </div>
@ -153,7 +153,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<div class="flex flex-row justify-center items-center space-x-0.5"> <div class="flex flex-row justify-center items-center space-x-0.5">
<MaterialSymbolsSendOutline class="flex mx-auto text-gray-600 h-[0.8rem]" /> <MaterialSymbolsSendOutline class="flex mx-auto text-gray-600 h-[0.8rem]" />
<div class="text-xs text-gray-600">{{ $t('activity.inviteMore') }}</div> <div class="text-xs text-gray-600" data-rec="true">{{ $t('activity.inviteMore') }}</div>
</div> </div>
</a-button> </a-button>
</div> </div>
@ -177,7 +177,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
name="emails" name="emails"
:rules="[{ required: true, message: 'Please input email' }]" :rules="[{ required: true, message: 'Please input email' }]"
> >
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('datatype.Email') }}:</div> <div class="ml-1 mb-1 text-xs text-gray-500" data-rec="true">{{ $t('datatype.Email') }}:</div>
<a-input <a-input
:ref="emailInput" :ref="emailInput"
@ -191,7 +191,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<div class="flex flex-col w-2/4"> <div class="flex flex-col w-2/4">
<a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]"> <a-form-item name="role" :rules="[{ required: true, message: 'Role required' }]">
<div class="ml-1 mb-1 text-xs text-gray-500">{{ $t('labels.selectUserRole') }}</div> <div class="ml-1 mb-1 text-xs text-gray-500" data-rec="true">{{ $t('labels.selectUserRole') }}</div>
<a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role"> <a-select v-model:value="usersData.role" class="nc-user-roles" dropdown-class-name="nc-dropdown-user-role">
<a-select-option <a-select-option
@ -199,8 +199,8 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
:value="OrgUserRoles.CREATOR" :value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)" :label="$t(`objects.roleType.orgLevelCreator`)"
> >
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div> <div data-rec="true">{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"> <span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }} {{ $t('msg.info.roles.orgCreator') }}
</span> </span>
</a-select-option> </a-select-option>
@ -210,8 +210,8 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
:value="OrgUserRoles.VIEWER" :value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)" :label="$t(`objects.roleType.orgLevelViewer`)"
> >
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div> <div data-rec="true">{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal"> <span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }} {{ $t('msg.info.roles.orgViewer') }}
</span> </span>
</a-select-option> </a-select-option>
@ -224,7 +224,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<a-button type="primary" class="!rounded-md" html-type="submit"> <a-button type="primary" class="!rounded-md" html-type="submit">
<div class="flex flex-row justify-center items-center space-x-1.5"> <div class="flex flex-row justify-center items-center space-x-1.5">
<MaterialSymbolsSendOutline class="flex h-[0.8rem]" /> <MaterialSymbolsSendOutline class="flex h-[0.8rem]" />
<div>{{ $t('activity.invite') }}</div> <div data-rec="true">{{ $t('activity.invite') }}</div>
</div> </div>
</a-button> </a-button>
</div> </div>

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

@ -66,10 +66,10 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
<tr> <tr>
<th></th> <th></th>
<th> <th>
<div class="text-left font-normal ml-2">{{ $t('labels.headerName') }}</div> <div class="text-left font-normal ml-2" data-rec="true">{{ $t('labels.headerName') }}</div>
</th> </th>
<th> <th>
<div class="text-left font-normal ml-2">{{ $t('placeholder.value') }}</div> <div class="text-left font-normal ml-2" data-rec="true">{{ $t('placeholder.value') }}</div>
</th> </th>
<th class="w-8"></th> <th class="w-8"></th>
</tr> </tr>
@ -124,7 +124,7 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
<td :colspan="12" class=""> <td :colspan="12" class="">
<NcButton size="small" type="secondary" @click="addHeaderRow"> <NcButton size="small" type="secondary" @click="addHeaderRow">
<div class="flex flex-row items-center gap-x-1"> <div class="flex flex-row items-center gap-x-1">
<div>{{ $t('labels.addHeader') }}</div> <div data-rec="true">{{ $t('labels.addHeader') }}</div>
<component :is="iconMap.plus" class="flex mx-auto" /> <component :is="iconMap.plus" class="flex mx-auto" />
</div> </div>
</NcButton> </NcButton>

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

@ -24,11 +24,11 @@ const deleteParamRow = (i: number) => {
<thead class="h-8"> <thead class="h-8">
<tr> <tr>
<th> <th>
<div class="text-left font-normal ml-2">{{ $t('title.parameterName') }}</div> <div class="text-left font-normal ml-2" data-rec="true">{{ $t('title.parameterName') }}</div>
</th> </th>
<th> <th>
<div class="text-left font-normal ml-2">{{ $t('placeholder.value') }}</div> <div class="text-left font-normal ml-2" data-rec="true">{{ $t('placeholder.value') }}</div>
</th> </th>
<th class="w-8"> <th class="w-8">
@ -69,7 +69,7 @@ const deleteParamRow = (i: number) => {
<td :colspan="12" class=""> <td :colspan="12" class="">
<NcButton size="small" type="secondary" @click="addParamRow"> <NcButton size="small" type="secondary" @click="addParamRow">
<div class="flex flex-row items-center gap-x-1"> <div class="flex flex-row items-center gap-x-1">
<div>{{ $t('activity.addParameter') }}</div> <div data-rec="true">{{ $t('activity.addParameter') }}</div>
<component :is="iconMap.plus" class="flex mx-auto" /> <component :is="iconMap.plus" class="flex mx-auto" />
</div> </div>
</NcButton> </NcButton>

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

@ -91,7 +91,11 @@ useSelectedCellKeyupListener(active, (e) => {
}" }"
@click="onClick(false, $event)" @click="onClick(false, $event)"
> >
<div class="items-center" :class="{ 'w-full justify-start': isEditColumnMenu || isGallery || isForm }" @click="onClick(true)"> <div
class="items-center"
:class="{ 'w-full justify-start': isEditColumnMenu || isGallery || isForm, 'py-2': isEditColumnMenu }"
@click="onClick(true)"
>
<Transition name="layout" mode="out-in" :duration="100"> <Transition name="layout" mode="out-in" :duration="100">
<component <component
:is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)" :is="getMdiIcon(vModel ? checkboxMeta.icon.checked : checkboxMeta.icon.unchecked)"

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

@ -42,7 +42,9 @@ const localValueState = ref<string | undefined>()
const error = ref<string | undefined>() const error = ref<string | undefined>()
const isExpanded = inject(JsonExpandInj, ref(false)) const _isExpanded = inject(JsonExpandInj, ref(false))
const isExpanded = ref(false)
const localValue = computed<string | Record<string, any> | undefined>({ const localValue = computed<string | Record<string, any> | undefined>({
get: () => localValueState.value, get: () => localValueState.value,
@ -137,6 +139,10 @@ useSelectedCellKeyupListener(active, (e) => {
break break
} }
}) })
watch(isExpanded, () => {
_isExpanded.value = isExpanded.value
})
</script> </script>
<template> <template>

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

@ -8,6 +8,7 @@ import {
DropZoneRef, DropZoneRef,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsGalleryInj, IsGalleryInj,
IsKanbanInj,
RowHeightInj, RowHeightInj,
iconMap, iconMap,
inject, inject,
@ -46,6 +47,8 @@ const isLockedMode = inject(IsLockedInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false)) const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false)) const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()! const { isSharedForm } = useSmartsheetStoreOrThrow()!
@ -185,6 +188,7 @@ const onExpand = () => {
v-model="isOverDropZone" v-model="isOverDropZone"
inline inline
:target="currentCellRef" :target="currentCellRef"
data-rec="true"
class="nc-attachment-cell-dropzone text-white text-lg ring ring-accent ring-opacity-100 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl" class="nc-attachment-cell-dropzone text-white text-lg ring ring-accent ring-opacity-100 bg-gray-700/75 flex items-center justify-center gap-2 backdrop-blur-xl"
> >
<MaterialSymbolsFileCopyOutline class="text-accent" /> <MaterialSymbolsFileCopyOutline class="text-accent" />
@ -202,7 +206,9 @@ const onExpand = () => {
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" /> <component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<NcTooltip placement="bottom"> <NcTooltip placement="bottom">
<template #title>{{ $t('activity.attachmentDrop') }} </template> <template #title
><span data-rec="true">{{ $t('activity.attachmentDrop') }} </span></template
>
<div v-if="active || !visibleItems.length || (isForm && visibleItems.length)" class="flex items-center gap-1"> <div v-if="active || !visibleItems.length || (isForm && visibleItems.length)" class="flex items-center gap-1">
<MaterialSymbolsAttachFile <MaterialSymbolsAttachFile
@ -210,6 +216,7 @@ const onExpand = () => {
/> />
<div <div
v-if="!visibleItems.length" v-if="!visibleItems.length"
data-rec="true"
class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs" class="group-hover:text-primary text-gray-500 dark:text-gray-200 dark:group-hover:!text-white text-xs"
> >
{{ $t('activity.addFiles') }} {{ $t('activity.addFiles') }}
@ -223,7 +230,7 @@ const onExpand = () => {
<template v-if="visibleItems.length"> <template v-if="visibleItems.length">
<div <div
ref="sortableRef" ref="sortableRef"
:class="{ 'justify-center': !isExpandedForm && !isGallery }" :class="{ 'justify-center': !isExpandedForm && !isGallery && !isKanban }"
class="flex cursor-pointer w-full items-center flex-wrap gap-2 py-1.5 scrollbar-thin-dull overflow-hidden mt-0 items-start" class="flex cursor-pointer w-full items-center flex-wrap gap-2 py-1.5 scrollbar-thin-dull overflow-hidden mt-0 items-start"
:style="{ :style="{
maxHeight: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`, maxHeight: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
@ -240,7 +247,7 @@ const onExpand = () => {
:class="{ 'ml-2': active }" :class="{ 'ml-2': active }"
@click=" @click="
() => { () => {
if (isGallery || isMobileMode) return if (isGallery || isMobileMode || (isKanban && !isExpandedForm)) return
selectedImage = item selectedImage = item
} }
" "

2
packages/nc-gui/components/dashboard/Sidebar.vue

@ -54,7 +54,7 @@ onUnmounted(() => {
'pt-0.25': isSharedBase, 'pt-0.25': isSharedBase,
}" }"
> >
<LazyDashboardTreeView v-if="!isWorkspaceLoading" /> <DashboardTreeView v-if="!isWorkspaceLoading" />
</div> </div>
<div v-if="!isSharedBase"> <div v-if="!isSharedBase">
<DashboardSidebarUserInfo /> <DashboardSidebarUserInfo />

20
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -307,12 +307,6 @@ function openErdView(source: SourceType) {
} }
} }
const reloadTables = async () => {
$e('a:table:refresh:navdraw')
// await loadTables()
}
const contextMenuBase = computed(() => { const contextMenuBase = computed(() => {
if (contextMenuTarget.type === 'source') { if (contextMenuTarget.type === 'source') {
return contextMenuTarget.value return contextMenuTarget.value
@ -483,7 +477,7 @@ const projectDelete = () => {
</span> </span>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div> <div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(base)"></div>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']"> <NcDropdown v-if="!isSharedBase" v-model:visible="isOptionsOpen" :trigger="['click']">
<NcButton <NcButton
v-e="['c:base:options']" v-e="['c:base:options']"
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
@ -663,7 +657,7 @@ const projectDelete = () => {
class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title" class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title"
@contextmenu="setMenuContext('source', source)" @contextmenu="setMenuContext('source', source)"
> >
<GeneralBaseLogo :source-type="source.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" /> <GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
{{ $t('general.default') }} {{ $t('general.default') }}
</div> </div>
<div <div
@ -671,7 +665,7 @@ const projectDelete = () => {
class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full" class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full"
@contextmenu="setMenuContext('source', source)" @contextmenu="setMenuContext('source', source)"
> >
<GeneralBaseLogo :source-type="source.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" /> <GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
<div <div
:data-testid="`nc-sidebar-base-${source.alias}`" :data-testid="`nc-sidebar-base-${source.alias}`"
class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none" class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none"
@ -787,14 +781,6 @@ const projectDelete = () => {
</div> </div>
</NcMenuItem> </NcMenuItem>
</template> </template>
<template v-else>
<NcMenuItem @click="reloadTables">
<div class="nc-base-option-item">
{{ $t('general.reload') }}
</div>
</NcMenuItem>
</template>
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>

11
packages/nc-gui/components/dashboard/View.vue

@ -111,6 +111,14 @@ watch(isMobileMode, () => {
isLeftSidebarOpen.value = !isMobileMode.value isLeftSidebarOpen.value = !isMobileMode.value
}) })
watch(sidebarState, () => {
if (sidebarState.value === 'peekCloseEnd') {
setTimeout(() => {
sidebarState.value = 'hiddenEnd'
}, animationDuration)
}
})
onMounted(() => { onMounted(() => {
handleSidebarOpenOnMobileForNonViews() handleSidebarOpenOnMobileForNonViews()
}) })
@ -132,7 +140,7 @@ onMounted(() => {
> >
<div <div
ref="wrapperRef" ref="wrapperRef"
class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-32 absolute overflow-visible" class="nc-sidebar-wrapper relative flex flex-col h-full justify-center !min-w-12 absolute overflow-visible"
:class="{ :class="{
'mobile': isMobileMode, 'mobile': isMobileMode,
'minimized-height': !isLeftSidebarOpen, 'minimized-height': !isLeftSidebarOpen,
@ -171,6 +179,7 @@ onMounted(() => {
> * { > * {
@apply opacity-0; @apply opacity-0;
z-index: -1 !important;
transform: translateX(-100%); transform: translateX(-100%);
} }
} }

250
packages/nc-gui/components/dashboard/settings/DataSources.vue

@ -315,7 +315,7 @@ const isEditBaseModalOpen = computed({
<div class="ds-table-col ds-table-enabled cursor-pointer" @dblclick="forceAwaken">{{ $t('general.visibility') }}</div> <div class="ds-table-col ds-table-enabled cursor-pointer" @dblclick="forceAwaken">{{ $t('general.visibility') }}</div>
<div class="ds-table-col ds-table-name">{{ $t('general.name') }}</div> <div class="ds-table-col ds-table-name">{{ $t('general.name') }}</div>
<div class="ds-table-col ds-table-type">{{ $t('general.type') }}</div> <div class="ds-table-col ds-table-type">{{ $t('general.type') }}</div>
<div class="ds-table-col ds-table-actions pl-2">{{ $t('labels.actions') }}</div> <div class="ds-table-col ds-table-actions -ml-13">{{ $t('labels.actions') }}</div>
<div class="ds-table-col ds-table-crud"></div> <div class="ds-table-col ds-table-crud"></div>
</div> </div>
</div> </div>
@ -351,58 +351,82 @@ const isEditBaseModalOpen = computed({
<div class="ds-table-col ds-table-actions"> <div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a-button <NcTooltip v-if="!sources[0].is_meta && !sources[0].is_local">
v-if="!sources[0].is_meta && !sources[0].is_local" <template #title>
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" />
{{ $t('tooltip.metaSync') }} {{ $t('tooltip.metaSync') }}
</div> </template>
</a-button> <NcButton
<a-button class="nc-action-btn cursor-pointer outline-0"
class="nc-action-btn cursor-pointer outline-0" type="text"
type="text" data-testid="nc-data-sources-view-meta-sync"
@click="baseAction(sources[0].id, DataSourcesSubTab.ERD)" size="small"
> @click="baseAction(sources[0].id, DataSourcesSubTab.Metadata)"
<div class="flex items-center gap-2 text-gray-600"> >
<GeneralIcon icon="erd" class="group-hover:text-accent" /> <div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('title.relations') }} {{ $t('title.relations') }}
</div> </template>
</a-button> <NcButton
<a-button size="small"
class="nc-action-btn cursor-pointer outline-0" class="nc-action-btn cursor-pointer outline-0"
type="text" type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)" data-testid="nc-data-sources-view-erd"
> @click="baseAction(sources[0].id, DataSourcesSubTab.ERD)"
<div class="flex items-center gap-2 text-gray-600"> >
<GeneralIcon icon="acl" class="group-hover:text-accent" /> <div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('labels.uiAcl') }} {{ $t('labels.uiAcl') }}
</div> </template>
</a-button> <NcButton
<a-button size="small"
class="nc-action-btn cursor-pointer outline-0" class="nc-action-btn cursor-pointer outline-0"
type="text" type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Audit)" data-testid="nc-data-sources-view-ui-acl"
> @click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)"
<div class="flex items-center gap-2 text-gray-600"> >
<GeneralIcon icon="book" class="group-hover:text-accent" /> <div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('title.audit') }} {{ $t('title.audit') }}
</div> </template>
</a-button> <NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-audit"
@click="baseAction(sources[0].id, DataSourcesSubTab.Audit)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="book" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
</div> </div>
</div> </div>
<div class="ds-table-col ds-table-crud"> <div class="ds-table-col ds-table-crud">
<a-button <NcButton
v-if="!sources[0].is_meta && !sources[0].is_local" v-if="!sources[0].is_meta && !sources[0].is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg" class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text" type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Edit)" @click="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
> >
<GeneralIcon icon="edit" class="text-gray-600" /> <GeneralIcon icon="edit" class="text-gray-600" />
</a-button> </NcButton>
</div> </div>
</div> </div>
</template> </template>
@ -419,12 +443,12 @@ const isEditBaseModalOpen = computed({
</a-tooltip> </a-tooltip>
</div> </div>
</div> </div>
<div class="ds-table-col ds-table-name font-medium"> <div class="ds-table-col ds-table-name font-medium w-full">
<GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" /> <GeneralIcon v-if="sources.length > 2" icon="dragVertical" small class="ds-table-handle" />
<div v-if="source.is_meta || source.is_local">-</div> <div v-if="source.is_meta || source.is_local">-</div>
<div v-else class="flex items-center gap-1"> <span v-else class="truncate">
{{ source.is_meta || source.is_local ? $t('general.base') : source.alias }} {{ source.is_meta || source.is_local ? $t('general.base') : source.alias }}
</div> </span>
</div> </div>
<div class="ds-table-col ds-table-type"> <div class="ds-table-col ds-table-type">
@ -437,71 +461,97 @@ const isEditBaseModalOpen = computed({
<div class="ds-table-col ds-table-actions"> <div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a-button <NcTooltip>
class="nc-action-btn cursor-pointer outline-0" <template #title>
type="text"
@click="baseAction(source.id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
{{ $t('title.relations') }} {{ $t('title.relations') }}
</div> </template>
</a-button> <NcButton
<a-button size="small"
type="text" class="nc-action-btn cursor-pointer outline-0"
class="nc-action-btn cursor-pointer outline-0" type="text"
@click="baseAction(source.id, DataSourcesSubTab.UIAcl)" data-testid="nc-data-sources-view-erd"
> @click="baseAction(source.id, DataSourcesSubTab.ERD)"
<div class="flex items-center gap-2 text-gray-600"> >
<GeneralIcon icon="acl" class="group-hover:text-accent" /> <div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('labels.uiAcl') }} {{ $t('labels.uiAcl') }}
</div> </template>
</a-button> <NcButton
<a-button size="small"
v-if="!source.is_meta && !source.is_local" type="text"
type="text" class="nc-action-btn cursor-pointer outline-0"
class="nc-action-btn cursor-pointer outline-0" data-testid="nc-data-sources-view-ui-acl"
@click="baseAction(source.id, DataSourcesSubTab.Metadata)" @click="baseAction(source.id, DataSourcesSubTab.UIAcl)"
> >
<div class="flex items-center gap-2 text-gray-600"> <div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" /> <GeneralIcon icon="acl" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('tooltip.metaSync') }} {{ $t('tooltip.metaSync') }}
</div> </template>
</a-button> <NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
type="text"
data-testid="nc-data-sources-view-meta-sync"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(source.id, DataSourcesSubTab.Metadata)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="sync" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
</div> </div>
</div> </div>
<div class="ds-table-col ds-table-crud justify-end gap-x-1"> <div class="ds-table-col ds-table-crud justify-end gap-x-1">
<a-button <NcTooltip>
v-if="!source.is_meta && !source.is_local" <template #title>
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5" {{ $t('general.edit') }}
type="text" </template>
@click="baseAction(source.id, DataSourcesSubTab.Edit)" <NcButton
> v-if="!source.is_meta && !source.is_local"
<GeneralIcon icon="edit" class="text-gray-600 -mt-0.5" /> size="small"
</a-button> class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
<a-button type="text"
v-if="!source.is_meta && !source.is_local" @click="baseAction(source.id, DataSourcesSubTab.Edit)"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5" >
type="text" <GeneralIcon icon="edit" class="text-gray-600" />
@click="openDeleteBase(source)" </NcButton>
> </NcTooltip>
<GeneralIcon icon="delete" class="text-red-500 -mt-0.5" /> <NcTooltip>
</a-button> <template #title>
{{ $t('general.delete') }}
</template>
<NcButton
v-if="!source.is_meta && !source.is_local"
size="small"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg"
type="text"
@click="openDeleteBase(source)"
>
<GeneralIcon icon="delete" class="text-red-500" />
</NcButton>
</NcTooltip>
</div> </div>
</div> </div>
</template> </template>
</Draggable> </Draggable>
</div> </div>
</div> </div>
<GeneralModal v-model:visible="isNewBaseModalOpen" closable :mask-closable="false" size="medium"> <LazyDashboardSettingsDataSourcesCreateBase
<div class="py-6 px-8"> v-model:open="isNewBaseModalOpen"
<LazyDashboardSettingsDataSourcesCreateBase :connection-type="clientType"
:connection-type="clientType" @source-created="loadBases(true)"
@source-created="loadBases(true)" />
@close="isNewBaseModalOpen = false"
/>
</div>
</GeneralModal>
<GeneralModal v-model:visible="isErdModalOpen" size="large"> <GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]"> <div class="h-[80vh]">
<LazyDashboardSettingsErd :source-id="activeBaseId" /> <LazyDashboardSettingsErd :source-id="activeBaseId" />
@ -550,7 +600,7 @@ const isEditBaseModalOpen = computed({
<style> <style>
.ds-table-head { .ds-table-head {
@apply flex items-center border-0 text-gray-400; @apply flex items-center border-0 text-gray-500;
} }
.ds-table-body { .ds-table-body {
@ -570,15 +620,15 @@ const isEditBaseModalOpen = computed({
} }
.ds-table-name { .ds-table-name {
@apply col-span-6 items-center capitalize; @apply col-span-9 items-center capitalize;
} }
.ds-table-type { .ds-table-type {
@apply col-span-3 items-center; @apply col-span-2 items-center;
} }
.ds-table-actions { .ds-table-actions {
@apply col-span-7; @apply col-span-5 flex w-full justify-end;
} }
.ds-table-crud { .ds-table-crud {

519
packages/nc-gui/components/dashboard/settings/data-sources/CreateBase.vue

@ -27,9 +27,11 @@ import {
watch, watch,
} from '#imports' } from '#imports'
const props = defineProps<{ connectionType?: ClientType }>() const props = defineProps<{ open: boolean; connectionType?: ClientType }>()
const emit = defineEmits(['sourceCreated', 'close']) const emit = defineEmits(['update:open', 'sourceCreated'])
const vOpen = useVModel(props, 'open', emit)
const connectionType = computed(() => props.connectionType ?? ClientType.MYSQL) const connectionType = computed(() => props.connectionType ?? ClientType.MYSQL)
@ -284,7 +286,7 @@ const createSource = async () => {
} }
emit('sourceCreated') emit('sourceCreated')
emit('close') vOpen.value = false
creatingSource.value = false creatingSource.value = false
} else if (status === JobStatus.FAILED) { } else if (status === JobStatus.FAILED) {
message.error('Failed to create base') message.error('Failed to create base')
@ -295,6 +297,7 @@ const createSource = async () => {
) )
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
creatingSource.value = false
} finally { } finally {
refreshCommandPalette() refreshCommandPalette()
} }
@ -404,263 +407,295 @@ watch(
}, },
{ immediate: true }, { immediate: true },
) )
const toggleModal = (val: boolean) => {
vOpen.value = val
}
</script> </script>
<template> <template>
<div class="create-source bg-white relative flex flex-col justify-center gap-2 w-full"> <GeneralModal
<h1 class="prose-xl font-bold self-start mb-4 flex items-center gap-2"> :visible="vOpen"
{{ $t('title.newBase') }} :closable="!creatingSource"
<DashboardSettingsDataSourcesInfo /> :keyboard="!creatingSource"
<span class="flex-grow"></span> :mask-closable="false"
</h1> size="medium"
@update:visible="toggleModal"
<a-form ref="form" :model="formState" name="external-base-create-form" layout="horizontal" no-style :label-col="{ span: 8 }"> >
<div <div class="py-6 px-8">
class="nc-scrollbar-md" <div class="create-source bg-white relative flex flex-col justify-center gap-2 w-full">
:style="{ <h1 class="prose-xl font-bold self-start mb-4 flex items-center gap-2">
maxHeight: '60vh', {{ $t('title.newBase') }}
}" <DashboardSettingsDataSourcesInfo />
> <span class="flex-grow"></span>
<a-form-item label="Source Name" v-bind="validateInfos.title"> </h1>
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item> <a-form
ref="form"
<a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']"> :model="formState"
<a-select name="external-base-create-form"
v-model:value="formState.dataSource.client" layout="horizontal"
class="nc-extdb-db-type" no-style
dropdown-class-name="nc-dropdown-ext-db-type" :label-col="{ span: 8 }"
@change="onClientChange"
>
<a-select-option v-for="client in clientTypes" :key="client.value" :value="client.value"
>{{ client.text }}
</a-select-option>
</a-select>
</a-form-item>
<!-- SQLite File -->
<a-form-item
v-if="formState.dataSource.client === ClientType.SQLITE"
:label="$t('labels.sqliteFile')"
v-bind="validateInfos['dataSource.connection.connection.filename']"
> >
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" /> <div
</a-form-item> class="nc-scrollbar-md"
:style="{
<template v-else> maxHeight: '60vh',
<!-- Host Address --> }"
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<a-input v-model:value="(formState.dataSource.connection as DefaultConnection).host" class="nc-extdb-host-address" />
</a-form-item>
<!-- Port Number -->
<a-form-item :label="$t('labels.port')" v-bind="validateInfos['dataSource.connection.port']">
<a-input-number
v-model:value="(formState.dataSource.connection as DefaultConnection).port"
class="!w-full nc-extdb-host-port"
/>
</a-form-item>
<!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<a-input v-model:value="(formState.dataSource.connection as DefaultConnection).user" class="nc-extdb-host-user" />
</a-form-item>
<!-- Password -->
<a-form-item :label="$t('labels.password')">
<a-input-password
v-model:value="(formState.dataSource.connection as DefaultConnection).password"
class="nc-extdb-host-password"
/>
</a-form-item>
<!-- Database -->
<a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
<!-- Database : create if not exists -->
<a-input
v-model:value="formState.dataSource.connection.database"
:placeholder="$t('labels.dbCreateIfNotExists')"
class="nc-extdb-host-database"
/>
</a-form-item>
<!-- Schema name -->
<a-form-item
v-if="[ClientType.MSSQL, ClientType.PG].includes(formState.dataSource.client) && formState.dataSource.searchPath"
:label="$t('labels.schemaName')"
v-bind="validateInfos['dataSource.searchPath.0']"
> >
<a-input v-model:value="formState.dataSource.searchPath[0]" /> <a-form-item label="Source Name" v-bind="validateInfos.title">
</a-form-item> <a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
<div class="flex items-right justify-end gap-2"> </a-form-item>
<!-- Use Connection URL -->
<NcButton type="ghost" size="small" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true"> <a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']">
{{ $t('activity.useConnectionUrl') }} <a-select
</NcButton> v-model:value="formState.dataSource.client"
</div> class="nc-extdb-db-type"
dropdown-class-name="nc-dropdown-ext-db-type"
<a-collapse ghost expand-icon-position="right" class="!mt-6"> @change="onClientChange"
<a-collapse-panel key="1"> >
<template #header> <a-select-option v-for="client in clientTypes" :key="client.value" :value="client.value"
<span>{{ $t('title.advancedParameters') }}</span> >{{ client.text }}
</template> </a-select-option>
<a-form-item label="SSL mode"> </a-select>
<a-select v-model:value="formState.sslUse" dropdown-class-name="nc-dropdown-ssl-mode" @select="onSSLModeChange"> </a-form-item>
<a-select-option v-for="opt in Object.values(SSLUsage)" :key="opt" :value="opt">{{ opt }} </a-select-option>
</a-select> <!-- SQLite File -->
<a-form-item
v-if="formState.dataSource.client === ClientType.SQLITE"
:label="$t('labels.sqliteFile')"
v-bind="validateInfos['dataSource.connection.connection.filename']"
>
<a-input v-model:value="(formState.dataSource.connection as SQLiteConnection).connection.filename" />
</a-form-item>
<template v-else>
<!-- Host Address -->
<a-form-item :label="$t('labels.hostAddress')" v-bind="validateInfos['dataSource.connection.host']">
<a-input
v-model:value="(formState.dataSource.connection as DefaultConnection).host"
class="nc-extdb-host-address"
/>
</a-form-item> </a-form-item>
<a-form-item label="SSL keys"> <!-- Port Number -->
<div class="flex gap-2"> <a-form-item :label="$t('labels.port')" v-bind="validateInfos['dataSource.connection.port']">
<a-tooltip placement="top"> <a-input-number
<!-- Select .cert file --> v-model:value="(formState.dataSource.connection as DefaultConnection).port"
<template #title> class="!w-full nc-extdb-host-port"
<span>{{ $t('tooltip.clientCert') }}</span> />
</template>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="certFileInput?.click()">
{{ $t('labels.clientCert') }}
</NcButton>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select .key file -->
<template #title>
<span>{{ $t('tooltip.clientKey') }}</span>
</template>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="keyFileInput?.click()">
{{ $t('labels.clientKey') }}
</NcButton>
</a-tooltip>
<a-tooltip placement="top">
<!-- Select CA file -->
<template #title>
<span>{{ $t('tooltip.clientCA') }}</span>
</template>
<NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="caFileInput?.click()">
{{ $t('labels.serverCA') }}
</NcButton>
</a-tooltip>
</div>
</a-form-item> </a-form-item>
<input ref="caFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.ca, caFileInput)" /> <!-- Username -->
<a-form-item :label="$t('labels.username')" v-bind="validateInfos['dataSource.connection.user']">
<input ref="certFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.cert, certFileInput)" /> <a-input v-model:value="(formState.dataSource.connection as DefaultConnection).user" class="nc-extdb-host-user" />
<input ref="keyFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.key, keyFileInput)" />
<a-divider />
<!-- Extra connection parameters -->
<a-form-item class="mb-2" :label="$t('labels.extraConnectionParameters')" v-bind="validateInfos.extraParameters">
<a-card>
<div v-for="(item, index) of formState.extraParameters" :key="index">
<div class="flex py-1 items-center gap-1">
<a-input v-model:value="item.key" />
<span>:</span>
<a-input v-model:value="item.value" />
<component
:is="iconMap.close"
:style="{ 'font-size': '1.5em', 'color': 'red' }"
@click="removeParam(index)"
/>
</div>
</div>
<NcButton size="small" type="dashed" class="w-full caption mt-2" @click="addNewParam">
<div class="flex items-center justify-center">
<component :is="iconMap.plus" />
</div>
</NcButton>
</a-card>
</a-form-item> </a-form-item>
<a-divider /> <!-- Password -->
<a-form-item :label="$t('labels.password')">
<a-form-item :label="$t('labels.inflection.tableName')"> <a-input-password
<a-select v-model:value="(formState.dataSource.connection as DefaultConnection).password"
v-model:value="formState.inflection.inflectionTable" class="nc-extdb-host-password"
dropdown-class-name="nc-dropdown-inflection-table-name" />
>
<a-select-option v-for="tp in inflectionTypes" :key="tp" :value="tp">{{ tp }} </a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item :label="$t('labels.inflection.columnName')"> <!-- Database -->
<a-select <a-form-item :label="$t('labels.database')" v-bind="validateInfos['dataSource.connection.database']">
v-model:value="formState.inflection.inflectionColumn" <!-- Database : create if not exists -->
dropdown-class-name="nc-dropdown-inflection-column-name" <a-input
> v-model:value="formState.dataSource.connection.database"
<a-select-option v-for="tp in inflectionTypes" :key="tp" :value="tp">{{ tp }} </a-select-option> :placeholder="$t('labels.dbCreateIfNotExists')"
</a-select> class="nc-extdb-host-database"
/>
</a-form-item> </a-form-item>
<div class="flex justify-end"> <!-- Schema name -->
<NcButton type="primary" size="small" class="!rounded-md" @click="handleEditJSON()"> <a-form-item
<!-- Edit connection JSON --> v-if="[ClientType.MSSQL, ClientType.PG].includes(formState.dataSource.client) && formState.dataSource.searchPath"
{{ $t('activity.editConnJson') }} :label="$t('labels.schemaName')"
v-bind="validateInfos['dataSource.searchPath.0']"
>
<a-input v-model:value="formState.dataSource.searchPath[0]" />
</a-form-item>
<div class="flex items-right justify-end gap-2">
<!-- Use Connection URL -->
<NcButton type="ghost" size="small" class="nc-extdb-btn-import-url !rounded-md" @click.stop="importURLDlg = true">
{{ $t('activity.useConnectionUrl') }}
</NcButton> </NcButton>
</div> </div>
</a-collapse-panel>
</a-collapse>
</template>
</div>
<a-form-item class="flex justify-end !mt-5"> <a-collapse ghost expand-icon-position="right" class="!mt-6">
<div class="flex justify-end gap-2"> <a-collapse-panel key="1">
<NcButton <template #header>
:type="testSuccess ? 'ghost' : 'primary'" <span>{{ $t('title.advancedParameters') }}</span>
size="small" </template>
class="nc-extdb-btn-test-connection !rounded-md" <a-form-item label="SSL mode">
:loading="testingConnection" <a-select
@click="testConnection" v-model:value="formState.sslUse"
> dropdown-class-name="nc-dropdown-ssl-mode"
<GeneralIcon v-if="testSuccess" icon="circleCheck" class="text-primary mr-2" /> @select="onSSLModeChange"
{{ $t('activity.testDbConn') }} >
</NcButton> <a-select-option v-for="opt in Object.values(SSLUsage)" :key="opt" :value="opt">{{ opt }} </a-select-option>
</a-select>
<NcButton </a-form-item>
size="small"
type="primary" <a-form-item label="SSL keys">
:disabled="!testSuccess" <div class="flex gap-2">
:loading="creatingSource" <a-tooltip placement="top">
class="nc-extdb-btn-submit !rounded-md" <!-- Select .cert file -->
@click="createSource" <template #title>
> <span>{{ $t('tooltip.clientCert') }}</span>
{{ $t('general.submit') }} </template>
</NcButton>
</div> <NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="certFileInput?.click()">
</a-form-item> {{ $t('labels.clientCert') }}
</a-form> </NcButton>
</a-tooltip>
<a-modal
v-model:visible="configEditDlg" <a-tooltip placement="top">
:title="$t('activity.editConnJson')" <!-- Select .key file -->
width="600px" <template #title>
wrap-class-name="nc-modal-edit-connection-json" <span>{{ $t('tooltip.clientKey') }}</span>
@ok="handleOk" </template>
> <NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="keyFileInput?.click()">
<MonacoEditor v-if="configEditDlg" v-model="customFormState" class="h-[400px] w-full" /> {{ $t('labels.clientKey') }}
</a-modal> </NcButton>
</a-tooltip>
<!-- Use Connection URL -->
<a-modal <a-tooltip placement="top">
v-model:visible="importURLDlg" <!-- Select CA file -->
:title="$t('activity.useConnectionUrl')" <template #title>
width="500px" <span>{{ $t('tooltip.clientCA') }}</span>
:ok-text="$t('general.ok')" </template>
:cancel-text="$t('general.cancel')"
wrap-class-name="nc-modal-connection-url" <NcButton size="small" :disabled="!sslFilesRequired" class="shadow" @click="caFileInput?.click()">
@ok="handleImportURL" {{ $t('labels.serverCA') }}
> </NcButton>
<a-input v-model:value="importURL" /> </a-tooltip>
</a-modal> </div>
</div> </a-form-item>
<input ref="caFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.ca, caFileInput)" />
<input ref="certFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.cert, certFileInput)" />
<input ref="keyFileInput" type="file" class="!hidden" @change="onFileSelect(CertTypes.key, keyFileInput)" />
<a-divider />
<!-- Extra connection parameters -->
<a-form-item
class="mb-2"
:label="$t('labels.extraConnectionParameters')"
v-bind="validateInfos.extraParameters"
>
<a-card>
<div v-for="(item, index) of formState.extraParameters" :key="index">
<div class="flex py-1 items-center gap-1">
<a-input v-model:value="item.key" />
<span>:</span>
<a-input v-model:value="item.value" />
<component
:is="iconMap.close"
:style="{ 'font-size': '1.5em', 'color': 'red' }"
@click="removeParam(index)"
/>
</div>
</div>
<NcButton size="small" type="dashed" class="w-full caption mt-2" @click="addNewParam">
<div class="flex items-center justify-center">
<component :is="iconMap.plus" />
</div>
</NcButton>
</a-card>
</a-form-item>
<a-divider />
<a-form-item :label="$t('labels.inflection.tableName')">
<a-select
v-model:value="formState.inflection.inflectionTable"
dropdown-class-name="nc-dropdown-inflection-table-name"
>
<a-select-option v-for="tp in inflectionTypes" :key="tp" :value="tp">{{ tp }} </a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('labels.inflection.columnName')">
<a-select
v-model:value="formState.inflection.inflectionColumn"
dropdown-class-name="nc-dropdown-inflection-column-name"
>
<a-select-option v-for="tp in inflectionTypes" :key="tp" :value="tp">{{ tp }} </a-select-option>
</a-select>
</a-form-item>
<div class="flex justify-end">
<NcButton type="primary" size="small" class="!rounded-md" @click="handleEditJSON()">
<!-- Edit connection JSON -->
{{ $t('activity.editConnJson') }}
</NcButton>
</div>
</a-collapse-panel>
</a-collapse>
</template>
</div>
<a-form-item class="flex justify-end !mt-5">
<div class="flex justify-end gap-2">
<NcButton
:type="testSuccess ? 'ghost' : 'primary'"
size="small"
class="nc-extdb-btn-test-connection !rounded-md"
:loading="testingConnection"
@click="testConnection"
>
<GeneralIcon v-if="testSuccess" icon="circleCheck" class="text-primary mr-2" />
{{ $t('activity.testDbConn') }}
</NcButton>
<NcButton
size="small"
type="primary"
:disabled="!testSuccess"
:loading="creatingSource"
class="nc-extdb-btn-submit !rounded-md"
@click="createSource"
>
{{ $t('general.submit') }}
</NcButton>
</div>
</a-form-item>
</a-form>
<a-modal
v-model:visible="configEditDlg"
:title="$t('activity.editConnJson')"
width="600px"
wrap-class-name="nc-modal-edit-connection-json"
@ok="handleOk"
>
<MonacoEditor v-if="configEditDlg" v-model="customFormState" class="h-[400px] w-full" />
</a-modal>
<!-- Use Connection URL -->
<a-modal
v-model:visible="importURLDlg"
:title="$t('activity.useConnectionUrl')"
width="500px"
:ok-text="$t('general.ok')"
:cancel-text="$t('general.cancel')"
wrap-class-name="nc-modal-connection-url"
@ok="handleImportURL"
>
<a-input v-model:value="importURL" />
</a-modal>
</div>
</div>
</GeneralModal>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

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

@ -52,7 +52,7 @@ const onDelete = async () => {
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete"> <GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete">
<template #entity-preview> <template #entity-preview>
<div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4"> <div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralProjectIcon :type="base.type" class="nc-view-icon px-1.5"></GeneralProjectIcon> <GeneralProjectIcon :type="base.type" class="nc-view-icon px-1.5 w-10" />
<div <div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75" class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

100
packages/nc-gui/components/dlg/SharedBaseDuplicate.vue

@ -0,0 +1,100 @@
<script setup lang="ts">
import { ProjectTypes } from 'nocodb-sdk'
import { isEeUI, useApi, useVModel, useWorkspace } from '#imports'
const props = defineProps<{
modelValue: boolean
onOk: (jobData: { name: string; id: string }) => Promise<void>
}>()
const emit = defineEmits(['update:modelValue'])
const { api } = useApi()
const { sharedBaseId } = useCopySharedBase()
const { workspacesList } = storeToRefs(useWorkspace())
const dialogShow = useVModel(props, 'modelValue', emit)
const options = ref({
includeData: true,
includeViews: true,
})
const optionsToExclude = computed(() => {
const { includeData, includeViews } = options.value
return {
excludeData: !includeData,
excludeViews: !includeViews,
}
})
const isLoading = ref(false)
const selectedWorkspace = ref<string>()
const _duplicate = async () => {
if (!selectedWorkspace.value && isEeUI) return
try {
isLoading.value = true
const jobData = await api.base.duplicateShared(selectedWorkspace.value ?? 'nc', sharedBaseId.value, {
options: optionsToExclude.value,
base: isEeUI
? {
fk_workspace_id: selectedWorkspace.value,
type: ProjectTypes.DATABASE,
}
: {},
})
sharedBaseId.value = null
props.onOk({ ...jobData, workspace_id: selectedWorkspace.value } as any)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isLoading.value = false
dialogShow.value = false
}
}
</script>
<template>
<GeneralModal v-model:visible="dialogShow" class="!w-[30rem]" wrap-class-name="nc-modal-project-duplicate">
<div>
<div class="prose-xl font-bold self-center">{{ $t('general.duplicate') }} {{ $t('labels.sharedBase') }}</div>
<template v-if="isEeUI">
<div class="my-4">Select workspace to duplicate shared base to:</div>
<NcSelect
v-model:value="selectedWorkspace"
class="w-full"
:options="workspacesList.map((w) => ({ label: `${w.title[0].toUpperCase()}${w.title.slice(1)}`, value: w.id }))"
placeholder="Select Workspace"
/>
</template>
<div class="prose-md self-center text-gray-500 mt-4">{{ $t('title.advancedSettings') }}</div>
<a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox>
</div>
</div>
<div class="flex flex-row gap-x-2 mt-2.5 pt-2.5 justify-end">
<NcButton key="back" type="secondary" @click="dialogShow = false">{{ $t('general.cancel') }}</NcButton>
<NcButton
key="submit"
v-e="['a:shared-base:duplicate']"
:loading="isLoading"
:disabled="!selectedWorkspace && isEeUI"
@click="_duplicate"
>{{ $t('general.confirm') }}
</NcButton>
</div>
</GeneralModal>
</template>

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

@ -80,7 +80,7 @@ const isEaster = ref(false)
<a-divider class="!m-0 !p-0 !my-2" /> <a-divider class="!m-0 !p-0 !my-2" />
<div class="text-xs p-2"> <div class="text-xs p-2">
<a-checkbox v-model:checked="options.includeData">{{ $t('labels.includeData') }}a</a-checkbox> <a-checkbox v-model:checked="options.includeData">{{ $t('labels.includeData') }}</a-checkbox>
<a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox> <a-checkbox v-model:checked="options.includeViews">{{ $t('labels.includeView') }}</a-checkbox>
<a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">{{ $t('labels.includeWebhook') }}</a-checkbox> <a-checkbox v-show="isEaster" v-model:checked="options.includeHooks">{{ $t('labels.includeWebhook') }}</a-checkbox>
</div> </div>

38
packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue

@ -1,5 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { extractSdkResponseErrorMsg, message, onMounted, storeToRefs, useBase, useDashboard, useNuxtApp } from '#imports' import {
extractSdkResponseErrorMsg,
message,
onMounted,
storeToRefs,
useBase,
useDashboard,
useGlobal,
useNuxtApp,
useWorkspace,
} from '#imports'
interface ShareBase { interface ShareBase {
uuid?: string uuid?: string
@ -20,9 +30,23 @@ const sharedBase = ref<null | ShareBase>(null)
const { base } = storeToRefs(useBase()) const { base } = storeToRefs(useBase())
const url = computed(() => const { getBaseUrl, appInfo } = useGlobal()
sharedBase.value && sharedBase.value.uuid ? `${dashboardUrl.value}#/base/${sharedBase.value.uuid}` : '',
) const workspaceStore = useWorkspace()
const url = computed(() => {
if (!sharedBase.value || !sharedBase.value.uuid) return ''
// get base url for workspace
const baseUrl = getBaseUrl(workspaceStore.activeWorkspaceId)
let dashboardUrl1 = dashboardUrl.value
if (baseUrl) {
dashboardUrl1 = `${baseUrl}${appInfo.value?.dashboardPath}`
}
return encodeURI(`${dashboardUrl1}#/base/${sharedBase.value.uuid}`)
})
const loadBase = async () => { const loadBase = async () => {
try { try {
@ -50,6 +74,8 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
sharedBase.value = res ?? {} sharedBase.value = res ?? {}
sharedBase.value!.role = role sharedBase.value!.role = role
base.value.uuid = res.uuid
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -63,6 +89,8 @@ const disableSharedBase = async () => {
await $api.base.sharedBaseDisable(base.value.id) await $api.base.sharedBaseDisable(base.value.id)
sharedBase.value = null sharedBase.value = null
base.value.uuid = undefined
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -131,7 +159,7 @@ const onRoleToggle = async () => {
</div> </div>
<div v-if="isSharedBaseEnabled" class="flex flex-col w-full mt-3 border-t-1 pt-3 border-gray-100"> <div v-if="isSharedBaseEnabled" class="flex flex-col w-full mt-3 border-t-1 pt-3 border-gray-100">
<GeneralCopyUrl v-model:url="url" /> <GeneralCopyUrl v-model:url="url" />
<div class="flex flex-row justify-between mt-3 bg-gray-50 px-3 py-2 rounded-md"> <div v-if="!appInfo.ee" class="flex flex-row justify-between mt-3 bg-gray-50 px-3 py-2 rounded-md">
<div class="text-black">{{ $t('activity.editingAccess') }}</div> <div class="text-black">{{ $t('activity.editingAccess') }}</div>
<a-switch <a-switch
v-e="['c:share:base:role:toggle']" v-e="['c:share:base:role:toggle']"

13
packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue

@ -6,6 +6,7 @@ import { useMetas } from '#imports'
const { view: _view, $api } = useSmartsheetStoreOrThrow() const { view: _view, $api } = useSmartsheetStoreOrThrow()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { getBaseUrl, appInfo } = useGlobal()
const { dashboardUrl } = useDashboard() const { dashboardUrl } = useDashboard()
@ -13,6 +14,8 @@ const viewStore = useViewsStore()
const { metas } = useMetas() const { metas } = useMetas()
const workspaceStore = useWorkspace()
const isUpdating = ref({ const isUpdating = ref({
public: false, public: false,
password: false, password: false,
@ -162,7 +165,15 @@ function sharedViewUrl() {
viewType = 'view' viewType = 'view'
} }
return encodeURI(`${dashboardUrl?.value}#/nc/${viewType}/${activeView.value.uuid}`) // get base url for workspace
const baseUrl = getBaseUrl(workspaceStore.activeWorkspaceId)
let dashboardUrl1 = dashboardUrl.value
if (baseUrl) {
dashboardUrl1 = `${baseUrl}${appInfo.value?.dashboardPath}`
}
return encodeURI(`${dashboardUrl1}#/nc/${viewType}/${activeView.value.uuid}`)
} }
const toggleViewShare = async () => { const toggleViewShare = async () => {

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

@ -32,7 +32,7 @@ const hasColumns = computed(() => data.pkAndFkColumns.length || data.nonPkColumn
const nonPkColumns = computed(() => const nonPkColumns = computed(() =>
data.nonPkColumns data.nonPkColumns
// Removed MM system column from the table node // Removed MM system column from the table node
.filter((col) => !(col.system && isLinksOrLTAR(col) && /nc_.*___nc_m2m_.*/.test(col.title!))), .filter((col) => !(col.system && isLinksOrLTAR(col) && /.*_nc_m2m_.*/.test(col.title!))),
) )
watch( watch(
@ -65,7 +65,7 @@ watch(
:class="[showSkeleton ? '' : '', hasColumns ? '' : '']" :class="[showSkeleton ? '' : '', hasColumns ? '' : '']"
class="text-gray-800 text-sm py-4 border-b-1 border-gray-200 rounded-t-lg w-full h-full px-3 font-medium flex items-center" class="text-gray-800 text-sm py-4 border-b-1 border-gray-200 rounded-t-lg w-full h-full px-3 font-medium flex items-center"
> >
<GeneralTableIcon class="text-primary" :class="{ '!text-6xl !w-auto mr-2': showSkeleton }" :meta="table" /> <GeneralTableIcon class="text-primary" :class="{ '!text-6xl !w-auto mr-2 !h-18': showSkeleton }" :meta="table" />
<div :class="showSkeleton ? 'text-6xl' : ''" class="flex pr-2 pl-1"> <div :class="showSkeleton ? 'text-6xl' : ''" class="flex pr-2 pl-1">
{{ table.title }} {{ table.title }}
</div> </div>

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

@ -6,10 +6,10 @@ import SimpleIconsMicrosoftsqlserver from '~icons/simple-icons/microsoftsqlserve
import LogosSnowflakeIcon from '~icons/logos/snowflake-icon' import LogosSnowflakeIcon from '~icons/logos/snowflake-icon'
import MdiDatabaseOutline from '~icons/mdi/database-outline' import MdiDatabaseOutline from '~icons/mdi/database-outline'
const { baseType } = defineProps<{ baseType?: string }>() const { sourceType } = defineProps<{ sourceType?: string }>()
const baseIcon = computed(() => { const baseIcon = computed(() => {
switch (baseType) { switch (sourceType) {
case ClientType.MYSQL: case ClientType.MYSQL:
return LogosMysqlIcon return LogosMysqlIcon
case ClientType.PG: case ClientType.PG:

6
packages/nc-gui/components/general/Modal.vue

@ -7,18 +7,21 @@ const props = withDefaults(
destroyOnClose?: boolean destroyOnClose?: boolean
maskClosable?: boolean maskClosable?: boolean
closable?: boolean closable?: boolean
keyboard?: boolean
}>(), }>(),
{ {
size: 'medium', size: 'medium',
destroyOnClose: true, destroyOnClose: true,
maskClosable: true, maskClosable: true,
closable: false, closable: false,
keyboard: true,
}, },
) )
const emits = defineEmits(['update:visible']) const emits = defineEmits(['update:visible'])
const { width: propWidth, destroyOnClose, closable, maskClosable } = props const { width: propWidth, destroyOnClose } = props
const { maskClosable, closable, keyboard } = toRefs(props)
const width = computed(() => { const width = computed(() => {
if (propWidth) { if (propWidth) {
@ -65,6 +68,7 @@ const visible = useVModel(props, 'visible', emits)
:class="{ active: visible }" :class="{ active: visible }"
:width="width" :width="width"
:closable="closable" :closable="closable"
:keyboard="keyboard"
wrap-class-name="nc-modal-wrapper" wrap-class-name="nc-modal-wrapper"
:footer="null" :footer="null"
:destroy-on-close="destroyOnClose" :destroy-on-close="destroyOnClose"

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

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { isDrawerOrModalExist, isEeUI, isMac, useNuxtApp, useRoles } from '#imports' import { isDrawerOrModalExist, isMac, useNuxtApp, useRoles } from '#imports'
interface Props { interface Props {
disabled?: boolean disabled?: boolean
@ -8,11 +8,12 @@ interface Props {
const { disabled, isViewToolbar } = defineProps<Props>() const { disabled, isViewToolbar } = defineProps<Props>()
const { isMobileMode } = useGlobal() const { isMobileMode, getMainUrl } = useGlobal()
const { visibility, showShareModal } = storeToRefs(useShare()) const { visibility, showShareModal } = storeToRefs(useShare())
const { activeTable } = storeToRefs(useTablesStore()) const { activeTable } = storeToRefs(useTablesStore())
const { base, isSharedBase } = storeToRefs(useBase()) const { base, isSharedBase } = storeToRefs(useBase())
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -38,7 +39,8 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
}) })
const copySharedBase = async () => { const copySharedBase = async () => {
navigateTo(`/copy-shared-base?base=${route.params.baseId}`) const baseUrl = getMainUrl()
window.open(`${baseUrl || ''}#/copy-shared-base?base=${route.params.baseId}`, '_blank')
} }
</script> </script>
@ -70,7 +72,7 @@ const copySharedBase = async () => {
</NcButton> </NcButton>
</div> </div>
<template v-else-if="isSharedBase && isEeUI"> <template v-else-if="isSharedBase">
<div class="flex-1"></div> <div class="flex-1"></div>
<div class="flex flex-col justify-center h-full"> <div class="flex flex-col justify-center h-full">
<div class="flex flex-row items-center w-full"> <div class="flex flex-row items-center w-full">

15
packages/nc-gui/components/nc/Pagination.vue

@ -70,8 +70,11 @@ const pagesList = computed(() => {
> >
<GeneralIcon icon="arrowLeft" /> <GeneralIcon icon="arrowLeft" />
</NcButton> </NcButton>
<div class="text-gray-600"> <div v-if="!isMobileMode" class="text-gray-600">
<a-select v-model:value="current" class="!mr-[2px]" virtual> <a-select v-model:value="current" class="!mr-[2px]" virtual>
<template #suffixIcon>
<GeneralIcon icon="arrowDown" class="text-gray-500 nc-select-expand-btn" />
</template>
<a-select-option v-for="p of pagesList" :key="`p-${p}`" @click="changePage({ set: p })">{{ p }}</a-select-option> <a-select-option v-for="p of pagesList" :key="`p-${p}`" @click="changePage({ set: p })">{{ p }}</a-select-option>
</a-select> </a-select>
<span class="mx-1"> {{ mode !== 'full' ? '/' : 'of' }} </span> <span class="mx-1"> {{ mode !== 'full' ? '/' : 'of' }} </span>
@ -104,3 +107,13 @@ const pagesList = computed(() => {
</NcButton> </NcButton>
</div> </div>
</template> </template>
<style lang="scss" scoped>
:deep(.ant-select-selector) {
@apply !border-gray-200 !rounded-lg;
}
:deep(.ant-select-dropdown) {
@apply !rounded-lg !overflow-hidden;
}
</style>

5
packages/nc-gui/components/nc/Tooltip.vue

@ -12,6 +12,7 @@ interface Props {
disabled?: boolean disabled?: boolean
placement?: TooltipPlacement | undefined placement?: TooltipPlacement | undefined
hideOnClick?: boolean hideOnClick?: boolean
overlayClassName?: string
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -36,6 +37,8 @@ const attrs = useAttrs()
const isKeyPressed = ref(false) const isKeyPressed = ref(false)
const overlayClassName = computed(() => props.overlayClassName)
onKeyStroke( onKeyStroke(
(e) => e.key === modifierKey.value, (e) => e.key === modifierKey.value,
(e) => { (e) => {
@ -100,7 +103,7 @@ const onClick = () => {
<template> <template>
<a-tooltip <a-tooltip
v-model:visible="showTooltip" v-model:visible="showTooltip"
:overlay-class-name="`nc-tooltip ${showTooltip ? 'visible' : 'hidden'}`" :overlay-class-name="`nc-tooltip ${showTooltip ? 'visible' : 'hidden'} ${overlayClassName}`"
:overlay-style="tooltipStyle" :overlay-style="tooltipStyle"
arrow-point-at-center arrow-point-at-center
:trigger="[]" :trigger="[]"

10
packages/nc-gui/components/project/AllTables.vue

@ -147,8 +147,8 @@ const onCreateBaseClick = () => {
</div> </div>
<div class="w-1/5 text-gray-600" data-testid="proj-view-list__item-type"> <div class="w-1/5 text-gray-600" data-testid="proj-view-list__item-type">
<div v-if="table.source_id === defaultBase?.id" class="ml-0.75">-</div> <div v-if="table.source_id === defaultBase?.id" class="ml-0.75">-</div>
<div v-else> <div v-else class="capitalize flex flex-row items-center gap-x-0.5">
<GeneralBaseLogo :source-type="sources.get(table.source_id!)?.type" class="w-4 mr-1" /> <GeneralBaseLogo class="w-4 mr-1" />
{{ sources.get(table.source_id!)?.alias }} {{ sources.get(table.source_id!)?.alias }}
</div> </div>
</div> </div>
@ -158,11 +158,7 @@ const onCreateBaseClick = () => {
</div> </div>
</div> </div>
<ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :source="defaultBase" /> <ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :source="defaultBase" />
<GeneralModal v-model:visible="isNewBaseModalOpen" size="medium"> <LazyDashboardSettingsDataSourcesCreateBase v-model:open="isNewBaseModalOpen" />
<div class="py-6 px-8">
<LazyDashboardSettingsDataSourcesCreateBase @close="isNewBaseModalOpen = false" />
</div>
</GeneralModal>
</div> </div>
</template> </template>

14
packages/nc-gui/components/project/View.vue

@ -3,7 +3,7 @@ import { useTitle } from '@vueuse/core'
import NcLayout from '~icons/nc-icons/layout' import NcLayout from '~icons/nc-icons/layout'
const { openedProject } = storeToRefs(useBases()) const { openedProject } = storeToRefs(useBases())
const { activeTables } = storeToRefs(useTablesStore()) const { activeTables } = storeToRefs(useTablesStore())
const { activeWorkspace } = storeToRefs(useWorkspace()) const { activeWorkspace, workspaceUserCount } = storeToRefs(useWorkspace())
const { navigateToProjectPage } = useBase() const { navigateToProjectPage } = useBase()
@ -113,6 +113,16 @@ watch(
<div class="tab-title" data-testid="proj-view-tab__access-settings"> <div class="tab-title" data-testid="proj-view-tab__access-settings">
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" /> <GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
<div>{{ $t('labels.members') }}</div> <div>{{ $t('labels.members') }}</div>
<div
v-if="workspaceUserCount"
class="tab-info"
:class="{
'bg-primary-selected': projectPageTab === 'data-source',
'bg-gray-50': projectPageTab !== 'data-source',
}"
>
{{ workspaceUserCount }}
</div>
</div> </div>
</template> </template>
<ProjectAccessSettings /> <ProjectAccessSettings />
@ -130,7 +140,7 @@ watch(
'bg-gray-50': projectPageTab !== 'data-source', 'bg-gray-50': projectPageTab !== 'data-source',
}" }"
> >
{{ base.sources.length - 1 }} {{ base.sources.length }}
</div> </div>
</div> </div>
</template> </template>

2
packages/nc-gui/components/smartsheet/Details.vue

@ -33,7 +33,7 @@ const openedSubTab = computed({
watch(openedSubTab, () => { watch(openedSubTab, () => {
// TODO: Find a good way to know when the roles are populated and check // TODO: Find a good way to know when the roles are populated and check
// Re-enable this check for first render // Re-enable this check for first render
if (openedSubTab.value === 'field' && !isUIAllowed('hookList')) { if (openedSubTab.value === 'field' && !isUIAllowed('fieldAdd')) {
onViewsTabChange('relation') onViewsTabChange('relation')
} }
if (openedSubTab.value === 'webhook' && !isUIAllowed('hookList')) { if (openedSubTab.value === 'webhook' && !isUIAllowed('hookList')) {

183
packages/nc-gui/components/smartsheet/Kanban.vue

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import Draggable from 'vuedraggable' import Draggable from 'vuedraggable'
import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk' import { ViewTypes, isVirtualCol } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
FieldsInj, FieldsInj,
@ -16,7 +16,6 @@ import {
iconMap, iconMap,
inject, inject,
isImage, isImage,
isLTAR,
onBeforeUnmount, onBeforeUnmount,
provide, provide,
useAttachment, useAttachment,
@ -56,6 +55,8 @@ const expandedFormRow = ref<RowType>()
const expandedFormRowState = ref<Record<string, any>>() const expandedFormRowState = ref<Record<string, any>>()
provide(RowHeightInj, ref(1 as const))
const deleteStackVModel = ref(false) const deleteStackVModel = ref(false)
const stackToBeDeleted = ref('') const stackToBeDeleted = ref('')
@ -107,7 +108,9 @@ const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== kanbanMetaData.value?.fk_cover_image_col_id)) const fieldsWithoutDisplay = computed(() => fields.value.filter((f) => !isPrimary(f)))
const displayField = computed(() => meta.value?.columns?.find((c) => c.pv && fields.value.includes(c)) ?? null)
const coverImageColumn: any = computed(() => const coverImageColumn: any = computed(() =>
meta.value?.columnsById meta.value?.columnsById
@ -209,6 +212,8 @@ const expandedFormOnRowIdDlg = computed({
}) })
const expandFormClick = async (e: MouseEvent, row: RowType) => { const expandFormClick = async (e: MouseEvent, row: RowType) => {
const target = e.target as HTMLElement
if (target.closest('.arrow') || target.closest('.slick-dots')) return
if (e.target as HTMLElement) { if (e.target as HTMLElement) {
expandForm(row) expandForm(row)
} }
@ -380,6 +385,11 @@ watch(
immediate: true, immediate: true,
}, },
) )
const getRowId = (row: RowType) => {
const pk = extractPkFromRow(row.row, meta.value!.columns!)
return pk ? `row-${pk}` : ''
}
</script> </script>
<template> <template>
@ -425,7 +435,7 @@ watch(
<!-- Non Collapsed Stacks --> <!-- Non Collapsed Stacks -->
<a-card <a-card
v-if="!stack.collapsed" v-if="!stack.collapsed"
:key="stack.id" :key="`${stack.id}-${stackIdx}`"
class="mx-4 !bg-gray-100 flex flex-col w-80 h-full !rounded-xl overflow-y-hidden" class="mx-4 !bg-gray-100 flex flex-col w-80 h-full !rounded-xl overflow-y-hidden"
:class="{ :class="{
'not-draggable': stack.title === null || isLocked || isPublic || !hasEditPermission, 'not-draggable': stack.title === null || isLocked || isPublic || !hasEditPermission,
@ -516,95 +526,123 @@ watch(
@end="(e) => e.target.classList.remove('grabbing')" @end="(e) => e.target.classList.remove('grabbing')"
@change="onMove($event, stack.title)" @change="onMove($event, stack.title)"
> >
<template #item="{ element: record }"> <template #item="{ element: record, index }">
<div class="nc-kanban-item py-2 pl-3 pr-2"> <div class="nc-kanban-item py-2 pl-3 pr-2">
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<a-card <a-card
hoverable :key="`${getRowId(record)}-${index}`"
class="!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] shadow-sm hover:shadow-md cursor-pointer"
:body-style="{ padding: '0px' }"
:data-stack="stack.title" :data-stack="stack.title"
class="!rounded-xl h-full overflow-hidden break-all max-w-[450px]" :data-testid="`nc-gallery-card-${record.row.id}`"
:class="{ :class="{
'not-draggable': isLocked || !hasEditPermission || isPublic, 'not-draggable': isLocked || !hasEditPermission || isPublic,
'!cursor-default': isLocked || !hasEditPermission || isPublic, '!cursor-default': isLocked || !hasEditPermission || isPublic,
}" }"
:body-style="{ padding: '10px' }"
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, record)" @contextmenu="showContextMenu($event, record)"
> >
<template v-if="kanbanMetaData?.fk_cover_image_col_id" #cover> <template v-if="kanbanMetaData?.fk_cover_image_col_id" #cover>
<a-carousel <template v-if="!reloadAttachments && attachments(record).length">
v-if="!reloadAttachments && attachments(record).length" <a-carousel
autoplay :key="attachments(record).reduce((acc, curr) => acc + curr?.path, '')"
class="gallery-carousel" class="gallery-carousel !border-b-1 !border-gray-200"
arrows >
> <template #customPaging>
<template #customPaging> <a>
<a> <div class="pt-[12px]">
<div class="pt-[12px]"> <div></div>
<div></div> </div>
</a>
</template>
<template #prevArrow>
<div class="z-10 arrow">
<MdiChevronLeft
class="text-gray-700 w-6 h-6 absolute left-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div> </div>
</a> </template>
</template>
<template #prevArrow>
<div style="z-index: 1"></div>
</template>
<template #nextArrow>
<div style="z-index: 1"></div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-cover"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<component :is="iconMap.imagePlaceholder" v-else class="w-full h-48 my-4 text-cool-gray-200" /> <template #nextArrow>
<div class="z-10 arrow">
<MdiChevronRight
class="text-gray-700 w-6 h-6 absolute right-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template v-for="attachment in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="attachment.path"
class="h-52 object-cover"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
</template>
<div
v-else
class="h-52 w-full !flex flex-row !border-b-1 !border-gray-200 items-center justify-center"
>
<img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" />
</div>
</template> </template>
<div <h2 v-if="displayField" class="text-base mt-3 mx-3 font-bold">
v-for="col in fieldsWithoutCover" <LazySmartsheetVirtualCell
:key="`record-${record.row.id}-${col.id}`" v-if="isVirtualCol(displayField)"
class="flex flex-col rounded-lg w-full" v-model="record.row[displayField.title]"
> class="!text-gray-600"
<div v-if="!isRowEmpty(record, col) || isLTAR(col.uidt, col.colOptions)"> :column="displayField"
<!-- Smartsheet Header (Virtual) Cell --> :row="record"
<div class="flex flex-row w-full justify-start pt-2"> />
<div class="w-full text-gray-400">
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
class="!text-gray-600"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</h2>
<div v-for="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`">
<div class="flex flex-col first:mt-3 ml-2 !pr-3.5 !mb-[0.75rem] rounded-lg w-full">
<div class="flex flex-row w-full justify-start scale-75">
<div class="w-full pb-1 text-gray-300">
<LazySmartsheetHeaderVirtualCell <LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)" v-if="isVirtualCol(col)"
:column="col" :column="col"
:hide-menu="true" :hide-menu="true"
:hide-icon="true"
/> />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" :hide-icon="true" />
</div> </div>
</div> </div>
<!-- Smartsheet (Virtual) Cell -->
<div <div
class="flex flex-row w-full items-center justify-start" v-if="!isRowEmpty(record, col)"
:class="{ '!ml-[-12px] pl-3': col.uidt === UITypes.SingleSelect }" class="flex flex-row w-full text-gray-700 px-1 mt-[-0.25rem] items-center justify-start"
> >
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="col.title && isVirtualCol(col)" v-if="isVirtualCol(col)"
v-model="record.row[col.title]" v-model="record.row[col.title]"
class="text-sm pt-1 pl-5"
:column="col" :column="col"
:row="record" :row="record"
/> />
<LazySmartsheetCell <LazySmartsheetCell
v-else-if="col.title" v-else
v-model="record.row[col.title]" v-model="record.row[col.title]"
class="text-sm pt-1 pl-7.25"
:column="col" :column="col"
:edit-enabled="false" :edit-enabled="false"
:read-only="true" :read-only="true"
/> />
</div> </div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">-</div>
</div> </div>
</div> </div>
</a-card> </a-card>
@ -755,4 +793,37 @@ watch(
transform-origin: left top 0px; transform-origin: left top 0px;
transition: left 0.2s ease-in-out 0s; transition: left 0.2s ease-in-out 0s;
} }
:deep(.slick-dots li button) {
@apply !bg-black;
}
.ant-carousel.gallery-carousel :deep(.slick-dots) {
@apply !w-auto absolute h-auto bottom-[-15px] absolute h-auto;
height: auto;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
@apply rounded-full border-0 cursor-pointer block opacity-100 p-0 outline-none transition-all duration-500 text-transparent h-2 w-2 bg-[#d9d9d9];
font-size: 0;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li.slick-active div > div) {
@apply bg-brand-500 opacity-100;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li) {
@apply !w-auto;
}
.ant-carousel.gallery-carousel :deep(.slick-prev) {
@apply left-0;
}
.ant-carousel.gallery-carousel :deep(.slick-next) {
@apply right-0;
}
:deep(.slick-slide) {
@apply !pointer-events-none;
}
</style> </style>

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

@ -14,6 +14,7 @@ interface Props {
fixedSize?: number fixedSize?: number
extraStyle?: string extraStyle?: string
showApiTiming?: boolean showApiTiming?: boolean
alignLeft?: boolean
} }
const props = defineProps<Props>() const props = defineProps<Props>()
@ -34,6 +35,8 @@ const extraStyle = toRef(props, 'extraStyle')
const isGroupBy = inject(IsGroupByInj, ref(false)) const isGroupBy = inject(IsGroupByInj, ref(false))
const alignLeft = computed(() => props.alignLeft ?? false)
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore()) const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
@ -69,7 +72,12 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
isGroupBy ? 'margin-top:1px; border-radius: 0 0 12px 12px !important;' : '' isGroupBy ? 'margin-top:1px; border-radius: 0 0 12px 12px !important;' : ''
}${extraStyle}`" }${extraStyle}`"
> >
<div class="flex-1 flex items-center"> <div
class="flex items-center"
:class="{
'flex-1': !alignLeft,
}"
>
<slot name="add-record" /> <slot name="add-record" />
<span <span
v-if="!alignCountOnRight && count !== null && count !== Infinity" v-if="!alignCountOnRight && count !== null && count !== Infinity"
@ -84,7 +92,8 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
v-if="!hidePagination" v-if="!hidePagination"
class="transition-all duration-350" class="transition-all duration-350"
:class="{ :class="{
'-ml-17': isLeftSidebarOpen, '-ml-17': isLeftSidebarOpen && !alignLeft,
'ml-8': alignLeft,
}" }"
> >
<div v-if="isPaginationLoading" class="flex flex-row justify-center item-center min-h-10 min-w-42"> <div v-if="isPaginationLoading" class="flex flex-row justify-center item-center min-h-10 min-w-42">

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

@ -48,7 +48,7 @@ const { getMeta } = useMetas()
const { t } = useI18n() const { t } = useI18n()
const columnLabel = computed(() => props.columnLabel || t('objects.column')) const columnLabel = computed(() => props.columnLabel || t('objects.field'))
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -66,7 +66,7 @@ const isForm = inject(IsFormInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false)) const isKanban = inject(IsKanbanInj, ref(false))
const { isMysql, isMssql } = useBase() const { isMysql, isMssql, isXcdbBase } = useBase()
const reloadDataTrigger = inject(ReloadViewDataHookInj) const reloadDataTrigger = inject(ReloadViewDataHookInj)
@ -86,9 +86,9 @@ const showDeprecated = ref(false)
const uiTypesOptions = computed<typeof uiTypes>(() => { const uiTypesOptions = computed<typeof uiTypes>(() => {
return [ return [
...uiTypes.filter( ...uiTypes
(t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual) && (!t.deprecated || showDeprecated.value), .filter((t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual) && (!t.deprecated || showDeprecated.value))
), .filter((t) => !(t.name === UITypes.SpecificDBType && isXcdbBase(meta.value?.source_id))),
...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk) ...(!isEdit.value && meta?.value?.columns?.every((c) => !c.pk)
? [ ? [
{ {
@ -214,7 +214,7 @@ if (props.fromTableExplorer) {
'bg-white': !props.fromTableExplorer, 'bg-white': !props.fromTableExplorer,
'w-[400px]': !props.embedMode, 'w-[400px]': !props.embedMode,
'!w-[600px]': formState.uidt === UITypes.Formula && !props.embedMode, '!w-[600px]': formState.uidt === UITypes.Formula && !props.embedMode,
'!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode, '!w-[500px]': formState.uidt === UITypes.Attachment && !props.embedMode && !appInfo.ee,
'shadow-lg border-1 border-gray-50 shadow-gray-100 rounded-md p-6': !embedMode, 'shadow-lg border-1 border-gray-50 shadow-gray-100 rounded-md p-6': !embedMode,
}" }"
@keydown="handleEscape" @keydown="handleEscape"
@ -331,7 +331,7 @@ if (props.fromTableExplorer) {
</div> </div>
<div <div
v-if="!props.hideAdditionalOptions && !isVirtualCol(formState.uidt) && !appInfo.ee" v-if="!props.hideAdditionalOptions && !isVirtualCol(formState.uidt) && (!appInfo.ee || (appInfo.ee && !isXcdbBase(meta!.source_id) && formState.uidt === UITypes.SpecificDBType))"
class="text-xs cursor-pointer text-gray-400 nc-more-options mb-1 mt-4 flex items-center gap-1 justify-end" class="text-xs cursor-pointer text-gray-400 nc-more-options mb-1 mt-4 flex items-center gap-1 justify-end"
@click="advancedOptions = !advancedOptions" @click="advancedOptions = !advancedOptions"
> >

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

@ -44,7 +44,7 @@ const refTables = computed(() => {
isLinksOrLTAR(column) && isLinksOrLTAR(column) &&
!column.system && !column.system &&
column.source_id === meta.value?.source_id && column.source_id === meta.value?.source_id &&
(!appInfo.value.ee || (column?.colOptions as any)?.type === 'bt'), (!appInfo.value.ee || vModel.value.fk_relation_column_id === column.id || (column?.colOptions as any)?.type === 'bt'),
) )
.map((column) => ({ .map((column) => ({
col: column.colOptions, col: column.colOptions,
@ -61,7 +61,8 @@ const columns = computed<ColumnType[]>(() => {
return [] return []
} }
return metas.value[selectedTable.id].columns.filter( return metas.value[selectedTable.id].columns.filter(
(c: ColumnType) => !isSystemColumn(c) && c.id !== vModel.value.id && c.uidt !== UITypes.Links, (c: ColumnType) =>
vModel.value.fk_lookup_column_id === c.id || (!isSystemColumn(c) && c.id !== vModel.value.id && c.uidt !== UITypes.Links),
) )
}) })
@ -85,7 +86,7 @@ const cellIcon = (column: ColumnType) =>
<template> <template>
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4"> <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"> <div v-if="refTables.length" class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.links')" v-bind="validateInfos.fk_relation_column_id"> <a-form-item class="flex w-1/2 pb-2" :label="$t('labels.links')" v-bind="validateInfos.fk_relation_column_id">
<a-select <a-select
v-model:value="vModel.fk_relation_column_id" v-model:value="vModel.fk_relation_column_id"
@ -104,7 +105,7 @@ const cellIcon = (column: ColumnType) =>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id"> <a-form-item class="flex w-1/2" :label="$t('labels.childField')" v-bind="validateInfos.fk_lookup_column_id">
<a-select <a-select
v-model:value="vModel.fk_lookup_column_id" v-model:value="vModel.fk_lookup_column_id"
name="fk_lookup_column_id" name="fk_lookup_column_id"
@ -120,6 +121,7 @@ const cellIcon = (column: ColumnType) =>
</a-select> </a-select>
</a-form-item> </a-form-item>
</div> </div>
<div v-else>{{ $t('msg.linkColumnClearNotSupportedYet') }}</div>
</div> </div>
</template> </template>

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

@ -3,7 +3,7 @@ import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import InfiniteLoading from 'v3-infinite-loading' import InfiniteLoading from 'v3-infinite-loading'
import { IsKanbanInj, enumColor, iconMap, onMounted, useColumnCreateStoreOrThrow, useVModel, watch } from '#imports' import { IsKanbanInj, enumColor, iconMap, onMounted, useColumnCreateStoreOrThrow, useVModel } from '#imports'
interface Option { interface Option {
color: string color: string
@ -21,16 +21,22 @@ const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, isMysql, isPg } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
// const { base } = storeToRefs(useBase()) // const { base } = storeToRefs(useBase())
const { optionsMagic: _optionsMagic } = useNocoEe() const { optionsMagic: _optionsMagic } = useNocoEe()
const optionsWrapperDomRef = ref<HTMLElement>()
const options = ref<(Option & { status?: 'remove' })[]>([]) const options = ref<(Option & { status?: 'remove' })[]>([])
const isAddingOption = ref(false)
// TODO: Implement proper top and bottom virtual scrolling
const OPTIONS_PAGE_COUNT = 20 const OPTIONS_PAGE_COUNT = 20
const loadedOptionCount = ref(OPTIONS_PAGE_COUNT) const loadedOptionAnchor = ref(OPTIONS_PAGE_COUNT)
const isReverseLazyLoad = ref(false)
const renderedOptions = ref<(Option & { status?: 'remove' })[]>([]) const renderedOptions = ref<(Option & { status?: 'remove' })[]>([])
const savedDefaultOption = ref<Option | null>(null) const savedDefaultOption = ref<Option | null>(null)
@ -40,7 +46,6 @@ const colorMenus = ref<any>({})
const colors = ref(enumColor.light) const colors = ref(enumColor.light)
const inputs = ref()
const defaultOption = ref() const defaultOption = ref()
const isKanban = inject(IsKanbanInj, ref(false)) const isKanban = inject(IsKanbanInj, ref(false))
@ -89,11 +94,13 @@ onMounted(() => {
} }
} }
isReverseLazyLoad.value = false
options.value = vModel.value.colOptions.options options.value = vModel.value.colOptions.options
loadedOptionCount.value = Math.min(loadedOptionCount.value, options.value.length) loadedOptionAnchor.value = Math.min(loadedOptionAnchor.value, options.value.length)
renderedOptions.value = [...options.value].slice(0, loadedOptionCount.value) renderedOptions.value = [...options.value].slice(0, loadedOptionAnchor.value)
// Support for older options // Support for older options
for (const op of options.value.filter((el) => el.order === null)) { for (const op of options.value.filter((el) => el.order === null)) {
@ -129,19 +136,36 @@ const getNextColor = () => {
} }
const addNewOption = () => { const addNewOption = () => {
isAddingOption.value = true
const tempOption = { const tempOption = {
title: '', title: '',
color: getNextColor(), color: getNextColor(),
} }
options.value.push(tempOption) options.value.push(tempOption)
loadedOptionCount.value = options.value.length isReverseLazyLoad.value = true
renderedOptions.value = [...options.value]
loadedOptionAnchor.value = options.value.length - OPTIONS_PAGE_COUNT
loadedOptionAnchor.value = Math.max(loadedOptionAnchor.value, 0)
renderedOptions.value = options.value.slice(loadedOptionAnchor.value, options.value.length)
optionsWrapperDomRef.value!.scrollTop = optionsWrapperDomRef.value!.scrollHeight
nextTick(() => { nextTick(() => {
if (inputs.value?.$el) { // Last child doesnt work for query selector
inputs.value.$el.focus() setTimeout(() => {
} const doms = document.querySelectorAll(`.nc-col-option-select-option .nc-select-col-option-select-option`)
const dom = doms[doms.length - 1] as HTMLInputElement
if (dom) {
dom.focus()
}
}, 150)
optionsWrapperDomRef.value!.scrollTop = optionsWrapperDomRef.value!.scrollHeight
isAddingOption.value = false
}) })
} }
@ -181,34 +205,87 @@ const undoRemoveRenderedOption = (index: number) => {
} }
} }
const loadListData = async ($state: any) => { // focus last created input
if (loadedOptionCount.value === options.value.length) { // watch(inputs, () => {
// if (inputs.value?.$el) {
// inputs.value.$el.focus()
// }
// })
// Removes the Select Option from cdf if the option is removed
watch(vModel.value, (next) => {
const cdfs = (next.cdf ?? '').split(',')
const values = (next.colOptions.options ?? []).map((col) => {
return col.title.replace(/^'/, '').replace(/'$/, '')
})
const newCdf = cdfs.filter((c: string) => values.includes(c)).join(',')
next.cdf = newCdf.length === 0 ? null : newCdf
})
const loadListDataReverse = async ($state: any) => {
if (isAddingOption.value) return
if (loadedOptionAnchor.value === 0) {
$state.complete() $state.complete()
return return
} }
$state.loading() $state.loading()
loadedOptionCount.value += OPTIONS_PAGE_COUNT loadedOptionAnchor.value -= OPTIONS_PAGE_COUNT
loadedOptionCount.value = Math.min(loadedOptionCount.value, options.value.length) loadedOptionAnchor.value = Math.max(loadedOptionAnchor.value, 0)
renderedOptions.value = options.value.slice(loadedOptionAnchor.value, options.value.length)
renderedOptions.value = options.value.slice(0, loadedOptionCount.value) optionsWrapperDomRef.value!.scrollTop = optionsWrapperDomRef.value!.scrollTop + 100
if (loadedOptionCount.value === options.value.length) { if (loadedOptionAnchor.value === 0) {
$state.complete() $state.complete()
return return
} }
$state.loaded() $state.loaded()
} }
const loadListData = async ($state: any) => {
if (isAddingOption.value) return
if (loadedOptionAnchor.value === options.value.length) {
return $state.complete()
}
$state.loading()
loadedOptionAnchor.value += OPTIONS_PAGE_COUNT
loadedOptionAnchor.value = Math.min(loadedOptionAnchor.value, options.value.length)
renderedOptions.value = options.value.slice(0, loadedOptionAnchor.value)
if (loadedOptionAnchor.value === options.value.length) {
return $state.complete()
}
$state.loaded()
}
</script> </script>
<template> <template>
<div class="w-full"> <div class="w-full">
<div <div
class="overflow-x-auto scrollbar-thin-dull" ref="optionsWrapperDomRef"
class="nc-col-option-select-option overflow-x-auto scrollbar-thin-dull"
:style="{ :style="{
maxHeight: 'calc(min(30vh, 250px))', maxHeight: 'calc(min(30vh, 250px))',
}" }"
> >
<InfiniteLoading v-if="isReverseLazyLoad" v-bind="$attrs" @infinite="loadListDataReverse">
<template #spinner>
<div class="flex flex-row w-full justify-center mt-2">
<GeneralLoader />
</div>
</template>
<template #complete>
<span></span>
</template>
</InfiniteLoading>
<Draggable :list="renderedOptions" item-key="id" handle=".nc-child-draggable-icon" @change="syncOptions"> <Draggable :list="renderedOptions" item-key="id" handle=".nc-child-draggable-icon" @change="syncOptions">
<template #item="{ element, index }"> <template #item="{ element, index }">
<div class="flex py-1 items-center nc-select-option"> <div class="flex py-1 items-center nc-select-option">
@ -245,9 +322,8 @@ const loadListData = async ($state: any) => {
</a-dropdown> </a-dropdown>
<a-input <a-input
ref="inputs"
v-model:value="element.title" v-model:value="element.title"
class="caption !rounded-lg" class="caption !rounded-lg nc-select-col-option-select-option"
:data-testid="`select-column-option-input-${index}`" :data-testid="`select-column-option-input-${index}`"
:disabled="element.status === 'remove'" :disabled="element.status === 'remove'"
@keydown.enter.prevent="element.title?.trim() && addNewOption()" @keydown.enter.prevent="element.title?.trim() && addNewOption()"
@ -273,7 +349,7 @@ const loadListData = async ($state: any) => {
</div> </div>
</template> </template>
</Draggable> </Draggable>
<InfiniteLoading v-bind="$attrs" @infinite="loadListData"> <InfiniteLoading v-if="!isReverseLazyLoad" v-bind="$attrs" @infinite="loadListData">
<template #spinner> <template #spinner>
<div class="flex flex-row w-full justify-center mt-2"> <div class="flex flex-row w-full justify-center mt-2">
<GeneralLoader /> <GeneralLoader />

9
packages/nc-gui/components/smartsheet/details/Erd.vue

@ -23,6 +23,13 @@ const indicator = h(LoadingOutlined, {
<a-spin size="large" :indicator="indicator" /> <a-spin size="large" :indicator="indicator" />
</div> </div>
<LazyErdView v-else :table="activeTable" :source-id="activeTable?.source_id" :show-all-columns="false" /> <Suspense v-else>
<LazyErdView :table="activeTable" :source-id="activeTable?.source_id" :show-all-columns="false" />
<template #fallback>
<div class="h-full w-full flex flex-col justify-center items-center mt-28">
<a-spin size="large" :indicator="indicator" />
</div>
</template>
</Suspense>
</div> </div>
</template> </template>

11
packages/nc-gui/components/smartsheet/details/Fields.vue

@ -43,6 +43,8 @@ const moveOps = ref<moveOp[]>([])
const visibilityOps = ref<fieldsVisibilityOps[]>([]) const visibilityOps = ref<fieldsVisibilityOps[]>([])
const fieldsListWrapperDomRef = ref<HTMLElement>()
const { const {
fields: viewFields, fields: viewFields,
toggleFieldVisibility, toggleFieldVisibility,
@ -192,6 +194,13 @@ const addField = (field?: TableExplorerColumn, before = false) => {
setFieldMoveHook(field, before) setFieldMoveHook(field, before)
} }
changeField({}) changeField({})
// Scroll to the bottom of the list for new field add
setTimeout(() => {
if (!field && !before && fieldsListWrapperDomRef.value) {
fieldsListWrapperDomRef.value.scrollTop = fieldsListWrapperDomRef.value.scrollHeight
}
}, 100)
} }
const displayColumn = computed(() => { const displayColumn = computed(() => {
@ -626,7 +635,7 @@ onMounted(async () => {
</div> </div>
</div> </div>
<div class="flex flex-row rounded-lg border-1 border-gray-200"> <div class="flex flex-row rounded-lg border-1 border-gray-200">
<div class="nc-scrollbar-md !overflow-auto w-full flex-grow-1 nc-fields-height"> <div ref="fieldsListWrapperDomRef" class="nc-scrollbar-md !overflow-auto w-full flex-grow-1 nc-fields-height">
<Draggable v-model="fields" item-key="id" @change="onMove($event)"> <Draggable v-model="fields" item-key="id" @change="onMove($event)">
<template #item="{ element: field }"> <template #item="{ element: field }">
<div <div

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

@ -8,7 +8,7 @@ const { loadCommentsAndLogs, commentsAndLogs, saveComment: _saveComment, comment
const commentsWrapperEl = ref<HTMLDivElement>() const commentsWrapperEl = ref<HTMLDivElement>()
const { user } = useGlobal() const { user, appInfo } = useGlobal()
const tab = ref<'comments' | 'audits'>('comments') const tab = ref<'comments' | 'audits'>('comments')
@ -98,6 +98,12 @@ const saveComment = async () => {
watch(commentsWrapperEl, () => { watch(commentsWrapperEl, () => {
scrollComments() scrollComments()
}) })
const onClickAudit = () => {
if (appInfo.value.ee) return
tab.value = 'audits'
}
</script> </script>
<template> <template>
@ -108,7 +114,7 @@ watch(commentsWrapperEl, () => {
v-e="['c:row-expand:comment']" v-e="['c:row-expand:comment']"
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg" class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
:class="{ :class="{
'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'comments', 'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'comments' || appInfo.ee,
}" }"
@click="tab = 'comments'" @click="tab = 'comments'"
> >
@ -117,13 +123,29 @@ watch(commentsWrapperEl, () => {
Comments Comments
</div> </div>
</div> </div>
<NcTooltip v-if="appInfo.ee" class="tab flex-1">
<template #title>
<span class="!text-base"> Coming soon </span>
</template>
<div
v-e="['c:row-expand:audit']"
class="flex-1 px-4 py-2 transition-all text-gray-400 cursor-not-allowed bg-gray-50 rounded-lg"
@click="onClickAudit"
>
<div class="tab-title nc-tab select-none">
<MdiFileDocumentOutline class="h-4 w-4" />
Audits
</div>
</div>
</NcTooltip>
<div <div
v-else
v-e="['c:row-expand:audit']" v-e="['c:row-expand:audit']"
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg" class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
:class="{ :class="{
'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'audits', 'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'audits',
}" }"
@click="tab = 'audits'" @click="onClickAudit"
> >
<div class="tab-title nc-tab"> <div class="tab-title nc-tab">
<MdiFileDocumentOutline class="h-4 w-4" /> <MdiFileDocumentOutline class="h-4 w-4" />
@ -135,7 +157,7 @@ watch(commentsWrapperEl, () => {
<div <div
class="h-[calc(100%-4rem)]" class="h-[calc(100%-4rem)]"
:class="{ :class="{
'pb-2': tab !== 'comments', 'pb-2': tab !== 'comments' && !appInfo.ee,
}" }"
> >
<div v-if="tab === 'comments'" class="flex flex-col h-full"> <div v-if="tab === 'comments'" class="flex flex-col h-full">
@ -251,6 +273,9 @@ watch(commentsWrapperEl, () => {
</template> </template>
<style scoped> <style scoped>
.tab {
@apply max-w-1/2;
}
.tab .tab-title { .tab .tab-title {
@apply min-w-0 flex justify-center gap-2 font-semibold items-center; @apply min-w-0 flex justify-center gap-2 font-semibold items-center;
word-break: 'keep-all'; word-break: 'keep-all';

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

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TableType, ViewType } from 'nocodb-sdk' import type { TableType, ViewType } from 'nocodb-sdk'
import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import MdiChevronDown from '~icons/mdi/chevron-down' import MdiChevronDown from '~icons/mdi/chevron-down'
@ -47,6 +47,8 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev']) const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const { activeView } = storeToRefs(useViewsStore())
const key = ref(0) const key = ref(0)
const wrapper = ref() const wrapper = ref()
@ -86,6 +88,8 @@ const { isUIAllowed } = useRoles()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
// override cell click hook to avoid unexpected behavior at form fields // override cell click hook to avoid unexpected behavior at form fields
provide(CellClickHookInj, undefined) provide(CellClickHookInj, undefined)
@ -184,7 +188,16 @@ const save = async () => {
await syncLTARRefs(data) await syncLTARRefs(data)
reloadTrigger?.trigger() reloadTrigger?.trigger()
} else { } else {
await _save() let kanbanClbk
if (activeView.value?.type === ViewTypes.KANBAN) {
kanbanClbk = (row: any, isNewRow: boolean) => {
addOrEditStackRow(row, isNewRow)
}
}
await _save(undefined, undefined, {
kanbanClbk,
})
reloadTrigger?.trigger() reloadTrigger?.trigger()
} }
isUnsavedFormExist.value = false isUnsavedFormExist.value = false
@ -389,7 +402,7 @@ const onDeleteRowClick = () => {
const onConfirmDeleteRowClick = async () => { const onConfirmDeleteRowClick = async () => {
showDeleteRowModal.value = false showDeleteRowModal.value = false
await deleteRowById(primaryKey.value) await deleteRowById(primaryKey.value)
message.success('Row deleted') message.success('Record deleted')
reloadTrigger.trigger() reloadTrigger.trigger()
onClose() onClose()
showDeleteRowModal.value = false showDeleteRowModal.value = false

17
packages/nc-gui/components/smartsheet/grid/GroupBy.vue

@ -18,6 +18,7 @@ const props = defineProps<{
viewWidth?: number viewWidth?: number
scrollLeft?: number scrollLeft?: number
fullPage?: boolean
depth?: number depth?: number
maxDepth?: number maxDepth?: number
@ -40,6 +41,12 @@ const wrapper = ref<HTMLElement | undefined>()
const scrollable = ref<HTMLElement | undefined>() const scrollable = ref<HTMLElement | undefined>()
const tableHeader = ref<HTMLElement | undefined>()
const fullPage = computed<boolean>(() => {
return props.fullPage ?? (tableHeader.value?.offsetWidth ?? 0) > (props.viewWidth ?? 0)
})
const _activeGroupKeys = ref<string[] | string>() const _activeGroupKeys = ref<string[] | string>()
const activeGroups = computed<string[]>(() => { const activeGroups = computed<string[]>(() => {
@ -149,7 +156,7 @@ const onScroll = (e: Event) => {
style="background-color: #f9f9fa; border-color: #e7e7e9; border-bottom-width: 1px" style="background-color: #f9f9fa; border-color: #e7e7e9; border-bottom-width: 1px"
:style="{ 'padding-left': `${(maxDepth || 1) * 13}px` }" :style="{ 'padding-left': `${(maxDepth || 1) * 13}px` }"
></div> ></div>
<Table class="mb-2" :data="[]" :header-only="true" /> <Table ref="tableHeader" class="mb-2" :data="[]" :header-only="true" />
</div> </div>
<div :class="{ 'px-[12px]': vGroup.root === true }"> <div :class="{ 'px-[12px]': vGroup.root === true }">
<a-collapse <a-collapse
@ -258,11 +265,12 @@ const onScroll = (e: Event) => {
:row-height="rowHeight" :row-height="rowHeight"
:redistribute-rows="redistributeRows" :redistribute-rows="redistributeRows"
:expand-form="expandForm" :expand-form="expandForm"
:pagination-fixed-size="props.viewWidth" :pagination-fixed-size="fullPage ? props.viewWidth : undefined"
:pagination-hide-sidebars="true" :pagination-hide-sidebars="true"
:scroll-left="props.scrollLeft || _scrollLeft" :scroll-left="props.scrollLeft || _scrollLeft"
:view-width="viewWidth" :view-width="viewWidth"
:scrollable="scrollable" :scrollable="scrollable"
:full-page="fullPage"
/> />
<GroupBy <GroupBy
v-else v-else
@ -277,6 +285,7 @@ const onScroll = (e: Event) => {
:view-width="viewWidth" :view-width="viewWidth"
:depth="_depth + 1" :depth="_depth + 1"
:scroll-left="scrollBump" :scroll-left="scrollBump"
:full-page="fullPage"
/> />
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
@ -288,6 +297,7 @@ const onScroll = (e: Event) => {
v-model:pagination-data="vGroup.paginationData" v-model:pagination-data="vGroup.paginationData"
align-count-on-right align-count-on-right
custom-label="groups" custom-label="groups"
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)" :change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 12px 12px !important;' : ''}`" :style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 12px 12px !important;' : ''}`"
></LazySmartsheetPagination> ></LazySmartsheetPagination>
@ -296,10 +306,11 @@ const onScroll = (e: Event) => {
v-model:pagination-data="vGroup.paginationData" v-model:pagination-data="vGroup.paginationData"
align-count-on-right align-count-on-right
custom-label="groups" custom-label="groups"
show-api-timing
:change-page="(p: number) => groupWrapperChangePage(p, vGroup)" :change-page="(p: number) => groupWrapperChangePage(p, vGroup)"
:hide-sidebars="true" :hide-sidebars="true"
:style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 12px 12px !important;' : ''}margin-left: ${scrollBump}px;`" :style="`${props.depth && props.depth > 0 ? 'border-radius: 0 0 12px 12px !important;' : ''}margin-left: ${scrollBump}px;`"
:fixed-size="props.viewWidth" :fixed-size="fullPage ? props.viewWidth : undefined"
></LazySmartsheetPagination> ></LazySmartsheetPagination>
</div> </div>
</template> </template>

89
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import axios from 'axios'
import { nextTick } from '@vue/runtime-core' import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } from 'nocodb-sdk'
import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { UITypes, ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
@ -122,7 +123,7 @@ const reloadViewDataHook = inject(ReloadViewDataHookInj, createEventHook())
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook()) const openNewRecordFormHook = inject(OpenNewRecordFormHookInj, createEventHook())
useViewColumns(view, meta, () => reloadViewDataHook.trigger()) const { isViewColumnsLoading } = useViewColumns(view, meta, () => reloadViewDataHook.trigger())
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
@ -383,11 +384,19 @@ const gridWrapperClass = computed<string>(() => {
return classes.join(' ') return classes.join(' ')
}) })
const dummyDataForLoading = computed(() => { const dummyColumnDataForLoading = computed(() => {
let length = fields.value?.length ?? 40
length = length || 40
return Array.from({ length: length + 1 }).map(() => ({}))
})
const dummyRowDataForLoading = computed(() => {
return Array.from({ length: 40 }).map(() => ({})) return Array.from({ length: 40 }).map(() => ({}))
}) })
const showSkeleton = computed(() => disableSkeleton !== true && (isViewDataLoading.value || isPaginationLoading.value)) const showSkeleton = computed(
() => disableSkeleton !== true && (isViewDataLoading.value || isPaginationLoading.value || isViewColumnsLoading.value),
)
// #Grid // #Grid
@ -405,7 +414,8 @@ const closeAddColumnDropdown = (scrollToLastCol = false) => {
preloadColumn.value = {} preloadColumn.value = {}
if (scrollToLastCol) { if (scrollToLastCol) {
setTimeout(() => { setTimeout(() => {
const lastAddNewRowHeader = tableHeadEl.value?.querySelector('th:last-child') const lastAddNewRowHeader =
tableHeadEl.value?.querySelector('.nc-grid-add-edit-column') ?? tableHeadEl.value?.querySelector('th:last-child')
if (lastAddNewRowHeader) { if (lastAddNewRowHeader) {
lastAddNewRowHeader.scrollIntoView({ behavior: 'smooth' }) lastAddNewRowHeader.scrollIntoView({ behavior: 'smooth' })
} }
@ -884,7 +894,7 @@ const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?
} }
// #Grid Resize // #Grid Resize
const { updateGridViewColumn, resizingColWidth, resizingCol } = useGridViewColumnOrThrow() const { updateGridViewColumn, gridViewCols, resizingColOldWith } = useGridViewColumnOrThrow()
const onresize = (colID: string | undefined, event: any) => { const onresize = (colID: string | undefined, event: any) => {
if (!colID) return if (!colID) return
@ -893,8 +903,12 @@ const onresize = (colID: string | undefined, event: any) => {
const onXcResizing = (cn: string | undefined, event: any) => { const onXcResizing = (cn: string | undefined, event: any) => {
if (!cn) return if (!cn) return
resizingCol.value = cn gridViewCols.value[cn].width = `${event.detail}`
resizingColWidth.value = event.detail }
const onXcStartResizing = (cn: string | undefined, event: any) => {
if (!cn) return
resizingColOldWith.value = event.detail
} }
const loadColumn = (title: string, tp: string, colOptions?: any) => { const loadColumn = (title: string, tp: string, colOptions?: any) => {
@ -1059,6 +1073,22 @@ onBeforeUnmount(async () => {
reloadViewDataHook?.on(reloadViewDataHandler) reloadViewDataHook?.on(reloadViewDataHandler)
openNewRecordFormHook?.on(openNewRecordHandler) openNewRecordFormHook?.on(openNewRecordHandler)
// TODO: Use CSS animations
const showLoaderAfterDelay = ref(false)
watch([isViewDataLoading, showSkeleton, isPaginationLoading], () => {
if (!isViewDataLoading.value && !showSkeleton.value && !isPaginationLoading.value) {
showLoaderAfterDelay.value = false
return
}
showLoaderAfterDelay.value = false
setTimeout(() => {
showLoaderAfterDelay.value = true
}, 500)
})
// #Watchers // #Watchers
// reset context menu target on hide // reset context menu target on hide
@ -1099,8 +1129,10 @@ watch(
try { try {
await loadData?.() await loadData?.()
} catch (e) { } catch (e) {
console.log(e) if (!axios.isCancel(e)) {
message.error(t('msg.errorLoadingData')) console.log(e)
message.error(t('msg.errorLoadingData'))
}
} finally { } finally {
isViewDataLoading.value = false isViewDataLoading.value = false
} }
@ -1163,7 +1195,7 @@ const loaderText = computed(() => {
<div class="flex flex-col" :class="`${headerOnly !== true ? 'h-full w-full' : ''}`"> <div class="flex flex-col" :class="`${headerOnly !== true ? 'h-full w-full' : ''}`">
<div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 relative" :class="gridWrapperClass"> <div ref="gridWrapper" class="nc-grid-wrapper min-h-0 flex-1 relative" :class="gridWrapperClass">
<div <div
v-show="showSkeleton && !isPaginationLoading" v-show="showSkeleton && !isPaginationLoading && showLoaderAfterDelay"
class="flex items-center justify-center absolute l-0 t-0 w-full h-full z-10 pb-10" class="flex items-center justify-center absolute l-0 t-0 w-full h-full z-10 pb-10"
> >
<div class="flex flex-col justify-center gap-2"> <div class="flex flex-col justify-center gap-2">
@ -1187,11 +1219,11 @@ const loaderText = computed(() => {
@contextmenu="showContextMenu" @contextmenu="showContextMenu"
> >
<thead v-show="hideHeader !== true" ref="tableHeadEl"> <thead v-show="hideHeader !== true" ref="tableHeadEl">
<tr v-if="showSkeleton && isPaginationLoading"> <tr v-if="isViewColumnsLoading">
<td <td
v-for="(col, colIndex) of dummyDataForLoading" v-for="(col, colIndex) of dummyColumnDataForLoading"
:key="colIndex" :key="colIndex"
class="!bg-gray-50 h-full" class="!bg-gray-50 h-full border-b-1 border-r-1"
:class="{ 'min-w-50': colIndex !== 0, 'min-w-21.25': colIndex === 0 }" :class="{ 'min-w-50': colIndex !== 0, 'min-w-21.25': colIndex === 0 }"
> >
<a-skeleton <a-skeleton
@ -1203,7 +1235,7 @@ const loaderText = computed(() => {
/> />
</td> </td>
</tr> </tr>
<tr v-show="!isPaginationLoading" class="nc-grid-header"> <tr v-show="!isViewColumnsLoading" class="nc-grid-header">
<th class="w-[85px] min-w-[85px]" data-testid="grid-id-column" @dblclick="() => {}"> <th class="w-[85px] min-w-[85px]" data-testid="grid-id-column" @dblclick="() => {}">
<div class="w-full h-full flex pl-5 pr-1 items-center" data-testid="nc-check-all"> <div class="w-full h-full flex pl-5 pr-1 items-center" data-testid="nc-check-all">
<template v-if="!readOnly"> <template v-if="!readOnly">
@ -1228,9 +1260,14 @@ const loaderText = computed(() => {
v-xc-ver-resize v-xc-ver-resize
:data-col="col.id" :data-col="col.id"
:data-title="col.title" :data-title="col.title"
:style="{
'min-width': gridViewCols[col.id]?.width || '200px',
'max-width': gridViewCols[col.id]?.width || '200px',
'width': gridViewCols[col.id]?.width || '200px',
}"
@xcstartresizing="onXcStartResizing(col.id, $event)"
@xcresize="onresize(col.id, $event)" @xcresize="onresize(col.id, $event)"
@xcresizing="onXcResizing(col.title, $event)" @xcresizing="onXcResizing(col.id, $event)"
@xcresized="resizingCol = null"
> >
<div class="w-full h-full flex items-center"> <div class="w-full h-full flex items-center">
<LazySmartsheetHeaderVirtualCell <LazySmartsheetHeaderVirtualCell
@ -1352,10 +1389,11 @@ const loaderText = computed(() => {
</thead> </thead>
<tbody v-if="headerOnly !== true" ref="tableBodyEl"> <tbody v-if="headerOnly !== true" ref="tableBodyEl">
<template v-if="showSkeleton"> <template v-if="showSkeleton">
<tr v-for="(row, rowIndex) of dummyDataForLoading" :key="rowIndex"> <tr v-for="(row, rowIndex) of dummyRowDataForLoading" :key="rowIndex">
<td <td
v-for="(col, colIndex) of dummyDataForLoading" v-for="(col, colIndex) of dummyColumnDataForLoading"
:key="colIndex" :key="colIndex"
class="border-b-1 border-r-1"
:class="{ 'min-w-50': colIndex !== 0, 'min-w-21.25': colIndex === 0 }" :class="{ 'min-w-50': colIndex !== 0, 'min-w-21.25': colIndex === 0 }"
></td> ></td>
</tr> </tr>
@ -1456,6 +1494,11 @@ const loaderText = computed(() => {
hasEditPermission && hasEditPermission &&
isCellSelected(rowIndex, colIndex), isCellSelected(rowIndex, colIndex),
}" }"
:style="{
'min-width': gridViewCols[columnObj.id]?.width || '200px',
'max-width': gridViewCols[columnObj.id]?.width || '200px',
'width': gridViewCols[columnObj.id]?.width || '200px',
}"
:data-testid="`cell-${columnObj.title}-${rowIndex}`" :data-testid="`cell-${columnObj.title}-${rowIndex}`"
:data-key="`data-key-${rowIndex}-${columnObj.id}`" :data-key="`data-key-${rowIndex}-${columnObj.id}`"
:data-col="columnObj.id" :data-col="columnObj.id"
@ -1639,8 +1682,9 @@ const loaderText = computed(() => {
v-if="headerOnly !== true" v-if="headerOnly !== true"
:key="isMobileMode" :key="isMobileMode"
v-model:pagination-data="paginationDataRef" v-model:pagination-data="paginationDataRef"
show-api-timing :show-api-timing="!isGroupBy"
align-count-on-right align-count-on-right
:align-left="isGroupBy"
:change-page="changePage" :change-page="changePage"
:hide-sidebars="paginationStyleRef?.hideSidebars === true" :hide-sidebars="paginationStyleRef?.hideSidebars === true"
:fixed-size="paginationStyleRef?.fixedSize" :fixed-size="paginationStyleRef?.fixedSize"
@ -1949,4 +1993,11 @@ tbody tr:hover {
.nc-fill-handle:focus { .nc-fill-handle:focus {
@apply w-[8px] h-[8px] mt-[-5px] ml-[-5px]; @apply w-[8px] h-[8px] mt-[-5px] ml-[-5px];
} }
:deep(.ant-skeleton-input) {
@apply rounded text-gray-100 !bg-gray-100 !bg-opacity-65;
animation: slow-show-1 5s ease 5s forwards;
}
</style> </style>
<style lang="scss"></style>

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

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ColumnType, FilterType } from 'nocodb-sdk' import type { ColumnType, FilterType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk' import { PlanLimitTypes, UITypes } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
AllFiltersInj,
MetaInj, MetaInj,
ReloadViewDataHookInj, ReloadViewDataHookInj,
comparisonOpList, comparisonOpList,
@ -56,6 +57,8 @@ const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)! const reloadDataHook = inject(ReloadViewDataHookInj)!
const isPublic = inject(IsPublicInj, ref(false))
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { nestedFilters } = useSmartsheetStoreOrThrow() const { nestedFilters } = useSmartsheetStoreOrThrow()
@ -83,6 +86,8 @@ const {
webHook.value, webHook.value,
) )
const { getPlanLimit } = useWorkspace()
const localNestedFilters = ref() const localNestedFilters = ref()
const wrapperDomRef = ref<HTMLElement>() const wrapperDomRef = ref<HTMLElement>()
@ -183,13 +188,22 @@ watch(
}, },
) )
const allFilters: Ref<Record<string, FilterType[]>> = inject(AllFiltersInj, ref({}))
watch( watch(
() => nonDeletedFilters.value.length, () => nonDeletedFilters.value.length,
(length: number) => { (length: number) => {
allFilters.value[parentId?.value ?? 'root'] = [...nonDeletedFilters.value]
emit('update:filtersLength', length ?? 0) emit('update:filtersLength', length ?? 0)
}, },
) )
const filtersCount = computed(() => {
return Object.values(allFilters.value).reduce((acc, filters) => {
return acc + filters.filter((el) => !el.is_group).length
}, 0)
})
const applyChanges = async (hookId?: string, _nested = false) => { const applyChanges = async (hookId?: string, _nested = false) => {
await sync(hookId, _nested) await sync(hookId, _nested)
@ -299,6 +313,10 @@ onMounted(() => {
onMounted(async () => { onMounted(async () => {
await loadBtLookupTypes() await loadBtLookupTypes()
}) })
onBeforeUnmount(() => {
if (parentId.value) delete allFilters.value[parentId.value]
})
</script> </script>
<template> <template>
@ -471,23 +489,44 @@ onMounted(async () => {
</template> </template>
</div> </div>
<div ref="addFiltersRowDomRef" class="flex gap-2"> <template v-if="isEeUI && !isPublic">
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()"> <div v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)" ref="addFiltersRowDomRef" class="flex gap-2">
<div class="flex items-center gap-1"> <NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()">
<component :is="iconMap.plus" /> <div class="flex items-center gap-1">
<!-- Add Filter --> <component :is="iconMap.plus" />
{{ $t('activity.addFilter') }} <!-- Add Filter -->
</div> {{ $t('activity.addFilter') }}
</NcButton> </div>
</NcButton>
<NcButton v-if="!webHook && nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1"> <NcButton v-if="!webHook && nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()">
<!-- Add Filter Group --> <div class="flex items-center gap-1">
<component :is="iconMap.plus" /> <!-- Add Filter Group -->
{{ $t('activity.addFilterGroup') }} <component :is="iconMap.plus" />
</div> {{ $t('activity.addFilterGroup') }}
</NcButton> </div>
</div> </NcButton>
</div>
</template>
<template v-else>
<div ref="addFiltersRowDomRef" class="flex gap-2">
<NcButton size="small" type="text" class="!text-brand-500" @click.stop="addFilter()">
<div class="flex items-center gap-1">
<component :is="iconMap.plus" />
<!-- Add Filter -->
{{ $t('activity.addFilter') }}
</div>
</NcButton>
<NcButton v-if="!webHook && nestedLevel < 5" type="text" size="small" @click.stop="addFilterGroup()">
<div class="flex items-center gap-1">
<!-- Add Filter Group -->
<component :is="iconMap.plus" />
{{ $t('activity.addFilterGroup') }}
</div>
</NcButton>
</div>
</template>
<div <div
v-if="!filters.length" v-if="!filters.length"
class="flex flex-row text-gray-400 mt-2" class="flex flex-row text-gray-400 mt-2"

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

@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
ActiveViewInj, ActiveViewInj,
AllFiltersInj,
IsLockedInj, IsLockedInj,
computed, computed,
iconMap,
inject, inject,
ref, ref,
useGlobal, useGlobal,
@ -10,7 +12,6 @@ import {
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useViewFilters, useViewFilters,
watch, watch,
iconMap,
} from '#imports' } from '#imports'
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
@ -48,6 +49,10 @@ watch(
const open = ref(false) const open = ref(false)
const allFilters = ref({})
provide(AllFiltersInj, allFilters)
useMenuCloseOnEsc(open) useMenuCloseOnEsc(open)
</script> </script>

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

@ -346,7 +346,7 @@ useMenuCloseOnEsc(open)
<div class="pt-0.25 w-full bg-gray-50"></div> <div class="pt-0.25 w-full bg-gray-50"></div>
</div> </div>
<div class="flex flex-col py-1 nc-scrollbar-md max-h-[47.5vh] pr-5"> <div class="flex flex-col my-1.5 nc-scrollbar-md max-h-[47.5vh] pr-5">
<div class="nc-fields-list"> <div class="nc-fields-list">
<div <div
v-if="!fields?.filter((el) => el.title.toLowerCase().includes(filterQuery.toLowerCase())).length" v-if="!fields?.filter((el) => el.title.toLowerCase().includes(filterQuery.toLowerCase())).length"

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

@ -275,7 +275,7 @@ onMounted(async () => {
type="text" type="text"
@click.stop="removeFieldFromGroupBy(i)" @click.stop="removeFieldFromGroupBy(i)"
> >
<GeneralIcon icon="delete" class="" /> <component :is="iconMap.deleteListItem" />
</NcButton> </NcButton>
</a-tooltip> </a-tooltip>
</template> </template>

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

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk' import { PlanLimitTypes, RelationTypes, UITypes, isLinksOrLTAR, isSystemColumn } from 'nocodb-sdk'
import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk' import type { ColumnType, LinkToAnotherRecordType } from 'nocodb-sdk'
import { import {
ActiveViewInj, ActiveViewInj,
@ -10,6 +10,7 @@ import {
getSortDirectionOptions, getSortDirectionOptions,
iconMap, iconMap,
inject, inject,
isEeUI,
ref, ref,
useMenuCloseOnEsc, useMenuCloseOnEsc,
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
@ -21,6 +22,7 @@ const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref()) const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))
const reloadDataHook = inject(ReloadViewDataHookInj) const reloadDataHook = inject(ReloadViewDataHookInj)
const isPublic = inject(IsPublicInj, ref(false))
const { eventBus } = useSmartsheetStoreOrThrow() const { eventBus } = useSmartsheetStoreOrThrow()
@ -32,6 +34,8 @@ const showCreateSort = ref(false)
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { getPlanLimit } = useWorkspace()
eventBus.on((event) => { eventBus.on((event) => {
if (event === SmartsheetStoreEvents.SORT_RELOAD) { if (event === SmartsheetStoreEvents.SORT_RELOAD) {
loadSorts() loadSorts()
@ -181,13 +185,31 @@ watch(open, () => {
:trigger="['click']" :trigger="['click']"
overlay-class-name="nc-toolbar-dropdown" overlay-class-name="nc-toolbar-dropdown"
> >
<NcButton v-e="['c:sort:add']" class="!text-brand-500" type="text" size="small" @click.stop="showCreateSort = true"> <template v-if="isEeUI && !isPublic">
<div class="flex gap-1 items-center"> <NcButton
<component :is="iconMap.plus" /> v-if="sorts.length < getPlanLimit(PlanLimitTypes.SORT_LIMIT)"
<!-- Add Sort Option --> v-e="['c:sort:add']"
{{ $t('activity.addSort') }} class="!text-brand-500"
</div> type="text"
</NcButton> size="small"
@click.stop="showCreateSort = true"
>
<div class="flex gap-1 items-center">
<component :is="iconMap.plus" />
<!-- Add Sort Option -->
{{ $t('activity.addSort') }}
</div>
</NcButton>
</template>
<template v-else>
<NcButton v-e="['c:sort:add']" class="!text-brand-500" type="text" size="small" @click.stop="showCreateSort = true">
<div class="flex gap-1 items-center">
<component :is="iconMap.plus" />
<!-- Add Sort Option -->
{{ $t('activity.addSort') }}
</div>
</NcButton>
</template>
<template #overlay> <template #overlay>
<SmartsheetToolbarCreateSort :is-parent-open="showCreateSort" @created="addSort" /> <SmartsheetToolbarCreateSort :is-parent-open="showCreateSort" @created="addSort" />
</template> </template>

7
packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { ViewTypes } from 'nocodb-sdk'
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { openedViewsTab, activeView } = storeToRefs(useViewsStore()) const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
@ -16,9 +14,8 @@ const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
<div <div
class="ml-0.25 flex flex-row font-medium items-center border-gray-50 transition-all duration-100" class="ml-0.25 flex flex-row font-medium items-center border-gray-50 transition-all duration-100"
:class="{ :class="{
'min-w-36/100 max-w-36/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && isLeftSidebarOpen, 'min-w-36/100 max-w-36/100': !isMobileMode && isLeftSidebarOpen,
'min-w-39/100 max-w-39/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && !isLeftSidebarOpen, 'min-w-39/100 max-w-39/100': !isMobileMode && !isLeftSidebarOpen,
'min-w-1/4 max-w-1/4': !isMobileMode && activeView?.type === ViewTypes.KANBAN,
'w-2/3 text-base ml-1.5': isMobileMode, 'w-2/3 text-base ml-1.5': isMobileMode,
'!max-w-3/4': isSharedBase && !isMobileMode, '!max-w-3/4': isSharedBase && !isMobileMode,
}" }"

12
packages/nc-gui/components/smartsheet/topbar/SelectMode.vue

@ -3,7 +3,17 @@ import { storeToRefs, useViewsStore } from '#imports'
const { openedViewsTab, activeView } = storeToRefs(useViewsStore()) const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
const { isUIAllowed } = useRoles()
const { onViewsTabChange } = useViewsStore() const { onViewsTabChange } = useViewsStore()
const onClickDetails = () => {
if (isUIAllowed('fieldAdd')) {
onViewsTabChange('field')
} else {
onViewsTabChange('relation')
}
}
</script> </script>
<template> <template>
@ -26,7 +36,7 @@ const { onViewsTabChange } = useViewsStore()
:class="{ :class="{
active: openedViewsTab !== 'view', active: openedViewsTab !== 'view',
}" }"
@click="onViewsTabChange('field')" @click="onClickDetails"
> >
<GeneralIcon <GeneralIcon
icon="erd" icon="erd"

7
packages/nc-gui/components/template/Editor.vue

@ -128,10 +128,7 @@ const validators = computed(() =>
hasSelectColumn.value[tableIdx] = false hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => { table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [ acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator(), fieldLengthValidator()]
fieldRequiredValidator(),
fieldLengthValidator(base.value?.sources?.[0].type || ClientType.MYSQL),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()] acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) { if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true hasSelectColumn.value[tableIdx] = true
@ -434,7 +431,7 @@ async function importTemplate() {
let input = row[col.srcCn] let input = row[col.srcCn]
// parse potential boolean values // parse potential boolean values
if (v.uidt === UITypes.Checkbox) { if (v.uidt === UITypes.Checkbox) {
input = input.replace(/["']/g, '').toLowerCase().trim() input = input ? input.replace(/["']/g, '').toLowerCase().trim() : 'false'
if (input === 'false' || input === 'no' || input === 'n') { if (input === 'false' || input === 'no' || input === 'n') {
input = '0' input = '0'
} else if (input === 'true' || input === 'yes' || input === 'y') { } else if (input === 'true' || input === 'yes' || input === 'y') {

10
packages/nc-gui/components/virtual-cell/Links.vue

@ -77,6 +77,8 @@ const onAttachRecord = () => {
} }
const openChildList = () => { const openChildList = () => {
if (isUnderLookup.value) return
if (!isLocked.value) { if (!isLocked.value) {
childListDlg.value = true childListDlg.value = true
} }
@ -98,6 +100,12 @@ const localCellValue = computed<any[]>(() => {
} }
return [] return []
}) })
const openListDlg = () => {
if (isUnderLookup.value) return
listItemsDlg.value = true
}
</script> </script>
<template> <template>
@ -120,7 +128,7 @@ const localCellValue = computed<any[]>(() => {
<MdiPlus <MdiPlus
v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm" v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus" class="select-none !text-md text-gray-700 nc-action-icon nc-plus"
@click.stop="listItemsDlg = true" @click.stop="openListDlg"
/> />
</div> </div>

5
packages/nc-gui/components/virtual-cell/components/ListChildItems.vue

@ -41,6 +41,8 @@ const injectedColumn = inject(ColumnInj, ref())
const readonly = inject(ReadonlyInj, ref(false)) const readonly = inject(ReadonlyInj, ref(false))
const { isSharedBase } = storeToRefs(useBase())
const { const {
childrenList, childrenList,
childrenListCount, childrenListCount,
@ -165,6 +167,8 @@ const isDataExist = computed<boolean>(() => {
}) })
const linkOrUnLink = (rowRef: Record<string, string>, id: string) => { const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
if (isSharedBase.value) return
if (isPublic.value && !isForm.value) return if (isPublic.value && !isForm.value) return
if (isNew.value || isChildrenListLinked.value[parseInt(id)]) { if (isNew.value || isChildrenListLinked.value[parseInt(id)]) {
unlinkRow(rowRef, parseInt(id)) unlinkRow(rowRef, parseInt(id))
@ -345,6 +349,7 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
new: true, new: true,
}, },
}" }"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
use-meta-fields use-meta-fields
/> />
</Suspense> </Suspense>

4
packages/nc-gui/components/virtual-cell/components/ListItem.vue

@ -38,6 +38,8 @@ const row = useVModel(props, 'row')
const isPublic = inject(IsPublicInj, ref(false)) const isPublic = inject(IsPublicInj, ref(false))
const readonly = inject(ReadonlyInj, ref(false))
const { getPossibleAttachmentSrc } = useAttachment() const { getPossibleAttachmentSrc } = useAttachment()
interface Attachment { interface Attachment {
@ -151,7 +153,7 @@ const attachments: ComputedRef<Attachment[]> = computed(() => {
</div> </div>
</div> </div>
<NcButton <NcButton
v-if="!isForm && !isPublic" v-if="!isForm && !isPublic && !readonly"
v-e="['c:row-expand:open']" v-e="['c:row-expand:open']"
type="text" type="text"
size="lg" size="lg"

19
packages/nc-gui/components/virtual-cell/components/ListItems.vue

@ -25,6 +25,8 @@ const { isMobileMode } = useGlobal()
const injectedColumn = inject(ColumnInj) const injectedColumn = inject(ColumnInj)
const { isSharedBase } = storeToRefs(useBase())
const filterQueryRef = ref() const filterQueryRef = ref()
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
@ -162,6 +164,15 @@ watch(expandedFormDlg, () => {
onKeyStroke('Escape', () => { onKeyStroke('Escape', () => {
vModel.value = false vModel.value = false
}) })
const onClick = (refRow: any, id: string) => {
if (isSharedBase.value) return
if (isChildrenExcludedListLinked.value[Number.parseInt(id)]) {
unlinkRow(refRow, Number.parseInt(id))
} else {
linkRow(refRow, Number.parseInt(id))
}
}
</script> </script>
<template> <template>
@ -272,12 +283,7 @@ onKeyStroke('Escape', () => {
expandedFormDlg = true expandedFormDlg = true
} }
" "
@click=" @click="() => onClick(refRow, id)"
() => {
if (isChildrenExcludedListLinked[Number.parseInt(id)]) unlinkRow(refRow, Number.parseInt(id))
else linkRow(refRow, Number.parseInt(id))
}
"
/> />
</template> </template>
</div> </div>
@ -335,6 +341,7 @@ onKeyStroke('Escape', () => {
new: true, new: true,
}, },
}" }"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
:state="newRowState" :state="newRowState"
use-meta-fields use-meta-fields
/> />

44
packages/nc-gui/components/workspace/Billing.vue

@ -1,44 +0,0 @@
<script lang="ts" setup>
import { WorkspacePlan } from 'nocodb-sdk'
import { storeToRefs } from 'pinia'
import { extractSdkResponseErrorMsg } from '#imports'
const workspaceStore = useWorkspace()
const { upgradeActiveWorkspace } = workspaceStore
const { activeWorkspace } = storeToRefs(workspaceStore)
const isUpgrading = ref(false)
const upgradeWorkspace = async () => {
isUpgrading.value = true
try {
await upgradeActiveWorkspace()
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
isUpgrading.value = false
}
}
</script>
<template>
<div class="h-full w-full flex flex-col justify-center items-center">
<div class="mt-20 px-8 py-6 flex flex-col justify-center items-center gap-y-8 border-1 border-gray-100 rounded-md">
<template v-if="activeWorkspace.plan === WorkspacePlan.FREE">
<div class="flex text-xl font-medium">Upgrade your workspace</div>
<a-button
v-e="['c:workspace:settings:upgrade']"
type="primary"
size="large"
class="!rounded-md"
:loading="isUpgrading"
@click="upgradeWorkspace"
>Upgrade
</a-button>
</template>
<template v-else>
<div class="flex text-xl font-medium">Your workspace is upgraded</div>
</template>
</div>
</div>
</template>

11
packages/nc-gui/components/workspace/View.vue

@ -69,17 +69,6 @@ onMounted(() => {
</a-tab-pane> </a-tab-pane>
</template> </template>
<template v-if="isUIAllowed('workspaceBilling')">
<a-tab-pane key="billing" class="w-full">
<template #tab>
<div class="flex flex-row items-center px-2 pb-1 gap-x-1.5">
<MaterialSymbolsCreditCardOutline />
Billing
</div>
</template>
<WorkspaceBilling />
</a-tab-pane>
</template>
<template v-if="isUIAllowed('workspaceManage')"> <template v-if="isUIAllowed('workspaceManage')">
<a-tab-pane key="settings" class="w-full"> <a-tab-pane key="settings" class="w-full">
<template #tab> <template #tab>

2
packages/nc-gui/composables/useColumnCreateStore.ts

@ -126,7 +126,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
}) })
}, },
}, },
fieldLengthValidator(baseType.value || ClientType.MYSQL), fieldLengthValidator(),
], ],
uidt: [ uidt: [
{ {

9
packages/nc-gui/composables/useCopySharedBase.ts

@ -0,0 +1,9 @@
import { createSharedComposable, ref } from '#imports'
export const useCopySharedBase = createSharedComposable(() => {
const sharedBaseId = ref<string | null>(null)
return {
sharedBaseId,
}
})

2
packages/nc-gui/composables/useData.ts

@ -494,7 +494,7 @@ export function useData(args: {
if (res.message) { if (res.message) {
message.info( message.info(
`Row delete failed: ${`Unable to delete row with ID ${id} because of the following: `Record delete failed: ${`Unable to delete record with ID ${id} because of the following:
\n${res.message.join('\n')}.\n \n${res.message.join('\n')}.\n
Clear the data first & try again`})}`, Clear the data first & try again`})}`,
) )

19
packages/nc-gui/composables/useExpandedFormStore.ts

@ -153,7 +153,16 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
$e('a:row-expand:comment') $e('a:row-expand:comment')
} }
const save = async (ltarState: Record<string, any> = {}, undo = false) => { const save = async (
ltarState: Record<string, any> = {},
undo = false,
// TODO: Hack. Remove this when kanban injection store issue is resolved
{
kanbanClbk,
}: {
kanbanClbk?: (row: Row, isNewRow: boolean) => void
} = {},
) => {
let data let data
try { try {
const isNewRow = row.value.rowMeta?.new ?? false const isNewRow = row.value.rowMeta?.new ?? false
@ -266,13 +275,13 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
} }
} }
if (activeView.value?.type === ViewTypes.KANBAN) { if (activeView.value?.type === ViewTypes.KANBAN && kanbanClbk) {
const { addOrEditStackRow } = useKanbanViewStoreOrThrow() kanbanClbk(row.value, isNewRow)
addOrEditStackRow(row.value, isNewRow)
} }
changedColumns.value = new Set() changedColumns.value = new Set()
} catch (e: any) { } catch (e: any) {
console.error(e)
message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`) message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
} }
$e('a:row-expand:add') $e('a:row-expand:add')
@ -313,7 +322,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (res.message) { if (res.message) {
message.info( message.info(
`Row delete failed: ${`Unable to delete row with ID ${rowId} because of the following: `Record delete failed: ${`Unable to delete record with ID ${rowId} because of the following:
\n${res.message.join('\n')}.\n \n${res.message.join('\n')}.\n
Clear the data first & try again`})}`, Clear the data first & try again`})}`,
) )

6
packages/nc-gui/composables/useGlobal/actions.ts

@ -146,5 +146,9 @@ export function useGlobalActions(state: State): Actions {
return undefined return undefined
} }
return { signIn, signOut, refreshToken, loadAppInfo, setIsMobileMode, navigateToProject, getBaseUrl, ncNavigateTo } const getMainUrl = () => {
return undefined
}
return { signIn, signOut, refreshToken, loadAppInfo, setIsMobileMode, navigateToProject, getBaseUrl, ncNavigateTo, getMainUrl }
} }

1
packages/nc-gui/composables/useGlobal/types.ts

@ -78,6 +78,7 @@ export interface Actions {
viewId?: string viewId?: string
}) => void }) => void
getBaseUrl: (workspaceId: string) => string | undefined getBaseUrl: (workspaceId: string) => string | undefined
getMainUrl: (workspaceId: string) => string | undefined
} }
export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'> export type ReadonlyState = Readonly<Pick<State, 'token' | 'user'>> & Omit<State, 'token' | 'user'>

32
packages/nc-gui/composables/useGridViewColumn.ts

@ -1,11 +1,9 @@
import type { ColumnType, GridColumnReqType, GridColumnType, ViewType } from 'nocodb-sdk' import type { ColumnType, GridColumnReqType, GridColumnType, ViewType } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { IsPublicInj, computed, inject, ref, useMetas, useNuxtApp, useRoles, useStyleTag, useUndoRedo, watch } from '#imports' import { IsPublicInj, computed, inject, ref, useMetas, useNuxtApp, useRoles, useUndoRedo, watch } from '#imports'
const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState( const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
(view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>, statePublic = false) => { (view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>, statePublic = false) => {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
@ -15,30 +13,11 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
const { addUndo, defineViewScope } = useUndoRedo() const { addUndo, defineViewScope } = useUndoRedo()
const gridViewCols = ref<Record<string, GridColumnType>>({}) const gridViewCols = ref<Record<string, GridColumnType>>({})
const resizingCol = ref<string | null>('') const resizingColOldWith = ref('200px')
const resizingColWidth = ref('200px')
const isPublic = inject(IsPublicInj, ref(statePublic)) const isPublic = inject(IsPublicInj, ref(statePublic))
const columns = computed<ColumnType[]>(() => metas.value?.[view.value?.fk_model_id as string]?.columns || []) const columns = computed<ColumnType[]>(() => metas.value?.[view.value?.fk_model_id as string]?.columns || [])
watch(
[gridViewCols, resizingCol, resizingColWidth],
() => {
let style = ''
for (const c of columns?.value || []) {
const val = gridViewCols?.value?.[c?.id as string]?.width || '200px'
if (val && c.title !== resizingCol?.value) {
style += `[data-col="${c.id}"]{min-width:${val};max-width:${val};width: ${val};}`
} else {
style += `[data-col="${c.id}"]{min-width:${resizingColWidth?.value};max-width:${resizingColWidth?.value};width: ${resizingColWidth?.value};}`
}
}
css.value = style
},
{ deep: true, immediate: true },
)
const loadGridViewColumns = async () => { const loadGridViewColumns = async () => {
if (!view.value?.id && !isPublic.value) return if (!view.value?.id && !isPublic.value) return
@ -52,8 +31,6 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
}), }),
{}, {},
) )
loadCss()
} }
/** when columns changes(create/delete) reload grid columns /** when columns changes(create/delete) reload grid columns
@ -70,7 +47,8 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
if (!undo) { if (!undo) {
const oldProps = Object.keys(props).reduce<Partial<GridColumnReqType>>((o: any, k) => { const oldProps = Object.keys(props).reduce<Partial<GridColumnReqType>>((o: any, k) => {
if (gridViewCols.value[id][k as keyof GridColumnType]) { if (gridViewCols.value[id][k as keyof GridColumnType]) {
o[k] = gridViewCols.value[id][k as keyof GridColumnType] if (k === 'width') o[k] = `${resizingColOldWith.value}px`
else o[k] = gridViewCols.value[id][k as keyof GridColumnType]
} }
return o return o
}, {}) }, {})
@ -105,7 +83,7 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
} }
} }
return { loadGridViewColumns, updateGridViewColumn, resizingCol, resizingColWidth, gridViewCols, loadCss, unloadCss } return { loadGridViewColumns, updateGridViewColumn, gridViewCols, resizingColOldWith }
}, },
'useGridViewColumn', 'useGridViewColumn',
) )

2
packages/nc-gui/composables/useKanbanViewStore.ts

@ -679,7 +679,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
if (res.message) { if (res.message) {
message.info( message.info(
`Row delete failed: ${`Unable to delete row with ID ${id} because of the following: `Record delete failed: ${`Unable to delete record with ID ${id} because of the following:
\n${res.message.join('\n')}.\n \n${res.message.join('\n')}.\n
Clear the data first & try again`})}`, Clear the data first & try again`})}`,
) )

2
packages/nc-gui/composables/useLTARStore.ts

@ -291,7 +291,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
if (res.message) { if (res.message) {
message.info( message.info(
`Row delete failed: ${`Unable to delete row with ID ${id} because of the following: `Record delete failed: ${`Unable to delete record with ID ${id} because of the following:
\n${res.message.join('\n')}.\n \n${res.message.join('\n')}.\n
Clear the data first & try again`})}`, Clear the data first & try again`})}`,
) )

3
packages/nc-gui/context/index.ts

@ -1,4 +1,4 @@
import type { ColumnType, TableType, ViewType } from 'nocodb-sdk' import type { ColumnType, FilterType, TableType, ViewType } from 'nocodb-sdk'
import type { ComputedRef, InjectionKey, Ref } from 'vue' import type { ComputedRef, InjectionKey, Ref } from 'vue'
import type { EventHook } from '@vueuse/core' import type { EventHook } from '@vueuse/core'
import type { NcProject, PageSidebarNode, Row, TabItem } from '#imports' import type { NcProject, PageSidebarNode, Row, TabItem } from '#imports'
@ -51,3 +51,4 @@ export const TreeViewInj: InjectionKey<{
contextMenuTarget: { type?: 'base' | 'base' | 'table' | 'main' | 'layout'; value?: any } contextMenuTarget: { type?: 'base' | 'base' | 'table' | 'main' | 'layout'; value?: any }
}> = Symbol('tree-view-functions-injection') }> = Symbol('tree-view-functions-injection')
export const JsonExpandInj: InjectionKey<Ref<boolean>> = Symbol('json-expand-injection') export const JsonExpandInj: InjectionKey<Ref<boolean>> = Symbol('json-expand-injection')
export const AllFiltersInj: InjectionKey<Ref<Record<string, FilterType[]>>> = Symbol('all-filters-injection')

196
packages/nc-gui/ee/assets/img/fieldPlaceholder.svg

@ -1,196 +0,0 @@
<svg width="166" height="80" viewBox="0 0 166 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_784_33028" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="166" height="80">
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" fill="white"/>
</mask>
<g mask="url(#mask0_784_33028)">
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" fill="white"/>
<g filter="url(#filter0_dd_784_33028)">
<path d="M7.08193 3.68799H4.81572C4.18993 3.68799 3.68262 4.19603 3.68262 4.82274V7.09224C3.68262 7.71895 4.18993 8.227 4.81572 8.227H7.08193C7.70773 8.227 8.21504 7.71895 8.21504 7.09224V4.82274C8.21504 4.19603 7.70773 3.68799 7.08193 3.68799Z" fill="#3366FF"/>
<path d="M7.0819 3.82983H4.81569C4.26811 3.82983 3.82422 4.27437 3.82422 4.82274V7.09225C3.82422 7.64061 4.26811 8.08515 4.81569 8.08515H7.0819C7.62947 8.08515 8.07337 7.64061 8.07337 7.09225V4.82274C8.07337 4.27437 7.62947 3.82983 7.0819 3.82983Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 5.10645L5.52399 6.66673L4.8158 5.95751" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 3.68799H11.6144C10.9886 3.68799 10.4813 4.19603 10.4813 4.82274V7.09224C10.4813 7.71895 10.9886 8.227 11.6144 8.227H13.8806C14.5064 8.227 15.0137 7.71895 15.0137 7.09224V4.82274C15.0137 4.19603 14.5064 3.68799 13.8806 3.68799Z" fill="#E7E7E9"/>
<path d="M50.14 3.89429H18.413C17.7872 3.89429 17.2799 4.40233 17.2799 5.02904V6.88592C17.2799 7.51262 17.7872 8.02067 18.413 8.02067H50.14C50.7658 8.02067 51.2731 7.51262 51.2731 6.88592V5.02904C51.2731 4.40233 50.7658 3.89429 50.14 3.89429Z" fill="#E7E7E9"/>
<path d="M165.717 11.3475H0.283325V11.9149H165.717V11.3475Z" fill="#E7E7E9"/>
<g filter="url(#filter1_dd_784_33028)">
<path d="M7.08193 15.0355H4.81572C4.18993 15.0355 3.68262 15.5436 3.68262 16.1703V18.4398C3.68262 19.0665 4.18993 19.5745 4.81572 19.5745H7.08193C7.70773 19.5745 8.21504 19.0665 8.21504 18.4398V16.1703C8.21504 15.5436 7.70773 15.0355 7.08193 15.0355Z" fill="#3366FF"/>
<path d="M7.0819 15.1774H4.81569C4.26811 15.1774 3.82422 15.6219 3.82422 16.1703V18.4398C3.82422 18.9881 4.26811 19.4327 4.81569 19.4327H7.0819C7.62947 19.4327 8.07337 18.9881 8.07337 18.4398V16.1703C8.07337 15.6219 7.62947 15.1774 7.0819 15.1774Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 16.4539L5.52399 18.0141L4.8158 17.3049" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 15.0355H11.6144C10.9886 15.0355 10.4813 15.5436 10.4813 16.1703V18.4398C10.4813 19.0665 10.9886 19.5745 11.6144 19.5745H13.8806C14.5064 19.5745 15.0137 19.0665 15.0137 18.4398V16.1703C15.0137 15.5436 14.5064 15.0355 13.8806 15.0355Z" fill="#E7E7E9"/>
<path d="M75.6349 15.0355H18.413C17.7872 15.0355 17.2799 15.5436 17.2799 16.1703V18.4398C17.2799 19.0665 17.7872 19.5745 18.413 19.5745H75.6349C76.2607 19.5745 76.768 19.0665 76.768 18.4398V16.1703C76.768 15.5436 76.2607 15.0355 75.6349 15.0355Z" fill="#E7E7E9"/>
<path d="M165.717 22.6951H0.283325V23.2624H165.717V22.6951Z" fill="#E7E7E9"/>
<path d="M165.575 23.1206H0.424927V34.1844H165.575V23.1206Z" fill="#EBF0FF"/>
<g filter="url(#filter2_dd_784_33028)">
<path d="M7.08193 26.3829H4.81572C4.18993 26.3829 3.68262 26.891 3.68262 27.5177V29.7872C3.68262 30.4139 4.18993 30.9219 4.81572 30.9219H7.08193C7.70773 30.9219 8.21504 30.4139 8.21504 29.7872V27.5177C8.21504 26.891 7.70773 26.3829 7.08193 26.3829Z" fill="#3366FF"/>
<path d="M7.0819 26.5248H4.81569C4.26811 26.5248 3.82422 26.9693 3.82422 27.5177V29.7872C3.82422 30.3356 4.26811 30.7801 4.81569 30.7801H7.0819C7.62947 30.7801 8.07337 30.3356 8.07337 29.7872V27.5177C8.07337 26.9693 7.62947 26.5248 7.0819 26.5248Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 27.8014L5.52399 29.3617L4.8158 28.6525" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 26.3829H11.6144C10.9886 26.3829 10.4813 26.891 10.4813 27.5177V29.7872C10.4813 30.4139 10.9886 30.9219 11.6144 30.9219H13.8806C14.5064 30.9219 15.0137 30.4139 15.0137 29.7872V27.5177C15.0137 26.891 14.5064 26.3829 13.8806 26.3829Z" fill="#E7E7E9"/>
<path d="M50.14 26.5894H18.413C17.7872 26.5894 17.2799 27.0974 17.2799 27.7241V29.581C17.2799 30.2077 17.7872 30.7157 18.413 30.7157H50.14C50.7658 30.7157 51.2731 30.2077 51.2731 29.581V27.7241C51.2731 27.0974 50.7658 26.5894 50.14 26.5894Z" fill="#E7E7E9"/>
<path d="M165 23H1V34H165V23Z" stroke="#3366FF"/>
<g filter="url(#filter3_dd_784_33028)">
<path d="M7.08193 37.7305H4.81572C4.18993 37.7305 3.68262 38.2385 3.68262 38.8652V41.1347C3.68262 41.7614 4.18993 42.2695 4.81572 42.2695H7.08193C7.70773 42.2695 8.21504 41.7614 8.21504 41.1347V38.8652C8.21504 38.2385 7.70773 37.7305 7.08193 37.7305Z" fill="#3366FF"/>
<path d="M7.0819 37.8723H4.81569C4.26811 37.8723 3.82422 38.3169 3.82422 38.8652V41.1347C3.82422 41.6831 4.26811 42.1276 4.81569 42.1276H7.0819C7.62947 42.1276 8.07337 41.6831 8.07337 41.1347V38.8652C8.07337 38.3169 7.62947 37.8723 7.0819 37.8723Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 39.1489L5.52399 40.7092L4.8158 40" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 37.7305H11.6144C10.9886 37.7305 10.4813 38.2385 10.4813 38.8652V41.1347C10.4813 41.7614 10.9886 42.2695 11.6144 42.2695H13.8806C14.5064 42.2695 15.0137 41.7614 15.0137 41.1347V38.8652C15.0137 38.2385 14.5064 37.7305 13.8806 37.7305Z" fill="#E7E7E9"/>
<path d="M75.6349 37.7305H18.413C17.7872 37.7305 17.2799 38.2385 17.2799 38.8652V41.1347C17.2799 41.7614 17.7872 42.2695 18.413 42.2695H75.6349C76.2607 42.2695 76.768 41.7614 76.768 41.1347V38.8652C76.768 38.2385 76.2607 37.7305 75.6349 37.7305Z" fill="#E7E7E9"/>
<path d="M165.717 45.3901H0.283325V45.9575H165.717V45.3901Z" fill="#E7E7E9"/>
<g filter="url(#filter4_dd_784_33028)">
<path d="M7.08193 49.078H4.81572C4.18993 49.078 3.68262 49.586 3.68262 50.2128V52.4823C3.68262 53.109 4.18993 53.617 4.81572 53.617H7.08193C7.70773 53.617 8.21504 53.109 8.21504 52.4823V50.2128C8.21504 49.586 7.70773 49.078 7.08193 49.078Z" fill="#3366FF"/>
<path d="M7.0819 49.2198H4.81569C4.26811 49.2198 3.82422 49.6644 3.82422 50.2128V52.4823C3.82422 53.0306 4.26811 53.4752 4.81569 53.4752H7.0819C7.62947 53.4752 8.07337 53.0306 8.07337 52.4823V50.2128C8.07337 49.6644 7.62947 49.2198 7.0819 49.2198Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 50.4965L5.52399 52.0567L4.8158 51.3475" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 49.078H11.6144C10.9886 49.078 10.4813 49.586 10.4813 50.2128V52.4823C10.4813 53.109 10.9886 53.617 11.6144 53.617H13.8806C14.5064 53.617 15.0137 53.109 15.0137 52.4823V50.2128C15.0137 49.586 14.5064 49.078 13.8806 49.078Z" fill="#E7E7E9"/>
<path d="M50.14 49.2843H18.413C17.7872 49.2843 17.2799 49.7923 17.2799 50.4191V52.2759C17.2799 52.9026 17.7872 53.4107 18.413 53.4107H50.14C50.7658 53.4107 51.2731 52.9026 51.2731 52.2759V50.4191C51.2731 49.7923 50.7658 49.2843 50.14 49.2843Z" fill="#E7E7E9"/>
<path d="M165.717 56.7375H0.283325V57.3049H165.717V56.7375Z" fill="#E7E7E9"/>
<g filter="url(#filter5_dd_784_33028)">
<path d="M7.08193 60.4255H4.81572C4.18993 60.4255 3.68262 60.9336 3.68262 61.5603V63.8298C3.68262 64.4565 4.18993 64.9645 4.81572 64.9645H7.08193C7.70773 64.9645 8.21504 64.4565 8.21504 63.8298V61.5603C8.21504 60.9336 7.70773 60.4255 7.08193 60.4255Z" fill="#3366FF"/>
<path d="M7.0819 60.5674H4.81569C4.26811 60.5674 3.82422 61.0119 3.82422 61.5603V63.8298C3.82422 64.3782 4.26811 64.8227 4.81569 64.8227H7.0819C7.62947 64.8227 8.07337 64.3782 8.07337 63.8298V61.5603C8.07337 61.0119 7.62947 60.5674 7.0819 60.5674Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 61.844L5.52399 63.4043L4.8158 62.6951" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 60.4255H11.6144C10.9886 60.4255 10.4813 60.9336 10.4813 61.5603V63.8298C10.4813 64.4565 10.9886 64.9645 11.6144 64.9645H13.8806C14.5064 64.9645 15.0137 64.4565 15.0137 63.8298V61.5603C15.0137 60.9336 14.5064 60.4255 13.8806 60.4255Z" fill="#E7E7E9"/>
<path d="M75.6349 60.4255H18.413C17.7872 60.4255 17.2799 60.9336 17.2799 61.5603V63.8298C17.2799 64.4565 17.7872 64.9645 18.413 64.9645H75.6349C76.2607 64.9645 76.768 64.4565 76.768 63.8298V61.5603C76.768 60.9336 76.2607 60.4255 75.6349 60.4255Z" fill="#E7E7E9"/>
<path d="M165.717 68.0851H0.283325V68.6525H165.717V68.0851Z" fill="#E7E7E9"/>
<g filter="url(#filter6_dd_784_33028)">
<path d="M7.08193 71.7731H4.81572C4.18993 71.7731 3.68262 72.2811 3.68262 72.9078V75.1773C3.68262 75.804 4.18993 76.3121 4.81572 76.3121H7.08193C7.70773 76.3121 8.21504 75.804 8.21504 75.1773V72.9078C8.21504 72.2811 7.70773 71.7731 7.08193 71.7731Z" fill="#3366FF"/>
<path d="M7.0819 71.9149H4.81569C4.26811 71.9149 3.82422 72.3595 3.82422 72.9078V75.1773C3.82422 75.7257 4.26811 76.1702 4.81569 76.1702H7.0819C7.62947 76.1702 8.07337 75.7257 8.07337 75.1773V72.9078C8.07337 72.3595 7.62947 71.9149 7.0819 71.9149Z" stroke="#3366FF"/>
</g>
<path d="M7.08201 73.1915L5.52399 74.7518L4.8158 74.0426" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.8806 71.7731H11.6144C10.9886 71.7731 10.4813 72.2811 10.4813 72.9078V75.1773C10.4813 75.804 10.9886 76.3121 11.6144 76.3121H13.8806C14.5064 76.3121 15.0137 75.804 15.0137 75.1773V72.9078C15.0137 72.2811 14.5064 71.7731 13.8806 71.7731Z" fill="#E7E7E9"/>
<path d="M50.14 71.9792H18.413C17.7872 71.9792 17.2799 72.4873 17.2799 73.114V74.9709C17.2799 75.5976 17.7872 76.1056 18.413 76.1056H50.14C50.7658 76.1056 51.2731 75.5976 51.2731 74.9709V73.114C51.2731 72.4873 50.7658 71.9792 50.14 71.9792Z" fill="#E7E7E9"/>
<path d="M165.717 79.4326H0.283325V80H165.717V79.4326Z" fill="#E7E7E9"/>
</g>
<g filter="url(#filter7_d_784_33028)">
<path d="M115.669 34.6749C115.494 34.4499 115.275 33.9937 114.894 33.4249C114.675 33.1124 114.138 32.5187 113.975 32.2124C113.858 32.0264 113.824 31.7997 113.881 31.5874C113.979 31.1838 114.362 30.9161 114.775 30.9624C115.094 31.0266 115.388 31.1831 115.619 31.4124C115.78 31.5644 115.929 31.7296 116.063 31.9062C116.163 32.0312 116.188 32.0812 116.3 32.2249C116.413 32.3687 116.488 32.5124 116.431 32.2999C116.388 31.9874 116.313 31.4624 116.206 30.9937C116.125 30.6374 116.106 30.5812 116.031 30.3124C115.956 30.0437 115.913 29.8187 115.831 29.5124C115.757 29.2116 115.699 28.907 115.656 28.5999C115.577 28.2073 115.635 27.7995 115.819 27.4437C116.037 27.2383 116.357 27.1841 116.631 27.3062C116.907 27.5095 117.112 27.7935 117.219 28.1187C117.383 28.5189 117.492 28.9394 117.544 29.3687C117.644 29.9937 117.838 30.9062 117.844 31.0937C117.844 30.8624 117.8 30.3749 117.844 30.1562C117.887 29.9282 118.046 29.7389 118.263 29.6562C118.449 29.5991 118.646 29.5862 118.838 29.6187C119.031 29.6592 119.203 29.7707 119.319 29.9312C119.464 30.2958 119.544 30.6828 119.556 31.0749C119.573 30.7316 119.632 30.3916 119.731 30.0624C119.836 29.9153 119.988 29.8092 120.163 29.7624C120.369 29.7247 120.581 29.7247 120.788 29.7624C120.957 29.8191 121.105 29.9259 121.213 30.0687C121.345 30.4002 121.425 30.7502 121.45 31.1062C121.45 31.1937 121.494 30.8624 121.631 30.6437C121.703 30.4316 121.882 30.2737 122.101 30.2295C122.321 30.1853 122.547 30.2616 122.695 30.4295C122.843 30.5974 122.89 30.8316 122.819 31.0437C122.819 31.4499 122.819 31.4312 122.819 31.7062C122.819 31.9812 122.819 32.2249 122.819 32.4562C122.796 32.8219 122.746 33.1854 122.669 33.5437C122.56 33.8606 122.409 34.1613 122.219 34.4374C121.915 34.7749 121.665 35.1563 121.475 35.5687C121.428 35.7736 121.407 35.9836 121.413 36.1937C121.412 36.3879 121.437 36.5812 121.488 36.7687C121.232 36.7957 120.974 36.7957 120.719 36.7687C120.475 36.7312 120.175 36.2437 120.094 36.0937C120.054 36.0132 119.971 35.9622 119.881 35.9622C119.791 35.9622 119.709 36.0132 119.669 36.0937C119.531 36.3312 119.225 36.7624 119.013 36.7874C118.594 36.8374 117.731 36.7874 117.05 36.7874C117.05 36.7874 117.163 36.1624 116.906 35.9374C116.65 35.7124 116.388 35.4499 116.194 35.2749L115.669 34.6749Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M115.669 34.6749C115.494 34.4499 115.275 33.9937 114.894 33.4249C114.675 33.1124 114.138 32.5187 113.975 32.2124C113.858 32.0264 113.824 31.7997 113.881 31.5874C113.979 31.1838 114.362 30.9161 114.775 30.9624C115.094 31.0266 115.388 31.1831 115.619 31.4124C115.78 31.5644 115.929 31.7296 116.063 31.9062C116.163 32.0312 116.188 32.0812 116.3 32.2249C116.413 32.3687 116.488 32.5124 116.431 32.2999C116.388 31.9874 116.313 31.4624 116.206 30.9937C116.125 30.6374 116.106 30.5812 116.031 30.3124C115.956 30.0437 115.913 29.8187 115.831 29.5124C115.757 29.2116 115.699 28.907 115.656 28.5999C115.577 28.2073 115.635 27.7995 115.819 27.4437C116.037 27.2383 116.357 27.1841 116.631 27.3062C116.907 27.5095 117.112 27.7935 117.219 28.1187C117.383 28.5189 117.492 28.9394 117.544 29.3687C117.644 29.9937 117.838 30.9062 117.844 31.0937C117.844 30.8624 117.8 30.3749 117.844 30.1562C117.887 29.9282 118.046 29.7389 118.263 29.6562C118.449 29.5991 118.646 29.5862 118.838 29.6187C119.031 29.6592 119.203 29.7707 119.319 29.9312C119.464 30.2958 119.544 30.6828 119.556 31.0749C119.573 30.7316 119.632 30.3916 119.731 30.0624C119.836 29.9153 119.988 29.8092 120.163 29.7624C120.369 29.7247 120.581 29.7247 120.788 29.7624C120.957 29.8191 121.105 29.9259 121.213 30.0687C121.345 30.4002 121.425 30.7502 121.45 31.1062C121.45 31.1937 121.494 30.8624 121.631 30.6437C121.703 30.4316 121.882 30.2737 122.101 30.2295C122.321 30.1853 122.547 30.2616 122.695 30.4295C122.843 30.5974 122.89 30.8316 122.819 31.0437C122.819 31.4499 122.819 31.4312 122.819 31.7062C122.819 31.9812 122.819 32.2249 122.819 32.4562C122.796 32.8219 122.746 33.1854 122.669 33.5437C122.56 33.8606 122.409 34.1613 122.219 34.4374C121.915 34.7749 121.665 35.1563 121.475 35.5687C121.428 35.7736 121.407 35.9836 121.413 36.1937C121.412 36.3879 121.437 36.5812 121.488 36.7687C121.232 36.7957 120.974 36.7957 120.719 36.7687C120.475 36.7312 120.175 36.2437 120.094 36.0937C120.054 36.0132 119.971 35.9622 119.881 35.9622C119.791 35.9622 119.709 36.0132 119.669 36.0937C119.531 36.3312 119.225 36.7624 119.013 36.7874C118.594 36.8374 117.731 36.7874 117.05 36.7874C117.05 36.7874 117.163 36.1624 116.906 35.9374C116.65 35.7124 116.388 35.4499 116.194 35.2749L115.669 34.6749Z" stroke="black" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M120.969 35.0162V32.8588C120.969 32.7297 120.864 32.625 120.734 32.625C120.605 32.625 120.5 32.7297 120.5 32.8588V35.0162C120.5 35.1453 120.605 35.25 120.734 35.25C120.864 35.25 120.969 35.1453 120.969 35.0162Z" fill="black"/>
<path d="M119.731 35.0154L119.719 32.8569C119.718 32.7281 119.612 32.6243 119.483 32.625C119.354 32.6258 119.249 32.7308 119.25 32.8596L119.263 35.0181C119.263 35.1469 119.369 35.2508 119.498 35.25C119.628 35.2493 119.732 35.1442 119.731 35.0154Z" fill="black"/>
<path d="M118 32.8619L118.013 35.0159C118.013 35.1459 118.119 35.2508 118.248 35.25C118.378 35.2493 118.482 35.1432 118.481 35.0131L118.469 32.8591C118.468 32.7291 118.362 32.6243 118.233 32.625C118.104 32.6258 117.999 32.7318 118 32.8619Z" fill="black"/>
</g>
<path d="M163.451 0.283691H2.54954C1.29794 0.283691 0.283325 1.29978 0.283325 2.55319V77.4468C0.283325 78.7002 1.29794 79.7163 2.54954 79.7163H163.451C164.702 79.7163 165.717 78.7002 165.717 77.4468V2.55319C165.717 1.29978 164.702 0.283691 163.451 0.283691Z" stroke="#E7E7E9"/>
<defs>
<filter id="filter0_dd_784_33028" x="0.324219" y="3.32983" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter1_dd_784_33028" x="0.324219" y="14.6774" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter2_dd_784_33028" x="0.324219" y="26.0248" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter3_dd_784_33028" x="0.324219" y="37.3723" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter4_dd_784_33028" x="0.324219" y="48.7198" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter5_dd_784_33028" x="0.324219" y="60.0674" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter6_dd_784_33028" x="0.324219" y="71.4149" width="11.2491" height="13.2554" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="5"/>
<feGaussianBlur stdDeviation="1.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.02 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="3"/>
<feGaussianBlur stdDeviation="0.5"/>
<feComposite in2="hardAlpha" operator="out"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.06 0"/>
<feBlend mode="normal" in2="effect1_dropShadow_784_33028" result="effect2_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_784_33028" result="shape"/>
</filter>
<filter id="filter7_d_784_33028" x="112.679" y="26.8667" width="11.3476" height="12.118" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="1"/>
<feGaussianBlur stdDeviation="0.4"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_784_33028"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_784_33028" result="shape"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

1
packages/nc-gui/helpers/parsers/CSVTemplateAdapter.ts

@ -224,7 +224,6 @@ export default class CSVTemplateAdapter {
const data = (row.data as [])[columnIdx] === '' ? null : (row.data as [])[columnIdx] const data = (row.data as [])[columnIdx] === '' ? null : (row.data as [])[columnIdx]
if (column.uidt === UITypes.Checkbox) { if (column.uidt === UITypes.Checkbox) {
rowData[column.column_name] = getCheckboxValue(data) rowData[column.column_name] = getCheckboxValue(data)
rowData[column.column_name] = data
} else if (column.uidt === UITypes.SingleSelect || column.uidt === UITypes.MultiSelect) { } else if (column.uidt === UITypes.SingleSelect || column.uidt === UITypes.MultiSelect) {
rowData[column.column_name] = (data || '').toString().trim() || null rowData[column.column_name] = (data || '').toString().trim() || null
} else { } else {

139
packages/nc-gui/lang/en.json

@ -185,8 +185,8 @@
"tables": "Tables", "tables": "Tables",
"field": "Field", "field": "Field",
"fields": "Fields", "fields": "Fields",
"column": "Column", "column": "Field",
"columns": "Columns", "columns": "Fields",
"page": "Page", "page": "Page",
"pages": "Pages", "pages": "Pages",
"record": "record", "record": "record",
@ -219,7 +219,7 @@
"orgLevelViewer": "Organization Level Viewer" "orgLevelViewer": "Organization Level Viewer"
}, },
"sqlVIew": "SQL View", "sqlVIew": "SQL View",
"rowHeight": "Row Height", "rowHeight": "Record Height",
"heightClass": { "heightClass": {
"short": "Short", "short": "Short",
"medium": "Medium", "medium": "Medium",
@ -291,11 +291,11 @@
"dateJoined": "Date Joined", "dateJoined": "Date Joined",
"tokenName": "Token name", "tokenName": "Token name",
"inDesktop": "in Desktop", "inDesktop": "in Desktop",
"rowData": "Row data", "rowData": "Record data",
"creator": "Creator", "creator": "Creator",
"qrCode": "QR Code", "qrCode": "QR Code",
"termsOfService": "Terms of Service", "termsOfService": "Terms of Service",
"updateSelectedRows": "Update Selected Rows", "updateSelectedRows": "Update Selected Records",
"noFiltersAdded": "No filters added", "noFiltersAdded": "No filters added",
"editCards": "Edit Cards", "editCards": "Edit Cards",
"noFieldsFound": "No fields found", "noFieldsFound": "No fields found",
@ -366,7 +366,7 @@
"codeSnippet": "Code Snippet", "codeSnippet": "Code Snippet",
"keyboardShortcut": "Keyboard Shortcuts", "keyboardShortcut": "Keyboard Shortcuts",
"generateRandomName": "Generate Random Name", "generateRandomName": "Generate Random Name",
"findRowByScanningCode": "Find row by scanning a QR or Barcode", "findRowByScanningCode": "Find record by scanning a QR or Barcode",
"tokenManagement": "Token Management", "tokenManagement": "Token Management",
"addNewToken": "Add new token", "addNewToken": "Add new token",
"accountSettings": "Account Settings", "accountSettings": "Account Settings",
@ -388,6 +388,7 @@
} }
}, },
"labels": { "labels": {
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed", "tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached", "duplicateAttachment": "File with name {filename} already attached",
"toAddress": "To Address", "toAddress": "To Address",
@ -457,9 +458,9 @@
"createKanbanView": "Create Kanban View", "createKanbanView": "Create Kanban View",
"viewName": "View name", "viewName": "View name",
"viewLink": "View Link", "viewLink": "View Link",
"columnName": "Column Name", "columnName": "Field Name",
"columnToScanFor": "Column to scan", "columnToScanFor": "Field to scan",
"columnType": "Column Type", "columnType": "Field Type",
"roleName": "Role Name", "roleName": "Role Name",
"roleDescription": "Role Description", "roleDescription": "Role Description",
"databaseType": "Type in Database", "databaseType": "Type in Database",
@ -501,8 +502,8 @@
"sqlOutput": "SQL Output", "sqlOutput": "SQL Output",
"addOption": "Add option", "addOption": "Add option",
"interfaceColor": "Interface Color", "interfaceColor": "Interface Color",
"qrCodeValueColumn": "Column with QR code value", "qrCodeValueColumn": "Field with QR code value",
"barcodeValueColumn": "Column with Barcode value", "barcodeValueColumn": "Field with Barcode value",
"barcodeFormat": "Barcode format", "barcodeFormat": "Barcode format",
"qrCodeValueTooLong": "Too many characters for a QR code", "qrCodeValueTooLong": "Too many characters for a QR code",
"barcodeValueTooLong": "Too many characters for a barcode", "barcodeValueTooLong": "Too many characters for a barcode",
@ -518,7 +519,7 @@
"requriedIdentity": "Required-IDENTITY", "requriedIdentity": "Required-IDENTITY",
"inflection": { "inflection": {
"tableName": "Inflection - Table name", "tableName": "Inflection - Table name",
"columnName": "Inflection - Column name" "columnName": "Inflection - Field name"
}, },
"community": { "community": {
"starUs1": "Star", "starUs1": "Star",
@ -535,7 +536,8 @@
"docReference": "Document Reference", "docReference": "Document Reference",
"selectUserRole": "Select User Role", "selectUserRole": "Select User Role",
"childTable": "Child table", "childTable": "Child table",
"childColumn": "Child column", "childColumn": "Child field",
"childField": "Child field",
"linkToAnotherRecord": "Link to another record", "linkToAnotherRecord": "Link to another record",
"links": "Links", "links": "Links",
"onUpdate": "On Update", "onUpdate": "On Update",
@ -551,16 +553,16 @@
"sharedBase": "Shared Base", "sharedBase": "Shared Base",
"importData": "Import Data", "importData": "Import Data",
"importSecondaryViews": "Import Secondary Views", "importSecondaryViews": "Import Secondary Views",
"importRollupColumns": "Import Rollup Columns", "importRollupColumns": "Import Rollup Fields",
"importLookupColumns": "Import Lookup Columns", "importLookupColumns": "Import Lookup Fields",
"importAttachmentColumns": "Import Attachment Columns", "importAttachmentColumns": "Import Attachment Fields",
"importFormulaColumns": "Import Formula Columns", "importFormulaColumns": "Import Formula Fields",
"importUsers": "Import Users (by email)", "importUsers": "Import Users (by email)",
"noData": "No Data", "noData": "No Data",
"goToDashboard": "Go to Dashboard", "goToDashboard": "Go to Dashboard",
"importing": "Importing", "importing": "Importing",
"formatJson": "Format JSON", "formatJson": "Format JSON",
"firstRowAsHeaders": "Use First Row as Headers", "firstRowAsHeaders": "Use First Record as Headers",
"flattenNested": "Flatten Nested", "flattenNested": "Flatten Nested",
"downloadAllowed": "Download allowed", "downloadAllowed": "Download allowed",
"weAreHiring": "We are Hiring!", "weAreHiring": "We are Hiring!",
@ -577,8 +579,8 @@
"agreeToTos": "By signing up, you agree to the Terms of Service", "agreeToTos": "By signing up, you agree to the Terms of Service",
"welcomeToNc": "Welcome to NocoDB!", "welcomeToNc": "Welcome to NocoDB!",
"inviteOnlySignup": "Allow signup only using invite url", "inviteOnlySignup": "Allow signup only using invite url",
"nextRow": "Next Row", "nextRow": "Next Record",
"prevRow": "Previous Row", "prevRow": "Previous Record",
"addRowGrid": "Manually add data in grid view", "addRowGrid": "Manually add data in grid view",
"addRowForm": "Enter record data through a form", "addRowForm": "Enter record data through a form",
"noAccess": "No access", "noAccess": "No access",
@ -587,7 +589,7 @@
"includeData": "Include Data", "includeData": "Include Data",
"includeView": "Include View", "includeView": "Include View",
"includeWebhook": "Include Webhook", "includeWebhook": "Include Webhook",
"zoomInToViewColumns": "Zoom in to view columns" "zoomInToViewColumns": "Zoom in to view fields"
}, },
"activity": { "activity": {
"onCondition": "On Condition", "onCondition": "On Condition",
@ -675,8 +677,8 @@
"newUser": "New User", "newUser": "New User",
"editUser": "Edit user", "editUser": "Edit user",
"deleteUser": "Remove user from base", "deleteUser": "Remove user from base",
"resendInvite": "Resend invite E-mail", "resendInvite": "Resend Invite E-mail",
"copyInviteURL": "Copy invite URL", "copyInviteURL": "Copy Invite URL",
"copyPasswordResetURL": "Copy password reset URL", "copyPasswordResetURL": "Copy password reset URL",
"newRole": "New role", "newRole": "New role",
"reloadRoles": "Reload roles", "reloadRoles": "Reload roles",
@ -694,17 +696,17 @@
"deleteTable": "Delete Table", "deleteTable": "Delete Table",
"addField": "Add new field to this table", "addField": "Add new field to this table",
"setDisplay": "Set as Display value", "setDisplay": "Set as Display value",
"addRow": "Add new row", "addRow": "Add new record",
"saveRow": "Save row", "saveRow": "Save record",
"saveAndExit": "Save & Exit", "saveAndExit": "Save & Exit",
"saveAndStay": "Save & Stay", "saveAndStay": "Save & Stay",
"insertRow": "Insert new row", "insertRow": "Insert new record",
"duplicateRow": "Duplicate row", "duplicateRow": "Duplicate record",
"deleteRow": "Delete row", "deleteRow": "Delete record",
"deleteRows": "Delete rows", "deleteRows": "Delete records",
"predictColumns": "Predict Columns", "predictColumns": "Predict Fields",
"predictFormulas": "Predict Formulas", "predictFormulas": "Predict Formulas",
"deleteSelectedRow": "Delete selected rows", "deleteSelectedRow": "Delete Selected Records",
"importExcel": "Import Excel", "importExcel": "Import Excel",
"importCSV": "Import CSV", "importCSV": "Import CSV",
"downloadCSV": "Download as CSV", "downloadCSV": "Download as CSV",
@ -718,7 +720,7 @@
"changePwd": "Change Password", "changePwd": "Change Password",
"createView": "Create a View", "createView": "Create a View",
"shareView": "Share View", "shareView": "Share View",
"findRowByCodeScan": "Find row by scan", "findRowByCodeScan": "Find record by scan",
"fillByCodeScan": "Fill by scan", "fillByCodeScan": "Fill by scan",
"listSharedView": "Shared View List", "listSharedView": "Shared View List",
"ListView": "Views List", "ListView": "Views List",
@ -762,7 +764,7 @@
"expandRecord": "Expand Record", "expandRecord": "Expand Record",
"deleteRecord": "Delete Record", "deleteRecord": "Delete Record",
"erd": { "erd": {
"showColumns": "Show Columns", "showColumns": "Show Fields",
"showPkAndFk": "Show Primary and Foreign Keys", "showPkAndFk": "Show Primary and Foreign Keys",
"showSqlViews": "Show SQL Views", "showSqlViews": "Show SQL Views",
"showMMTables": "Show Many to Many tables", "showMMTables": "Show Many to Many tables",
@ -820,8 +822,8 @@
"selectDiscordChannels": "Select Discord channels", "selectDiscordChannels": "Select Discord channels",
"selectMattermostChannels": "Select Mattermost channels", "selectMattermostChannels": "Select Mattermost channels",
"webhookTitle": "Webhook Title", "webhookTitle": "Webhook Title",
"barcodeColumn": "Select a column for the Barcode value", "barcodeColumn": "Select a field for the Barcode value",
"notFoundContent": "No valid Column Type can be found.", "notFoundContent": "No valid field Type can be found.",
"selectBarcodeFormat": "Select a Barcode format", "selectBarcodeFormat": "Select a Barcode format",
"projName": "Enter Base Name", "projName": "Enter Base Name",
"selectGroupField": "Select a Grouping Field", "selectGroupField": "Select a Grouping Field",
@ -835,11 +837,11 @@
"save": "Save password", "save": "Save password",
"confirm": "Confirm new password" "confirm": "Confirm new password"
}, },
"selectAColumnForTheQRCodeValue": "Select a column for the QR code value", "selectAColumnForTheQRCodeValue": "Select a field for the QR code value",
"allowNegativeNumbers": "Allow negative numbers", "allowNegativeNumbers": "Allow negative numbers",
"searchProjectTree": "Search tables", "searchProjectTree": "Search tables",
"searchFields": "Search fields", "searchFields": "Search fields",
"searchColumn": "Search {search} column", "searchColumn": "Search {search} field",
"searchApps": "Search apps", "searchApps": "Search apps",
"searchModels": "Search models", "searchModels": "Search models",
"noItemsFound": "No items found", "noItemsFound": "No items found",
@ -870,8 +872,9 @@
"newFormWillBeLoaded": "New form will be loaded after {seconds} seconds", "newFormWillBeLoaded": "New form will be loaded after {seconds} seconds",
"optimizedQueryDisabled": "Optimized query is disabled", "optimizedQueryDisabled": "Optimized query is disabled",
"optimizedQueryEnabled": "Optimized query is enabled", "optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup field is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time", "invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "Link column clear is not supported yet", "linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"recordCouldNotBeFound": "Record could not be found", "recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number", "invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed", "pageSizeChanged": "Page size changed",
@ -880,14 +883,14 @@
"webhookBodyMsg2": "body", "webhookBodyMsg2": "body",
"webhookBodyMsg3": "to refer the record under consideration", "webhookBodyMsg3": "to refer the record under consideration",
"formula": { "formula": {
"hintStart": "Hint: Use {placeholder1} to reference columns, e.g: {placeholder2}. For more, please check out", "hintStart": "Hint: Use {placeholder1} to reference fields, e.g: {placeholder2}. For more, please check out",
"hintEnd": "Formulas.", "hintEnd": "Formulas.",
"noSuggestedFormulaFound": "No suggested formula found", "noSuggestedFormulaFound": "No suggested formula found",
"numericTypeIsExpected": "Numeric type is expected", "numericTypeIsExpected": "Numeric type is expected",
"stringTypeIsExpected": "String type is expected", "stringTypeIsExpected": "String type is expected",
"operationNotAvailable": "{operation} operation not available", "operationNotAvailable": "{operation} operation not available",
"cantSaveFieldFormulaInvalid": "Can’t save field because formula is invalid", "cantSaveFieldFormulaInvalid": "Can’t save field because formula is invalid",
"notSupportedToReferenceColumn": "Not supported to reference column {columnName}", "notSupportedToReferenceColumn": "Not supported to reference field {columnName}",
"typeIsExpectedButFound": "Type {type} is expected but found Type {found}", "typeIsExpectedButFound": "Type {type} is expected but found Type {found}",
"requiredArgumentsFormula": "{calleeName} requires {requiredArguments} arguments", "requiredArgumentsFormula": "{calleeName} requires {requiredArguments} arguments",
"minRequiredArgumentsFormula": "{calleeName} required minimum {minRequiredArguments} arguments", "minRequiredArgumentsFormula": "{calleeName} required minimum {minRequiredArguments} arguments",
@ -901,14 +904,14 @@
"firstParamDateDiffHaveDate": "The first parameter of DATEDIFF() should have date value", "firstParamDateDiffHaveDate": "The first parameter of DATEDIFF() should have date value",
"secondParamDateDiffHaveDate": "The second parameter of DATEDIFF() should have date value", "secondParamDateDiffHaveDate": "The second parameter of DATEDIFF() should have date value",
"thirdParamDateDiffHaveDate": "The third parameter of DATETIME_DIFF() should have value either \"milliseconds\", \"ms\", \"seconds\", \"s\", \"minutes\", \"m\", \"hours\", \"h\", \"days\", \"d\", \"weeks\", \"w\", \"months\", \"M\", \"quarters\", \"Q\", \"years\", or \"y\"", "thirdParamDateDiffHaveDate": "The third parameter of DATETIME_DIFF() should have value either \"milliseconds\", \"ms\", \"seconds\", \"s\", \"minutes\", \"m\", \"hours\", \"h\", \"days\", \"d\", \"weeks\", \"w\", \"months\", \"M\", \"quarters\", \"Q\", \"years\", or \"y\"",
"columnNotAvailable": "Column {columnName} is not available", "columnNotAvailable": "Field {columnName} is not available",
"cantSaveCircularReference": "Can’t save field because it causes a circular reference", "cantSaveCircularReference": "Can’t save field because it causes a circular reference",
"columnWithTypeFoundButExpected": "Column {columnName} with {columnType} type is found but {expectedType} type is expected", "columnWithTypeFoundButExpected": "Field {columnName} with {columnType} type is found but {expectedType} type is expected",
"columnNotMatchedWithType": "{columnName} is not matched with {columnType}" "columnNotMatchedWithType": "{columnName} is not matched with {columnType}"
}, },
"selectOption": { "selectOption": {
"cantBeNull": "Select options can't be null", "cantBeNull": "Select options can't be null",
"multiSelectCantHaveCommas": "MultiSelect columns can't have commas(',')", "multiSelectCantHaveCommas": "MultiSelect fields can't have commas(',')",
"cantHaveDuplicates": "Select options can't have duplicates", "cantHaveDuplicates": "Select options can't have duplicates",
"createNewOptionNamed": "Create new option named" "createNewOptionNamed": "Create new option named"
}, },
@ -917,7 +920,7 @@
"invalidLocale": "Invalid locale", "invalidLocale": "Invalid locale",
"invalidCurrencyCode": "Invalid Currency Code", "invalidCurrencyCode": "Invalid Currency Code",
"postgresHasItsOwnCurrencySettings": "PostgreSQL 'money' type has own currency settings", "postgresHasItsOwnCurrencySettings": "PostgreSQL 'money' type has own currency settings",
"validColumnsForBarCode": "The valid Column Types for a Barcode Column are: Number, Single Line Text, Long Text, Phone Number, URL, Email, Decimal. Please create one first.", "validColumnsForBarCode": "The valid Field Types for a Barcode Field are: Number, Single Line Text, Long Text, Phone Number, URL, Email, Decimal. Please create one first.",
"hm": { "hm": {
"title": "Has Many Relation", "title": "Has Many Relation",
"tooltip_desc": "A single record from table ", "tooltip_desc": "A single record from table ",
@ -949,7 +952,7 @@
"createWebhookMsg2": "Create web-hooks to power you automations,", "createWebhookMsg2": "Create web-hooks to power you automations,",
"createWebhookMsg3": "Get notified as soon as there are changes in your data", "createWebhookMsg3": "Get notified as soon as there are changes in your data",
"areYouSureUWantTo": "Are you sure you want to delete the following", "areYouSureUWantTo": "Are you sure you want to delete the following",
"idColumnRequired": "ID column is required, you can rename this later if required.", "idColumnRequired": "ID field is required, you can rename this later if required.",
"length59Required": "The length exceeds the max 59 characters", "length59Required": "The length exceeds the max 59 characters",
"warning": { "warning": {
"dbValid": "Please make sure database you are trying to connect is valid! This operation can cause schema loss!!", "dbValid": "Please make sure database you are trying to connect is valid! This operation can cause schema loss!!",
@ -972,22 +975,22 @@
}, },
"codeScanner": { "codeScanner": {
"loadingScanner": "Loading the scanner...", "loadingScanner": "Loading the scanner...",
"selectColumn": "Select a column (QR code or Barcode) that you want to use for finding a row by scanning.", "selectColumn": "Select a field (QR code or Barcode) that you want to use for finding a record by scanning.",
"moreThanOneRowFoundForCode": "More than one row found for this code. Currently only unique codes are supported.", "moreThanOneRowFoundForCode": "More than one record found for this code. Currently only unique codes are supported.",
"noRowFoundForCode": "No row found for this code for the selected column" "noRowFoundForCode": "No record found for this code for the selected field"
}, },
"map": { "map": {
"overLimit": "You're over the limit.", "overLimit": "You're over the limit.",
"closeLimit": "You're getting close to the limit.", "closeLimit": "You're getting close to the limit.",
"limitNumber": "The limit of markers shown in a Map View is 1000 records." "limitNumber": "The limit of markers shown in a Map View is 1000 records."
}, },
"footerInfo": "Rows per page", "footerInfo": "Records per page",
"upload": "Select file to Upload", "upload": "Select file to Upload",
"upload_sub": "or drag and drop file", "upload_sub": "or drag and drop file",
"excelSupport": "Supported: .xls, .xlsx, .xlsm, .ods, .ots", "excelSupport": "Supported: .xls, .xlsx, .xlsm, .ods, .ots",
"excelURL": "Enter excel file URL", "excelURL": "Enter excel file URL",
"csvURL": "Enter CSV file URL", "csvURL": "Enter CSV file URL",
"footMsg": "# of rows to parse to infer datatype", "footMsg": "# of records to parse to infer datatype",
"excelImport": "sheet(s) are available for import", "excelImport": "sheet(s) are available for import",
"exportMetadata": "Do you want to export metadata from meta tables?", "exportMetadata": "Do you want to export metadata from meta tables?",
"importMetadata": "Do you want to import metadata from meta tables?", "importMetadata": "Do you want to import metadata from meta tables?",
@ -1071,8 +1074,8 @@
"enterTableName": "Enter table name", "enterTableName": "Enter table name",
"enterLayoutName": "Enter Layout name", "enterLayoutName": "Enter Layout name",
"enterDashboardName": "Enter Dashboard name", "enterDashboardName": "Enter Dashboard name",
"defaultColumns": "Default columns", "defaultColumns": "Default fields",
"addDefaultColumns": "Add default columns", "addDefaultColumns": "Add default fields",
"tableNameInDb": "Table name as saved in database", "tableNameInDb": "Table name as saved in database",
"airtable": { "airtable": {
"credentials": "Where to find this?" "credentials": "Where to find this?"
@ -1092,7 +1095,7 @@
"cacheEmpty": "Cache is empty", "cacheEmpty": "Cache is empty",
"exportedCache": "Exported Cache Successfully", "exportedCache": "Exported Cache Successfully",
"valueAlreadyInList": "This value is already in the list", "valueAlreadyInList": "This value is already in the list",
"noColumnsToUpdate": "No columns to update", "noColumnsToUpdate": "No fields to update",
"tableDeleted": "Deleted table successfully", "tableDeleted": "Deleted table successfully",
"layoutDeleted": "Deleted layout successfully", "layoutDeleted": "Deleted layout successfully",
"generatePublicShareableReadonlyBase": "Generate publicly shareable readonly base", "generatePublicShareableReadonlyBase": "Generate publicly shareable readonly base",
@ -1147,11 +1150,11 @@
"internalError": "Some internal error occurred", "internalError": "Some internal error occurred",
"templateGeneratorNotFound": "Template Generator cannot be found!", "templateGeneratorNotFound": "Template Generator cannot be found!",
"fileUploadFailed": "Failed to upload file", "fileUploadFailed": "Failed to upload file",
"primaryColumnUpdateFailed": "Failed to update primary column", "primaryColumnUpdateFailed": "Failed to update primary field",
"formDescriptionTooLong": "Data too long for Form Description", "formDescriptionTooLong": "Data too long for Form Description",
"columnsRequired": "Following columns are required", "columnsRequired": "Following fields are required",
"selectAtleastOneColumn": "At least one column has to be selected", "selectAtleastOneColumn": "At least one field has to be selected",
"columnDescriptionNotFound": "Cannot find the destination column for", "columnDescriptionNotFound": "Cannot find the destination field for",
"duplicateMappingFound": "Duplicate mapping found, please remove one of the mapping", "duplicateMappingFound": "Duplicate mapping found, please remove one of the mapping",
"nullValueViolatesNotNull": "Null value violates not-null constraint", "nullValueViolatesNotNull": "Null value violates not-null constraint",
"sourceHasInvalidNumbers": "Source data contains some invalid numbers", "sourceHasInvalidNumbers": "Source data contains some invalid numbers",
@ -1163,17 +1166,17 @@
"failedToLoadChildrenList": "Failed to load children list", "failedToLoadChildrenList": "Failed to load children list",
"deleteFailed": "Delete failed", "deleteFailed": "Delete failed",
"unlinkFailed": "Unlink failed", "unlinkFailed": "Unlink failed",
"rowUpdateFailed": "Row update failed", "rowUpdateFailed": "Record update failed",
"deleteRowFailed": "Failed to delete row", "deleteRowFailed": "Failed to delete record",
"setFormDataFailed": "Failed to set form data", "setFormDataFailed": "Failed to set form data",
"formViewUpdateFailed": "Failed to update form view", "formViewUpdateFailed": "Failed to update form view",
"tableNameRequired": "Table name is required", "tableNameRequired": "Table name is required",
"nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _", "nameShouldStartWithAnAlphabetOr_": "Name should start with an alphabet or _",
"followingCharactersAreNotAllowed": "Following characters are not allowed", "followingCharactersAreNotAllowed": "Following characters are not allowed",
"columnNameRequired": "Column name is required", "columnNameRequired": "Field name is required",
"duplicateColumnName": "Duplicate column name", "duplicateColumnName": "Duplicate field name",
"uiDataTypeRequired": "UI data type is required", "uiDataTypeRequired": "UI data type is required",
"columnNameExceedsCharacters": "The length of column name exceeds the max {value} characters", "columnNameExceedsCharacters": "The length of field name exceeds the max {value} characters",
"projectNameExceeds50Characters": "Base name exceeds 50 characters", "projectNameExceeds50Characters": "Base name exceeds 50 characters",
"projectNameCannotStartWithSpace": "Base name cannot start with space", "projectNameCannotStartWithSpace": "Base name cannot start with space",
"requiredField": "Required field", "requiredField": "Required field",
@ -1197,7 +1200,7 @@
"deleteProject": "Base deleted successfully", "deleteProject": "Base deleted successfully",
"authToken": "Auth token copied to clipboard", "authToken": "Auth token copied to clipboard",
"projInfo": "Copied base info to clipboard", "projInfo": "Copied base info to clipboard",
"inviteUrlCopy": "Copied invite URL to clipboard", "inviteUrlCopy": "Copied Invite URL to clipboard",
"createView": "View created successfully", "createView": "View created successfully",
"formEmailSMTP": "Please activate SMTP plugin in App store for enabling email notification", "formEmailSMTP": "Please activate SMTP plugin in App store for enabling email notification",
"collabView": "Successfully Switched to collaborative view", "collabView": "Successfully Switched to collaborative view",
@ -1206,8 +1209,8 @@
}, },
"success": { "success": {
"licenseKeyUpdated": "License Key Updated", "licenseKeyUpdated": "License Key Updated",
"columnDuplicated": "Column duplicated successfully", "columnDuplicated": "Field duplicated successfully",
"rowDuplicatedWithoutSavedYet": "Row duplicated (not saved)", "rowDuplicatedWithoutSavedYet": "Record duplicated (not saved)",
"updatedUIACL": "Updated UI ACL for tables successfully", "updatedUIACL": "Updated UI ACL for tables successfully",
"pluginUninstalled": "Plugin uninstalled successfully", "pluginUninstalled": "Plugin uninstalled successfully",
"pluginSettingsSaved": "Plugin settings saved successfully", "pluginSettingsSaved": "Plugin settings saved successfully",
@ -1215,7 +1218,7 @@
"tableRenamed": "Table renamed successfully", "tableRenamed": "Table renamed successfully",
"layoutRenamed": "Layout renamed successfully", "layoutRenamed": "Layout renamed successfully",
"viewDeleted": "View deleted successfully", "viewDeleted": "View deleted successfully",
"primaryColumnUpdated": "Successfully updated as primary column", "primaryColumnUpdated": "Successfully updated as primary field",
"tableDataExported": "Successfully exported all table data", "tableDataExported": "Successfully exported all table data",
"updated": "Successfully updated", "updated": "Successfully updated",
"sharedViewDeleted": "Deleted shared view successfully", "sharedViewDeleted": "Deleted shared view successfully",
@ -1237,8 +1240,8 @@
"webhookUpdated": "Webhook details updated successfully", "webhookUpdated": "Webhook details updated successfully",
"webhookDeleted": "Hook deleted successfully", "webhookDeleted": "Hook deleted successfully",
"webhookTested": "Webhook tested successfully", "webhookTested": "Webhook tested successfully",
"columnUpdated": "Column updated", "columnUpdated": "Field updated",
"columnCreated": "Column created", "columnCreated": "Field created",
"passwordChanged": "Password changed successfully. Please login again.", "passwordChanged": "Password changed successfully. Please login again.",
"settingsSaved": "Settings saved successfully", "settingsSaved": "Settings saved successfully",
"roleUpdated": "Role updated successfully" "roleUpdated": "Role updated successfully"

1
packages/nc-gui/lib/types.ts

@ -125,6 +125,7 @@ type NcProject = BaseType & {
temp_title?: string temp_title?: string
edit?: boolean edit?: boolean
starred?: boolean starred?: boolean
uuid?: string
} }
interface UndoRedoAction { interface UndoRedoAction {

16
packages/nc-gui/middleware/auth.global.ts

@ -1,6 +1,7 @@
import type { Api } from 'nocodb-sdk' import type { Api } from 'nocodb-sdk'
import type { Actions } from '~/composables/useGlobal/types' import type { Actions } from '~/composables/useGlobal/types'
import { defineNuxtRouteMiddleware, extractSdkResponseErrorMsg, message, navigateTo, useApi, useGlobal, useRoles } from '#imports' import { defineNuxtRouteMiddleware, message, navigateTo, useApi, useGlobal, useRoles } from '#imports'
import { extractSdkResponseErrorMsg } from '~/utils'
/** /**
* Global auth middleware * Global auth middleware
@ -46,7 +47,9 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
} }
/** if user isn't signed in and google auth is enabled, try to check if sign-in data is present */ /** if user isn't signed in and google auth is enabled, try to check if sign-in data is present */
if (!state.signedIn.value && state.appInfo.value.googleAuthEnabled) await tryGoogleAuth(api, state.signIn) if (!state.signedIn.value && state.appInfo.value.googleAuthEnabled) {
await tryGoogleAuth(api, state.signIn)
}
/** if public allow all visitors */ /** if public allow all visitors */
if (to.meta.public) return if (to.meta.public) return
@ -54,13 +57,18 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
/** if shared base allow without validating */ /** if shared base allow without validating */
if (to.params.typeOrId === 'base') return if (to.params.typeOrId === 'base') return
/** if auth is required or unspecified (same as required) and user is not signed in, redirect to signin page */ /** if auth is required or unspecified (same `as required) and user is not signed in, redirect to signin page */
if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) { if ((to.meta.requiresAuth || typeof to.meta.requiresAuth === 'undefined') && !state.signedIn.value) {
/** If this is the first usern navigate to signup page directly */ /** If this is the first usern navigate to signup page directly */
if (state.appInfo.value.firstUser) { if (state.appInfo.value.firstUser) {
const query = to.fullPath !== '/' && to.fullPath.match(/^\/(?!\?)/) ? { continueAfterSignIn: to.fullPath } : {}
if (query.continueAfterSignIn) {
localStorage.setItem('continueAfterSignIn', query.continueAfterSignIn)
}
return navigateTo({ return navigateTo({
path: '/signup', path: '/signup',
query: to.fullPath !== '/' && to.fullPath.match(/^\/(?!\?)/) ? { continueAfterSignIn: to.fullPath } : {}, query,
}) })
} }

13
packages/nc-gui/pages/account/index.vue

@ -145,7 +145,7 @@ const logout = async () => {
<!-- Sub Tabs --> <!-- Sub Tabs -->
<div class="flex flex-col w-full ml-65"> <div class="flex flex-col w-full ml-65">
<div class="flex flex-row p-3 items-center"> <div class="flex flex-row p-3 items-center h-14">
<div class="flex-1" /> <div class="flex-1" />
<LazyGeneralReleaseInfo /> <LazyGeneralReleaseInfo />
@ -185,8 +185,15 @@ const logout = async () => {
</NcDropdown> </NcDropdown>
</template> </template>
</div> </div>
<div class="flex flex-col container mx-auto mt-2"> <div
<NuxtPage /> class="flex flex-col container mx-auto"
:style="{
height: 'calc(100vh - 3.5rem)',
}"
>
<div class="mt-2 h-full">
<NuxtPage />
</div>
</div> </div>
</div> </div>
</div> </div>

21
packages/nc-gui/pages/copy-shared-base.vue

@ -0,0 +1,21 @@
<script lang="ts" setup>
import { useBase, useCopySharedBase, useRoute } from '#imports'
const route = useRoute()
const { sharedBaseId } = useCopySharedBase()
const { forcedProjectId } = storeToRefs(useBase())
onMounted(() => {
sharedBaseId.value = route.query.base as string
if (forcedProjectId?.value) forcedProjectId.value = undefined
navigateTo(`/`)
})
</script>
<template>
<div></div>
</template>
<style scoped></style>

72
packages/nc-gui/pages/index.vue

@ -16,7 +16,9 @@ const basesStore = useBases()
const { populateWorkspace } = useWorkspace() const { populateWorkspace } = useWorkspace()
const { signedIn } = useGlobal() const { signedIn, ncNavigateTo } = useGlobal()
const { isUIAllowed } = useRoles()
const router = useRouter() const router = useRouter()
@ -46,6 +48,10 @@ const isSharedFormView = computed(() => {
return routeName.startsWith('index-typeOrId-form-viewId') return routeName.startsWith('index-typeOrId-form-viewId')
}) })
const { sharedBaseId } = useCopySharedBase()
const isDuplicateDlgOpen = ref(false)
async function handleRouteTypeIdChange() { async function handleRouteTypeIdChange() {
// avoid loading bases for shared views // avoid loading bases for shared views
if (isSharedView.value) { if (isSharedView.value) {
@ -82,7 +88,29 @@ watch(
// immediate watch, because if route is changed during page transition // immediate watch, because if route is changed during page transition
// It will error out nuxt // It will error out nuxt
onMounted(() => { onMounted(() => {
handleRouteTypeIdChange() if (route.value.query?.continueAfterSignIn) {
localStorage.removeItem('continueAfterSignIn')
return navigateTo(route.value.query.continueAfterSignIn as string)
} else {
const continueAfterSignIn = localStorage.getItem('continueAfterSignIn')
if (continueAfterSignIn) {
return navigateTo({
path: continueAfterSignIn,
query: route.value.query,
})
}
}
handleRouteTypeIdChange().then(() => {
if (sharedBaseId.value) {
if (!isUIAllowed('baseDuplicate')) {
message.error('You are not allowed to create base')
return
}
isDuplicateDlgOpen.value = true
}
})
}) })
function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) { function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: string) {
@ -93,6 +121,40 @@ function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: str
} }
provide(ToggleDialogInj, toggleDialog) provide(ToggleDialogInj, toggleDialog)
const { $e, $poller } = useNuxtApp()
const DlgSharedBaseDuplicateOnOk = async (jobData: { id: string; base_id: string; workspace_id: string }) => {
await populateWorkspace()
$poller.subscribe(
{ id: jobData.id },
async (data: {
id: string
status?: string
data?: {
error?: {
message: string
}
message?: string
result?: any
}
}) => {
if (data.status !== 'close') {
if (data.status === JobStatus.COMPLETED) {
await ncNavigateTo({
baseId: jobData.base_id,
})
} else if (data.status === JobStatus.FAILED) {
message.error('Failed to duplicate shared base')
await populateWorkspace()
}
}
},
)
$e('a:base:duplicate-shared-base')
}
</script> </script>
<template> <template>
@ -117,6 +179,12 @@ provide(ToggleDialogInj, toggleDialog)
v-model:data-sources-state="dataSourcesState" v-model:data-sources-state="dataSourcesState"
:base-id="baseId" :base-id="baseId"
/> />
<DlgSharedBaseDuplicate
v-if="isUIAllowed('baseDuplicate')"
v-model="isDuplicateDlgOpen"
:shared-base-id="sharedBaseId"
:on-ok="DlgSharedBaseDuplicateOnOk"
/>
</div> </div>
</template> </template>

10
packages/nc-gui/plugins/a.i18n.ts

@ -1,6 +1,6 @@
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
import { isClient } from '@vueuse/core' import { isClient } from '@vueuse/core'
import { LanguageAlias, applyLanguageDirection, defineNuxtPlugin, isRtlLang, nextTick } from '#imports' import { LanguageAlias, applyLanguageDirection, defineNuxtPlugin, isEeUI, isRtlLang, nextTick } from '#imports'
import type { Language, NocoI18n } from '#imports' import type { Language, NocoI18n } from '#imports'
let globalI18n: NocoI18n let globalI18n: NocoI18n
@ -43,10 +43,16 @@ export async function loadLocaleMessages(
return nextTick() return nextTick()
} }
export default defineNuxtPlugin(async (nuxtApp) => { const i18nPlugin = async (nuxtApp) => {
globalI18n = await createI18nPlugin() globalI18n = await createI18nPlugin()
nuxtApp.vueApp.i18n = globalI18n nuxtApp.vueApp.i18n = globalI18n
nuxtApp.vueApp.use(globalI18n) nuxtApp.vueApp.use(globalI18n)
}
export default defineNuxtPlugin(async function (nuxtApp) {
if (!isEeUI) return await i18nPlugin(nuxtApp)
}) })
export { i18nPlugin }

10
packages/nc-gui/plugins/api.ts

@ -1,6 +1,12 @@
import { defineNuxtPlugin, useApi } from '#imports' import { defineNuxtPlugin, isEeUI, useApi } from '#imports'
export default defineNuxtPlugin((nuxtApp) => { const apiPlugin = (nuxtApp) => {
/** injects a global api instance */ /** injects a global api instance */
nuxtApp.provide('api', useApi().api) nuxtApp.provide('api', useApi().api)
}
export { apiPlugin }
export default defineNuxtPlugin(function (nuxtApp) {
if (!isEeUI) return apiPlugin(nuxtApp)
}) })

1
packages/nc-gui/plugins/resizeDirective.ts

@ -36,6 +36,7 @@ export default defineNuxtPlugin((nuxtApp) => {
startWidth = parseInt(document.defaultView?.getComputedStyle(el)?.width || '0', 10) startWidth = parseInt(document.defaultView?.getComputedStyle(el)?.width || '0', 10)
document.documentElement.addEventListener('mousemove', doDrag, false) document.documentElement.addEventListener('mousemove', doDrag, false)
document.documentElement.addEventListener('mouseup', stopDrag, false) document.documentElement.addEventListener('mouseup', stopDrag, false)
emit('xcstartresizing', startWidth)
} }
;(el as any).initDrag = initDrag ;(el as any).initDrag = initDrag

10
packages/nc-gui/plugins/state.ts

@ -1,4 +1,4 @@
import { Language, LanguageAlias, defineNuxtPlugin, useApi, useGlobal } from '#imports' import { Language, LanguageAlias, defineNuxtPlugin, isEeUI, useApi, useGlobal } from '#imports'
import { loadLocaleMessages, setI18nLanguage } from '~/plugins/a.i18n' import { loadLocaleMessages, setI18nLanguage } from '~/plugins/a.i18n'
/** /**
@ -13,7 +13,7 @@ import { loadLocaleMessages, setI18nLanguage } from '~/plugins/a.i18n'
* console.log($state.lang.value) // 'en' * console.log($state.lang.value) // 'en'
* ``` * ```
*/ */
export default defineNuxtPlugin(async () => { const statePlugin = async (_nuxtApp) => {
const state = useGlobal() const state = useGlobal()
const { api } = useApi({ useGlobalInstance: true }) const { api } = useApi({ useGlobalInstance: true })
@ -34,4 +34,10 @@ export default defineNuxtPlugin(async () => {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
}
export default defineNuxtPlugin(async function (nuxtApp) {
if (!isEeUI) return await statePlugin(nuxtApp)
}) })
export { statePlugin }

BIN
packages/nc-gui/public/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

71
packages/nc-gui/store/views.ts

@ -1,18 +1,35 @@
import type { ViewType } from 'nocodb-sdk' import type { ViewType, ViewTypes } from 'nocodb-sdk'
import { acceptHMRUpdate, defineStore } from 'pinia' import { acceptHMRUpdate, defineStore } from 'pinia'
import type { ViewPageType } from '~/lib' import type { ViewPageType } from '~/lib'
export const useViewsStore = defineStore('viewsStore', () => { export const useViewsStore = defineStore('viewsStore', () => {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
interface RecentView {
viewName: string
viewId: string | undefined
viewType: ViewTypes
tableID: string
isDefault: boolean
baseName: string
workspaceId: string
baseId: string
}
const router = useRouter() const router = useRouter()
const recentViews = computed(() => []) // Store recent views from all Workspaces
const allRecentViews = ref<RecentView[]>([])
const allRecentViews = ref<any>([])
const route = router.currentRoute const route = router.currentRoute
const bases = useBases()
const tablesStore = useTablesStore() const tablesStore = useTablesStore()
const { activeWorkspaceId } = storeToRefs(useWorkspace())
const recentViews = computed<RecentView[]>(() =>
allRecentViews.value.filter((f) => f.workspaceId === activeWorkspaceId.value).splice(0, 10),
)
const viewsByTable = ref<Map<string, ViewType[]>>(new Map()) const viewsByTable = ref<Map<string, ViewType[]>>(new Map())
const views = computed({ const views = computed({
get: () => (tablesStore.activeTableId ? viewsByTable.value.get(tablesStore.activeTableId) : []) ?? [], get: () => (tablesStore.activeTableId ? viewsByTable.value.get(tablesStore.activeTableId) : []) ?? [],
@ -122,10 +139,28 @@ export const useViewsStore = defineStore('viewsStore', () => {
}) })
} }
const changeView = async (..._args: any) => {} const changeView = async ({ viewId, tableId, baseId }: { viewId: string | null; tableId: string; baseId: string }) => {
const routeName = 'index-typeOrId-baseId-index-index-viewId-viewTitle'
const removeFromRecentViews = (..._args: any) => {} await router.push({ name: routeName, params: { viewTitle: viewId || '', viewId: tableId, baseId } })
}
const removeFromRecentViews = ({
viewId,
tableId,
baseId,
}: {
viewId?: string | undefined
tableId: string
baseId?: string
}) => {
if (baseId && !viewId && !tableId) {
allRecentViews.value = allRecentViews.value.filter((f) => f.baseId !== baseId)
} else if (baseId && tableId && !viewId) {
allRecentViews.value = allRecentViews.value.filter((f) => f.baseId !== baseId || f.tableID !== tableId)
} else if (tableId && viewId) {
allRecentViews.value = allRecentViews.value.filter((f) => f.viewId !== viewId || f.tableID !== tableId)
}
}
watch( watch(
() => tablesStore.activeTableId, () => tablesStore.activeTableId,
async (newId, oldId) => { async (newId, oldId) => {
@ -232,6 +267,28 @@ export const useViewsStore = defineStore('viewsStore', () => {
isPaginationLoading.value = true isPaginationLoading.value = true
}) })
watch(activeView, (view) => {
if (!view) return
if (!view.base_id) return
const tableName = tablesStore.baseTables.get(view.base_id)?.find((t) => t.id === view.fk_model_id)?.title
const baseName = bases.basesList.find((p) => p.id === view.base_id)?.title
allRecentViews.value = [
{
viewId: view.id,
baseId: view.base_id as string,
tableID: view.fk_model_id,
isDefault: !!view.is_default,
viewName: view.is_default ? (tableName as string) : view.title,
viewType: view.type,
workspaceId: activeWorkspaceId.value,
baseName: baseName as string,
},
...allRecentViews.value.filter((f) => f.viewId !== view.id || f.tableID !== view.fk_model_id),
]
})
return { return {
isLockedView, isLockedView,
isViewsLoading, isViewsLoading,

5
packages/nc-gui/store/workspace.ts

@ -207,6 +207,10 @@ export const useWorkspace = defineStore('workspaceStore', () => {
isWorkspaceLoading.value = isLoading isWorkspaceLoading.value = isLoading
} }
const getPlanLimit = (_arg: any) => {
return 9999
}
return { return {
loadWorkspaces, loadWorkspaces,
workspaces, workspaces,
@ -241,6 +245,7 @@ export const useWorkspace = defineStore('workspaceStore', () => {
lastPopulatedWorkspaceId, lastPopulatedWorkspaceId,
isWorkspaceSettingsPageOpened, isWorkspaceSettingsPageOpened,
workspaceUserCount, workspaceUserCount,
getPlanLimit,
} }
}) })

88
packages/nc-gui/utils/colorsUtils.ts

@ -113,85 +113,85 @@ export const baseThemeColors = [
const designSystem = { const designSystem = {
light: [ light: [
'#EBF0FF', // '#EBF0FF',
'#D6E0FF', // '#D6E0FF',
'#ADC2FF', // '#ADC2FF',
'#85A3FF', // '#85A3FF',
'#5C85FF', // '#5C85FF',
'#3366FF', '#3366FF',
'#2952CC', '#2952CC',
'#1F3D99', '#1F3D99',
'#142966', '#142966',
'#0A1433', '#0A1433',
'#FCFCFC', // '#FCFCFC',
'#F9F9FA', // '#F9F9FA',
'#F4F4F5', // '#F4F4F5',
'#E7E7E9', // '#E7E7E9',
'#D5D5D9', // '#D5D5D9',
'#9AA2AF', // '#9AA2AF',
'#6A7184', // '#6A7184',
'#4A5268', '#4A5268',
'#374151', '#374151',
'#1F293A', '#1F293A',
'#101015', '#101015',
'#FFF2F1', // '#FFF2F1',
'#FFDBD9', // '#FFDBD9',
'#FFB7B2', // '#FFB7B2',
'#FF928C', // '#FF928C',
'#FF6E65', // '#FF6E65',
'#FF4A3F', // '#FF4A3F',
'#E8463C', '#E8463C',
'#CB3F36', '#CB3F36',
'#B23830', '#B23830',
'#7D2721', '#7D2721',
'#FFEEFB', // '#FFEEFB',
'#FED8F4', // '#FED8F4',
'#FEB0E8', // '#FEB0E8',
'#FD89DD', // '#FD89DD',
'#FD61D1', // '#FD61D1',
'#FC3AC6', '#FC3AC6',
'#CA2E9E', '#CA2E9E',
'#972377', '#972377',
'#65174F', '#65174F',
'#320C28', '#320C28',
'#FFF5EF', // '#FFF5EF',
'#FEE6D6', // '#FEE6D6',
'#FDCDAD', // '#FDCDAD',
'#FCB483', // '#FCB483',
'#FB9B5A', // '#FB9B5A',
'#FA8231', '#FA8231',
'#E1752C', '#E1752C',
'#C86827', '#C86827',
'#964E1D', '#964E1D',
'#4B270F', '#4B270F',
'#F3ECFA', // '#F3ECFA',
'#E5D4F5', // '#E5D4F5',
'#CBA8EB', // '#CBA8EB',
'#B17DE1', // '#B17DE1',
'#9751D7', // '#9751D7',
'#7D26CD', '#7D26CD',
'#641EA4', '#641EA4',
'#4B177B', '#4B177B',
'#320F52', '#320F52',
'#190829', '#190829',
'#EDF9FF', // '#EDF9FF',
'#D7F2FF', // '#D7F2FF',
'#AFE5FF', // '#AFE5FF',
'#86D9FF', // '#86D9FF',
'#5ECCFF', // '#5ECCFF',
'#36BFFF', '#36BFFF',
'#2B99CC', '#2B99CC',
'#207399', '#207399',
'#164C66', '#164C66',
'#0B2633', '#0B2633',
'#fffbf2', // '#fffbf2',
'#fff0d1', // '#fff0d1',
'#fee5b0', // '#fee5b0',
'#fdd889', // '#fdd889',
'#fdcb61', // '#fdcb61',
'#fcbe3a', // '#fcbe3a',
'#ca982e', '#ca982e',
'#977223', '#977223',
'#654c17', '#654c17',

18
packages/nc-gui/utils/validation.ts

@ -98,21 +98,17 @@ export const fieldRequiredValidator = () => {
} }
} }
export const fieldLengthValidator = (sqlClientType: string) => { export const fieldLengthValidator = () => {
return { return {
validator: (rule: any, value: any) => { validator: (rule: any, value: any) => {
const { t } = getI18n().global const { t } = getI18n().global
// no limit for sqlite but set as 255 // mysql allows 64 characters for column_name
let fieldLengthLimit = 255 // postgres allows 59 characters for column_name
// mssql allows 128 characters for column_name
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') { // sqlite allows any number of characters for column_name
fieldLengthLimit = 64 // We allow 255 for all databases, truncate will be handled by backend for column_name
} else if (sqlClientType === 'pg') { const fieldLengthLimit = 255
fieldLengthLimit = 59
} else if (sqlClientType === 'mssql') {
fieldLengthLimit = 128
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (value?.length > fieldLengthLimit) { if (value?.length > fieldLengthLimit) {

BIN
packages/noco-docs/static/img/favicon.ico vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

36
packages/nocodb-sdk/src/lib/Api.ts

@ -10435,7 +10435,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name List * @name List
* @summary List Table Rows * @summary List Table Rows
* @request GET:/api/v1/tables/{tableId}/rows * @request GET:/api/v2/tables/{tableId}/rows
* @response `200` `{ * @response `200` `{
\** List of data objects *\ \** List of data objects *\
list: (object)[], list: (object)[],
@ -10489,7 +10489,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/rows`, path: `/api/v2/tables/${tableId}/rows`,
method: 'GET', method: 'GET',
query: query, query: query,
format: 'json', format: 'json',
@ -10502,7 +10502,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name Create * @name Create
* @summary Create Table Rows * @summary Create Table Rows
* @request POST:/api/v1/tables/{tableId}/rows * @request POST:/api/v2/tables/{tableId}/rows
* @response `200` `any` OK * @response `200` `any` OK
* @response `400` `{ * @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\ \** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10526,7 +10526,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/rows`, path: `/api/v2/tables/${tableId}/rows`,
method: 'POST', method: 'POST',
query: query, query: query,
body: data, body: data,
@ -10541,7 +10541,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name Update * @name Update
* @summary Update Table Rows * @summary Update Table Rows
* @request PUT:/api/v1/tables/{tableId}/rows * @request PUT:/api/v2/tables/{tableId}/rows
* @response `200` `any` OK * @response `200` `any` OK
* @response `400` `{ * @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\ \** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10565,7 +10565,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/rows`, path: `/api/v2/tables/${tableId}/rows`,
method: 'PUT', method: 'PUT',
query: query, query: query,
body: data, body: data,
@ -10580,7 +10580,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name Delete * @name Delete
* @summary Delete Table Rows * @summary Delete Table Rows
* @request DELETE:/api/v1/tables/{tableId}/rows * @request DELETE:/api/v2/tables/{tableId}/rows
* @response `200` `any` OK * @response `200` `any` OK
* @response `400` `{ * @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\ \** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10604,7 +10604,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/rows`, path: `/api/v2/tables/${tableId}/rows`,
method: 'DELETE', method: 'DELETE',
query: query, query: query,
body: data, body: data,
@ -10619,7 +10619,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name Read * @name Read
* @summary Read Table Row * @summary Read Table Row
* @request GET:/api/v1/tables/{tableId}/rows/{rowId} * @request GET:/api/v2/tables/{tableId}/records/{rowId}
* @response `200` `object` OK * @response `200` `object` OK
* @response `400` `{ * @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\ \** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10650,7 +10650,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/rows/${rowId}`, path: `/api/v2/tables/${tableId}/records/${rowId}`,
method: 'GET', method: 'GET',
query: query, query: query,
format: 'json', format: 'json',
@ -10663,7 +10663,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name Count * @name Count
* @summary Table Rows Count * @summary Table Rows Count
* @request GET:/api/v1/tables/{tableId}/rows/count * @request GET:/api/v2/tables/{tableId}/records/count
* @response `200` `{ * @response `200` `{
count?: number, count?: number,
@ -10697,7 +10697,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/rows/count`, path: `/api/v2/tables/${tableId}/records/count`,
method: 'GET', method: 'GET',
query: query, query: query,
format: 'json', format: 'json',
@ -10710,7 +10710,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name NestedList * @name NestedList
* @summary Get Nested Relations Rows * @summary Get Nested Relations Rows
* @request GET:/api/v1/tables/{tableId}/links/{columnId}/rows/{rowId} * @request GET:/api/v2/tables/{tableId}/links/{columnId}/records/{rowId}
* @response `200` `{ * @response `200` `{
\** List of data objects *\ \** List of data objects *\
list: (object)[], list: (object)[],
@ -10766,7 +10766,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/links/${columnId}/rows/${rowId}`, path: `/api/v2/tables/${tableId}/links/${columnId}/records/${rowId}`,
method: 'GET', method: 'GET',
query: query, query: query,
format: 'json', format: 'json',
@ -10779,7 +10779,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name NestedLink * @name NestedLink
* @summary Create Nested Relations Rows * @summary Create Nested Relations Rows
* @request POST:/api/v1/tables/{tableId}/links/{columnId}/rows/{rowId} * @request POST:/api/v2/tables/{tableId}/links/{columnId}/records/{rowId}
* @response `200` `any` OK * @response `200` `any` OK
* @response `400` `{ * @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\ \** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10805,7 +10805,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/links/${columnId}/rows/${rowId}`, path: `/api/v2/tables/${tableId}/links/${columnId}/records/${rowId}`,
method: 'POST', method: 'POST',
query: query, query: query,
body: data, body: data,
@ -10820,7 +10820,7 @@ export class Api<
* @tags DB Data Table Row * @tags DB Data Table Row
* @name NestedUnlink * @name NestedUnlink
* @summary Delete Nested Relations Rows * @summary Delete Nested Relations Rows
* @request DELETE:/api/v1/tables/{tableId}/links/{columnId}/rows/{rowId} * @request DELETE:/api/v2/tables/{tableId}/links/{columnId}/records/{rowId}
* @response `200` `any` OK * @response `200` `any` OK
* @response `400` `{ * @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\ \** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10846,7 +10846,7 @@ export class Api<
msg: string; msg: string;
} }
>({ >({
path: `/api/v1/tables/${tableId}/links/${columnId}/rows/${rowId}`, path: `/api/v2/tables/${tableId}/links/${columnId}/records/${rowId}`,
method: 'DELETE', method: 'DELETE',
query: query, query: query,
body: data, body: data,

30
packages/nocodb-sdk/src/lib/enums.ts

@ -157,7 +157,9 @@ export enum WorkspaceStatus {
export enum WorkspacePlan { export enum WorkspacePlan {
FREE = 'free', FREE = 'free',
PAID = 'paid', STANDARD = 'standard',
BUSINESS = 'business',
BUSINESS_PRO = 'business-pro',
} }
export const RoleLabels = { export const RoleLabels = {
@ -254,3 +256,29 @@ export const OrderedProjectRoles = [
ProjectRoles.VIEWER, ProjectRoles.VIEWER,
ProjectRoles.NO_ACCESS, ProjectRoles.NO_ACCESS,
]; ];
export enum PlanLimitTypes {
// PER USER
FREE_WORKSPACE_LIMIT = 'FREE_WORKSPACE_LIMIT',
// PER WORKSPACE
WORKSPACE_USER_LIMIT = 'WORKSPACE_USER_LIMIT',
WORKSPACE_ROW_LIMIT = 'WORKSPACE_ROW_LIMIT',
BASE_LIMIT = 'BASE_LIMIT',
// PER BASE
SOURCE_LIMIT = 'SOURCE_LIMIT',
// PER BASE
TABLE_LIMIT = 'TABLE_LIMIT',
// PER TABLE
COLUMN_LIMIT = 'COLUMN_LIMIT',
TABLE_ROW_LIMIT = 'TABLE_ROW_LIMIT',
WEBHOOK_LIMIT = 'WEBHOOK_LIMIT',
VIEW_LIMIT = 'VIEW_LIMIT',
// PER VIEW
FILTER_LIMIT = 'FILTER_LIMIT',
SORT_LIMIT = 'SORT_LIMIT',
}

1
packages/nocodb/package.json

@ -154,6 +154,7 @@
"pg": "^8.10.0", "pg": "^8.10.0",
"redlock": "^5.0.0-beta.2", "redlock": "^5.0.0-beta.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"request-filtering-agent": "^1.1.2",
"request-ip": "^2.1.3", "request-ip": "^2.1.3",
"rmdir": "^1.2.0", "rmdir": "^1.2.0",
"rxjs": "^7.2.0", "rxjs": "^7.2.0",

4
packages/nocodb/src/app.config.ts

@ -2,9 +2,7 @@ import type { AppConfig } from './interface/config';
const config: AppConfig = { const config: AppConfig = {
throttler: { throttler: {
ttl: 60, calc_execution_time: false,
max_apis: 10000,
calc_execution_time: true,
}, },
basicAuth: { basicAuth: {
username: process.env.NC_HTTP_BASIC_USER ?? 'defaultusername', username: process.env.NC_HTTP_BASIC_USER ?? 'defaultusername',

3
packages/nocodb/src/app.module.ts

@ -10,7 +10,6 @@ import { GuiMiddleware } from '~/middlewares/gui/gui.middleware';
import { DatasModule } from '~/modules/datas/datas.module'; import { DatasModule } from '~/modules/datas/datas.module';
import { EventEmitterModule } from '~/modules/event-emitter/event-emitter.module'; import { EventEmitterModule } from '~/modules/event-emitter/event-emitter.module';
import { AuthService } from '~/services/auth.service'; import { AuthService } from '~/services/auth.service';
import { TestModule } from '~/modules/test/test.module';
import { GlobalModule } from '~/modules/global/global.module'; import { GlobalModule } from '~/modules/global/global.module';
import { LocalStrategy } from '~/strategies/local.strategy'; import { LocalStrategy } from '~/strategies/local.strategy';
import { AuthTokenStrategy } from '~/strategies/authtoken.strategy/authtoken.strategy'; import { AuthTokenStrategy } from '~/strategies/authtoken.strategy/authtoken.strategy';
@ -31,7 +30,6 @@ export const ceModuleConfig = {
GlobalModule, GlobalModule,
UsersModule, UsersModule,
AuthModule, AuthModule,
...(process.env['PLAYWRIGHT_TEST'] === 'true' ? [TestModule] : []),
MetasModule, MetasModule,
DatasModule, DatasModule,
EventEmitterModule, EventEmitterModule,
@ -41,7 +39,6 @@ export const ceModuleConfig = {
load: [() => appConfig], load: [() => appConfig],
isGlobal: true, isGlobal: true,
}), }),
TestModule,
], ],
providers: [ providers: [
AuthService, AuthService,

1
packages/nocodb/src/cache/CacheMgr.ts vendored

@ -6,6 +6,7 @@ export default abstract class CacheMgr {
value: any, value: any,
seconds: number, seconds: number,
): Promise<any>; ): Promise<any>;
public abstract incrby(key: string, value: number): Promise<any>;
public abstract del(key: string): Promise<any>; public abstract del(key: string): Promise<any>;
public abstract getAll(pattern: string): Promise<any[]>; public abstract getAll(pattern: string): Promise<any[]>;
public abstract delAll(scope: string, pattern: string): Promise<any[]>; public abstract delAll(scope: string, pattern: string): Promise<any[]>;

5
packages/nocodb/src/cache/NocoCache.ts vendored

@ -38,6 +38,11 @@ export default class NocoCache {
); );
} }
public static async incrby(key, value): Promise<boolean> {
if (this.cacheDisabled) return Promise.resolve(true);
return this.client.incrby(`${this.prefix}:${key}`, value);
}
public static async get(key, type): Promise<any> { public static async get(key, type): Promise<any> {
if (this.cacheDisabled) { if (this.cacheDisabled) {
if (type === CacheGetType.TYPE_ARRAY) return Promise.resolve([]); if (type === CacheGetType.TYPE_ARRAY) return Promise.resolve([]);

5
packages/nocodb/src/cache/RedisCacheMgr.ts vendored

@ -118,6 +118,11 @@ export default class RedisCacheMgr extends CacheMgr {
} }
} }
// @ts-ignore
async incrby(key: string, value = 1): Promise<any> {
return this.client.incrby(key, value);
}
// @ts-ignore // @ts-ignore
async getAll(pattern: string): Promise<any> { async getAll(pattern: string): Promise<any> {
return this.client.hgetall(pattern); return this.client.hgetall(pattern);

5
packages/nocodb/src/cache/RedisMockCacheMgr.ts vendored

@ -117,6 +117,11 @@ export default class RedisMockCacheMgr extends CacheMgr {
} }
} }
// @ts-ignore
async incrby(key: string, value = 1): Promise<any> {
return this.client.incrby(key, value);
}
// @ts-ignore // @ts-ignore
async getAll(pattern: string): Promise<any> { async getAll(pattern: string): Promise<any> {
return this.client.hgetall(pattern); return this.client.hgetall(pattern);

6
packages/nocodb/src/controllers/api-docs/api-docs.controller.ts

@ -11,6 +11,8 @@ import getRedocHtml from './template/redocHtml';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { ApiDocsService } from '~/services/api-docs/api-docs.service'; import { ApiDocsService } from '~/services/api-docs/api-docs.service';
import { PublicApiLimiterGuard } from '~/guards/public-api-limiter.guard';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
export class ApiDocsController { export class ApiDocsController {
@ -20,7 +22,7 @@ export class ApiDocsController {
'/api/v1/db/meta/projects/:baseId/swagger.json', '/api/v1/db/meta/projects/:baseId/swagger.json',
'/api/v1/meta/bases/:baseId/swagger.json', '/api/v1/meta/bases/:baseId/swagger.json',
]) ])
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Acl('swaggerJson') @Acl('swaggerJson')
async swaggerJson(@Param('baseId') baseId: string, @Request() req) { async swaggerJson(@Param('baseId') baseId: string, @Request() req) {
const swagger = await this.apiDocsService.swaggerJson({ const swagger = await this.apiDocsService.swaggerJson({
@ -35,10 +37,12 @@ export class ApiDocsController {
'/api/v1/meta/bases/:baseId/swagger', '/api/v1/meta/bases/:baseId/swagger',
'/api/v1/db/meta/projects/:baseId/swagger', '/api/v1/db/meta/projects/:baseId/swagger',
]) ])
@UseGuards(PublicApiLimiterGuard)
swaggerHtml(@Param('baseId') baseId: string, @Response() res) { swaggerHtml(@Param('baseId') baseId: string, @Response() res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' })); res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
} }
@UseGuards(PublicApiLimiterGuard)
@Get([ @Get([
'/api/v1/db/meta/projects/:baseId/redoc', '/api/v1/db/meta/projects/:baseId/redoc',
'/api/v1/meta/bases/:baseId/redoc', '/api/v1/meta/bases/:baseId/redoc',

3
packages/nocodb/src/controllers/api-tokens.controller.ts

@ -13,9 +13,10 @@ import { GlobalGuard } from '~/guards/global/global.guard';
import { PagedResponseImpl } from '~/helpers/PagedResponse'; import { PagedResponseImpl } from '~/helpers/PagedResponse';
import { ApiTokensService } from '~/services/api-tokens.service'; import { ApiTokensService } from '~/services/api-tokens.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class ApiTokensController { export class ApiTokensController {
constructor(private readonly apiTokensService: ApiTokensService) {} constructor(private readonly apiTokensService: ApiTokensService) {}

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

Loading…
Cancel
Save