Browse Source

Merge branch 'develop' into fix/i18n

pull/6614/head
Reenphy George 1 year ago committed by GitHub
parent
commit
edff143139
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      SECURITY.md
  2. 4
      charts/nocodb/templates/deployment.yaml
  3. 2
      charts/nocodb/templates/pvc.yaml
  4. 4
      charts/nocodb/values.yaml
  5. 9
      packages/nc-gui/assets/nc-icons/owner.svg
  6. 12
      packages/nc-gui/components/account/Profile.vue
  7. 20
      packages/nc-gui/components/account/ResetPassword.vue
  8. 4
      packages/nc-gui/components/account/SignupSettings.vue
  9. 124
      packages/nc-gui/components/account/Token.vue
  10. 87
      packages/nc-gui/components/account/UserList.vue
  11. 25
      packages/nc-gui/components/account/UsersModal.vue
  12. 6
      packages/nc-gui/components/api-client/Headers.vue
  13. 6
      packages/nc-gui/components/api-client/Params.vue
  14. 11
      packages/nc-gui/components/cell/attachment/index.vue
  15. 4
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  16. 236
      packages/nc-gui/components/dashboard/settings/DataSources.vue
  17. 2
      packages/nc-gui/components/dlg/ProjectDelete.vue
  18. 36
      packages/nc-gui/components/dlg/share-and-collaborate/ShareBase.vue
  19. 13
      packages/nc-gui/components/dlg/share-and-collaborate/SharePage.vue
  20. 2
      packages/nc-gui/components/erd/TableNode.vue
  21. 4
      packages/nc-gui/components/general/BaseLogo.vue
  22. 7
      packages/nc-gui/components/general/ShareProject.vue
  23. 15
      packages/nc-gui/components/nc/Pagination.vue
  24. 5
      packages/nc-gui/components/nc/Tooltip.vue
  25. 4
      packages/nc-gui/components/project/AllTables.vue
  26. 14
      packages/nc-gui/components/project/View.vue
  27. 183
      packages/nc-gui/components/smartsheet/Kanban.vue
  28. 2
      packages/nc-gui/components/smartsheet/VirtualCell.vue
  29. 3
      packages/nc-gui/components/smartsheet/column/LookupOptions.vue
  30. 4
      packages/nc-gui/components/smartsheet/column/SelectOptions.vue
  31. 9
      packages/nc-gui/components/smartsheet/details/Erd.vue
  32. 11
      packages/nc-gui/components/smartsheet/details/Fields.vue
  33. 17
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  34. 83
      packages/nc-gui/components/smartsheet/grid/Table.vue
  35. 2
      packages/nc-gui/components/smartsheet/toolbar/ColumnFilterMenu.vue
  36. 2
      packages/nc-gui/components/smartsheet/toolbar/FieldsMenu.vue
  37. 7
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  38. 1
      packages/nc-gui/components/virtual-cell/components/ListChildItems.vue
  39. 1
      packages/nc-gui/components/virtual-cell/components/ListItems.vue
  40. 17
      packages/nc-gui/composables/useExpandedFormStore.ts
  41. 6
      packages/nc-gui/composables/useGlobal/actions.ts
  42. 1
      packages/nc-gui/composables/useGlobal/types.ts
  43. 32
      packages/nc-gui/composables/useGridViewColumn.ts
  44. 10
      packages/nc-gui/lang/en.json
  45. 1
      packages/nc-gui/lib/types.ts
  46. 16
      packages/nc-gui/middleware/auth.global.ts
  47. 13
      packages/nc-gui/pages/account/index.vue
  48. 10
      packages/nc-gui/plugins/a.i18n.ts
  49. 10
      packages/nc-gui/plugins/api.ts
  50. 1
      packages/nc-gui/plugins/resizeDirective.ts
  51. 10
      packages/nc-gui/plugins/state.ts
  52. 71
      packages/nc-gui/store/views.ts
  53. 88
      packages/nc-gui/utils/colorsUtils.ts
  54. 5
      packages/noco-docs/docs/020.getting-started/020.environment-variables.md
  55. 1
      packages/nocodb/package.json
  56. 4
      packages/nocodb/src/app.config.ts
  57. 3
      packages/nocodb/src/app.module.ts
  58. 6
      packages/nocodb/src/controllers/api-docs/api-docs.controller.ts
  59. 3
      packages/nocodb/src/controllers/api-tokens.controller.ts
  60. 5
      packages/nocodb/src/controllers/attachments-secure.controller.ts
  61. 5
      packages/nocodb/src/controllers/attachments.controller.ts
  62. 3
      packages/nocodb/src/controllers/audits.controller.ts
  63. 19
      packages/nocodb/src/controllers/auth/auth.controller.ts
  64. 3
      packages/nocodb/src/controllers/base-users.controller.ts
  65. 3
      packages/nocodb/src/controllers/bases.controller.ts
  66. 3
      packages/nocodb/src/controllers/bulk-data-alias.controller.ts
  67. 3
      packages/nocodb/src/controllers/caches.controller.ts
  68. 3
      packages/nocodb/src/controllers/columns.controller.ts
  69. 4
      packages/nocodb/src/controllers/data-alias-export.controller.ts
  70. 3
      packages/nocodb/src/controllers/data-alias-nested.controller.ts
  71. 3
      packages/nocodb/src/controllers/data-alias.controller.ts
  72. 5
      packages/nocodb/src/controllers/data-table.controller.ts
  73. 3
      packages/nocodb/src/controllers/datas.controller.ts
  74. 3
      packages/nocodb/src/controllers/filters.controller.ts
  75. 3
      packages/nocodb/src/controllers/form-columns.controller.ts
  76. 3
      packages/nocodb/src/controllers/forms.controller.ts
  77. 3
      packages/nocodb/src/controllers/galleries.controller.ts
  78. 3
      packages/nocodb/src/controllers/grid-columns.controller.ts
  79. 3
      packages/nocodb/src/controllers/grids.controller.ts
  80. 3
      packages/nocodb/src/controllers/hooks.controller.ts
  81. 0
      packages/nocodb/src/controllers/imports/helpers/job.ts
  82. 0
      packages/nocodb/src/controllers/imports/import.controller.ts
  83. 3
      packages/nocodb/src/controllers/kanbans.controller.ts
  84. 3
      packages/nocodb/src/controllers/maps.controller.ts
  85. 3
      packages/nocodb/src/controllers/meta-diffs.controller.ts
  86. 3
      packages/nocodb/src/controllers/model-visibilities.controller.ts
  87. 3
      packages/nocodb/src/controllers/notifications.controller.ts
  88. 3
      packages/nocodb/src/controllers/old-datas/old-datas.controller.ts
  89. 3
      packages/nocodb/src/controllers/org-lcense.controller.ts
  90. 3
      packages/nocodb/src/controllers/org-tokens.controller.ts
  91. 3
      packages/nocodb/src/controllers/org-users.controller.ts
  92. 3
      packages/nocodb/src/controllers/plugins.controller.ts
  93. 11
      packages/nocodb/src/controllers/public-datas-export.controller.ts
  94. 3
      packages/nocodb/src/controllers/public-datas.controller.ts
  95. 4
      packages/nocodb/src/controllers/public-metas.controller.ts
  96. 3
      packages/nocodb/src/controllers/shared-bases.controller.ts
  97. 3
      packages/nocodb/src/controllers/sorts.controller.ts
  98. 3
      packages/nocodb/src/controllers/sources.controller.ts
  99. 3
      packages/nocodb/src/controllers/sql-views.controller.ts
  100. 3
      packages/nocodb/src/controllers/sync.controller.ts
  101. Some files were not shown because too many files have changed in this diff Show More

4
SECURITY.md

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

4
charts/nocodb/templates/deployment.yaml

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

2
charts/nocodb/templates/pvc.yaml

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

4
charts/nocodb/values.yaml

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

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

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

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -657,7 +657,7 @@ const projectDelete = () => {
class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title" class="source-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title"
@contextmenu="setMenuContext('source', source)" @contextmenu="setMenuContext('source', source)"
> >
<GeneralBaseLogo :source-type="source.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" /> <GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
{{ $t('general.default') }} {{ $t('general.default') }}
</div> </div>
<div <div
@ -665,7 +665,7 @@ const projectDelete = () => {
class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full" class="source-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full"
@contextmenu="setMenuContext('source', source)" @contextmenu="setMenuContext('source', source)"
> >
<GeneralBaseLogo :source-type="source.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" /> <GeneralBaseLogo class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
<div <div
:data-testid="`nc-sidebar-base-${source.alias}`" :data-testid="`nc-sidebar-base-${source.alias}`"
class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none" class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none"

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

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

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

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

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

@ -1,5 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { extractSdkResponseErrorMsg, message, onMounted, storeToRefs, useBase, useDashboard, useNuxtApp } from '#imports' import {
extractSdkResponseErrorMsg,
message,
onMounted,
storeToRefs,
useBase,
useDashboard,
useGlobal,
useNuxtApp,
useWorkspace,
} from '#imports'
interface ShareBase { interface ShareBase {
uuid?: string uuid?: string
@ -20,9 +30,23 @@ const sharedBase = ref<null | ShareBase>(null)
const { base } = storeToRefs(useBase()) const { base } = storeToRefs(useBase())
const url = computed(() => const { getBaseUrl, appInfo } = useGlobal()
sharedBase.value && sharedBase.value.uuid ? `${dashboardUrl.value}#/base/${sharedBase.value.uuid}` : '',
) const workspaceStore = useWorkspace()
const url = computed(() => {
if (!sharedBase.value || !sharedBase.value.uuid) return ''
// get base url for workspace
const baseUrl = getBaseUrl(workspaceStore.activeWorkspaceId)
let dashboardUrl1 = dashboardUrl.value
if (baseUrl) {
dashboardUrl1 = `${baseUrl}${appInfo.value?.dashboardPath}`
}
return encodeURI(`${dashboardUrl1}#/base/${sharedBase.value.uuid}`)
})
const loadBase = async () => { const loadBase = async () => {
try { try {
@ -50,6 +74,8 @@ const createShareBase = async (role = ShareBaseRole.Viewer) => {
sharedBase.value = res ?? {} sharedBase.value = res ?? {}
sharedBase.value!.role = role sharedBase.value!.role = role
base.value.uuid = res.uuid
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }
@ -63,6 +89,8 @@ const disableSharedBase = async () => {
await $api.base.sharedBaseDisable(base.value.id) await $api.base.sharedBaseDisable(base.value.id)
sharedBase.value = null sharedBase.value = null
base.value.uuid = undefined
} catch (e: any) { } catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e)) message.error(await extractSdkResponseErrorMsg(e))
} }

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

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

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

@ -32,7 +32,7 @@ const hasColumns = computed(() => data.pkAndFkColumns.length || data.nonPkColumn
const nonPkColumns = computed(() => const nonPkColumns = computed(() =>
data.nonPkColumns data.nonPkColumns
// Removed MM system column from the table node // Removed MM system column from the table node
.filter((col) => !(col.system && isLinksOrLTAR(col) && /nc_.*___nc_m2m_.*/.test(col.title!))), .filter((col) => !(col.system && isLinksOrLTAR(col) && /.*_nc_m2m_.*/.test(col.title!))),
) )
watch( watch(

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

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

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

@ -8,7 +8,7 @@ interface Props {
const { disabled, isViewToolbar } = defineProps<Props>() const { disabled, isViewToolbar } = defineProps<Props>()
const { isMobileMode } = useGlobal() const { isMobileMode, getMainUrl } = useGlobal()
const { visibility, showShareModal } = storeToRefs(useShare()) const { visibility, showShareModal } = storeToRefs(useShare())
@ -38,7 +38,10 @@ useEventListener(document, 'keydown', async (e: KeyboardEvent) => {
}) })
const copySharedBase = async () => { const copySharedBase = async () => {
navigateTo(`/copy-shared-base?base=${route.params.baseId}`) const baseUrl = getMainUrl()
navigateTo(`${baseUrl || ''}#/copy-shared-base?base=${route.params.baseId}`, {
external: true,
})
} }
</script> </script>

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

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

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

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

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

@ -147,8 +147,8 @@ const onCreateBaseClick = () => {
</div> </div>
<div class="w-1/5 text-gray-600" data-testid="proj-view-list__item-type"> <div class="w-1/5 text-gray-600" data-testid="proj-view-list__item-type">
<div v-if="table.source_id === defaultBase?.id" class="ml-0.75">-</div> <div v-if="table.source_id === defaultBase?.id" class="ml-0.75">-</div>
<div v-else> <div v-else class="capitalize flex flex-row items-center gap-x-0.5">
<GeneralBaseLogo :source-type="sources.get(table.source_id!)?.type" class="w-4 mr-1" /> <GeneralBaseLogo class="w-4 mr-1" />
{{ sources.get(table.source_id!)?.alias }} {{ sources.get(table.source_id!)?.alias }}
</div> </div>
</div> </div>

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

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

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

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

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

@ -51,8 +51,6 @@ const isForm = inject(IsFormInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false)) const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const isUnderLookup = inject(IsUnderLookupInj, ref(false))
function onNavigate(dir: NavigateDir, e: KeyboardEvent) { function onNavigate(dir: NavigateDir, e: KeyboardEvent) {
emit('navigate', dir) emit('navigate', dir)

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

@ -86,7 +86,7 @@ const cellIcon = (column: ColumnType) =>
<template> <template>
<div class="p-6 w-full flex flex-col border-2 mb-2 mt-4"> <div class="p-6 w-full flex flex-col border-2 mb-2 mt-4">
<div class="w-full flex flex-row space-x-2"> <div v-if="refTables.length" class="w-full flex flex-row space-x-2">
<a-form-item class="flex w-1/2 pb-2" :label="$t('labels.links')" v-bind="validateInfos.fk_relation_column_id"> <a-form-item class="flex w-1/2 pb-2" :label="$t('labels.links')" v-bind="validateInfos.fk_relation_column_id">
<a-select <a-select
v-model:value="vModel.fk_relation_column_id" v-model:value="vModel.fk_relation_column_id"
@ -121,6 +121,7 @@ const cellIcon = (column: ColumnType) =>
</a-select> </a-select>
</a-form-item> </a-form-item>
</div> </div>
<div v-else>{{ $t('msg.linkColumnClearNotSupportedYet') }}</div>
</div> </div>
</template> </template>

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

@ -3,7 +3,7 @@ import Draggable from 'vuedraggable'
import { UITypes } from 'nocodb-sdk' import { UITypes } from 'nocodb-sdk'
import InfiniteLoading from 'v3-infinite-loading' import InfiniteLoading from 'v3-infinite-loading'
import { IsKanbanInj, enumColor, iconMap, onMounted, useColumnCreateStoreOrThrow, useVModel, watch } from '#imports' import { IsKanbanInj, enumColor, iconMap, onMounted, useColumnCreateStoreOrThrow, useVModel } from '#imports'
interface Option { interface Option {
color: string color: string
@ -21,7 +21,7 @@ const emit = defineEmits(['update:value'])
const vModel = useVModel(props, 'value', emit) const vModel = useVModel(props, 'value', emit)
const { setAdditionalValidations, validateInfos, isMysql, isPg } = useColumnCreateStoreOrThrow() const { setAdditionalValidations, validateInfos } = useColumnCreateStoreOrThrow()
// const { base } = storeToRefs(useBase()) // const { base } = storeToRefs(useBase())

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

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

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

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

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

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

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

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

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

@ -3,6 +3,7 @@ import {
ActiveViewInj, ActiveViewInj,
IsLockedInj, IsLockedInj,
computed, computed,
iconMap,
inject, inject,
ref, ref,
useGlobal, useGlobal,
@ -10,7 +11,6 @@ import {
useSmartsheetStoreOrThrow, useSmartsheetStoreOrThrow,
useViewFilters, useViewFilters,
watch, watch,
iconMap,
} from '#imports' } from '#imports'
const isLocked = inject(IsLockedInj, ref(false)) const isLocked = inject(IsLockedInj, ref(false))

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

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

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

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

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

@ -345,6 +345,7 @@ const linkOrUnLink = (rowRef: Record<string, string>, id: string) => {
new: true, new: true,
}, },
}" }"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
use-meta-fields use-meta-fields
/> />
</Suspense> </Suspense>

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

@ -335,6 +335,7 @@ onKeyStroke('Escape', () => {
new: true, new: true,
}, },
}" }"
:row-id="extractPkFromRow(expandedFormRow, relatedTableMeta.columns as ColumnType[])"
:state="newRowState" :state="newRowState"
use-meta-fields use-meta-fields
/> />

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

@ -153,7 +153,16 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
$e('a:row-expand:comment') $e('a:row-expand:comment')
} }
const save = async (ltarState: Record<string, any> = {}, undo = false) => { const save = async (
ltarState: Record<string, any> = {},
undo = false,
// TODO: Hack. Remove this when kanban injection store issue is resolved
{
kanbanClbk,
}: {
kanbanClbk?: (row: Row, isNewRow: boolean) => void
} = {},
) => {
let data let data
try { try {
const isNewRow = row.value.rowMeta?.new ?? false const isNewRow = row.value.rowMeta?.new ?? false
@ -266,13 +275,13 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
} }
} }
if (activeView.value?.type === ViewTypes.KANBAN) { if (activeView.value?.type === ViewTypes.KANBAN && kanbanClbk) {
const { addOrEditStackRow } = useKanbanViewStoreOrThrow() kanbanClbk(row.value, isNewRow)
addOrEditStackRow(row.value, isNewRow)
} }
changedColumns.value = new Set() changedColumns.value = new Set()
} catch (e: any) { } catch (e: any) {
console.error(e)
message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`) message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
} }
$e('a:row-expand:add') $e('a:row-expand:add')

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

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

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

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

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

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

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

@ -403,6 +403,7 @@
} }
}, },
"labels": { "labels": {
"noToken": "No Token",
"tokenLimit": "Only one token per user is allowed", "tokenLimit": "Only one token per user is allowed",
"duplicateAttachment": "File with name {filename} already attached", "duplicateAttachment": "File with name {filename} already attached",
"toAddress": "To Address", "toAddress": "To Address",
@ -698,8 +699,8 @@
"newUser": "New User", "newUser": "New User",
"editUser": "Edit user", "editUser": "Edit user",
"deleteUser": "Remove user from base", "deleteUser": "Remove user from base",
"resendInvite": "Resend invite E-mail", "resendInvite": "Resend Invite E-mail",
"copyInviteURL": "Copy invite URL", "copyInviteURL": "Copy Invite URL",
"copyPasswordResetURL": "Copy password reset URL", "copyPasswordResetURL": "Copy password reset URL",
"newRole": "New role", "newRole": "New role",
"reloadRoles": "Reload roles", "reloadRoles": "Reload roles",
@ -903,8 +904,9 @@
"newFormWillBeLoaded": "New form will be loaded after {seconds} seconds", "newFormWillBeLoaded": "New form will be loaded after {seconds} seconds",
"optimizedQueryDisabled": "Optimized query is disabled", "optimizedQueryDisabled": "Optimized query is disabled",
"optimizedQueryEnabled": "Optimized query is enabled", "optimizedQueryEnabled": "Optimized query is enabled",
"lookupNonBtWarning": "Lookup column is not supported for non-Belongs to relation",
"invalidTime": "Invalid Time", "invalidTime": "Invalid Time",
"linkColumnClearNotSupportedYet": "Link column clear is not supported yet", "linkColumnClearNotSupportedYet": "You don't have any supported links for Lookup",
"recordCouldNotBeFound": "Record could not be found", "recordCouldNotBeFound": "Record could not be found",
"invalidPhoneNumber": "Invalid phone number", "invalidPhoneNumber": "Invalid phone number",
"pageSizeChanged": "Page size changed", "pageSizeChanged": "Page size changed",
@ -1248,7 +1250,7 @@
"deleteProject": "Base deleted successfully", "deleteProject": "Base deleted successfully",
"authToken": "Auth token copied to clipboard", "authToken": "Auth token copied to clipboard",
"projInfo": "Copied base info to clipboard", "projInfo": "Copied base info to clipboard",
"inviteUrlCopy": "Copied invite URL to clipboard", "inviteUrlCopy": "Copied Invite URL to clipboard",
"createView": "View created successfully", "createView": "View created successfully",
"formEmailSMTP": "Please activate SMTP plugin in App store for enabling email notification", "formEmailSMTP": "Please activate SMTP plugin in App store for enabling email notification",
"collabView": "Successfully Switched to collaborative view", "collabView": "Successfully Switched to collaborative view",

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

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

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

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

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

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

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

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

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 = defineNuxtPlugin((nuxtApp) => {
/** injects a global api instance */ /** injects a global api instance */
nuxtApp.provide('api', useApi().api) nuxtApp.provide('api', useApi().api)
}) })
const defaultExport = isEeUI ? defineNuxtPlugin(async () => {}) : apiPlugin
export { apiPlugin }
export default defaultExport

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

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

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

@ -1,4 +1,4 @@
import { Language, LanguageAlias, defineNuxtPlugin, useApi, useGlobal } from '#imports' import { Language, LanguageAlias, defineNuxtPlugin, isEeUI, useApi, useGlobal } from '#imports'
import { loadLocaleMessages, setI18nLanguage } from '~/plugins/a.i18n' import { loadLocaleMessages, setI18nLanguage } from '~/plugins/a.i18n'
/** /**
@ -13,7 +13,7 @@ import { loadLocaleMessages, setI18nLanguage } from '~/plugins/a.i18n'
* console.log($state.lang.value) // 'en' * console.log($state.lang.value) // 'en'
* ``` * ```
*/ */
export default defineNuxtPlugin(async () => { const statePlugin = defineNuxtPlugin(async () => {
const state = useGlobal() const state = useGlobal()
const { api } = useApi({ useGlobalInstance: true }) const { api } = useApi({ useGlobalInstance: true })
@ -35,3 +35,9 @@ export default defineNuxtPlugin(async () => {
console.error(e) console.error(e)
} }
}) })
const defaultExport = isEeUI ? defineNuxtPlugin(async () => {}) : statePlugin
export { statePlugin }
export default defaultExport

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

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

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

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

5
packages/noco-docs/docs/020.getting-started/020.environment-variables.md

@ -62,5 +62,6 @@ For production usecases, it is **recommended** to configure
| NC_MINIMAL_DBS | Create a new SQLite file for each project. All the db files are stored in `nc_minimal_dbs` folder in current working directory. (This option restricts project creation on external sources) | | | NC_MINIMAL_DBS | Create a new SQLite file for each project. All the db files are stored in `nc_minimal_dbs` folder in current working directory. (This option restricts project creation on external sources) | |
| NC_DISABLE_AUDIT | Disable Audit Log | `false` | | NC_DISABLE_AUDIT | Disable Audit Log | `false` |
| NC_AUTOMATION_LOG_LEVEL | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/developer-resources/webhooks#call-log) for details. | `OFF` | | NC_AUTOMATION_LOG_LEVEL | Possible Values: `OFF`, `ERROR`, `ALL`. See [Webhooks](/developer-resources/webhooks#call-log) for details. | `OFF` |
| NC_SECURE_ATTACHMENTS | Allow accessing attachments only through presigned urls. To enable secure set value as `true` any other value treated as false. (⚠ this will make existing links inaccessible ⚠) | `false` | | NC_SECURE_ATTACHMENTS | Allow accessing attachments only through presigned urls. To enable set value as `true` any other value treated as false. (⚠ this will make existing links inaccessible ⚠) | `false` |
| NC_ATTACHMENT_EXPIRE_SECONDS | How many seconds before expiring presigned attachment urls. (Attachments will expire in at least set seconds and at most 10mins after set time) | 7200 (2 hours) | | NC_ATTACHMENT_EXPIRE_SECONDS | How many seconds before expiring presigned attachment urls. (Attachments will expire in at least set seconds and at most 10mins after set time) | 7200 (2 hours) |
| NC_ALLOW_LOCAL_HOOKS | To enable set value as `true` any other value treated as false. (⚠ this will allow webhooks to call local links which can raise security issues ⚠) | `false` |

1
packages/nocodb/package.json

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

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

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

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

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

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

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

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

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

5
packages/nocodb/src/controllers/attachments-secure.controller.ts

@ -19,12 +19,13 @@ import { GlobalGuard } from '~/guards/global/global.guard';
import { AttachmentsService } from '~/services/attachments.service'; import { AttachmentsService } from '~/services/attachments.service';
import { PresignedUrl } from '~/models'; import { PresignedUrl } from '~/models';
import { UploadAllowedInterceptor } from '~/interceptors/is-upload-allowed/is-upload-allowed.interceptor'; import { UploadAllowedInterceptor } from '~/interceptors/is-upload-allowed/is-upload-allowed.interceptor';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
export class AttachmentsSecureController { export class AttachmentsSecureController {
constructor(private readonly attachmentsService: AttachmentsService) {} constructor(private readonly attachmentsService: AttachmentsService) {}
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Post(['/api/v1/db/storage/upload', '/api/v1/storage/upload']) @Post(['/api/v1/db/storage/upload', '/api/v1/storage/upload'])
@HttpCode(200) @HttpCode(200)
@UseInterceptors(UploadAllowedInterceptor, AnyFilesInterceptor()) @UseInterceptors(UploadAllowedInterceptor, AnyFilesInterceptor())
@ -42,7 +43,7 @@ export class AttachmentsSecureController {
@Post(['/api/v1/db/storage/upload-by-url', '/api/v1/storage/upload-by-url']) @Post(['/api/v1/db/storage/upload-by-url', '/api/v1/storage/upload-by-url'])
@HttpCode(200) @HttpCode(200)
@UseInterceptors(UploadAllowedInterceptor) @UseInterceptors(UploadAllowedInterceptor)
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
async uploadViaURL(@Body() body: any, @Request() req) { async uploadViaURL(@Body() body: any, @Request() req) {
const path = `${moment().format('YYYY/MM/DD')}/${hash(req.user.id)}`; const path = `${moment().format('YYYY/MM/DD')}/${hash(req.user.id)}`;

5
packages/nocodb/src/controllers/attachments.controller.ts

@ -18,12 +18,13 @@ import { UploadAllowedInterceptor } from '~/interceptors/is-upload-allowed/is-up
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { AttachmentsService } from '~/services/attachments.service'; import { AttachmentsService } from '~/services/attachments.service';
import { PresignedUrl } from '~/models'; import { PresignedUrl } from '~/models';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
export class AttachmentsController { export class AttachmentsController {
constructor(private readonly attachmentsService: AttachmentsService) {} constructor(private readonly attachmentsService: AttachmentsService) {}
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Post(['/api/v1/db/storage/upload', '/api/v1/storage/upload']) @Post(['/api/v1/db/storage/upload', '/api/v1/storage/upload'])
@HttpCode(200) @HttpCode(200)
@UseInterceptors(UploadAllowedInterceptor, AnyFilesInterceptor()) @UseInterceptors(UploadAllowedInterceptor, AnyFilesInterceptor())
@ -43,7 +44,7 @@ export class AttachmentsController {
@Post(['/api/v1/db/storage/upload-by-url', '/api/v1/storage/upload-by-url']) @Post(['/api/v1/db/storage/upload-by-url', '/api/v1/storage/upload-by-url'])
@HttpCode(200) @HttpCode(200)
@UseInterceptors(UploadAllowedInterceptor) @UseInterceptors(UploadAllowedInterceptor)
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
async uploadViaURL(@Body() body: any, @Query('path') path: string) { async uploadViaURL(@Body() body: any, @Query('path') path: string) {
const attachments = await this.attachmentsService.uploadViaURL({ const attachments = await this.attachmentsService.uploadViaURL({
urls: body, urls: body,

3
packages/nocodb/src/controllers/audits.controller.ts

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

19
packages/nocodb/src/controllers/auth/auth.controller.ts

@ -23,6 +23,8 @@ import { GlobalGuard } from '~/guards/global/global.guard';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { User } from '~/models'; import { User } from '~/models';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
import { PublicApiLimiterGuard } from '~/guards/public-api-limiter.guard';
@Controller() @Controller()
export class AuthController { export class AuthController {
@ -37,6 +39,7 @@ export class AuthController {
'/api/v1/db/auth/user/signup', '/api/v1/db/auth/user/signup',
'/api/v1/auth/user/signup', '/api/v1/auth/user/signup',
]) ])
@UseGuards(PublicApiLimiterGuard)
@HttpCode(200) @HttpCode(200)
async signup(@Request() req: any, @Response() res: any): Promise<any> { async signup(@Request() req: any, @Response() res: any): Promise<any> {
if (this.config.get('auth', { infer: true }).disableEmailAuth) { if (this.config.get('auth', { infer: true }).disableEmailAuth) {
@ -56,6 +59,7 @@ export class AuthController {
'/api/v1/db/auth/token/refresh', '/api/v1/db/auth/token/refresh',
'/api/v1/auth/token/refresh', '/api/v1/auth/token/refresh',
]) ])
@UseGuards(PublicApiLimiterGuard)
@HttpCode(200) @HttpCode(200)
async refreshToken(@Request() req: any, @Response() res: any): Promise<any> { async refreshToken(@Request() req: any, @Response() res: any): Promise<any> {
res.json( res.json(
@ -72,7 +76,7 @@ export class AuthController {
'/api/v1/db/auth/user/signin', '/api/v1/db/auth/user/signin',
'/api/v1/auth/user/signin', '/api/v1/auth/user/signin',
]) ])
@UseGuards(AuthGuard('local')) @UseGuards(PublicApiLimiterGuard, AuthGuard('local'))
@HttpCode(200) @HttpCode(200)
async signin(@Request() req, @Response() res) { async signin(@Request() req, @Response() res) {
if (this.config.get('auth', { infer: true }).disableEmailAuth) { if (this.config.get('auth', { infer: true }).disableEmailAuth) {
@ -99,20 +103,20 @@ export class AuthController {
@Post(`/auth/google/genTokenByCode`) @Post(`/auth/google/genTokenByCode`)
@HttpCode(200) @HttpCode(200)
@UseGuards(AuthGuard('google')) @UseGuards(PublicApiLimiterGuard, AuthGuard('google'))
async googleSignin(@Request() req, @Response() res) { async googleSignin(@Request() req, @Response() res) {
await this.setRefreshToken({ req, res }); await this.setRefreshToken({ req, res });
res.json(await this.usersService.login(req.user)); res.json(await this.usersService.login(req.user));
} }
@Get('/auth/google') @Get('/auth/google')
@UseGuards(AuthGuard('google')) @UseGuards(PublicApiLimiterGuard, AuthGuard('google'))
googleAuthenticate() { googleAuthenticate() {
// google strategy will take care the request // google strategy will take care the request
} }
@Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me']) @Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me'])
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
async me(@Request() req) { async me(@Request() req) {
const user = { const user = {
...req.user, ...req.user,
@ -128,7 +132,7 @@ export class AuthController {
'/api/v1/db/auth/password/change', '/api/v1/db/auth/password/change',
'/api/v1/auth/password/change', '/api/v1/auth/password/change',
]) ])
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Acl('passwordChange', { @Acl('passwordChange', {
scope: 'org', scope: 'org',
}) })
@ -152,6 +156,7 @@ export class AuthController {
'/api/v1/db/auth/password/forgot', '/api/v1/db/auth/password/forgot',
'/api/v1/auth/password/forgot', '/api/v1/auth/password/forgot',
]) ])
@UseGuards(PublicApiLimiterGuard)
@HttpCode(200) @HttpCode(200)
async passwordForgot(@Request() req: any): Promise<any> { async passwordForgot(@Request() req: any): Promise<any> {
await this.usersService.passwordForgot({ await this.usersService.passwordForgot({
@ -168,6 +173,7 @@ export class AuthController {
'/api/v1/db/auth/token/validate/:tokenId', '/api/v1/db/auth/token/validate/:tokenId',
'/api/v1/auth/token/validate/:tokenId', '/api/v1/auth/token/validate/:tokenId',
]) ])
@UseGuards(PublicApiLimiterGuard)
@HttpCode(200) @HttpCode(200)
async tokenValidate(@Param('tokenId') tokenId: string): Promise<any> { async tokenValidate(@Param('tokenId') tokenId: string): Promise<any> {
await this.usersService.tokenValidate({ await this.usersService.tokenValidate({
@ -181,6 +187,7 @@ export class AuthController {
'/api/v1/db/auth/password/reset/:tokenId', '/api/v1/db/auth/password/reset/:tokenId',
'/api/v1/auth/password/reset/:tokenId', '/api/v1/auth/password/reset/:tokenId',
]) ])
@UseGuards(PublicApiLimiterGuard)
@HttpCode(200) @HttpCode(200)
async passwordReset( async passwordReset(
@Request() req: any, @Request() req: any,
@ -200,6 +207,7 @@ export class AuthController {
'/api/v1/db/auth/email/validate/:tokenId', '/api/v1/db/auth/email/validate/:tokenId',
'/api/v1/auth/email/validate/:tokenId', '/api/v1/auth/email/validate/:tokenId',
]) ])
@UseGuards(PublicApiLimiterGuard)
@HttpCode(200) @HttpCode(200)
async emailVerification( async emailVerification(
@Request() req: any, @Request() req: any,
@ -217,6 +225,7 @@ export class AuthController {
'/api/v1/db/auth/password/reset/:tokenId', '/api/v1/db/auth/password/reset/:tokenId',
'/auth/password/reset/:tokenId', '/auth/password/reset/:tokenId',
]) ])
@UseGuards(PublicApiLimiterGuard)
async renderPasswordReset( async renderPasswordReset(
@Request() req: any, @Request() req: any,
@Response() res: any, @Response() res: any,

3
packages/nocodb/src/controllers/base-users.controller.ts

@ -15,8 +15,9 @@ import { GlobalGuard } from '~/guards/global/global.guard';
import { BaseUsersService } from '~/services/base-users/base-users.service'; import { BaseUsersService } from '~/services/base-users/base-users.service';
import { NcError } from '~/helpers/catchError'; import { NcError } from '~/helpers/catchError';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Controller() @Controller()
export class BaseUsersController { export class BaseUsersController {
constructor(protected readonly baseUsersService: BaseUsersService) {} constructor(protected readonly baseUsersService: BaseUsersService) {}

3
packages/nocodb/src/controllers/bases.controller.ts

@ -21,8 +21,9 @@ import { packageVersion } from '~/utils/packageVersion';
import { BasesService } from '~/services/bases.service'; import { BasesService } from '~/services/bases.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { Filter } from '~/models'; import { Filter } from '~/models';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
@Controller() @Controller()
export class BasesController { export class BasesController {
constructor(protected readonly projectsService: BasesService) {} constructor(protected readonly projectsService: BasesService) {}

3
packages/nocodb/src/controllers/bulk-data-alias.controller.ts

@ -13,9 +13,10 @@ import {
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { BulkDataAliasService } from '~/services/bulk-data-alias.service'; import { BulkDataAliasService } from '~/services/bulk-data-alias.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(DataApiLimiterGuard, GlobalGuard)
export class BulkDataAliasController { export class BulkDataAliasController {
constructor(private bulkDataAliasService: BulkDataAliasService) {} constructor(private bulkDataAliasService: BulkDataAliasService) {}

3
packages/nocodb/src/controllers/caches.controller.ts

@ -3,9 +3,10 @@ import { OrgUserRoles } from 'nocodb-sdk';
import { CachesService } from '~/services/caches.service'; import { CachesService } from '~/services/caches.service';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class CachesController { export class CachesController {
constructor(private readonly cachesService: CachesService) {} constructor(private readonly cachesService: CachesService) {}

3
packages/nocodb/src/controllers/columns.controller.ts

@ -15,9 +15,10 @@ import type { Column } from '~/models';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { ColumnsService } from '~/services/columns.service'; import { ColumnsService } from '~/services/columns.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class ColumnsController { export class ColumnsController {
constructor(private readonly columnsService: ColumnsService) {} constructor(private readonly columnsService: ColumnsService) {}

4
packages/nocodb/src/controllers/data-alias-export.controller.ts

@ -5,9 +5,10 @@ import { DatasService } from '~/services/datas.service';
import { extractCsvData, extractXlsxData } from '~/modules/datas/helpers'; import { extractCsvData, extractXlsxData } from '~/modules/datas/helpers';
import { View } from '~/models'; import { View } from '~/models';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(DataApiLimiterGuard, GlobalGuard)
export class DataAliasExportController { export class DataAliasExportController {
constructor(private datasService: DatasService) {} constructor(private datasService: DatasService) {}
@ -39,6 +40,7 @@ export class DataAliasExportController {
}); });
res.end(buf); res.end(buf);
} }
@Get([ @Get([
'/api/v1/db/data/:orgs/:baseName/:tableName/views/:viewName/export/csv', '/api/v1/db/data/:orgs/:baseName/:tableName/views/:viewName/export/csv',
'/api/v1/db/data/:orgs/:baseName/:tableName/export/csv', '/api/v1/db/data/:orgs/:baseName/:tableName/export/csv',

3
packages/nocodb/src/controllers/data-alias-nested.controller.ts

@ -11,9 +11,10 @@ import {
import { DataAliasNestedService } from '~/services/data-alias-nested.service'; import { DataAliasNestedService } from '~/services/data-alias-nested.service';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(DataApiLimiterGuard, GlobalGuard)
export class DataAliasNestedController { export class DataAliasNestedController {
constructor(private dataAliasNestedService: DataAliasNestedService) {} constructor(private dataAliasNestedService: DataAliasNestedService) {}

3
packages/nocodb/src/controllers/data-alias.controller.ts

@ -16,9 +16,10 @@ import { GlobalGuard } from '~/guards/global/global.guard';
import { parseHrtimeToMilliSeconds } from '~/helpers'; import { parseHrtimeToMilliSeconds } from '~/helpers';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { DatasService } from '~/services/datas.service'; import { DatasService } from '~/services/datas.service';
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(DataApiLimiterGuard, GlobalGuard)
export class DataAliasController { export class DataAliasController {
constructor(private readonly datasService: DatasService) {} constructor(private readonly datasService: DatasService) {}

5
packages/nocodb/src/controllers/data-table.controller.ts

@ -12,13 +12,14 @@ import {
Response, Response,
UseGuards, UseGuards,
} from '@nestjs/common'; } from '@nestjs/common';
import { GlobalGuard } from '~/guards/global/global.guard';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { DataTableService } from '~/services/data-table.service'; import { DataTableService } from '~/services/data-table.service';
import { parseHrtimeToMilliSeconds } from '~/helpers'; import { parseHrtimeToMilliSeconds } from '~/helpers';
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard';
import { GlobalGuard } from '~/guards/global/global.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(DataApiLimiterGuard, GlobalGuard)
export class DataTableController { export class DataTableController {
constructor(private readonly dataTableService: DataTableService) {} constructor(private readonly dataTableService: DataTableService) {}

3
packages/nocodb/src/controllers/datas.controller.ts

@ -13,9 +13,10 @@ import {
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { DatasService } from '~/services/datas.service'; import { DatasService } from '~/services/datas.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(DataApiLimiterGuard, GlobalGuard)
export class DatasController { export class DatasController {
constructor(private readonly datasService: DatasService) {} constructor(private readonly datasService: DatasService) {}

3
packages/nocodb/src/controllers/filters.controller.ts

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

3
packages/nocodb/src/controllers/form-columns.controller.ts

@ -2,11 +2,12 @@ import { Body, Controller, Param, Patch, UseGuards } from '@nestjs/common';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { FormColumnsService } from '~/services/form-columns.service'; import { FormColumnsService } from '~/services/form-columns.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
class FormColumnUpdateReqType {} class FormColumnUpdateReqType {}
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class FormColumnsController { export class FormColumnsController {
constructor(private readonly formColumnsService: FormColumnsService) {} constructor(private readonly formColumnsService: FormColumnsService) {}

3
packages/nocodb/src/controllers/forms.controller.ts

@ -13,9 +13,10 @@ import { ViewCreateReqType } from 'nocodb-sdk';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { FormsService } from '~/services/forms.service'; import { FormsService } from '~/services/forms.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class FormsController { export class FormsController {
constructor(private readonly formsService: FormsService) {} constructor(private readonly formsService: FormsService) {}

3
packages/nocodb/src/controllers/galleries.controller.ts

@ -13,9 +13,10 @@ import { GalleryUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { GalleriesService } from '~/services/galleries.service'; import { GalleriesService } from '~/services/galleries.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class GalleriesController { export class GalleriesController {
constructor(private readonly galleriesService: GalleriesService) {} constructor(private readonly galleriesService: GalleriesService) {}

3
packages/nocodb/src/controllers/grid-columns.controller.ts

@ -3,9 +3,10 @@ import { GridColumnReqType } from 'nocodb-sdk';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { GridColumnsService } from '~/services/grid-columns.service'; import { GridColumnsService } from '~/services/grid-columns.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class GridColumnsController { export class GridColumnsController {
constructor(private readonly gridColumnsService: GridColumnsService) {} constructor(private readonly gridColumnsService: GridColumnsService) {}

3
packages/nocodb/src/controllers/grids.controller.ts

@ -12,9 +12,10 @@ import { ViewCreateReqType } from 'nocodb-sdk';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { GridsService } from '~/services/grids.service'; import { GridsService } from '~/services/grids.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class GridsController { export class GridsController {
constructor(private readonly gridsService: GridsService) {} constructor(private readonly gridsService: GridsService) {}

3
packages/nocodb/src/controllers/hooks.controller.ts

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

0
packages/nocodb/src/controllers/imports/helpers/job.ts

0
packages/nocodb/src/controllers/imports/import.controller.ts

3
packages/nocodb/src/controllers/kanbans.controller.ts

@ -13,9 +13,10 @@ import { ViewCreateReqType } from 'nocodb-sdk';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { KanbansService } from '~/services/kanbans.service'; import { KanbansService } from '~/services/kanbans.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class KanbansController { export class KanbansController {
constructor(private readonly kanbansService: KanbansService) {} constructor(private readonly kanbansService: KanbansService) {}

3
packages/nocodb/src/controllers/maps.controller.ts

@ -13,9 +13,10 @@ import { MapUpdateReqType, ViewCreateReqType } from 'nocodb-sdk';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { MapsService } from '~/services/maps.service'; import { MapsService } from '~/services/maps.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class MapsController { export class MapsController {
constructor(private readonly mapsService: MapsService) {} constructor(private readonly mapsService: MapsService) {}

3
packages/nocodb/src/controllers/meta-diffs.controller.ts

@ -2,9 +2,10 @@ import { Controller, Get, Param, UseGuards } from '@nestjs/common';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { MetaDiffsService } from '~/services/meta-diffs.service'; import { MetaDiffsService } from '~/services/meta-diffs.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class MetaDiffsController { export class MetaDiffsController {
constructor(private readonly metaDiffsService: MetaDiffsService) {} constructor(private readonly metaDiffsService: MetaDiffsService) {}

3
packages/nocodb/src/controllers/model-visibilities.controller.ts

@ -11,9 +11,10 @@ import {
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { ModelVisibilitiesService } from '~/services/model-visibilities.service'; import { ModelVisibilitiesService } from '~/services/model-visibilities.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class ModelVisibilitiesController { export class ModelVisibilitiesController {
constructor( constructor(
private readonly modelVisibilitiesService: ModelVisibilitiesService, private readonly modelVisibilitiesService: ModelVisibilitiesService,

3
packages/nocodb/src/controllers/notifications.controller.ts

@ -13,9 +13,10 @@ import {
import { NotificationsService } from '~/services/notifications.service'; import { NotificationsService } from '~/services/notifications.service';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { extractProps } from '~/helpers/extractProps'; import { extractProps } from '~/helpers/extractProps';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class NotificationsController { export class NotificationsController {
constructor(private readonly notificationsService: NotificationsService) {} constructor(private readonly notificationsService: NotificationsService) {}

3
packages/nocodb/src/controllers/old-datas/old-datas.controller.ts

@ -14,9 +14,10 @@ import {
import { OldDatasService } from './old-datas.service'; import { OldDatasService } from './old-datas.service';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { DataApiLimiterGuard } from '~/guards/data-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(DataApiLimiterGuard, GlobalGuard)
export class OldDatasController { export class OldDatasController {
constructor(private readonly oldDatasService: OldDatasService) {} constructor(private readonly oldDatasService: OldDatasService) {}

3
packages/nocodb/src/controllers/org-lcense.controller.ts

@ -10,9 +10,10 @@ import { OrgUserRoles } from 'nocodb-sdk';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { OrgLcenseService } from '~/services/org-lcense.service'; import { OrgLcenseService } from '~/services/org-lcense.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class OrgLcenseController { export class OrgLcenseController {
constructor(private readonly orgLcenseService: OrgLcenseService) {} constructor(private readonly orgLcenseService: OrgLcenseService) {}

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

@ -15,8 +15,9 @@ import { getConditionalHandler } from '~/helpers/getHandler';
import { OrgTokensEeService } from '~/services/org-tokens-ee.service'; import { OrgTokensEeService } from '~/services/org-tokens-ee.service';
import { OrgTokensService } from '~/services/org-tokens.service'; import { OrgTokensService } from '~/services/org-tokens.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@UseGuards(AuthGuard('jwt')) @UseGuards(MetaApiLimiterGuard, AuthGuard('jwt'))
@Controller() @Controller()
export class OrgTokensController { export class OrgTokensController {
constructor( constructor(

3
packages/nocodb/src/controllers/org-users.controller.ts

@ -16,9 +16,10 @@ import { PagedResponseImpl } from '~/helpers/PagedResponse';
import { OrgUsersService } from '~/services/org-users.service'; import { OrgUsersService } from '~/services/org-users.service';
import { User } from '~/models'; import { User } from '~/models';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class OrgUsersController { export class OrgUsersController {
constructor(private readonly orgUsersService: OrgUsersService) {} constructor(private readonly orgUsersService: OrgUsersService) {}

3
packages/nocodb/src/controllers/plugins.controller.ts

@ -12,6 +12,7 @@ import { GlobalGuard } from '~/guards/global/global.guard';
import { PagedResponseImpl } from '~/helpers/PagedResponse'; import { PagedResponseImpl } from '~/helpers/PagedResponse';
import { PluginsService } from '~/services/plugins.service'; import { PluginsService } from '~/services/plugins.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
// todo: move to a interceptor // todo: move to a interceptor
// const blockInCloudMw = (_req, res, next) => { // const blockInCloudMw = (_req, res, next) => {
@ -21,7 +22,7 @@ import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
// }; // };
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class PluginsController { export class PluginsController {
constructor(private readonly pluginsService: PluginsService) {} constructor(private readonly pluginsService: PluginsService) {}

11
packages/nocodb/src/controllers/public-datas-export.controller.ts

@ -1,4 +1,11 @@
import { Controller, Get, Param, Request, Response } from '@nestjs/common'; import {
Controller,
Get,
Param,
Request,
Response,
UseGuards,
} from '@nestjs/common';
import { ErrorMessages, isSystemColumn, ViewTypes } from 'nocodb-sdk'; import { ErrorMessages, isSystemColumn, ViewTypes } from 'nocodb-sdk';
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { nocoExecute } from 'nc-help'; import { nocoExecute } from 'nc-help';
@ -9,7 +16,9 @@ import { serializeCellValue } from '~/modules/datas/helpers';
import { PublicDatasExportService } from '~/services/public-datas-export.service'; import { PublicDatasExportService } from '~/services/public-datas-export.service';
import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2'; import NcConnectionMgrv2 from '~/utils/common/NcConnectionMgrv2';
import { Column, Model, Source, View } from '~/models'; import { Column, Model, Source, View } from '~/models';
import { PublicApiLimiterGuard } from '~/guards/public-api-limiter.guard';
@UseGuards(PublicApiLimiterGuard)
@Controller() @Controller()
export class PublicDatasExportController { export class PublicDatasExportController {
constructor( constructor(

3
packages/nocodb/src/controllers/public-datas.controller.ts

@ -5,11 +5,14 @@ import {
Param, Param,
Post, Post,
Request, Request,
UseGuards,
UseInterceptors, UseInterceptors,
} from '@nestjs/common'; } from '@nestjs/common';
import { AnyFilesInterceptor } from '@nestjs/platform-express'; import { AnyFilesInterceptor } from '@nestjs/platform-express';
import { PublicDatasService } from '~/services/public-datas.service'; import { PublicDatasService } from '~/services/public-datas.service';
import { PublicApiLimiterGuard } from '~/guards/public-api-limiter.guard';
@UseGuards(PublicApiLimiterGuard)
@Controller() @Controller()
export class PublicDatasController { export class PublicDatasController {
constructor(private readonly publicDatasService: PublicDatasService) {} constructor(private readonly publicDatasService: PublicDatasService) {}

4
packages/nocodb/src/controllers/public-metas.controller.ts

@ -1,6 +1,8 @@
import { Controller, Get, Param, Request } from '@nestjs/common'; import { Controller, Get, Param, Request, UseGuards } from '@nestjs/common';
import { PublicMetasService } from '~/services/public-metas.service'; import { PublicMetasService } from '~/services/public-metas.service';
import { PublicApiLimiterGuard } from '~/guards/public-api-limiter.guard';
@UseGuards(PublicApiLimiterGuard)
@Controller() @Controller()
export class PublicMetasController { export class PublicMetasController {
constructor(private readonly publicMetasService: PublicMetasService) {} constructor(private readonly publicMetasService: PublicMetasService) {}

3
packages/nocodb/src/controllers/shared-bases.controller.ts

@ -13,9 +13,10 @@ import {
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { SharedBasesService } from '~/services/shared-bases.service'; import { SharedBasesService } from '~/services/shared-bases.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class SharedBasesController { export class SharedBasesController {
constructor(private readonly sharedBasesService: SharedBasesService) {} constructor(private readonly sharedBasesService: SharedBasesService) {}

3
packages/nocodb/src/controllers/sorts.controller.ts

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

3
packages/nocodb/src/controllers/sources.controller.ts

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

3
packages/nocodb/src/controllers/sql-views.controller.ts

@ -9,9 +9,10 @@ import {
import { SqlViewsService } from '~/services/sql-views.service'; import { SqlViewsService } from '~/services/sql-views.service';
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class SqlViewsController { export class SqlViewsController {
constructor(private readonly sqlViewsService: SqlViewsService) {} constructor(private readonly sqlViewsService: SqlViewsService) {}

3
packages/nocodb/src/controllers/sync.controller.ts

@ -13,9 +13,10 @@ import {
import { GlobalGuard } from '~/guards/global/global.guard'; import { GlobalGuard } from '~/guards/global/global.guard';
import { SyncService } from '~/services/sync.service'; import { SyncService } from '~/services/sync.service';
import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware'; import { Acl } from '~/middlewares/extract-ids/extract-ids.middleware';
import { MetaApiLimiterGuard } from '~/guards/meta-api-limiter.guard';
@Controller() @Controller()
@UseGuards(GlobalGuard) @UseGuards(MetaApiLimiterGuard, GlobalGuard)
export class SyncController { export class SyncController {
constructor(private readonly syncService: SyncService) {} constructor(private readonly syncService: SyncService) {}

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

Loading…
Cancel
Save