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. 4
      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:
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
env:
API_TOKEN_GITHUB: ${{ secrets.GH_TOKEN }}
with:
source_file: 'packages/nocodb/src/schema/swagger.json'
destination_repo: 'nocodb/noco-apis-doc'
destination_folder: 'src'
destination_folder: 'data-apis-v1'
user_email: 'oof1lab@gmail.com'
user_name: 'o1lab'
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
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-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_name: 'o1lab'
commit_message: 'Autorelease from github.com/nocodb/nocodb'

4
SECURITY.md

@ -3,5 +3,5 @@
### Reporting a Vulnerability
Please report (suspected) security vulnerabilities to security@nocodb.com
- You will receive a response from us within 3 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.
- 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.

4
charts/nocodb/templates/deployment.yaml

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

2
charts/nocodb/templates/pvc.yaml

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

4
charts/nocodb/values.yaml

@ -86,6 +86,10 @@ extraSecretEnvs:
NC_DB: "mysql2://mysql:3306?u=nocodb&p=secretPass&d=nocodb"
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
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">
<mask id="mask0_18_1042" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<rect width="16" height="16" fill="#D9D9D9"/>
<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="currentColor"/>
</mask>
<g mask="url(#mask0_18_1042)">
<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="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 mask="url(#mask0_18_1022)">
<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"/>
</g>
</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>
<div class="flex flex-col items-center">
<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="flex font-medium text-base">{{ $t('labels.accountDetails') }}</div>
<div class="flex text-gray-500">{{ $t('labels.controlAppearance') }}</div>
<div class="flex font-medium text-base" data-rec="true">{{ $t('labels.accountDetails') }}</div>
<div class="flex text-gray-500" data-rec="true">{{ $t('labels.controlAppearance') }}</div>
<div class="flex flex-row mt-4">
<div class="flex h-20 mt-1.5">
<GeneralUserIcon size="xlarge" :email="user?.email" />
@ -81,7 +81,7 @@ const onValidate = async (_: any, valid: boolean) => {
@finish="onSubmit"
@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-input
v-model:value="form.title"
@ -90,7 +90,7 @@ const onValidate = async (_: any, valid: boolean) => {
data-testid="nc-account-settings-rename-input"
/>
</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
v-model:value="email"
class="w-full !rounded-md !py-1.5"
@ -98,7 +98,7 @@ const onValidate = async (_: any, valid: boolean) => {
disabled
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
type="primary"
html-type="submit"

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

@ -72,14 +72,19 @@ const resetError = () => {
>
<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 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 />
{{ error }}
</div>
</div>
</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
v-model:value="form.currentPassword"
data-testid="nc-user-settings-form__current-password"
@ -90,7 +95,7 @@ const resetError = () => {
/>
</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
v-model:value="form.password"
data-testid="nc-user-settings-form__new-password"
@ -101,7 +106,12 @@ const resetError = () => {
/>
</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
v-model:value="form.passwordRepeat"
data-testid="nc-user-settings-form__new-password-repeat"
@ -120,7 +130,7 @@ const resetError = () => {
type="primary"
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" />
{{ $t('activity.changePwd') }}
</div>

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

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

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

@ -160,10 +160,10 @@ const handleCancel = () => {
</script>
<template>
<div class="h-full overflow-y-scroll scrollbar-thin-dull pt-2">
<div class="max-w-[810px] mx-auto p-4" data-testid="nc-token-list">
<div class="py-2 flex gap-4 items-center justify-between">
<h6 class="text-2xl my-4 text-left font-bold">{{ $t('title.apiTokens') }}</h6>
<div class="h-full pt-2">
<div class="max-w-202 mx-auto px-4 h-full" data-testid="nc-token-list">
<div class="py-2 flex gap-4 items-baseline justify-between">
<h6 class="text-2xl text-left font-bold" data-rec="true">{{ $t('title.apiTokens') }}</h6>
<NcTooltip :disabled="!(isEeUI && tokens.length)">
<template #title>{{ $t('labels.tokenLimit') }}</template>
<NcButton
@ -175,28 +175,39 @@ const handleCancel = () => {
tooltip="bottom"
@click="showNewTokenModal = true"
>
<span class="hidden md:block">
<span class="hidden md:block" data-rec="true">
{{ $t('title.addNewToken') }}
</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" />
</span>
</NcButton>
</NcTooltip>
</div>
<span>{{ $t('msg.apiTokenCreate') }}</span>
<div class="w-full mt-5 rounded-md h-136 overflow-y-scroll">
<div>
<div class="flex w-full pl-5 bg-gray-50 border-1">
<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 text-start">{{ $t('title.creator') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-3/9 text-start">{{ $t('labels.token') }}</span>
<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 data-rec="true">{{ $t('msg.apiTokenCreate') }}</span>
<div class="mt-5 h-[calc(100%-13rem)]">
<div class="h-full w-full !overflow-hidden rounded-md">
<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" 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" data-rec="true">{{
$t('title.creator')
}}</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>
<main>
<div class="nc-scrollbar-md !overflow-y-auto flex flex-col h-[calc(100%-5rem)]">
<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 class="flex flex-col w-full">
<div
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
:ref="selectInputOnMount"
v-model:value="selectedTokenData.description"
@ -207,7 +218,9 @@ const handleCancel = () => {
data-testid="nc-token-input"
@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 class="flex gap-2 justify-start">
<NcButton v-if="!isLoading" type="secondary" size="small" @click="handleCancel">
@ -224,17 +237,19 @@ const handleCancel = () => {
</NcButton>
</div>
</div>
<NcDivider />
</div>
<div v-if="!tokens.length" class="h-118 justify-center flex items-center">
<a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('title.noLabels')" />
<div
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
v-for="el of tokens"
:key="el.id"
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">
<GeneralTruncateText placement="top" length="20">
@ -250,39 +265,42 @@ const handleCancel = () => {
<GeneralTruncateText v-if="el.token === selectedToken.id && selectedToken.isShow" placement="top" length="29">
{{ el.token }}
</GeneralTruncateText>
<span v-else>**************************************</span>
<span v-else>************************************</span>
</span>
<!-- 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">
<NcTooltip placement="top">
<template #title>{{ $t('labels.showOrHide') }}</template>
<component
:is="iconMap.eye"
class="nc-toggle-token-visibility hover::cursor-pointer"
@click="hideOrShowToken(el.token as string)"
/>
</NcTooltip>
<NcTooltip placement="top" class="h-4">
<template #title>{{ $t('general.copy') }}</template>
<component :is="iconMap.copy" class="hover::cursor-pointer" @click="copyToken(el.token)" />
</NcTooltip>
<NcTooltip placement="top" class="mb-0.5">
<template #title>{{ $t('general.delete') }}</template>
<component
:is="iconMap.delete"
data-testid="nc-token-row-action-icon"
class="nc-delete-icon hover::cursor-pointer"
@click="triggerDeleteModal(el.token as string, el.description as string)"
/>
</NcTooltip>
</div>
</span>
<div class="flex justify-end items-center gap-3 pr-5 text-gray-500 font-medium text-3.5 w-2/9">
<NcTooltip placement="top">
<template #title>{{ $t('labels.showOrHide') }}</template>
<component
:is="iconMap.eye"
class="nc-toggle-token-visibility hover::cursor-pointer w-h-4 mb-[1.8px]"
@click="hideOrShowToken(el.token as string)"
/>
</NcTooltip>
<NcTooltip placement="top" class="h-4">
<template #title>{{ $t('general.copy') }}</template>
<component
:is="iconMap.copy"
class="hover::cursor-pointer w-4 h-4 text-gray-600 mt-0.25"
@click="copyToken(el.token)"
/>
</NcTooltip>
<NcTooltip placement="top" class="mb-0.5">
<template #title>{{ $t('general.delete') }}</template>
<component
:is="iconMap.delete"
data-testid="nc-token-row-action-icon"
class="nc-delete-icon hover::cursor-pointer w-4 h-4"
@click="triggerDeleteModal(el.token as string, el.description as string)"
/>
</NcTooltip>
</div>
</div>
</main>
</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
v-model:current="currentPage"
:total="pagination.total"
@ -313,3 +331,9 @@ const handleCancel = () => {
</GeneralDeleteModal>
</div>
</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>
<template>
<div data-testid="nc-super-user-list">
<div class="max-w-195 mx-auto">
<div class="text-2xl my-4 text-left font-weight-bold">{{ $t('title.userManagement') }}</div>
<div data-testid="nc-super-user-list" class="h-full">
<div class="max-w-195 mx-auto h-full">
<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">
<a-input v-model:value="searchText" class="!max-w-90 !rounded-md" placeholder="Search members" @change="loadUsers()">
<template #prefix>
@ -165,42 +165,50 @@ const openDeleteModal = (user: UserType) => {
<div class="flex gap-3 items-center justify-center">
<component :is="iconMap.reload" class="cursor-pointer" @click="loadUsers(currentPage, currentLimit)" />
<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" />
{{ $t('activity.inviteUser') }}
</div>
</NcButton>
</div>
</div>
<div class="w-full mt-5 border-1 rounded-md h-[613px] max-w-250">
<div class="flex w-full bg-gray-50 border-b-1">
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start pl-10">{{ $t('labels.email') }}</span>
<span class="py-3.5 text-gray-500 font-medium text-3.5 w-1/3 text-start pl-20">{{ $t('objects.role') }}</span>
<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 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-1 rounded-t-md">
<div class="py-3.5 text-gray-500 font-medium text-3.5 w-2/3 text-start pl-6" data-rec="true">
{{ $t('labels.email') }}
</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 v-if="isLoading" class="flex items-center justify-center text-center h-[513px]">
<GeneralLoader size="xlarge" />
</div>
<!-- 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')" />
</div>
<section v-else class="tbody">
<section v-else class="tbody h-[calc(100%-4rem)] nc-scrollbar-md border-t-0 !overflow-auto">
<div
v-for="el of users"
:key="el.id"
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="{
'py-4': el.roles?.includes('super'),
}"
>
<span class="text-3.5 text-start w-1/3 pl-5">
{{ el.email }}
</span>
<span class="text-3.5 text-start w-1/3 pl-18">
<div v-if="el?.roles?.includes('super')" class="font-weight-bold">{{ $t('labels.superAdmin') }}</div>
<a-select
<div class="text-3.5 text-start w-2/3 pl-5 flex items-center">
<GeneralTruncateText length="29">
{{ el.email }}
</GeneralTruncateText>
</div>
<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-model:value="el.roles"
class="w-55 nc-user-roles"
@ -212,8 +220,8 @@ const openDeleteModal = (user: UserType) => {
:value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)"
>
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }}
</span>
</a-select-option>
@ -223,14 +231,14 @@ const openDeleteModal = (user: UserType) => {
:value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)"
>
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }}
</span>
</a-select-option>
</a-select>
</span>
<span class="w-1/3 pl-43">
</NcSelect>
</div>
<span class="w-26 flex items-center justify-end mr-4">
<div
class="flex items-center gap-2"
:class="{
@ -238,28 +246,31 @@ const openDeleteModal = (user: UserType) => {
}"
>
<NcDropdown :trigger="['click']">
<MdiDotsVertical
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)"
/>
<NcButton size="xsmall" type="ghost">
<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>
<NcMenu>
<template v-if="!el.roles?.includes('super')">
<!-- Resend invite Email -->
<NcMenuItem @click="resendInvite(el)">
<component :is="iconMap.email" class="flex text-gray-500" />
<div>{{ $t('activity.resendInvite') }}</div>
<component :is="iconMap.email" class="flex text-gray-600" />
<div data-rec="true">{{ $t('activity.resendInvite') }}</div>
</NcMenuItem>
<NcMenuItem @click="copyInviteUrl(el)">
<component :is="iconMap.copy" class="flex text-gray-500" />
<div>{{ $t('activity.copyInviteURL') }}</div>
<component :is="iconMap.copy" class="flex text-gray-600" />
<div data-rec="true">{{ $t('activity.copyInviteURL') }}</div>
</NcMenuItem>
<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>
</NcMenuItem>
</template>
<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 />
{{ $t('general.remove') }} {{ $t('objects.user') }}
</NcMenuItem>
@ -271,7 +282,7 @@ const openDeleteModal = (user: UserType) => {
</div>
</section>
</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
v-model:current="currentPage"
: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">
<GeneralIcon icon="account" class="nc-view-icon"></GeneralIcon>
<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' }"
>
{{ deleteModalInfo?.email }}
@ -301,7 +312,7 @@ const openDeleteModal = (user: UserType) => {
</template>
<style scoped>
.tbody div:nth-child(10) {
border-bottom: none;
.user:last-child {
@apply rounded-b-md;
}
</style>

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

@ -80,7 +80,7 @@ const copyUrl = async () => {
await copy(inviteUrl.value)
// Copied shareable source url to clipboard!
message.success(t('msg.success.shareableURLCopied'))
message.success(t('msg.toast.inviteUrlCopy'))
} catch (e: any) {
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-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')">
<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-row items-center pl-1.5 pb-1 h-[1.1rem]">
<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>
<a-alert class="!mt-2" type="success" show-icon>
<template #message>
<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 }}
</div>
@ -143,7 +143,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
</template>
</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') }}
{{ usersData.invitationToken && usersData.emails }}
</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">
<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>
</a-button>
</div>
@ -177,7 +177,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
name="emails"
: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
:ref="emailInput"
@ -191,7 +191,7 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
<div class="flex flex-col w-2/4">
<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-option
@ -199,8 +199,8 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
:value="OrgUserRoles.CREATOR"
:label="$t(`objects.roleType.orgLevelCreator`)"
>
<div>{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelCreator`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgCreator') }}
</span>
</a-select-option>
@ -210,8 +210,8 @@ const emailInput: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
:value="OrgUserRoles.VIEWER"
:label="$t(`objects.roleType.orgLevelViewer`)"
>
<div>{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal">
<div data-rec="true">{{ $t(`objects.roleType.orgLevelViewer`) }}</div>
<span class="text-gray-500 text-xs whitespace-normal" data-rec="true">
{{ $t('msg.info.roles.orgViewer') }}
</span>
</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">
<div class="flex flex-row justify-center items-center space-x-1.5">
<MaterialSymbolsSendOutline class="flex h-[0.8rem]" />
<div>{{ $t('activity.invite') }}</div>
<div data-rec="true">{{ $t('activity.invite') }}</div>
</div>
</a-button>
</div>

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

@ -66,10 +66,10 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
<tr>
<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>
<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 class="w-8"></th>
</tr>
@ -124,7 +124,7 @@ const filterOption = (input: string, option: Option) => option.value.toUpperCase
<td :colspan="12" class="">
<NcButton size="small" type="secondary" @click="addHeaderRow">
<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" />
</div>
</NcButton>

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

@ -24,11 +24,11 @@ const deleteParamRow = (i: number) => {
<thead class="h-8">
<tr>
<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>
<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 class="w-8">
@ -69,7 +69,7 @@ const deleteParamRow = (i: number) => {
<td :colspan="12" class="">
<NcButton size="small" type="secondary" @click="addParamRow">
<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" />
</div>
</NcButton>

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

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

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

@ -8,6 +8,7 @@ import {
DropZoneRef,
IsExpandedFormOpenInj,
IsGalleryInj,
IsKanbanInj,
RowHeightInj,
iconMap,
inject,
@ -46,6 +47,8 @@ const isLockedMode = inject(IsLockedInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()!
@ -185,6 +188,7 @@ const onExpand = () => {
v-model="isOverDropZone"
inline
: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"
>
<MaterialSymbolsFileCopyOutline class="text-accent" />
@ -202,7 +206,9 @@ const onExpand = () => {
<component :is="iconMap.reload" v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<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">
<MaterialSymbolsAttachFile
@ -210,6 +216,7 @@ const onExpand = () => {
/>
<div
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"
>
{{ $t('activity.addFiles') }}
@ -223,7 +230,7 @@ const onExpand = () => {
<template v-if="visibleItems.length">
<div
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"
:style="{
maxHeight: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,
@ -240,7 +247,7 @@ const onExpand = () => {
:class="{ 'ml-2': active }"
@click="
() => {
if (isGallery || isMobileMode) return
if (isGallery || isMobileMode || (isKanban && !isExpandedForm)) return
selectedImage = item
}
"

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

@ -54,7 +54,7 @@ onUnmounted(() => {
'pt-0.25': isSharedBase,
}"
>
<LazyDashboardTreeView v-if="!isWorkspaceLoading" />
<DashboardTreeView v-if="!isWorkspaceLoading" />
</div>
<div v-if="!isSharedBase">
<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(() => {
if (contextMenuTarget.type === 'source') {
return contextMenuTarget.value
@ -483,7 +477,7 @@ const projectDelete = () => {
</span>
<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
v-e="['c:base:options']"
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"
@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') }}
</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"
@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
:data-testid="`nc-sidebar-base-${source.alias}`"
class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none"
@ -787,14 +781,6 @@ const projectDelete = () => {
</div>
</NcMenuItem>
</template>
<template v-else>
<NcMenuItem @click="reloadTables">
<div class="nc-base-option-item">
{{ $t('general.reload') }}
</div>
</NcMenuItem>
</template>
</NcMenu>
</template>
</NcDropdown>

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

@ -111,6 +111,14 @@ watch(isMobileMode, () => {
isLeftSidebarOpen.value = !isMobileMode.value
})
watch(sidebarState, () => {
if (sidebarState.value === 'peekCloseEnd') {
setTimeout(() => {
sidebarState.value = 'hiddenEnd'
}, animationDuration)
}
})
onMounted(() => {
handleSidebarOpenOnMobileForNonViews()
})
@ -132,7 +140,7 @@ onMounted(() => {
>
<div
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="{
'mobile': isMobileMode,
'minimized-height': !isLeftSidebarOpen,
@ -171,6 +179,7 @@ onMounted(() => {
> * {
@apply opacity-0;
z-index: -1 !important;
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-name">{{ $t('general.name') }}</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>
</div>
@ -351,58 +351,82 @@ const isEditBaseModalOpen = computed({
<div class="ds-table-col ds-table-actions">
<div class="flex items-center gap-2">
<a-button
v-if="!sources[0].is_meta && !sources[0].is_local"
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" />
<NcTooltip v-if="!sources[0].is_meta && !sources[0].is_local">
<template #title>
{{ $t('tooltip.metaSync') }}
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.ERD)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="erd" class="group-hover:text-accent" />
</template>
<NcButton
class="nc-action-btn cursor-pointer outline-0"
type="text"
data-testid="nc-data-sources-view-meta-sync"
size="small"
@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" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('title.relations') }}
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
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="erd" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('labels.uiAcl') }}
</div>
</a-button>
<a-button
class="nc-action-btn cursor-pointer outline-0"
type="text"
@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" />
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
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="acl" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('title.audit') }}
</div>
</a-button>
</template>
<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 class="ds-table-col ds-table-crud">
<a-button
<NcButton
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"
type="text"
@click="baseAction(sources[0].id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600" />
</a-button>
</NcButton>
</div>
</div>
</template>
@ -419,12 +443,12 @@ const isEditBaseModalOpen = computed({
</a-tooltip>
</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" />
<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 }}
</div>
</span>
</div>
<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="flex items-center gap-2">
<a-button
class="nc-action-btn cursor-pointer outline-0"
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" />
<NcTooltip>
<template #title>
{{ $t('title.relations') }}
</div>
</a-button>
<a-button
type="text"
class="nc-action-btn cursor-pointer outline-0"
@click="baseAction(source.id, DataSourcesSubTab.UIAcl)"
>
<div class="flex items-center gap-2 text-gray-600">
<GeneralIcon icon="acl" class="group-hover:text-accent" />
</template>
<NcButton
size="small"
class="nc-action-btn cursor-pointer outline-0"
type="text"
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="erd" class="group-hover:text-accent" />
</div>
</NcButton>
</NcTooltip>
<NcTooltip>
<template #title>
{{ $t('labels.uiAcl') }}
</div>
</a-button>
<a-button
v-if="!source.is_meta && !source.is_local"
type="text"
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" />
</template>
<NcButton
size="small"
type="text"
class="nc-action-btn cursor-pointer outline-0"
data-testid="nc-data-sources-view-ui-acl"
@click="baseAction(source.id, DataSourcesSubTab.UIAcl)"
>
<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('tooltip.metaSync') }}
</div>
</a-button>
</template>
<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 class="ds-table-col ds-table-crud justify-end gap-x-1">
<a-button
v-if="!source.is_meta && !source.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text"
@click="baseAction(source.id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600 -mt-0.5" />
</a-button>
<a-button
v-if="!source.is_meta && !source.is_local"
class="nc-action-btn cursor-pointer outline-0 !w-8 !px-1 !rounded-lg mt-0.5"
type="text"
@click="openDeleteBase(source)"
>
<GeneralIcon icon="delete" class="text-red-500 -mt-0.5" />
</a-button>
<NcTooltip>
<template #title>
{{ $t('general.edit') }}
</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="baseAction(source.id, DataSourcesSubTab.Edit)"
>
<GeneralIcon icon="edit" class="text-gray-600" />
</NcButton>
</NcTooltip>
<NcTooltip>
<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>
</template>
</Draggable>
</div>
</div>
<GeneralModal v-model:visible="isNewBaseModalOpen" closable :mask-closable="false" size="medium">
<div class="py-6 px-8">
<LazyDashboardSettingsDataSourcesCreateBase
:connection-type="clientType"
@source-created="loadBases(true)"
@close="isNewBaseModalOpen = false"
/>
</div>
</GeneralModal>
<LazyDashboardSettingsDataSourcesCreateBase
v-model:open="isNewBaseModalOpen"
:connection-type="clientType"
@source-created="loadBases(true)"
/>
<GeneralModal v-model:visible="isErdModalOpen" size="large">
<div class="h-[80vh]">
<LazyDashboardSettingsErd :source-id="activeBaseId" />
@ -550,7 +600,7 @@ const isEditBaseModalOpen = computed({
<style>
.ds-table-head {
@apply flex items-center border-0 text-gray-400;
@apply flex items-center border-0 text-gray-500;
}
.ds-table-body {
@ -570,15 +620,15 @@ const isEditBaseModalOpen = computed({
}
.ds-table-name {
@apply col-span-6 items-center capitalize;
@apply col-span-9 items-center capitalize;
}
.ds-table-type {
@apply col-span-3 items-center;
@apply col-span-2 items-center;
}
.ds-table-actions {
@apply col-span-7;
@apply col-span-5 flex w-full justify-end;
}
.ds-table-crud {

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

@ -27,9 +27,11 @@ import {
watch,
} 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)
@ -284,7 +286,7 @@ const createSource = async () => {
}
emit('sourceCreated')
emit('close')
vOpen.value = false
creatingSource.value = false
} else if (status === JobStatus.FAILED) {
message.error('Failed to create base')
@ -295,6 +297,7 @@ const createSource = async () => {
)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
creatingSource.value = false
} finally {
refreshCommandPalette()
}
@ -404,263 +407,295 @@ watch(
},
{ immediate: true },
)
const toggleModal = (val: boolean) => {
vOpen.value = val
}
</script>
<template>
<div class="create-source bg-white relative flex flex-col justify-center gap-2 w-full">
<h1 class="prose-xl font-bold self-start mb-4 flex items-center gap-2">
{{ $t('title.newBase') }}
<DashboardSettingsDataSourcesInfo />
<span class="flex-grow"></span>
</h1>
<a-form ref="form" :model="formState" name="external-base-create-form" layout="horizontal" no-style :label-col="{ span: 8 }">
<div
class="nc-scrollbar-md"
:style="{
maxHeight: '60vh',
}"
>
<a-form-item label="Source Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item>
<a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']">
<a-select
v-model:value="formState.dataSource.client"
class="nc-extdb-db-type"
dropdown-class-name="nc-dropdown-ext-db-type"
@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']"
<GeneralModal
:visible="vOpen"
:closable="!creatingSource"
:keyboard="!creatingSource"
:mask-closable="false"
size="medium"
@update:visible="toggleModal"
>
<div class="py-6 px-8">
<div class="create-source bg-white relative flex flex-col justify-center gap-2 w-full">
<h1 class="prose-xl font-bold self-start mb-4 flex items-center gap-2">
{{ $t('title.newBase') }}
<DashboardSettingsDataSourcesInfo />
<span class="flex-grow"></span>
</h1>
<a-form
ref="form"
:model="formState"
name="external-base-create-form"
layout="horizontal"
no-style
:label-col="{ span: 8 }"
>
<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>
<!-- 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']"
<div
class="nc-scrollbar-md"
:style="{
maxHeight: '60vh',
}"
>
<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>
</div>
<a-collapse ghost expand-icon-position="right" class="!mt-6">
<a-collapse-panel key="1">
<template #header>
<span>{{ $t('title.advancedParameters') }}</span>
</template>
<a-form-item label="SSL mode">
<a-select v-model:value="formState.sslUse" dropdown-class-name="nc-dropdown-ssl-mode" @select="onSSLModeChange">
<a-select-option v-for="opt in Object.values(SSLUsage)" :key="opt" :value="opt">{{ opt }} </a-select-option>
</a-select>
<a-form-item label="Source Name" v-bind="validateInfos.title">
<a-input v-model:value="formState.title" class="nc-extdb-proj-name" />
</a-form-item>
<a-form-item :label="$t('labels.dbType')" v-bind="validateInfos['dataSource.client']">
<a-select
v-model:value="formState.dataSource.client"
class="nc-extdb-db-type"
dropdown-class-name="nc-dropdown-ext-db-type"
@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" />
</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 label="SSL keys">
<div class="flex gap-2">
<a-tooltip placement="top">
<!-- Select .cert file -->
<template #title>
<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>
<!-- 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>
<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>
<!-- 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>
<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>
<!-- 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>
<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>
<!-- 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>
<div class="flex justify-end">
<NcButton type="primary" size="small" class="!rounded-md" @click="handleEditJSON()">
<!-- Edit connection JSON -->
{{ $t('activity.editConnJson') }}
<!-- 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>
<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>
</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>
<a-collapse ghost expand-icon-position="right" class="!mt-6">
<a-collapse-panel key="1">
<template #header>
<span>{{ $t('title.advancedParameters') }}</span>
</template>
<a-form-item label="SSL mode">
<a-select
v-model:value="formState.sslUse"
dropdown-class-name="nc-dropdown-ssl-mode"
@select="onSSLModeChange"
>
<a-select-option v-for="opt in Object.values(SSLUsage)" :key="opt" :value="opt">{{ opt }} </a-select-option>
</a-select>
</a-form-item>
<a-form-item label="SSL keys">
<div class="flex gap-2">
<a-tooltip placement="top">
<!-- Select .cert file -->
<template #title>
<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>
<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>
<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">
<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">
<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
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
: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" />
<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-show="isEaster" v-model:checked="options.includeHooks">{{ $t('labels.includeWebhook') }}</a-checkbox>
</div>

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

@ -1,5 +1,15 @@
<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 {
uuid?: string
@ -20,9 +30,23 @@ const sharedBase = ref<null | ShareBase>(null)
const { base } = storeToRefs(useBase())
const url = computed(() =>
sharedBase.value && sharedBase.value.uuid ? `${dashboardUrl.value}#/base/${sharedBase.value.uuid}` : '',
)
const { getBaseUrl, appInfo } = useGlobal()
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 () => {
try {
@ -50,6 +74,8 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
sharedBase.value = res ?? {}
sharedBase.value!.role = role
base.value.uuid = res.uuid
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -63,6 +89,8 @@ const disableSharedBase = async () => {
await $api.base.sharedBaseDisable(base.value.id)
sharedBase.value = null
base.value.uuid = undefined
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -131,7 +159,7 @@ const onRoleToggle = async () => {
</div>
<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" />
<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>
<a-switch
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 { $e } = useNuxtApp()
const { getBaseUrl, appInfo } = useGlobal()
const { dashboardUrl } = useDashboard()
@ -13,6 +14,8 @@ const viewStore = useViewsStore()
const { metas } = useMetas()
const workspaceStore = useWorkspace()
const isUpdating = ref({
public: false,
password: false,
@ -162,7 +165,15 @@ function sharedViewUrl() {
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 () => {

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

@ -32,7 +32,7 @@ const hasColumns = computed(() => data.pkAndFkColumns.length || data.nonPkColumn
const nonPkColumns = computed(() =>
data.nonPkColumns
// 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(
@ -65,7 +65,7 @@ watch(
: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"
>
<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">
{{ table.title }}
</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 MdiDatabaseOutline from '~icons/mdi/database-outline'
const { baseType } = defineProps<{ baseType?: string }>()
const { sourceType } = defineProps<{ sourceType?: string }>()
const baseIcon = computed(() => {
switch (baseType) {
switch (sourceType) {
case ClientType.MYSQL:
return LogosMysqlIcon
case ClientType.PG:

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

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

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

@ -1,5 +1,5 @@
<script setup lang="ts">
import { isDrawerOrModalExist, isEeUI, isMac, useNuxtApp, useRoles } from '#imports'
import { isDrawerOrModalExist, isMac, useNuxtApp, useRoles } from '#imports'
interface Props {
disabled?: boolean
@ -8,11 +8,12 @@ interface Props {
const { disabled, isViewToolbar } = defineProps<Props>()
const { isMobileMode } = useGlobal()
const { isMobileMode, getMainUrl } = useGlobal()
const { visibility, showShareModal } = storeToRefs(useShare())
const { activeTable } = storeToRefs(useTablesStore())
const { base, isSharedBase } = storeToRefs(useBase())
const { $e } = useNuxtApp()
@ -38,7 +39,8 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
})
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>
@ -70,7 +72,7 @@ const copySharedBase = async () => {
</NcButton>
</div>
<template v-else-if="isSharedBase && isEeUI">
<template v-else-if="isSharedBase">
<div class="flex-1"></div>
<div class="flex flex-col justify-center h-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" />
</NcButton>
<div class="text-gray-600">
<div v-if="!isMobileMode" class="text-gray-600">
<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>
<span class="mx-1"> {{ mode !== 'full' ? '/' : 'of' }} </span>
@ -104,3 +107,13 @@ const pagesList = computed(() => {
</NcButton>
</div>
</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
placement?: TooltipPlacement | undefined
hideOnClick?: boolean
overlayClassName?: string
}
const props = defineProps<Props>()
@ -36,6 +37,8 @@ const attrs = useAttrs()
const isKeyPressed = ref(false)
const overlayClassName = computed(() => props.overlayClassName)
onKeyStroke(
(e) => e.key === modifierKey.value,
(e) => {
@ -100,7 +103,7 @@ const onClick = () => {
<template>
<a-tooltip
v-model:visible="showTooltip"
:overlay-class-name="`nc-tooltip ${showTooltip ? 'visible' : 'hidden'}`"
:overlay-class-name="`nc-tooltip ${showTooltip ? 'visible' : 'hidden'} ${overlayClassName}`"
:overlay-style="tooltipStyle"
arrow-point-at-center
:trigger="[]"

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

@ -147,8 +147,8 @@ const onCreateBaseClick = () => {
</div>
<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-else>
<GeneralBaseLogo :source-type="sources.get(table.source_id!)?.type" class="w-4 mr-1" />
<div v-else class="capitalize flex flex-row items-center gap-x-0.5">
<GeneralBaseLogo class="w-4 mr-1" />
{{ sources.get(table.source_id!)?.alias }}
</div>
</div>
@ -158,11 +158,7 @@ const onCreateBaseClick = () => {
</div>
</div>
<ProjectImportModal v-if="defaultBase" v-model:visible="isImportModalOpen" :source="defaultBase" />
<GeneralModal v-model:visible="isNewBaseModalOpen" size="medium">
<div class="py-6 px-8">
<LazyDashboardSettingsDataSourcesCreateBase @close="isNewBaseModalOpen = false" />
</div>
</GeneralModal>
<LazyDashboardSettingsDataSourcesCreateBase v-model:open="isNewBaseModalOpen" />
</div>
</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'
const { openedProject } = storeToRefs(useBases())
const { activeTables } = storeToRefs(useTablesStore())
const { activeWorkspace } = storeToRefs(useWorkspace())
const { activeWorkspace, workspaceUserCount } = storeToRefs(useWorkspace())
const { navigateToProjectPage } = useBase()
@ -113,6 +113,16 @@ watch(
<div class="tab-title" data-testid="proj-view-tab__access-settings">
<GeneralIcon icon="users" class="!h-3.5 !w-3.5" />
<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>
</template>
<ProjectAccessSettings />
@ -130,7 +140,7 @@ watch(
'bg-gray-50': projectPageTab !== 'data-source',
}"
>
{{ base.sources.length - 1 }}
{{ base.sources.length }}
</div>
</div>
</template>

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

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

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

@ -1,6 +1,6 @@
<script lang="ts" setup>
import Draggable from 'vuedraggable'
import { UITypes, ViewTypes, isVirtualCol } from 'nocodb-sdk'
import { ViewTypes, isVirtualCol } from 'nocodb-sdk'
import {
ActiveViewInj,
FieldsInj,
@ -16,7 +16,6 @@ import {
iconMap,
inject,
isImage,
isLTAR,
onBeforeUnmount,
provide,
useAttachment,
@ -56,6 +55,8 @@ const expandedFormRow = ref<RowType>()
const expandedFormRowState = ref<Record<string, any>>()
provide(RowHeightInj, ref(1 as const))
const deleteStackVModel = ref(false)
const stackToBeDeleted = ref('')
@ -107,7 +108,9 @@ const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
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(() =>
meta.value?.columnsById
@ -209,6 +212,8 @@ const expandedFormOnRowIdDlg = computed({
})
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) {
expandForm(row)
}
@ -380,6 +385,11 @@ watch(
immediate: true,
},
)
const getRowId = (row: RowType) => {
const pk = extractPkFromRow(row.row, meta.value!.columns!)
return pk ? `row-${pk}` : ''
}
</script>
<template>
@ -425,7 +435,7 @@ watch(
<!-- Non Collapsed Stacks -->
<a-card
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="{
'not-draggable': stack.title === null || isLocked || isPublic || !hasEditPermission,
@ -516,95 +526,123 @@ watch(
@end="(e) => e.target.classList.remove('grabbing')"
@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">
<LazySmartsheetRow :row="record">
<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"
class="!rounded-xl h-full overflow-hidden break-all max-w-[450px]"
:data-testid="`nc-gallery-card-${record.row.id}`"
:class="{
'not-draggable': isLocked || !hasEditPermission || isPublic,
'!cursor-default': isLocked || !hasEditPermission || isPublic,
}"
:body-style="{ padding: '10px' }"
@click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, record)"
>
<template v-if="kanbanMetaData?.fk_cover_image_col_id" #cover>
<a-carousel
v-if="!reloadAttachments && attachments(record).length"
autoplay
class="gallery-carousel"
arrows
>
<template #customPaging>
<a>
<div class="pt-[12px]">
<div></div>
<template v-if="!reloadAttachments && attachments(record).length">
<a-carousel
:key="attachments(record).reduce((acc, curr) => acc + curr?.path, '')"
class="gallery-carousel !border-b-1 !border-gray-200"
>
<template #customPaging>
<a>
<div class="pt-[12px]">
<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>
</a>
</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>
</template>
<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>
<div
v-for="col in fieldsWithoutCover"
:key="`record-${record.row.id}-${col.id}`"
class="flex flex-col rounded-lg w-full"
>
<div v-if="!isRowEmpty(record, col) || isLTAR(col.uidt, col.colOptions)">
<!-- Smartsheet Header (Virtual) Cell -->
<div class="flex flex-row w-full justify-start pt-2">
<div class="w-full text-gray-400">
<h2 v-if="displayField" class="text-base mt-3 mx-3 font-bold">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
class="!text-gray-600"
:column="displayField"
:row="record"
/>
<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
v-if="isVirtualCol(col)"
:column="col"
: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>
<!-- Smartsheet (Virtual) Cell -->
<div
class="flex flex-row w-full items-center justify-start"
:class="{ '!ml-[-12px] pl-3': col.uidt === UITypes.SingleSelect }"
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-700 px-1 mt-[-0.25rem] items-center justify-start"
>
<LazySmartsheetVirtualCell
v-if="col.title && isVirtualCol(col)"
v-if="isVirtualCol(col)"
v-model="record.row[col.title]"
class="text-sm pt-1 pl-5"
:column="col"
:row="record"
/>
<LazySmartsheetCell
v-else-if="col.title"
v-else
v-model="record.row[col.title]"
class="text-sm pt-1 pl-7.25"
:column="col"
:edit-enabled="false"
:read-only="true"
/>
</div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">-</div>
</div>
</div>
</a-card>
@ -755,4 +793,37 @@ watch(
transform-origin: left top 0px;
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>

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

@ -14,6 +14,7 @@ interface Props {
fixedSize?: number
extraStyle?: string
showApiTiming?: boolean
alignLeft?: boolean
}
const props = defineProps<Props>()
@ -34,6 +35,8 @@ const extraStyle = toRef(props, 'extraStyle')
const isGroupBy = inject(IsGroupByInj, ref(false))
const alignLeft = computed(() => props.alignLeft ?? false)
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
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;' : ''
}${extraStyle}`"
>
<div class="flex-1 flex items-center">
<div
class="flex items-center"
:class="{
'flex-1': !alignLeft,
}"
>
<slot name="add-record" />
<span
v-if="!alignCountOnRight && count !== null && count !== Infinity"
@ -84,7 +92,8 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
v-if="!hidePagination"
class="transition-all duration-350"
: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">

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

@ -48,7 +48,7 @@ const { getMeta } = useMetas()
const { t } = useI18n()
const columnLabel = computed(() => props.columnLabel || t('objects.column'))
const columnLabel = computed(() => props.columnLabel || t('objects.field'))
const { $e } = useNuxtApp()
@ -66,7 +66,7 @@ const isForm = inject(IsFormInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const { isMysql, isMssql } = useBase()
const { isMysql, isMssql, isXcdbBase } = useBase()
const reloadDataTrigger = inject(ReloadViewDataHookInj)
@ -86,9 +86,9 @@ const showDeprecated = ref(false)
const uiTypesOptions = computed<typeof uiTypes>(() => {
return [
...uiTypes.filter(
(t) => geoDataToggleCondition(t) && (!isEdit.value || !t.virtual) && (!t.deprecated || showDeprecated.value),
),
...uiTypes
.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)
? [
{
@ -214,7 +214,7 @@ if (props.fromTableExplorer) {
'bg-white': !props.fromTableExplorer,
'w-[400px]': !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,
}"
@keydown="handleEscape"
@ -331,7 +331,7 @@ if (props.fromTableExplorer) {
</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"
@click="advancedOptions = !advancedOptions"
>

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

@ -44,7 +44,7 @@ const refTables = computed(() => {
isLinksOrLTAR(column) &&
!column.system &&
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) => ({
col: column.colOptions,
@ -61,7 +61,8 @@ const columns = computed<ColumnType[]>(() => {
return []
}
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>
<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-select
v-model:value="vModel.fk_relation_column_id"
@ -104,7 +105,7 @@ const cellIcon = (column: ColumnType) =>
</a-select>
</a-form-item>
<a-form-item class="flex w-1/2" :label="$t('labels.childColumn')" v-bind="validateInfos.fk_lookup_column_id">
<a-form-item class="flex w-1/2" :label="$t('labels.childField')" v-bind="validateInfos.fk_lookup_column_id">
<a-select
v-model:value="vModel.fk_lookup_column_id"
name="fk_lookup_column_id"
@ -120,6 +121,7 @@ const cellIcon = (column: ColumnType) =>
</a-select>
</a-form-item>
</div>
<div v-else>{{ $t('msg.linkColumnClearNotSupportedYet') }}</div>
</div>
</template>

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

@ -3,7 +3,7 @@ import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk'
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 {
color: string
@ -21,16 +21,22 @@ const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, isMysql, isPg } = useColumnCreateStoreOrThrow()
const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
// const { base } = storeToRefs(useBase())
const { optionsMagic: _optionsMagic } = useNocoEe()
const optionsWrapperDomRef = ref<HTMLElement>()
const options = ref<(Option & { status?: 'remove' })[]>([])
const isAddingOption = ref(false)
// TODO: Implement proper top and bottom virtual scrolling
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 savedDefaultOption = ref<Option | null>(null)
@ -40,7 +46,6 @@ const colorMenus = ref<any>({})
const colors = ref(enumColor.light)
const inputs = ref()
const defaultOption = ref()
const isKanban = inject(IsKanbanInj, ref(false))
@ -89,11 +94,13 @@ onMounted(() => {
}
}
isReverseLazyLoad.value = false
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
for (const op of options.value.filter((el) => el.order === null)) {
@ -129,19 +136,36 @@ const getNextColor = () => {
}
const addNewOption = () => {
isAddingOption.value = true
const tempOption = {
title: '',
color: getNextColor(),
}
options.value.push(tempOption)
loadedOptionCount.value = options.value.length
renderedOptions.value = [...options.value]
isReverseLazyLoad.value = true
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(() => {
if (inputs.value?.$el) {
inputs.value.$el.focus()
}
// Last child doesnt work for query selector
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) => {
if (loadedOptionCount.value === options.value.length) {
// focus last created input
// 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()
return
}
$state.loading()
loadedOptionCount.value += OPTIONS_PAGE_COUNT
loadedOptionCount.value = Math.min(loadedOptionCount.value, options.value.length)
loadedOptionAnchor.value -= OPTIONS_PAGE_COUNT
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()
return
}
$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>
<template>
<div class="w-full">
<div
class="overflow-x-auto scrollbar-thin-dull"
ref="optionsWrapperDomRef"
class="nc-col-option-select-option overflow-x-auto scrollbar-thin-dull"
:style="{
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">
<template #item="{ element, index }">
<div class="flex py-1 items-center nc-select-option">
@ -245,9 +322,8 @@ const loadListData = async ($state: any) => {
</a-dropdown>
<a-input
ref="inputs"
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}`"
:disabled="element.status === 'remove'"
@keydown.enter.prevent="element.title?.trim() && addNewOption()"
@ -273,7 +349,7 @@ const loadListData = async ($state: any) => {
</div>
</template>
</Draggable>
<InfiniteLoading v-bind="$attrs" @infinite="loadListData">
<InfiniteLoading v-if="!isReverseLazyLoad" v-bind="$attrs" @infinite="loadListData">
<template #spinner>
<div class="flex flex-row w-full justify-center mt-2">
<GeneralLoader />

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

@ -23,6 +23,13 @@ const indicator = h(LoadingOutlined, {
<a-spin size="large" :indicator="indicator" />
</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>
</template>

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

@ -43,6 +43,8 @@ const moveOps = ref<moveOp[]>([])
const visibilityOps = ref<fieldsVisibilityOps[]>([])
const fieldsListWrapperDomRef = ref<HTMLElement>()
const {
fields: viewFields,
toggleFieldVisibility,
@ -192,6 +194,13 @@ const addField = (field?: TableExplorerColumn, before = false) => {
setFieldMoveHook(field, before)
}
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(() => {
@ -626,7 +635,7 @@ onMounted(async () => {
</div>
</div>
<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)">
<template #item="{ element: field }">
<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 { user } = useGlobal()
const { user, appInfo } = useGlobal()
const tab = ref<'comments' | 'audits'>('comments')
@ -98,6 +98,12 @@ const saveComment = async () => {
watch(commentsWrapperEl, () => {
scrollComments()
})
const onClickAudit = () => {
if (appInfo.value.ee) return
tab.value = 'audits'
}
</script>
<template>
@ -108,7 +114,7 @@ watch(commentsWrapperEl, () => {
v-e="['c:row-expand:comment']"
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
: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'"
>
@ -117,13 +123,29 @@ watch(commentsWrapperEl, () => {
Comments
</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
v-else
v-e="['c:row-expand:audit']"
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
:class="{
'bg-white shadow !text-brand-500 !hover:text-brand-500': tab === 'audits',
}"
@click="tab = 'audits'"
@click="onClickAudit"
>
<div class="tab-title nc-tab">
<MdiFileDocumentOutline class="h-4 w-4" />
@ -135,7 +157,7 @@ watch(commentsWrapperEl, () => {
<div
class="h-[calc(100%-4rem)]"
:class="{
'pb-2': tab !== 'comments',
'pb-2': tab !== 'comments' && !appInfo.ee,
}"
>
<div v-if="tab === 'comments'" class="flex flex-col h-full">
@ -251,6 +273,9 @@ watch(commentsWrapperEl, () => {
</template>
<style scoped>
.tab {
@apply max-w-1/2;
}
.tab .tab-title {
@apply min-w-0 flex justify-center gap-2 font-semibold items-center;
word-break: 'keep-all';

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

@ -1,6 +1,6 @@
<script setup lang="ts">
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 MdiChevronDown from '~icons/mdi/chevron-down'
@ -47,6 +47,8 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const { activeView } = storeToRefs(useViewsStore())
const key = ref(0)
const wrapper = ref()
@ -86,6 +88,8 @@ const { isUIAllowed } = useRoles()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
// override cell click hook to avoid unexpected behavior at form fields
provide(CellClickHookInj, undefined)
@ -184,7 +188,16 @@ const save = async () => {
await syncLTARRefs(data)
reloadTrigger?.trigger()
} 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()
}
isUnsavedFormExist.value = false
@ -389,7 +402,7 @@ const onDeleteRowClick = () => {
const onConfirmDeleteRowClick = async () => {
showDeleteRowModal.value = false
await deleteRowById(primaryKey.value)
message.success('Row deleted')
message.success('Record deleted')
reloadTrigger.trigger()
onClose()
showDeleteRowModal.value = false

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

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

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

@ -1,4 +1,5 @@
<script lang="ts" setup>
import axios from 'axios'
import { nextTick } from '@vue/runtime-core'
import type { ColumnReqType, ColumnType, PaginatedType, TableType, ViewType } 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())
useViewColumns(view, meta, () => reloadViewDataHook.trigger())
const { isViewColumnsLoading } = useViewColumns(view, meta, () => reloadViewDataHook.trigger())
const { isMobileMode } = useGlobal()
@ -383,11 +384,19 @@ const gridWrapperClass = computed<string>(() => {
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(() => ({}))
})
const showSkeleton = computed(() => disableSkeleton !== true && (isViewDataLoading.value || isPaginationLoading.value))
const showSkeleton = computed(
() => disableSkeleton !== true && (isViewDataLoading.value || isPaginationLoading.value || isViewColumnsLoading.value),
)
// #Grid
@ -405,7 +414,8 @@ const closeAddColumnDropdown = (scrollToLastCol = false) => {
preloadColumn.value = {}
if (scrollToLastCol) {
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) {
lastAddNewRowHeader.scrollIntoView({ behavior: 'smooth' })
}
@ -884,7 +894,7 @@ const saveOrUpdateRecords = async (args: { metaValue?: TableType; viewMetaValue?
}
// #Grid Resize
const { updateGridViewColumn, resizingColWidth, resizingCol } = useGridViewColumnOrThrow()
const { updateGridViewColumn, gridViewCols, resizingColOldWith } = useGridViewColumnOrThrow()
const onresize = (colID: string | undefined, event: any) => {
if (!colID) return
@ -893,8 +903,12 @@ const onresize = (colID: string | undefined, event: any) => {
const onXcResizing = (cn: string | undefined, event: any) => {
if (!cn) return
resizingCol.value = cn
resizingColWidth.value = event.detail
gridViewCols.value[cn].width = `${event.detail}`
}
const onXcStartResizing = (cn: string | undefined, event: any) => {
if (!cn) return
resizingColOldWith.value = event.detail
}
const loadColumn = (title: string, tp: string, colOptions?: any) => {
@ -1059,6 +1073,22 @@ onBeforeUnmount(async () => {
reloadViewDataHook?.on(reloadViewDataHandler)
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
// reset context menu target on hide
@ -1099,8 +1129,10 @@ watch(
try {
await loadData?.()
} catch (e) {
console.log(e)
message.error(t('msg.errorLoadingData'))
if (!axios.isCancel(e)) {
console.log(e)
message.error(t('msg.errorLoadingData'))
}
} finally {
isViewDataLoading.value = false
}
@ -1163,7 +1195,7 @@ const loaderText = computed(() => {
<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
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"
>
<div class="flex flex-col justify-center gap-2">
@ -1187,11 +1219,11 @@ const loaderText = computed(() => {
@contextmenu="showContextMenu"
>
<thead v-show="hideHeader !== true" ref="tableHeadEl">
<tr v-if="showSkeleton && isPaginationLoading">
<tr v-if="isViewColumnsLoading">
<td
v-for="(col, colIndex) of dummyDataForLoading"
v-for="(col, colIndex) of dummyColumnDataForLoading"
: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 }"
>
<a-skeleton
@ -1203,7 +1235,7 @@ const loaderText = computed(() => {
/>
</td>
</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="() => {}">
<div class="w-full h-full flex pl-5 pr-1 items-center" data-testid="nc-check-all">
<template v-if="!readOnly">
@ -1228,9 +1260,14 @@ const loaderText = computed(() => {
v-xc-ver-resize
:data-col="col.id"
: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)"
@xcresizing="onXcResizing(col.title, $event)"
@xcresized="resizingCol = null"
@xcresizing="onXcResizing(col.id, $event)"
>
<div class="w-full h-full flex items-center">
<LazySmartsheetHeaderVirtualCell
@ -1352,10 +1389,11 @@ const loaderText = computed(() => {
</thead>
<tbody v-if="headerOnly !== true" ref="tableBodyEl">
<template v-if="showSkeleton">
<tr v-for="(row, rowIndex) of dummyDataForLoading" :key="rowIndex">
<tr v-for="(row, rowIndex) of dummyRowDataForLoading" :key="rowIndex">
<td
v-for="(col, colIndex) of dummyDataForLoading"
v-for="(col, colIndex) of dummyColumnDataForLoading"
:key="colIndex"
class="border-b-1 border-r-1"
:class="{ 'min-w-50': colIndex !== 0, 'min-w-21.25': colIndex === 0 }"
></td>
</tr>
@ -1456,6 +1494,11 @@ const loaderText = computed(() => {
hasEditPermission &&
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-key="`data-key-${rowIndex}-${columnObj.id}`"
:data-col="columnObj.id"
@ -1639,8 +1682,9 @@ const loaderText = computed(() => {
v-if="headerOnly !== true"
:key="isMobileMode"
v-model:pagination-data="paginationDataRef"
show-api-timing
:show-api-timing="!isGroupBy"
align-count-on-right
:align-left="isGroupBy"
:change-page="changePage"
:hide-sidebars="paginationStyleRef?.hideSidebars === true"
:fixed-size="paginationStyleRef?.fixedSize"
@ -1949,4 +1993,11 @@ tbody tr:hover {
.nc-fill-handle:focus {
@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 lang="scss"></style>

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

@ -1,8 +1,9 @@
<script setup lang="ts">
import type { ColumnType, FilterType } from 'nocodb-sdk'
import { UITypes } from 'nocodb-sdk'
import { PlanLimitTypes, UITypes } from 'nocodb-sdk'
import {
ActiveViewInj,
AllFiltersInj,
MetaInj,
ReloadViewDataHookInj,
comparisonOpList,
@ -56,6 +57,8 @@ const activeView = inject(ActiveViewInj, ref())
const reloadDataHook = inject(ReloadViewDataHookInj)!
const isPublic = inject(IsPublicInj, ref(false))
const { $e } = useNuxtApp()
const { nestedFilters } = useSmartsheetStoreOrThrow()
@ -83,6 +86,8 @@ const {
webHook.value,
)
const { getPlanLimit } = useWorkspace()
const localNestedFilters = ref()
const wrapperDomRef = ref<HTMLElement>()
@ -183,13 +188,22 @@ watch(
},
)
const allFilters: Ref<Record<string, FilterType[]>> = inject(AllFiltersInj, ref({}))
watch(
() => nonDeletedFilters.value.length,
(length: number) => {
allFilters.value[parentId?.value ?? 'root'] = [...nonDeletedFilters.value]
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) => {
await sync(hookId, _nested)
@ -299,6 +313,10 @@ onMounted(() => {
onMounted(async () => {
await loadBtLookupTypes()
})
onBeforeUnmount(() => {
if (parentId.value) delete allFilters.value[parentId.value]
})
</script>
<template>
@ -471,23 +489,44 @@ onMounted(async () => {
</template>
</div>
<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 v-if="isEeUI && !isPublic">
<div v-if="filtersCount < getPlanLimit(PlanLimitTypes.FILTER_LIMIT)" 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>
<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
v-if="!filters.length"
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">
import {
ActiveViewInj,
AllFiltersInj,
IsLockedInj,
computed,
iconMap,
inject,
ref,
useGlobal,
@ -10,7 +12,6 @@ import {
useSmartsheetStoreOrThrow,
useViewFilters,
watch,
iconMap,
} from '#imports'
const isLocked = inject(IsLockedInj, ref(false))
@ -48,6 +49,10 @@ watch(
const open = ref(false)
const allFilters = ref({})
provide(AllFiltersInj, allFilters)
useMenuCloseOnEsc(open)
</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>
<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
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"
@click.stop="removeFieldFromGroupBy(i)"
>
<GeneralIcon icon="delete" class="" />
<component :is="iconMap.deleteListItem" />
</NcButton>
</a-tooltip>
</template>

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

@ -1,5 +1,5 @@
<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 {
ActiveViewInj,
@ -10,6 +10,7 @@ import {
getSortDirectionOptions,
iconMap,
inject,
isEeUI,
ref,
useMenuCloseOnEsc,
useSmartsheetStoreOrThrow,
@ -21,6 +22,7 @@ const meta = inject(MetaInj, ref())
const view = inject(ActiveViewInj, ref())
const isLocked = inject(IsLockedInj, ref(false))
const reloadDataHook = inject(ReloadViewDataHookInj)
const isPublic = inject(IsPublicInj, ref(false))
const { eventBus } = useSmartsheetStoreOrThrow()
@ -32,6 +34,8 @@ const showCreateSort = ref(false)
const { isMobileMode } = useGlobal()
const { getPlanLimit } = useWorkspace()
eventBus.on((event) => {
if (event === SmartsheetStoreEvents.SORT_RELOAD) {
loadSorts()
@ -181,13 +185,31 @@ watch(open, () => {
:trigger="['click']"
overlay-class-name="nc-toolbar-dropdown"
>
<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 v-if="isEeUI && !isPublic">
<NcButton
v-if="sorts.length < getPlanLimit(PlanLimitTypes.SORT_LIMIT)"
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 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>
<SmartsheetToolbarCreateSort :is-parent-open="showCreateSort" @created="addSort" />
</template>

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

@ -1,6 +1,4 @@
<script setup lang="ts">
import { ViewTypes } from 'nocodb-sdk'
const { isMobileMode } = useGlobal()
const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
@ -16,9 +14,8 @@ const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
<div
class="ml-0.25 flex flex-row font-medium items-center border-gray-50 transition-all duration-100"
:class="{
'min-w-36/100 max-w-36/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && isLeftSidebarOpen,
'min-w-39/100 max-w-39/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && !isLeftSidebarOpen,
'min-w-1/4 max-w-1/4': !isMobileMode && activeView?.type === ViewTypes.KANBAN,
'min-w-36/100 max-w-36/100': !isMobileMode && isLeftSidebarOpen,
'min-w-39/100 max-w-39/100': !isMobileMode && !isLeftSidebarOpen,
'w-2/3 text-base ml-1.5': 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 { isUIAllowed } = useRoles()
const { onViewsTabChange } = useViewsStore()
const onClickDetails = () => {
if (isUIAllowed('fieldAdd')) {
onViewsTabChange('field')
} else {
onViewsTabChange('relation')
}
}
</script>
<template>
@ -26,7 +36,7 @@ const { onViewsTabChange } = useViewsStore()
:class="{
active: openedViewsTab !== 'view',
}"
@click="onViewsTabChange('field')"
@click="onClickDetails"
>
<GeneralIcon
icon="erd"

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

@ -128,10 +128,7 @@ const validators = computed(() =>
hasSelectColumn.value[tableIdx] = false
table.columns?.forEach((column, columnIdx) => {
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [
fieldRequiredValidator(),
fieldLengthValidator(base.value?.sources?.[0].type || ClientType.MYSQL),
]
acc[`tables.${tableIdx}.columns.${columnIdx}.column_name`] = [fieldRequiredValidator(), fieldLengthValidator()]
acc[`tables.${tableIdx}.columns.${columnIdx}.uidt`] = [fieldRequiredValidator()]
if (isSelect(column)) {
hasSelectColumn.value[tableIdx] = true
@ -434,7 +431,7 @@ async function importTemplate() {
let input = row[col.srcCn]
// parse potential boolean values
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') {
input = '0'
} 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 = () => {
if (isUnderLookup.value) return
if (!isLocked.value) {
childListDlg.value = true
}
@ -98,6 +100,12 @@ const localCellValue = computed<any[]>(() => {
}
return []
})
const openListDlg = () => {
if (isUnderLookup.value) return
listItemsDlg.value = true
}
</script>
<template>
@ -120,7 +128,7 @@ const localCellValue = computed<any[]>(() => {
<MdiPlus
v-if="(!readOnly && isUIAllowed('dataEdit')) || isForm"
class="select-none !text-md text-gray-700 nc-action-icon nc-plus"
@click.stop="listItemsDlg = true"
@click.stop="openListDlg"
/>
</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 { isSharedBase } = storeToRefs(useBase())
const {
childrenList,
childrenListCount,
@ -165,6 +167,8 @@ const isDataExist = computed<boolean>(() => {
})
const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
if (isSharedBase.value) return
if (isPublic.value && !isForm.value) return
if (isNew.value || isChildrenListLinked.value[parseInt(id)]) {
unlinkRow(rowRef, parseInt(id))
@ -345,6 +349,7 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
new: true,
},
}"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
use-meta-fields
/>
</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 readonly = inject(ReadonlyInj, ref(false))
const { getPossibleAttachmentSrc } = useAttachment()
interface Attachment {
@ -151,7 +153,7 @@ const attachments: ComputedRef<Attachment[]> = computed(() => {
</div>
</div>
<NcButton
v-if="!isForm && !isPublic"
v-if="!isForm && !isPublic && !readonly"
v-e="['c:row-expand:open']"
type="text"
size="lg"

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

@ -25,6 +25,8 @@ const { isMobileMode } = useGlobal()
const injectedColumn = inject(ColumnInj)
const { isSharedBase } = storeToRefs(useBase())
const filterQueryRef = ref()
const { $e } = useNuxtApp()
@ -162,6 +164,15 @@ watch(expandedFormDlg, () => {
onKeyStroke('Escape', () => {
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>
<template>
@ -272,12 +283,7 @@ onKeyStroke('Escape', () => {
expandedFormDlg = true
}
"
@click="
() => {
if (isChildrenExcludedListLinked[Number.parseInt(id)]) unlinkRow(refRow, Number.parseInt(id))
else linkRow(refRow, Number.parseInt(id))
}
"
@click="() => onClick(refRow, id)"
/>
</template>
</div>
@ -335,6 +341,7 @@ onKeyStroke('Escape', () => {
new: true,
},
}"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
:state="newRowState"
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>
</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')">
<a-tab-pane key="settings" class="w-full">
<template #tab>

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

@ -126,7 +126,7 @@ const [useProvideColumnCreateStore, useColumnCreateStore] = createInjectionState
})
},
},
fieldLengthValidator(baseType.value || ClientType.MYSQL),
fieldLengthValidator(),
],
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) {
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
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')
}
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
try {
const isNewRow = row.value.rowMeta?.new ?? false
@ -266,13 +275,13 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
}
if (activeView.value?.type === ViewTypes.KANBAN) {
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
addOrEditStackRow(row.value, isNewRow)
if (activeView.value?.type === ViewTypes.KANBAN && kanbanClbk) {
kanbanClbk(row.value, isNewRow)
}
changedColumns.value = new Set()
} catch (e: any) {
console.error(e)
message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
$e('a:row-expand:add')
@ -313,7 +322,7 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
if (res.message) {
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
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 { 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
}) => void
getBaseUrl: (workspaceId: string) => string | undefined
getMainUrl: (workspaceId: string) => string | undefined
}
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 { 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(
(view: Ref<(ViewType & { columns?: GridColumnType[] }) | undefined>, statePublic = false) => {
const { css, load: loadCss, unload: unloadCss } = useStyleTag('')
const { isUIAllowed } = useRoles()
const { $api } = useNuxtApp()
@ -15,30 +13,11 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
const { addUndo, defineViewScope } = useUndoRedo()
const gridViewCols = ref<Record<string, GridColumnType>>({})
const resizingCol = ref<string | null>('')
const resizingColWidth = ref('200px')
const resizingColOldWith = ref('200px')
const isPublic = inject(IsPublicInj, ref(statePublic))
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 () => {
if (!view.value?.id && !isPublic.value) return
@ -52,8 +31,6 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
}),
{},
)
loadCss()
}
/** when columns changes(create/delete) reload grid columns
@ -70,7 +47,8 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
if (!undo) {
const oldProps = Object.keys(props).reduce<Partial<GridColumnReqType>>((o: any, k) => {
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
}, {})
@ -105,7 +83,7 @@ const [useProvideGridViewColumn, useGridViewColumn] = useInjectionState(
}
}
return { loadGridViewColumns, updateGridViewColumn, resizingCol, resizingColWidth, gridViewCols, loadCss, unloadCss }
return { loadGridViewColumns, updateGridViewColumn, gridViewCols, resizingColOldWith }
},
'useGridViewColumn',
)

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

@ -679,7 +679,7 @@ const [useProvideKanbanViewStore, useKanbanViewStore] = useInjectionState(
if (res.message) {
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
Clear the data first & try again`})}`,
)

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

@ -291,7 +291,7 @@ const [useProvideLTARStore, useLTARStore] = useInjectionState(
if (res.message) {
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
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 { EventHook } from '@vueuse/core'
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 }
}> = Symbol('tree-view-functions-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]
if (column.uidt === UITypes.Checkbox) {
rowData[column.column_name] = getCheckboxValue(data)
rowData[column.column_name] = data
} else if (column.uidt === UITypes.SingleSelect || column.uidt === UITypes.MultiSelect) {
rowData[column.column_name] = (data || '').toString().trim() || null
} else {

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

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

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

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

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

@ -1,6 +1,7 @@
import type { Api } from 'nocodb-sdk'
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
@ -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 (!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 (to.meta.public) return
@ -54,13 +57,18 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
/** if shared base allow without validating */
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 this is the first usern navigate to signup page directly */
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({
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 -->
<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" />
<LazyGeneralReleaseInfo />
@ -185,8 +185,15 @@ const logout = async () => {
</NcDropdown>
</template>
</div>
<div class="flex flex-col container mx-auto mt-2">
<NuxtPage />
<div
class="flex flex-col container mx-auto"
:style="{
height: 'calc(100vh - 3.5rem)',
}"
>
<div class="mt-2 h-full">
<NuxtPage />
</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 { signedIn } = useGlobal()
const { signedIn, ncNavigateTo } = useGlobal()
const { isUIAllowed } = useRoles()
const router = useRouter()
@ -46,6 +48,10 @@ const isSharedFormView = computed(() => {
return routeName.startsWith('index-typeOrId-form-viewId')
})
const { sharedBaseId } = useCopySharedBase()
const isDuplicateDlgOpen = ref(false)
async function handleRouteTypeIdChange() {
// avoid loading bases for shared views
if (isSharedView.value) {
@ -82,7 +88,29 @@ watch(
// immediate watch, because if route is changed during page transition
// It will error out nuxt
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) {
@ -93,6 +121,40 @@ function toggleDialog(value?: boolean, key?: string, dsState?: string, pId?: str
}
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>
<template>
@ -117,6 +179,12 @@ provide(ToggleDialogInj, toggleDialog)
v-model:data-sources-state="dataSourcesState"
:base-id="baseId"
/>
<DlgSharedBaseDuplicate
v-if="isUIAllowed('baseDuplicate')"
v-model="isDuplicateDlgOpen"
:shared-base-id="sharedBaseId"
:on-ok="DlgSharedBaseDuplicateOnOk"
/>
</div>
</template>

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

@ -1,6 +1,6 @@
import { createI18n } from 'vue-i18n'
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'
let globalI18n: NocoI18n
@ -43,10 +43,16 @@ export async function loadLocaleMessages(
return nextTick()
}
export default defineNuxtPlugin(async (nuxtApp) => {
const i18nPlugin = async (nuxtApp) => {
globalI18n = await createI18nPlugin()
nuxtApp.vueApp.i18n = 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 */
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)
document.documentElement.addEventListener('mousemove', doDrag, false)
document.documentElement.addEventListener('mouseup', stopDrag, false)
emit('xcstartresizing', startWidth)
}
;(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'
/**
@ -13,7 +13,7 @@ import { loadLocaleMessages, setI18nLanguage } from '~/plugins/a.i18n'
* console.log($state.lang.value) // 'en'
* ```
*/
export default defineNuxtPlugin(async () => {
const statePlugin = async (_nuxtApp) => {
const state = useGlobal()
const { api } = useApi({ useGlobalInstance: true })
@ -34,4 +34,10 @@ export default defineNuxtPlugin(async () => {
} catch (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 type { ViewPageType } from '~/lib'
export const useViewsStore = defineStore('viewsStore', () => {
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 recentViews = computed(() => [])
const allRecentViews = ref<any>([])
// Store recent views from all Workspaces
const allRecentViews = ref<RecentView[]>([])
const route = router.currentRoute
const bases = useBases()
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 views = computed({
get: () => (tablesStore.activeTableId ? viewsByTable.value.get(tablesStore.activeTableId) : []) ?? [],
@ -122,10 +139,28 @@ export const useViewsStore = defineStore('viewsStore', () => {
})
}
const changeView = async (..._args: any) => {}
const removeFromRecentViews = (..._args: any) => {}
const changeView = async ({ viewId, tableId, baseId }: { viewId: string | null; tableId: string; baseId: string }) => {
const routeName = 'index-typeOrId-baseId-index-index-viewId-viewTitle'
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(
() => tablesStore.activeTableId,
async (newId, oldId) => {
@ -232,6 +267,28 @@ export const useViewsStore = defineStore('viewsStore', () => {
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 {
isLockedView,
isViewsLoading,

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

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

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

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

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

@ -98,21 +98,17 @@ export const fieldRequiredValidator = () => {
}
}
export const fieldLengthValidator = (sqlClientType: string) => {
export const fieldLengthValidator = () => {
return {
validator: (rule: any, value: any) => {
const { t } = getI18n().global
// no limit for sqlite but set as 255
let fieldLengthLimit = 255
if (sqlClientType === 'mysql2' || sqlClientType === 'mysql') {
fieldLengthLimit = 64
} else if (sqlClientType === 'pg') {
fieldLengthLimit = 59
} else if (sqlClientType === 'mssql') {
fieldLengthLimit = 128
}
// mysql allows 64 characters for column_name
// postgres allows 59 characters for column_name
// mssql allows 128 characters for column_name
// sqlite allows any number of characters for column_name
// We allow 255 for all databases, truncate will be handled by backend for column_name
const fieldLengthLimit = 255
return new Promise((resolve, reject) => {
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
* @name List
* @summary List Table Rows
* @request GET:/api/v1/tables/{tableId}/rows
* @request GET:/api/v2/tables/{tableId}/rows
* @response `200` `{
\** List of data objects *\
list: (object)[],
@ -10489,7 +10489,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/rows`,
path: `/api/v2/tables/${tableId}/rows`,
method: 'GET',
query: query,
format: 'json',
@ -10502,7 +10502,7 @@ export class Api<
* @tags DB Data Table Row
* @name Create
* @summary Create Table Rows
* @request POST:/api/v1/tables/{tableId}/rows
* @request POST:/api/v2/tables/{tableId}/rows
* @response `200` `any` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10526,7 +10526,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/rows`,
path: `/api/v2/tables/${tableId}/rows`,
method: 'POST',
query: query,
body: data,
@ -10541,7 +10541,7 @@ export class Api<
* @tags DB Data Table Row
* @name Update
* @summary Update Table Rows
* @request PUT:/api/v1/tables/{tableId}/rows
* @request PUT:/api/v2/tables/{tableId}/rows
* @response `200` `any` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10565,7 +10565,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/rows`,
path: `/api/v2/tables/${tableId}/rows`,
method: 'PUT',
query: query,
body: data,
@ -10580,7 +10580,7 @@ export class Api<
* @tags DB Data Table Row
* @name Delete
* @summary Delete Table Rows
* @request DELETE:/api/v1/tables/{tableId}/rows
* @request DELETE:/api/v2/tables/{tableId}/rows
* @response `200` `any` OK
* @response `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10604,7 +10604,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/rows`,
path: `/api/v2/tables/${tableId}/rows`,
method: 'DELETE',
query: query,
body: data,
@ -10619,7 +10619,7 @@ export class Api<
* @tags DB Data Table Row
* @name Read
* @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 `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10650,7 +10650,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/rows/${rowId}`,
path: `/api/v2/tables/${tableId}/records/${rowId}`,
method: 'GET',
query: query,
format: 'json',
@ -10663,7 +10663,7 @@ export class Api<
* @tags DB Data Table Row
* @name Count
* @summary Table Rows Count
* @request GET:/api/v1/tables/{tableId}/rows/count
* @request GET:/api/v2/tables/{tableId}/records/count
* @response `200` `{
count?: number,
@ -10697,7 +10697,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/rows/count`,
path: `/api/v2/tables/${tableId}/records/count`,
method: 'GET',
query: query,
format: 'json',
@ -10710,7 +10710,7 @@ export class Api<
* @tags DB Data Table Row
* @name NestedList
* @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` `{
\** List of data objects *\
list: (object)[],
@ -10766,7 +10766,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/links/${columnId}/rows/${rowId}`,
path: `/api/v2/tables/${tableId}/links/${columnId}/records/${rowId}`,
method: 'GET',
query: query,
format: 'json',
@ -10779,7 +10779,7 @@ export class Api<
* @tags DB Data Table Row
* @name NestedLink
* @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 `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10805,7 +10805,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/links/${columnId}/rows/${rowId}`,
path: `/api/v2/tables/${tableId}/links/${columnId}/records/${rowId}`,
method: 'POST',
query: query,
body: data,
@ -10820,7 +10820,7 @@ export class Api<
* @tags DB Data Table Row
* @name NestedUnlink
* @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 `400` `{
\** @example BadRequest [Error]: <ERROR MESSAGE> *\
@ -10846,7 +10846,7 @@ export class Api<
msg: string;
}
>({
path: `/api/v1/tables/${tableId}/links/${columnId}/rows/${rowId}`,
path: `/api/v2/tables/${tableId}/links/${columnId}/records/${rowId}`,
method: 'DELETE',
query: query,
body: data,

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

@ -157,7 +157,9 @@ export enum WorkspaceStatus {
export enum WorkspacePlan {
FREE = 'free',
PAID = 'paid',
STANDARD = 'standard',
BUSINESS = 'business',
BUSINESS_PRO = 'business-pro',
}
export const RoleLabels = {
@ -254,3 +256,29 @@ export const OrderedProjectRoles = [
ProjectRoles.VIEWER,
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",
"redlock": "^5.0.0-beta.2",
"reflect-metadata": "^0.1.13",
"request-filtering-agent": "^1.1.2",
"request-ip": "^2.1.3",
"rmdir": "^1.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 = {
throttler: {
ttl: 60,
max_apis: 10000,
calc_execution_time: true,
calc_execution_time: false,
},
basicAuth: {
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 { EventEmitterModule } from '~/modules/event-emitter/event-emitter.module';
import { AuthService } from '~/services/auth.service';
import { TestModule } from '~/modules/test/test.module';
import { GlobalModule } from '~/modules/global/global.module';
import { LocalStrategy } from '~/strategies/local.strategy';
import { AuthTokenStrategy } from '~/strategies/authtoken.strategy/authtoken.strategy';
@ -31,7 +30,6 @@ export const ceModuleConfig = {
GlobalModule,
UsersModule,
AuthModule,
...(process.env['PLAYWRIGHT_TEST'] === 'true' ? [TestModule] : []),
MetasModule,
DatasModule,
EventEmitterModule,
@ -41,7 +39,6 @@ export const ceModuleConfig = {
load: [() => appConfig],
isGlobal: true,
}),
TestModule,
],
providers: [
AuthService,

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

@ -6,6 +6,7 @@ export default abstract class CacheMgr {
value: any,
seconds: number,
): Promise<any>;
public abstract incrby(key: string, value: number): Promise<any>;
public abstract del(key: string): Promise<any>;
public abstract getAll(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> {
if (this.cacheDisabled) {
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
async getAll(pattern: string): Promise<any> {
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
async getAll(pattern: string): Promise<any> {
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 { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
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()
export class ApiDocsController {
@ -20,7 +22,7 @@ export class ApiDocsController {
'/api/v1/db/meta/projects/:baseId/swagger.json',
'/api/v1/meta/bases/:baseId/swagger.json',
])
@UseGuards(GlobalGuard)
@UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Acl('swaggerJson')
async swaggerJson(@Param('baseId') baseId: string, @Request() req) {
const swagger = await this.apiDocsService.swaggerJson({
@ -35,10 +37,12 @@ export class ApiDocsController {
'/api/v1/meta/bases/:baseId/swagger',
'/api/v1/db/meta/projects/:baseId/swagger',
])
@UseGuards(PublicApiLimiterGuard)
swaggerHtml(@Param('baseId') baseId: string, @Response() res) {
res.send(getSwaggerHtml({ ncSiteUrl: process.env.NC_PUBLIC_URL || '' }));
}
@UseGuards(PublicApiLimiterGuard)
@Get([
'/api/v1/db/meta/projects/: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 { ApiTokensService } from '~/services/api-tokens.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller()
@UseGuards(GlobalGuard)
@UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class ApiTokensController {
constructor(private readonly apiTokensService: ApiTokensService) {}

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

Loading…
Cancel
Save